Merge remote-tracking branch 'aosp/upstream-main' into HEAD

Change-Id: Iebf400937d2010e8cc6efcece2105209cf78032a
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..782848c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,69 @@
+# See also https://docs.github.com/en/actions/learn-github-actions/expressions
+# See also https://github.com/marketplace/actions/setup-android-ndk
+
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        build:
+        - android
+        - linux-gcc
+        - linux-clang
+        - linux-x86-gcc
+        - linux-powerpc64-gcc
+        - linux-mingw64-gcc
+        - macos
+        include:
+        - build: android
+          cc: clang
+          host: aarch64-linux-android32
+        - build: linux-gcc
+          cc: gcc
+        - build: linux-clang
+          cc: clang
+        - build: linux-x86-gcc
+          cc: gcc
+          arch: x86
+        - build: linux-powerpc64-gcc
+          cc: gcc
+          host: powerpc64-linux-gnu
+        - build: linux-mingw64-gcc
+          cc: gcc
+          host: x86_64-w64-mingw32
+        - build: macos
+          cc: clang
+          os: macos-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Install Android NDK
+        run: |
+          if [ ${{matrix.build}} = android ]; then \
+              wget --quiet https://dl.google.com/android/repository/android-ndk-r24-linux.zip; \
+              unzip -q android-ndk-r24-linux.zip;  \
+          fi
+      - name: Install Ubuntu packages
+        run: |
+          sudo apt-get -q update
+          case "${{matrix.host}}" in                                        \
+            x86_64-w64-mingw32)                                             \
+              sudo apt-get -q install -y binutils-mingw-w64 gcc-mingw-w64;; \
+            powerpc64-linux-gnu)                                            \
+              sudo apt-get -q install -y binutils-powerpc64-linux-gnu       \
+              gcc-powerpc64-linux-gnu;;                                     \
+          esac
+      - name: Build
+        run: |
+          echo "HOST=${{matrix.host}}"
+          NDK=$PWD/android-ndk-r24/toolchains/llvm/prebuilt/linux-x86_64/bin
+          export PATH="$NDK:$PATH"
+          ./autogen.sh
+          ./configure --host=${{matrix.host}} \
+              CC=${{ matrix.host && format('{0}-{1}', matrix.host, matrix.cc) || matrix.cc }} \
+              CFLAGS="-Wall -Wextra -Werror -Wno-sign-compare -Wno-unused-function -Wno-unused-parameter ${{matrix.cflags}}"
+          make -j$(nproc)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8ed670d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,109 @@
+# Please keep the entries in this file sorted with the following vi command:
+# :3,$!LC_ALL=C sort -fu
+
+*.exe
+*.la
+*.lo
+*.o
+*~
+.deps/
+.libs/
+/aclocal.m4
+/ar-lib
+/autom4te.cache/
+/compile
+/config.guess
+/config.h
+/config.h.in
+/config.log
+/config.status
+/config.sub
+/configure
+/depcomp
+/doc/Makefile
+/doc/sg_scan.8
+/include/Makefile
+/install-sh
+/lib/Makefile
+/libtool
+/ltmain.sh
+/Makefile
+/missing
+/scripts/Makefile
+/sg3_utils-*.tar.gz
+/src/Makefile
+/src/sginfo
+/src/sgm_dd
+/src/sgp_dd
+/src/sg_bg_ctl
+/src/sg_compare_and_write
+/src/sg_copy_results
+/src/sg_dd
+/src/sg_decode_sense
+/src/sg_emc_trespass
+/src/sg_format
+/src/sg_get_config
+/src/sg_get_elem_status
+/src/sg_get_lba_status
+/src/sg_ident
+/src/sg_inq
+/src/sg_logs
+/src/sg_luns
+/src/sg_map
+/src/sg_map26
+/src/sg_modes
+/src/sg_opcodes
+/src/sg_persist
+/src/sg_prevent
+/src/sg_raw
+/src/sg_rbuf
+/src/sg_rdac
+/src/sg_read
+/src/sg_readcap
+/src/sg_read_attr
+/src/sg_read_block_limits
+/src/sg_read_buffer
+/src/sg_read_long
+/src/sg_reassign
+/src/sg_referrals
+/src/sg_rem_rest_elem
+/src/sg_rep_density
+/src/sg_rep_pip
+/src/sg_rep_zones
+/src/sg_requests
+/src/sg_reset
+/src/sg_reset_wp
+/src/sg_rmsn
+/src/sg_rtpg
+/src/sg_safte
+/src/sg_sanitize
+/src/sg_sat_identify
+/src/sg_sat_phy_event
+/src/sg_sat_read_gplog
+/src/sg_sat_set_features
+/src/sg_scan
+/src/sg_seek
+/src/sg_senddiag
+/src/sg_ses
+/src/sg_ses_microcode
+/src/sg_start
+/src/sg_stpg
+/src/sg_stream_ctl
+/src/sg_sync
+/src/sg_test_rwbuf
+/src/sg_timestamp
+/src/sg_turs
+/src/sg_unmap
+/src/sg_verify
+/src/sg_vpd
+/src/sg_write_buffer
+/src/sg_write_long
+/src/sg_write_same
+/src/sg_write_verify
+/src/sg_write_x
+/src/sg_wr_mode
+/src/sg_xcopy
+/src/sg_zone
+/src/sg_z_act_query
+/stamp-h1
+Makefile.in
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..f72f863
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,3 @@
+Douglas Gilbert <dgilbert at interlog dot com>
+
+See the CREDITS file for the names of those who have contributed.
diff --git a/BSD_LICENSE b/BSD_LICENSE
new file mode 100644
index 0000000..426cf50
--- /dev/null
+++ b/BSD_LICENSE
@@ -0,0 +1,27 @@
+
+Copyright (c) 1999-2022, Douglas Gilbert
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+Above is the:
+SPDX-License-Identifier: BSD-2-Clause
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..c584dc5
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,32 @@
+
+Upstream Authors: Douglas Gilbert <dgilbert at interlog dot com>,
+                  Bruce Allen  <ballen at gravity dot phys dot uwm dot edu>,
+                  Peter Allworth <linsol at zeta dot org dot au>,
+                  James Bottomley  <jejb at parisc-linux dot org>,
+                  Lars Marowsky-Bree <lmb at suse dot de>,
+                  Kurt Garloff,
+                  Grant Grundler  <grundler at parisc-linux dot org>,
+                  Christophe Varoqui <christophe dot varoqui at free dot fr>,
+                  Michael Weller <eowmob at exp-math dot uni-essen dot de>,
+                  Eric Youngdale <eric at andante dot org>
+
+Copyright:
+
+This software is copyright(c) 1994-2021 by the authors
+
+Most of the code in this package is covered by a BSD license.
+On Debian systems, the complete text of the BSD License
+can be found in `/usr/share/common-licenses/BSD'. All the code
+in the library (usually called libsgutils) is covered by a
+BSD license.
+
+Some of the older utilities are covered by the GPL. More precisely:
+You are free to distribute this software under the terms of the
+GNU General Public License either version 2, or (at your option)
+any later version. On Debian systems, the complete text of the GNU
+General Public License can be found in /usr/share/common-licenses/GPL-2
+file. The later GPL-3 is found in /usr/share/common-licenses/GPL-3
+file but no code in this package refers to that license.
+
+Douglas Gilbert
+4th October 2021
diff --git a/COVERAGE b/COVERAGE
new file mode 100644
index 0000000..09c9010
--- /dev/null
+++ b/COVERAGE
@@ -0,0 +1,188 @@
+                        Command coverage
+                        ================
+The following table lists SCSI commands in alphabetical order on the
+left and the sg3_utils (or related) utilities that implement invocations
+of them on the right. The second table lists supported ATA commands. The
+third table list supported NVMe commands.
+
+SCSI command        sg3_utils utilities that use this SCSI command
+------------        -------------------------------------------------
+ATA COMMAND PASS-THROUGH(12)  sg_sat_identify, ++
+ATA COMMAND PASS-THROUGH(16)  sg_sat_identify, sg_sat_set_features,
+                    sg_sat_phy_event, sg_sat_read_gplog ++
+                    [sg_sat_chk_power, sg__sat_identify,
+                     sg__sat_set_features, sg_sat_smart_rd_data
+                     (previous four in the examples directory)]
+ATA COMMAND PASS-THROUGH(32)  sg_sat_identify, ++
+BACKGROUND CONTROL  sg_bg_ctl
+CLOSE ZONE          sg_zone
+COMPARE AND WRITE   sg_compare_and_write
+COPY OPERATION ABORT    ddptctl, ++
+EXTENDED COPY(LID1)    sg_xcopy, ddpt, ++
+GET CONFIGURATION   sg_get_config, ++
+GET LBA STATUS      sg_get_lba_status, ++
+GET PHYSICAL ELEMENT STATUS      sg_get_elem_status, ++
+GET STREAM STATUS   sg_stream_ctl
+INQUIRY             sg_dd, sg_format, sg_inq, sginfo,
+                    sg_logs, sg_map('-i'), sg_modes, sg_opcodes,
+                    sg_persist, sg_scan, sg_ses, sg_vpd ++
+FINISH ZONE         sg_zone
+FORMAT MEDIUM       sg_format, ++ [SSC]
+FORMAT UNIT         sg_format, ++ [SBC]
+FORMAT WITH PRESET    sg_format, ++ [SBC]
+LOG SELECT          sg_logs('-r' or '-select'), ++
+LOG SENSE           sg_logs, ++
+MODE SELECT(6)      sdparm, sg_wr_mode, sginfo, sg_format,
+                    sg_emc_trespass, sg_rdac, ++
+MODE SELECT(10)     sdparm, sg_wr_mode, sginfo, sg_format,
+                    sg_emc_trespass, sg_rdac, ++
+MODE SENSE(6)       sdparm, sg_modes, sg_wr_mode, sginfo, sg_format,
+                    sg_senddiag('-e'), sg_rdac, ++
+MODE SENSE(10)      sdparm, sg_modes, sg_wr_mode, sginfo, sg_format,
+                    sg_senddiag('-e'), sg_rdac, ++
+OPEN ZONE           sg_zone
+ORWRITE(16)         sg_write_x
+ORWRITE(32)         sg_write_x
+PERSISTENT RESERVE IN       sg_persist, ++
+PERSISTENT RESERVE OUT      sg_persist, ++
+POPULATE TOKEN      ddpt, ddptctl, ++
+PRE-FETCH(10)       sg_seek
+PRE-FETCH(16)       sg_seek
+PREVENT ALLOW MEDIUM REMOVAL        sg_prevent, ++
+READ(6)             sg_dd, sgm_dd, sgp_dd, sg_read
+READ(10)            sg_dd, sgm_dd, sgp_dd, sg_read
+READ(12)            sg_dd, sgm_dd, sgp_dd, sg_read
+READ(16)            sg_dd, sgm_dd, sgp_dd, sg_read
+READ ATTRIBUTE      sg_read_attr
+READ BLOCK LIMITS   sg_read_block_limits, ++
+READ BUFFER(10)     sg_rbuf, sg_test_rwbuf, sg_read_buffer, sg_safte, ++
+READ BUFFER(16)     sg_read_buffer
+READ CAPACITY(10)   sg_readcap, sg_dd, sgm_dd, sgp_dd, sg_format, ++
+READ CAPACITY(16)   sg_readcap, sg_dd, sgm_dd, sgp_dd, sg_format, ++
+READ DEFECT(10)     sginfo('-d' or '-G'), sg_reassign('-g'), smartmontools, ++
+READ DEFECT(12)     sginfo('-d' or '-G'), smartmontools
+READ LONG(10)       sg_read_long, sg_dd, ++
+READ LONG(16)       sg_read_long, ++
+READ MEDIA SERIAL NUMBER     sg_rmsn, ++
+REASSIGN BLOCKS     sg_reassign, ++
+RECEIVE COPY DATA(LID1)    sg_copy_results, ++
+RECEIVE COPY FAILURE DETAILS(LID1)    sg_copy_results, ++
+RECEIVE COPY OPERATING PARAMETERS    ddpt, sg_copy_results, sg_xcopy, ++
+RECEIVE COPY STATUS(LID1)    sg_copy_results, ++
+RECEIVE DIAGNOSTIC RESULTS  sg_senddiag, sg_ses, sg_ses_microcode ++
+RECEIVE ROD TOKEN INFORMATION    ddpt, ddptctl ++
+REMOVE ELEMENT AND MODIFY ZONES   sg_zone
+REMOVE ELEMENT AND TRUNCATE       sg_rem_rest_elem
+REPORT ALL ROD TOKENS    ddptctl ++
+REPORT DENSITY SUPPORT    sg_rep_density
+REPORT IDENTIFYING INFORMATION  sg_ident, ++ (2)
+REPORT LUNS         sg_luns, ++
+REPORT PROVISIONING INITIALIZATION PATTERN    sg_rep_pip, ++
+REPORT REALMS       sg_rep_zones
+REPORT REFERRALS    sg_referrals, ++
+REPORT SUPPORTED OPERATION CODES              sg_opcodes
+REPORT SUPPORTED TASK MANAGEMENT FUNCTIONS    sg_opcodes
+REPORT TARGET PORT GROUPS       sg_rtpg, sg_stpg ++
+REPORT TIMESTAMP    sg_timestamp
+REPORT ZONES        sg_rep_zones
+REPORT ZONE DOMAINS  sg_rep_zones
+REQUEST SENSE       sg_requests, ++
+RESET WRITE POINTER sg_reset_wp
+RESTORE ELEMENTS AND REBUILD    sg_rem_rest_elem
+SANITIZE            sg_sanitize
+SEEK(10)            sg_seek ++
+SEND DIAGNOSTIC     sg_senddiag, sg_ses, sg_ses_microcode ++
+SEQUENTIALIZE ZONE  sg_zone
+SET IDENTIFYING INFORMATION  sg_ident, ++ (3)
+SET TARGET PORT GROUPS       sg_stpg, ++
+SET TIMESTAMP       sg_timestamp
+START STOP          sg_start, ++
+STREAM CONTROL      sg_stream_ctl
+SYNCHRONIZE CACHE(10)   sg_sync, sg_dd, sgm_dd, sgp_dd, ++
+SYNCHRONIZE CACHE(16)   sg_sync++
+TEST UNIT READY     sg_turs, sg_format, ++
+UNMAP               sg_unmap, ++
+VERIFY(10)          sg_verify, ++
+VERIFY(16)          sg_verify, ++
+WRITE(6)            sg_dd, sgm_dd, sgp_dd
+WRITE(10)           sg_dd, sgm_dd, sgp_dd
+WRITE(12)           sg_dd, sgm_dd, sgp_dd
+WRITE(16)           sg_dd, sgm_dd, sgp_dd, sg_write_x
+WRITE(32)           sg_write_x
+WRITE AND VERIFY(10)      sg_write_verify
+WRITE AND VERIFY(16)      sg_write_verify
+WRITE ATOMIC(16)    ddpt, sg_write_x
+WRITE ATOMIC(32)    sg_write_x
+WRITE BUFFER        sg_test_rwbuf, sg_write_buffer, ++
+WRITE LONG(10)      sg_write_long, ++
+WRITE LONG(16)      sg_write_long, ++
+WRITE SAME(10)      sg_write_same
+WRITE SAME(16)      sg_write_same, sg_write_x
+WRITE SAME(32)      sg_write_same, sg_write_x
+WRITE SCATTERED(16)    sg_write_x
+WRITE SCATTERED(32)    sg_write_x
+WRITE STREAM(16)    sg_write_x
+WRITE STREAM(32)    sg_write_x
+WRITE USING TOKEN   ddpt, ddptctl, ++
+ZONE ACTIVATE       sg_z_act_query
+ZONE QUERY          sg_z_act_query
+<most commands>     sg_raw
+
+
+ATA command         sg3_utils utilities that use this (S)ATA command
+-----------         ------------------------------------------------
+CHECK POWER MODE    examples/sg_sat_chk_power
+IDENTIFY DEVICE     sg_inq, sg_scan, sg_sat_identify,
+                    examples/sg__sat_identify
+IDENTIFY PACKET DEVICE     sg_inq, sg_sat_identify,
+                    examples/sg__sat_identify
+READ LOG EXT        sg_sat_phy_event, examples/sg__sat_phy_event
+                    sg_sat_read_gplog
+READ LOG DMA EXT    sg_sat_read_gplog
+SET FEATURES        sg_sat_set_features
+                    examples/sg__sat_set_features
+SMART READ DATA     examples/sg_sat_smart_rd_data
+
+
+NVMe command        sg3_utils utilities that use this NVMe command
+------------        ------------------------------------------------
+Identify            sg_inq
+SES Read            sg_senddiag, sg_ses (NVME-MI command)
+SES Write           sg_senddiag, sg_ses (NVME-MI command)
+Device self-test    [SNTL of SEND DIAGNOSTIC] sg_senddiag
+Get features(power management) [SNTL of REQUEST SENSE] sg_requests
+Read                [SCSI READ(10) -->SNTL--> Read]
+                    [SCSI READ(16) -->SNTL--> Read]
+Write               [SCSI WRITE(10) -->SNTL--> Write]
+                    [SCSI WRITE(16) -->SNTL--> Write]
+Compare             [SCSI VERIFY(10,BYTCHK=1) -->SNTL--> Compare]
+                    [SCSI VERIFY(16,BYTCHK=1) -->SNTL--> Compare]
+Write zeroes        [SCSI WRITE SAME(10,zeros) -->SNTL--> Write zeroes]
+                    [SCSI WRITE SAME(16,zeros) -->SNTL--> Write zeroes]
+Flush               [SCSI SYNCHRONIZE CACHE -->SNTL--> Flush]
+Set Features        [SCSI MODE SELECT(10) -->SNTL--> Set Features]
+                       only for WCE in Caching page
+
+The following SCSI commands do nothing (currently) in the SNTL but
+do return GOOD status: TEST UNIT READY, START STOP UNIT, REPORT LUNS
+and REQUEST SENSE. READ CAPACITY(10 and 16) yield appropriate data
+by examining the response to the NVMe Identify command.
+
+
+++  command wrapper found in sg_cmds_basic.c, sg_cmds_mmc.c  or
+    sg_cmds_extra.c for this command
+(2) this command was known as REPORT DEVICE IDENTIFIER prior to spc4r07
+(3) this command was known as SET DEVICE IDENTIFIER prior to spc4r07
+
+Note that any SCSI command, including bi-directional and variable length
+commands (whose cdb size is > 16 bytes) can be issued by the sg_raw utility.
+
+The RECEIVE COPY * commands in SPC-4 were grouped as one command name
+with 4 service actions in SPC-3 and earlier. The single SPC-3 command
+name is RECEIVE COPY RESULTS. The two opcodes associated with all
+EXTENDED COPY commands are now known as THIRD PARTY COPY IN (0x84) and
+THIRD PARTY COPY IN (0x83).
+
+
+Douglas Gilbert
+10 June 2022
diff --git a/CREDITS b/CREDITS
new file mode 100644
index 0000000..1b1de83
--- /dev/null
+++ b/CREDITS
@@ -0,0 +1,163 @@
+The author of sg3_utils would like to thank the following people who
+have made contributions:
+
+
+Andries Brouwer <aebr at win dot tue dot nl> rewrite of isosize (original
+        written by Joerg Schilling). isosize is now found in the util-linux
+        package and in the archive directory of this package.
+
+Bart Van Assche <bart dot vanassche at sandisk dot com>
+        harden (improve) code in rescan-scsi-bus.sh [20160224]
+        configure.ac and Makefile.am cleanup plus sgp_dd code
+        to replace pthread_cancel with pthread_kill [20180102]
+        sg_xcopy: fix identification CSCD descriptor's designator
+        length, fix CSCD descriptor mask [20210902]
+
+Bean Huo <beanhuo dot micron dot com>
+        sg_write_buffer: patch to allow comma or period separated bytes
+        (in decimal or hex) to be decoded when given as standard input.
+
+Brian Bunker <Brian dot Bunker at netapp dot com> contributed
+        sg_read_block_limits and the target reset addition to sg_reset
+        [20090615]
+
+Christophe Varoqui <christophe dot varoqui at free dot fr> original sg_rtpg
+        [20041229]
+
+Clayton Weaver <cgweav at email dot com> contributed safe_strerror().
+
+Dan Horak <dhorak at redhat dot com> website support for this package and
+        others. Lot of fixes, recently man pages [20140128]
+
+Dave Johnson <ddj at ccv dot brown dot edu> improved disk defect list
+        handling [20051218]
+
+Dave Williams <dave at opensourcesolutions dot co dot uk> help with
+        sgp_dd especially and "> 0x7fffffff" with sg*_dd [20060303]
+
+Eric Schwartz <emschwar at debian dot org> who wrote these man pages:
+        sg_readcap, sg_reset, sg_scan, sg_start, sg_test_rwbuf,
+        sg_turs and sginfo
+
+Eric Seppanen <eric @ purestorage dot com> borrowed ideas from alternate
+        implementation of sg_compare_and_write [20130823]
+
+Eric Youngdale <eric at andante dot org> author of scsi_info on which sginfo
+        is based.
+
+Fabrice Fontaine <fontaine dot fabrice at gmail dot com>
+        various build fixes [20211116]
+
+Frank Jansen <fjansen at egenera dot com>: additions to sg_scan; contributed
+        code for '--alloc-length=' option in sg_persist [20090402]
+
+Grant Grundler <grundler at parisc-linux dot org> co-author of blk512-linux
+        that has become sg_format [20050201]
+
+Greg Inozemtsev <greg at purestorage dot com>
+        extensions to sg_xcopy [20130207+20130816]
+
+Hannes Reinecke <hare at suse dot de>
+        contributed sg_rdac, (and some corresponding VPD entries to
+        sg_vpd_vendor), sg_stpg and sg_safte [20071013+20130110]
+        sg_referrals [20100906]
+        sg_inq --export option [20120220+20130109]
+        sg_xcopy+sg_copy_results [20120322]
+        rescan-scsi-bus.sh patches to Kurt Garloff's v1.57 [20130715]
+        55-scsi-sg3_id.rules + 58-scsi-sg3_symlink.rules [20140527]
+        sg_sat_read_gplog [20141107]
+        sg_inq --only option plus --inhex fixes [20180102]
+
+Hayashi Naoyuki <titan at culzean dot org>
+        port to Tru64 [20060127]
+
+Heiko Eissfeldt <heiko at colossus dot escape dot de> sg based example
+        programs for the original sg driver
+
+Ilan Steinberg <ilan dot steinberg at kaminario dot com>
+        sg_xcopy: contributed --on_src and --on_dst options [20130505]
+
+Ingo van Lil <inguin at gmx dot de>
+        contributed sg_raw [20070331]
+
+James Bottomley <jejb at parisc-linux dot org> co-author of blk512-linux
+        that has become sg_format [20050201]
+
+Jan Engelhardt <jengelh at inai dot de>
+        autotools clean-up [20150216]
+
+Joe Krahn <krahn at niehs dot nih dot gov> help with int64_t cleanup
+        [20071219]
+
+Kai Makisara <Kai dot Makisara at kolumbus dot fi> help with tape
+        minor numbers in lk 2.6 plus earlier advice [20081008]
+
+Kurt Garloff: original sg_start and sg_test_rwbuf.
+        Additions to sginfo and sg_map. Author of rescan-scsi-bus.sh with
+        latest update to v1.57 [20130331]
+
+Lars Marowsky-Brée <lmb at suse dot de> contributed Unit Path Report VPD
+        page decoding in sg_inq (vendor specific: EMC) and sg_emc_trespass
+        utility
+
+Luben Tuikov <ltuikov at yahoo dot com>
+        help with documentation and other suggestions [20061014]
+        contribution sg_read_buffer and sg_write_buffer [20061103]
+
+Marius Konitzer <marius dot konitzer at ruhr-uni-bochum dot de
+        log pages on IBM LTO Ultrium drives [20100225]
+
+Mark Knibbs <markk at clara dot co dot uk>
+        suggested and tested oflag=sparse for sg_dd
+
+Martin Schwenke <martin at meltin dot net> added the raw switch "-r" to sg_inq
+
+Martin Wilck <mwilck at suse dot com> contributed script files [20190425 and
+        20220218]]
+
+Nate Dailey < Nate dot Dailey at stratus dot com > extended sg_map for sparse
+        disk node names (e.g. /dev/sdaaa) [20050511]
+
+Nitin U. Yewale < nyewale at redhat dot com> sent patch via github:
+        https://github.com/doug-gilbert/sg3_utils/pull/10/ to fix crash with
+        rescan-scsi-bus.sh -r due to rev 867 change to sg_inq [20220103]
+
+Pat LaVarre <p.lavarre at ieee dot org> pointed out danger of negative bpt
+        values in sg_dd (and friends); also problems when reading /dev/null
+
+Peter Allworth <linsol at zeta dot org dot au> original dd clone design used
+        by sg3_utils's dd variants (e.g. sg_dd).
+
+Roland Dreier <roland at purestorage dot com>
+        extension and correction to sg_xcopy [20120205]
+
+Ronnie Sahlberg <ronniesahlberg at gmail dot com> has written libiscsi and a
+        set of external patches to add direct iSCSI support to this package.
+        See README.iscsi [20110518]
+
+Saeed Bishara contributed sg_write_long
+
+Sean Stewart <Sean dot Stewart at netapp dot com> various improvements
+        to rescan-scsi-bush.sh script [20130827]
+
+Shahar Salzman <shahar dot salzman at kaminario dot com> contributed
+        sg_compare_and_write [20121205]
+
+Thomas Kolbe <tkolbe at partnersdata dot com>
+        Solaris port help and testing [20070503]
+
+Tim Hunt <tim at timhunt dot net> increased number of (sd and sg) devices
+        that sginfo could detect.
+
+Tom Steudten <steudten at gmx dot ch> sginfo addition: add '-Fhead' option
+        to sort defect list by head.
+
+Trent Piepho <xyzzy at speakeasy dot org> print out some "sense key specific"
+        data and "-6" switch for sg_modes
+
+Xose Vazquez Perez <xose dot vazquez at gmail dot com>
+        documentation corrections [20200117]
+
+
+Douglas Gilbert
+18th February 2022
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..1adf83f
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,1919 @@
+Each utility has its own version number, date of last change and
+some description at the top of its ".c" file. All utilities in the main
+directory have their own "man" pages. There is also a sg3_utils man page.
+
+Changelog for pre-release sg3_utils-1.48 [20221112] [svn: r983]
+  - some utilities: add experimental --json[=JO] option
+  - sg_z_act_query: new utility for sending either a
+    Zone activate or Zone query command
+  - sg_rep_density: new utility for decoding the response of
+    Report density support command [ssc (tape)]
+  - sg_rem_rest_elem: new utility for removing or restoring
+    elements
+  - sg_rtpg: https://github.com/hreinecke/sg3_utils/pull/79
+    applied
+  - rescan-scsi-bus.sh: with '-r' it crashed due to change in
+    rev 867 in sg_inq.c from Device_type= to PDT= .
+    Change script to use either
+    - undo regression in rev 815 that added newline after
+      each LUN in the default (no option) output
+    - rev 815 changed the order of listing hosts from
+      numeric to alphabetical, change it back to numeric
+    - https://github.com/doug-gilbert/sg3_utils/pull/17
+      applied with tweaks: add timeout parameter
+    - clean $norm handling
+    - fix handling of '-I <secs>' option
+    - sgdevice26: do not traverse sg class if scsi_device
+      is not added
+    - add -no-lip-scan option
+    - https://github.com/doug-gilbert/sg3_utils/pull/21
+    - speed testonline() when peripheral_qualifier != 0
+      see https://github.com/doug-gilbert/sg3_utils/issues/24
+    - speed multipath scans with many LUNs, cache multipath
+      LUN info in temporary file, see
+      https://github.com/doug-gilbert/sg3_utils/issues/22
+  - sg_rep_zones: add Report zone starting LBA granularity
+    field in REPORT ZONES response [zbc2r12]
+    - add --brief option, show part of header and last
+      descriptor fetched
+    - add --find ZT option to find the first occurrence of
+      ZT; if ZT prefixed by - or ! find first not equal to ZT
+    - add --statistics option
+  - sg_get_elem_status: change '--maxlen=' option default to
+    1056 (header plus 32 physical element status descriptors)
+  - sg_decode_sense: add --nodecode option
+  - sg_logs: tweak the meaning of --list option to more closely
+    reflect the contents of log pages 0x0 and 0x0,0xff
+    - make '-lll' set union of log pages 0x0 and 0x0,0xff
+    - add --exclude option to exclude vendor specific pages
+      and parameters
+    - add --undefined option for hex format of undefined/
+      unrecognized fields
+    - for short binary fields, remove address (index) from
+      the left hand side of each line of hex
+    - improve 'last_n' log pages; supply VPD and mode pages
+      with their name (if T10 defined)
+    - update names for TapeAlert lpage
+    - allow --page selection with --inhex=FN
+  - sg_modes: improve handling of zbc disks with pdt=0x14
+  - sg_inq, sg_vpd: merge VPD page processing for T10
+    defined pages, VS pages still differ
+    - Device Identication VPD page, change
+      "IEEE Company_id" to "AOI" as per spc6r06.pdf
+    - add support for Hitachi/HP open-v ldev names
+  - sg_vpd: apply github pull 18 (missing LF)
+    - add --sinq_inraw=RFN option
+  - sg_opcodes: cleanup error reporting
+    - add --inhex=FN to process earlier -HHH
+  - sg_format: allow disk formats on ZBC (zoned) disks
+  - sg_read_buffer: add --eh_code= and --no_output options
+  - sg_ses: add exp_sas_addr acronym for getting expander's
+    SAS address
+  - sg_turs: change nanosleep() to Sleep() in MinGW
+    - see sg_lib below for two new exit status values
+  - sg_stream_ctl: fix --get indexing
+  - sg_read_block_limits: fix granularity value
+     - add --mloi option
+  - inhex/logs_last_n.hex: new, tests for the 4 "Last n ..."
+    log (sub)pages
+  - sg_lib: add sg_pdt_s_eq() to cope with ZBC disks which may
+    be either PDT_ZBC (if host managed) or PDT_DISK 
+    - add hex2fp(), similar to hex2str() but outputs to FILE
+    - cleanup masks for PDT [0x1f] and group_number [0x3f]
+    - new sg_json_builder.[hc] files local to lib folder
+    - document internal json interface in include/sg_pr2serr.h
+    - add SG_C_CPP_ZERO_INIT to better handle aggregate stack
+      instance zeroing (C23 adding 'struct T t {};' will help)
+    - correct fixed format sense data command-specific field
+      for ata-passthrough (lba field), see:
+      https://github.com/doug-gilbert/sg3_utils/pull/25
+    - add sg_ll_read_block_limits_v2() for MLOI bit
+    - https://github.com/doug-gilbert/sg3_utils/pull/32 added
+      with tweaks; adds SG_LIB_CAT_STANDBY and
+      SG_LIB_CAT_UNAVAILABLE which refine the 'not ready'
+      exit status. Useful for sg_turs
+  - initialize all sense buffers to 0
+  - linux: replace references to /proc/scsi/sg with
+    /sys/module/sg/parameters/
+  - rework main README file
+  - rev 921+922 are bugfix revs on release 1.47 [r919,920]
+  - configure.ac: map msys to mingw
+    - repeat tweak to accept uclinux as linux
+  - sg_dd: change uint type to uint32_t
+    - remove unused out2_off in main()
+  - sg_pt_dummy.c: remove problematic include
+  - sg3_utils.spec: change tarball extension from .tgz to
+    .tar.gz ; fix build issue with Fedora 36
+  - build cleanups for 'make distcheck', see
+    https://github.com/doug-gilbert/sg3_utils/pull/26, 27 and
+    28; need svn:ignore property or maybe global-ignores
+  - round of coverity identified issue fixes (and non-issues)
+  - autoconf: upgrade version 2.70 to 2.71; automake upgrade
+    to version 1.16.5 (from Fedora 36)
+  - remove archive directory (and its contents)
+  - codespell fixes
+
+Changelog for released sg3_utils-1.47 [20211110] [svn: r919]
+  - sg_rep_zones: add support for REPORT ZONE DOMAINS and
+    REPORT REALMS in this utility
+  - sg_raw: fix prints of NVMe NVM command names
+  - sg_ses: fix Windows problem "No command (cdb) given"
+    - fix crash when '-m LEN' < 252
+    - guard against smaller '--maxlen=' values
+  - sg_logs: additions to Volume statistics lpage [ssc5r05c]
+    - additions to Command duration limits statistics log
+      page [spc6r06]
+  - sg_vpd: fix do_hex type on some recent pages
+    - zoned block dev char vpd: add zone alignment mode and
+      zone starting LBA granularity [zbc2r11]
+  - sg_read_buffer: fix --length= problem
+  - sg_dd, sgm_dd, sgp_dd: don't close negative file descriptors
+  - sg_dd: srand48_r() and mrand48_r() are GNU libc specific,
+    put conditional in so non-reentrant version used otherwise
+    - 'iflag=00,ff' places the 32 bit block address (big endian)
+      into each block
+  - sgp_dd: major rework, fix issue with error being ignored
+    - new: --chkaddr which checks for block address in each block
+    - add check for stdatomic.h presence in configure.ac
+  - sg_xcopy: tweak CSCD identification descriptor
+  - sg_get_elem_status: fix issue with '--maxlen=' option
+    - add 2 depopulation revocation health attributes [sbc5r01]
+  - transport error handling improved. To fix report of a
+    BAD_TARGET transport error but the utility still continued.
+    - introduce SG_LIB_TRANSPORT_ERROR [35] exit status
+  - several utilities: override '--maxlen=LEN' when LEN
+    is < 16 (or 4), take default (or 4) instead
+  - scripts: 55-scsi-sg3_id.rules remove outdated rule
+  - sg_lib: add sg_scsi_status_is_good(),
+    sg_scsi_status_is_bad() and sg_get_zone_type_str()
+  - pt_linux: fix verify(BytChk=0) which Linux SNTL translated
+    to write, other SNTL cleanups
+  - pt_linux_nvme: fix fua setting
+  - pt: check_pt_file_handle() add return value of 5 for
+    FreeBSD for nvme(cam)
+  - pt: new configure option --enable-pt_dummy builds the
+    library with sg_pt_dummy.c instead of OS specific code;
+    for experimenting with --inhex= decoding on netbsd
+  - pt: add Haiku OS support
+  - gcc -fanalyzer fixes: in sg_pt_linux.c + sg_write_x.c
+  - sg_pt_dummy.c: add list of functions that a new pt
+    needs to define
+  - configure.ac: tweak to accept uclinux as linux
+  - move some hex files from examples to inhex directory
+  - major rework of lib/sg_pt_freebsd.c; make SNTL as similar
+    as practical to the Linux implementation
+  - add testing/sg_take_snap
+  - change links to http://sg.danny/cz/sg/* to https
+
+Changelog for released sg3_utils-1.46 [20210329] [svn: r891]
+  - sg_rep_pip: new utility: report provisioning initialization
+    pattern command
+  - sg_turs: estimated time-to-ready [spc6r03]
+    - add --delay=MS option
+  - sg_requests: substantial cleanup
+  - sg_vpd: add Format presets and Concurrent positioning ranges
+    - add hot-pluggable field in standard Inquiry [spc6r05]
+    - fix vendor struct opts_t alignment
+  - sg_inq: add hot-pluggable field in standard Inquiry
+  - sg_dd: --verify : separate category for miscompare errors
+    - --verify : oflag=coe continue on miscompares, counts them
+    - add cdl= operand for command duration limit indexes
+    - add oflag=nocreat and conv=nocreat : OFILE must exist
+    - add iflag=00, ff, random flags
+    - setup conditional auto rule for getrandom()
+    - add command timeout after comma in time= operand
+  - sg_get_elem_status: add ralwd bit sbc4r20a
+  - sg_write_x: add dld bits to write(32) [sbc4r19a]
+  - sg_rep_zones: print invalid write pointer LBA as -1 rather
+    than 16 "f"s
+  - sg_opcodes: improve handling of RWCDLP field
+  - sg_ses: use fan speed factor field for calculation [ses4r04]
+    - add --all (-a) option, same action as --join
+  - sg_compare_and_write: add examples section to its manpage
+  - sg_modes: document '-s' option (same as '-6')
+  - sg_sanitize + sg_format: when --verbose given once report
+    probable success; without --verbose 'no news is good news'
+  - sg_zone: add Remove element and modify zones command
+  - sg_raw: increase maximum data-in and data-out buffer size
+    from 64 KB to 1 MB
+    - fix --cmdfile= handling
+    - add --nvm option to send commands from the NVM command set
+    - add --cmdset option to bypass cdb heuristic
+    - add --scan= first_opcode,last_opcode
+  - sg_pt_freebsd: allow device names without leading /dev/
+    thus fix for regression introduced in rev 731 (ver: 1.43)
+  - sg_pt_solaris+sg_pt_osf1: fix problem with clear_scsi_pt_obj()
+    which needs to remember is_nvme and dev_fd values
+  - sg_lib: add ZBC (2020) feature set entries
+  - sg_lib: restore elements and rebuild command added
+  - sg_lib,sg_pt: add partial_clear_scsi_pt_obj(),
+    get_scsi_pt_cdb_len() and get_scsi_pt_cdb_buf()
+    - add do_nvm_pt() for the NVM (sub-)command set
+    - tweak transport error handling in Linux
+  - sg_lib: Linux NVMe SNTL: add read, write and verify;
+    synchronize cache and write same translations
+    - add dummy start stop unit and test unit ready commands
+    - wire cache mpage's WCE to nvme 'volatile write cache'
+    - fix crash in sg_f2hex_arr() when fname not found
+  - sg_lib: reprint cdb with illegal request sense key
+    - asc/ascq match asc-num.txt @t10 20200708 [spc6r02]
+  - gcc-10: suppress warnings
+  - autoconf: upgrade version 2.69 to 2.70
+  - remove space from end of source lines for git-svn
+  - testing/sg_mrq_testing: new, for blocking mrq usage
+  - testing/sgs_dd: add evfd flags and eventfd processing
+  - testing: remove master-slave terminology for sgv4
+  - examples: add nvme_read_ctl.hex and nvme_write_ctl.hex
+
+Changelog for released sg3_utils-1.45 [20200229] [svn: r843]
+  - sg_get_elem_status: new utility [sbc4r16]
+  - sg_ses: bug: --page= being overridden when --control and --data= also
+    given; fix
+    - document explicit Element type codes and example
+    - rename 'SAS SlimLine' to SlimSAS [ses4r02]
+    - add --inhex=FN, equivalent to --data=@FN, for compatibility with
+      other utilities
+    - 'fan speed factor' field added in 20-013r1
+  - sg_opcodes: expand MLU (now 2 bits, spc5r20)
+    - include RWCDLP field as extension of CDLP field (spc5r01)
+  - sg_write_buffer: allow comma and period separated lists when input
+    from stdin
+  - sg_inq: update version descriptors to spc5r21
+    - add some NVMe 1.4 snippets to ctl identify
+  - sg_format: add --dcrt used twice (FOV=1 DCRT=0)
+    - add support for FORMAT WITH PRESET (sbc4r18)
+  - sg_raw: fix --send bug when using stdin
+  - sg_vpd: 3pc VPD page add copy group descriptor
+    - add --examine option
+    - new zoned block device char. page (zbc2r04)
+  - sg_read_buffer: decode read microcode status page
+    - add --inhex=FN option
+  - sg_request: add --error option, replaces opcode with 0xff (or skips call
+    to pass-through)
+  - sg_get_lba_status: add --inhex=FN option
+  - sg_xcopy: add --fco (fast copy only) (spc5r20)
+    - implement --app=1 (append) on regular OFILE type
+  - sg_scan (win32): expand limits for big arrays
+  - sg_modes: placeholders for Command duration limit  T2A and T2B mpages
+    (sbc4r17)
+    - improve zbc support (e.g. caching mpage)
+  - sg_logs: add Command duration limits statistics lpage (spc6r01)
+    - zoned block device statistics log page: shorten counter fields from
+      8 to 4 bytes (zbc2r02)
+      - new field in this log page (zbc2r04)
+    - change '-ll' option to suppress subpages=0xff apart from page
+      0x0,0xff. Used three times: list all pages and subpages names
+      reported
+  - sg_reassign: for defect list format 6 (vendor specific) don't try to decode
+  - sg_rep_zones: expand some fields per zbc2r04
+    - add --num= and --wp options
+  - sg_verify: correct so issues VERIFY(16)
+    - add --0 and --ff options and implement bytchk=3 properly
+  - sg_write_same: add --ff for 0xff fill
+  - sg_luns: report new "target commands" w-lun (19-117)
+  - sg_dd: add --verify support
+  - sgp_dd: support memory-mapped IO via mmap flag
+  - inhex directory: new, contains ASCII hex files that can be used with
+    the '--inhex=' option
+  - sg_lib: add sg_t10_uuid_desig2str()
+    - add sg_get_command_str and sg_print_command_len()
+    - speed up sg_print_command()
+    - sg_scsi_normalize_sense(): populate byte4,5,6
+    - tweak sg_pt interface to better handle bidi
+    - sg_cmds_process_resp(): two arguments removed
+    - add ${PACKAGE_VERSION} to '.so' name
+    - add sg_f2hex_arr()
+    - update some tables for NVMe 1.4
+    - sg_get_num()+sg_get_llnum(): add 'e' decoding, exabytes; allow
+      addition (e.g. --count=3+1k)
+    - asc/ascq match asc-num.txt @t10 20191014
+    - new zbc2r04 service actions
+  - sg_pt_freebsd: fixes for FreeBSD 12.0 release
+  - scripts: update 54-before-scsi-sg3_id.rules, scsi-enable-target-scan.sh
+    and 59-fc-wwpn-id.rules
+  - linux: add nanosecond durations when SG3_UTILS_LINUX_NANO environment
+    variable givenand Linux sg driver >= 4.0.30
+  - rescan-scsi-bus: widen LUN 0 only scanning
+    - multiple patches to sync with Suse
+  - testing/sg_tst_async: fix free_list issue
+  - testing/sg_tst_ioctl: for sg 4.0 driver
+  - testing/sg_tst_bidi: for sg 4.0 driver
+  - testing/sgh_dd: test request sharing, mreqs...
+    - add --verify support
+  - testing/sgs_dd: back from archive, for testing SIGPOLL (SIGIO) and
+    realtime (RT) signals
+  - testing/sg_chk_asc: allow LF and CR/LF in asc-num.txt
+  - testing: 'make' now builds both C and C++ programs
+  - sg_pt: add sg_get_opcode_translation() to replace global pointer to
+    array: sg_opcode_info_arr[]
+    - extend small SNTL to support read capacity
+  - utils/hxascdmp: add -o=<offset> option
+    - add -1, -2 and -q options
+  - sg_io_linux (sg_lib): add sg_linux_sense_print()
+  - sg_pt_linux: uses sg v4 interface if sg driver >= 4.0.0 . Force sg v3
+    always by building with './configure --disable-linux-sgv4'
+    - add sg_linux_get_sg_version() function
+  - add: 'SPDX-License-Identifier: BSD-2-Clause' or a small number of
+    'GPL-2.0-or-later'
+  - gcc-9: suppress (pointless) warnings
+  - automake: upgrade to version 1.16.1
+  - autoconf: upgrade to version 2.69
+  - sync with fixes from Redhat, via github
+
+Changelog for sg3_utils-1.44 [20180912] [svn: r791]
+  - same code as release 1.43 20180911 svn rev 789;
+    new release due to sync problem with git mirror at:
+    https://github.com/hreinecke/sg3_utils
+    that has a v1.43 tag dated 20160217 [svn: r663]
+
+Changelog for sg3_utils-1.43 [20180911] [svn: r789]
+  - release 1.43 20180911 svn rev 789
+  - sg_write_x: where x can be normal, atomic, or(write),
+    same, scattered, or stream writes with 16 or 32 byte
+    cdbs (sbc4r04 for atomic, sbc4r11 for scattered)
+  - sg_bg_ctl: new Background control command (sbc4r08)
+  - sg_seek: new SEEK(10) or PRE-FETCH(10 or 16)
+  - sg_stream_ctl: new, STREAM CONTROL or GET STREAM STATUS
+  - sg_senddiag: add --timeout=SECS option
+  - sg_sanitize: add --timeout=SECS option
+    - add --dry-run option
+  - sg_format: add --timeout=SECS option
+    - add --dry-run option to bypass modifying behaviour
+    - add --quick option to skip reconsideration time
+    - extend --wait timeout to 40 hours for disk sizes
+      > 4 TB and 80 hours if > 8 TB
+    - when changing block size allow for Mode Select
+      rejecting SP=1 (Save Page): repeat with SP=0
+    - FFMT tweaks: default CMPLST to false, shorten poll
+    - make all data-in and data-out buffers page aligned
+  - sg_decode sense: add --cdb and --err=ES options
+  - sg_ses: handle 2 bit EIIOE field in aes dpage
+    - increase join array size from 260 to 520 elements
+    - add --quiet option to suppress messages
+    - expand join handling of SAS connectors and others
+    - expand join debug code
+    - allow multiple --clear=, --get= and --set= options
+    - allow individual index ranges (e.g. --index=3-5)
+    - allow --index=IIA with -ee to enumerate only fields
+      belonging to element type IIA
+    - --data=@FN with --status now decodes dpage(s) in FN
+    - add 'offset_temp' and 'rqst_override' to temperature
+      sensor element type
+    - add 'hw_reset' and 'sw_reset' to enclosure services
+      controller electronics element type (18-047r1)
+    - interpret '--join --page=aes' to only display join
+      rows that have a corresponding AES dpage element
+    - support NVMe attached enclosure via NVME-MI Send and
+      Receive SES commands
+    - decode array status diagnostic page (obsolete)
+    - sync to ses4r01
+  - sg_ses_microcode: add --dry-run and -ealsd options
+  - sg_ses, sg_ses_microcode, sg_senddiag: make all access
+    buffer page size aligned (typically page_size=4096)
+  - sg_write_buffer: add --dry-run option
+  - sg_luns: resync with drafts (sam6r02+spc5r10)
+    - remove undocumented test "W" format
+    - accept and output on request "quad dashed" format
+  - sg_logs: fix volume statistics lpage when subpage
+    is zero (ssc5r02a); decode mount history log parameter
+    - add --vendor=VP and '--pdt=DT' options
+    - decode Requested recovery, TapeAlert response, and
+      Service buffer information lpages for tape
+    - add min+max 'mounted' temperature and rel. humidity
+      fields to Environmental reporting lpage (spc5r10)
+    - add last n Inquiry/Mode_page data changed log
+      pages (spc5r17)
+    - add zoned block device statistics lpage (zbc2r?,
+      16-264r4)
+    - fixup enumeration in power condition transition
+      log page (from H. Reinecke, Suse)
+  - sg_inq: fix potential unbounded loop in --export
+    - add --only to stop standard inquiry decoding also
+      doing a serial number vpd page (0x80) fetch
+    - update version descriptor list to 20170114
+    - add further checks so CDROM standard inquiry response
+      doesn't trick --inhex into thinking it's VPD pg 0x80
+    - decode NVMe Identify controller/nsid commands
+    - with NVMe --only restricts to a single Identify
+      controller command
+    - add --long which decodes more of the NVMe Identify
+      command responses
+  - sg_inq+sg_vpd: update Extended inquiry data vpd
+    page (spc5r09 and 17-142r5)
+    - block limits and block limit extension VPD pages:
+      add extra info about corner cases
+    - add maximum inquiry|mode_page change logs fields
+      to extended inquiry vpd page (spc5r17)
+    - both now return EDOM (adjusted sg error code) when
+      requested page not in Supported VPD Pages page
+    - add --force option to bypass checking Supported
+      Vpd Pages page and fetch requested page directly
+  - sg_vpd: 3 party copy VPD page improvements
+    - fully implement Device constituents VPD page
+    - decode some WDC/Hitachi vendor VPD pages
+    - improve handling of unknown pages
+  - sg_reassign+sg_write_same: fix ULONG_MAX problem
+  - sg_rdac: add sanity checks for -f=lun value
+  - sg_turs+sg_requests: make both accept '--num=NUM'
+    and '--number=NUM' for mutual compatibility
+  - sg_turs: add --low option
+    - fix exit status when not ready in single case
+  - sg_zone: fix debug cdb naming
+    - add --sequentialize, --count=ZC options, zbc2r01b
+  - sg_reset_wp add --count=ZC option, zbc2r01b
+  - sg_persist: add --maxlen-LEN option, LEN defaults to
+    decimal, similar to --alloc-length= which takes hex
+    - add Replace lost reservation capable (RLR_C) bit
+      in Report Capabilities (spc4r36)
+  - sg_dd: add --dry-run and --verbose options
+    - allow multiple short options (e.g. -dvv )
+  - sgp_dd: pthread_cancel() has issues in C++ (and
+    the Android multi-threaded library doesn't supply it)
+    so use pthread_kill() in its place [Linux only]
+    - add --dry-run and --verbose options
+  - sgm_dd: add --dry-run and --verbose options
+  - sg_opcode: add '--enumerate' and '--pdt=' options
+    - support CDLP (command duration limit page)
+    - support MLU, Multiple Logical Units (18-045r1)
+    - check resid and trim response if necessary
+    - report when --no-inquiry is ignored
+  - sg_raw: add --enumerate option
+    - add --cmdfile=CF option, permit 64 byte NVMe
+      admin commands to be sent
+    - add --raw option (for CF in binary)
+    - page align input and output buffers
+  - sg_get_lba_status: add --report-type= option (sbc4r12)
+    - add support for 32 byte cdb variant (sbc4r14)
+    - add support for --element-id= and --scan-len=
+      options (sbc4r14)
+    - decode response's RTP and two more provisioning
+      statuses and the additional status (sbc4r12)
+    - decode completion condition (sbc4r14)
+  - sg_modes: add Out of band management control mpage
+    - accept acronym for page/subpage codes
+  - sg_rep_zones: expand --help option information
+  - sg_unmap: add --all=ST,RN[,LA] option to unmap
+    large contiguous segments of a disk/ssd
+    - add --dry-run and --force options
+  - sg_wr_mode: add --rtd option for RTD bit
+  - sg_timestamp: add '--no-timestamp' option
+    - add --elapsed and --hex options
+  - sginfo: don't open /dev/snapshot
+  - introduce SG3_UTILS_DSENSE environment variable
+  - manpages and usage messages: corrections from
+    Gris Ge via github
+  - group_number: is 6 bit field allowing 0 to 63,
+    code in several utilities limited it to 31, fix
+  - convert many two valued 'int's to bool
+  - sg_lib: add SSC maintenance in/out sa names
+    - enhance exit status values and associated
+      strings, add SG_LIB_OS_BASE_ERR (50)
+    - add sg_ll_inquiry_v2(), sg_ll_inquiry_pt() and
+      sg_simple_inquiry_pt()
+    - add sg_ll_report_luns_pt()
+    - add sg_ll_log_sense_v2()
+    - add sg_ll_mode_sense10_v2()
+    - add sg_ll_mode_select6_v2() and
+      sg_ll_mode_select10_v2() for RTD bit
+    - add sg_ll_receive_diag_v2()
+    - add sg_ll_write_buffer_v2()
+    - add sg_get_llnum_nomult()
+    - add sg_ll_get_lba_status16()
+    - add sg_ll_get_lba_status32()
+    - add sg_ll_format_unit_v2()
+    - add sg_ll_test_unit_ready_progress_pt()
+    - add sg_ll_start_stop_unit_pt()
+    - add sg_ll_request_sense_pt()
+    - add sg_ll_send_diag_pt(), sg_ll_receive_diag_pt()
+    - add sg_get_sfs_name() for spc5r11 (Feature sets)
+    - add sg_decode_transportid_str()
+    - add sg_msense_calc_length()
+    - add sg_all_zeros(), sg_all_ffs()
+    - add sg_get_sense_cmd_spec_fld()
+    - add sg_is_scsi_cdb()
+    - add sg_get_nvme_cmd_status_str()
+    - add sg_nvme_status2scsi()
+    - add sg_nvme_desc2sense()
+    - add sg_build_sense_buffer()
+    - add sg_get_nvme_opcode_name()
+    - add sg_memalign() and sg_get_page_size()
+    - add sg_is_aligned() and pr2ws()
+    - add sg_get_big_endian(), sg_set_big_endian()
+    - add hex2stdout(), hex2stderr() and hex2str()
+    - add sg_convert_errno()
+    - add sg_if_can2stdout(), sg_if_can2stderr() and
+      sg_exit2str()
+    - implement 'format' argument in dStrHexStr()
+    - add read buffer(16) command mode names
+    - add Microcode activation sense descriptor spc5r10
+    - add SG_LIB_OK_TRUE(0) and SG_LIB_OK_FALSE(36)
+      non "error" code defines for exit status
+    - add SG_LIB_LBA_OUT_OF_RANGE error code
+    - add SG_LIB_UNBOUNDED_32BIT (_16BIT and _64BIT)
+      defines to help with decoding corner cases
+    - identify vendor specific sense data (response
+      code 0x7f), print contents in hex
+  - sg_pr2serr.h: add sg_scnpr() [like lk scnprintf()]
+  - sg_pt: add construct_scsi_pt_obj_with_fd()
+    - add pt_device_is_nvme(), get_pt_nvme_nsid()
+    - add check_pt_file_handle()
+    - add get_pt_file_handle(), set_pt_file_handle()
+    - add small SNTL to support sg_ses on NVMe
+  - sg_lib_data: sync asc/ascq codes with T10 20170114
+    - add write scattered (16+32) cdb names sbc4r11
+  - sg_cmds_extra: expand sg_ll_ata_pt() to send new
+    Ata pass-through(32) command (sat4r05)
+  - sg_sat_identify: expand to take --len=32
+  - sg_pt: add dummy pt_device_is_nvme()
+  - rescan-scsi-bus.sh: harden code
+    - fixes from Suse; bump version
+    - bump version to 20180615
+    - add to install list in Makefile, hope it does
+      not clash with other package providing it
+    - add --ignore-rev to ignore revision change
+  - 55-scsi-sg3_id.rules: fixes from Suse
+  - https://github.com/hreinecke/sg3_utils branch
+    sles15 synced 20170914
+  - move some testing utilities out of the
+    'examples' and 'utils' directories into the new
+    'testing' directory
+  - add testing/sg_tst_nvme utility
+  - clean Makefile.freebsd in examples/ and testing/
+  - gcc 7.2 cleanups (sysmacros.h etc)
+  - clang --analyze static checker clean ups
+  - shellcheck cleanup on scripts
+  - ./configure automake utility:
+    - option --enable-debug added for testing
+    - option --disable-linuxbsg retired, still accepted
+      but now ignored, Linux sg v3 or v4 interface
+      decision made at runtime
+    - Info section now printed at end of ./configure
+  - automake: add AM_PROG_AR to configure.ac
+    - upgrade to version 1.15
+    - various configure.ac and Makefile.am cleanups
+    - add SG_LIB_ANDROID build 'define'. If defined then
+      SG_LIB_LINUX is also defined, so test for Android
+      before Linux if need to differentiate
+  - update BSD license from 3 to 2 clause aka FreeBSD
+    license (without reference to FreeBSD project)
+  - debian: bump compat file contents from 7 to 10
+
+Changelog for sg3_utils-1.42 [20160217] [svn: r663]
+  - sg_timestamp: new, to report or set timestamp
+  - sg_read_attr: new, supported by tape drives
+  - sg_stpg: fix truncation of target port field
+  - sg_inq: cope with unicode strings, udev fixes
+    - update version descriptor list to 20160125
+    - '--export': new entries for UUID descriptor
+  - sg_ses: add more field acronyms (ses3r11)
+  - sg_logs: add Utilization lpage (sbc4r07)
+    - add Background operation lpage
+    - add Pending defects lpage
+    - add LPS misalignment lpage (sbc4r10)
+    - document '--All' ('-A') option
+    - rework lto tape vendor lpages
+  - sg_vpd: add Block limits extension VPD page
+    - add Device constituents VPD page
+    - add LB Protection VPD page (ssc ssc5r02a)
+    - LB provisioning VPD page: expand LBPRZ, add
+      Minimum and Threshold percentage fields
+    - rework lto tape vendor VPD pages
+  - sg_inq+sg_vpd+sg_xcopy: add support for locally
+    assigned UUIDs in VPD page 0x83 (spc5r08)
+  - sg_sanitize: add --znr option (sbc4r07)
+  - sg_rep_zones: add --partial option (zbc-r04)
+  - sg_format: add ffmt option (sbc4r10)
+    - add support for FORMAT MEDIUM (for tape)
+  - sg_raw: document length relationships
+  - rescan-scsi-bus.sh: updates from Suse
+  - sg_lib_data: sync asc/ascq codes with T10 20151126
+  - sg_lib: add 'sense' categories for SCSI statuses:
+    condition met, busy, task set full, ACA active and
+    task aborted
+    - add pr2serr() extern
+    - change sg_get_sense_str() and dStrHexStr(), return
+      chars written (returned void previously)
+    - add sg_get_sense_descriptors_str() function
+    - add sg_get_designation_descriptor_str() function
+    - sg_get_desig_type_str()+sg_get_desig_assoc_str()
+      and sg_get_desig_code_set_str() added
+    - sg_get_opcode_sa_name() break out zoning in/out,
+      read attribute and read position service actions
+  - sg_cmds_extra: add sg_ll_format_unit2() for FFMT
+  - sg_pr2serr.h: new, to shorten fprintf(stderr, ...)
+  - sg_io_linux, sg_pt_linux: drop SUGGEST_* decoding
+  - sg_unaligned.h: add 48 bit support and gets for
+    variable length unsigned integers
+    - add specializations for little and big endian
+  - change sg_ll_*() function's 'int noisy' to bool
+
+Changelog for sg3_utils-1.41 [20150511] [svn: r644]
+  - sg_zone: new utility for open, close and finish
+    zone commands introduced in zbc-r02
+  - sg_rep_zones and sg_reset_wp: change opcodes as
+    indicated in zbc-r02
+  - sg_read_buffer: add READ BUFFER(16) support (spc5r02)
+  - sg_logs: add --enumerate and acronyms
+    - allow decode from hex or binary in file
+    - decode environmental reporting + limits lpages
+  - sg_write_buffer: add --timeout=TO option
+  - sg_lib interface: add sg_lib_pdt_decay(), TPROTO_PCIE
+    plus support for zoning service actions
+  - sg_lib: in Linux blocked devices yield ENXIO from
+    ioctl(SG_IO), map to SG_LIB_CAT_NOT_READY
+    - clean up sg_warnings_stream handling
+  - sg_inq+sg_vpd: fix SCSI name string decoding in
+    device identification VPD page (0x83)
+    - increase sanity on Unit Serial number VPD page
+    - improve rdac vpd page reporting (vendor)
+  - sg_inq: improve NAA handling in dev_id VPD page
+    - update version descriptor list to 20150126
+  - sg_vpd: add atomic boundary values (sbc4r04)
+    - block limits VPD page: fix unmap granularity
+      alignment value; spc5r02 additions
+  - sg_readcap: add support for ZBC's rc_basis field
+  - sg_senddiag: fix bug with --raw option
+    - add support for -HHH for output suitable for --raw
+  - sg_ses: enclosure element: add failure and warning
+    acronyms, fix warning indication output
+    - additional element status dpage: add PCIe/NVMe
+    - handle element descriptor names that count
+      multiple trailing NULLs
+  - rescan-scsi-bus.sh: add --issue-lip-wait option and
+    improve error handling
+    - improve dm-multipath handling
+  - sg_modes: make '-HHH' output suitable as input to
+    'sdparm --inhex='
+  - sg_rdac: add '-6' option for mode sense/select(6)
+    - add support for reporting more SCSI transports
+      and accessing rdac extended mode page 0x2c
+  - sg_write_same: cleanup, mainly man page
+  - scsi_logging_level: replace use of tr command
+  - examples/sg_tst_async: cleanup
+  - examples/sg-simple_aio.c: remove
+  - sg_lib_data: sync asc/ascq codes with T10 20150423
+  - Makefile cleanup
+  - autogen.sh: upgrade to buildconf 20091223 version
+
+Changelog for sg3_utils-1.40 [20141110] [svn: r620]
+  - sg_write_verify: new utility for WRITE AND VERIFY
+  - sg_ses_microcode: new utility
+  - sg_sat_read_gplog: new utility
+  - sg_senddiag: add --maxlen= option
+  - sg_copy_results: correct response length calculations
+  - sg_format: make '-FFF' bypass mode sense/select
+    - add --mode=MP to supply alternate mode page,
+      default remains read-write error recovery mpage
+    - output unit serial number and LU name prior to
+  - sg_inq: expand Block limits VPD page output
+    - fix --cmddt output if not supported by device
+    - more sanity checks on vendor supplied fields
+  - sg_vpd: add --all option
+    - more TPC VPD page decoding
+    - add zoned block device characteristics page
+    - more sanity checks on vendor supplied fields
+  - sg_ses: fix problem with --index=sse (and ssc)
+    - mask status element before using as control
+    - defeat previous item with --mask (ignore) option
+    - SAS connector status element: add overcurrent bit
+    - handle element descriptor names that count a
+      trailing NULL
+    - add --warn option mainly for broken joins
+    - add optional descriptions to -ee output
+    - sync with ses3r07
+  - sg_sanitize: add --desc and --zero options
+    - output unit serial number and LU name prior to
+  - sg_rep_zones: corrections, sync with zbc-r01c
+  - sg_persist: split help into two pages, '-hh' for 2nd
+  - sg_logs: refine tape drive output
+  - sg_raw: with -vvv decode T10 CDB name
+    - do not output/print data-in if error
+  - sg_opcodes: add --compact field
+  - sg_senddiag: add --page=PG option
+  - sg_reset: add words for EAGAIN from reset ioctl
+  - sg_sat_*: mention t_type and multiple_count fields
+  - win32: sg_scan: handle larger configurations
+  - sg_lib: trim trailing spaces in dStrHex() and friends
+  - sg_lib_data: sync asc/ascq codes with T10 20140924
+    - clean up service action string functions
+    - sg_ll_unmap_v2(): fix group number
+    - sg_ll_inquiry(), sg_ll_mode_sense*(),
+      sg_ll_log_sense(): use resid to clear unfilled
+      data-in buffer
+  - sg_unaligned.h: add header for building parameters
+  - examples/sg_tst_async: new Linux sg test utility
+
+Changelog for sg3_utils-1.39 [20140612] [svn: r588]
+  - sg_rep_zones: new utility for ZBC REPORT ZONES
+  - sg_reset_wp: new utility, ZBC RESET WRITE POINTER
+  - sg_ses: add --eiioe=auto|force option
+    - fix AES dpage element indexing problems
+    - add --readonly option
+  - sg_write_buffer: add --bpw=CS option to call
+    write buffer multiple times for big blobs
+  - sg_format: add --ip_def option to fully provision
+  - sg_opcodes: add --mask option
+  - sg_logs: add --in=FN option for log select params
+    - add --filter=PARC (parameter code)
+    - add --no_inq for suppress initial INQUIRY call
+    - add --readonly option
+  - sg_persist: add --readonly option, environment
+    variable SG_PERSIST_IN_RDONLY sets ro on prin cmds
+  - sg_inq: sync version descriptors dated 20105176
+    - suppress dev-id VPD messages so they only appear
+      when --verbose is given
+    - add new SCSI_IDENT_*_ATA pair to --export output
+  - sg_luns: add decoding for conglomerate LUNS
+    - add --lu_cong option to simulate the LU_CONG bit
+  - sg_vpd: add --vendor=VP option, re-order vendor
+    specific pages, split lto into lto5 and lto6
+    - add Supported block lengths and protection types
+      page (sbc4r01)
+    - add Block device characteristics extension
+      page (sbc4r02)
+  - sg_copy_results, sg_get_lba_status, sg_luns,
+    sg_read_buffer, sg_readcap, sg_referrals, sg_rtpg,
+    sg_sat_set_features, sg_sat_identify:
+    add --readonly option
+  - sginfo: strip trailing spaces from INQUIRY text
+  - sg_rbuf: add --echo option (to use echo buffer)
+  - sg_lib: add sanitize command service action names
+    - add 'sense' categories for reservation conflict,
+      data protect and protection information violations
+    - add sg_get_category_sense_str() to API
+    - change struct sg_simple_inquiry_resp::rmb to byte_1
+    - add initial zbc service actions
+    - dStrHex(Err): fix output truncation error
+    - linux, sg: support SCSI_PT_FLAGS_QUEUE_AT_TAIL and
+      SCSI_PT_FLAGS_QUEUE_AT_HEAD (block layer queueing)
+  - sg_lib_data: sync asc/ascq codes with T10 20140516
+    - sync operation code with T10 20140515
+    - add id string for SPC-5
+  - scripts/59-scsi-sg3_utils.rules: removed
+    - functionality split into two scripts:
+      55-scsi-sg3_id.rules + 58-scsi-sg3_symlink.rules
+  - examples/sg_persist_tst.sh: add --exclusive option
+  - win32: sg_scan, sg_ses and sg_log fixes
+  - examples/sgq_dd: re-add old utility as example
+
+Changelog for sg3_utils-1.38 [20140401] [svn: r563]
+  - sg_ses: add --dev-slot-num= and --sas-addr=
+    - fix --data=- problem with large buffers
+    - new --data=@FN to read hex data from file FN
+    - error and warning message cleanup
+    - add --maxlen= option
+  - sg_inq: add --block=0|1 option to control opens
+    - add --inhex=FN to read response in ASCII hex from
+      a file; --inhex=FN --raw reads response in binary
+    - make -HHH (-HHHH for '-p ai') output suitable for
+      another sg_inq invocation to use --inhex to decode
+    - add LU_CONG to standard inquiry response output
+    - decode ASCII information VPD pages
+    - add HAW_ZBC in block dev char. VPD page (sbc4r01)
+    - sync version descriptors dated 20131126
+    - allow --page=-1 to force std INQUIRY decoding
+    - fix overflow in encode_whitespaces
+    - improve unit serial number display (VPD page 0x80)
+  - sg_vpd: add LU_CONG to standard inquiry response output
+    - add --inhex=FN to read response in ASCII hex from
+      a file; --inhex=FN --raw reads response in binary
+    - decode Third Party Copy (tpc) page
+    - add HAW_ZBC in block dev char. VPD page (sbc4r01)
+    - add LTO and DDS vendor pages
+    - allow --page=num to restrict --enumerate output
+  - sg_persist: add PROUT: Replace Lost Reservation (spc4r36)
+    - add --transport-id= for SOP: 'sop,<routing_id_in_hex>'
+  - sg_readcap: for --16 show physical block size if
+    different from logical block size
+  - sg_xcopy: environment variables: XCOPY_TO_SRC and
+    XCOPY_TO_DST indicate where xcopy command is sent
+    - change default to send xcopy to dst (was src)
+    - improve CL handling of short options (e.g. '-vv')
+  - sg_luns: guard against garbage response
+  - sg_decode_sense: with --nospace ignore spaces on
+    command line, so multiple arguments are concatenated
+  - sg_write_same: repeat if unit attention
+  - sg_rtpg: fix indexing bug with --extended option
+  - sg_logs: placeholder for pending defects lpage
+  - sg_unmap: fix another problem with --grpnum= option
+  - sg_lib.h: add PDT_ZBC define (spc4r36p)
+  - sg_lib_data: sync asc/ascq codes with T10 dated 20140320
+    - add pdt string for ZBC (spc4r36p)
+  - sg_lib: extensions to sg_get_num() and sg_get_llnum()
+  - sg_cmds_extra: fix sa bug in sg_ll_3party_copy_out()
+  - scripts/rescan-scsi-bus.sh: check if FC driver exports
+    issue_lip before using it
+    - man page added (Linux only)
+  - scripts/59-scsi-sg3_utils.rules: linux specific udev rules
+  - examples: add sg_tst_excl3 for testing O_EXCL
+    - improve sg_tst_excl and sg_tst_excl2
+    - add sg_tst_context for testing file handle contexts
+  - upgrade automake to version 1.13.3
+  - add suse directory and 'spec' file to facilitate builds
+
+Changelog for sg3_utils-1.37 [20131014] [svn: r522]
+  - sg_compare_and_write: fix wrprotect setting
+    - add --quiet option to suppress miscompare report
+    - merge features from another implementation
+  - sg_inq: fix referrals VPD page
+    - dev_id VPD: T10 vendor id designator clean up
+  - sg_logs: improve for tape drives, general cleanup
+  - sg_persist: fix core dump on -Q option
+  - sg_unmap: fix core dump on -g option
+  - sg_vpd: dev_id VPD: T10 vendor id designator clean up
+    - cleanup up dev_id NAA-3: locally assigned
+  - sg_ses: add --nickname and --nickid options
+    - eiioe added to additional element status page (ses3r6)
+    - multiple --filter options to prune output
+  - sg_verify: improve miscompare handling
+    - rename --btychk=ndo option to --ndo=ndo (hide former)
+    - add --quiet option
+  - sg_xcopy: allow sg and bsg devices
+    - fix for bpt going negative
+    - limit each XCOPY(LID1) command to 65535 blocks
+    - fix for seek in multi-segment copies
+  - sg_sanitize: skip 15 second safety delay with --fail
+  - sg_libs: extended copy opcode renamed (spc4r34)
+    - sg_ll_receive_copy_results(): expand for all sa_s
+    - add sg_get_sense_key()
+    - add sg_ll_3party_copy_out()
+    - add dStrHexErr(): ascii hex to stderr
+    - add dStrHexStr(): ascii hex to string
+    - add SG_LIB_CAT_MISCOMPARE to categories
+    - clean header files
+  - sg_pt_freebsd: sanity check on sense_resid; fix leaks
+  - scripts/rescan-scsi-bus.sh KG's v1.57 + HR patch
+    - improve wlun handling, detect updated and resized
+      devices, better multipath support
+  - Makefile.am cleanup
+  - examples: add sg_tst_excl and sg_tst_excl2
+
+Changelog for sg3_utils-1.36 [20130531] [svn: r497]
+  - sg_vpd: Protocol-specific port information VPD page
+    for SAS SSP, persistent connection (spl3r2), power
+    disable (spl3r3)
+    - block device characteristics: add FUAB bit
+  - sg_xcopy: handle more descriptor types; handle zero
+    maximum segment length; allow list IDs to be disabled;
+    improve skip/seek handling; allow xcopy on destination
+  - sg_reset: and --no-esc option to stop reset escalation
+    - clean up cli, add long option names
+  - sg_luns: add --test=ALUN option for decoding LUNs
+    - decoded luns output in decimal or hex (if -HH given)
+    - add '--linux' option to show Linux LUN after T10
+      representation, can map one to the other
+  - sg_inq: add --vendor option to show standard inquiry's
+    vendor specific fields in ASCII
+    - take resid into account with response output
+  - sg_sync: add --16 (for 16 byte command) and --timeout=
+  - sg_logs: add data compression page (ssc4)
+  - sg_sat_set_features: increase --lba from 1 to 4 bytes
+  - sg_write_same: add --ndob option (sbc3r35d)
+  - sg_map: mark as deprecated
+  - sginfo: mark as deprecated, especially -l (list)
+  - sg_lib: improve snprintf handling
+  - sg_lib_data: sync asc/ascq codes with T10 20130117
+  - sg_cmds (lib): if noisy given, give more UA info
+  - make code more C++ friendly
+
+Changelog for sg3_utils-1.35 [20130117] [svn: r476]
+  - sg_compare_and_write: new utility
+  - sg_inq+sg_vpd: block device characteristics VPD page:
+    add product_type, WABEREQ, WACEREQ and VBULS fields
+  - sg_inq: more --export option changes for udev
+  - sg_vpd: add more rdac vendor specific vpd pages
+  - sg_verify: add --ebytchk option for sbc3r34 changes
+  - sg_stpg: --offline option: fix 'Invalid state 0xe'
+  - sg_ses: Door Lock element changed to Door element and
+    abbreviation changed from 'dl' to 'do' (ses3r05)
+  - archive/rescan-scsi-bus.sh: upgrade to version 1.53hr
+    - move rescan-scsi-bus.sh to scripts directory
+  - sync to sbc3r34
+  - sg_lib: sg_ll_verify10+16 expand BYTCHK to 2 bit field
+  - sg_pt_win32, sg_scan(win32): changes for cygwin 1.7.17
+  - clean up man page summary lines
+
+Changelog for sg3_utils-1.34 [20121013] [svn: r461]
+  - sg_xcopy: new dd like utility for extended copy command
+  - sg_copy_results: new utility for receive copy results
+  - sg_verify: add 16 byte cdb, bytchk (data-out buffer)
+    and group number support
+  - sync to spc4r36 and sbc3r32
+  - sg_inq: add --export so sg_inq can replace udev's scsi_id
+    - decode old EMC Symmetrix abuse of VPD page 0x83
+  - sg_vpd: decode old EMC Symmetrix abuse of VPD page 0x83
+  - sg_ses: increase max dpage response size to 64 KB
+    - allow ident,locate on enclosure controller
+    - more sanity for additional element status descriptor
+  - sg_sanitize: add --ause, --fail and --test=
+  - sg_luns: add long extended flat space addressing format
+  - sg_logs: add ATA pass-through results lpage (SAT-2)
+  - sg_rtpg: add --extended option
+  - sg_senddiag: list rebuild assist diag page name
+  - sg_pt_linux: expand DID_ (host_byte) codes
+    - cope with a transport error plus sense data
+    - prefer major() over MAJOR() macro
+  - sg_lib: fix sg_get_command_name() service actions
+    - report sdat_ovfl bit (if set) in sense data
+    - decode extended_copy and receive_copy service actions
+    - decode read_buffer and write_buffer modes
+    - decode ATA PT fixed format sense (SAT-2)
+  - sg_cmds_extra: add sg_ll_report_tgt_prt_grp2()
+  - ./configure options:
+    - change --enable-no-linux-bsg to --disable-linuxbsg
+    - add --disable-scsistrings to reduce utility sizes
+
+Changelog for sg3_utils-1.33 [20120118] [svn: r435]
+  - sg_ses: major rework of indexes (again), now two level
+  - sg_write_buffer: new --specific option for mode specific
+    field; new mode 13 (spc4r32)
+  - sg_vpd: add hp3par volume info vendor VPD page
+    - fix 'scsi ports' [0x88] page problem
+    - add 'sinq' pseudo page for standard inquiry response
+    - add power consumption page
+  - sg_format: add --poll= option for request sense polling
+    - improve handling of disks > 2 TB and DIF (protection)
+  - sg_logs: LB provision lpage extra (sbc3r28)
+  - sg_modes: application tag mpage subcode 0xf0->0x2
+  - sg_write_same: no prot fields when wrprotect=0
+  - sg_get_lba_status: reflect change in sbc3r25 to Parameter
+    Data Length response field (offset reduced from 8 to 4)
+  - sg_inq, sg_vpd: sync with spc4r33
+  - win32: change DataBufferOffset type per MSDN; caused
+    problem with 64 bit machines (with buffered interface)
+  - sg_luns: tweak documentation for vendor specific reports
+  - add man pages for scsi_loging_level, scsi_mandat,
+    scsi_satl and scsi_temperature
+
+Changelog for sg3_utils-1.32 [20110730] [svn: r410]
+  - sg_sanitize: new utility for command added in sb3r27
+  - sg_sat_identify: add '--ident' to output WWN
+  - sg_ses: major rework of descriptor output
+    - add --index, --descriptor, --join, --clear, --get,
+      and --set options
+  - sg_raw: exit status corrections
+  - sg_decode_sense: add --nospace and --hex options
+  - sg_logs: fix bug with large --maxlen
+    - zero response length when resid implies it is invalid
+    - add scope field to lb provisioning lpage (sb3r27)
+  - sg_inq: sync version descriptors with spc4r31
+  - sg_lib_data: sync asc/ascq codes with spc4r31
+  - sg_vpd: add LBPRZ field in LB provisioning VPD page
+  - sg_format: allow format of pdt 7 (some MO drives)
+  - sg_cmds_basic: sg_cmds_process_resp() handle status good
+    with a sense key other than no_sense (e.g. completed)
+  - add README.iscsi
+
+Changelog for sg3_utils-1.31 [20110216] [svn: r386]
+  - sg_decode_sense: new utility to decode sense data
+  - sg_vpd: LB provisioning + Block limits pages (sbc3r26)
+  - sync asc/ascq and version descriptors with spc4r28
+  - sg_get_config, sg_rmsn, sg_verify: add --readonly option
+  - sg_lib: implement forwarded sense data descriptor
+    - decode user data segment referral sense data descriptor
+  - sg_lib, sg_turs, sg_format: more precision for progress
+    indication (two places after decimal point)
+  - sg_lib(win32): add runtime selection of SPT direct or
+    indirect interface
+    - sg_read_buffer+sg_write_buffer: set SPT direct
+  - add examples/forwarded_sense.txt + examples/ref_sense.txt
+
+Changelog for sg3_utils-1.30 [20101111] [svn: r363]
+  - sg_referrals: new utility for REPORT REFERRALS
+  - sbc3r25 renames 'thin' provisioning' to 'logical block
+    provisioning': changes in sg_format, sg_inq, sg_logs,
+    sg_modes, sg_readcap, sg_vpd
+  - sg_inq: update version descriptor list to spc4r27
+    - extended inquiry vpd page add extended self test
+      completion minutes field
+  - sg_lib: sync asc/ascq list to spc4r27
+    - dStrHex(): trim excess trailing spaces
+  - sg_read_long: add --readonly option (open() is rw)
+  - sg_raw: add --readonly option (open() is rw)
+    - allow bidirectional commands
+  - sg_vpd: rdac vendor page [0xc8] parse corrections
+    - extended inquiry vpd page add extended self test
+      completion minutes field
+  - sg_ses: expand --data (in) buffer to 2048 bytes
+  - sg_opcodes: add extended parameter data for TMFs (spc4r26)
+  - sg_dd: clean count calculation, document nocache flag
+    - treat bsg devices as implicit sg_io
+    - add more conversions
+  - sg_write_same: if READ CAPACITY(16) fails try 10 byte variant
+    - anticipate approval of proposal to allow UNMAP and ANCHOR
+      bits to be set on WRITE SAME(10) with '--10' option
+  - sg3_utils man page: sections added for OS device names
+
+Changelog for sg3_utils-1.29 [20100406] [svn: r334]
+  - sg_rtpg: new logical block dependent state and bit (spc4r23)
+  - sg_start: add '--readonly' option for ATA disks
+  - sg_lib: update asc/ascq list to spc4r23
+  - sg_inq: update version descriptor list to spc4r23
+  - sg_vpd: block device characteristics page: fix form factor
+    - update Extended Inquiry VPD page to spc4r23
+    - update Block Limits VPD page to sbc3r22
+    - update Thin Provisioning VPD page to sbc3r22
+    - Automation device serial number and Data transfer device
+      element VPD pages (ssc4r01)
+    - add Referrals VPD page (sbc3r22)
+  - sg_logs: add thin provisioning and solid state media log pages
+    - addition of IBM LTO specific log pages
+  - sg_modes: new page names from ssc4r01
+  - sg_ses: sync with ses3r02 (SAS-2.1 connector types)
+  - sg_unmap: add '--anchor' option (sbc3r22)
+  - sg_write_same: add '--anchor' option (sbc3r22)
+  - sg_pt interface: add set_scsi_pt_flags() to permit passing
+    through SCSI_PT_FLAGS_QUEUE_AT_TAIL and AT_HEAD flags
+  - add examples/sg_queue_tst+bsg_queue_tst for SG_FLAG_Q_AT_TAIL
+  - add AM_MAINTAINER_MODE to configure.ac to lessen build issues
+  - add BSD_LICENSE file to this and lib directories, refer to
+    it from source and header files. Some source has GPL license
+
+Changelog for sg3_utils-1.28 [20091002] [svn: r315]
+  - sg_unmap: new utility for thin provisioning
+    - add examples/sg_unmap_example.txt
+  - sg_get_lba_status: new utility for thin provisioning
+  - sg_read_block_limits: new utility for tape drives
+  - sg_logs: add cache memory statistics log (sub)page
+  - sg_vpd, sg_inq: extend Block limits VPD page (sbc3r19)
+  - sg_vpd: add Thin provisioning VPD page (sbc3r20) and
+            TapeAlert supported flags VPD page
+  - sg_inq: note VPD page support better in sg_vpd
+  - sg_persist: add transport specific transportID format
+    - allow transportIDs to be read from named file
+  - sg_opcodes: allow --opcode= option to take OP and SA
+    values (comma separated)
+    - tweak print format, remove test code
+  - sg_requests: remove test code in progress calculation
+  - sg_reset: add target reset option
+  - sg_luns: reduce default maxlen to 8192 (for FreeBSD)
+  - sg_raw: extend max cdb length from 16 to 256 bytes
+    - align heap allocs to page boundaries
+  - sg_lib: sg_set_binary_mode() needs config.h included
+    - add progress indication sense data descriptor (0xa)
+    - change SG3_UTILS_* constants to SG_LIB_*
+    - decode service actions within persistent reserve in/out
+    - sync with spc4r21
+  - sg_cmds_extra: add sg_ll_unmap() and sg_ll_get_lba_status()
+  - sg_pt_linux: fix check condition but empty sense buffer; occurred
+    when sg v3 node used and /usr/include/linux/bsg.h visible
+    - major() macro grief, if present include <linux/kdev_t.h> and
+      use MAJOR() instead
+  - scripts/sas_disk_blink: moved from this package to sdparm
+  - utils/hxascdmp: in Windows set binary mode on read files
+  - examples/sg_persist_tst.sh: add PRIN read full status command
+  - sg_raw,sg_write_buffer,sg_write_long,sg_write_same: in Windows
+    set binary mode on read files
+  - sg_pt_win32: default to non-direct variant of SPT interface
+    - use './configure --enable-win32-spt-direct' to override
+    - non-direct data length set to 16 KB, extended if required
+  - debian: incorporate patch from debian sid
+
+Changelog for sg3_utils-1.27 [20090411] [svn: r250]
+  - sg_write_same: new utility: 10, 16 and 32 byte cdb variants
+  - sg_inq: sync version descriptors with spc4r18
+    - add power condition VPD page
+    - expand block limits VPD page (sbc3r18)
+  - sg_vpd: add power condition VPD page
+    - expand block limits VPD page (sbc3r18)
+  - sg_map26: fix for lk 2.6.26 when CONFIG_SYSFS_DEPRECATED_V2
+    is not defined
+    - output cdb when verbose option given
+    - correct tape minors >= 32
+  - sg_dd: flock flag (does LOCK_EX|LOCK_NB)
+    - switch open on input for sg device nodes: first open
+      read-write and if that fails try opening read-only
+    - experiment with of2=OFILE2; add conv=sparse
+    - use posix_fadvise() to defeat caching of normal+block files
+      when new 'nocache' flag given
+    - sg_dd copied to own package called ddpt
+  - sg_dd, sgm_dd, sgp_dd: accept 'count=-1' for calculate count,
+    accept '-V' for version string
+  - sg_get_config: add OSSC feature [mmc6r02]
+  - sg_modes: add ATA power condition mode page
+  - sg_logs: protocol specific (SAS) lpage sync to sas2r15
+    - power condition transitions lpage (added in spc4r18)
+    - extra parameters for start-stop cycle counter lpage
+  - sg_format: add '--fmtpinfo=' and '--pie=' options (sbc3r18)
+  - sg_readcap: more protection + thin provisioning (sbc3r18)
+    - add a '--16' option for 16 byte cdb version
+  - sg_persist: code clean up
+    - allow '--transport-id=' argument to use space as separator
+    - add '--alloc-length=' argument
+  - sg_scan: (win32) new format, scsi adapter scan optional
+  - sginfo: fix crash when 1024 sg device nodes (or more)
+  - sg_ses: allow '--data=' argument to use space as separator
+  - sg_senddiag: allow '--raw=' argument to use space as separator
+  - sg_reassign: allow '--address=' argument to use space as
+    separator
+  - sg_wr_mode: allow '--contents=' and '--mask=' arguments to
+    use space as separator
+  - sg3_utils.spec: correction to configure call
+  - sg_pt: add scsi_pt_open_device_flags() call
+    - add scsi_pt_version() and clear_scsi_pt_obj() calls
+    - clear os_err at start of do_scsi_pt()
+    - add linux bsg support via runtime detection
+  - sg_cmds: add sg_cmds_open_device_flags()
+  - sg_cmds_extra: sg_ll_format_unit: remove rto_req argument,
+    the expanded fmtpinfo argument subsumes it.
+  - clearer split between Linux and Windows only code and doc
+  - automake tools: change to what Ubuntu 8.10 provides
+    - Ubuntu 8.10 libtool problems -> Debian 4.0
+
+Changelog for sg3_utils-1.26 [20080625] [svn: r183]
+  - sg_sat_phy_event: new utility; copied from examples
+    directory and enhanced, rename original to sg__sat_phy_event
+  - sg_ses: sync with ses2r19b, many nomenclature changes
+  - sg_get_config: sync with mmc6r01
+    - allow Microcode upgrade and DVD read feature descriptors
+      to be 4 bytes long
+    - add '--raw' option
+  - sg_verify: add --vrprotect= option
+  - sg_vpd: add nominal form factor to block dev. char. VPD page
+    - add --maxlen= option to set allocation length in cdb
+  - sg_inq: add --maxlen= option that does same as --len=
+    - move version descriptors (spc4r15) to sg_inq_data.c file
+  - sg_inq+sg_vpd: logic for "NAA-3 Locally assigned" identifier
+    - update extended inquiry VPD page
+  - sg_modes: add --maxlen= option to specify allocation length
+  - sg_start: add '--noflush' and '--mod=PC_MOD' options (sbc3r14)
+  - sg_request: add a '--progress' option (similar to sg_turs)
+    - add --maxlen= option to set allocation length in cdb
+  - sg_luns: add --maxlen= option to specify allocation length
+  - sg_dd: improve MMC handling of 'illegal mode for this track'
+    read errors (with ILI and info field)
+  - sg_dd, sgm_dd, sgp_dd, sginfo, sg_rbuf, sg_read: replace
+    "%lld" and friends with PRI macros
+  - sg_opcodes: tmf name change in spc4r15 (async event)
+  - sg_turs: add more to man page about '--progress' indication
+  - sg_write_long: add examples section to man page
+  - '--raw' option: modify utilities that can send binary output
+    to call sg_set_binary_mode(). For MingGW port CR problem.
+  - sg_lib: update asc/ascq and command name strings to spc4r15
+    - split sg_lib into sg_lib_data.[hc] and sg_lib.[hc]
+    - split sg_cmds_extra into sg_cmds_extra and sg_cmds_mmc
+    - add osd2r03 service actions (all different from osd-r10)
+    - add sg_get_trans_proto_str()
+    - add sg_get_sense_filemark_eom_ili() function (MMC uses ILI)
+    - add sense key specific unit attention condition queue
+      overflow decoding (added in spc4r13)
+    - add sg_set_text_mode() and sg_set_binary_mode() functions
+      for non-Unix OSes
+  - sg_cmds_mmc: add sg_ll_set_streaming() function
+  - sg_cmds_extra: add vrprotect argument to sg_ll_verify10()
+    - add sg_ll_get_performance() and sg_ll_set_cd_speed()
+  - change 'long long' to int64_t and 'unsigned long long' to
+    uint64_t to stress that 64 bit integer wanted, not larger
+  - audit of dangerous 'u64 = uch[24] << 24' code, replace most
+    'unsigned long's
+  - multiple documentation corrections provided by Dan Horak
+  - win32/MinGW: define SG3_UTILS_MINGW when detected
+  - remove archive/pre_configure subdirectory
+  - move sg_io_linux.c into the lib subdirectory
+  - utils/hxascdmp: add hxascdmp(1) man page
+  - switch primary build to ubuntu environment, rename
+    library to libsgutils2 to avoid clash
+
+Changelog for sg3_utils-1.25 [20071016] [svn: r115]
+  - sg_stpg: new utility to Set Target Port Groups
+  - sg_safte: new utility to query SAF-TE processor (SES like)
+  - sg_sat_set_features: new utility (actually copied from examples
+    directory); renamed examples version to: sg__sat_set_features
+  - sg_read_buffer: restore (had fallen out of build scripts)
+  - sg_dd: add oflag=sparse to step over bs*bpt number of zeros;
+    - with oflag=sparse, write last bs*bpt segment at end or after
+      error so file length of OFILE is appropriate
+    - when coe>1 then SCSI READ LONG logic remembers extended block
+      length of first encountered error
+  - sg_dd, sgm_dd, sgp_dd: allow iflag=null and oflag=null both of
+    which do nothing (placeholders)
+  - sg_ses: sync with ses2r17 then r18
+  - sg_vpd, sg_inq: add block device characteristics VPD page
+  - sg_inq: add '--vpd' option (or '-e') for backward compatibility
+  - sg_vpd: decode protocol specific lu information page for SAS
+    - add more RDAC vendor VPD pages
+  - sg_logs: update background scan results log page, sbc3r11
+    - add generation code to protocol specific page for SAS SSP
+    - add media changer diagnostic data log page
+  - sg_raw: fix error message when do_scsi_pt() fails
+  - sg_lib: sync asc/ascq codes with spc4r11
+    - add sg_get_num_nomult()
+    - add TPROTO_* protocol identifier constants to sg_lib.h
+  - sg_cmds_extra: add sg_ll_set_tgt_prt_grp()
+  - place source in subversion repository
+  - split code into src/ lib/ and include/ directories
+  - sync debian directory with their 1.24 version (sid unstable)
+  - convert build logic to use autotools (i.e. './configure ; make')
+    - rename this file from CHANGELOG to ChangeLog
+    - note: only code in lib/ and src/ directories built by
+      autotools; some other subdirectories still use hand-crafted
+      Makefiles
+
+Changelog for sg3_utils-1.24 [20070507] [svn: r77]
+  - sg_raw: new utility to send arbitrary SCSI commands
+  - sg_luns: increase number of luns that can be fetched
+    - fix length of raw and hex output
+    - add '--quiet' option to output only ASCII hex
+      representation of each lun
+  - sg_rtpg: update for changes in spc4r09
+  - sg_persist: update documentation, spc-4 references
+    - fix exit status values
+  - sg_inq: update version descriptors per spc4r09
+    - fix '--id' and '--extended'
+    - extend block limits VPD page (sbc3r09)
+  - sg_vpd: extend block limits VPD page (sbc3r09)
+    - append relative target port identifier to SAS target
+      port address with '-iq' option
+  - sg_logs: add decoding for stats+performance log pages
+    - fix showing of page names for pdt > 0
+    - implement '-HH' for single and all pages, fix '-r'
+    - when '--maxlen=' given, only do single fetch
+    - add Tape Alert (ssc), Media and Element statistics (smc) pages
+    - add '--brief' option
+  - sg_ses: sync with ses2r16
+    - fix bay number for SAS
+  - sg_format: add '--dcrt' and '--security' options
+  - sgm_dd: add 'smmap' oflag for shared_mmap_io testing
+    - add 'dio' oflag
+  - sg_dd, sgp_dd: add 'dio' iflag and oflag
+  - sg_modes: change SAS mode page names per sas2r09
+    - check validity of block descriptors length
+  - sg_pt: change opaque context object from 'void *'
+    to 'struct sg_pt_base *'
+  - sg_opcodes: anticipate extra tmfs from 07-159r0
+  - sg_sat_set_features: add more usage information
+    - add man page
+  - sg_sat_phy_event: add to examples directory
+  - sg_lib: sync asc/ascq codes with spc4r10
+  - Solaris port: using uscsi interface
+  - various .html files removed from doc directory
+
+Changelog for sg3_utils-1.23 [20070131] [svn: 75]
+  - sg_read_buffer: new utility
+  - sg_write_buffer: new utility
+  - sg_opcodes, sg_senddiag, sg_logs, sg_modes, sg_start, sg_inq,
+    sg_turs, sg_readcap, sg_rbuf: add getopt_long() based cli;
+    old and new cli selectable, new getopt_long cli is default
+  - scripts: new subdirectory containing some bash scripts
+    - add scripts/README file
+  - sg_reassign: add '--hex' option for grown and primary lists
+  - sg_rtpg: add '--raw' option
+  - sg_lib.h, sg_cmds_basic.h + sg_cmds_extra.h: add C++
+    'extern "C" ' wrappers
+    - cleanup C code so it will compile as C++
+  - sg_lib: sync with spc4r08
+    - include <inttypes.h>, use PRId64 instead of %lld form
+    - fix sg_get_sense_str() when empty sense buffer
+  - win32 port: add Makefile.mingw + related support for MinGW
+  - sg_cmds_extra: add sg_ll_read_buffer() and sg_ll_write_buffer()
+  - sg_dd, sgp_dd, sgm_dd, sg_read: use lseek64() instead of llseek.c
+  - sgm_dd: accept coe=<n> for interworking with sg_dd
+  - sg_rdac: fix on non-linux ports
+  - sg_ses: fix spurious warning in additional element status page
+    - '-rr' option outputs a diagnostic page in binary to stdout
+  - sg_opcodes: add command timeout descriptor support (spc4r08)
+    - change linux specific pass through to generic pass through
+  - sg_logs: add 'name=value' decoding for SAS specific lpage
+  - examples+utils subdirectories: remove symlinks
+  - synchronize man pages with usage messages
+  - sg3_utils.spec: rework
+
+Changelog for sg3_utils-1.22 [20061016] [svn: 72]
+  - sgp_dd: accept verbose=<n> as well as deb=<n> to ease
+    interworking with sg_dd and sgm_dd
+  - sg_sat_set_features: added to examples directory
+  - sg_lib: sync asc/ascq text with spc4r06
+    - move SG_LIB_CAT_NO_SENSE and SG_LIB_CAT_RECOVERED to
+      20 and 21 respectively; add SG_LIB_CAT_ABORTED_COMMAND
+      at 11 (its sense key value)
+  - sg_vpd: tweak '--page=sp --quiet' output
+    - change '-HHH' so same as '-rr' (prepares ATA Information
+      (ai) response for hdparm)
+  - sg_requests: add '-s' option to set exit status from
+    parameter data
+  - sg_modes: exit quickly from '-e' if device not ready
+  - sg_logs: sync sas log pages with sas2r05a
+    - expand background scan results log page
+    - add '-m=<max_len>' to limit response length
+    - drop '-scum' and '-sthr' options and add '-select'
+  - sg_write_long: add '--16' option to send 16 byte cdb
+    - add '--wr_uncor' and '--pblock' options
+  - sg_senddiag: cleanup and add sdiag_sas_p1_stop.txt
+    to examples directory
+  - sg_format: add '--cmplst=<n>' option (default: 1)
+    - add '--pfu=<n>' option
+    - expand man page to discuss P/D/C/GLISTs
+  - sg_reassign: add '--primary' option to fetch primary
+    defect list (PLIST) length
+  - sg_readcap: add '-H' option to output response in hex
+    and '-r' to output response in binary to stdout
+    - add logical blocks per physical block (sbc3r07)
+  - sginfo: add PLIST and GLIST designation to defect lists
+  - sg_cmds: split this support file into sg_cmds_basic.[hc]
+    and sg_cmds_extra.[hc]
+    - add sg_ll_ata_pt() (SATL ATA pass) to sg_cmds_extra.[hc]
+  - sg_rdac: fix includes for FreeBSD
+  - sg_dd: add 'coe_limit=' option to exit after <n>
+    consecutive 'coe' type read errors
+  - sgm_dd: print out throughput information when signal arrives
+    if time=1 (like sg_dd does already)
+  - sg_inq: change '-HHH' so same as '-rr'. Now sg_inq, sg_vpd
+    and sdparm output for hdparm with '-HHH'
+    -add '-l=<resp_len>' option
+  - sg_read_long: add '--pblock' option for physical blocks
+  - sg_luns: add '--hex' and '--raw' options
+  - sg_requests: add '--hex' and '--raw' options
+  - sg_scan: windows version added (was previously linux only)
+    - 2 man pages: sg_scan.8l and sg_scan.8w that are installed
+      as sg_scan.8
+  - archive directory: removed all but rescan-scsi-bus.sh
+      - README points to previous version in that directory
+  - sg_sat_identify: add to main directory
+      - rename earlier version to examples/sg__sat_identify.c
+  - sg_ident: rework as spc4r07 changed command names and
+    expanded functionality
+
+Changelog for sg3_utils-1.21 [20060706] [svn: 70]
+  - sg_vpd: new utility for decoding VPD pages. sg_inq's cli is
+    cluttered; also borrows from sdparm's VPD handling
+  - sg_rdac: new utility for vendor specific work
+  - sg_lib: add sg_vpd_dev_id_iter() to iterate over di VPD page
+    - add sg_ata_get_chars() to fetch chars from ATA words
+    - sync additional sense code strings with spc4r05a
+    - add SG_LIB_CAT_NOT_READY category when sense_key is NOT READY
+    - add SG_LIB_FILE_ERROR category for open problems
+    - add SG_LIB_SYNTAX_ERROR category for command line problems
+    - broaden SG_LIB_CAT_MEDIA_CHANGED to SG_LIB_CAT_UNIT_ATTENTION
+    - add SG_LIB_CAT_MALFORMED for bad responses
+    - BEWARE: these changes cause confusion if an executable from this
+      version is run with a libsgutils library from 1.20 or earlier
+  - sg_cmds: add SG_LIB_CAT_NOT_READY return to most "ll" functions
+    - alter many utilities to report SG_LIB_CAT_NOT_READY
+  - sg_dd: add retries=<n> option for sg_io
+  - sg_logs: add '-T' option to output protocol specific port log page
+    - add support for log subpages (new in spc4r05)
+    - more sanity checks in Start Stop Cycle Counter page
+  - sg_cmds: add sg_ll_read_long16()
+    - add page_code and subpage_code to sg_ll_log_select()
+    - add subpage_code to sg_ll_log_sense()
+  - sg_read_long: do READ LONG(16) when '--16' given
+  - sg_read: accept and ignore 'of=' arguments
+  - sg_dd: expand medium/hardware error "coe' processing to include
+    the "blank check" sense key (for optical devices)
+  - sg_ses: expand display element (per 05-011r2)
+  - sg_format: clear 'cmplst' bit (for MO disks)
+    - add '--six' ('-6') option for mode sense/select(6)
+  - sg_format + sg_test_rwbuf: fix for when char is unsigned
+  - sg_inq: VPD page 0x89: output ATA IDENTIFY DEVICE strings
+    - for IDENTIFY (PACKET) DEVICE response use sg_ata_get_chars()
+  - sg3_utils.html : new name, was previously u_index.html. Copy
+    placed in doc subdirectory
+  - tools.html : SCSI and storage tools reference, copy placed in
+    doc subdirectory
+  - sg3_utils.8 : add a new man page containing general information
+    especially common exit status values
+  - sg_sat_identify: added to examples directory (SAT passthrough test)
+    - extend to pass through IDENTIFY PACKET DEVICE with '-p' option
+  - sg_sat_chk_power: added to examples directory
+  - sg_sat_smart_rd_data: added to examples directory
+  - sg_chk_asc: added to utils directory to check asc_ascq codes
+  - debian: stop placing archive directory under examples
+    - add build_debian.sh script
+
+Changelog for sg3_utils-1.20 [20060418] [svn: 68]
+  - sg_logs: decode phy event descriptors in SAS port specific
+    log page (sas2r03)
+    - new parameter control byte format (spc4r03), subpages to come
+  - update Makefile (linux) to install sg_io_linux.h + sg_linux_inc.h
+  - sg_map26: fix for block device mapping in lk 2.6.16-rc1 and beyond
+    - cope with sysfs removal of 'generic' symlink post lk 2.6.16,
+      anticipate removal of 'tape' symlink
+  - sg_dd, sgm_dd, sgp_dd: fix problem around 0x7fffffff blocks
+  - sg_dd: fix read_long processing error (when 'coe=2' or 3)
+    - expand 'coe=' to take 0...3 (invokes read long with 2 or 3)
+    - allow for SG_GET_RESERVED_SIZE yielding 0, lk 2.6.16 feature
+  - sgp_dd: add 'iflag=' and 'oflag=' arguments; signals (like sg_dd)
+  - sgm_dd: add 'iflag=' and 'oflag=' arguments; signals (like sg_dd)
+  - sg_get_config: double->dual renaming (mmc5r03)
+  - sg_read: add 'dpo=' and 'fua=' options
+    - allow 'count' < 0 (or 'bpt=0') for issuing zero block READs
+    - allow for SG_GET_RESERVED_SIZE yielding 0, lk 2.6.16 feature
+    - add 'no_dxfer=0|1' option
+  - sg_modes: fix exit value when MODE SENSE fails
+    - add '-e' to examine presence of page codes from 0x0 to 0x3e
+  - sg_requests: add '--num=' and '--time' options for timing multiple
+    invocations
+  - sg_inq: fix vpd 0x83 designator code 8 name: "scsi name string"
+  - sg_scan: if lk 2.6, use sysfs to find active sg device nodes
+  - sg_map: if lk 2.6, use sysfs to find active sg device nodes
+  - sg_ses: expand display element (per 05-011r1)
+  - sg_start: add an '-i' option which is equivalent to '--imm=1'
+  - sg_senddiag: update man page showing use of two scripts in
+    examples directory (sdiag_sas_p0_cjtpat.txt and _p1_)
+  - sg_lib: fix sg_get_sense_descriptors_str() case 9 (ATA Return)
+
+Changelog for sg3_utils-1.19 [20060127] [svn: 66]
+  - sg_start: accept '--' options (e.g. 'sg_start --stop')
+    - add '--fl=<n>' option for jump to format layer (mmc5)
+  - sg_logs: background scan log page, resync with sbc3r03
+    - add '-scum' and '-sthr' for setting defaults
+    - add device statistics log page (ssc + adc)
+    - fix "last n" deferred errors/error events incrementing
+    - partial addition of log subpages (spc4r03)
+  - sg_get_config: sync features with mmc5 rev 02b
+  - sg_wr_mode: mask out dpofua bit in mode select header
+  - sg_inq: try harder with '-A' to identify ATA device
+    - broaden meaning of '-d' option to decode ...
+    - decode software interface id VPD page ('-p=84 -d')
+    - decode device capabilities (ssc) VPD page ('-p=b0 -d')
+  - sginfo: correct defect list handling ('-d' and '-G')
+  - sg_verify: improve error processing (e.g. medium errors)
+  - sg_ses: scsi target_initiator port additional element
+     status (ses2r14)
+  - many: arguments that currently accept '0x' or '0X' to
+     indicate a hex number may alternatively take a trailing
+     'h' or 'H' to indicate hex
+  - sg_lib: update asc/ascq strings (spc4r03)
+  - sg_lib+sg_cmds: make independent of linux via
+    sg_pt.h function based interface.
+    - linux pass through code placed in sg_pt_linux.c
+    - rename sg_include.h to sg_linux_inc.h
+    - linux specific code in sg_lib.[hc] moved to
+      sg_io_linux.[hc]
+  - port to FreeBSD: using sg_pt_freebsd.c
+  - port to Tru64: using sg_pt_osf1.c
+  - sg_cmds: add sg_ll_get_config(), sg_ll_format_unit(),
+     sg_ll_reassign_blocks(), sg_ll_persistent_reserve_in+out(),
+     sg_ll_read_long10(), sg_ll_verify10(), sg_ll_write_long10()
+  - sg_persist: add "allow commands" to report capabilities
+  - sg_persist_tst: (examples) takes device node as argument
+  - sg_luns: add "security protocol" wlun
+
+Changelog for sg3_utils-1.18 [20051118] [svn:63]
+  - sg_map26: new utility to map sg devices in lk 2.6
+  - sg_luns: luns > 16,384 (sam-4 rev 4)
+  - sg_ses: bump fan speed field to 11 bits
+    - SAS connector names (ses2r13)
+  - sg_inq: add '-rr' option for "hdparm --Istdin"
+  - sg_get_config: tracking mmc-5
+  - sg_write_long: add support for COR_DIS bit
+  - sg_cmds: add sg_ll_test_unit_ready_progress()
+  - sg_turs: '-p' option shows progress
+  - sg_dd: add 'iflag=' and 'oflag=' options
+    - remove output of mode page info when verbose > 0
+    - add control of DPO bit via iflag/oflag
+  - sg_lib: add sg_get_pdt_str()
+    - update asc/ascq strings
+  - sg_modes + sginfo: add SAS(2) SSP shared mode subpage
+  - doc: rename "html" directory to "doc"
+  - Makefile: add 'libtool --finish' to install
+
+Changelog for sg3_utils-1.17 [20050922] [svn: 60]
+  - sg_inq: add '-a' option for ATA information VPD page
+    - add '-b' option for Block limits VPD page (SBC)
+    - add '-A' option for probing ATA or ATAPI device
+    - increase raw ('-r') and verbose ('-v') output for
+      ATA(PI) devices to 512 bytes (was 256 bytes)
+    - output hex ('-H') and verbose response for ATA(PI)
+      devices in 16 bit words (corrected for endianness)
+      - output bytes if '-HH' option given
+    - sync with spc4 rev 02
+  - sg_lib: add dWordHex() and sg_is_big_endian()
+    - sync asc/ascq with spc4 rev 02
+  - sg_cmds: defensive prefill for inquiry commands
+  - sg_opcodes: sync with spc4 rev 02 (add tmf I_T nexus reset)
+  - sginfo: add EBACKERR in Informational exception mode page
+    - add Background control mode page (SBC-3)
+  - sgm_dd: add 'verbose=<n>' option
+
+Changelog for sg3_utils-1.16 [20050810] [svn: 58]
+  - sg_ident: new utility to report+set device identifier
+  - sg_map: increase MAX_SG_DEVS from 256 to 2048
+  - debian: new directory to support deb package builds
+  - sg_get_config: add '--current' option, same as '--rt=1'
+    - update for DVD+RW Dual Layer
+  - sg_inq: add notes in source about use of SCSI INQUIRY
+     - decode Management network addresses VPD page ('-m')
+     - decode Mode page policy VPD page ('-M')
+  - sginfo: increase device mapping capability (> 78 disks)
+     - add '-r' option to scan /dev/raw* device nodes [Tim Hunt]
+  - sg_dd: change bpt default value to 32 when bs >= 2048 bytes
+  - sg_ses: mention SAF-TE in man page
+  - sg_readcap: add '-b' option for brief output (2 hex numbers)
+  - sg_cmds: add sg_ll_start_stop_unit(), sg_ll_prevent_allow(),
+      sg_ll_report_dev_id() and sg_ll_set_dev_id()
+  - sg_lib: add extra argument to sense print functions to enable
+      the suppression of the raw output of the sense buffer
+      - resid > 0 warnings now includes number actually fetched
+  - sg_start: add '-load' and '-eject' options
+    - default to start action when no other indication given
+    - change -imm=0|1 option default to 0 (was 1)
+  - gcc 4.0: cleanup warnings (apart from sgp_dd: revisit later)
+
+Changelog for sg3_utils-1.15 [20050605] [svn: 56]
+  - sg_cmds: sg_get_mode_page_controls(): improve error processing,
+     add double fetch
+  - sg_turs, sg_rbuf, sg_requests, sg_test_rwbuf, sg_format,
+    sg_dd and sgm_dd: add O_NONBLOCK to open()
+  - sgm_dd: switch to use SG_IO ioctl (that leaves only
+     sgp_dd using the asynchronous sg write()/read() sequence)
+  - sg_ses: sync with rev 12 changes
+  - sg_map: extend to cope with sparse disk device names with
+     up to 3 letters (e.g. /dev/sdaaa) [Nate Dailey]
+  - sg_modes: add '-f' option to flexibly decode broken mode
+     sense responses.
+     - zero prefill response; stop decoding response after 3
+       unit attention mode pages seen (i.e. malformed)
+     - add '-L' option for LLBAA bit in msense 10 cdb
+  - sg_reset: update man page
+  - sg_inq: VPD page 0x83: output eui addresses in hex as well
+  - Makefile: fix bug in rules for sgp_dd (when 'make dep' used)
+  - sg_format: expand explanations in its man page
+  - sg_inq, sg_logs, sg_modes, sg_opcodes, sg_rbuf, sg_readcap,
+    sg_scan, sg_senddiag, sg_start and sg_turs: allow command
+    line to take concaternated options
+  - sg_start: add -start and -stop to parallel "1" and "0"
+  - sg_senddiag: set pf bit with '-l' option
+
+Changelog for sg3_utils-1.14 [20050506] [svn: 54]
+  - sg_rmsn: new utility to read media serial number
+  - sg_rtpg: add T_SUP bit report
+  - sg_ses: ses-2 rev 11 changes (mainly to additional element status)
+        - add 'bay number' to SAS additional element status
+  - sg_modes: recognise attached enclosure and medium changer
+  - sg_inq: spell out non-zero peripheral qualifiers
+        - note VS bit preceding MultiP(ort) when latter set
+        - VPD page 0x83: output naa addresses in hex as well
+  - sginfo: recognise attached enclosure and medium changer
+        - increase device mapping capability (to 78 disks)
+          [Tim Hunt]
+  - sg_senddiag: add option to send raw diagnostic page
+  - sg_get_config: update some BD information
+  - sg_reasign: add '-g' option to give grown defect list length
+  - sg_dd: note default bpt value (128) may be too high for cd/dvds
+  - sg_lib: sync with SPC-3 rev 22a [opcodes + asc/q]
+        - add DID_IMM_RETRY and DID_REQUEUE [linux specific "host" bytes]
+  - sg_cmds: add send+receive diagnostic, read defect data commands
+        - add duration output on some commands when verbose > 2
+  - spec: change to produce libsgutils (and -devel variant) as well as
+          sg3_utils binary rpms
+  - sdparm: new utility like hdparm but for SCSI disks (or other devices)
+        - moved to its own package called: sdparm
+
+Changelog for sg3_utils-1.13 [20050313] [svn: 52]
+  - sg_format: new utility to format disks (perhaps change block size)
+  - sg_ses: rename "device element" to "additional element" [SES-2 rev 10]
+        - add SAS expander and connector elements; add download
+          microcode and subenclosure nickname diagnostic pages
+        - fix additional element descriptor for SAS
+        - off by 1 error when no type descriptor text in config page
+          <David dot Baldwin at anu dot edu dot au>
+  - sg_logs: log page for background media scan results
+  - sginfo: add "-flba64" option for outputting 64 bit lba defect lists
+  - sg_get_config: additions for BD from MMC-5 rev 1b
+  - sg_lib: add SG_LIB_CAT_ILLEGAL_REQ sense category
+        - add sg_get_sense_progress_fld()
+        - SPC-3 rev21d updates: report + set timestamp
+        - sg_get_num() + sg_get_llnum(): switch to multipliers that
+          are compatible with SI and with IEC 60027-2. Used modern
+          GNU's dd command as guide.
+        - report field replaceable unit code in fixed format
+  - sg_dd: add logic to use read_long on unrecovered read errors when
+           'coe' set, read just prior to error if 'coe' clear
+        - both 'odir' and 'blk_sgio" are honoured on block devices
+        - add 'verbose' switch, output some mode page info when verbose
+        - print out elapsed time/throughput when signal received
+        - add new web page discussing sg_dd, copy in html subdirectory
+  - sg_read: add 'blk_sgio' and 'odir' options
+  - sg_wr_mode: clear mode data length in mode select(10)
+  - sg_test_rwbuf: add long options, allow test to run multiple times
+  - sg_cmds: add sg_get_mode_page_types() [get current, changeable, etc]
+  - llseek.c: add Makefile rule without "-std=c99", breaks on some archs
+
+Changelog for sg3_utils-1.12 [20050121] [svn: 50]
+  - sg_wr_mode: new utility to modify (i.e. write to) mode pages
+  - sg_reassign: new utility: issues Reassign Blocks command
+  - sg_rtpg: new utility: issues Report Target Port Groups command
+             [Christophe Varoqui]
+  - ATA IDENTIFY command misspelt as "IDENTITY" in several places
+  - sginfo: tweak SAS mode pages to match sas 1.1 rev 07
+        - add NV_DIS bit to disk caching mode page
+  - sg_map: open /dev/nst* rather than /dev/st* (to stop spurious rewinds)
+  - sg_lib: ATA return sense descriptor
+        - add sg_get_sense_info_fld() to fetch info field from sense data
+        - fix bug in sg_scsi_sense_desc_find()
+        - add sense key specific decoding for fixed format sense data
+  - sg_modes: extend '-p' option to allow '-p=<page_code>,<subpage_code>'
+        - add '-A' option to output all mode pages and subpages
+        - extend '-l' option to show subpages, selected command set pages
+  - sg_inq: fix LUN WWN output in unit path report VPD page (emc)
+            [Hergen Lange]
+  - sg_get_config: some additions for DVD-R dual layer
+  - sg_modes: show write protect (WP) and DpoFua flags for disks
+  - sg_cmds: add llbaa argument to sg_ll_mode_sense10()
+
+Changelog for sg3_utils-1.11 [20041126] [svn: 48]
+  - sg_sync: new utility: invokes the synchronize cache command
+  - sg_prevent: new utility: invokes the prevent allow medium removal command
+  - sg_get_config: new utility: get configuration command for dvds and cds
+  - sg_request: fix, allocation length wasn't set
+  - sg_start: remove '-s', as start_stop_unit implicitly syncs caches
+  - sg_ses: add SAS expander element type
+  - sg_inq: add sanity check to unit serial number (VPD page 0x80)
+        - output ANSI version string (e.g. "SPC-2", previously was number)
+        - add '-s' option to decode SCSI Ports VPD page
+  - sg_logs: decoding of format status and non-volatile cache log
+        pages (0x8 and 0x17 in sbc-2)
+  - sg_dd: handle compile error when O_DIRECT not defined
+  - sginfo: tighten sanity checks around Unit serial number VPD page
+
+Changelog for sg3_utils-1.10 [20041030] [svn: 46]
+  - sg_readcap, sg_dd, sgm_dd, sgp_dd: fix sg_ll_readcap_10+16 (sg_cmds.c)
+  - sg_luns: new utility to report luns
+  - sg_logs: with '-t' (show temperature) ignore extra parameters in
+        temperature log page (still show them with '-p=d')
+  - sg_ses: clean argument sanity checks
+  - sg_cmds: add more common command wrappers
+
+Changelog for sg3_utils-1.09 [20041022] [svn: 44]
+  - sg_ses: new utility to get status and set control on SES devices
+  - sg_verify: new utility to verify block devices
+  - sg_emc_trespass: new utility for EMC specific trespass mode page
+  - sg_request: new utility that sends a REQUEST SENSE command
+  - sg_logs: '-r' to reset to manufacturer's defaults
+      - decode last n error events and last n deferred errors pages
+      - add names of ADC log pages
+  - sg_inq: update to SPC-3 rev 21
+      - decode Extended INQUIRY data VPD page [0x86] {'-x'}
+      - decode Unit Path Report VPD page [0xc0] (EMC) {'-P'}
+  - sginfo: decode SAS protocol specific lu mode page
+  - sg_err: convert to sg_lib + update to SPC-3 rev 21
+      - change GPL to FreeBSD license
+      - flag vendor specific asc/ascq ranges
+  - libsgutils: library made from sg_lib.c and sg_cmds.c
+    - rpm "spec" file additionally builds a "-devel" rpm with
+      static libsgutils.a and sg_lib.h and sg_cmds.h
+  - utils/hxascdmp.c: add FreeBSD license
+  - sg_persist: additions to man page
+      - add sg_persist_tst.sh example script to examples directory
+  - sg_turs: add '-v' and '-V' options
+  - sg_senddiag: add '-v' option
+
+Changelog for sg3_utils-1.08 [20040831] [svn: 42]
+  - sg_inq: fix noisy message when EVPD and raw modes set
+      - for VPD page 0, list supported page names if known {'-e'}
+      - add '-d' option to list version descriptors
+  - sg_opcodes: numerically sort list of opcodes unless '-u' given
+      - add '-a' to sort alphabetically list of opcode names
+      - add '-t' to report supported task management functions
+  - sg_persist: add 'register and move" PROUT service action
+      - add transportId support, document in sg_persist.8 and example
+  - sg_modes: handle subpage code for known pages (e.g. control extension)
+      - clean up sense buffer handling (allow for descriptor format)
+      - SPC-3 draft revision 20a updates
+  - sg_write_long: new utility to exercise WRITE LONG command
+  - sg_read_long: new utility to exercise READ LONG command
+  - sg_err.c: fix compile errors when SG_KERNEL_INCLUDES defined in lk 2.6
+      - sg_includes.h typedef of u64 for BLKGETSIZE64 ioctl in lk 2.4
+      - add safe_strerror(), sg_scsi_normalize_sense(), sg_normalize_sense()
+        and sg_scsi_sense_desc_find() functions
+      - add more sense data descriptor format decoding
+      - move multiple implementations of dStrHex() into sg_err.c
+  - sg_logs: exit if SCSI INQUIRY fails (e.g. when applied to ATA disk)
+  - sginfo: bug fixes and SPC-3 revision 20a updates
+      - add '-E' option to access Control Extension mode (sub)page
+  - sg_start: change '-d' switch to '-v' (verbose) switch for consistency
+      - document extra power condition states in man page
+  - sg_readcap: output rto_en and prot_en bits from long read capacity data
+  - add COVERAGE file to list SCSI command coverage
+
+Changelog for sg3_utils-1.07 [20040708] [svn: 40]
+  - sginfo: clean up inquiry vendor,product,revision strings
+      - '-Fhead': sort defect list by head
+        Tom Steudten <steudten at mx dot ch>
+  - rework sg_err for better command name coverage: service actions,
+    variable length and peripheral device type
+      - update asc,ascq codes to SPC-3 revision 19
+  - move scsi_devfs_scan to archive directory
+  - add sg_opcodes utility to list supported operation codes
+  - add sg_persist utility to support persistent reservations
+  - add '-i' option to sg_inq to decode device identification VPD page (0x83)
+  - sg_inq tries an ATA IDENTIFY if SCSI INQUIRY fails
+  - sg_dd, sgm_dd and sgp_dd calculate block device sizes (if count not given)
+  - drop SG_GET_VERSION_NUM ioctl guard in most utilities
+
+Changelog for sg3_utils-1.06 [20040426] [svn: 37]
+  - sg_logs: some HBAs don't like odd transfer lengths so increment
+      - do INQUIRY and output product strings
+      - add ASCII rendering of the Protocol specific SAS page
+      - add '-v' verbose option to output cdb
+  - sg_scan: optionally take device file names (e.g. /dev/hdc and /dev/sda)
+      - only request 36 byte INQUIRY responses
+  - sg_err: add sg_decode_sense() function
+  - sg_inq: update output (ref: SPC-3 t10/1416-d rev 17, 28 January 2004)
+      - remove '-p' option to print out PCI address of host
+      - add '-v' verbose option to output cdb
+  - sginfo: allow '-u' to take hex arguments (prefixed by '0x'),
+            when subpage value is 255 show multiple subpages
+      - accept /dev/hd? ATAPI devices directly in lk 2.6
+      - add '-t <pn>[,<spn>]' argument; like '-u' but decodes page
+        if it recognizes it
+      - drop '-L' argument
+      - add cd/dvd, tape, SES, more disk and more SPC-3 decoded mode pages
+      - add transport protocol decoded mode pages for SPI-4, FCP and SAS
+  - sg_modes: print all subpages when '-subp=ff' is selected
+      - do INQUIRY and output product strings
+      - add '-v' verbose option to output cdb
+  - Makefile: add -W compile flag and fix exposed warnings
+  - .spec file: change to build on Mandrake without errors
+
+Changelog for sg3_utils-1.05 [20031112] [svn: 35]
+  - sginfo: major rework; add IE page, clean up control, cache +
+        disconnect pages (as per SPC-3 and SBC-2).
+      - when storing, update saved page (change from previous version)
+      - use 10 byte mode sense and select by default (override with '-6')
+      - mode subpage support
+  - sg_dd, sgm_dd + sgp_dd:
+      - 64 bit capable (read capacity; count, skip and seek values).
+      - numerical arguments accept hex (prefixed by '0x' or '0X')
+      - require bpt > 0
+      - fix problem when reading /dev/null
+  - sg_dd: Treat SIGUSR1 properly: print stats and continue;
+  - sgp_dd: reduce READ CAPACITY response size to 8 bytes
+  - sg_read: require bpt > 0
+  - sg_test_rwbuf: switch from sg_header to sg_io_hdr interface
+    N.B. After these changes no sg3_utils utilities (in the main directory)
+    use the sg_header interface
+  - sg_scan: switch from sg_header to sg_io_hdr interface
+  - sg_senddiag: increase extended foreground timeout to 60 minutes
+  - sg_inq: add names of peripheral device types
+  - sg_readcap: show total size in bytes, MB, GB
+  - sg_logs: read log pages twice (first time to get response length), for
+    fragile HBAs; decode Seagate 0x37 + 0x3e pages; display pcbs
+  - sg_modes: fix core dump when corrupted response, don't print extra pages
+  - sg_map: increase sg device scanning from 128 to 256
+  - change man page references from lk 2.5 to lk 2.6
+  - examples/sg_iovec_tst: added testing sg_iovec (sg_io_hdr iovec's)
+    [retrospective addition to this log: "#define __user" added into
+     sg_include.h so user space programs aren't broken if they choose
+     to include kernel header.]
+  - utils/hxascdmp: add utility for displaying ASCII hex
+
+Changelog for sg3_utils-1.04 [20030513] [svn: 33]
+  - all remaining utilities in the main directory have man pages [thanks
+    to Eric Schwartz <emschwar at debian dot org> for 7 man pages]
+  - add CREDITS file
+  - sg_simple1, sg_simple2, sg_simple3, sg_simple4, sg_simple16 and
+    scsi_inquiry: moved to the examples directory
+  - sg_debug: moved to the archive directory
+  - sg_modes: add '-subp=<n>' for sub page code, suggests 6/10 byte
+    alternative if bad opcode sense received, flip -cpf flag to -pf,
+    add page names for most peripheral types
+  - sg_turs: default '-n=' argument to 1, only when '-n=1' print error
+    message in full
+  - sg_logs: print temperature "<not available>" for 255, '-t' switch
+    for temperature (from either temperature or IE log page)
+  - sg_dd: add '-odir=0|1' switch for O_DIRECT on block devices
+  - sg_start: add '-imm', '-loej' and 'pc=<n>' switches plus man page
+  - sg_readcap: add '-pmi' and 'lba=<n>' switches
+  - open files O_NONBLOCK in sg_inq, sg_modes and sg_logs so they
+    can be used on cd/dvd drivers when there is no disk present
+
+Changelog for sg3_utils-1.03 [20030402] [svn: 30]
+  - sg_senddiag: added, allows self tests and listing of diag pages
+  - sg_start: changed to use SG_IO so works on block devices
+  - sg_err: print out some "sense key specific" data [Trent Piepho]
+  - sg_modes: add "-6" switch for force 6 byte MODE SENSE [Trent Piepho]
+  - sg_modes: more information on page codes and controls
+  - sg_inq, sg_modes, sg_logs, sg_senddiag: add man pages
+  - sg_dd: add "append=0|1" switch for glueing together large files
+  - note in README about utilities offered by scsirastools package
+
+Changelog for sg3_utils-1.02 [20030101] [svn: 28]
+  - sg_inq: check if cmddt or evpd bits ignored
+  - sg_inq: warn -o=<n> not used for standard INQUIRY
+  - sg_turs: add -t option to time Test Unit Ready commands
+  - sg_errs: (used by most utilities) warn if sense buffer empty
+  - sg_modes: make safe with block SG_IO (bypass SG_GET_SCSI_ID ioctl)
+  - sg_logs: make safe with block SG_IO, self-test page work
+  - sg_dd: add "blksg_io=" switch to experiment with block SG_IO
+  - sg_read: now use SG_IO ioctl rather than sg write/read
+  - sginfo: fix writing parameters, check for block devices that answer
+    sg's ioctls, recognize "scd<n>" device names
+  - sg_map: stop close error report on tape devices
+  - sg_readcap: make safe with block SG_IO
+  - sg_start: make safe with block SG_IO
+  - sg_test_rwbuf: make safe with block SG_IO
+
+Changelog for sg3_utils-1.01 [20020814] [svn: 27]
+----------------------------
+  - add raw switch ("-r") to sg_inq [Martin Schwenke]
+
+Changelog for sg3_utils-1.00 [20020728] [svn: 26]
+----------------------------
+  - update sg_err [to SPC-3 T10/1416-D Rev 07 3 May 2002]
+  - "sg_inq -cl" now outputs opcode names
+  - add "continue on error" option to sg_dd
+  - add _LARGEFILE64_SOURCE _FILE_OFFSET_BITS=64 defines to Makefile
+  - drop 'gen' argument from sg_dd and friends, allow any file types
+    except scsi tape device file names
+  - treat of=/dev/null as special (skip write). Accept of=. as alias
+    for of=/dev/null
+  - decode various log pages in sg_logs
+  - add 'dio' argument to sgm_dd for testing "zero copy" copies
+
+Changelog for sg3_utils-0.99 [20020317]
+----------------------------
+  - add 'fua' and 'sync' arguments to sg_dd, sgp_dd and sgm_dd
+  - improve sg_inq, add "-cl" and "-36" arguments
+  - add sg_modes + sg_logs for MODE SENSE and LOG SENSE queries
+  - add rescan-scsi-bus.sh [Kurt Garloff] to archive directory
+
+Changelog for sg3_utils-0.98 [20020216]
+----------------------------
+  - move sg_reset back from archive to main directory + build
+  - sprintf() to snprintf() changes
+  - add "time=<n>" argument to sg_dd, sgp_dd and sgm_dd to time transfer
+  - add man pages for sgm_dd and sg_read, update sg_dd and sgp_dd man pages
+  - add "cdbsz=" argument to sg_dd, sgp_dd, sgm_dd + sg_read
+  - add "gen=0|1" argument to sg_dd
+
+Changelog for sg3_utils-0.97 [20011223]
+----------------------------
+  - move isosize to archive since introduced into util-linux-2.10s
+
+Changelog for sg3_utils-0.96 [20011221]
+----------------------------
+  - add '-p' switch to sg_inq to provide PCI slot_name
+  - add '-n' switch to scsi_inquiry for non-blocking open
+  - new sgm_dd (dd variant) using mmap-ed IO
+  - sg_rbuf now has a '-m' argument to select mmap-ed IO
+  - sg_rbuf now has a '-t' switch to do timing + throughput calculation
+  - add sg_simple4 to demonstrate mmap-ed IO on an INQUIRY response
+  - add sg_simple16 to do a READ_16 (16 byte SCSI READ command)
+  - mmap-ed IO requires sg version 3.1.22 or later
+  - add sg_read to read multiple times starting at same offset
+
+Changelog for sg3_utils-0.95 [20010915]
+----------------------------
+  - make sg_dd, sgp_dd and archive/sgq_dd warn if dio selected but
+    /sys/module/sg/parameters/allow_dio is '0'
+  - sg_map can now do any INQUIRY (when '-i' argument given)
+  - expand example in scsi_inquiry
+
+Changelog for sg3_utils-0.94 [20010419]
+----------------------------
+  - add sg_start, documented in README.sg_start [Kurt Garloff]
+  - add osst support in sg_map [Kurt Garloff]
+  - improvements to sginfo [Kurt Garloff]
+
+Changelog for sg3_utils-0.93 [20010415]
+----------------------------
+  - more include file fine tuning
+  - some "dio" work sg_rbuf
+  - extend sgp_dd so "continue on error" works on normal files
+  - introduce sg_include.h to encapsulate sg include problems
+  - add scsi_devfs_scan
+  - add sg_bus_xfer to archive directory
+  - more error info in sginfo
+
+Changelog for sg3_utils-0.92 [20010116]
+----------------------------
+  - change sg_err.c output from stdout to stderr
+  - change sg_debug to call system("cat /proc/scsi/sg/debug");
+  - fix in+out totals in sg_dd and sgp_dd when partial transfers
+  - lower include dependencies in sg_err.h
+  - add sgq_dd + Makefile to archive directory
+
+Changelog for sg3_utils-0.91 [20001221]
+----------------------------
+  - signalling handling added to sg_dd (and documented in sg_dd.8)
+  - add man page for sg_rbuf (and a small change to its code)
+  - add "-d" switch to isosize and cope with > 2 GB (and man page)
+
+Changelog for sg3_utils-0.90
+----------------------------
+  - switch from dated versioning. Previous version was sg3_utils001012.
+    Arbitrarily start at package version 0.90 . Start Changelog.
+  - incorporate Kurt Garloff's patches from Suse scsi.spm source rpm
+    compilation:
+    - add Kurt Garloff's sg_test_rwbuf utility to read and write to disk
+      buffer
+    - clean up Makefile to include a "make install" (and also add a
+      "make uninstall").
+    - add "-uno" switch to sginfo
+  - make raw and sg devices equally acceptable to sg_dd and sgp_dd.
+    [Raw devices still not as fast as sg devices doing disk to disk
+    copies in sgp_dd but this may be improved soon. Still faster than
+    using dd!]
+  - change lseek() in sg_dd and sgp_dd to _llseek() [using code borrowed
+    from fdisk] so big disks can be properly offset with 'skip' and
+    'seek' arguments. [This change is significant for raw devices and
+    normal files since sg devices already use 31 bit block addressing.]
+  - rename sg_s3_inq to sg_inq. This utility allows the INQUIRY response
+    to be decoded as per SCSI 3 and 4. Also can probe VPD and CmdDt pages.
+  - change multiplier suffixes on sg_dd, sgp_dd and sg_turs so lower case
+    "k, m, g" are powers of 2 while "K, M, G" are powers of 10. This idea
+    borrowed from lmdd (lmbench suite)
+  - retire a few more less used utilities into the archive directory.
+  - add man pages for sg_dd, sgp_dd and sg_map
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..dc471d1
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,240 @@
+Installation Instructions
+*************************
+
+Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005,
+2006, 2007 Free Software Foundation, Inc.
+
+This file is free documentation; the Free Software Foundation gives
+unlimited permission to copy, distribute and modify it.
+
+Basic Installation
+==================
+
+Briefly, the shell commands './configure; make; make install' should
+configure, build, and install this package. If that fails try
+doing './autogen.sh' first and then repeat the above sequence. The
+autogen.sh script may require some autotools packages to be loaded.
+
+The following more detailed instructions are generic; see the `README'
+file for instructions specific to this package.
+
+   The `configure' shell script attempts to guess correct values for
+various system-dependent variables used during compilation.  It uses
+those values to create a `Makefile' in each directory of the package.
+It may also create one or more `.h' files containing system-dependent
+definitions.  Finally, it creates a shell script `config.status' that
+you can run in the future to recreate the current configuration, and a
+file `config.log' containing compiler output (useful mainly for
+debugging `configure').
+
+   It can also use an optional file (typically called `config.cache'
+and enabled with `--cache-file=config.cache' or simply `-C') that saves
+the results of its tests to speed up reconfiguring.  Caching is
+disabled by default to prevent problems with accidental use of stale
+cache files.
+
+   If you need to do unusual things to compile the package, please try
+to figure out how `configure' could check whether to do them, and mail
+diffs or instructions to the address given in the `README' so they can
+be considered for the next release.  If you are using the cache, and at
+some point `config.cache' contains results you don't want to keep, you
+may remove or edit it.
+
+   The file `configure.ac' (or `configure.in') is used to create
+`configure' by a program called `autoconf'.  You need `configure.ac' if
+you want to change it or regenerate `configure' using a newer version
+of `autoconf'.
+
+The simplest way to compile this package is:
+
+  1. `cd' to the directory containing the package's source code and type
+     `./configure' to configure the package for your system.
+
+     Running `configure' might take a while.  While running, it prints
+     some messages telling which features it is checking for.
+
+  2. Type `make' to compile the package.
+
+  3. Optionally, type `make check' to run any self-tests that come with
+     the package.
+
+  4. Type `make install' to install the programs and any data files and
+     documentation.
+
+  5. You can remove the program binaries and object files from the
+     source code directory by typing `make clean'.  To also remove the
+     files that `configure' created (so you can compile the package for
+     a different kind of computer), type `make distclean'.  There is
+     also a `make maintainer-clean' target, but that is intended mainly
+     for the package's developers.  If you use it, you may have to get
+     all sorts of other programs in order to regenerate files that came
+     with the distribution.
+
+  6. Often, you can also type `make uninstall' to remove the installed
+     files again.
+
+Compilers and Options
+=====================
+
+Some systems require unusual options for compilation or linking that the
+`configure' script does not know about.  Run `./configure --help' for
+details on some of the pertinent environment variables.
+
+   You can give `configure' initial values for configuration parameters
+by setting variables in the command line or in the environment.  Here
+is an example:
+
+     ./configure CC=c99 CFLAGS=-g LIBS=-lposix
+
+   *Note Defining Variables::, for more details.
+
+Compiling For Multiple Architectures
+====================================
+
+You can compile the package for more than one kind of computer at the
+same time, by placing the object files for each architecture in their
+own directory.  To do this, you can use GNU `make'.  `cd' to the
+directory where you want the object files and executables to go and run
+the `configure' script.  `configure' automatically checks for the
+source code in the directory that `configure' is in and in `..'.
+
+   With a non-GNU `make', it is safer to compile the package for one
+architecture at a time in the source code directory.  After you have
+installed the package for one architecture, use `make distclean' before
+reconfiguring for another architecture.
+
+Installation Names
+==================
+
+By default, `make install' installs the package's commands under
+`/usr/local/bin', include files under `/usr/local/include', etc.  You
+can specify an installation prefix other than `/usr/local' by giving
+`configure' the option `--prefix=PREFIX'.
+
+   You can specify separate installation prefixes for
+architecture-specific files and architecture-independent files.  If you
+pass the option `--exec-prefix=PREFIX' to `configure', the package uses
+PREFIX as the prefix for installing programs and libraries.
+Documentation and other data files still use the regular prefix.
+
+   In addition, if you use an unusual directory layout you can give
+options like `--bindir=DIR' to specify different values for particular
+kinds of files.  Run `configure --help' for a list of the directories
+you can set and what kinds of files go in them.
+
+   If the package supports it, you can cause programs to be installed
+with an extra prefix or suffix on their names by giving `configure' the
+option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'.
+
+Optional Features
+=================
+
+Some packages pay attention to `--enable-FEATURE' options to
+`configure', where FEATURE indicates an optional part of the package.
+They may also pay attention to `--with-PACKAGE' options, where PACKAGE
+is something like `gnu-as' or `x' (for the X Window System).  The
+`README' should mention any `--enable-' and `--with-' options that the
+package recognizes.
+
+   For packages that use the X Window System, `configure' can usually
+find the X include and library files automatically, but if it doesn't,
+you can use the `configure' options `--x-includes=DIR' and
+`--x-libraries=DIR' to specify their locations.
+
+Specifying the System Type
+==========================
+
+There may be some features `configure' cannot figure out automatically,
+but needs to determine by the type of machine the package will run on.
+Usually, assuming the package is built to be run on the _same_
+architectures, `configure' can figure that out, but if it prints a
+message saying it cannot guess the machine type, give it the
+`--build=TYPE' option.  TYPE can either be a short name for the system
+type, such as `sun4', or a canonical name which has the form:
+
+     CPU-COMPANY-SYSTEM
+
+where SYSTEM can have one of these forms:
+
+     OS KERNEL-OS
+
+   See the file `config.sub' for the possible values of each field.  If
+`config.sub' isn't included in this package, then this package doesn't
+need to know the machine type.
+
+   If you are _building_ compiler tools for cross-compiling, you should
+use the option `--target=TYPE' to select the type of system they will
+produce code for.
+
+   If you want to _use_ a cross compiler, that generates code for a
+platform different from the build platform, you should specify the
+"host" platform (i.e., that on which the generated programs will
+eventually be run) with `--host=TYPE'.
+
+Sharing Defaults
+================
+
+If you want to set default values for `configure' scripts to share, you
+can create a site shell script called `config.site' that gives default
+values for variables like `CC', `cache_file', and `prefix'.
+`configure' looks for `PREFIX/share/config.site' if it exists, then
+`PREFIX/etc/config.site' if it exists.  Or, you can set the
+`CONFIG_SITE' environment variable to the location of the site script.
+A warning: not all `configure' scripts look for a site script.
+
+Defining Variables
+==================
+
+Variables not defined in a site shell script can be set in the
+environment passed to `configure'.  However, some packages may run
+configure again during the build, and the customized values of these
+variables may be lost.  In order to avoid this problem, you should set
+them in the `configure' command line, using `VAR=value'.  For example:
+
+     ./configure CC=/usr/local2/bin/gcc
+
+causes the specified `gcc' to be used as the C compiler (unless it is
+overridden in the site shell script).
+
+Unfortunately, this technique does not work for `CONFIG_SHELL' due to
+an Autoconf bug.  Until the bug is fixed you can use this workaround:
+
+     CONFIG_SHELL=/bin/bash /bin/bash ./configure CONFIG_SHELL=/bin/bash
+
+`configure' Invocation
+======================
+
+`configure' recognizes the following options to control how it operates.
+
+`--help'
+`-h'
+     Print a summary of the options to `configure', and exit.
+
+`--version'
+`-V'
+     Print the version of Autoconf used to generate the `configure'
+     script, and exit.
+
+`--cache-file=FILE'
+     Enable the cache: use and save the results of the tests in FILE,
+     traditionally `config.cache'.  FILE defaults to `/dev/null' to
+     disable caching.
+
+`--config-cache'
+`-C'
+     Alias for `--cache-file=config.cache'.
+
+`--quiet'
+`--silent'
+`-q'
+     Do not print messages saying which checks are being made.  To
+     suppress all normal output, redirect it to `/dev/null' (any error
+     messages will still be shown).
+
+`--srcdir=DIR'
+     Look for the package's source code in directory DIR.  Usually
+     `configure' can determine that directory automatically.
+
+`configure' also accepts some other, not widely useful, options.  Run
+`configure --help' for more details.
+
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..240acbe
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,184 @@
+
+SUBDIRS = \
+	include \
+	lib \
+	src \
+	doc \
+	scripts
+
+EXTRA_DIST = \
+	autogen.sh \
+	COVERAGE \
+	CREDITS \
+	BSD_LICENSE \
+	build_debian.sh \
+	README.details \
+	README.freebsd \
+	README.iscsi \
+	README.sg_start \
+	README.solaris \
+	README.tru64 \
+	README.win32 \
+	sg3_utils.man8.html \
+	sg3_utils.spec
+
+EXTRA_DIST += \
+	archive/align_b4_memalign.c \
+	archive/llseek.c \
+	archive/llseek.h \
+	archive/o_scsi_logging_level \
+	archive/README \
+	archive/sg_json_writer.c \
+	archive/sg_json_writer.h
+
+EXTRA_DIST += \
+	debian/changelog \
+	debian/compat \
+	debian/control \
+	debian/copyright \
+	debian/docs \
+	debian/libsgutils2-2.install \
+	debian/libsgutils2-dev.install \
+	debian/README.debian4 \
+	debian/rules \
+	debian/sg3-utils.examples \
+	debian/sg3-utils.install
+
+EXTRA_DIST += \
+	examples/Makefile.freebsd \
+	examples/README \
+	examples/reassign_addr.txt \
+	examples/scsi_inquiry.c \
+	examples/sdiag_sas_p0_cjtpat.txt \
+	examples/sdiag_sas_p0_prbs9.txt \
+	examples/sdiag_sas_p1_cjtpat.txt \
+	examples/sdiag_sas_p1_idle.txt \
+	examples/sdiag_sas_p1_prbs15.txt \
+	examples/sdiag_sas_p1_stop.txt \
+	examples/sg_compare_and_write.txt \
+	examples/sg_excl.c \
+	examples/sg_persist_tst.sh \
+	examples/sgq_dd.c \
+	examples/sg_sat_chk_power.c \
+	examples/sg__sat_identify.c \
+	examples/sg__sat_phy_event.c \
+	examples/sg__sat_set_features.c \
+	examples/sg_sat_smart_rd_data.c \
+	examples/sg_simple16.c \
+	examples/sg_simple1.c \
+	examples/sg_simple2.c \
+	examples/sg_simple3.c \
+	examples/sg_simple4.c \
+	examples/sg_simple5.c \
+	examples/sg_unmap_example.txt \
+	examples/transport_ids.txt \
+	examples/Makefile
+
+EXTRA_DIST += \
+	getopt_long/getopt.h \
+	getopt_long/getopt_long.c
+
+EXTRA_DIST += \
+	include/freebsd_nvme_ioctl.h
+
+EXTRA_DIST += \
+	inhex/descriptor_sense.hex \
+	inhex/fixed_sense.hex \
+	inhex/forwarded_sense.hex \
+	inhex/get_elem_status.hex \
+	inhex/get_lba_status.hex \
+	inhex/inq_standard.hex \
+	inhex/logs_last_n.hex \
+	inhex/nvme_dev_self_test.hex \
+	inhex/nvme_identify_ctl.hex \
+	inhex/nvme_read_ctl.hex \
+	inhex/nvme_read_oob_ctl.hex \
+	inhex/nvme_write_ctl.hex \
+	inhex/opcodes.hex \
+	inhex/README \
+	inhex/ref_sense.hex \
+	inhex/rep_density.hex \
+	inhex/rep_density_media.hex \
+	inhex/rep_density_media_typem.hex \
+	inhex/rep_density_typem.hex \
+	inhex/rep_realms.hex \
+	inhex/rep_zdomains.hex \
+	inhex/rep_zones.hex \
+	inhex/ses_areca_all.hex \
+	inhex/vpd_bdce.hex \
+	inhex/vpd_consistuents.hex \
+	inhex/vpd_cpr.hex \
+	inhex/vpd_dev_id.hex \
+	inhex/vpd_di_all.hex \
+	inhex/vpd_fp.hex \
+	inhex/vpd_lbpro.hex \
+	inhex/vpd_lbpv.hex \
+	inhex/vpd_ref.hex \
+	inhex/vpd_sbl.hex \
+	inhex/vpd_sdeb.hex \
+	inhex/vpd_sfs.hex \
+	inhex/vpd_tpc.hex \
+	inhex/vpd_zbdc.hex \
+	inhex/vpd_zbdc.raw \
+	inhex/z_act_query.hex
+
+EXTRA_DIST += \
+	scripts/40-usb-blacklist.rules \
+	scripts/54-before-scsi-sg3_id.rules \
+	scripts/55-scsi-sg3_id.rules \
+	scripts/58-scsi-sg3_symlink.rules \
+	scripts/59-fc-wwpn-id.rules \
+	scripts/59-scsi-cciss_id.rules \
+	scripts/cciss_id \
+	scripts/fc_wwpn_id \
+	scripts/lunmask.service \
+	scripts/scsi-enable-target-scan.sh
+
+EXTRA_DIST += \
+	suse/sg3_utils.changes \
+	suse/sg3_utils.spec
+
+EXTRA_DIST += \
+	testing/bsg_queue_tst.c \
+	testing/Makefile \
+	testing/Makefile.cyg \
+	testing/Makefile.freebsd \
+	testing/README \
+	testing/sg_chk_asc.c \
+	testing/sgh_dd.cpp \
+	testing/sg_iovec_tst.cpp \
+	testing/sg_json_builder_test.c \
+	testing/sg_mrq_dd.cpp \
+	testing/sg_queue_tst.c \
+	testing/sg_scat_gath.cpp \
+	testing/sg_scat_gath.h \
+	testing/sgs_dd.c \
+	testing/sg_sense_test.c \
+	testing/sg_take_snap.c \
+	testing/sg_tst_async.cpp \
+	testing/sg_tst_bidi.c \
+	testing/sg_tst_context.cpp \
+	testing/sg_tst_excl2.cpp \
+	testing/sg_tst_excl3.cpp \
+	testing/sg_tst_excl.cpp \
+	testing/sg_tst_ioctl.c \
+	testing/sg_tst_json_builder.c \
+	testing/sg_tst_nvme.c \
+	testing/tst_sg_lib.c \
+	testing/uapi_sg.h
+
+EXTRA_DIST += \
+	utils/hxascdmp.1 \
+	utils/hxascdmp.c \
+	utils/Makefile \
+	utils/Makefile.cygwin \
+	utils/Makefile.freebsd \
+	utils/Makefile.mingw \
+	utils/Makefile.solaris \
+	utils/README
+
+distclean-local:
+	rm -rf autom4te.cache
+	rm -f build-stamp configure-stamp
+	rm -rf lib/.deps
+	rm -rf src/.deps
diff --git a/Makefile.in b/Makefile.in
new file mode 100644
index 0000000..b3722f3
--- /dev/null
+++ b/Makefile.in
@@ -0,0 +1,903 @@
+# Makefile.in generated by automake 1.16.5 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2021 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+VPATH = @srcdir@
+am__is_gnu_make = { \
+  if test -z '$(MAKELEVEL)'; then \
+    false; \
+  elif test -n '$(MAKE_HOST)'; then \
+    true; \
+  elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+    true; \
+  else \
+    false; \
+  fi; \
+}
+am__make_running_with_option = \
+  case $${target_option-} in \
+      ?) ;; \
+      *) echo "am__make_running_with_option: internal error: invalid" \
+              "target option '$${target_option-}' specified" >&2; \
+         exit 1;; \
+  esac; \
+  has_opt=no; \
+  sane_makeflags=$$MAKEFLAGS; \
+  if $(am__is_gnu_make); then \
+    sane_makeflags=$$MFLAGS; \
+  else \
+    case $$MAKEFLAGS in \
+      *\\[\ \	]*) \
+        bs=\\; \
+        sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+          | sed "s/$$bs$$bs[$$bs $$bs	]*//g"`;; \
+    esac; \
+  fi; \
+  skip_next=no; \
+  strip_trailopt () \
+  { \
+    flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+  }; \
+  for flg in $$sane_makeflags; do \
+    test $$skip_next = yes && { skip_next=no; continue; }; \
+    case $$flg in \
+      *=*|--*) continue;; \
+        -*I) strip_trailopt 'I'; skip_next=yes;; \
+      -*I?*) strip_trailopt 'I';; \
+        -*O) strip_trailopt 'O'; skip_next=yes;; \
+      -*O?*) strip_trailopt 'O';; \
+        -*l) strip_trailopt 'l'; skip_next=yes;; \
+      -*l?*) strip_trailopt 'l';; \
+      -[dEDm]) skip_next=yes;; \
+      -[JT]) skip_next=yes;; \
+    esac; \
+    case $$flg in \
+      *$$target_option*) has_opt=yes; break;; \
+    esac; \
+  done; \
+  test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = .
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+	$(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(top_srcdir)/configure \
+	$(am__configure_deps) $(am__DIST_COMMON)
+am__CONFIG_DISTCLEAN_FILES = config.status config.cache config.log \
+ configure.lineno config.status.lineno
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo "  GEN     " $@;
+am__v_GEN_1 = 
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 = 
+SOURCES =
+DIST_SOURCES =
+RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \
+	ctags-recursive dvi-recursive html-recursive info-recursive \
+	install-data-recursive install-dvi-recursive \
+	install-exec-recursive install-html-recursive \
+	install-info-recursive install-pdf-recursive \
+	install-ps-recursive install-recursive installcheck-recursive \
+	installdirs-recursive pdf-recursive ps-recursive \
+	tags-recursive uninstall-recursive
+am__can_run_installinfo = \
+  case $$AM_UPDATE_INFO_DIR in \
+    n|no|NO) false;; \
+    *) (install-info --version) >/dev/null 2>&1;; \
+  esac
+RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive	\
+  distclean-recursive maintainer-clean-recursive
+am__recursive_targets = \
+  $(RECURSIVE_TARGETS) \
+  $(RECURSIVE_CLEAN_TARGETS) \
+  $(am__extra_recursive_targets)
+AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \
+	cscope distdir distdir-am dist dist-all distcheck
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) \
+	config.h.in
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates.  Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+  BEGIN { nonempty = 0; } \
+  { items[$$0] = 1; nonempty = 1; } \
+  END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique.  This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+  list='$(am__tagged_files)'; \
+  unique=`for i in $$list; do \
+    if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+  done | $(am__uniquify_input)`
+DIST_SUBDIRS = $(SUBDIRS)
+am__DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/config.h.in AUTHORS \
+	COPYING ChangeLog INSTALL NEWS README ar-lib compile \
+	config.guess config.sub install-sh ltmain.sh missing
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+distdir = $(PACKAGE)-$(VERSION)
+top_distdir = $(distdir)
+am__remove_distdir = \
+  if test -d "$(distdir)"; then \
+    find "$(distdir)" -type d ! -perm -200 -exec chmod u+w {} ';' \
+      && rm -rf "$(distdir)" \
+      || { sleep 5 && rm -rf "$(distdir)"; }; \
+  else :; fi
+am__post_remove_distdir = $(am__remove_distdir)
+am__relativize = \
+  dir0=`pwd`; \
+  sed_first='s,^\([^/]*\)/.*$$,\1,'; \
+  sed_rest='s,^[^/]*/*,,'; \
+  sed_last='s,^.*/\([^/]*\)$$,\1,'; \
+  sed_butlast='s,/*[^/]*$$,,'; \
+  while test -n "$$dir1"; do \
+    first=`echo "$$dir1" | sed -e "$$sed_first"`; \
+    if test "$$first" != "."; then \
+      if test "$$first" = ".."; then \
+        dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \
+        dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \
+      else \
+        first2=`echo "$$dir2" | sed -e "$$sed_first"`; \
+        if test "$$first2" = "$$first"; then \
+          dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \
+        else \
+          dir2="../$$dir2"; \
+        fi; \
+        dir0="$$dir0"/"$$first"; \
+      fi; \
+    fi; \
+    dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \
+  done; \
+  reldir="$$dir2"
+DIST_ARCHIVES = $(distdir).tar.gz
+GZIP_ENV = --best
+DIST_TARGETS = dist-gzip
+# Exists only to be overridden by the user if desired.
+AM_DISTCHECK_DVI_TARGET = dvi
+distuninstallcheck_listfiles = find . -type f -print
+am__distuninstallcheck_listfiles = $(distuninstallcheck_listfiles) \
+  | sed 's|^\./|$(prefix)/|' | grep -v '$(infodir)/dir$$'
+distcleancheck_listfiles = find . -type f -print
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CSCOPE = @CSCOPE@
+CTAGS = @CTAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+ETAGS = @ETAGS@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FILECMD = @FILECMD@
+GETOPT_O_FILES = @GETOPT_O_FILES@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PTHREAD_LIB = @PTHREAD_LIB@
+RANLIB = @RANLIB@
+RT_LIB = @RT_LIB@
+SED = @SED@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+STRIP = @STRIP@
+VERSION = @VERSION@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+os_cflags = @os_cflags@
+os_libs = @os_libs@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+SUBDIRS = \
+	include \
+	lib \
+	src \
+	doc \
+	scripts
+
+EXTRA_DIST = autogen.sh COVERAGE CREDITS BSD_LICENSE build_debian.sh \
+	README.details README.freebsd README.iscsi README.sg_start \
+	README.solaris README.tru64 README.win32 sg3_utils.man8.html \
+	sg3_utils.spec archive/align_b4_memalign.c archive/llseek.c \
+	archive/llseek.h archive/o_scsi_logging_level archive/README \
+	archive/sg_json_writer.c archive/sg_json_writer.h \
+	debian/changelog debian/compat debian/control debian/copyright \
+	debian/docs debian/libsgutils2-2.install \
+	debian/libsgutils2-dev.install debian/README.debian4 \
+	debian/rules debian/sg3-utils.examples \
+	debian/sg3-utils.install examples/Makefile.freebsd \
+	examples/README examples/reassign_addr.txt \
+	examples/scsi_inquiry.c examples/sdiag_sas_p0_cjtpat.txt \
+	examples/sdiag_sas_p0_prbs9.txt \
+	examples/sdiag_sas_p1_cjtpat.txt \
+	examples/sdiag_sas_p1_idle.txt \
+	examples/sdiag_sas_p1_prbs15.txt \
+	examples/sdiag_sas_p1_stop.txt \
+	examples/sg_compare_and_write.txt examples/sg_excl.c \
+	examples/sg_persist_tst.sh examples/sgq_dd.c \
+	examples/sg_sat_chk_power.c examples/sg__sat_identify.c \
+	examples/sg__sat_phy_event.c examples/sg__sat_set_features.c \
+	examples/sg_sat_smart_rd_data.c examples/sg_simple16.c \
+	examples/sg_simple1.c examples/sg_simple2.c \
+	examples/sg_simple3.c examples/sg_simple4.c \
+	examples/sg_simple5.c examples/sg_unmap_example.txt \
+	examples/transport_ids.txt examples/Makefile \
+	getopt_long/getopt.h getopt_long/getopt_long.c \
+	include/freebsd_nvme_ioctl.h inhex/descriptor_sense.hex \
+	inhex/fixed_sense.hex inhex/forwarded_sense.hex \
+	inhex/get_elem_status.hex inhex/get_lba_status.hex \
+	inhex/inq_standard.hex inhex/logs_last_n.hex \
+	inhex/nvme_dev_self_test.hex inhex/nvme_identify_ctl.hex \
+	inhex/nvme_read_ctl.hex inhex/nvme_read_oob_ctl.hex \
+	inhex/nvme_write_ctl.hex inhex/opcodes.hex inhex/README \
+	inhex/ref_sense.hex inhex/rep_density.hex \
+	inhex/rep_density_media.hex inhex/rep_density_media_typem.hex \
+	inhex/rep_density_typem.hex inhex/rep_realms.hex \
+	inhex/rep_zdomains.hex inhex/rep_zones.hex \
+	inhex/ses_areca_all.hex inhex/vpd_bdce.hex \
+	inhex/vpd_consistuents.hex inhex/vpd_cpr.hex \
+	inhex/vpd_dev_id.hex inhex/vpd_di_all.hex inhex/vpd_fp.hex \
+	inhex/vpd_lbpro.hex inhex/vpd_lbpv.hex inhex/vpd_ref.hex \
+	inhex/vpd_sbl.hex inhex/vpd_sdeb.hex inhex/vpd_sfs.hex \
+	inhex/vpd_tpc.hex inhex/vpd_zbdc.hex inhex/vpd_zbdc.raw \
+	inhex/z_act_query.hex scripts/40-usb-blacklist.rules \
+	scripts/54-before-scsi-sg3_id.rules \
+	scripts/55-scsi-sg3_id.rules scripts/58-scsi-sg3_symlink.rules \
+	scripts/59-fc-wwpn-id.rules scripts/59-scsi-cciss_id.rules \
+	scripts/cciss_id scripts/fc_wwpn_id scripts/lunmask.service \
+	scripts/scsi-enable-target-scan.sh suse/sg3_utils.changes \
+	suse/sg3_utils.spec testing/bsg_queue_tst.c testing/Makefile \
+	testing/Makefile.cyg testing/Makefile.freebsd testing/README \
+	testing/sg_chk_asc.c testing/sgh_dd.cpp \
+	testing/sg_iovec_tst.cpp testing/sg_json_builder_test.c \
+	testing/sg_mrq_dd.cpp testing/sg_queue_tst.c \
+	testing/sg_scat_gath.cpp testing/sg_scat_gath.h \
+	testing/sgs_dd.c testing/sg_sense_test.c \
+	testing/sg_take_snap.c testing/sg_tst_async.cpp \
+	testing/sg_tst_bidi.c testing/sg_tst_context.cpp \
+	testing/sg_tst_excl2.cpp testing/sg_tst_excl3.cpp \
+	testing/sg_tst_excl.cpp testing/sg_tst_ioctl.c \
+	testing/sg_tst_json_builder.c testing/sg_tst_nvme.c \
+	testing/tst_sg_lib.c testing/uapi_sg.h utils/hxascdmp.1 \
+	utils/hxascdmp.c utils/Makefile utils/Makefile.cygwin \
+	utils/Makefile.freebsd utils/Makefile.mingw \
+	utils/Makefile.solaris utils/README
+all: config.h
+	$(MAKE) $(AM_MAKEFLAGS) all-recursive
+
+.SUFFIXES:
+am--refresh: Makefile
+	@:
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am  $(am__configure_deps)
+	@for dep in $?; do \
+	  case '$(am__configure_deps)' in \
+	    *$$dep*) \
+	      echo ' cd $(srcdir) && $(AUTOMAKE) --foreign'; \
+	      $(am__cd) $(srcdir) && $(AUTOMAKE) --foreign \
+		&& exit 0; \
+	      exit 1;; \
+	  esac; \
+	done; \
+	echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign Makefile'; \
+	$(am__cd) $(top_srcdir) && \
+	  $(AUTOMAKE) --foreign Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+	@case '$?' in \
+	  *config.status*) \
+	    echo ' $(SHELL) ./config.status'; \
+	    $(SHELL) ./config.status;; \
+	  *) \
+	    echo ' cd $(top_builddir) && $(SHELL) ./config.status $@ $(am__maybe_remake_depfiles)'; \
+	    cd $(top_builddir) && $(SHELL) ./config.status $@ $(am__maybe_remake_depfiles);; \
+	esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+	$(SHELL) ./config.status --recheck
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+	$(am__cd) $(srcdir) && $(AUTOCONF)
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+	$(am__cd) $(srcdir) && $(ACLOCAL) $(ACLOCAL_AMFLAGS)
+$(am__aclocal_m4_deps):
+
+config.h: stamp-h1
+	@test -f $@ || rm -f stamp-h1
+	@test -f $@ || $(MAKE) $(AM_MAKEFLAGS) stamp-h1
+
+stamp-h1: $(srcdir)/config.h.in $(top_builddir)/config.status
+	@rm -f stamp-h1
+	cd $(top_builddir) && $(SHELL) ./config.status config.h
+$(srcdir)/config.h.in: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) 
+	($(am__cd) $(top_srcdir) && $(AUTOHEADER))
+	rm -f stamp-h1
+	touch $@
+
+distclean-hdr:
+	-rm -f config.h stamp-h1
+
+mostlyclean-libtool:
+	-rm -f *.lo
+
+clean-libtool:
+	-rm -rf .libs _libs
+
+distclean-libtool:
+	-rm -f libtool config.lt
+
+# This directory's subdirectories are mostly independent; you can cd
+# into them and run 'make' without going through this Makefile.
+# To change the values of 'make' variables: instead of editing Makefiles,
+# (1) if the variable is set in 'config.status', edit 'config.status'
+#     (which will cause the Makefiles to be regenerated when you run 'make');
+# (2) otherwise, pass the desired values on the 'make' command line.
+$(am__recursive_targets):
+	@fail=; \
+	if $(am__make_keepgoing); then \
+	  failcom='fail=yes'; \
+	else \
+	  failcom='exit 1'; \
+	fi; \
+	dot_seen=no; \
+	target=`echo $@ | sed s/-recursive//`; \
+	case "$@" in \
+	  distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \
+	  *) list='$(SUBDIRS)' ;; \
+	esac; \
+	for subdir in $$list; do \
+	  echo "Making $$target in $$subdir"; \
+	  if test "$$subdir" = "."; then \
+	    dot_seen=yes; \
+	    local_target="$$target-am"; \
+	  else \
+	    local_target="$$target"; \
+	  fi; \
+	  ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \
+	  || eval $$failcom; \
+	done; \
+	if test "$$dot_seen" = "no"; then \
+	  $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \
+	fi; test -z "$$fail"
+
+ID: $(am__tagged_files)
+	$(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-recursive
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+	set x; \
+	here=`pwd`; \
+	if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \
+	  include_option=--etags-include; \
+	  empty_fix=.; \
+	else \
+	  include_option=--include; \
+	  empty_fix=; \
+	fi; \
+	list='$(SUBDIRS)'; for subdir in $$list; do \
+	  if test "$$subdir" = .; then :; else \
+	    test ! -f $$subdir/TAGS || \
+	      set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \
+	  fi; \
+	done; \
+	$(am__define_uniq_tagged_files); \
+	shift; \
+	if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+	  test -n "$$unique" || unique=$$empty_fix; \
+	  if test $$# -gt 0; then \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      "$$@" $$unique; \
+	  else \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      $$unique; \
+	  fi; \
+	fi
+ctags: ctags-recursive
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+	$(am__define_uniq_tagged_files); \
+	test -z "$(CTAGS_ARGS)$$unique" \
+	  || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+	     $$unique
+
+GTAGS:
+	here=`$(am__cd) $(top_builddir) && pwd` \
+	  && $(am__cd) $(top_srcdir) \
+	  && gtags -i $(GTAGS_ARGS) "$$here"
+cscope: cscope.files
+	test ! -s cscope.files \
+	  || $(CSCOPE) -b -q $(AM_CSCOPEFLAGS) $(CSCOPEFLAGS) -i cscope.files $(CSCOPE_ARGS)
+clean-cscope:
+	-rm -f cscope.files
+cscope.files: clean-cscope cscopelist
+cscopelist: cscopelist-recursive
+
+cscopelist-am: $(am__tagged_files)
+	list='$(am__tagged_files)'; \
+	case "$(srcdir)" in \
+	  [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+	  *) sdir=$(subdir)/$(srcdir) ;; \
+	esac; \
+	for i in $$list; do \
+	  if test -f "$$i"; then \
+	    echo "$(subdir)/$$i"; \
+	  else \
+	    echo "$$sdir/$$i"; \
+	  fi; \
+	done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+	-rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+	-rm -f cscope.out cscope.in.out cscope.po.out cscope.files
+distdir: $(BUILT_SOURCES)
+	$(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+	$(am__remove_distdir)
+	test -d "$(distdir)" || mkdir "$(distdir)"
+	@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	list='$(DISTFILES)'; \
+	  dist_files=`for file in $$list; do echo $$file; done | \
+	  sed -e "s|^$$srcdirstrip/||;t" \
+	      -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+	case $$dist_files in \
+	  */*) $(MKDIR_P) `echo "$$dist_files" | \
+			   sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+			   sort -u` ;; \
+	esac; \
+	for file in $$dist_files; do \
+	  if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+	  if test -d $$d/$$file; then \
+	    dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+	    if test -d "$(distdir)/$$file"; then \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+	      cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+	  else \
+	    test -f "$(distdir)/$$file" \
+	    || cp -p $$d/$$file "$(distdir)/$$file" \
+	    || exit 1; \
+	  fi; \
+	done
+	@list='$(DIST_SUBDIRS)'; for subdir in $$list; do \
+	  if test "$$subdir" = .; then :; else \
+	    $(am__make_dryrun) \
+	      || test -d "$(distdir)/$$subdir" \
+	      || $(MKDIR_P) "$(distdir)/$$subdir" \
+	      || exit 1; \
+	    dir1=$$subdir; dir2="$(distdir)/$$subdir"; \
+	    $(am__relativize); \
+	    new_distdir=$$reldir; \
+	    dir1=$$subdir; dir2="$(top_distdir)"; \
+	    $(am__relativize); \
+	    new_top_distdir=$$reldir; \
+	    echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \
+	    echo "     am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \
+	    ($(am__cd) $$subdir && \
+	      $(MAKE) $(AM_MAKEFLAGS) \
+	        top_distdir="$$new_top_distdir" \
+	        distdir="$$new_distdir" \
+		am__remove_distdir=: \
+		am__skip_length_check=: \
+		am__skip_mode_fix=: \
+	        distdir) \
+	      || exit 1; \
+	  fi; \
+	done
+	-test -n "$(am__skip_mode_fix)" \
+	|| find "$(distdir)" -type d ! -perm -755 \
+		-exec chmod u+rwx,go+rx {} \; -o \
+	  ! -type d ! -perm -444 -links 1 -exec chmod a+r {} \; -o \
+	  ! -type d ! -perm -400 -exec chmod a+r {} \; -o \
+	  ! -type d ! -perm -444 -exec $(install_sh) -c -m a+r {} {} \; \
+	|| chmod -R a+r "$(distdir)"
+dist-gzip: distdir
+	tardir=$(distdir) && $(am__tar) | eval GZIP= gzip $(GZIP_ENV) -c >$(distdir).tar.gz
+	$(am__post_remove_distdir)
+
+dist-bzip2: distdir
+	tardir=$(distdir) && $(am__tar) | BZIP2=$${BZIP2--9} bzip2 -c >$(distdir).tar.bz2
+	$(am__post_remove_distdir)
+
+dist-lzip: distdir
+	tardir=$(distdir) && $(am__tar) | lzip -c $${LZIP_OPT--9} >$(distdir).tar.lz
+	$(am__post_remove_distdir)
+
+dist-xz: distdir
+	tardir=$(distdir) && $(am__tar) | XZ_OPT=$${XZ_OPT--e} xz -c >$(distdir).tar.xz
+	$(am__post_remove_distdir)
+
+dist-zstd: distdir
+	tardir=$(distdir) && $(am__tar) | zstd -c $${ZSTD_CLEVEL-$${ZSTD_OPT--19}} >$(distdir).tar.zst
+	$(am__post_remove_distdir)
+
+dist-tarZ: distdir
+	@echo WARNING: "Support for distribution archives compressed with" \
+		       "legacy program 'compress' is deprecated." >&2
+	@echo WARNING: "It will be removed altogether in Automake 2.0" >&2
+	tardir=$(distdir) && $(am__tar) | compress -c >$(distdir).tar.Z
+	$(am__post_remove_distdir)
+
+dist-shar: distdir
+	@echo WARNING: "Support for shar distribution archives is" \
+	               "deprecated." >&2
+	@echo WARNING: "It will be removed altogether in Automake 2.0" >&2
+	shar $(distdir) | eval GZIP= gzip $(GZIP_ENV) -c >$(distdir).shar.gz
+	$(am__post_remove_distdir)
+
+dist-zip: distdir
+	-rm -f $(distdir).zip
+	zip -rq $(distdir).zip $(distdir)
+	$(am__post_remove_distdir)
+
+dist dist-all:
+	$(MAKE) $(AM_MAKEFLAGS) $(DIST_TARGETS) am__post_remove_distdir='@:'
+	$(am__post_remove_distdir)
+
+# This target untars the dist file and tries a VPATH configuration.  Then
+# it guarantees that the distribution is self-contained by making another
+# tarfile.
+distcheck: dist
+	case '$(DIST_ARCHIVES)' in \
+	*.tar.gz*) \
+	  eval GZIP= gzip $(GZIP_ENV) -dc $(distdir).tar.gz | $(am__untar) ;;\
+	*.tar.bz2*) \
+	  bzip2 -dc $(distdir).tar.bz2 | $(am__untar) ;;\
+	*.tar.lz*) \
+	  lzip -dc $(distdir).tar.lz | $(am__untar) ;;\
+	*.tar.xz*) \
+	  xz -dc $(distdir).tar.xz | $(am__untar) ;;\
+	*.tar.Z*) \
+	  uncompress -c $(distdir).tar.Z | $(am__untar) ;;\
+	*.shar.gz*) \
+	  eval GZIP= gzip $(GZIP_ENV) -dc $(distdir).shar.gz | unshar ;;\
+	*.zip*) \
+	  unzip $(distdir).zip ;;\
+	*.tar.zst*) \
+	  zstd -dc $(distdir).tar.zst | $(am__untar) ;;\
+	esac
+	chmod -R a-w $(distdir)
+	chmod u+w $(distdir)
+	mkdir $(distdir)/_build $(distdir)/_build/sub $(distdir)/_inst
+	chmod a-w $(distdir)
+	test -d $(distdir)/_build || exit 0; \
+	dc_install_base=`$(am__cd) $(distdir)/_inst && pwd | sed -e 's,^[^:\\/]:[\\/],/,'` \
+	  && dc_destdir="$${TMPDIR-/tmp}/am-dc-$$$$/" \
+	  && am__cwd=`pwd` \
+	  && $(am__cd) $(distdir)/_build/sub \
+	  && ../../configure \
+	    $(AM_DISTCHECK_CONFIGURE_FLAGS) \
+	    $(DISTCHECK_CONFIGURE_FLAGS) \
+	    --srcdir=../.. --prefix="$$dc_install_base" \
+	  && $(MAKE) $(AM_MAKEFLAGS) \
+	  && $(MAKE) $(AM_MAKEFLAGS) $(AM_DISTCHECK_DVI_TARGET) \
+	  && $(MAKE) $(AM_MAKEFLAGS) check \
+	  && $(MAKE) $(AM_MAKEFLAGS) install \
+	  && $(MAKE) $(AM_MAKEFLAGS) installcheck \
+	  && $(MAKE) $(AM_MAKEFLAGS) uninstall \
+	  && $(MAKE) $(AM_MAKEFLAGS) distuninstallcheck_dir="$$dc_install_base" \
+	        distuninstallcheck \
+	  && chmod -R a-w "$$dc_install_base" \
+	  && ({ \
+	       (cd ../.. && umask 077 && mkdir "$$dc_destdir") \
+	       && $(MAKE) $(AM_MAKEFLAGS) DESTDIR="$$dc_destdir" install \
+	       && $(MAKE) $(AM_MAKEFLAGS) DESTDIR="$$dc_destdir" uninstall \
+	       && $(MAKE) $(AM_MAKEFLAGS) DESTDIR="$$dc_destdir" \
+	            distuninstallcheck_dir="$$dc_destdir" distuninstallcheck; \
+	      } || { rm -rf "$$dc_destdir"; exit 1; }) \
+	  && rm -rf "$$dc_destdir" \
+	  && $(MAKE) $(AM_MAKEFLAGS) dist \
+	  && rm -rf $(DIST_ARCHIVES) \
+	  && $(MAKE) $(AM_MAKEFLAGS) distcleancheck \
+	  && cd "$$am__cwd" \
+	  || exit 1
+	$(am__post_remove_distdir)
+	@(echo "$(distdir) archives ready for distribution: "; \
+	  list='$(DIST_ARCHIVES)'; for i in $$list; do echo $$i; done) | \
+	  sed -e 1h -e 1s/./=/g -e 1p -e 1x -e '$$p' -e '$$x'
+distuninstallcheck:
+	@test -n '$(distuninstallcheck_dir)' || { \
+	  echo 'ERROR: trying to run $@ with an empty' \
+	       '$$(distuninstallcheck_dir)' >&2; \
+	  exit 1; \
+	}; \
+	$(am__cd) '$(distuninstallcheck_dir)' || { \
+	  echo 'ERROR: cannot chdir into $(distuninstallcheck_dir)' >&2; \
+	  exit 1; \
+	}; \
+	test `$(am__distuninstallcheck_listfiles) | wc -l` -eq 0 \
+	   || { echo "ERROR: files left after uninstall:" ; \
+	        if test -n "$(DESTDIR)"; then \
+	          echo "  (check DESTDIR support)"; \
+	        fi ; \
+	        $(distuninstallcheck_listfiles) ; \
+	        exit 1; } >&2
+distcleancheck: distclean
+	@if test '$(srcdir)' = . ; then \
+	  echo "ERROR: distcleancheck can only run from a VPATH build" ; \
+	  exit 1 ; \
+	fi
+	@test `$(distcleancheck_listfiles) | wc -l` -eq 0 \
+	  || { echo "ERROR: files left in build directory after distclean:" ; \
+	       $(distcleancheck_listfiles) ; \
+	       exit 1; } >&2
+check-am: all-am
+check: check-recursive
+all-am: Makefile config.h
+installdirs: installdirs-recursive
+installdirs-am:
+install: install-recursive
+install-exec: install-exec-recursive
+install-data: install-data-recursive
+uninstall: uninstall-recursive
+
+install-am: all-am
+	@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-recursive
+install-strip:
+	if test -z '$(STRIP)'; then \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	      install; \
+	else \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	    "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+	fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+	-test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+	-test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+	@echo "This command is intended for maintainers to use"
+	@echo "it deletes files that may require special tools to rebuild."
+clean: clean-recursive
+
+clean-am: clean-generic clean-libtool mostlyclean-am
+
+distclean: distclean-recursive
+	-rm -f $(am__CONFIG_DISTCLEAN_FILES)
+	-rm -f Makefile
+distclean-am: clean-am distclean-generic distclean-hdr \
+	distclean-libtool distclean-local distclean-tags
+
+dvi: dvi-recursive
+
+dvi-am:
+
+html: html-recursive
+
+html-am:
+
+info: info-recursive
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-recursive
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-recursive
+
+install-html-am:
+
+install-info: install-info-recursive
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-recursive
+
+install-pdf-am:
+
+install-ps: install-ps-recursive
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-recursive
+	-rm -f $(am__CONFIG_DISTCLEAN_FILES)
+	-rm -rf $(top_srcdir)/autom4te.cache
+	-rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-recursive
+
+mostlyclean-am: mostlyclean-generic mostlyclean-libtool
+
+pdf: pdf-recursive
+
+pdf-am:
+
+ps: ps-recursive
+
+ps-am:
+
+uninstall-am:
+
+.MAKE: $(am__recursive_targets) all install-am install-strip
+
+.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am \
+	am--refresh check check-am clean clean-cscope clean-generic \
+	clean-libtool cscope cscopelist-am ctags ctags-am dist \
+	dist-all dist-bzip2 dist-gzip dist-lzip dist-shar dist-tarZ \
+	dist-xz dist-zip dist-zstd distcheck distclean \
+	distclean-generic distclean-hdr distclean-libtool \
+	distclean-local distclean-tags distcleancheck distdir \
+	distuninstallcheck dvi dvi-am html html-am info info-am \
+	install install-am install-data install-data-am install-dvi \
+	install-dvi-am install-exec install-exec-am install-html \
+	install-html-am install-info install-info-am install-man \
+	install-pdf install-pdf-am install-ps install-ps-am \
+	install-strip installcheck installcheck-am installdirs \
+	installdirs-am maintainer-clean maintainer-clean-generic \
+	mostlyclean mostlyclean-generic mostlyclean-libtool pdf pdf-am \
+	ps ps-am tags tags-am uninstall uninstall-am
+
+.PRECIOUS: Makefile
+
+
+distclean-local:
+	rm -rf autom4te.cache
+	rm -f build-stamp configure-stamp
+	rm -rf lib/.deps
+	rm -rf src/.deps
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..6a95914
--- /dev/null
+++ b/NEWS
@@ -0,0 +1 @@
+See the ChangeLog file.
diff --git a/README b/README
new file mode 100644
index 0000000..731788b
--- /dev/null
+++ b/README
@@ -0,0 +1,113 @@
+                        README for sg3_utils
+                        ====================
+Introduction
+------------
+sg3_utils is a package of utilities originally written to send individual
+SCSI commands to storage devices that used one of the SCSI command sets.
+These utilities can be divided into three groups:
+   - sg_raw: the user supplies the cdb (command descriptor block) and
+     optionally the size of the data-in and data-out buffers
+   - one command utilities: the majority of the utilities in this package
+     send one SCSI command. Their names start with "sg_" while the
+     remaining part of their name alludes to the command which is sent. For
+     example, "sg_inq" sends the SCSI INQUIRY command. Some utilities in
+     this group send one of a selection of commands, typically those
+     commands have a lot it common (e.g. sg_write_x).
+   - copy type utilities: sg_dd, sgp_dd and sgm_dd use the Unix dd command
+     as a template. sg_xcopy sends the SCSI EXTENDED COPY command which in
+     some cases can do offloaded copies. As well as copying some of these
+     utilities can compare if two data segments held on disks are the same.
+
+Platforms
+---------
+These utilities were written on Linux and should work from Linux kernel
+(lk) 2.4 through to the current series 5. The third group ("copy type")
+are only implemented on Linux, but a separate portable package/utility
+called ddpt implements similar functionality. The first two groups are
+implemented (i.e. ported) to Android, FreeBSD, Solaris and Windows. The
+Windows port uses either a Cygwin or MinGW (plus Msys) build environment
+(rather than Visual Studio).
+
+Library
+-------
+Many of these utilities share a lot of code (e.g. SCSI error messages)
+so a lot of repetition (potentially error prone) is saved by having a
+library called libsgutils or some variation on that name. Distributions
+(especially of Linux) have differing policies on how a library (and a
+package) should be named. For that reason this package is sometimes
+known as "sg3-utils" (i.e. the underscore is turned into a hyphen).
+Various other packages use libsgutils. The library interface is not
+altered from one package release, to the next, but the library interface
+may be expanded. If a utility from one release is used with a libsgutils
+from an earlier release, then the runtime linking may fail. Typically
+package managers take care of these details so that runtime linking
+errors should be rare.
+
+Command Sets
+------------
+SCSI command sets are not the only storage command sets in wide use, there
+are also ATA and NVMe command sets. There is a SCSI command set to
+translate SCSI commands to ATA commands (called SAT: SCSI to ATA
+Translation). SAT includes an ATA PASS-THROUGH SCSI command and sg_sat_*
+utilities (there are four) are examples of using SAT. The SAS transport
+(Serial Attached SCSI) can convey ATA commands through a SCSI/SAS domain
+via its Serial ATA Tunnelled Protocol (STP).
+
+NVMe command sets (e.g. Admin, NVM and MI) are relatively new. There was an
+early paper on a SCSI to NVMe Translation Layer (SNTL) but it hasn't been
+standardized. The sg_inq utility will send (and decode the response of) a
+SCSI INQUIRY command if the underlying device is a SCSI device. If the
+underlying device is a NVMe controller or namespace, then sg_inq will send
+a NVMe Admin Identify command and decode the response. The sg_ses utility
+(for SCSI Enclosure Services) also checks whether its underlying device is
+SCSI or NVME. In the NVMe case, sg_ses translates the SCSI SEND DIAGNOSTIC
+and READ DIAGNOSTIC RESULTS commands to the NVMe Management Interface (MI)
+SES Send and SES Receive commands respectively. The output of the sg_ses
+utility should be similar, irrespective of whether the "SES" device is
+SCSI or NVMe.
+
+The sg_raw utility may send NVMe Admin or NVM commands (as well as SCSI
+commands). One difficulty with a command-line utility invoking NVME
+commands is that those commands contain memory addresses for data-in (from
+the storage device) or data-out (toward the storage device) transfers. See
+the sg_raw manpage for how this difficulty is addressed.
+
+Documentation
+-------------
+Manual pages ("manpages") are the primary method of utility documentation.
+All utilities and scripts that are installed by this package have a
+manpage. There are utilities in the examples, testing and utils
+directories that are not installed and do not have manpages. Nearly
+all utilities have runtime help, usually invoked with either the '-h'
+short option or the '--help' long option. There is also an overarching
+manpage called "sg3_utils". All manpages are placed in chapter 8 which
+is for system administration commands/utilities.
+
+The sg3_utils package and some more complex utilities have html pages:
+   sg3_utils: https://sg.danny.cz/sg/sg3_utils.html
+   sg_ses:    https://sg.danny.cz/sg/sg_ses.html
+   sg_dd:     https://sg.danny.cz/sg/sg_dd.html
+
+A tarball (and zip) of all the manpages from the previous release are
+here:
+   https://sg.danny.cz/sg/p/sg3_utils_man_html.tgz
+   https://sg.danny.cz/sg/p/sg3_utils_man_html.zip
+
+There is a html rendering of the sg3_utils manpage in the same directory
+as this README file called sg3_utils.man8.html .
+
+The previous README file is now called README.details plus there are
+these OS specific files: README.freebsd , README.solaris , README.tru64
+and README.win32 . To know the current state of the package the ChangeLog
+file is the good reference.
+
+The author's primary source code repository uses subversion and is on
+the author's equipment (a RPi). One advantage of subversion is its
+revision numbers which are simply integers starting at 1 and ascending.
+For this package the current revision is 928 . The subversion repository
+is mirrored in git (using "git svn" tools) here:
+    https://github.com/doug-gilbert/sg3_utils
+
+
+Douglas Gilbert
+31st December 2021
diff --git a/README.details b/README.details
new file mode 100644
index 0000000..560ea55
--- /dev/null
+++ b/README.details
@@ -0,0 +1,560 @@
+                      README.details for sg3_utils
+                      ============================
+Introduction
+============
+This package contains low level command line utilities for devices that use
+the SCSI command set. Originally the SCSI command set was associated
+exclusively with the SCSI Parallel Interface (SPI) transport. SPI has now
+almost been completely replaced by the Serial Attached SCSI (SAS) transport
+which also accepts the SCSI command set. Additionally many other storage
+related transports use the SCSI command set (amongst others); examples are
+ATAPI devices (CD/DVDs and tapes), USB mass storage devices (including those
+using the newer UAS[P]), Fibre Channel disks, IEEE 1394 storage devices (SBP
+protocol), iSCSI, FCoE and SOP devices. Even NVMe which has its own command
+set accepts SCSI commands in some contexts; one example is for enclosure
+management where NVME-MI has SES Send and SES Receive commands. SES refers
+to the SCSI Enclosure Services command set.
+
+This package originally targeted the Linux SCSI subsystem. Since most
+operating systems contain a SCSI command pass-through mechanism, many
+utilities within this package have been ported. This README mainly
+concentrates on Linux: see the README.freebsd file for the FreeBSD port,
+README.solaris for the Solaris port, the README.tru64 file for the Tru64
+(OSF) port and README.win32 for the Windows ports (of which there are two
+variants).
+
+Most utilities within the sg3_utils package work at the SCSI command level.
+For example the sg_inq utility issues a SCSI INQUIRY command and decodes the
+response. The COVERAGE file has a table containing a row for each SCSI
+command issued by this package; to the right of each row is the utility
+(sometimes more than one) that issue that SCSI command. The COVERAGE file
+has a second table for ATA commands usage.
+
+Some utilities interface at a slightly higher level, for example: sg_dd,
+sgm_dd and sgp_dd. These are closely related to the Unix dd command and
+typically issue a sequence of SCSI READ and WRITE commands to copy data.
+These utilities are relatively tightly bound to Linux and are not ported to
+other Operating Systems. A new utility called ddpt (in a package of the same
+name) is more generic while still allowing a copy to be done in terms of
+SCSI READ and WRITE commands. ddpt has been ported to other OSes.
+
+License
+=======
+All utilities and libraries have either a "2 clause" BSD license or are
+"GPL-2ed". The "2 clause" BSD license is taken from the FreeBSD project but
+drops the last paragraph that directly refers to the "FreeBSD project".
+That BSD license was updated from the "3 clause" to the newer "2 clause"
+version on 20180119. To save space various source code files refer to a
+file called "BSD_LICENSE" in the main, src and lib directories. The author's
+intention is that users may incorporate all or part of the code in their work
+as they please. Attribution is encouraged. Please check the code as other
+contributors (apart from the author) may also have copyright notices. For a
+list of contributors see the CREDITS file.
+
+
+Description
+===========
+A web site supporting the sg3_utils package can be found at
+https://sg.danny.cz/sg/sg3_utils.html . That page has a table of released
+versions for download. The most recent release or beta of sg3_utils may
+be found on this page: https://sg.danny.cz/sg in the News section.
+
+The predecessor to this package was called sg_utils. It is described in
+https://sg.danny.cz/sg/uu_index.html and old versions can be downloaded
+from the Downloads section of https://sg.danny.cz/sg .
+
+In the Linux 2.4 kernel series these utilities need to use the SCSI generic
+(sg) driver to access SCSI devices. The name of this package (i.e. sg3_utils)
+refers to version 3 of the SCSI generic (sg) driver which was introduced at
+the beginning of the 2.4 Linux kernel series. Significantly this added a new
+SCSI command interface structure (i.e. struct sg_io_hdr) that is more
+flexible than the older "sg_header" structure found in the sg driver in the
+2.2 and earlier Linux kernel series. The sg_io_hdr structure is also more
+flexible than the awkward (and limiting) interface to the
+SCSI_IOCTL_SEND_COMMAND ioctl supported by the Linux SCSI mid level. The
+version 3 sg driver also added the SG_IO ioctl that is synchronous (i.e. it
+issues the requested SCSI command and waits for the response (or a timeout)
+before the ioctl returns to the user space program that invoked it). The
+SG_IO ioctl is now supported in other parts of the Linux kernel in the 2.6
+series.
+
+In sg3_utils version 1.27 support has been added for the Linux bsg driver
+which use the sg version 4 interface. There seems no point in renaming
+this package sg4_utils. The existing utilities just silently support either.
+Currently the source build must be able to see the /usr/include/linux/bsg.h
+file. Then at run time the /proc/devices pseudo file needs to have an entry
+for the bsg driver (appeared around lk 2.6.28). With this in place each
+utility at run time checks the device it has been given and if it is a char
+device whose major number matches the bsg entry in /proc/devices then the
+sg v4 interface is used. Otherwise the sg v3 interface is used.
+
+Utilities that wish to use the asynchronous SCSI command interface (i.e. via
+a write() read() sequence) or issue special "commands" (e.g. bus and device
+resets) still need to use the Linux sg driver. Note that various
+drivers (e.g. cdrom/sr) have different open() flag and permissions policies
+that the user may need to take into account.
+
+If users have problems or questions about them please contact the author.
+Documentation for the Linux sg device driver can be found at:
+https://sg.danny.cz/sg/p/sg_v3_ho.html . This is written in DocBook and the
+original xml can be found in the same directory with the ".xml" extension.
+Postscript and pdf renderings are also in that directory. Older documentation
+for the sg version 3 driver can be found at:
+https://sg.danny.cz/sg/p/scsi_generic_v3.txt .
+
+To save the repetition of common code (e.g. SCSI error processing) and
+reduce the size of the executable files, a shared library called
+libsgutils<num>.so (its Linux name) is created during the build process.
+That library is built from the contents of the include and lib
+subdirectories. The header files in the include subdirectory can be seen
+as the API of libsgutils and are commented with that in mind. The SCSI
+pass-through code for the supported operating systems is found in the lib
+subdirectory with names like sg_pt_linux.c and sg_pt_win32.c .
+
+Various distributions (of Linux mainly) distribute sg3_utils as 3
+installable packages. One is a package containing the shared library
+discussed above (e.g. libsgutils2-2_1.33-0.1_i386.deb). A second package
+contains the utilities (e.g. sg3-utils_1.33-0.1_i386.deb) and depends on the
+first package). Finally there is an optional package that contains header
+files and a static library (e.g. libsgutils2-dev_1.33-0.1_i386.deb). This
+final package is only needed to build other packages (e.g. sdparm) that
+wish to use the sg3_utils shared library.
+
+All the utilities in the src subdirectory have "man" pages that are
+placed in the doc subdirectory. There is also a sg3_utils (8) man page that
+summarizes common facilities including exit statuses. Additional
+information (including each utility's version number) can be found towards
+the top of each ".c" file corresponding to the utility name.
+
+The sg driver in Linux can be seen as having 3 distinct versions:
+
+   v1   lk < 2.2.6     sg_header based relatively unchanged since 1992
+   v2   lk >= 2.2.6    enhanced sg_header interface structure [1999/4/16]
+   v3   lk >= 2.4      additional sg_io_hdr interface structure [2001/1/4]
+   v3   lk >= 2.6      same interface as found in lk 2.4 [2.6.0: 2003/12/18]
+
+and the bsg driver supports the sg v4 interface and was added around
+lk 2.6.28 . This package is targeted at "v3" and "v4". Another package called
+"sg_utils" is targeted at "v2" and to a lesser extent "v1". The "sg_utils"
+package has a subset of the utilities found in this package.
+
+In Linux some sg driver ioctls (notably SG_IO) are defined for many block
+devices in lk 2.6 series. In practice this means all SCSI block devices,
+ATAPI block devices (mainly CD, DVD and BD optical devices) but _not_ ATA
+disks, depending on which kernel configuration options, can be accessed by
+the utilities in this package. SATA disks that use the libata kernel library
+(or some other SCSI to ATA Translation (SAT) Layer (SATL)) accept SCSI
+commands and thus are supported. Support for the SG_IO as been added to the
+scsi tape driver (st) in lk 2.6.6 .
+
+In the src directory the bulk of the utilities are written in relatively
+clean POSIX compliant C code with Linux specific system calls and structures
+removed and placed in Linux specific files in the lib directory. A small
+number of utilities in the src directory do contain Linux specific logic
+and are not ported to other OSes (e.g. sg_dd). One utility, sg_scan, has
+two separate implementations, one for Linux (sg_scan_linux.c) and one for
+Windows (sg_scan_win32.c). The src-lib directory split approach allows
+FreeBSD, Solaris, Tru64 and Windows specific code to be isolated to a few
+files in the lib directory whose interfaces match those of the Linux
+specific code.
+
+Darwin is not supported because the Apple folks do not want to give their
+users a pass-through SCSI interface. The author has read about creative
+hackers using a VM containing a real OS to circumvent the Apple restriction.
+
+C standard is C11
+==================
+The C code in this package is written for portability rather than speed.
+It assumes a level of C99 compliance (the C standard prior to C11) and
+favours POSIX system and library calls over OS specific calls.
+
+The C code is written in a C++ friendly way and is checked from time to
+time that it compiles clean with C++. To accommodate C++ certain C99
+constructs such as designated initializers cannot be used. To build
+with C++, C++11 (i.e. the C++ standard from 2011) or later is required.
+Finding a common C and C++ syntax for zeroing stack variables (including
+aggregates) may need to wait until C23 allows this syntax:
+   struct example_t ex1 {};
+which C++ introduced in C++11.  In the meantime the SG_C_CPP_ZERO_INIT
+define (hack) does this.
+
+The author has not seriously attempted to build this code on MSVC (aka
+Visual Studio). There are a few roadblocks (that may be overcome in the
+future) that include MSVC being basically a C++ compiler, not a C/C++
+compiler. For some reason MSVC only claims C89 compliance (i.e. the first
+C standard from 1989). MSVC 2013 and 2015 are moving closer to C99
+compliance and may be sufficient to compile this package. Another problem
+is the assumption of the availability of basic Unix system calls such as
+open(). Nearly 20 years ago Microsoft indicated (promised ?) that it
+would move in the direction of POSIX compliance, but very little ever
+happened. "Talk is cheap, there should be a tax on it."
+
+Building
+========
+This package is designed to be built with the usual:
+    "./configure ; make ; make install"
+sequence. In some situations that may need to be prefixed by a call to
+the "./autogen.sh" script which invokes autoconf and automake. That in turn
+may require packages containing those utilities to be installed. The
+libtool utility is also required. Naturally a C compiler is required
+and due to the vagaries of libtool a C++ compiler also.
+
+The "./configure" takes many command line options with the defaults
+being usually sufficient to start with. One quirk is that the location
+of the installation is under the /usr/local directory. So the sg_inq
+utility will be installed at /usr/local/bin/sg_inq . This is controlled
+by the "--prefix=<directory>" option which defaults to
+"--prefix=/usr/local". As an example to install the executables in /usr/bin
+and disable the creation of the shared library (libsgutils<num>.so) this
+invocation could be used: "./configure --prefix=/usr --disable-shared".
+To reduce the size of an executable as well try this:
+"./configure --prefix=/usr --disable-shared --disable-scsistrings".
+Also --disable-shared will produce (relatively) "static" executables in
+the src directory that are easier to debug. And
+"./configure --enable-debug" will compile with more debug type options,
+including more compiler checks and defining "DEBUG" within the src and
+lib source files. Most utilities in the src directory set '-vv' (i.e.
+equivalent to calling "--verbose" twice) when "DEBUG" is set.
+
+In Linux there are package build files for "rpm" based and for "deb" based
+systems. The 'sg3_utils.spec' file in the main directory can be used like
+this: 'rpmbuild -ba sg3_utils.spec' in a rpmbuild tree SPECS directory.
+To cross build or make a more widely distributable package then the --target
+option may be useful: 'rpmbuild --target=i386 -ba sg3_utils.spec' or
+'rpmbuild --target=x86_64 -ba sg3_utils.spec' . The sg3_utils.spec file
+in the main directory targets Red Hat systems, an alternative "spec" file
+for Suse systems has been placed under the 'suse' directory.
+
+The 'build_debian.sh' script should build several "deb" packages and place
+them in the parent directory. In debian based systems doing
+a 'apt-get install build-essential' is one way to get most of build
+environment needed if it has not already been loaded. There are now some
+problems with this script and the superseded Debian 4.0 ("etch"). See
+debian/README.debian4 for a workaround. Amongst other things debian
+builds are sensitive to the value in the debian/compat file. If it
+contains "7" then it works on lenny and gives warning on squeeze (but
+fails on the earlier etch).
+
+Warning
+=======
+Many devices use SCSI command sets over transport protocols not normally
+associated with SCSI (as defined at https://www.t10.org ). Some of these
+devices react poorly (e.g. lock up) when sent SCSI commands that they don't
+support. Even sending a supported SCSI command with a field set to an
+unexpected value can cause problems. [The author is talking about billions
+of USB devices with horrible SCSI implementations.]
+
+For example, all "SCSI" devices must support the INQUIRY command which the
+SCSI-2 standard says should request a 36 byte response. However later SCSI
+standards (e.g. SPC-2) have increased that length but some SCSI devices lock
+up when they receive a request for anything other than a 36 byte response.
+
+Any well implemented "SCSI" device should react sensibly when a utility in
+sg3_utils sends a SCSI command that it doesn't support. Unfortunately this
+cannot be guaranteed.
+
+Prior to lk 2.6.29 USB mass storage limited sense data to 18 bytes which
+caused problems for certain types of descriptor based sense data. An
+example of this is the SCSI ATA PASS-THROUGH command with the CK_COND bit
+set.
+
+
+Utilities
+=========
+Here is list in alphabetical order of utilities found in the 'src'
+subdirectory of the sg3_utils package:
+    sginfo, sg_bt_ctl, sg_compare_and_write, sg_copy_results, sgm_dd, sgp_dd,
+    sg_dd, sg_decode_sense, sg_emc_trespass, sg_format, sg_get_config,
+    sg_get_elem_status, sg_get_lba_status, sg_ident, sg_inq, sg_logs,
+    sg_luns, sg_map, sg_map26, sg_modes, sg_opcodes, sg_persist, sg_prevent,
+    sg_raw, sg_rbuf, sg_rdac, sg_read, sg_read_attr, sg_readcap,
+    sg_read_block_limits, sg_read_buffer, sg_read_long, sg_reassign,
+    sg_referrals, sg_rem_rest_elem, sg_rep_density, sg_rep_pip, sg_rep_zones,
+    sg_request, sg_reset, sg_rmsn, sg_rtpg, sg_safte, sg_sanitize,
+    sg_sat_identify, sg_sat_phy_event, sg_sat_read_gplog, sg_sat_set_features,
+    sg_scan, sg_seek, sg_senddiag, sg_ses, sg_ses_microcode, sg_start,
+    sg_stpg, sg_stream_ctl, sg_sync, sg_test_rwbuff, sg_timestamp, sg_turs,
+    sg_unmap, sg_verify, sg_vpd, sg_write_buffer, sg_write_long,
+    sg_write_same, sg_write_verify, sg_write_x, sg_wr_mode, sg_xcopy, sg_zone,
+    sg_z_act_query
+
+Each of the above utilities depends on header files found in the 'include'
+subdirectory and library code found in the 'lib' subdirectory. Associated
+man pages are found in the 'doc' subdirectory. Additional programs found
+in the 'archive', 'examples' and 'utils' subdirectories in not build by the
+top level build infrastructure. Linux binary distributions of the sg3_utils
+package (e.g. "rpm" and debian packages) typically contain the shared
+library, the utilities found in the 'src' subdirectory, their associated man
+pages and some documentation files (e.g. README, INSTALL, CREDITS, COPYING
+and COVERAGE). See the INSTALL file for generic instructions about building
+with autotools (e.g. ./configure ).
+
+Man pages can be read (without building and installing the package) by
+going to the 'doc' subdirectory and executing something like this:
+ $ man ./sg_dd.8
+
+To see which SCSI commands (and ATA commands) are used by these utilities
+refer to the COVERAGE file.
+
+Here is a list in alphabetical order of utilities found in the 'examples'
+subdirectory:
+  - sg_excl, scsi_inquiry, sg_sat_chk_power, sg__sat_identify,
+    sg__sat_phy_event, sg__sat_set_features, sg_sat_smart_rd_data,
+    sg_simple1, sg_simple2, sg_simple3, sg_simple4, sg_simple5,
+    sg_simple16
+
+Also in that subdirectory is a script to test sg_persist, an example data
+file for sg_persist (called "transport_ids.txt") and an example data file for
+sg_reassign (called "reassign_addr.txt"). There are several scripts
+for 'sg_senddiag -pf -raw=-' that will put some SAS disk phys into
+a "compliant jitter tolerance pattern" (CJTPAT).
+
+The 'testing' subdirectory contains source and a Makefiles to test
+kernel pass-through and associated drivers, mainly for Linux. There is
+both C code (with the extension ".c") and C++ code (with the extension
+".cpp"). There is a "Makefile" to build the C + C++ code. The Makefile
+depends on some object files from the "lib" subdirectory. So a sequence
+like this may be required prior to invoking make: "cd <top_of_package> ;
+./configure ; cd lib ; make ; cd ../testing".
+
+Here is a list in alphabetical order of utilities found in the 'testing'
+subdirectory:
+  - bsg_queue_tst, sgh_dd (C++), sg_iovec_tst, sg_queue_tst, sg_sense_tst,
+    sg_tst_async (C++), sg_tst_context (C++), sg_tst_excl (C++),
+    sg_tst_excl2 (C++), sg_tst_excl3 (C++)
+
+The 'utils' subdirectory contains source and a Makefile to build "hxascdmp"
+which accepts binary data from stdin (or a file on the command line) and
+outputs an ASCII-HEX and ASCII representation of it. It is similar to the
+Unix od command. There is also code to sg_chk_asc.c which checks a given
+text file (typically a copy of https://www.t10.org/lists/asc-num.txt ) and
+checks it against the asc/ascq text strings held in sg_lib_data.c .
+
+The 'doc' subdirectory contains a README file containing the urls of
+various related documents.
+
+The 'scripts' subdirectory contains some Bourne (bash) shell scripts that
+rely on utilities in the main directory. One script uses the sdparm utility.
+These scripts are described in the scripts/README file and have usage
+messages.
+
+
+Notes for utilities without man pages
+=====================================
+These utils are found in the 'examples' subdirectory.
+
+The "scsi_inquiry" program shows the use of the SCSI_IOCTL_SEND_COMMAND
+ioctl to send a SCSI INQUIRY command. That ioctl() is supported by the
+SCSI sub system mid level and so is common to all sd, sr, st and sg devices.
+That ioctl is deprecated in the lk 2.6 series. This program has been placed
+in the "examples" subdirectory.
+
+"sg_simple1" and "sg_simple2" are example programs demonstrating calls
+to the SCSI INQUIRY and TEST UNIT READY commands. They only differ in their
+error processing: sg_simple1 uses sg_lib.[hc] for error processing while
+sg_simple2 does its own more primitive checks.
+
+"sg_simple3" tests out user space scatter gather added to the version 3
+sg driver.
+
+"sg_simple4" shows the INQUIRY command using mmap-ed IO to obtain its
+response buffer.
+
+"sg_simple5" also sends and INQUIRY and TEST UNIT READY commands. It
+uses the generic pass through mechanism based on sg_pt.h . It will
+currently build in Linux and FreeBSD (with "make -f Makefile.freebsd").
+It has extensive error checking code.
+
+"sg_simple16" attempts to send a 16 byte SCSI command, READ_16, to the
+scsi device. This is only supported for lk >= 2.4.15 and for adapter
+drivers that indicate that they have 16 byte CDB capability (otherwise
+DID_ABORT will appear in the host_status).
+
+"sg_sat_chk_power" attempts to push an ATA CHECK POWER MODE command
+through the SAT-defined ATA PASS_THROUGH (16) SCSI command. That
+ATA command needs to read the "FIS" registers after the command is
+completed which involves using the ATA Status Return (sense data)
+descriptor (as defined in SAT).
+
+"sg_sat_smart_rd_data" attempts to push an ATA SMART/READ DATA command
+through the SAT-defined ATA PASS_THROUGH (16) SCSI command. If
+successful, the 256 word (512 byte) response is output.
+
+"sg_tst_excl" and "sg_tst_excl2" use multiple threads to bombard the
+given device with O_EXCL open flags, so only one should succeed at a
+time. While holding O_EXCL control a thread attempts a double increment
+on an integer in the given LBA. If the integer starts even (after the
+first read) then it should remain even if the O_EXCL flag is doing its job.
+The "sg_tst_excl" variant uses the Linux SG_IO v3 interface while the
+"sg_tst_excl2" uses the more generic sg_pt infrastructure.
+
+"sg_tst_excl3" is a variant of "sg_tst_excl2". "sg_tst_excl3" only does
+the double increment from the first thread, each time using O_EXCL on
+open. The remaining threads check the value is even, each time doing
+an open without the O_EXCL flag.
+
+"bsg_queue_tst" sends an INQUIRY command via the Linux SG_IO v4 interface
+which is used by the bsg driver. So it will take device names like
+"/dev/bsg/6:0:0:0". It tests if sending repeated INQUIRYs with
+the BSG_FLAG_Q_AT_HEAD or BSG_FLAG_Q_AT_TAIL flag makes any difference.
+
+"sg_tst_async" is a test harness for the Linux sg driver. It is multi
+threaded, submitting either TEST UNIT READY, READ(16) or WRITE(16) SCSI
+commands asynchronously. Each thread opens a file descriptor and submits
+those commands up to the queue limit (sg driver has a per file descriptor
+queue limit of 16). Multiple threads doing the same thing act as a
+multiplier to that queue limit.
+
+
+NVME Support
+============
+Firstly the author has no intention of extending this package to contain
+general purpose NVMe utilities. That leaves the areas where SCSI overlaps
+with NVMe. There was a SCSI to NVMe Translation Layer (SNTL) driver in the
+Linux kernel based on a white paper from NVM Express. Intel has withdrawn
+that driver and T10 (SCSI) and NVM Express have made no further attempts
+to standardize a SNTL. Given the SCSI to ATA Translation Layer (SATL) which
+is standardized by T10, it is pretty clear what a SNTL should do.
+
+The NVMe Management Interface (NVME-MI) committee have decided to use SES-3
+standard from T10 via the newly added SES Send and SES Receive MI commands.
+So the sg_ses utility and this package's library have been extended to use
+these commands when a NVMe device (typically a disk enclosure) is detected.
+This has been tested by a disk vendor who is happy with the results. Other
+user reports are welcome as the author does not have equipment to test
+this.
+
+Other utilities in this package that use the SES Send and Receive commands,
+or the SNTL in the library are sg_senddiag, sg_inq, sg_raw and sg_readcap.
+
+
+Command line processing
+=======================
+These utilities can be divided into 3 groups when their handling of command
+line arguments is considered:
+  - ad hoc, typically in a short form only, sometimes longer (e.g.
+    "sg_logs -pcb /dev/sdc")
+  - inspired by the dd Unix command (e.g. sg_dd, sgm_dd, sgp_dd, sg_read)
+  - recent utilities use "getopt_long" (see "man getopt_long")
+    type command lines. These have short form (starting with "-")
+    and corresponding longer form (starting with "--") options.
+
+The older utilities that use ad hoc options, in alphabetical order:
+  - sg_emc_trespass, sginfo(1/2), sg_inq, sg_logs, sg_map, sg_modes,
+    sg_opcodes, sg_rbuf, sg_rdac, sg_readcap, sg_reset, sg_scan (Linux),
+    sg_senddiag, sg_start, sg_test_rwbuf, sg_turs
+In sg3_utils version 1.23 the following utilities from this group were
+converted to have a dual getopt_long/ad_hoc interface, defaulting to
+the getop_long interface:
+  - sg_inq, sg_logs, sg_modes, sg_opcodes, sg_rbuf, sg_readcap,
+    sg_senddiag, sg_start, sg_turs
+These can be switched back to the older (backward compatible) ad hoc
+interface by defining the SG3_UTILS_OLD_OPTS environment variable
+or using '-O' as the first command line option.
+
+The more recent utilities that use "getopt_long" only are:
+  - sg_bt_ctl, sg_compare_and_write, sg_decode_sense, sg_format,
+    sg_get_config, sg_get_lba_status, sg_ident, sg_luns, sg_map26,
+    sg_persist, sg_prevent, sg_raw, sg_read_attr, sg_read_block_limits,
+    sg_read_buffer, sg_read_long, sg_reassign, sg_referrals, sg_rep_pip,
+    sg_rep_zones, sg_requests, sg_rmsn, sg_rtpg, sg_safte, sg_sanitize,
+    sg_sat_identify, sg_sat_phy_event, sg_sat_read_gplog,
+    sg_sat_set_features, sg_scan(w), sg_seek, sg_ses, sg_ses_microcode,
+    sg_stpg, sg_stream_ctl, sg_sync, sg_test_rwbuf, sg_timestamp, sg_unmap,
+    sg_verify, sg_vpd, sg_write_buffer, sg_write_long, sg_write_same,
+    sg_write_verify, sg_write_x, sg_wr_mode, sg_zone, sg_z_act_query
+
+
+Dangerous code
+==============
+This C code snippet:
+    unsigned char uc = 0x80;
+    uint64_t ull;
+    ull = (uc << 24);
+Somewhat surprisingly sets ull to:
+    ull: 0xffffffff80000000
+This result is due to the 'unary conversion' of uc to a (32 bit signed)
+'int' before the shift. The resultant type from the shift is also an int
+and it has its top bit set so there is sign extension when it is assigned
+into a 64 bit unsigned integer. Making sure there is no conversion to 'int'
+solves the problem. In this case if uc is declared as unsigned int the
+result will be as expected (i.e. 0x80000000).
+
+
+Bypassing the somewhat dangerous shift operators
+================================================
+The shift operators in C are "<<" and ">>". They can be dangerous (as shown
+in the above section) or tedious and hence error prone to use. However they
+are often needed to cope with the translation of integers on the host OS to
+the corresponding representation within a SCSI command or parameter data
+moved to or from a SCSI device. The Logical Block Address (LBA) is a good
+example; it is either 32 or 64 bits long typically (i.e. 4 or 8 bytes
+respectively). The host machine representation may be big or little endian
+and may prefer or require alignment to a particular memory address boundary
+(e.g. module 4 (or in 'C' code: "(lba % 4) == 0")). For SCSI commands and
+the parameter data moved to or from a SCSI device, the integer
+representation is big endian and it is unaligned.
+
+Recent versions of this package have replaced the explicit use of the C
+shift operators with a group of functions modelled on those found in the
+Linux kernel. These functions contain either "get_unaligned" or
+"put_unaligned" in their names and are found in the asm/unaligned.h
+header. This package contains the sg_unaligned.h header that implements
+a similar set of functions. The current implementation favours correctness
+over speed. The functions in the package use a "sg_" prefix but otherwise
+use the same function name as the Linux kernel for the same action.
+
+An example of the change made to a snippet of sg_write_buffer.c may
+clarify this change. The old code was:
+
+   wbufCmdBlk[3] = (unsigned char)((buffer_offset >> 16) & 0xff);
+   wbufCmdBlk[4] = (unsigned char)((buffer_offset >> 8) & 0xff);
+   wbufCmdBlk[5] = (unsigned char)(buffer_offset & 0xff);
+
+and it has been replaced by:
+
+   sg_put_unaligned_be24(buffer_offset, wbufCmdBlk + 3);
+
+The Linux kernel only supplies "unaligned" functions for 16, 32 and 64
+bit quantities. SCSI commands also have cases of 24 and 48 bit numbers
+so sg_unaligned.h contains support for those plus a variant where the
+byte length is passed as an argument.
+
+The unaligned functions are inlined for speed (at the possible expense of
+space) and now have specializations depending whether the host is big or
+(more likely) little endian. These functions can be broken down to a
+memcpy() and optionally a byte-swap for 16, 32 and 64 bit operations.
+The memcpy() takes care of alignment while the byte-swap (bswap_16(),
+bswap_32() and bswap_64() ) addresses integer endianness. If the host is
+little endian and a little endian variant of the unaligned functions is
+requested, then no byte-swap is required. These specializations can be
+"compiled out" with this configure option: './configure
+--disable-fast-lebe' in which case the classic "C shifting" technique is
+used to implement all the unaligned functions.
+
+Associated with the above change, fixed length integer types seem a better
+fit for SCSI command and parameter integers than the traditional integer
+types in the C language. Fixed length integer types were standardized in
+C99 and require the inclusion of <stdint.h>. For example this means for
+an integer that will represent a 64 bit LBA, to favour using "uint64_t"
+over the "unsigned long long" type. Also "unsigned char" has mostly been
+replaced by "uint8_t" as the 8 bit (unsigned) byte type; "char" is still
+used for ASCII text.
+
+
+Coding Style
+============
+Everyone has their own C/C++ coding style and the author is no different.
+In terms of the GNU indent command:
+     indent -i4 -il0 -nut -br -npcs -ncs -ce
+is pretty close. That is similar to the Linux kernel coding style but
+with 4 space indentations and no tabs.
+
+
+Other SCSI and storage tools
+============================
+See https://sg.danny.cz/sg/tools.html
+
+
+Douglas Gilbert		dgilbert@interlog.com
+26th August 2022
diff --git a/README.freebsd b/README.freebsd
new file mode 100644
index 0000000..411f098
--- /dev/null
+++ b/README.freebsd
@@ -0,0 +1,164 @@
+Introduction
+============
+The FreeBSD port of sg3_utils contains those utilities that are _not_
+specific to Linux. In some cases the FreeBSD camcontrol command supplies
+similar functionality; for example 'sg_map' is similar to
+'camcontrol devlist'.
+
+The dd variants from the sg3_utils package (e.g. sg_dd) rely on too many
+Linux idiosyncrasies to be easily ported. A new package called 'ddpt'
+contains a utility with similar functionality to sg_dd and ddpt is available
+for FreeBSD.
+
+Supported Utilities
+===================
+Here is a list of utilities that have been ported:
+    sg_bg_ctl
+    sg_compare_and_write
+    sg_decode_sense
+    sg_format
+    sg_get_config
+    sg_get_elem_status
+    sg_get_lba_status
+    sg_ident
+    sg_inq          [dropped ATA IDENTIFY DEVICE capability]
+    sg_logs
+    sg_luns
+    sg_modes
+    sg_opcodes
+    sg_persist
+    sg_prevent
+    sg_raw
+    sg_rdac
+    sg_read_block_limits
+    sg_read_buffer
+    sg_read_long
+    sg_readcap
+    sg_reassign
+    sg_referrals
+    sg_rep_pip
+    sg_rep_zones
+    sg_requests
+    sg_rmsn
+    sg_rtpg
+    sg_safte
+    sg_sanitize
+    sg_sat_identify
+    sg_sat_phy_event
+    sg_sat_set_features
+    sg_seek
+    sg_senddiag
+    sg_ses
+    sg_start
+    sg_stpg
+    sg_stream_ctl
+    sg_sync
+    sg_turs
+    sg_verify
+    sg_unmap
+    sg_vpd
+    sg_wr_mode
+    sg_write_buffer
+    sg_write_long
+    sg_write_same
+    sg_write_verify
+    sg_write_x
+    sg_zone
+
+Most utility names are indicative of the main SCSI command
+that they execute.  Some utilities are slightly higher level, for
+example sg_ses fetches SCSI Enclosure Services (SES) status pages and
+can send control pages. Each utility has a man page (placed in
+section 8). An overview of sg3_utils can be found at:
+https://sg.danny.cz/sg/sg3_utils.html .
+A copy of the "sg3_utils.html" file is in the "doc" subdirectory.
+
+
+The executables and library can be built from the source code in
+the tarball and installed with the familiar
+"./configure ; make ; make install" sequence. If this fails try
+running the "./autogen.sh" script prior to that sequence. There
+are generic instruction on configure and friend in the INSTALL file.
+
+Some man pages have examples which use Linux device names which
+hopefully will not confuse the FreeBSD users.
+
+Device naming
+=============
+In FreeBSD disks have block names like '/dev/da0' with a corresponding
+pass-through device name like '/dev/pass0'. Use this command:
+"camcontrol devlist" to see that SCSI devices available. To list NVMe
+devices: "nvmecontrol devlist" can be used. Any many, but not all
+contexts, the device name can be used without the '/dev/' prefix.
+FreeBSD is relatively unique in this respect and support for this
+abbreviated form has been broken in this package and fixed in
+sg3_utils release 1.46 .
+
+Device naming for NVMe is a bit more complex. Controllers have names
+like /dev/nvme0 and namespaces /dev/nvme0ns1 . Partitions are not
+supported on /dev/nvme0ns1 type nodes. Instead there are /dev/nvd0
+and /dev/nvd0p<m> where <m> is th partition number starting at 1.
+The nvd driver (written by Intel) is not CAM compatible and has its
+own utility nvmecontrol which has similar capabilities as camcontrol
+has for CAM devices. In FreeBSD release 12 the nda driver was
+introduced with names like /dev/nda0 and /dev/nda0n<m>. The difference
+is that nda is CAM compatible. From the point of view of this package,
+the nda driver is preferred as CAM supports NVMe command timeouts and
+the error processing is more mature. 
+
+FreeBSD installation
+====================
+The traditional './configure ; make ; make install' sequence from the
+top level of the unpacked tarball will work on FreeBSD. But the man pages
+will be placed under the /usr/local/share/man directory which unfortunately
+is not on the standard manpath. One solution is to add this path by
+creating a file with a name like local_share.conf in the
+/usr/local/etc/man.d/ directory and placing this line in it:
+    MANPATH /usr/local/share/man
+
+FreeBSD 9.0 has a "ports" entry for sg3_utils under the
+/usr/ports/sysutils directory. It points to version 1.28 of sg3_utils
+which is now a bit dated. It could be used as a template to point
+to more recent versions.
+
+kFreeBSD
+========
+sg3_utils can be built into a Debian package for kFreeBSD using the
+./build_debian.sh script in the top level directory. This has been tested
+with Debian 6.0 release.
+
+Details
+=======
+Most of the ported utilities listed above use SCSI command functions
+declared in sg_cmds_*.h headers . Those SCSI command functions are
+implemented in the corresponding ".c" files. The ".c" files pass SCSI
+commands to the host operating system via an interface declared in sg_pt.h .
+There are currently five implementations of that interface depending on
+the host operating system:
+  - sg_pt_linux.c
+  - sg_pt_freebsd.c
+  - sg_pt_osf1.c  [Tru64]
+  - sg_pt_win32.c
+  - sg_pt_solaris.c
+
+The sg_pt_freebsd.c file uses the FreeBSD CAM SCSI pass through mechanism.
+Hence only FreeBSD device nodes that support CAM can be used. These can be
+viewed with the "camcontrol devlist" command. To access ATAPI devices (e.g.
+ATAPI DVD drives) the kernel may need to be configured with the "atapicam"
+device.
+
+Attempts to send SCSI commands with data-in or data-out buffers around 64 KB
+and larger failed on a FreeBSD 7.0 with an "argument list too long" error
+message. There is an associated kernel message (viewable with dmesg) that an
+attempt has been made to map <n> bytes which is greater than
+DFLTPHYS(65536). Still a problem in FreeBSD 8.1 . Due to CAM overhead the
+largest power of 2 that can fit through with one command is 32768 bytes (32
+KB).
+
+FreeBSD 9.0 is the most recent version of FreeBSD tested with these
+utilities.
+
+
+
+Douglas Gilbert
+1st May 2021
diff --git a/README.iscsi b/README.iscsi
new file mode 100644
index 0000000..917190a
--- /dev/null
+++ b/README.iscsi
@@ -0,0 +1,37 @@
+iSCSI support for sg3-utils is available from external patches.
+
+To build sg3-utils from sources and activate built-in iSCSI support
+you need both sg3-utils and the external user-space iSCSI library hosted at :
+
+https://github.com/sahlberg/libiscsi
+
+This library provides a client library for accessing remote iSCSI
+devices and also comes with patches to the sg3-utils source code
+distribution to compile a special version of sg3-utils with iSCSI
+support.
+
+No support for iSCSI is provided by the sg3-utils maintainer.
+
+
+
+Once sg3-utils is compiler and installed with libiscsi support, you
+can specify remote iSCSI devices through a special URL format instead
+of the normal /dev/* syntax.
+
+Example:
+
+sg_inq iscsi://ronnie%password@10.1.1.27/iqn.ronnie.test/1
+standard INQUIRY:
+ PQual=0  Device_type=0  RMB=0  version=0x05  [SPC-3]
+ [AERC=0]  [TrmTsk=1]  NormACA=0  HiSUP=0  Resp_data_format=2
+ SCCS=0  ACC=0  TPGS=0  3PC=0  Protect=0  BQue=0
+ EncServ=0  MultiP=0  [MChngr=0]  [ACKREQQ=0]  Addr16=0
+ [RelAdr=0]  WBus16=0  Sync=0  Linked=0  [TranDis=0]  CmdQue=1
+ [SPI: Clocking=0x0  QAS=0  IUS=0]
+   length=66 (0x42)   Peripheral device type: disk
+ Vendor identification: IET
+ Product identification: VIRTUAL-DISK
+ Product revision level: 0001
+ Unit serial number:                           beaf11
+
+
diff --git a/README.sg_start b/README.sg_start
new file mode 100644
index 0000000..22034d4
--- /dev/null
+++ b/README.sg_start
@@ -0,0 +1,38 @@
+Hi,
+
+you can use sg_start to start (spin-up, 1) and stop (spin-down, 0) devices.
+I also offers a parameter (-s) to send a synchronize cache command to a
+device, so it should write back its internal buffers to the medium.
+
+Be aware that the Linux SCSI subsystem at this time does not automatically
+starts stopped devices, so stopping a device which is in use may have fatal
+results for you.
+
+So, you should apply with care.
+I use it in my shutdown script at the end (before the poweroff command):
+
+# SG_SHUG_NOS is set in my config file rc.config
+# SG_SHUT_NOS="0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"
+if test -x /bin/sg_start; then
+    if test "`basename $command`" = "reboot"; then
+        for no in $SG_SHUT_NOS;
+	      do /bin/sg_start /dev/sg$no -s >/dev/null 2>&1;
+	 done
+    else
+        for no in $SG_SHUT_NOS;
+	    do /bin/sg_start /dev/sg$no -s 0 >/dev/null 2>&1;
+        done
+    fi
+fi
+
+Enjoy!
+Kurt Garloff
+
+
+Postscript
+==========
+sg_start has been reworked to allow a block device (e.g. /dev/sda) in
+addition to the sg device name (e.g. /dev/sg0) in the lk 2.6 series.
+sg_start now has more command line options, see its man page.
+
+	Douglas Gilbert <dgilbert at interlog dot com> 2004/5/8
diff --git a/README.solaris b/README.solaris
new file mode 100644
index 0000000..7dca2b7
--- /dev/null
+++ b/README.solaris
@@ -0,0 +1,168 @@
+Please Note:
+>>> Up to and including sg3_utils-1.33 the Solaris code was built
+>>> and tested on an OpenSolaris VM run with VirtualBox on Ubuntu
+>>> 11.10 . Now with Ubuntu 12.04 those VMs crash immediately when
+>>> started with VirtualBox. Further, Oracle (who owns SUN and thus
+>>> Solaris) no longer supports OpenSolaris and its package
+>>> repository has been withdrawn. The author can find no generic VMs
+>>> for Oracle Solaris 11 that run on VirtualBox or VMWare. The author
+>>> is also displeased with the withdrawal of the Open Software
+>>> OS and is disinclined to build a Solaris 11 system just to
+>>> virtualize it.
+>>> So as of sg3_utils-1.34 the Solaris port is provided "as-is" without
+>>> testing on a Solaris platform.
+
+Douglas Gilbert
+13th October 2012
+
+
+
+Introduction
+============
+The Solaris port of sg3_utils contains those utilities that are
+_not_ specific to Linux.
+
+The dd variants from the sg3_utils package (e.g. sg_dd) rely on too many
+Linux idiosyncrasies to be easily ported. A new package called 'ddpt'
+contains a utility with similar functionality to sg_dd and is available
+for Solaris.
+
+Supported Utilities
+===================
+Here is a list of utilities that have been ported:
+    sg_bg_ctl
+    sg_compare_and_write
+    sg_decode_sense
+    sg_format
+    sg_get_config
+    sg_get_elem_status
+    sg_get_lba_status
+    sg_ident
+    sg_inq          [dropped ATA IDENTIFY DEVICE capability]
+    sg_logs
+    sg_luns
+    sg_modes
+    sg_persist
+    sg_opcodes
+    sg_prevent
+    sg_raw
+    sg_rdac
+    sg_read_block_limts
+    sg_read_buffer
+    sg_read_long
+    sg_readcap
+    sg_reassign
+    sg_referrals
+    sg_rep_pip
+    sg_rep_zones
+    sg_requests
+    sg_rmsn
+    sg_rtpg
+    sg_safte
+    sg_sanitize
+    sg_sat_identify
+    sg_sat_phy_event
+    sg_sat_set_features
+    sg_seek
+    sg_senddiag
+    sg_ses
+    sg_start
+    sg_stpg
+    sg_stream_ctl
+    sg_sync
+    sg_turs
+    sg_unmap
+    sg_verify
+    sg_vpd
+    sg_wr_mode
+    sg_write_buffer
+    sg_write_long
+    sg_write_same
+    sg_write_verify
+    sg_write_x
+    sg_zone
+
+Most utility names are indicative of the main SCSI command
+that they execute.  Some utilities are slightly higher level, for
+example sg_ses fetches SCSI Enclosure Services (SES) status pages and
+can send control pages. Each utility has a man page (placed in
+section 8). An overview of sg3_utils can be found at:
+https://sg.danny.cz/sg/sg3_utils.html .
+A copy of the "sg3_utils.html" file is in the "doc" subdirectory.
+
+
+The executables and library can be built from the source code in
+the tarball and installed with the familiar
+"./configure ; make ; make install" sequence. If this fails try
+running the "./autogen.sh" script prior to that sequence. There
+are generic instruction on configure and friend in the INSTALL file.
+
+Some man pages have examples which use Linux device names which
+hopefully will not confuse the Solaris users.
+
+Device naming
+=============
+In Solaris, SCSI device names below the '/dev' directory have a
+form like: c5t4d3s2 where the number following "c" is the controller
+(HBA) number, the number following "t" is the target number (from
+the SCSI parallel interface days) and the number following "d" is
+the LUN. Following the "s" is the slice number which is related to
+a partition and by convention "s2" is the whole disk.
+
+OpenSolaris also has a c5t4d3p2 form where the number following
+the "p" is the partition number apart from "p0" which is the whole
+disk. So a whole disk may be referred to as either:
+  - c5t4d3
+  - c5t4d3s2
+  - c5t4d3p0
+
+And these device names are duplicated in the /dev/dsk and /dev/rdsk
+directories. The former is the block device name and the latter
+is for "raw" (or char device) access which is what sg3_utils needs.
+So in OpenSolaris something of the form:
+   sg_inq /dev/rdsk/c5t4d3p0
+should work. If it doesn't add a '-vvv' option. If that is attempted
+on the /dev/dsk/c5t4d3p0 variant an inappropriate ioctl for device
+error will result.
+
+The device names within the /dev directory are typically symbolic
+links to much longer topological names in the /device directory.
+
+In Solaris cd/dvd/bd players seem to be treated as disks and so are
+found in the /dev/rdsk directory. Tape drives appear in the /dev/rmt
+directory.
+
+There is also a sgen (SCSI generic) driver which by default does not
+attach to any device. See the /kernel/drv/sgen.conf file to control
+what is attached. Any attached device will have a device name of
+the form /dev/scsi/c5t4d3 .
+
+Listing available SCSI devices in Solaris seems to be a challenge.
+"Use the 'format' command" advice works but seems a very dangerous
+way to list devices. [It does prompt again before doing any damage.]
+'devfsadm -Cv' cleans out the clutter in the /dev/rdsk directory,
+only leaving what is "live". The "cfgadm -v" command looks promising.
+
+Details
+=======
+The ported utilities listed above, all use SCSI command functions
+declared in sg_cmds_basic.h and sg_cmds_extra.h . Those SCSI command
+functions are implemented in the corresponding ".c" files. The ".c"
+files pass SCSI commands to the host operating system via an interface
+declared in sg_pt.h . There are currently five implementations of that
+interface depending on the host operating system:
+  - sg_pt_linux.c
+  - sg_pt_freebsd.c
+  - sg_pt_osf1.c  [Tru64]
+  - sg_pt_solaris.c
+  - sg_pt_win32.c
+
+The sg_pt_solaris.c file uses the "uscsi" SCSI pass through mechanism. There
+seems to be no corresponding ATA pass through and recent SATA disks do not
+seem to have a SAT layer in front of them (within Solaris). If SAT is
+present (perhaps externally or within a HBA) then that would allow SATA
+disks to accept SCSI commands including the SCSI ATA PASS THROUGH commands.
+
+
+Douglas Gilbert
+5th June 2020
diff --git a/README.tru64 b/README.tru64
new file mode 100644
index 0000000..13c57e4
--- /dev/null
+++ b/README.tru64
@@ -0,0 +1,97 @@
+Introduction
+============
+The Tru64 port of sg3_utils contains those utilities that are _not_
+specific to Linux. In some cases a utility could be ported but
+requires more work. An example is sg_dd which needs more work
+beyond the SCSI command pass through mechanism.
+
+Supported Utilities
+===================
+Here is a list of utilities that have been ported:
+    sg_compare_and_write
+    sg_decode_sense
+    sg_format
+    sg_get_config
+    sg_get_lba_status
+    sg_ident
+    sg_inq          [dropped ATA IDENTIFY DEVICE capability]
+    sg_logs
+    sg_luns
+    sg_modes
+    sg_opcodes
+    sg_persist
+    sg_prevent
+    sg_raw
+    sg_rdac
+    sg_read_block_limits
+    sg_read_buffer
+    sg_read_long
+    sg_readcap
+    sg_reassign
+    sg_referrals
+    sg_requests
+    sg_rmsn
+    sg_rtpg
+    sg_safte
+    sg_sanitize
+    sg_sat_identify
+    sg_sat_phy_event
+    sg_sat_set_features
+    sg_senddiag
+    sg_ses
+    sg_start
+    sg_stpg
+    sg_sync
+    sg_turs
+    sg_unmap
+    sg_verify
+    sg_vpd
+    sg_wr_mode
+    sg_write_buffer
+    sg_write_long
+    sg_write_same
+
+Most utility names are indicative of the main SCSI command
+that they execute.  Some utilities are slightly higher level, for
+example sg_ses fetches SCSI Enclosure Services (SES) status pages and
+can send control pages. Each utility has a man page (placed in
+section 8). An overview of sg3_utils can be found at:
+https://sg.danny.cz/sg/sg3_utils.html .
+A copy of the "sg3_utils.html" file is in the "doc" subdirectory.
+
+This package uses autotools infrastructure with the now common
+"./configure ; make ; make install" sequence needed to build and install
+from the source found in the tarball. If the "./configure" sequence
+fails try using the ./autogen.sh prior to that sequence.
+
+Some man pages have examples which use Linux device names which hopefully
+will not confuse Tru64 users.
+
+
+Details
+=======
+Most of the ported utilities listed above use SCSI command functions
+declared in sg_cmds_*.h headers . Those SCSI command functions are
+implemented in the corresponding ".c" files. The ".c" files pass SCSI
+commands to the host operating system via an interface declared in sg_pt.h .
+There are currently five implementations of that interface depending on
+the host operating system:
+system:
+  - sg_pt_linux.c
+  - sg_pt_osf1.c  [Tru64]
+  - sg_pt_freebsd.c
+  - sg_pt_solaris.c
+  - sg_pt_win32.c
+
+The sg_pt_osf1.c file uses the Tru64 CAM SCSI pass through mechanism.
+
+Tru64 does not have general library support for "long" options
+(e.g. "--verbose") which are used extensively by most of the
+utilities in this package. Rather than change all the utilities
+and their man/web pages a local implementation of the missing
+function "getopt_long()" has been placed in the "getopt_long"
+subdirectory. Currently only the Tru64 port uses it.
+
+
+Douglas Gilbert
+14th January 2013
diff --git a/README.win32 b/README.win32
new file mode 100644
index 0000000..4d64c08
--- /dev/null
+++ b/README.win32
@@ -0,0 +1,247 @@
+Introduction
+============
+The win32 port of sg3_utils contains those utilities that are _not_ specific
+to Linux. One utility for listing available devices, sg_scan, has a
+Windows-specific version for this port.
+
+The dd variants from the sg3_utils package (e.g. sg_dd) rely on too many
+Linux idiosyncrasies to be easily ported. A new package called 'ddpt'
+contains a utility with similar functionality to sg_dd and is available
+for Windows.
+
+The Windows port uses the Microsoft SCSI Pass Through (SPT) interface.
+It has two variants: "SPT" where data is double buffered; and "SPTD"
+where data pointers to the user space are passed to the OS. Only Windows
+2000 and later (i.e. not 95, 98 or ME) support SPT.
+
+Two build environments are catered for: cygwin (see www.cygwin.com) and
+MinGW ("Minimalist GNU for Windows", see www.mingw.org). Both are based in
+the gcc compiler (although other C compilers should have little problem with
+the source code). Cygwin is a more sophisticated, commercial product that
+results in executables that depend on cygwin1.dll . No licensing is required
+since sg3_utils is open source (with either BSD or GPL licenses) but users
+will need to fetch that dll. On the other hand MinGW (and its companion MSYS
+shell) builds freestanding console executables. The Unix library support is
+not as advanced with MinGW which has led to some timing functions being
+compiled out when sg3_utils is built for MinGW.
+
+In later versions of Windows these utilities may need to be "run as
+Administrator" for disks and other devices to be seen. If not those devices
+will simply not be found as calls to query them fail with access permission
+problems.
+
+Supported Utilities
+===================
+Here is a list of utilities that have been ported:
+    sg_bg_ctl
+    sg_compare_and_write
+    sg_decode_sense
+    sg_format
+    sg_get_config
+    sg_get_elem_status
+    sg_get_lba_status
+    sg_ident
+    sg_inq          [dropped ATA IDENTIFY DEVICE capability]
+    sg_logs
+    sg_luns
+    sg_modes
+    sg_opcodes
+    sg_persist
+    sg_prevent
+    sg_raw
+    sg_rdac
+    sg_read_attr
+    sg_read_block_limits
+    sg_read_buffer
+    sg_read_long
+    sg_readcap
+    sg_reassign
+    sg_referrals
+    sg_rep_pip
+    sg_rep_zones
+    sg_requests
+    sg_reset_wp
+    sg_rmsn
+    sg_rtpg
+    sg_safte
+    sg_sanitize
+    sg_sat_identify
+    sg_sat_phy_event
+    sg_sat_read_gplog
+    sg_sat_set_features
+    sg_scan         [this is Windows specific]
+    sg_seek
+    sg_senddiag
+    sg_ses
+    sg_ses_microcode
+    sg_start
+    sg_stpg
+    sg_stream_ctl
+    sg_sync
+    sg_timestamp
+    sg_turs
+    sg_unmap
+    sg_verify
+    sg_vpd
+    sg_wr_mode
+    sg_write_buffer
+    sg_write_long
+    sg_write_same
+    sg_write_verify
+    sg_write_x
+    sg_zone
+
+Most utility names are indicative of the main SCSI command that they execute.
+Some utilities are slightly higher level, for example sg_ses fetches SCSI
+Enclosure Services (SES) status pages and can send control pages. Each
+utility has a man page (placed in section 8). There is summary of the mapping
+between utility names and the SCSI commands they execute in the COVERAGE
+file. An overview of sg3_utils can be found at:
+https://sg.danny.cz/sg/sg3_utils.html .
+A copy of the "sg3_utils.html" file is in the "doc" subdirectory.
+
+Some man pages have examples which use Linux device names which hopefully
+will not confuse Windows users.
+
+Two pass-through variants
+=========================
+The sg_pt_win32.c file uses the Windows SCSI Pass Through interface.
+That is often shortened to SPT or SPTI. There are two DeviceIoControl()
+ioctl variants provided: IOCTL_SCSI_PASS_THROUGH and
+IOCTL_SCSI_PASS_THROUGH_DIRECT. The former involves double handling of
+data (and perhaps an upper limit on the data length associated with
+one SCSI command; MS documentation mentions 16 KB). The "direct"
+variant passes a pointer from the user space and to be faster looks
+and more versatile.
+
+However the "direct" variant has potentially (unquantified) alignment
+requirements and may not be (well) implemented by the hardware driver.
+In practice some users have reported errors (e.g. 1117: a non-descript
+IO error) when the direct variant is used.
+
+Hence the non-direct variant is the default. The default size limit
+on the data buffer is set at 16 KB but if the user asks for more
+the data buffer will be extended. The OS or the hardware drivers
+may reject the extended data buffer but we tried.
+
+The package can be built using the direct variant with:
+   ./configure --enable-win32-spt-direct
+rather than:
+   ./configure
+prior to the 'make' call.
+
+In sg3_utils version 1.31 run-time selection of the direct or indirect
+interface was added with the scsi_pt_win32_direct(int state_direct)
+function declared in sg_pt.h. The default is indirect unless
+'./configure --enable-win32-spt-direct' was used in the build. If
+'state_direct' is 1 then the direct interface is used and if it is 0
+the indirect interface is used.
+
+Both sg_read_buffer and sg_write_buffer can transfer buffers larger
+than 16 KB. So in sg3_utils version 1.31, they use this new function
+to set direct interface mode. This is regardless of whether or
+not "--enable-win32-spt-direct" is given to ./configure .
+
+Details
+=======
+Most of the ported utilities listed above use SCSI command functions
+declared in sg_cmds_*.h headers . Those SCSI command functions are
+implemented in the corresponding ".c" files. The ".c" files pass SCSI
+commands to the host operating system via an interface declared in sg_pt.h .
+There are currently five implementations of that interface depending on
+the host operating system:
+  - sg_pt_linux.c
+  - sg_pt_freebsd.c
+  - sg_pt_osf1.c  [Tru64]
+  - sg_pt_solaris.c
+  - sg_pt_win32.c
+
+The ASPI32 interface which requires a dll from Adaptec is not supported.
+
+The sg_scan utility is a special version for Windows and it attempts to show
+the various available storage device names, one per line. Here is an example
+of sg_scan's output:
+
+# sg_scan
+PD0     [C]     FUJITSU   MHY2160BH         0000
+PD1     [DF]    WD        2500BEV External  1.05  WD-WXE90
+CDROM0  [E]     MATSHITA DVD/CDRW UJDA775  CB03
+
+Here is an example with added bus type:
+
+# sg_scan -b
+PD0     [C]     <Ata  >  FUJITSU   MHY2160BH         0000
+PD1     [DF]    <Usb  >  WD        2500BEV External  1.05  WD-WXE90
+CDROM0  [E]     <Atapi>  MATSHITA DVD/CDRW UJDA775  CB03
+
+Here is an example with added SCSI adapter scan:
+
+# sg_scan -b -s
+PD0     [C]     <Ata  >  ST380011A  8.01
+PD1             <Scsi >  SEAGATE   ST373455SS        2189
+PD2             <Scsi >  ATA       ST3160812AS       D
+PD3             <Scsi >  SEAGATE   ST336754SS        0003
+CDROM0  [F]     <Atapi>  HL-DT-ST DVDRAM GSA-4163B  A103
+TAPE0           <Scsi >  SONY      SDT-7000          0192
+
+SCSI0:0,0,0   claimed=1 pdt=0h dubious  ST380011  A                 8.01
+SCSI1:0,0,0   claimed=1 pdt=5h          HL-DT-ST  DVDRAM GSA-4163B  A103
+SCSI2:0,6,0   claimed=1 pdt=1h          SONY      SDT-7000          0192
+SCSI5:0,17,0  claimed=1 pdt=0h          SEAGATE   ST373455SS        2189
+SCSI5:0,19,0  claimed=1 pdt=0h          ATA       ST3160812AS       D
+SCSI5:0,21,0  claimed=1 pdt=0h          SEAGATE   ST336754SS        0003
+SCSI5:0,112,0 claimed=0 pdt=10h         LSI       PSEUDO DEVICE     2.34
+
+The storage devices scanned are PhysicalDrive<n> (shortened form PD<n> used),
+CDROM<n> (which includes DVD and BD drives) and TAPE<n>. There is also an
+optional SCSI adapter scan with device names of the form SCSI<n>:<b>:<t>:<l> .
+These only come into play for devices that are not claimed by one of the
+storage class drivers. The "LSI PSEUDO DEVICE" device above is an example
+of an unclaimed device. The SCSI adapter scan does not show USB and IEEE
+1394 connected devices.
+
+Volume names (e.g. "C:") that match a storage device (or perhaps a
+partition within that device) are shown in brackets. Notice there can be
+zero, one or more volume names for each storage device. Up to four volume
+names are listed in brackets, if there are more a "+" is added after the
+fourth.
+
+Several utilities have conditional compilation sections based on
+the SG_LIB_MINGW define. For those who want to try native C compilers
+on Windows setting the SG_LIB_MINGW define may help.
+
+Build environments
+==================
+This package uses autotools infrastructure with the now common
+"./configure ; make ; make install" sequence needed to build and install
+from the source found in the tarball. Two Windows environments for building
+Unix code are supported: cygwin and MinGW. If the "./configure" sequence
+fails try using the ./autogen.sh prior to that sequence. The executables
+produced are console applications that can be executed in either a cygwin,
+MSYS or "cmd" shell. Various build options are available by giving
+command line options to "./configure", see the INSTALL file for generic
+information about the build infrastructure.
+
+MinGW can be used to cross built on some Redhat (Linux) platforms. After
+loading the cross build packages, the ./configure call in the normal
+autotools sequence should be replaced by either mingw32-configure or
+mingw64-configure. These scripts will set up the environment for
+the cross build and then call ./configure (so this invocation should be
+made in the top level of the untarred source). Options given to either
+script (e.g. --enable-win32-spt-direct) will be passed through to
+./configure .
+
+Binary and Text files
+=====================
+A problem has been reported with binary output being written in a MinGW
+environment (or executables build by MinGW). Windows has a concept of text
+and binary files which is not found in Unix. Recent versions of MinGW
+default to opening files in text mode. This can lead to binary output
+(such as when the '--raw' option is given) having 0xa (i.e. LF) translated
+to 0xd,0xa (i.e. CR,LF). sg3_utils version 1.26 attempts to fix this
+problem by changing what it knows to be binary output files to "binary
+mode" with the setmode() Windows command.
+
+
+Douglas Gilbert
+6th June 2020
diff --git a/aclocal.m4 b/aclocal.m4
new file mode 100644
index 0000000..7b7c8e5
--- /dev/null
+++ b/aclocal.m4
@@ -0,0 +1,10363 @@
+# generated automatically by aclocal 1.16.5 -*- Autoconf -*-
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+m4_ifndef([AC_CONFIG_MACRO_DIRS], [m4_defun([_AM_CONFIG_MACRO_DIRS], [])m4_defun([AC_CONFIG_MACRO_DIRS], [_AM_CONFIG_MACRO_DIRS($@)])])
+m4_ifndef([AC_AUTOCONF_VERSION],
+  [m4_copy([m4_PACKAGE_VERSION], [AC_AUTOCONF_VERSION])])dnl
+m4_if(m4_defn([AC_AUTOCONF_VERSION]), [2.71],,
+[m4_warning([this file was generated for autoconf 2.71.
+You have another version of autoconf.  It may work, but is not guaranteed to.
+If you have problems, you may need to regenerate the build system entirely.
+To do so, use the procedure documented by the package, typically 'autoreconf'.])])
+
+# libtool.m4 - Configure libtool for the host system. -*-Autoconf-*-
+#
+#   Copyright (C) 1996-2001, 2003-2019, 2021-2022 Free Software
+#   Foundation, Inc.
+#   Written by Gordon Matzigkeit, 1996
+#
+# This file is free software; the Free Software Foundation gives
+# unlimited permission to copy and/or distribute it, with or without
+# modifications, as long as this notice is preserved.
+
+m4_define([_LT_COPYING], [dnl
+# Copyright (C) 2014 Free Software Foundation, Inc.
+# This is free software; see the source for copying conditions.  There is NO
+# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+# GNU Libtool is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of of the License, or
+# (at your option) any later version.
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program or library that is built
+# using GNU Libtool, you may include this file under the  same
+# distribution terms that you use for the rest of that program.
+#
+# GNU Libtool is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+])
+
+# serial 59 LT_INIT
+
+
+# LT_PREREQ(VERSION)
+# ------------------
+# Complain and exit if this libtool version is less that VERSION.
+m4_defun([LT_PREREQ],
+[m4_if(m4_version_compare(m4_defn([LT_PACKAGE_VERSION]), [$1]), -1,
+       [m4_default([$3],
+		   [m4_fatal([Libtool version $1 or higher is required],
+		             63)])],
+       [$2])])
+
+
+# _LT_CHECK_BUILDDIR
+# ------------------
+# Complain if the absolute build directory name contains unusual characters
+m4_defun([_LT_CHECK_BUILDDIR],
+[case `pwd` in
+  *\ * | *\	*)
+    AC_MSG_WARN([Libtool does not cope well with whitespace in `pwd`]) ;;
+esac
+])
+
+
+# LT_INIT([OPTIONS])
+# ------------------
+AC_DEFUN([LT_INIT],
+[AC_PREREQ([2.62])dnl We use AC_PATH_PROGS_FEATURE_CHECK
+AC_REQUIRE([AC_CONFIG_AUX_DIR_DEFAULT])dnl
+AC_BEFORE([$0], [LT_LANG])dnl
+AC_BEFORE([$0], [LT_OUTPUT])dnl
+AC_BEFORE([$0], [LTDL_INIT])dnl
+m4_require([_LT_CHECK_BUILDDIR])dnl
+
+dnl Autoconf doesn't catch unexpanded LT_ macros by default:
+m4_pattern_forbid([^_?LT_[A-Z_]+$])dnl
+m4_pattern_allow([^(_LT_EOF|LT_DLGLOBAL|LT_DLLAZY_OR_NOW|LT_MULTI_MODULE)$])dnl
+dnl aclocal doesn't pull ltoptions.m4, ltsugar.m4, or ltversion.m4
+dnl unless we require an AC_DEFUNed macro:
+AC_REQUIRE([LTOPTIONS_VERSION])dnl
+AC_REQUIRE([LTSUGAR_VERSION])dnl
+AC_REQUIRE([LTVERSION_VERSION])dnl
+AC_REQUIRE([LTOBSOLETE_VERSION])dnl
+m4_require([_LT_PROG_LTMAIN])dnl
+
+_LT_SHELL_INIT([SHELL=${CONFIG_SHELL-/bin/sh}])
+
+dnl Parse OPTIONS
+_LT_SET_OPTIONS([$0], [$1])
+
+# This can be used to rebuild libtool when needed
+LIBTOOL_DEPS=$ltmain
+
+# Always use our own libtool.
+LIBTOOL='$(SHELL) $(top_builddir)/libtool'
+AC_SUBST(LIBTOOL)dnl
+
+_LT_SETUP
+
+# Only expand once:
+m4_define([LT_INIT])
+])# LT_INIT
+
+# Old names:
+AU_ALIAS([AC_PROG_LIBTOOL], [LT_INIT])
+AU_ALIAS([AM_PROG_LIBTOOL], [LT_INIT])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_PROG_LIBTOOL], [])
+dnl AC_DEFUN([AM_PROG_LIBTOOL], [])
+
+
+# _LT_PREPARE_CC_BASENAME
+# -----------------------
+m4_defun([_LT_PREPARE_CC_BASENAME], [
+# Calculate cc_basename.  Skip known compiler wrappers and cross-prefix.
+func_cc_basename ()
+{
+    for cc_temp in @S|@*""; do
+      case $cc_temp in
+        compile | *[[\\/]]compile | ccache | *[[\\/]]ccache ) ;;
+        distcc | *[[\\/]]distcc | purify | *[[\\/]]purify ) ;;
+        \-*) ;;
+        *) break;;
+      esac
+    done
+    func_cc_basename_result=`$ECHO "$cc_temp" | $SED "s%.*/%%; s%^$host_alias-%%"`
+}
+])# _LT_PREPARE_CC_BASENAME
+
+
+# _LT_CC_BASENAME(CC)
+# -------------------
+# It would be clearer to call AC_REQUIREs from _LT_PREPARE_CC_BASENAME,
+# but that macro is also expanded into generated libtool script, which
+# arranges for $SED and $ECHO to be set by different means.
+m4_defun([_LT_CC_BASENAME],
+[m4_require([_LT_PREPARE_CC_BASENAME])dnl
+AC_REQUIRE([_LT_DECL_SED])dnl
+AC_REQUIRE([_LT_PROG_ECHO_BACKSLASH])dnl
+func_cc_basename $1
+cc_basename=$func_cc_basename_result
+])
+
+
+# _LT_FILEUTILS_DEFAULTS
+# ----------------------
+# It is okay to use these file commands and assume they have been set
+# sensibly after 'm4_require([_LT_FILEUTILS_DEFAULTS])'.
+m4_defun([_LT_FILEUTILS_DEFAULTS],
+[: ${CP="cp -f"}
+: ${MV="mv -f"}
+: ${RM="rm -f"}
+])# _LT_FILEUTILS_DEFAULTS
+
+
+# _LT_SETUP
+# ---------
+m4_defun([_LT_SETUP],
+[AC_REQUIRE([AC_CANONICAL_HOST])dnl
+AC_REQUIRE([AC_CANONICAL_BUILD])dnl
+AC_REQUIRE([_LT_PREPARE_SED_QUOTE_VARS])dnl
+AC_REQUIRE([_LT_PROG_ECHO_BACKSLASH])dnl
+
+_LT_DECL([], [PATH_SEPARATOR], [1], [The PATH separator for the build system])dnl
+dnl
+_LT_DECL([], [host_alias], [0], [The host system])dnl
+_LT_DECL([], [host], [0])dnl
+_LT_DECL([], [host_os], [0])dnl
+dnl
+_LT_DECL([], [build_alias], [0], [The build system])dnl
+_LT_DECL([], [build], [0])dnl
+_LT_DECL([], [build_os], [0])dnl
+dnl
+AC_REQUIRE([AC_PROG_CC])dnl
+AC_REQUIRE([LT_PATH_LD])dnl
+AC_REQUIRE([LT_PATH_NM])dnl
+dnl
+AC_REQUIRE([AC_PROG_LN_S])dnl
+test -z "$LN_S" && LN_S="ln -s"
+_LT_DECL([], [LN_S], [1], [Whether we need soft or hard links])dnl
+dnl
+AC_REQUIRE([LT_CMD_MAX_LEN])dnl
+_LT_DECL([objext], [ac_objext], [0], [Object file suffix (normally "o")])dnl
+_LT_DECL([], [exeext], [0], [Executable file suffix (normally "")])dnl
+dnl
+m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+m4_require([_LT_CHECK_SHELL_FEATURES])dnl
+m4_require([_LT_PATH_CONVERSION_FUNCTIONS])dnl
+m4_require([_LT_CMD_RELOAD])dnl
+m4_require([_LT_DECL_FILECMD])dnl
+m4_require([_LT_CHECK_MAGIC_METHOD])dnl
+m4_require([_LT_CHECK_SHAREDLIB_FROM_LINKLIB])dnl
+m4_require([_LT_CMD_OLD_ARCHIVE])dnl
+m4_require([_LT_CMD_GLOBAL_SYMBOLS])dnl
+m4_require([_LT_WITH_SYSROOT])dnl
+m4_require([_LT_CMD_TRUNCATE])dnl
+
+_LT_CONFIG_LIBTOOL_INIT([
+# See if we are running on zsh, and set the options that allow our
+# commands through without removal of \ escapes INIT.
+if test -n "\${ZSH_VERSION+set}"; then
+   setopt NO_GLOB_SUBST
+fi
+])
+if test -n "${ZSH_VERSION+set}"; then
+   setopt NO_GLOB_SUBST
+fi
+
+_LT_CHECK_OBJDIR
+
+m4_require([_LT_TAG_COMPILER])dnl
+
+case $host_os in
+aix3*)
+  # AIX sometimes has problems with the GCC collect2 program.  For some
+  # reason, if we set the COLLECT_NAMES environment variable, the problems
+  # vanish in a puff of smoke.
+  if test set != "${COLLECT_NAMES+set}"; then
+    COLLECT_NAMES=
+    export COLLECT_NAMES
+  fi
+  ;;
+esac
+
+# Global variables:
+ofile=libtool
+can_build_shared=yes
+
+# All known linkers require a '.a' archive for static linking (except MSVC and
+# ICC, which need '.lib').
+libext=a
+
+with_gnu_ld=$lt_cv_prog_gnu_ld
+
+old_CC=$CC
+old_CFLAGS=$CFLAGS
+
+# Set sane defaults for various variables
+test -z "$CC" && CC=cc
+test -z "$LTCC" && LTCC=$CC
+test -z "$LTCFLAGS" && LTCFLAGS=$CFLAGS
+test -z "$LD" && LD=ld
+test -z "$ac_objext" && ac_objext=o
+
+_LT_CC_BASENAME([$compiler])
+
+# Only perform the check for file, if the check method requires it
+test -z "$MAGIC_CMD" && MAGIC_CMD=file
+case $deplibs_check_method in
+file_magic*)
+  if test "$file_magic_cmd" = '$MAGIC_CMD'; then
+    _LT_PATH_MAGIC
+  fi
+  ;;
+esac
+
+# Use C for the default configuration in the libtool script
+LT_SUPPORTED_TAG([CC])
+_LT_LANG_C_CONFIG
+_LT_LANG_DEFAULT_CONFIG
+_LT_CONFIG_COMMANDS
+])# _LT_SETUP
+
+
+# _LT_PREPARE_SED_QUOTE_VARS
+# --------------------------
+# Define a few sed substitution that help us do robust quoting.
+m4_defun([_LT_PREPARE_SED_QUOTE_VARS],
+[# Backslashify metacharacters that are still active within
+# double-quoted strings.
+sed_quote_subst='s/\([["`$\\]]\)/\\\1/g'
+
+# Same as above, but do not quote variable references.
+double_quote_subst='s/\([["`\\]]\)/\\\1/g'
+
+# Sed substitution to delay expansion of an escaped shell variable in a
+# double_quote_subst'ed string.
+delay_variable_subst='s/\\\\\\\\\\\$/\\\\\\$/g'
+
+# Sed substitution to delay expansion of an escaped single quote.
+delay_single_quote_subst='s/'\''/'\'\\\\\\\'\''/g'
+
+# Sed substitution to avoid accidental globbing in evaled expressions
+no_glob_subst='s/\*/\\\*/g'
+])
+
+# _LT_PROG_LTMAIN
+# ---------------
+# Note that this code is called both from 'configure', and 'config.status'
+# now that we use AC_CONFIG_COMMANDS to generate libtool.  Notably,
+# 'config.status' has no value for ac_aux_dir unless we are using Automake,
+# so we pass a copy along to make sure it has a sensible value anyway.
+m4_defun([_LT_PROG_LTMAIN],
+[m4_ifdef([AC_REQUIRE_AUX_FILE], [AC_REQUIRE_AUX_FILE([ltmain.sh])])dnl
+_LT_CONFIG_LIBTOOL_INIT([ac_aux_dir='$ac_aux_dir'])
+ltmain=$ac_aux_dir/ltmain.sh
+])# _LT_PROG_LTMAIN
+
+
+
+# So that we can recreate a full libtool script including additional
+# tags, we accumulate the chunks of code to send to AC_CONFIG_COMMANDS
+# in macros and then make a single call at the end using the 'libtool'
+# label.
+
+
+# _LT_CONFIG_LIBTOOL_INIT([INIT-COMMANDS])
+# ----------------------------------------
+# Register INIT-COMMANDS to be passed to AC_CONFIG_COMMANDS later.
+m4_define([_LT_CONFIG_LIBTOOL_INIT],
+[m4_ifval([$1],
+          [m4_append([_LT_OUTPUT_LIBTOOL_INIT],
+                     [$1
+])])])
+
+# Initialize.
+m4_define([_LT_OUTPUT_LIBTOOL_INIT])
+
+
+# _LT_CONFIG_LIBTOOL([COMMANDS])
+# ------------------------------
+# Register COMMANDS to be passed to AC_CONFIG_COMMANDS later.
+m4_define([_LT_CONFIG_LIBTOOL],
+[m4_ifval([$1],
+          [m4_append([_LT_OUTPUT_LIBTOOL_COMMANDS],
+                     [$1
+])])])
+
+# Initialize.
+m4_define([_LT_OUTPUT_LIBTOOL_COMMANDS])
+
+
+# _LT_CONFIG_SAVE_COMMANDS([COMMANDS], [INIT_COMMANDS])
+# -----------------------------------------------------
+m4_defun([_LT_CONFIG_SAVE_COMMANDS],
+[_LT_CONFIG_LIBTOOL([$1])
+_LT_CONFIG_LIBTOOL_INIT([$2])
+])
+
+
+# _LT_FORMAT_COMMENT([COMMENT])
+# -----------------------------
+# Add leading comment marks to the start of each line, and a trailing
+# full-stop to the whole comment if one is not present already.
+m4_define([_LT_FORMAT_COMMENT],
+[m4_ifval([$1], [
+m4_bpatsubst([m4_bpatsubst([$1], [^ *], [# ])],
+              [['`$\]], [\\\&])]m4_bmatch([$1], [[!?.]$], [], [.])
+)])
+
+
+
+
+
+# _LT_DECL([CONFIGNAME], VARNAME, VALUE, [DESCRIPTION], [IS-TAGGED?])
+# -------------------------------------------------------------------
+# CONFIGNAME is the name given to the value in the libtool script.
+# VARNAME is the (base) name used in the configure script.
+# VALUE may be 0, 1 or 2 for a computed quote escaped value based on
+# VARNAME.  Any other value will be used directly.
+m4_define([_LT_DECL],
+[lt_if_append_uniq([lt_decl_varnames], [$2], [, ],
+    [lt_dict_add_subkey([lt_decl_dict], [$2], [libtool_name],
+	[m4_ifval([$1], [$1], [$2])])
+    lt_dict_add_subkey([lt_decl_dict], [$2], [value], [$3])
+    m4_ifval([$4],
+	[lt_dict_add_subkey([lt_decl_dict], [$2], [description], [$4])])
+    lt_dict_add_subkey([lt_decl_dict], [$2],
+	[tagged?], [m4_ifval([$5], [yes], [no])])])
+])
+
+
+# _LT_TAGDECL([CONFIGNAME], VARNAME, VALUE, [DESCRIPTION])
+# --------------------------------------------------------
+m4_define([_LT_TAGDECL], [_LT_DECL([$1], [$2], [$3], [$4], [yes])])
+
+
+# lt_decl_tag_varnames([SEPARATOR], [VARNAME1...])
+# ------------------------------------------------
+m4_define([lt_decl_tag_varnames],
+[_lt_decl_filter([tagged?], [yes], $@)])
+
+
+# _lt_decl_filter(SUBKEY, VALUE, [SEPARATOR], [VARNAME1..])
+# ---------------------------------------------------------
+m4_define([_lt_decl_filter],
+[m4_case([$#],
+  [0], [m4_fatal([$0: too few arguments: $#])],
+  [1], [m4_fatal([$0: too few arguments: $#: $1])],
+  [2], [lt_dict_filter([lt_decl_dict], [$1], [$2], [], lt_decl_varnames)],
+  [3], [lt_dict_filter([lt_decl_dict], [$1], [$2], [$3], lt_decl_varnames)],
+  [lt_dict_filter([lt_decl_dict], $@)])[]dnl
+])
+
+
+# lt_decl_quote_varnames([SEPARATOR], [VARNAME1...])
+# --------------------------------------------------
+m4_define([lt_decl_quote_varnames],
+[_lt_decl_filter([value], [1], $@)])
+
+
+# lt_decl_dquote_varnames([SEPARATOR], [VARNAME1...])
+# ---------------------------------------------------
+m4_define([lt_decl_dquote_varnames],
+[_lt_decl_filter([value], [2], $@)])
+
+
+# lt_decl_varnames_tagged([SEPARATOR], [VARNAME1...])
+# ---------------------------------------------------
+m4_define([lt_decl_varnames_tagged],
+[m4_assert([$# <= 2])dnl
+_$0(m4_quote(m4_default([$1], [[, ]])),
+    m4_ifval([$2], [[$2]], [m4_dquote(lt_decl_tag_varnames)]),
+    m4_split(m4_normalize(m4_quote(_LT_TAGS)), [ ]))])
+m4_define([_lt_decl_varnames_tagged],
+[m4_ifval([$3], [lt_combine([$1], [$2], [_], $3)])])
+
+
+# lt_decl_all_varnames([SEPARATOR], [VARNAME1...])
+# ------------------------------------------------
+m4_define([lt_decl_all_varnames],
+[_$0(m4_quote(m4_default([$1], [[, ]])),
+     m4_if([$2], [],
+	   m4_quote(lt_decl_varnames),
+	m4_quote(m4_shift($@))))[]dnl
+])
+m4_define([_lt_decl_all_varnames],
+[lt_join($@, lt_decl_varnames_tagged([$1],
+			lt_decl_tag_varnames([[, ]], m4_shift($@))))dnl
+])
+
+
+# _LT_CONFIG_STATUS_DECLARE([VARNAME])
+# ------------------------------------
+# Quote a variable value, and forward it to 'config.status' so that its
+# declaration there will have the same value as in 'configure'.  VARNAME
+# must have a single quote delimited value for this to work.
+m4_define([_LT_CONFIG_STATUS_DECLARE],
+[$1='`$ECHO "$][$1" | $SED "$delay_single_quote_subst"`'])
+
+
+# _LT_CONFIG_STATUS_DECLARATIONS
+# ------------------------------
+# We delimit libtool config variables with single quotes, so when
+# we write them to config.status, we have to be sure to quote all
+# embedded single quotes properly.  In configure, this macro expands
+# each variable declared with _LT_DECL (and _LT_TAGDECL) into:
+#
+#    <var>='`$ECHO "$<var>" | $SED "$delay_single_quote_subst"`'
+m4_defun([_LT_CONFIG_STATUS_DECLARATIONS],
+[m4_foreach([_lt_var], m4_quote(lt_decl_all_varnames),
+    [m4_n([_LT_CONFIG_STATUS_DECLARE(_lt_var)])])])
+
+
+# _LT_LIBTOOL_TAGS
+# ----------------
+# Output comment and list of tags supported by the script
+m4_defun([_LT_LIBTOOL_TAGS],
+[_LT_FORMAT_COMMENT([The names of the tagged configurations supported by this script])dnl
+available_tags='_LT_TAGS'dnl
+])
+
+
+# _LT_LIBTOOL_DECLARE(VARNAME, [TAG])
+# -----------------------------------
+# Extract the dictionary values for VARNAME (optionally with TAG) and
+# expand to a commented shell variable setting:
+#
+#    # Some comment about what VAR is for.
+#    visible_name=$lt_internal_name
+m4_define([_LT_LIBTOOL_DECLARE],
+[_LT_FORMAT_COMMENT(m4_quote(lt_dict_fetch([lt_decl_dict], [$1],
+					   [description])))[]dnl
+m4_pushdef([_libtool_name],
+    m4_quote(lt_dict_fetch([lt_decl_dict], [$1], [libtool_name])))[]dnl
+m4_case(m4_quote(lt_dict_fetch([lt_decl_dict], [$1], [value])),
+    [0], [_libtool_name=[$]$1],
+    [1], [_libtool_name=$lt_[]$1],
+    [2], [_libtool_name=$lt_[]$1],
+    [_libtool_name=lt_dict_fetch([lt_decl_dict], [$1], [value])])[]dnl
+m4_ifval([$2], [_$2])[]m4_popdef([_libtool_name])[]dnl
+])
+
+
+# _LT_LIBTOOL_CONFIG_VARS
+# -----------------------
+# Produce commented declarations of non-tagged libtool config variables
+# suitable for insertion in the LIBTOOL CONFIG section of the 'libtool'
+# script.  Tagged libtool config variables (even for the LIBTOOL CONFIG
+# section) are produced by _LT_LIBTOOL_TAG_VARS.
+m4_defun([_LT_LIBTOOL_CONFIG_VARS],
+[m4_foreach([_lt_var],
+    m4_quote(_lt_decl_filter([tagged?], [no], [], lt_decl_varnames)),
+    [m4_n([_LT_LIBTOOL_DECLARE(_lt_var)])])])
+
+
+# _LT_LIBTOOL_TAG_VARS(TAG)
+# -------------------------
+m4_define([_LT_LIBTOOL_TAG_VARS],
+[m4_foreach([_lt_var], m4_quote(lt_decl_tag_varnames),
+    [m4_n([_LT_LIBTOOL_DECLARE(_lt_var, [$1])])])])
+
+
+# _LT_TAGVAR(VARNAME, [TAGNAME])
+# ------------------------------
+m4_define([_LT_TAGVAR], [m4_ifval([$2], [$1_$2], [$1])])
+
+
+# _LT_CONFIG_COMMANDS
+# -------------------
+# Send accumulated output to $CONFIG_STATUS.  Thanks to the lists of
+# variables for single and double quote escaping we saved from calls
+# to _LT_DECL, we can put quote escaped variables declarations
+# into 'config.status', and then the shell code to quote escape them in
+# for loops in 'config.status'.  Finally, any additional code accumulated
+# from calls to _LT_CONFIG_LIBTOOL_INIT is expanded.
+m4_defun([_LT_CONFIG_COMMANDS],
+[AC_PROVIDE_IFELSE([LT_OUTPUT],
+	dnl If the libtool generation code has been placed in $CONFIG_LT,
+	dnl instead of duplicating it all over again into config.status,
+	dnl then we will have config.status run $CONFIG_LT later, so it
+	dnl needs to know what name is stored there:
+        [AC_CONFIG_COMMANDS([libtool],
+            [$SHELL $CONFIG_LT || AS_EXIT(1)], [CONFIG_LT='$CONFIG_LT'])],
+    dnl If the libtool generation code is destined for config.status,
+    dnl expand the accumulated commands and init code now:
+    [AC_CONFIG_COMMANDS([libtool],
+        [_LT_OUTPUT_LIBTOOL_COMMANDS], [_LT_OUTPUT_LIBTOOL_COMMANDS_INIT])])
+])#_LT_CONFIG_COMMANDS
+
+
+# Initialize.
+m4_define([_LT_OUTPUT_LIBTOOL_COMMANDS_INIT],
+[
+
+# The HP-UX ksh and POSIX shell print the target directory to stdout
+# if CDPATH is set.
+(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
+
+sed_quote_subst='$sed_quote_subst'
+double_quote_subst='$double_quote_subst'
+delay_variable_subst='$delay_variable_subst'
+_LT_CONFIG_STATUS_DECLARATIONS
+LTCC='$LTCC'
+LTCFLAGS='$LTCFLAGS'
+compiler='$compiler_DEFAULT'
+
+# A function that is used when there is no print builtin or printf.
+func_fallback_echo ()
+{
+  eval 'cat <<_LTECHO_EOF
+\$[]1
+_LTECHO_EOF'
+}
+
+# Quote evaled strings.
+for var in lt_decl_all_varnames([[ \
+]], lt_decl_quote_varnames); do
+    case \`eval \\\\\$ECHO \\\\""\\\\\$\$var"\\\\"\` in
+    *[[\\\\\\\`\\"\\\$]]*)
+      eval "lt_\$var=\\\\\\"\\\`\\\$ECHO \\"\\\$\$var\\" | \\\$SED \\"\\\$sed_quote_subst\\"\\\`\\\\\\"" ## exclude from sc_prohibit_nested_quotes
+      ;;
+    *)
+      eval "lt_\$var=\\\\\\"\\\$\$var\\\\\\""
+      ;;
+    esac
+done
+
+# Double-quote double-evaled strings.
+for var in lt_decl_all_varnames([[ \
+]], lt_decl_dquote_varnames); do
+    case \`eval \\\\\$ECHO \\\\""\\\\\$\$var"\\\\"\` in
+    *[[\\\\\\\`\\"\\\$]]*)
+      eval "lt_\$var=\\\\\\"\\\`\\\$ECHO \\"\\\$\$var\\" | \\\$SED -e \\"\\\$double_quote_subst\\" -e \\"\\\$sed_quote_subst\\" -e \\"\\\$delay_variable_subst\\"\\\`\\\\\\"" ## exclude from sc_prohibit_nested_quotes
+      ;;
+    *)
+      eval "lt_\$var=\\\\\\"\\\$\$var\\\\\\""
+      ;;
+    esac
+done
+
+_LT_OUTPUT_LIBTOOL_INIT
+])
+
+# _LT_GENERATED_FILE_INIT(FILE, [COMMENT])
+# ------------------------------------
+# Generate a child script FILE with all initialization necessary to
+# reuse the environment learned by the parent script, and make the
+# file executable.  If COMMENT is supplied, it is inserted after the
+# '#!' sequence but before initialization text begins.  After this
+# macro, additional text can be appended to FILE to form the body of
+# the child script.  The macro ends with non-zero status if the
+# file could not be fully written (such as if the disk is full).
+m4_ifdef([AS_INIT_GENERATED],
+[m4_defun([_LT_GENERATED_FILE_INIT],[AS_INIT_GENERATED($@)])],
+[m4_defun([_LT_GENERATED_FILE_INIT],
+[m4_require([AS_PREPARE])]dnl
+[m4_pushdef([AS_MESSAGE_LOG_FD])]dnl
+[lt_write_fail=0
+cat >$1 <<_ASEOF || lt_write_fail=1
+#! $SHELL
+# Generated by $as_me.
+$2
+SHELL=\${CONFIG_SHELL-$SHELL}
+export SHELL
+_ASEOF
+cat >>$1 <<\_ASEOF || lt_write_fail=1
+AS_SHELL_SANITIZE
+_AS_PREPARE
+exec AS_MESSAGE_FD>&1
+_ASEOF
+test 0 = "$lt_write_fail" && chmod +x $1[]dnl
+m4_popdef([AS_MESSAGE_LOG_FD])])])# _LT_GENERATED_FILE_INIT
+
+# LT_OUTPUT
+# ---------
+# This macro allows early generation of the libtool script (before
+# AC_OUTPUT is called), incase it is used in configure for compilation
+# tests.
+AC_DEFUN([LT_OUTPUT],
+[: ${CONFIG_LT=./config.lt}
+AC_MSG_NOTICE([creating $CONFIG_LT])
+_LT_GENERATED_FILE_INIT(["$CONFIG_LT"],
+[# Run this file to recreate a libtool stub with the current configuration.])
+
+cat >>"$CONFIG_LT" <<\_LTEOF
+lt_cl_silent=false
+exec AS_MESSAGE_LOG_FD>>config.log
+{
+  echo
+  AS_BOX([Running $as_me.])
+} >&AS_MESSAGE_LOG_FD
+
+lt_cl_help="\
+'$as_me' creates a local libtool stub from the current configuration,
+for use in further configure time tests before the real libtool is
+generated.
+
+Usage: $[0] [[OPTIONS]]
+
+  -h, --help      print this help, then exit
+  -V, --version   print version number, then exit
+  -q, --quiet     do not print progress messages
+  -d, --debug     don't remove temporary files
+
+Report bugs to <bug-libtool@gnu.org>."
+
+lt_cl_version="\
+m4_ifset([AC_PACKAGE_NAME], [AC_PACKAGE_NAME ])config.lt[]dnl
+m4_ifset([AC_PACKAGE_VERSION], [ AC_PACKAGE_VERSION])
+configured by $[0], generated by m4_PACKAGE_STRING.
+
+Copyright (C) 2011 Free Software Foundation, Inc.
+This config.lt script is free software; the Free Software Foundation
+gives unlimited permision to copy, distribute and modify it."
+
+while test 0 != $[#]
+do
+  case $[1] in
+    --version | --v* | -V )
+      echo "$lt_cl_version"; exit 0 ;;
+    --help | --h* | -h )
+      echo "$lt_cl_help"; exit 0 ;;
+    --debug | --d* | -d )
+      debug=: ;;
+    --quiet | --q* | --silent | --s* | -q )
+      lt_cl_silent=: ;;
+
+    -*) AC_MSG_ERROR([unrecognized option: $[1]
+Try '$[0] --help' for more information.]) ;;
+
+    *) AC_MSG_ERROR([unrecognized argument: $[1]
+Try '$[0] --help' for more information.]) ;;
+  esac
+  shift
+done
+
+if $lt_cl_silent; then
+  exec AS_MESSAGE_FD>/dev/null
+fi
+_LTEOF
+
+cat >>"$CONFIG_LT" <<_LTEOF
+_LT_OUTPUT_LIBTOOL_COMMANDS_INIT
+_LTEOF
+
+cat >>"$CONFIG_LT" <<\_LTEOF
+AC_MSG_NOTICE([creating $ofile])
+_LT_OUTPUT_LIBTOOL_COMMANDS
+AS_EXIT(0)
+_LTEOF
+chmod +x "$CONFIG_LT"
+
+# configure is writing to config.log, but config.lt does its own redirection,
+# appending to config.log, which fails on DOS, as config.log is still kept
+# open by configure.  Here we exec the FD to /dev/null, effectively closing
+# config.log, so it can be properly (re)opened and appended to by config.lt.
+lt_cl_success=:
+test yes = "$silent" &&
+  lt_config_lt_args="$lt_config_lt_args --quiet"
+exec AS_MESSAGE_LOG_FD>/dev/null
+$SHELL "$CONFIG_LT" $lt_config_lt_args || lt_cl_success=false
+exec AS_MESSAGE_LOG_FD>>config.log
+$lt_cl_success || AS_EXIT(1)
+])# LT_OUTPUT
+
+
+# _LT_CONFIG(TAG)
+# ---------------
+# If TAG is the built-in tag, create an initial libtool script with a
+# default configuration from the untagged config vars.  Otherwise add code
+# to config.status for appending the configuration named by TAG from the
+# matching tagged config vars.
+m4_defun([_LT_CONFIG],
+[m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+_LT_CONFIG_SAVE_COMMANDS([
+  m4_define([_LT_TAG], m4_if([$1], [], [C], [$1]))dnl
+  m4_if(_LT_TAG, [C], [
+    # See if we are running on zsh, and set the options that allow our
+    # commands through without removal of \ escapes.
+    if test -n "${ZSH_VERSION+set}"; then
+      setopt NO_GLOB_SUBST
+    fi
+
+    cfgfile=${ofile}T
+    trap "$RM \"$cfgfile\"; exit 1" 1 2 15
+    $RM "$cfgfile"
+
+    cat <<_LT_EOF >> "$cfgfile"
+#! $SHELL
+# Generated automatically by $as_me ($PACKAGE) $VERSION
+# NOTE: Changes made to this file will be lost: look at ltmain.sh.
+
+# Provide generalized library-building support services.
+# Written by Gordon Matzigkeit, 1996
+
+_LT_COPYING
+_LT_LIBTOOL_TAGS
+
+# Configured defaults for sys_lib_dlsearch_path munging.
+: \${LT_SYS_LIBRARY_PATH="$configure_time_lt_sys_library_path"}
+
+# ### BEGIN LIBTOOL CONFIG
+_LT_LIBTOOL_CONFIG_VARS
+_LT_LIBTOOL_TAG_VARS
+# ### END LIBTOOL CONFIG
+
+_LT_EOF
+
+    cat <<'_LT_EOF' >> "$cfgfile"
+
+# ### BEGIN FUNCTIONS SHARED WITH CONFIGURE
+
+_LT_PREPARE_MUNGE_PATH_LIST
+_LT_PREPARE_CC_BASENAME
+
+# ### END FUNCTIONS SHARED WITH CONFIGURE
+
+_LT_EOF
+
+  case $host_os in
+  aix3*)
+    cat <<\_LT_EOF >> "$cfgfile"
+# AIX sometimes has problems with the GCC collect2 program.  For some
+# reason, if we set the COLLECT_NAMES environment variable, the problems
+# vanish in a puff of smoke.
+if test set != "${COLLECT_NAMES+set}"; then
+  COLLECT_NAMES=
+  export COLLECT_NAMES
+fi
+_LT_EOF
+    ;;
+  esac
+
+  _LT_PROG_LTMAIN
+
+  # We use sed instead of cat because bash on DJGPP gets confused if
+  # if finds mixed CR/LF and LF-only lines.  Since sed operates in
+  # text mode, it properly converts lines to CR/LF.  This bash problem
+  # is reportedly fixed, but why not run on old versions too?
+  $SED '$q' "$ltmain" >> "$cfgfile" \
+     || (rm -f "$cfgfile"; exit 1)
+
+   mv -f "$cfgfile" "$ofile" ||
+    (rm -f "$ofile" && cp "$cfgfile" "$ofile" && rm -f "$cfgfile")
+  chmod +x "$ofile"
+],
+[cat <<_LT_EOF >> "$ofile"
+
+dnl Unfortunately we have to use $1 here, since _LT_TAG is not expanded
+dnl in a comment (ie after a #).
+# ### BEGIN LIBTOOL TAG CONFIG: $1
+_LT_LIBTOOL_TAG_VARS(_LT_TAG)
+# ### END LIBTOOL TAG CONFIG: $1
+_LT_EOF
+])dnl /m4_if
+],
+[m4_if([$1], [], [
+    PACKAGE='$PACKAGE'
+    VERSION='$VERSION'
+    RM='$RM'
+    ofile='$ofile'], [])
+])dnl /_LT_CONFIG_SAVE_COMMANDS
+])# _LT_CONFIG
+
+
+# LT_SUPPORTED_TAG(TAG)
+# ---------------------
+# Trace this macro to discover what tags are supported by the libtool
+# --tag option, using:
+#    autoconf --trace 'LT_SUPPORTED_TAG:$1'
+AC_DEFUN([LT_SUPPORTED_TAG], [])
+
+
+# C support is built-in for now
+m4_define([_LT_LANG_C_enabled], [])
+m4_define([_LT_TAGS], [])
+
+
+# LT_LANG(LANG)
+# -------------
+# Enable libtool support for the given language if not already enabled.
+AC_DEFUN([LT_LANG],
+[AC_BEFORE([$0], [LT_OUTPUT])dnl
+m4_case([$1],
+  [C],			[_LT_LANG(C)],
+  [C++],		[_LT_LANG(CXX)],
+  [Go],			[_LT_LANG(GO)],
+  [Java],		[_LT_LANG(GCJ)],
+  [Fortran 77],		[_LT_LANG(F77)],
+  [Fortran],		[_LT_LANG(FC)],
+  [Windows Resource],	[_LT_LANG(RC)],
+  [m4_ifdef([_LT_LANG_]$1[_CONFIG],
+    [_LT_LANG($1)],
+    [m4_fatal([$0: unsupported language: "$1"])])])dnl
+])# LT_LANG
+
+
+# _LT_LANG(LANGNAME)
+# ------------------
+m4_defun([_LT_LANG],
+[m4_ifdef([_LT_LANG_]$1[_enabled], [],
+  [LT_SUPPORTED_TAG([$1])dnl
+  m4_append([_LT_TAGS], [$1 ])dnl
+  m4_define([_LT_LANG_]$1[_enabled], [])dnl
+  _LT_LANG_$1_CONFIG($1)])dnl
+])# _LT_LANG
+
+
+m4_ifndef([AC_PROG_GO], [
+# NOTE: This macro has been submitted for inclusion into   #
+#  GNU Autoconf as AC_PROG_GO.  When it is available in    #
+#  a released version of Autoconf we should remove this    #
+#  macro and use it instead.                               #
+m4_defun([AC_PROG_GO],
+[AC_LANG_PUSH(Go)dnl
+AC_ARG_VAR([GOC],     [Go compiler command])dnl
+AC_ARG_VAR([GOFLAGS], [Go compiler flags])dnl
+_AC_ARG_VAR_LDFLAGS()dnl
+AC_CHECK_TOOL(GOC, gccgo)
+if test -z "$GOC"; then
+  if test -n "$ac_tool_prefix"; then
+    AC_CHECK_PROG(GOC, [${ac_tool_prefix}gccgo], [${ac_tool_prefix}gccgo])
+  fi
+fi
+if test -z "$GOC"; then
+  AC_CHECK_PROG(GOC, gccgo, gccgo, false)
+fi
+])#m4_defun
+])#m4_ifndef
+
+
+# _LT_LANG_DEFAULT_CONFIG
+# -----------------------
+m4_defun([_LT_LANG_DEFAULT_CONFIG],
+[AC_PROVIDE_IFELSE([AC_PROG_CXX],
+  [LT_LANG(CXX)],
+  [m4_define([AC_PROG_CXX], defn([AC_PROG_CXX])[LT_LANG(CXX)])])
+
+AC_PROVIDE_IFELSE([AC_PROG_F77],
+  [LT_LANG(F77)],
+  [m4_define([AC_PROG_F77], defn([AC_PROG_F77])[LT_LANG(F77)])])
+
+AC_PROVIDE_IFELSE([AC_PROG_FC],
+  [LT_LANG(FC)],
+  [m4_define([AC_PROG_FC], defn([AC_PROG_FC])[LT_LANG(FC)])])
+
+dnl The call to [A][M_PROG_GCJ] is quoted like that to stop aclocal
+dnl pulling things in needlessly.
+AC_PROVIDE_IFELSE([AC_PROG_GCJ],
+  [LT_LANG(GCJ)],
+  [AC_PROVIDE_IFELSE([A][M_PROG_GCJ],
+    [LT_LANG(GCJ)],
+    [AC_PROVIDE_IFELSE([LT_PROG_GCJ],
+      [LT_LANG(GCJ)],
+      [m4_ifdef([AC_PROG_GCJ],
+	[m4_define([AC_PROG_GCJ], defn([AC_PROG_GCJ])[LT_LANG(GCJ)])])
+       m4_ifdef([A][M_PROG_GCJ],
+	[m4_define([A][M_PROG_GCJ], defn([A][M_PROG_GCJ])[LT_LANG(GCJ)])])
+       m4_ifdef([LT_PROG_GCJ],
+	[m4_define([LT_PROG_GCJ], defn([LT_PROG_GCJ])[LT_LANG(GCJ)])])])])])
+
+AC_PROVIDE_IFELSE([AC_PROG_GO],
+  [LT_LANG(GO)],
+  [m4_define([AC_PROG_GO], defn([AC_PROG_GO])[LT_LANG(GO)])])
+
+AC_PROVIDE_IFELSE([LT_PROG_RC],
+  [LT_LANG(RC)],
+  [m4_define([LT_PROG_RC], defn([LT_PROG_RC])[LT_LANG(RC)])])
+])# _LT_LANG_DEFAULT_CONFIG
+
+# Obsolete macros:
+AU_DEFUN([AC_LIBTOOL_CXX], [LT_LANG(C++)])
+AU_DEFUN([AC_LIBTOOL_F77], [LT_LANG(Fortran 77)])
+AU_DEFUN([AC_LIBTOOL_FC], [LT_LANG(Fortran)])
+AU_DEFUN([AC_LIBTOOL_GCJ], [LT_LANG(Java)])
+AU_DEFUN([AC_LIBTOOL_RC], [LT_LANG(Windows Resource)])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_LIBTOOL_CXX], [])
+dnl AC_DEFUN([AC_LIBTOOL_F77], [])
+dnl AC_DEFUN([AC_LIBTOOL_FC], [])
+dnl AC_DEFUN([AC_LIBTOOL_GCJ], [])
+dnl AC_DEFUN([AC_LIBTOOL_RC], [])
+
+
+# _LT_TAG_COMPILER
+# ----------------
+m4_defun([_LT_TAG_COMPILER],
+[AC_REQUIRE([AC_PROG_CC])dnl
+
+_LT_DECL([LTCC], [CC], [1], [A C compiler])dnl
+_LT_DECL([LTCFLAGS], [CFLAGS], [1], [LTCC compiler flags])dnl
+_LT_TAGDECL([CC], [compiler], [1], [A language specific compiler])dnl
+_LT_TAGDECL([with_gcc], [GCC], [0], [Is the compiler the GNU compiler?])dnl
+
+# If no C compiler was specified, use CC.
+LTCC=${LTCC-"$CC"}
+
+# If no C compiler flags were specified, use CFLAGS.
+LTCFLAGS=${LTCFLAGS-"$CFLAGS"}
+
+# Allow CC to be a program name with arguments.
+compiler=$CC
+])# _LT_TAG_COMPILER
+
+
+# _LT_COMPILER_BOILERPLATE
+# ------------------------
+# Check for compiler boilerplate output or warnings with
+# the simple compiler test code.
+m4_defun([_LT_COMPILER_BOILERPLATE],
+[m4_require([_LT_DECL_SED])dnl
+ac_outfile=conftest.$ac_objext
+echo "$lt_simple_compile_test_code" >conftest.$ac_ext
+eval "$ac_compile" 2>&1 >/dev/null | $SED '/^$/d; /^ *+/d' >conftest.err
+_lt_compiler_boilerplate=`cat conftest.err`
+$RM conftest*
+])# _LT_COMPILER_BOILERPLATE
+
+
+# _LT_LINKER_BOILERPLATE
+# ----------------------
+# Check for linker boilerplate output or warnings with
+# the simple link test code.
+m4_defun([_LT_LINKER_BOILERPLATE],
+[m4_require([_LT_DECL_SED])dnl
+ac_outfile=conftest.$ac_objext
+echo "$lt_simple_link_test_code" >conftest.$ac_ext
+eval "$ac_link" 2>&1 >/dev/null | $SED '/^$/d; /^ *+/d' >conftest.err
+_lt_linker_boilerplate=`cat conftest.err`
+$RM -r conftest*
+])# _LT_LINKER_BOILERPLATE
+
+# _LT_REQUIRED_DARWIN_CHECKS
+# -------------------------
+m4_defun_once([_LT_REQUIRED_DARWIN_CHECKS],[
+  case $host_os in
+    rhapsody* | darwin*)
+    AC_CHECK_TOOL([DSYMUTIL], [dsymutil], [:])
+    AC_CHECK_TOOL([NMEDIT], [nmedit], [:])
+    AC_CHECK_TOOL([LIPO], [lipo], [:])
+    AC_CHECK_TOOL([OTOOL], [otool], [:])
+    AC_CHECK_TOOL([OTOOL64], [otool64], [:])
+    _LT_DECL([], [DSYMUTIL], [1],
+      [Tool to manipulate archived DWARF debug symbol files on Mac OS X])
+    _LT_DECL([], [NMEDIT], [1],
+      [Tool to change global to local symbols on Mac OS X])
+    _LT_DECL([], [LIPO], [1],
+      [Tool to manipulate fat objects and archives on Mac OS X])
+    _LT_DECL([], [OTOOL], [1],
+      [ldd/readelf like tool for Mach-O binaries on Mac OS X])
+    _LT_DECL([], [OTOOL64], [1],
+      [ldd/readelf like tool for 64 bit Mach-O binaries on Mac OS X 10.4])
+
+    AC_CACHE_CHECK([for -single_module linker flag],[lt_cv_apple_cc_single_mod],
+      [lt_cv_apple_cc_single_mod=no
+      if test -z "$LT_MULTI_MODULE"; then
+	# By default we will add the -single_module flag. You can override
+	# by either setting the environment variable LT_MULTI_MODULE
+	# non-empty at configure time, or by adding -multi_module to the
+	# link flags.
+	rm -rf libconftest.dylib*
+	echo "int foo(void){return 1;}" > conftest.c
+	echo "$LTCC $LTCFLAGS $LDFLAGS -o libconftest.dylib \
+-dynamiclib -Wl,-single_module conftest.c" >&AS_MESSAGE_LOG_FD
+	$LTCC $LTCFLAGS $LDFLAGS -o libconftest.dylib \
+	  -dynamiclib -Wl,-single_module conftest.c 2>conftest.err
+        _lt_result=$?
+	# If there is a non-empty error log, and "single_module"
+	# appears in it, assume the flag caused a linker warning
+        if test -s conftest.err && $GREP single_module conftest.err; then
+	  cat conftest.err >&AS_MESSAGE_LOG_FD
+	# Otherwise, if the output was created with a 0 exit code from
+	# the compiler, it worked.
+	elif test -f libconftest.dylib && test 0 = "$_lt_result"; then
+	  lt_cv_apple_cc_single_mod=yes
+	else
+	  cat conftest.err >&AS_MESSAGE_LOG_FD
+	fi
+	rm -rf libconftest.dylib*
+	rm -f conftest.*
+      fi])
+
+    AC_CACHE_CHECK([for -exported_symbols_list linker flag],
+      [lt_cv_ld_exported_symbols_list],
+      [lt_cv_ld_exported_symbols_list=no
+      save_LDFLAGS=$LDFLAGS
+      echo "_main" > conftest.sym
+      LDFLAGS="$LDFLAGS -Wl,-exported_symbols_list,conftest.sym"
+      AC_LINK_IFELSE([AC_LANG_PROGRAM([],[])],
+	[lt_cv_ld_exported_symbols_list=yes],
+	[lt_cv_ld_exported_symbols_list=no])
+	LDFLAGS=$save_LDFLAGS
+    ])
+
+    AC_CACHE_CHECK([for -force_load linker flag],[lt_cv_ld_force_load],
+      [lt_cv_ld_force_load=no
+      cat > conftest.c << _LT_EOF
+int forced_loaded() { return 2;}
+_LT_EOF
+      echo "$LTCC $LTCFLAGS -c -o conftest.o conftest.c" >&AS_MESSAGE_LOG_FD
+      $LTCC $LTCFLAGS -c -o conftest.o conftest.c 2>&AS_MESSAGE_LOG_FD
+      echo "$AR $AR_FLAGS libconftest.a conftest.o" >&AS_MESSAGE_LOG_FD
+      $AR $AR_FLAGS libconftest.a conftest.o 2>&AS_MESSAGE_LOG_FD
+      echo "$RANLIB libconftest.a" >&AS_MESSAGE_LOG_FD
+      $RANLIB libconftest.a 2>&AS_MESSAGE_LOG_FD
+      cat > conftest.c << _LT_EOF
+int main() { return 0;}
+_LT_EOF
+      echo "$LTCC $LTCFLAGS $LDFLAGS -o conftest conftest.c -Wl,-force_load,./libconftest.a" >&AS_MESSAGE_LOG_FD
+      $LTCC $LTCFLAGS $LDFLAGS -o conftest conftest.c -Wl,-force_load,./libconftest.a 2>conftest.err
+      _lt_result=$?
+      if test -s conftest.err && $GREP force_load conftest.err; then
+	cat conftest.err >&AS_MESSAGE_LOG_FD
+      elif test -f conftest && test 0 = "$_lt_result" && $GREP forced_load conftest >/dev/null 2>&1; then
+	lt_cv_ld_force_load=yes
+      else
+	cat conftest.err >&AS_MESSAGE_LOG_FD
+      fi
+        rm -f conftest.err libconftest.a conftest conftest.c
+        rm -rf conftest.dSYM
+    ])
+    case $host_os in
+    rhapsody* | darwin1.[[012]])
+      _lt_dar_allow_undefined='$wl-undefined ${wl}suppress' ;;
+    darwin1.*)
+      _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;;
+    darwin*)
+      case $MACOSX_DEPLOYMENT_TARGET,$host in
+        10.[[012]],*|,*powerpc*-darwin[[5-8]]*)
+          _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;;
+        *)
+          _lt_dar_allow_undefined='$wl-undefined ${wl}dynamic_lookup' ;;
+      esac
+    ;;
+  esac
+    if test yes = "$lt_cv_apple_cc_single_mod"; then
+      _lt_dar_single_mod='$single_module'
+    fi
+    if test yes = "$lt_cv_ld_exported_symbols_list"; then
+      _lt_dar_export_syms=' $wl-exported_symbols_list,$output_objdir/$libname-symbols.expsym'
+    else
+      _lt_dar_export_syms='~$NMEDIT -s $output_objdir/$libname-symbols.expsym $lib'
+    fi
+    if test : != "$DSYMUTIL" && test no = "$lt_cv_ld_force_load"; then
+      _lt_dsymutil='~$DSYMUTIL $lib || :'
+    else
+      _lt_dsymutil=
+    fi
+    ;;
+  esac
+])
+
+
+# _LT_DARWIN_LINKER_FEATURES([TAG])
+# ---------------------------------
+# Checks for linker and compiler features on darwin
+m4_defun([_LT_DARWIN_LINKER_FEATURES],
+[
+  m4_require([_LT_REQUIRED_DARWIN_CHECKS])
+  _LT_TAGVAR(archive_cmds_need_lc, $1)=no
+  _LT_TAGVAR(hardcode_direct, $1)=no
+  _LT_TAGVAR(hardcode_automatic, $1)=yes
+  _LT_TAGVAR(hardcode_shlibpath_var, $1)=unsupported
+  if test yes = "$lt_cv_ld_force_load"; then
+    _LT_TAGVAR(whole_archive_flag_spec, $1)='`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience $wl-force_load,$conv\"; done; func_echo_all \"$new_convenience\"`'
+    m4_case([$1], [F77], [_LT_TAGVAR(compiler_needs_object, $1)=yes],
+                  [FC],  [_LT_TAGVAR(compiler_needs_object, $1)=yes])
+  else
+    _LT_TAGVAR(whole_archive_flag_spec, $1)=''
+  fi
+  _LT_TAGVAR(link_all_deplibs, $1)=yes
+  _LT_TAGVAR(allow_undefined_flag, $1)=$_lt_dar_allow_undefined
+  case $cc_basename in
+     ifort*|nagfor*) _lt_dar_can_shared=yes ;;
+     *) _lt_dar_can_shared=$GCC ;;
+  esac
+  if test yes = "$_lt_dar_can_shared"; then
+    output_verbose_link_cmd=func_echo_all
+    _LT_TAGVAR(archive_cmds, $1)="\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$libobjs \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring $_lt_dar_single_mod$_lt_dsymutil"
+    _LT_TAGVAR(module_cmds, $1)="\$CC \$allow_undefined_flag -o \$lib -bundle \$libobjs \$deplibs \$compiler_flags$_lt_dsymutil"
+    _LT_TAGVAR(archive_expsym_cmds, $1)="$SED 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$libobjs \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring $_lt_dar_single_mod$_lt_dar_export_syms$_lt_dsymutil"
+    _LT_TAGVAR(module_expsym_cmds, $1)="$SED -e 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC \$allow_undefined_flag -o \$lib -bundle \$libobjs \$deplibs \$compiler_flags$_lt_dar_export_syms$_lt_dsymutil"
+    m4_if([$1], [CXX],
+[   if test yes != "$lt_cv_apple_cc_single_mod"; then
+      _LT_TAGVAR(archive_cmds, $1)="\$CC -r -keep_private_externs -nostdlib -o \$lib-master.o \$libobjs~\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$lib-master.o \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring$_lt_dsymutil"
+      _LT_TAGVAR(archive_expsym_cmds, $1)="$SED 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC -r -keep_private_externs -nostdlib -o \$lib-master.o \$libobjs~\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$lib-master.o \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring$_lt_dar_export_syms$_lt_dsymutil"
+    fi
+],[])
+  else
+  _LT_TAGVAR(ld_shlibs, $1)=no
+  fi
+])
+
+# _LT_SYS_MODULE_PATH_AIX([TAGNAME])
+# ----------------------------------
+# Links a minimal program and checks the executable
+# for the system default hardcoded library path. In most cases,
+# this is /usr/lib:/lib, but when the MPI compilers are used
+# the location of the communication and MPI libs are included too.
+# If we don't find anything, use the default library path according
+# to the aix ld manual.
+# Store the results from the different compilers for each TAGNAME.
+# Allow to override them for all tags through lt_cv_aix_libpath.
+m4_defun([_LT_SYS_MODULE_PATH_AIX],
+[m4_require([_LT_DECL_SED])dnl
+if test set = "${lt_cv_aix_libpath+set}"; then
+  aix_libpath=$lt_cv_aix_libpath
+else
+  AC_CACHE_VAL([_LT_TAGVAR([lt_cv_aix_libpath_], [$1])],
+  [AC_LINK_IFELSE([AC_LANG_PROGRAM],[
+  lt_aix_libpath_sed='[
+      /Import File Strings/,/^$/ {
+	  /^0/ {
+	      s/^0  *\([^ ]*\) *$/\1/
+	      p
+	  }
+      }]'
+  _LT_TAGVAR([lt_cv_aix_libpath_], [$1])=`dump -H conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"`
+  # Check for a 64-bit object if we didn't find anything.
+  if test -z "$_LT_TAGVAR([lt_cv_aix_libpath_], [$1])"; then
+    _LT_TAGVAR([lt_cv_aix_libpath_], [$1])=`dump -HX64 conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"`
+  fi],[])
+  if test -z "$_LT_TAGVAR([lt_cv_aix_libpath_], [$1])"; then
+    _LT_TAGVAR([lt_cv_aix_libpath_], [$1])=/usr/lib:/lib
+  fi
+  ])
+  aix_libpath=$_LT_TAGVAR([lt_cv_aix_libpath_], [$1])
+fi
+])# _LT_SYS_MODULE_PATH_AIX
+
+
+# _LT_SHELL_INIT(ARG)
+# -------------------
+m4_define([_LT_SHELL_INIT],
+[m4_divert_text([M4SH-INIT], [$1
+])])# _LT_SHELL_INIT
+
+
+
+# _LT_PROG_ECHO_BACKSLASH
+# -----------------------
+# Find how we can fake an echo command that does not interpret backslash.
+# In particular, with Autoconf 2.60 or later we add some code to the start
+# of the generated configure script that will find a shell with a builtin
+# printf (that we can use as an echo command).
+m4_defun([_LT_PROG_ECHO_BACKSLASH],
+[ECHO='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'
+ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO
+ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO$ECHO
+
+AC_MSG_CHECKING([how to print strings])
+# Test print first, because it will be a builtin if present.
+if test "X`( print -r -- -n ) 2>/dev/null`" = X-n && \
+   test "X`print -r -- $ECHO 2>/dev/null`" = "X$ECHO"; then
+  ECHO='print -r --'
+elif test "X`printf %s $ECHO 2>/dev/null`" = "X$ECHO"; then
+  ECHO='printf %s\n'
+else
+  # Use this function as a fallback that always works.
+  func_fallback_echo ()
+  {
+    eval 'cat <<_LTECHO_EOF
+$[]1
+_LTECHO_EOF'
+  }
+  ECHO='func_fallback_echo'
+fi
+
+# func_echo_all arg...
+# Invoke $ECHO with all args, space-separated.
+func_echo_all ()
+{
+    $ECHO "$*"
+}
+
+case $ECHO in
+  printf*) AC_MSG_RESULT([printf]) ;;
+  print*) AC_MSG_RESULT([print -r]) ;;
+  *) AC_MSG_RESULT([cat]) ;;
+esac
+
+m4_ifdef([_AS_DETECT_SUGGESTED],
+[_AS_DETECT_SUGGESTED([
+  test -n "${ZSH_VERSION+set}${BASH_VERSION+set}" || (
+    ECHO='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'
+    ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO
+    ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO$ECHO
+    PATH=/empty FPATH=/empty; export PATH FPATH
+    test "X`printf %s $ECHO`" = "X$ECHO" \
+      || test "X`print -r -- $ECHO`" = "X$ECHO" )])])
+
+_LT_DECL([], [SHELL], [1], [Shell to use when invoking shell scripts])
+_LT_DECL([], [ECHO], [1], [An echo program that protects backslashes])
+])# _LT_PROG_ECHO_BACKSLASH
+
+
+# _LT_WITH_SYSROOT
+# ----------------
+AC_DEFUN([_LT_WITH_SYSROOT],
+[m4_require([_LT_DECL_SED])dnl
+AC_MSG_CHECKING([for sysroot])
+AC_ARG_WITH([sysroot],
+[AS_HELP_STRING([--with-sysroot@<:@=DIR@:>@],
+  [Search for dependent libraries within DIR (or the compiler's sysroot
+   if not specified).])],
+[], [with_sysroot=no])
+
+dnl lt_sysroot will always be passed unquoted.  We quote it here
+dnl in case the user passed a directory name.
+lt_sysroot=
+case $with_sysroot in #(
+ yes)
+   if test yes = "$GCC"; then
+     lt_sysroot=`$CC --print-sysroot 2>/dev/null`
+   fi
+   ;; #(
+ /*)
+   lt_sysroot=`echo "$with_sysroot" | $SED -e "$sed_quote_subst"`
+   ;; #(
+ no|'')
+   ;; #(
+ *)
+   AC_MSG_RESULT([$with_sysroot])
+   AC_MSG_ERROR([The sysroot must be an absolute path.])
+   ;;
+esac
+
+ AC_MSG_RESULT([${lt_sysroot:-no}])
+_LT_DECL([], [lt_sysroot], [0], [The root where to search for ]dnl
+[dependent libraries, and where our libraries should be installed.])])
+
+# _LT_ENABLE_LOCK
+# ---------------
+m4_defun([_LT_ENABLE_LOCK],
+[AC_ARG_ENABLE([libtool-lock],
+  [AS_HELP_STRING([--disable-libtool-lock],
+    [avoid locking (might break parallel builds)])])
+test no = "$enable_libtool_lock" || enable_libtool_lock=yes
+
+# Some flags need to be propagated to the compiler or linker for good
+# libtool support.
+case $host in
+ia64-*-hpux*)
+  # Find out what ABI is being produced by ac_compile, and set mode
+  # options accordingly.
+  echo 'int i;' > conftest.$ac_ext
+  if AC_TRY_EVAL(ac_compile); then
+    case `$FILECMD conftest.$ac_objext` in
+      *ELF-32*)
+	HPUX_IA64_MODE=32
+	;;
+      *ELF-64*)
+	HPUX_IA64_MODE=64
+	;;
+    esac
+  fi
+  rm -rf conftest*
+  ;;
+*-*-irix6*)
+  # Find out what ABI is being produced by ac_compile, and set linker
+  # options accordingly.
+  echo '[#]line '$LINENO' "configure"' > conftest.$ac_ext
+  if AC_TRY_EVAL(ac_compile); then
+    if test yes = "$lt_cv_prog_gnu_ld"; then
+      case `$FILECMD conftest.$ac_objext` in
+	*32-bit*)
+	  LD="${LD-ld} -melf32bsmip"
+	  ;;
+	*N32*)
+	  LD="${LD-ld} -melf32bmipn32"
+	  ;;
+	*64-bit*)
+	  LD="${LD-ld} -melf64bmip"
+	;;
+      esac
+    else
+      case `$FILECMD conftest.$ac_objext` in
+	*32-bit*)
+	  LD="${LD-ld} -32"
+	  ;;
+	*N32*)
+	  LD="${LD-ld} -n32"
+	  ;;
+	*64-bit*)
+	  LD="${LD-ld} -64"
+	  ;;
+      esac
+    fi
+  fi
+  rm -rf conftest*
+  ;;
+
+mips64*-*linux*)
+  # Find out what ABI is being produced by ac_compile, and set linker
+  # options accordingly.
+  echo '[#]line '$LINENO' "configure"' > conftest.$ac_ext
+  if AC_TRY_EVAL(ac_compile); then
+    emul=elf
+    case `$FILECMD conftest.$ac_objext` in
+      *32-bit*)
+	emul="${emul}32"
+	;;
+      *64-bit*)
+	emul="${emul}64"
+	;;
+    esac
+    case `$FILECMD conftest.$ac_objext` in
+      *MSB*)
+	emul="${emul}btsmip"
+	;;
+      *LSB*)
+	emul="${emul}ltsmip"
+	;;
+    esac
+    case `$FILECMD conftest.$ac_objext` in
+      *N32*)
+	emul="${emul}n32"
+	;;
+    esac
+    LD="${LD-ld} -m $emul"
+  fi
+  rm -rf conftest*
+  ;;
+
+x86_64-*kfreebsd*-gnu|x86_64-*linux*|powerpc*-*linux*| \
+s390*-*linux*|s390*-*tpf*|sparc*-*linux*)
+  # Find out what ABI is being produced by ac_compile, and set linker
+  # options accordingly.  Note that the listed cases only cover the
+  # situations where additional linker options are needed (such as when
+  # doing 32-bit compilation for a host where ld defaults to 64-bit, or
+  # vice versa); the common cases where no linker options are needed do
+  # not appear in the list.
+  echo 'int i;' > conftest.$ac_ext
+  if AC_TRY_EVAL(ac_compile); then
+    case `$FILECMD conftest.o` in
+      *32-bit*)
+	case $host in
+	  x86_64-*kfreebsd*-gnu)
+	    LD="${LD-ld} -m elf_i386_fbsd"
+	    ;;
+	  x86_64-*linux*)
+	    case `$FILECMD conftest.o` in
+	      *x86-64*)
+		LD="${LD-ld} -m elf32_x86_64"
+		;;
+	      *)
+		LD="${LD-ld} -m elf_i386"
+		;;
+	    esac
+	    ;;
+	  powerpc64le-*linux*)
+	    LD="${LD-ld} -m elf32lppclinux"
+	    ;;
+	  powerpc64-*linux*)
+	    LD="${LD-ld} -m elf32ppclinux"
+	    ;;
+	  s390x-*linux*)
+	    LD="${LD-ld} -m elf_s390"
+	    ;;
+	  sparc64-*linux*)
+	    LD="${LD-ld} -m elf32_sparc"
+	    ;;
+	esac
+	;;
+      *64-bit*)
+	case $host in
+	  x86_64-*kfreebsd*-gnu)
+	    LD="${LD-ld} -m elf_x86_64_fbsd"
+	    ;;
+	  x86_64-*linux*)
+	    LD="${LD-ld} -m elf_x86_64"
+	    ;;
+	  powerpcle-*linux*)
+	    LD="${LD-ld} -m elf64lppc"
+	    ;;
+	  powerpc-*linux*)
+	    LD="${LD-ld} -m elf64ppc"
+	    ;;
+	  s390*-*linux*|s390*-*tpf*)
+	    LD="${LD-ld} -m elf64_s390"
+	    ;;
+	  sparc*-*linux*)
+	    LD="${LD-ld} -m elf64_sparc"
+	    ;;
+	esac
+	;;
+    esac
+  fi
+  rm -rf conftest*
+  ;;
+
+*-*-sco3.2v5*)
+  # On SCO OpenServer 5, we need -belf to get full-featured binaries.
+  SAVE_CFLAGS=$CFLAGS
+  CFLAGS="$CFLAGS -belf"
+  AC_CACHE_CHECK([whether the C compiler needs -belf], lt_cv_cc_needs_belf,
+    [AC_LANG_PUSH(C)
+     AC_LINK_IFELSE([AC_LANG_PROGRAM([[]],[[]])],[lt_cv_cc_needs_belf=yes],[lt_cv_cc_needs_belf=no])
+     AC_LANG_POP])
+  if test yes != "$lt_cv_cc_needs_belf"; then
+    # this is probably gcc 2.8.0, egcs 1.0 or newer; no need for -belf
+    CFLAGS=$SAVE_CFLAGS
+  fi
+  ;;
+*-*solaris*)
+  # Find out what ABI is being produced by ac_compile, and set linker
+  # options accordingly.
+  echo 'int i;' > conftest.$ac_ext
+  if AC_TRY_EVAL(ac_compile); then
+    case `$FILECMD conftest.o` in
+    *64-bit*)
+      case $lt_cv_prog_gnu_ld in
+      yes*)
+        case $host in
+        i?86-*-solaris*|x86_64-*-solaris*)
+          LD="${LD-ld} -m elf_x86_64"
+          ;;
+        sparc*-*-solaris*)
+          LD="${LD-ld} -m elf64_sparc"
+          ;;
+        esac
+        # GNU ld 2.21 introduced _sol2 emulations.  Use them if available.
+        if ${LD-ld} -V | grep _sol2 >/dev/null 2>&1; then
+          LD=${LD-ld}_sol2
+        fi
+        ;;
+      *)
+	if ${LD-ld} -64 -r -o conftest2.o conftest.o >/dev/null 2>&1; then
+	  LD="${LD-ld} -64"
+	fi
+	;;
+      esac
+      ;;
+    esac
+  fi
+  rm -rf conftest*
+  ;;
+esac
+
+need_locks=$enable_libtool_lock
+])# _LT_ENABLE_LOCK
+
+
+# _LT_PROG_AR
+# -----------
+m4_defun([_LT_PROG_AR],
+[AC_CHECK_TOOLS(AR, [ar], false)
+: ${AR=ar}
+_LT_DECL([], [AR], [1], [The archiver])
+
+# Use ARFLAGS variable as AR's operation code to sync the variable naming with
+# Automake.  If both AR_FLAGS and ARFLAGS are specified, AR_FLAGS should have
+# higher priority because thats what people were doing historically (setting
+# ARFLAGS for automake and AR_FLAGS for libtool).  FIXME: Make the AR_FLAGS
+# variable obsoleted/removed.
+
+test ${AR_FLAGS+y} || AR_FLAGS=${ARFLAGS-cr}
+lt_ar_flags=$AR_FLAGS
+_LT_DECL([], [lt_ar_flags], [0], [Flags to create an archive (by configure)])
+
+# Make AR_FLAGS overridable by 'make ARFLAGS='.  Don't try to run-time override
+# by AR_FLAGS because that was never working and AR_FLAGS is about to die.
+_LT_DECL([], [AR_FLAGS], [\@S|@{ARFLAGS-"\@S|@lt_ar_flags"}],
+         [Flags to create an archive])
+
+AC_CACHE_CHECK([for archiver @FILE support], [lt_cv_ar_at_file],
+  [lt_cv_ar_at_file=no
+   AC_COMPILE_IFELSE([AC_LANG_PROGRAM],
+     [echo conftest.$ac_objext > conftest.lst
+      lt_ar_try='$AR $AR_FLAGS libconftest.a @conftest.lst >&AS_MESSAGE_LOG_FD'
+      AC_TRY_EVAL([lt_ar_try])
+      if test 0 -eq "$ac_status"; then
+	# Ensure the archiver fails upon bogus file names.
+	rm -f conftest.$ac_objext libconftest.a
+	AC_TRY_EVAL([lt_ar_try])
+	if test 0 -ne "$ac_status"; then
+          lt_cv_ar_at_file=@
+        fi
+      fi
+      rm -f conftest.* libconftest.a
+     ])
+  ])
+
+if test no = "$lt_cv_ar_at_file"; then
+  archiver_list_spec=
+else
+  archiver_list_spec=$lt_cv_ar_at_file
+fi
+_LT_DECL([], [archiver_list_spec], [1],
+  [How to feed a file listing to the archiver])
+])# _LT_PROG_AR
+
+
+# _LT_CMD_OLD_ARCHIVE
+# -------------------
+m4_defun([_LT_CMD_OLD_ARCHIVE],
+[_LT_PROG_AR
+
+AC_CHECK_TOOL(STRIP, strip, :)
+test -z "$STRIP" && STRIP=:
+_LT_DECL([], [STRIP], [1], [A symbol stripping program])
+
+AC_CHECK_TOOL(RANLIB, ranlib, :)
+test -z "$RANLIB" && RANLIB=:
+_LT_DECL([], [RANLIB], [1],
+    [Commands used to install an old-style archive])
+
+# Determine commands to create old-style static archives.
+old_archive_cmds='$AR $AR_FLAGS $oldlib$oldobjs'
+old_postinstall_cmds='chmod 644 $oldlib'
+old_postuninstall_cmds=
+
+if test -n "$RANLIB"; then
+  case $host_os in
+  bitrig* | openbsd*)
+    old_postinstall_cmds="$old_postinstall_cmds~\$RANLIB -t \$tool_oldlib"
+    ;;
+  *)
+    old_postinstall_cmds="$old_postinstall_cmds~\$RANLIB \$tool_oldlib"
+    ;;
+  esac
+  old_archive_cmds="$old_archive_cmds~\$RANLIB \$tool_oldlib"
+fi
+
+case $host_os in
+  darwin*)
+    lock_old_archive_extraction=yes ;;
+  *)
+    lock_old_archive_extraction=no ;;
+esac
+_LT_DECL([], [old_postinstall_cmds], [2])
+_LT_DECL([], [old_postuninstall_cmds], [2])
+_LT_TAGDECL([], [old_archive_cmds], [2],
+    [Commands used to build an old-style archive])
+_LT_DECL([], [lock_old_archive_extraction], [0],
+    [Whether to use a lock for old archive extraction])
+])# _LT_CMD_OLD_ARCHIVE
+
+
+# _LT_COMPILER_OPTION(MESSAGE, VARIABLE-NAME, FLAGS,
+#		[OUTPUT-FILE], [ACTION-SUCCESS], [ACTION-FAILURE])
+# ----------------------------------------------------------------
+# Check whether the given compiler option works
+AC_DEFUN([_LT_COMPILER_OPTION],
+[m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+m4_require([_LT_DECL_SED])dnl
+AC_CACHE_CHECK([$1], [$2],
+  [$2=no
+   m4_if([$4], , [ac_outfile=conftest.$ac_objext], [ac_outfile=$4])
+   echo "$lt_simple_compile_test_code" > conftest.$ac_ext
+   lt_compiler_flag="$3"  ## exclude from sc_useless_quotes_in_assignment
+   # Insert the option either (1) after the last *FLAGS variable, or
+   # (2) before a word containing "conftest.", or (3) at the end.
+   # Note that $ac_compile itself does not contain backslashes and begins
+   # with a dollar sign (not a hyphen), so the echo should work correctly.
+   # The option is referenced via a variable to avoid confusing sed.
+   lt_compile=`echo "$ac_compile" | $SED \
+   -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
+   -e 's: [[^ ]]*conftest\.: $lt_compiler_flag&:; t' \
+   -e 's:$: $lt_compiler_flag:'`
+   (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&AS_MESSAGE_LOG_FD)
+   (eval "$lt_compile" 2>conftest.err)
+   ac_status=$?
+   cat conftest.err >&AS_MESSAGE_LOG_FD
+   echo "$as_me:$LINENO: \$? = $ac_status" >&AS_MESSAGE_LOG_FD
+   if (exit $ac_status) && test -s "$ac_outfile"; then
+     # The compiler can only warn and ignore the option if not recognized
+     # So say no if there are warnings other than the usual output.
+     $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' >conftest.exp
+     $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2
+     if test ! -s conftest.er2 || diff conftest.exp conftest.er2 >/dev/null; then
+       $2=yes
+     fi
+   fi
+   $RM conftest*
+])
+
+if test yes = "[$]$2"; then
+    m4_if([$5], , :, [$5])
+else
+    m4_if([$6], , :, [$6])
+fi
+])# _LT_COMPILER_OPTION
+
+# Old name:
+AU_ALIAS([AC_LIBTOOL_COMPILER_OPTION], [_LT_COMPILER_OPTION])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_LIBTOOL_COMPILER_OPTION], [])
+
+
+# _LT_LINKER_OPTION(MESSAGE, VARIABLE-NAME, FLAGS,
+#                  [ACTION-SUCCESS], [ACTION-FAILURE])
+# ----------------------------------------------------
+# Check whether the given linker option works
+AC_DEFUN([_LT_LINKER_OPTION],
+[m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+m4_require([_LT_DECL_SED])dnl
+AC_CACHE_CHECK([$1], [$2],
+  [$2=no
+   save_LDFLAGS=$LDFLAGS
+   LDFLAGS="$LDFLAGS $3"
+   echo "$lt_simple_link_test_code" > conftest.$ac_ext
+   if (eval $ac_link 2>conftest.err) && test -s conftest$ac_exeext; then
+     # The linker can only warn and ignore the option if not recognized
+     # So say no if there are warnings
+     if test -s conftest.err; then
+       # Append any errors to the config.log.
+       cat conftest.err 1>&AS_MESSAGE_LOG_FD
+       $ECHO "$_lt_linker_boilerplate" | $SED '/^$/d' > conftest.exp
+       $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2
+       if diff conftest.exp conftest.er2 >/dev/null; then
+         $2=yes
+       fi
+     else
+       $2=yes
+     fi
+   fi
+   $RM -r conftest*
+   LDFLAGS=$save_LDFLAGS
+])
+
+if test yes = "[$]$2"; then
+    m4_if([$4], , :, [$4])
+else
+    m4_if([$5], , :, [$5])
+fi
+])# _LT_LINKER_OPTION
+
+# Old name:
+AU_ALIAS([AC_LIBTOOL_LINKER_OPTION], [_LT_LINKER_OPTION])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_LIBTOOL_LINKER_OPTION], [])
+
+
+# LT_CMD_MAX_LEN
+#---------------
+AC_DEFUN([LT_CMD_MAX_LEN],
+[AC_REQUIRE([AC_CANONICAL_HOST])dnl
+# find the maximum length of command line arguments
+AC_MSG_CHECKING([the maximum length of command line arguments])
+AC_CACHE_VAL([lt_cv_sys_max_cmd_len], [dnl
+  i=0
+  teststring=ABCD
+
+  case $build_os in
+  msdosdjgpp*)
+    # On DJGPP, this test can blow up pretty badly due to problems in libc
+    # (any single argument exceeding 2000 bytes causes a buffer overrun
+    # during glob expansion).  Even if it were fixed, the result of this
+    # check would be larger than it should be.
+    lt_cv_sys_max_cmd_len=12288;    # 12K is about right
+    ;;
+
+  gnu*)
+    # Under GNU Hurd, this test is not required because there is
+    # no limit to the length of command line arguments.
+    # Libtool will interpret -1 as no limit whatsoever
+    lt_cv_sys_max_cmd_len=-1;
+    ;;
+
+  cygwin* | mingw* | cegcc*)
+    # On Win9x/ME, this test blows up -- it succeeds, but takes
+    # about 5 minutes as the teststring grows exponentially.
+    # Worse, since 9x/ME are not pre-emptively multitasking,
+    # you end up with a "frozen" computer, even though with patience
+    # the test eventually succeeds (with a max line length of 256k).
+    # Instead, let's just punt: use the minimum linelength reported by
+    # all of the supported platforms: 8192 (on NT/2K/XP).
+    lt_cv_sys_max_cmd_len=8192;
+    ;;
+
+  mint*)
+    # On MiNT this can take a long time and run out of memory.
+    lt_cv_sys_max_cmd_len=8192;
+    ;;
+
+  amigaos*)
+    # On AmigaOS with pdksh, this test takes hours, literally.
+    # So we just punt and use a minimum line length of 8192.
+    lt_cv_sys_max_cmd_len=8192;
+    ;;
+
+  bitrig* | darwin* | dragonfly* | freebsd* | midnightbsd* | netbsd* | openbsd*)
+    # This has been around since 386BSD, at least.  Likely further.
+    if test -x /sbin/sysctl; then
+      lt_cv_sys_max_cmd_len=`/sbin/sysctl -n kern.argmax`
+    elif test -x /usr/sbin/sysctl; then
+      lt_cv_sys_max_cmd_len=`/usr/sbin/sysctl -n kern.argmax`
+    else
+      lt_cv_sys_max_cmd_len=65536	# usable default for all BSDs
+    fi
+    # And add a safety zone
+    lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 4`
+    lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \* 3`
+    ;;
+
+  interix*)
+    # We know the value 262144 and hardcode it with a safety zone (like BSD)
+    lt_cv_sys_max_cmd_len=196608
+    ;;
+
+  os2*)
+    # The test takes a long time on OS/2.
+    lt_cv_sys_max_cmd_len=8192
+    ;;
+
+  osf*)
+    # Dr. Hans Ekkehard Plesser reports seeing a kernel panic running configure
+    # due to this test when exec_disable_arg_limit is 1 on Tru64. It is not
+    # nice to cause kernel panics so lets avoid the loop below.
+    # First set a reasonable default.
+    lt_cv_sys_max_cmd_len=16384
+    #
+    if test -x /sbin/sysconfig; then
+      case `/sbin/sysconfig -q proc exec_disable_arg_limit` in
+        *1*) lt_cv_sys_max_cmd_len=-1 ;;
+      esac
+    fi
+    ;;
+  sco3.2v5*)
+    lt_cv_sys_max_cmd_len=102400
+    ;;
+  sysv5* | sco5v6* | sysv4.2uw2*)
+    kargmax=`grep ARG_MAX /etc/conf/cf.d/stune 2>/dev/null`
+    if test -n "$kargmax"; then
+      lt_cv_sys_max_cmd_len=`echo $kargmax | $SED 's/.*[[	 ]]//'`
+    else
+      lt_cv_sys_max_cmd_len=32768
+    fi
+    ;;
+  *)
+    lt_cv_sys_max_cmd_len=`(getconf ARG_MAX) 2> /dev/null`
+    if test -n "$lt_cv_sys_max_cmd_len" && \
+       test undefined != "$lt_cv_sys_max_cmd_len"; then
+      lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 4`
+      lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \* 3`
+    else
+      # Make teststring a little bigger before we do anything with it.
+      # a 1K string should be a reasonable start.
+      for i in 1 2 3 4 5 6 7 8; do
+        teststring=$teststring$teststring
+      done
+      SHELL=${SHELL-${CONFIG_SHELL-/bin/sh}}
+      # If test is not a shell built-in, we'll probably end up computing a
+      # maximum length that is only half of the actual maximum length, but
+      # we can't tell.
+      while { test X`env echo "$teststring$teststring" 2>/dev/null` \
+	         = "X$teststring$teststring"; } >/dev/null 2>&1 &&
+	      test 17 != "$i" # 1/2 MB should be enough
+      do
+        i=`expr $i + 1`
+        teststring=$teststring$teststring
+      done
+      # Only check the string length outside the loop.
+      lt_cv_sys_max_cmd_len=`expr "X$teststring" : ".*" 2>&1`
+      teststring=
+      # Add a significant safety factor because C++ compilers can tack on
+      # massive amounts of additional arguments before passing them to the
+      # linker.  It appears as though 1/2 is a usable value.
+      lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 2`
+    fi
+    ;;
+  esac
+])
+if test -n "$lt_cv_sys_max_cmd_len"; then
+  AC_MSG_RESULT($lt_cv_sys_max_cmd_len)
+else
+  AC_MSG_RESULT(none)
+fi
+max_cmd_len=$lt_cv_sys_max_cmd_len
+_LT_DECL([], [max_cmd_len], [0],
+    [What is the maximum length of a command?])
+])# LT_CMD_MAX_LEN
+
+# Old name:
+AU_ALIAS([AC_LIBTOOL_SYS_MAX_CMD_LEN], [LT_CMD_MAX_LEN])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_LIBTOOL_SYS_MAX_CMD_LEN], [])
+
+
+# _LT_HEADER_DLFCN
+# ----------------
+m4_defun([_LT_HEADER_DLFCN],
+[AC_CHECK_HEADERS([dlfcn.h], [], [], [AC_INCLUDES_DEFAULT])dnl
+])# _LT_HEADER_DLFCN
+
+
+# _LT_TRY_DLOPEN_SELF (ACTION-IF-TRUE, ACTION-IF-TRUE-W-USCORE,
+#                      ACTION-IF-FALSE, ACTION-IF-CROSS-COMPILING)
+# ----------------------------------------------------------------
+m4_defun([_LT_TRY_DLOPEN_SELF],
+[m4_require([_LT_HEADER_DLFCN])dnl
+if test yes = "$cross_compiling"; then :
+  [$4]
+else
+  lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
+  lt_status=$lt_dlunknown
+  cat > conftest.$ac_ext <<_LT_EOF
+[#line $LINENO "configure"
+#include "confdefs.h"
+
+#if HAVE_DLFCN_H
+#include <dlfcn.h>
+#endif
+
+#include <stdio.h>
+
+#ifdef RTLD_GLOBAL
+#  define LT_DLGLOBAL		RTLD_GLOBAL
+#else
+#  ifdef DL_GLOBAL
+#    define LT_DLGLOBAL		DL_GLOBAL
+#  else
+#    define LT_DLGLOBAL		0
+#  endif
+#endif
+
+/* We may have to define LT_DLLAZY_OR_NOW in the command line if we
+   find out it does not work in some platform. */
+#ifndef LT_DLLAZY_OR_NOW
+#  ifdef RTLD_LAZY
+#    define LT_DLLAZY_OR_NOW		RTLD_LAZY
+#  else
+#    ifdef DL_LAZY
+#      define LT_DLLAZY_OR_NOW		DL_LAZY
+#    else
+#      ifdef RTLD_NOW
+#        define LT_DLLAZY_OR_NOW	RTLD_NOW
+#      else
+#        ifdef DL_NOW
+#          define LT_DLLAZY_OR_NOW	DL_NOW
+#        else
+#          define LT_DLLAZY_OR_NOW	0
+#        endif
+#      endif
+#    endif
+#  endif
+#endif
+
+/* When -fvisibility=hidden is used, assume the code has been annotated
+   correspondingly for the symbols needed.  */
+#if defined __GNUC__ && (((__GNUC__ == 3) && (__GNUC_MINOR__ >= 3)) || (__GNUC__ > 3))
+int fnord () __attribute__((visibility("default")));
+#endif
+
+int fnord () { return 42; }
+int main ()
+{
+  void *self = dlopen (0, LT_DLGLOBAL|LT_DLLAZY_OR_NOW);
+  int status = $lt_dlunknown;
+
+  if (self)
+    {
+      if (dlsym (self,"fnord"))       status = $lt_dlno_uscore;
+      else
+        {
+	  if (dlsym( self,"_fnord"))  status = $lt_dlneed_uscore;
+          else puts (dlerror ());
+	}
+      /* dlclose (self); */
+    }
+  else
+    puts (dlerror ());
+
+  return status;
+}]
+_LT_EOF
+  if AC_TRY_EVAL(ac_link) && test -s "conftest$ac_exeext" 2>/dev/null; then
+    (./conftest; exit; ) >&AS_MESSAGE_LOG_FD 2>/dev/null
+    lt_status=$?
+    case x$lt_status in
+      x$lt_dlno_uscore) $1 ;;
+      x$lt_dlneed_uscore) $2 ;;
+      x$lt_dlunknown|x*) $3 ;;
+    esac
+  else :
+    # compilation failed
+    $3
+  fi
+fi
+rm -fr conftest*
+])# _LT_TRY_DLOPEN_SELF
+
+
+# LT_SYS_DLOPEN_SELF
+# ------------------
+AC_DEFUN([LT_SYS_DLOPEN_SELF],
+[m4_require([_LT_HEADER_DLFCN])dnl
+if test yes != "$enable_dlopen"; then
+  enable_dlopen=unknown
+  enable_dlopen_self=unknown
+  enable_dlopen_self_static=unknown
+else
+  lt_cv_dlopen=no
+  lt_cv_dlopen_libs=
+
+  case $host_os in
+  beos*)
+    lt_cv_dlopen=load_add_on
+    lt_cv_dlopen_libs=
+    lt_cv_dlopen_self=yes
+    ;;
+
+  mingw* | pw32* | cegcc*)
+    lt_cv_dlopen=LoadLibrary
+    lt_cv_dlopen_libs=
+    ;;
+
+  cygwin*)
+    lt_cv_dlopen=dlopen
+    lt_cv_dlopen_libs=
+    ;;
+
+  darwin*)
+    # if libdl is installed we need to link against it
+    AC_CHECK_LIB([dl], [dlopen],
+		[lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-ldl],[
+    lt_cv_dlopen=dyld
+    lt_cv_dlopen_libs=
+    lt_cv_dlopen_self=yes
+    ])
+    ;;
+
+  tpf*)
+    # Don't try to run any link tests for TPF.  We know it's impossible
+    # because TPF is a cross-compiler, and we know how we open DSOs.
+    lt_cv_dlopen=dlopen
+    lt_cv_dlopen_libs=
+    lt_cv_dlopen_self=no
+    ;;
+
+  *)
+    AC_CHECK_FUNC([shl_load],
+	  [lt_cv_dlopen=shl_load],
+      [AC_CHECK_LIB([dld], [shl_load],
+	    [lt_cv_dlopen=shl_load lt_cv_dlopen_libs=-ldld],
+	[AC_CHECK_FUNC([dlopen],
+	      [lt_cv_dlopen=dlopen],
+	  [AC_CHECK_LIB([dl], [dlopen],
+		[lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-ldl],
+	    [AC_CHECK_LIB([svld], [dlopen],
+		  [lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-lsvld],
+	      [AC_CHECK_LIB([dld], [dld_link],
+		    [lt_cv_dlopen=dld_link lt_cv_dlopen_libs=-ldld])
+	      ])
+	    ])
+	  ])
+	])
+      ])
+    ;;
+  esac
+
+  if test no = "$lt_cv_dlopen"; then
+    enable_dlopen=no
+  else
+    enable_dlopen=yes
+  fi
+
+  case $lt_cv_dlopen in
+  dlopen)
+    save_CPPFLAGS=$CPPFLAGS
+    test yes = "$ac_cv_header_dlfcn_h" && CPPFLAGS="$CPPFLAGS -DHAVE_DLFCN_H"
+
+    save_LDFLAGS=$LDFLAGS
+    wl=$lt_prog_compiler_wl eval LDFLAGS=\"\$LDFLAGS $export_dynamic_flag_spec\"
+
+    save_LIBS=$LIBS
+    LIBS="$lt_cv_dlopen_libs $LIBS"
+
+    AC_CACHE_CHECK([whether a program can dlopen itself],
+	  lt_cv_dlopen_self, [dnl
+	  _LT_TRY_DLOPEN_SELF(
+	    lt_cv_dlopen_self=yes, lt_cv_dlopen_self=yes,
+	    lt_cv_dlopen_self=no, lt_cv_dlopen_self=cross)
+    ])
+
+    if test yes = "$lt_cv_dlopen_self"; then
+      wl=$lt_prog_compiler_wl eval LDFLAGS=\"\$LDFLAGS $lt_prog_compiler_static\"
+      AC_CACHE_CHECK([whether a statically linked program can dlopen itself],
+	  lt_cv_dlopen_self_static, [dnl
+	  _LT_TRY_DLOPEN_SELF(
+	    lt_cv_dlopen_self_static=yes, lt_cv_dlopen_self_static=yes,
+	    lt_cv_dlopen_self_static=no,  lt_cv_dlopen_self_static=cross)
+      ])
+    fi
+
+    CPPFLAGS=$save_CPPFLAGS
+    LDFLAGS=$save_LDFLAGS
+    LIBS=$save_LIBS
+    ;;
+  esac
+
+  case $lt_cv_dlopen_self in
+  yes|no) enable_dlopen_self=$lt_cv_dlopen_self ;;
+  *) enable_dlopen_self=unknown ;;
+  esac
+
+  case $lt_cv_dlopen_self_static in
+  yes|no) enable_dlopen_self_static=$lt_cv_dlopen_self_static ;;
+  *) enable_dlopen_self_static=unknown ;;
+  esac
+fi
+_LT_DECL([dlopen_support], [enable_dlopen], [0],
+	 [Whether dlopen is supported])
+_LT_DECL([dlopen_self], [enable_dlopen_self], [0],
+	 [Whether dlopen of programs is supported])
+_LT_DECL([dlopen_self_static], [enable_dlopen_self_static], [0],
+	 [Whether dlopen of statically linked programs is supported])
+])# LT_SYS_DLOPEN_SELF
+
+# Old name:
+AU_ALIAS([AC_LIBTOOL_DLOPEN_SELF], [LT_SYS_DLOPEN_SELF])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_LIBTOOL_DLOPEN_SELF], [])
+
+
+# _LT_COMPILER_C_O([TAGNAME])
+# ---------------------------
+# Check to see if options -c and -o are simultaneously supported by compiler.
+# This macro does not hard code the compiler like AC_PROG_CC_C_O.
+m4_defun([_LT_COMPILER_C_O],
+[m4_require([_LT_DECL_SED])dnl
+m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+m4_require([_LT_TAG_COMPILER])dnl
+AC_CACHE_CHECK([if $compiler supports -c -o file.$ac_objext],
+  [_LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)],
+  [_LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)=no
+   $RM -r conftest 2>/dev/null
+   mkdir conftest
+   cd conftest
+   mkdir out
+   echo "$lt_simple_compile_test_code" > conftest.$ac_ext
+
+   lt_compiler_flag="-o out/conftest2.$ac_objext"
+   # Insert the option either (1) after the last *FLAGS variable, or
+   # (2) before a word containing "conftest.", or (3) at the end.
+   # Note that $ac_compile itself does not contain backslashes and begins
+   # with a dollar sign (not a hyphen), so the echo should work correctly.
+   lt_compile=`echo "$ac_compile" | $SED \
+   -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
+   -e 's: [[^ ]]*conftest\.: $lt_compiler_flag&:; t' \
+   -e 's:$: $lt_compiler_flag:'`
+   (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&AS_MESSAGE_LOG_FD)
+   (eval "$lt_compile" 2>out/conftest.err)
+   ac_status=$?
+   cat out/conftest.err >&AS_MESSAGE_LOG_FD
+   echo "$as_me:$LINENO: \$? = $ac_status" >&AS_MESSAGE_LOG_FD
+   if (exit $ac_status) && test -s out/conftest2.$ac_objext
+   then
+     # The compiler can only warn and ignore the option if not recognized
+     # So say no if there are warnings
+     $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' > out/conftest.exp
+     $SED '/^$/d; /^ *+/d' out/conftest.err >out/conftest.er2
+     if test ! -s out/conftest.er2 || diff out/conftest.exp out/conftest.er2 >/dev/null; then
+       _LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)=yes
+     fi
+   fi
+   chmod u+w . 2>&AS_MESSAGE_LOG_FD
+   $RM conftest*
+   # SGI C++ compiler will create directory out/ii_files/ for
+   # template instantiation
+   test -d out/ii_files && $RM out/ii_files/* && rmdir out/ii_files
+   $RM out/* && rmdir out
+   cd ..
+   $RM -r conftest
+   $RM conftest*
+])
+_LT_TAGDECL([compiler_c_o], [lt_cv_prog_compiler_c_o], [1],
+	[Does compiler simultaneously support -c and -o options?])
+])# _LT_COMPILER_C_O
+
+
+# _LT_COMPILER_FILE_LOCKS([TAGNAME])
+# ----------------------------------
+# Check to see if we can do hard links to lock some files if needed
+m4_defun([_LT_COMPILER_FILE_LOCKS],
+[m4_require([_LT_ENABLE_LOCK])dnl
+m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+_LT_COMPILER_C_O([$1])
+
+hard_links=nottested
+if test no = "$_LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)" && test no != "$need_locks"; then
+  # do not overwrite the value of need_locks provided by the user
+  AC_MSG_CHECKING([if we can lock with hard links])
+  hard_links=yes
+  $RM conftest*
+  ln conftest.a conftest.b 2>/dev/null && hard_links=no
+  touch conftest.a
+  ln conftest.a conftest.b 2>&5 || hard_links=no
+  ln conftest.a conftest.b 2>/dev/null && hard_links=no
+  AC_MSG_RESULT([$hard_links])
+  if test no = "$hard_links"; then
+    AC_MSG_WARN(['$CC' does not support '-c -o', so 'make -j' may be unsafe])
+    need_locks=warn
+  fi
+else
+  need_locks=no
+fi
+_LT_DECL([], [need_locks], [1], [Must we lock files when doing compilation?])
+])# _LT_COMPILER_FILE_LOCKS
+
+
+# _LT_CHECK_OBJDIR
+# ----------------
+m4_defun([_LT_CHECK_OBJDIR],
+[AC_CACHE_CHECK([for objdir], [lt_cv_objdir],
+[rm -f .libs 2>/dev/null
+mkdir .libs 2>/dev/null
+if test -d .libs; then
+  lt_cv_objdir=.libs
+else
+  # MS-DOS does not allow filenames that begin with a dot.
+  lt_cv_objdir=_libs
+fi
+rmdir .libs 2>/dev/null])
+objdir=$lt_cv_objdir
+_LT_DECL([], [objdir], [0],
+         [The name of the directory that contains temporary libtool files])dnl
+m4_pattern_allow([LT_OBJDIR])dnl
+AC_DEFINE_UNQUOTED([LT_OBJDIR], "$lt_cv_objdir/",
+  [Define to the sub-directory where libtool stores uninstalled libraries.])
+])# _LT_CHECK_OBJDIR
+
+
+# _LT_LINKER_HARDCODE_LIBPATH([TAGNAME])
+# --------------------------------------
+# Check hardcoding attributes.
+m4_defun([_LT_LINKER_HARDCODE_LIBPATH],
+[AC_MSG_CHECKING([how to hardcode library paths into programs])
+_LT_TAGVAR(hardcode_action, $1)=
+if test -n "$_LT_TAGVAR(hardcode_libdir_flag_spec, $1)" ||
+   test -n "$_LT_TAGVAR(runpath_var, $1)" ||
+   test yes = "$_LT_TAGVAR(hardcode_automatic, $1)"; then
+
+  # We can hardcode non-existent directories.
+  if test no != "$_LT_TAGVAR(hardcode_direct, $1)" &&
+     # If the only mechanism to avoid hardcoding is shlibpath_var, we
+     # have to relink, otherwise we might link with an installed library
+     # when we should be linking with a yet-to-be-installed one
+     ## test no != "$_LT_TAGVAR(hardcode_shlibpath_var, $1)" &&
+     test no != "$_LT_TAGVAR(hardcode_minus_L, $1)"; then
+    # Linking always hardcodes the temporary library directory.
+    _LT_TAGVAR(hardcode_action, $1)=relink
+  else
+    # We can link without hardcoding, and we can hardcode nonexisting dirs.
+    _LT_TAGVAR(hardcode_action, $1)=immediate
+  fi
+else
+  # We cannot hardcode anything, or else we can only hardcode existing
+  # directories.
+  _LT_TAGVAR(hardcode_action, $1)=unsupported
+fi
+AC_MSG_RESULT([$_LT_TAGVAR(hardcode_action, $1)])
+
+if test relink = "$_LT_TAGVAR(hardcode_action, $1)" ||
+   test yes = "$_LT_TAGVAR(inherit_rpath, $1)"; then
+  # Fast installation is not supported
+  enable_fast_install=no
+elif test yes = "$shlibpath_overrides_runpath" ||
+     test no = "$enable_shared"; then
+  # Fast installation is not necessary
+  enable_fast_install=needless
+fi
+_LT_TAGDECL([], [hardcode_action], [0],
+    [How to hardcode a shared library path into an executable])
+])# _LT_LINKER_HARDCODE_LIBPATH
+
+
+# _LT_CMD_STRIPLIB
+# ----------------
+m4_defun([_LT_CMD_STRIPLIB],
+[m4_require([_LT_DECL_EGREP])
+striplib=
+old_striplib=
+AC_MSG_CHECKING([whether stripping libraries is possible])
+if test -z "$STRIP"; then
+  AC_MSG_RESULT([no])
+else
+  if $STRIP -V 2>&1 | $GREP "GNU strip" >/dev/null; then
+    old_striplib="$STRIP --strip-debug"
+    striplib="$STRIP --strip-unneeded"
+    AC_MSG_RESULT([yes])
+  else
+    case $host_os in
+    darwin*)
+      # FIXME - insert some real tests, host_os isn't really good enough
+      striplib="$STRIP -x"
+      old_striplib="$STRIP -S"
+      AC_MSG_RESULT([yes])
+      ;;
+    freebsd*)
+      if $STRIP -V 2>&1 | $GREP "elftoolchain" >/dev/null; then
+        old_striplib="$STRIP --strip-debug"
+        striplib="$STRIP --strip-unneeded"
+        AC_MSG_RESULT([yes])
+      else
+        AC_MSG_RESULT([no])
+      fi
+      ;;
+    *)
+      AC_MSG_RESULT([no])
+      ;;
+    esac
+  fi
+fi
+_LT_DECL([], [old_striplib], [1], [Commands to strip libraries])
+_LT_DECL([], [striplib], [1])
+])# _LT_CMD_STRIPLIB
+
+
+# _LT_PREPARE_MUNGE_PATH_LIST
+# ---------------------------
+# Make sure func_munge_path_list() is defined correctly.
+m4_defun([_LT_PREPARE_MUNGE_PATH_LIST],
+[[# func_munge_path_list VARIABLE PATH
+# -----------------------------------
+# VARIABLE is name of variable containing _space_ separated list of
+# directories to be munged by the contents of PATH, which is string
+# having a format:
+# "DIR[:DIR]:"
+#       string "DIR[ DIR]" will be prepended to VARIABLE
+# ":DIR[:DIR]"
+#       string "DIR[ DIR]" will be appended to VARIABLE
+# "DIRP[:DIRP]::[DIRA:]DIRA"
+#       string "DIRP[ DIRP]" will be prepended to VARIABLE and string
+#       "DIRA[ DIRA]" will be appended to VARIABLE
+# "DIR[:DIR]"
+#       VARIABLE will be replaced by "DIR[ DIR]"
+func_munge_path_list ()
+{
+    case x@S|@2 in
+    x)
+        ;;
+    *:)
+        eval @S|@1=\"`$ECHO @S|@2 | $SED 's/:/ /g'` \@S|@@S|@1\"
+        ;;
+    x:*)
+        eval @S|@1=\"\@S|@@S|@1 `$ECHO @S|@2 | $SED 's/:/ /g'`\"
+        ;;
+    *::*)
+        eval @S|@1=\"\@S|@@S|@1\ `$ECHO @S|@2 | $SED -e 's/.*:://' -e 's/:/ /g'`\"
+        eval @S|@1=\"`$ECHO @S|@2 | $SED -e 's/::.*//' -e 's/:/ /g'`\ \@S|@@S|@1\"
+        ;;
+    *)
+        eval @S|@1=\"`$ECHO @S|@2 | $SED 's/:/ /g'`\"
+        ;;
+    esac
+}
+]])# _LT_PREPARE_PATH_LIST
+
+
+# _LT_SYS_DYNAMIC_LINKER([TAG])
+# -----------------------------
+# PORTME Fill in your ld.so characteristics
+m4_defun([_LT_SYS_DYNAMIC_LINKER],
+[AC_REQUIRE([AC_CANONICAL_HOST])dnl
+m4_require([_LT_DECL_EGREP])dnl
+m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+m4_require([_LT_DECL_OBJDUMP])dnl
+m4_require([_LT_DECL_SED])dnl
+m4_require([_LT_CHECK_SHELL_FEATURES])dnl
+m4_require([_LT_PREPARE_MUNGE_PATH_LIST])dnl
+AC_MSG_CHECKING([dynamic linker characteristics])
+m4_if([$1],
+	[], [
+if test yes = "$GCC"; then
+  case $host_os in
+    darwin*) lt_awk_arg='/^libraries:/,/LR/' ;;
+    *) lt_awk_arg='/^libraries:/' ;;
+  esac
+  case $host_os in
+    mingw* | cegcc*) lt_sed_strip_eq='s|=\([[A-Za-z]]:\)|\1|g' ;;
+    *) lt_sed_strip_eq='s|=/|/|g' ;;
+  esac
+  lt_search_path_spec=`$CC -print-search-dirs | awk $lt_awk_arg | $SED -e "s/^libraries://" -e $lt_sed_strip_eq`
+  case $lt_search_path_spec in
+  *\;*)
+    # if the path contains ";" then we assume it to be the separator
+    # otherwise default to the standard path separator (i.e. ":") - it is
+    # assumed that no part of a normal pathname contains ";" but that should
+    # okay in the real world where ";" in dirpaths is itself problematic.
+    lt_search_path_spec=`$ECHO "$lt_search_path_spec" | $SED 's/;/ /g'`
+    ;;
+  *)
+    lt_search_path_spec=`$ECHO "$lt_search_path_spec" | $SED "s/$PATH_SEPARATOR/ /g"`
+    ;;
+  esac
+  # Ok, now we have the path, separated by spaces, we can step through it
+  # and add multilib dir if necessary...
+  lt_tmp_lt_search_path_spec=
+  lt_multi_os_dir=/`$CC $CPPFLAGS $CFLAGS $LDFLAGS -print-multi-os-directory 2>/dev/null`
+  # ...but if some path component already ends with the multilib dir we assume
+  # that all is fine and trust -print-search-dirs as is (GCC 4.2? or newer).
+  case "$lt_multi_os_dir; $lt_search_path_spec " in
+  "/; "* | "/.; "* | "/./; "* | *"$lt_multi_os_dir "* | *"$lt_multi_os_dir/ "*)
+    lt_multi_os_dir=
+    ;;
+  esac
+  for lt_sys_path in $lt_search_path_spec; do
+    if test -d "$lt_sys_path$lt_multi_os_dir"; then
+      lt_tmp_lt_search_path_spec="$lt_tmp_lt_search_path_spec $lt_sys_path$lt_multi_os_dir"
+    elif test -n "$lt_multi_os_dir"; then
+      test -d "$lt_sys_path" && \
+	lt_tmp_lt_search_path_spec="$lt_tmp_lt_search_path_spec $lt_sys_path"
+    fi
+  done
+  lt_search_path_spec=`$ECHO "$lt_tmp_lt_search_path_spec" | awk '
+BEGIN {RS = " "; FS = "/|\n";} {
+  lt_foo = "";
+  lt_count = 0;
+  for (lt_i = NF; lt_i > 0; lt_i--) {
+    if ($lt_i != "" && $lt_i != ".") {
+      if ($lt_i == "..") {
+        lt_count++;
+      } else {
+        if (lt_count == 0) {
+          lt_foo = "/" $lt_i lt_foo;
+        } else {
+          lt_count--;
+        }
+      }
+    }
+  }
+  if (lt_foo != "") { lt_freq[[lt_foo]]++; }
+  if (lt_freq[[lt_foo]] == 1) { print lt_foo; }
+}'`
+  # AWK program above erroneously prepends '/' to C:/dos/paths
+  # for these hosts.
+  case $host_os in
+    mingw* | cegcc*) lt_search_path_spec=`$ECHO "$lt_search_path_spec" |\
+      $SED 's|/\([[A-Za-z]]:\)|\1|g'` ;;
+  esac
+  sys_lib_search_path_spec=`$ECHO "$lt_search_path_spec" | $lt_NL2SP`
+else
+  sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib"
+fi])
+library_names_spec=
+libname_spec='lib$name'
+soname_spec=
+shrext_cmds=.so
+postinstall_cmds=
+postuninstall_cmds=
+finish_cmds=
+finish_eval=
+shlibpath_var=
+shlibpath_overrides_runpath=unknown
+version_type=none
+dynamic_linker="$host_os ld.so"
+sys_lib_dlsearch_path_spec="/lib /usr/lib"
+need_lib_prefix=unknown
+hardcode_into_libs=no
+
+# when you set need_version to no, make sure it does not cause -set_version
+# flags to be left without arguments
+need_version=unknown
+
+AC_ARG_VAR([LT_SYS_LIBRARY_PATH],
+[User-defined run-time library search path.])
+
+case $host_os in
+aix3*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  library_names_spec='$libname$release$shared_ext$versuffix $libname.a'
+  shlibpath_var=LIBPATH
+
+  # AIX 3 has no versioning support, so we append a major version to the name.
+  soname_spec='$libname$release$shared_ext$major'
+  ;;
+
+aix[[4-9]]*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  hardcode_into_libs=yes
+  if test ia64 = "$host_cpu"; then
+    # AIX 5 supports IA64
+    library_names_spec='$libname$release$shared_ext$major $libname$release$shared_ext$versuffix $libname$shared_ext'
+    shlibpath_var=LD_LIBRARY_PATH
+  else
+    # With GCC up to 2.95.x, collect2 would create an import file
+    # for dependence libraries.  The import file would start with
+    # the line '#! .'.  This would cause the generated library to
+    # depend on '.', always an invalid library.  This was fixed in
+    # development snapshots of GCC prior to 3.0.
+    case $host_os in
+      aix4 | aix4.[[01]] | aix4.[[01]].*)
+      if { echo '#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 97)'
+	   echo ' yes '
+	   echo '#endif'; } | $CC -E - | $GREP yes > /dev/null; then
+	:
+      else
+	can_build_shared=no
+      fi
+      ;;
+    esac
+    # Using Import Files as archive members, it is possible to support
+    # filename-based versioning of shared library archives on AIX. While
+    # this would work for both with and without runtime linking, it will
+    # prevent static linking of such archives. So we do filename-based
+    # shared library versioning with .so extension only, which is used
+    # when both runtime linking and shared linking is enabled.
+    # Unfortunately, runtime linking may impact performance, so we do
+    # not want this to be the default eventually. Also, we use the
+    # versioned .so libs for executables only if there is the -brtl
+    # linker flag in LDFLAGS as well, or --with-aix-soname=svr4 only.
+    # To allow for filename-based versioning support, we need to create
+    # libNAME.so.V as an archive file, containing:
+    # *) an Import File, referring to the versioned filename of the
+    #    archive as well as the shared archive member, telling the
+    #    bitwidth (32 or 64) of that shared object, and providing the
+    #    list of exported symbols of that shared object, eventually
+    #    decorated with the 'weak' keyword
+    # *) the shared object with the F_LOADONLY flag set, to really avoid
+    #    it being seen by the linker.
+    # At run time we better use the real file rather than another symlink,
+    # but for link time we create the symlink libNAME.so -> libNAME.so.V
+
+    case $with_aix_soname,$aix_use_runtimelinking in
+    # AIX (on Power*) has no versioning support, so currently we cannot hardcode correct
+    # soname into executable. Probably we can add versioning support to
+    # collect2, so additional links can be useful in future.
+    aix,yes) # traditional libtool
+      dynamic_linker='AIX unversionable lib.so'
+      # If using run time linking (on AIX 4.2 or later) use lib<name>.so
+      # instead of lib<name>.a to let people know that these are not
+      # typical AIX shared libraries.
+      library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+      ;;
+    aix,no) # traditional AIX only
+      dynamic_linker='AIX lib.a[(]lib.so.V[)]'
+      # We preserve .a as extension for shared libraries through AIX4.2
+      # and later when we are not doing run time linking.
+      library_names_spec='$libname$release.a $libname.a'
+      soname_spec='$libname$release$shared_ext$major'
+      ;;
+    svr4,*) # full svr4 only
+      dynamic_linker="AIX lib.so.V[(]$shared_archive_member_spec.o[)]"
+      library_names_spec='$libname$release$shared_ext$major $libname$shared_ext'
+      # We do not specify a path in Import Files, so LIBPATH fires.
+      shlibpath_overrides_runpath=yes
+      ;;
+    *,yes) # both, prefer svr4
+      dynamic_linker="AIX lib.so.V[(]$shared_archive_member_spec.o[)], lib.a[(]lib.so.V[)]"
+      library_names_spec='$libname$release$shared_ext$major $libname$shared_ext'
+      # unpreferred sharedlib libNAME.a needs extra handling
+      postinstall_cmds='test -n "$linkname" || linkname="$realname"~func_stripname "" ".so" "$linkname"~$install_shared_prog "$dir/$func_stripname_result.$libext" "$destdir/$func_stripname_result.$libext"~test -z "$tstripme" || test -z "$striplib" || $striplib "$destdir/$func_stripname_result.$libext"'
+      postuninstall_cmds='for n in $library_names $old_library; do :; done~func_stripname "" ".so" "$n"~test "$func_stripname_result" = "$n" || func_append rmfiles " $odir/$func_stripname_result.$libext"'
+      # We do not specify a path in Import Files, so LIBPATH fires.
+      shlibpath_overrides_runpath=yes
+      ;;
+    *,no) # both, prefer aix
+      dynamic_linker="AIX lib.a[(]lib.so.V[)], lib.so.V[(]$shared_archive_member_spec.o[)]"
+      library_names_spec='$libname$release.a $libname.a'
+      soname_spec='$libname$release$shared_ext$major'
+      # unpreferred sharedlib libNAME.so.V and symlink libNAME.so need extra handling
+      postinstall_cmds='test -z "$dlname" || $install_shared_prog $dir/$dlname $destdir/$dlname~test -z "$tstripme" || test -z "$striplib" || $striplib $destdir/$dlname~test -n "$linkname" || linkname=$realname~func_stripname "" ".a" "$linkname"~(cd "$destdir" && $LN_S -f $dlname $func_stripname_result.so)'
+      postuninstall_cmds='test -z "$dlname" || func_append rmfiles " $odir/$dlname"~for n in $old_library $library_names; do :; done~func_stripname "" ".a" "$n"~func_append rmfiles " $odir/$func_stripname_result.so"'
+      ;;
+    esac
+    shlibpath_var=LIBPATH
+  fi
+  ;;
+
+amigaos*)
+  case $host_cpu in
+  powerpc)
+    # Since July 2007 AmigaOS4 officially supports .so libraries.
+    # When compiling the executable, add -use-dynld -Lsobjs: to the compileline.
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    ;;
+  m68k)
+    library_names_spec='$libname.ixlibrary $libname.a'
+    # Create ${libname}_ixlibrary.a entries in /sys/libs.
+    finish_eval='for lib in `ls $libdir/*.ixlibrary 2>/dev/null`; do libname=`func_echo_all "$lib" | $SED '\''s%^.*/\([[^/]]*\)\.ixlibrary$%\1%'\''`; $RM /sys/libs/${libname}_ixlibrary.a; $show "cd /sys/libs && $LN_S $lib ${libname}_ixlibrary.a"; cd /sys/libs && $LN_S $lib ${libname}_ixlibrary.a || exit 1; done'
+    ;;
+  esac
+  ;;
+
+beos*)
+  library_names_spec='$libname$shared_ext'
+  dynamic_linker="$host_os ld.so"
+  shlibpath_var=LIBRARY_PATH
+  ;;
+
+bsdi[[45]]*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  finish_cmds='PATH="\$PATH:/sbin" ldconfig $libdir'
+  shlibpath_var=LD_LIBRARY_PATH
+  sys_lib_search_path_spec="/shlib /usr/lib /usr/X11/lib /usr/contrib/lib /lib /usr/local/lib"
+  sys_lib_dlsearch_path_spec="/shlib /usr/lib /usr/local/lib"
+  # the default ld.so.conf also contains /usr/contrib/lib and
+  # /usr/X11R6/lib (/usr/X11 is a link to /usr/X11R6), but let us allow
+  # libtool to hard-code these into programs
+  ;;
+
+cygwin* | mingw* | pw32* | cegcc*)
+  version_type=windows
+  shrext_cmds=.dll
+  need_version=no
+  need_lib_prefix=no
+
+  case $GCC,$cc_basename in
+  yes,*)
+    # gcc
+    library_names_spec='$libname.dll.a'
+    # DLL is installed to $(libdir)/../bin by postinstall_cmds
+    postinstall_cmds='base_file=`basename \$file`~
+      dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; echo \$dlname'\''`~
+      dldir=$destdir/`dirname \$dlpath`~
+      test -d \$dldir || mkdir -p \$dldir~
+      $install_prog $dir/$dlname \$dldir/$dlname~
+      chmod a+x \$dldir/$dlname~
+      if test -n '\''$stripme'\'' && test -n '\''$striplib'\''; then
+        eval '\''$striplib \$dldir/$dlname'\'' || exit \$?;
+      fi'
+    postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; echo \$dlname'\''`~
+      dlpath=$dir/\$dldll~
+       $RM \$dlpath'
+    shlibpath_overrides_runpath=yes
+
+    case $host_os in
+    cygwin*)
+      # Cygwin DLLs use 'cyg' prefix rather than 'lib'
+      soname_spec='`echo $libname | $SED -e 's/^lib/cyg/'``echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext'
+m4_if([$1], [],[
+      sys_lib_search_path_spec="$sys_lib_search_path_spec /usr/lib/w32api"])
+      ;;
+    mingw* | cegcc*)
+      # MinGW DLLs use traditional 'lib' prefix
+      soname_spec='$libname`echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext'
+      ;;
+    pw32*)
+      # pw32 DLLs use 'pw' prefix rather than 'lib'
+      library_names_spec='`echo $libname | $SED -e 's/^lib/pw/'``echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext'
+      ;;
+    esac
+    dynamic_linker='Win32 ld.exe'
+    ;;
+
+  *,cl* | *,icl*)
+    # Native MSVC or ICC
+    libname_spec='$name'
+    soname_spec='$libname`echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext'
+    library_names_spec='$libname.dll.lib'
+
+    case $build_os in
+    mingw*)
+      sys_lib_search_path_spec=
+      lt_save_ifs=$IFS
+      IFS=';'
+      for lt_path in $LIB
+      do
+        IFS=$lt_save_ifs
+        # Let DOS variable expansion print the short 8.3 style file name.
+        lt_path=`cd "$lt_path" 2>/dev/null && cmd //C "for %i in (".") do @echo %~si"`
+        sys_lib_search_path_spec="$sys_lib_search_path_spec $lt_path"
+      done
+      IFS=$lt_save_ifs
+      # Convert to MSYS style.
+      sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e 's|\\\\|/|g' -e 's| \\([[a-zA-Z]]\\):| /\\1|g' -e 's|^ ||'`
+      ;;
+    cygwin*)
+      # Convert to unix form, then to dos form, then back to unix form
+      # but this time dos style (no spaces!) so that the unix form looks
+      # like /cygdrive/c/PROGRA~1:/cygdr...
+      sys_lib_search_path_spec=`cygpath --path --unix "$LIB"`
+      sys_lib_search_path_spec=`cygpath --path --dos "$sys_lib_search_path_spec" 2>/dev/null`
+      sys_lib_search_path_spec=`cygpath --path --unix "$sys_lib_search_path_spec" | $SED -e "s/$PATH_SEPARATOR/ /g"`
+      ;;
+    *)
+      sys_lib_search_path_spec=$LIB
+      if $ECHO "$sys_lib_search_path_spec" | [$GREP ';[c-zC-Z]:/' >/dev/null]; then
+        # It is most probably a Windows format PATH.
+        sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e 's/;/ /g'`
+      else
+        sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e "s/$PATH_SEPARATOR/ /g"`
+      fi
+      # FIXME: find the short name or the path components, as spaces are
+      # common. (e.g. "Program Files" -> "PROGRA~1")
+      ;;
+    esac
+
+    # DLL is installed to $(libdir)/../bin by postinstall_cmds
+    postinstall_cmds='base_file=`basename \$file`~
+      dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; echo \$dlname'\''`~
+      dldir=$destdir/`dirname \$dlpath`~
+      test -d \$dldir || mkdir -p \$dldir~
+      $install_prog $dir/$dlname \$dldir/$dlname'
+    postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; echo \$dlname'\''`~
+      dlpath=$dir/\$dldll~
+       $RM \$dlpath'
+    shlibpath_overrides_runpath=yes
+    dynamic_linker='Win32 link.exe'
+    ;;
+
+  *)
+    # Assume MSVC and ICC wrapper
+    library_names_spec='$libname`echo $release | $SED -e 's/[[.]]/-/g'`$versuffix$shared_ext $libname.lib'
+    dynamic_linker='Win32 ld.exe'
+    ;;
+  esac
+  # FIXME: first we should search . and the directory the executable is in
+  shlibpath_var=PATH
+  ;;
+
+darwin* | rhapsody*)
+  dynamic_linker="$host_os dyld"
+  version_type=darwin
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$major$shared_ext $libname$shared_ext'
+  soname_spec='$libname$release$major$shared_ext'
+  shlibpath_overrides_runpath=yes
+  shlibpath_var=DYLD_LIBRARY_PATH
+  shrext_cmds='`test .$module = .yes && echo .so || echo .dylib`'
+m4_if([$1], [],[
+  sys_lib_search_path_spec="$sys_lib_search_path_spec /usr/local/lib"])
+  sys_lib_dlsearch_path_spec='/usr/local/lib /lib /usr/lib'
+  ;;
+
+dgux*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  ;;
+
+freebsd* | dragonfly* | midnightbsd*)
+  # DragonFly does not have aout.  When/if they implement a new
+  # versioning mechanism, adjust this.
+  if test -x /usr/bin/objformat; then
+    objformat=`/usr/bin/objformat`
+  else
+    case $host_os in
+    freebsd[[23]].*) objformat=aout ;;
+    *) objformat=elf ;;
+    esac
+  fi
+  version_type=freebsd-$objformat
+  case $version_type in
+    freebsd-elf*)
+      library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+      soname_spec='$libname$release$shared_ext$major'
+      need_version=no
+      need_lib_prefix=no
+      ;;
+    freebsd-*)
+      library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix'
+      need_version=yes
+      ;;
+  esac
+  shlibpath_var=LD_LIBRARY_PATH
+  case $host_os in
+  freebsd2.*)
+    shlibpath_overrides_runpath=yes
+    ;;
+  freebsd3.[[01]]* | freebsdelf3.[[01]]*)
+    shlibpath_overrides_runpath=yes
+    hardcode_into_libs=yes
+    ;;
+  freebsd3.[[2-9]]* | freebsdelf3.[[2-9]]* | \
+  freebsd4.[[0-5]] | freebsdelf4.[[0-5]] | freebsd4.1.1 | freebsdelf4.1.1)
+    shlibpath_overrides_runpath=no
+    hardcode_into_libs=yes
+    ;;
+  *) # from 4.6 on, and DragonFly
+    shlibpath_overrides_runpath=yes
+    hardcode_into_libs=yes
+    ;;
+  esac
+  ;;
+
+haiku*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  dynamic_linker="$host_os runtime_loader"
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  sys_lib_dlsearch_path_spec='/boot/home/config/lib /boot/common/lib /boot/system/lib'
+  hardcode_into_libs=yes
+  ;;
+
+hpux9* | hpux10* | hpux11*)
+  # Give a soname corresponding to the major version so that dld.sl refuses to
+  # link against other versions.
+  version_type=sunos
+  need_lib_prefix=no
+  need_version=no
+  case $host_cpu in
+  ia64*)
+    shrext_cmds='.so'
+    hardcode_into_libs=yes
+    dynamic_linker="$host_os dld.so"
+    shlibpath_var=LD_LIBRARY_PATH
+    shlibpath_overrides_runpath=yes # Unless +noenvvar is specified.
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    soname_spec='$libname$release$shared_ext$major'
+    if test 32 = "$HPUX_IA64_MODE"; then
+      sys_lib_search_path_spec="/usr/lib/hpux32 /usr/local/lib/hpux32 /usr/local/lib"
+      sys_lib_dlsearch_path_spec=/usr/lib/hpux32
+    else
+      sys_lib_search_path_spec="/usr/lib/hpux64 /usr/local/lib/hpux64"
+      sys_lib_dlsearch_path_spec=/usr/lib/hpux64
+    fi
+    ;;
+  hppa*64*)
+    shrext_cmds='.sl'
+    hardcode_into_libs=yes
+    dynamic_linker="$host_os dld.sl"
+    shlibpath_var=LD_LIBRARY_PATH # How should we handle SHLIB_PATH
+    shlibpath_overrides_runpath=yes # Unless +noenvvar is specified.
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    soname_spec='$libname$release$shared_ext$major'
+    sys_lib_search_path_spec="/usr/lib/pa20_64 /usr/ccs/lib/pa20_64"
+    sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec
+    ;;
+  *)
+    shrext_cmds='.sl'
+    dynamic_linker="$host_os dld.sl"
+    shlibpath_var=SHLIB_PATH
+    shlibpath_overrides_runpath=no # +s is required to enable SHLIB_PATH
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    soname_spec='$libname$release$shared_ext$major'
+    ;;
+  esac
+  # HP-UX runs *really* slowly unless shared libraries are mode 555, ...
+  postinstall_cmds='chmod 555 $lib'
+  # or fails outright, so override atomically:
+  install_override_mode=555
+  ;;
+
+interix[[3-9]]*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  dynamic_linker='Interix 3.x ld.so.1 (PE, like ELF)'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  hardcode_into_libs=yes
+  ;;
+
+irix5* | irix6* | nonstopux*)
+  case $host_os in
+    nonstopux*) version_type=nonstopux ;;
+    *)
+	if test yes = "$lt_cv_prog_gnu_ld"; then
+		version_type=linux # correct to gnu/linux during the next big refactor
+	else
+		version_type=irix
+	fi ;;
+  esac
+  need_lib_prefix=no
+  need_version=no
+  soname_spec='$libname$release$shared_ext$major'
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$release$shared_ext $libname$shared_ext'
+  case $host_os in
+  irix5* | nonstopux*)
+    libsuff= shlibsuff=
+    ;;
+  *)
+    case $LD in # libtool.m4 will add one of these switches to LD
+    *-32|*"-32 "|*-melf32bsmip|*"-melf32bsmip ")
+      libsuff= shlibsuff= libmagic=32-bit;;
+    *-n32|*"-n32 "|*-melf32bmipn32|*"-melf32bmipn32 ")
+      libsuff=32 shlibsuff=N32 libmagic=N32;;
+    *-64|*"-64 "|*-melf64bmip|*"-melf64bmip ")
+      libsuff=64 shlibsuff=64 libmagic=64-bit;;
+    *) libsuff= shlibsuff= libmagic=never-match;;
+    esac
+    ;;
+  esac
+  shlibpath_var=LD_LIBRARY${shlibsuff}_PATH
+  shlibpath_overrides_runpath=no
+  sys_lib_search_path_spec="/usr/lib$libsuff /lib$libsuff /usr/local/lib$libsuff"
+  sys_lib_dlsearch_path_spec="/usr/lib$libsuff /lib$libsuff"
+  hardcode_into_libs=yes
+  ;;
+
+# No shared lib support for Linux oldld, aout, or coff.
+linux*oldld* | linux*aout* | linux*coff*)
+  dynamic_linker=no
+  ;;
+
+linux*android*)
+  version_type=none # Android doesn't support versioned libraries.
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext'
+  soname_spec='$libname$release$shared_ext'
+  finish_cmds=
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+
+  # This implies no fast_install, which is unacceptable.
+  # Some rework will be needed to allow for fast_install
+  # before this can be enabled.
+  hardcode_into_libs=yes
+
+  dynamic_linker='Android linker'
+  # Don't embed -rpath directories since the linker doesn't support them.
+  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+  ;;
+
+# This must be glibc/ELF.
+linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  finish_cmds='PATH="\$PATH:/sbin" ldconfig -n $libdir'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+
+  # Some binutils ld are patched to set DT_RUNPATH
+  AC_CACHE_VAL([lt_cv_shlibpath_overrides_runpath],
+    [lt_cv_shlibpath_overrides_runpath=no
+    save_LDFLAGS=$LDFLAGS
+    save_libdir=$libdir
+    eval "libdir=/foo; wl=\"$_LT_TAGVAR(lt_prog_compiler_wl, $1)\"; \
+	 LDFLAGS=\"\$LDFLAGS $_LT_TAGVAR(hardcode_libdir_flag_spec, $1)\""
+    AC_LINK_IFELSE([AC_LANG_PROGRAM([],[])],
+      [AS_IF([ ($OBJDUMP -p conftest$ac_exeext) 2>/dev/null | grep "RUNPATH.*$libdir" >/dev/null],
+	 [lt_cv_shlibpath_overrides_runpath=yes])])
+    LDFLAGS=$save_LDFLAGS
+    libdir=$save_libdir
+    ])
+  shlibpath_overrides_runpath=$lt_cv_shlibpath_overrides_runpath
+
+  # This implies no fast_install, which is unacceptable.
+  # Some rework will be needed to allow for fast_install
+  # before this can be enabled.
+  hardcode_into_libs=yes
+
+  # Ideally, we could use ldconfig to report *all* directores which are
+  # searched for libraries, however this is still not possible.  Aside from not
+  # being certain /sbin/ldconfig is available, command
+  # 'ldconfig -N -X -v | grep ^/' on 64bit Fedora does not report /usr/lib64,
+  # even though it is searched at run-time.  Try to do the best guess by
+  # appending ld.so.conf contents (and includes) to the search path.
+  if test -f /etc/ld.so.conf; then
+    lt_ld_extra=`awk '/^include / { system(sprintf("cd /etc; cat %s 2>/dev/null", \[$]2)); skip = 1; } { if (!skip) print \[$]0; skip = 0; }' < /etc/ld.so.conf | $SED -e 's/#.*//;/^[	 ]*hwcap[	 ]/d;s/[:,	]/ /g;s/=[^=]*$//;s/=[^= ]* / /g;s/"//g;/^$/d' | tr '\n' ' '`
+    sys_lib_dlsearch_path_spec="/lib /usr/lib $lt_ld_extra"
+  fi
+
+  # We used to test for /lib/ld.so.1 and disable shared libraries on
+  # powerpc, because MkLinux only supported shared libraries with the
+  # GNU dynamic linker.  Since this was broken with cross compilers,
+  # most powerpc-linux boxes support dynamic linking these days and
+  # people can always --disable-shared, the test was removed, and we
+  # assume the GNU/Linux dynamic linker is in use.
+  dynamic_linker='GNU/Linux ld.so'
+  ;;
+
+netbsdelf*-gnu)
+  version_type=linux
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='${libname}${release}${shared_ext}$versuffix ${libname}${release}${shared_ext}$major ${libname}${shared_ext}'
+  soname_spec='${libname}${release}${shared_ext}$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  hardcode_into_libs=yes
+  dynamic_linker='NetBSD ld.elf_so'
+  ;;
+
+netbsd*)
+  version_type=sunos
+  need_lib_prefix=no
+  need_version=no
+  if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix'
+    finish_cmds='PATH="\$PATH:/sbin" ldconfig -m $libdir'
+    dynamic_linker='NetBSD (a.out) ld.so'
+  else
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    soname_spec='$libname$release$shared_ext$major'
+    dynamic_linker='NetBSD ld.elf_so'
+  fi
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  hardcode_into_libs=yes
+  ;;
+
+newsos6)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  ;;
+
+*nto* | *qnx*)
+  version_type=qnx
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  hardcode_into_libs=yes
+  dynamic_linker='ldqnx.so'
+  ;;
+
+openbsd* | bitrig*)
+  version_type=sunos
+  sys_lib_dlsearch_path_spec=/usr/lib
+  need_lib_prefix=no
+  if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then
+    need_version=no
+  else
+    need_version=yes
+  fi
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix'
+  finish_cmds='PATH="\$PATH:/sbin" ldconfig -m $libdir'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  ;;
+
+os2*)
+  libname_spec='$name'
+  version_type=windows
+  shrext_cmds=.dll
+  need_version=no
+  need_lib_prefix=no
+  # OS/2 can only load a DLL with a base name of 8 characters or less.
+  soname_spec='`test -n "$os2dllname" && libname="$os2dllname";
+    v=$($ECHO $release$versuffix | tr -d .-);
+    n=$($ECHO $libname | cut -b -$((8 - ${#v})) | tr . _);
+    $ECHO $n$v`$shared_ext'
+  library_names_spec='${libname}_dll.$libext'
+  dynamic_linker='OS/2 ld.exe'
+  shlibpath_var=BEGINLIBPATH
+  sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib"
+  sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec
+  postinstall_cmds='base_file=`basename \$file`~
+    dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; $ECHO \$dlname'\''`~
+    dldir=$destdir/`dirname \$dlpath`~
+    test -d \$dldir || mkdir -p \$dldir~
+    $install_prog $dir/$dlname \$dldir/$dlname~
+    chmod a+x \$dldir/$dlname~
+    if test -n '\''$stripme'\'' && test -n '\''$striplib'\''; then
+      eval '\''$striplib \$dldir/$dlname'\'' || exit \$?;
+    fi'
+  postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; $ECHO \$dlname'\''`~
+    dlpath=$dir/\$dldll~
+    $RM \$dlpath'
+  ;;
+
+osf3* | osf4* | osf5*)
+  version_type=osf
+  need_lib_prefix=no
+  need_version=no
+  soname_spec='$libname$release$shared_ext$major'
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  shlibpath_var=LD_LIBRARY_PATH
+  sys_lib_search_path_spec="/usr/shlib /usr/ccs/lib /usr/lib/cmplrs/cc /usr/lib /usr/local/lib /var/shlib"
+  sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec
+  ;;
+
+rdos*)
+  dynamic_linker=no
+  ;;
+
+solaris*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  hardcode_into_libs=yes
+  # ldd complains unless libraries are executable
+  postinstall_cmds='chmod +x $lib'
+  ;;
+
+sunos4*)
+  version_type=sunos
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix'
+  finish_cmds='PATH="\$PATH:/usr/etc" ldconfig $libdir'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  if test yes = "$with_gnu_ld"; then
+    need_lib_prefix=no
+  fi
+  need_version=yes
+  ;;
+
+sysv4 | sysv4.3*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  case $host_vendor in
+    sni)
+      shlibpath_overrides_runpath=no
+      need_lib_prefix=no
+      runpath_var=LD_RUN_PATH
+      ;;
+    siemens)
+      need_lib_prefix=no
+      ;;
+    motorola)
+      need_lib_prefix=no
+      need_version=no
+      shlibpath_overrides_runpath=no
+      sys_lib_search_path_spec='/lib /usr/lib /usr/ccs/lib'
+      ;;
+  esac
+  ;;
+
+sysv4*MP*)
+  if test -d /usr/nec; then
+    version_type=linux # correct to gnu/linux during the next big refactor
+    library_names_spec='$libname$shared_ext.$versuffix $libname$shared_ext.$major $libname$shared_ext'
+    soname_spec='$libname$shared_ext.$major'
+    shlibpath_var=LD_LIBRARY_PATH
+  fi
+  ;;
+
+sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX* | sysv4*uw2*)
+  version_type=sco
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  hardcode_into_libs=yes
+  if test yes = "$with_gnu_ld"; then
+    sys_lib_search_path_spec='/usr/local/lib /usr/gnu/lib /usr/ccs/lib /usr/lib /lib'
+  else
+    sys_lib_search_path_spec='/usr/ccs/lib /usr/lib'
+    case $host_os in
+      sco3.2v5*)
+        sys_lib_search_path_spec="$sys_lib_search_path_spec /lib"
+	;;
+    esac
+  fi
+  sys_lib_dlsearch_path_spec='/usr/lib'
+  ;;
+
+tpf*)
+  # TPF is a cross-target only.  Preferred cross-host = GNU/Linux.
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  hardcode_into_libs=yes
+  ;;
+
+uts4*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  ;;
+
+*)
+  dynamic_linker=no
+  ;;
+esac
+AC_MSG_RESULT([$dynamic_linker])
+test no = "$dynamic_linker" && can_build_shared=no
+
+variables_saved_for_relink="PATH $shlibpath_var $runpath_var"
+if test yes = "$GCC"; then
+  variables_saved_for_relink="$variables_saved_for_relink GCC_EXEC_PREFIX COMPILER_PATH LIBRARY_PATH"
+fi
+
+if test set = "${lt_cv_sys_lib_search_path_spec+set}"; then
+  sys_lib_search_path_spec=$lt_cv_sys_lib_search_path_spec
+fi
+
+if test set = "${lt_cv_sys_lib_dlsearch_path_spec+set}"; then
+  sys_lib_dlsearch_path_spec=$lt_cv_sys_lib_dlsearch_path_spec
+fi
+
+# remember unaugmented sys_lib_dlsearch_path content for libtool script decls...
+configure_time_dlsearch_path=$sys_lib_dlsearch_path_spec
+
+# ... but it needs LT_SYS_LIBRARY_PATH munging for other configure-time code
+func_munge_path_list sys_lib_dlsearch_path_spec "$LT_SYS_LIBRARY_PATH"
+
+# to be used as default LT_SYS_LIBRARY_PATH value in generated libtool
+configure_time_lt_sys_library_path=$LT_SYS_LIBRARY_PATH
+
+_LT_DECL([], [variables_saved_for_relink], [1],
+    [Variables whose values should be saved in libtool wrapper scripts and
+    restored at link time])
+_LT_DECL([], [need_lib_prefix], [0],
+    [Do we need the "lib" prefix for modules?])
+_LT_DECL([], [need_version], [0], [Do we need a version for libraries?])
+_LT_DECL([], [version_type], [0], [Library versioning type])
+_LT_DECL([], [runpath_var], [0],  [Shared library runtime path variable])
+_LT_DECL([], [shlibpath_var], [0],[Shared library path variable])
+_LT_DECL([], [shlibpath_overrides_runpath], [0],
+    [Is shlibpath searched before the hard-coded library search path?])
+_LT_DECL([], [libname_spec], [1], [Format of library name prefix])
+_LT_DECL([], [library_names_spec], [1],
+    [[List of archive names.  First name is the real one, the rest are links.
+    The last name is the one that the linker finds with -lNAME]])
+_LT_DECL([], [soname_spec], [1],
+    [[The coded name of the library, if different from the real name]])
+_LT_DECL([], [install_override_mode], [1],
+    [Permission mode override for installation of shared libraries])
+_LT_DECL([], [postinstall_cmds], [2],
+    [Command to use after installation of a shared archive])
+_LT_DECL([], [postuninstall_cmds], [2],
+    [Command to use after uninstallation of a shared archive])
+_LT_DECL([], [finish_cmds], [2],
+    [Commands used to finish a libtool library installation in a directory])
+_LT_DECL([], [finish_eval], [1],
+    [[As "finish_cmds", except a single script fragment to be evaled but
+    not shown]])
+_LT_DECL([], [hardcode_into_libs], [0],
+    [Whether we should hardcode library paths into libraries])
+_LT_DECL([], [sys_lib_search_path_spec], [2],
+    [Compile-time system search path for libraries])
+_LT_DECL([sys_lib_dlsearch_path_spec], [configure_time_dlsearch_path], [2],
+    [Detected run-time system search path for libraries])
+_LT_DECL([], [configure_time_lt_sys_library_path], [2],
+    [Explicit LT_SYS_LIBRARY_PATH set during ./configure time])
+])# _LT_SYS_DYNAMIC_LINKER
+
+
+# _LT_PATH_TOOL_PREFIX(TOOL)
+# --------------------------
+# find a file program that can recognize shared library
+AC_DEFUN([_LT_PATH_TOOL_PREFIX],
+[m4_require([_LT_DECL_EGREP])dnl
+AC_MSG_CHECKING([for $1])
+AC_CACHE_VAL(lt_cv_path_MAGIC_CMD,
+[case $MAGIC_CMD in
+[[\\/*] |  ?:[\\/]*])
+  lt_cv_path_MAGIC_CMD=$MAGIC_CMD # Let the user override the test with a path.
+  ;;
+*)
+  lt_save_MAGIC_CMD=$MAGIC_CMD
+  lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR
+dnl $ac_dummy forces splitting on constant user-supplied paths.
+dnl POSIX.2 word splitting is done only on the output of word expansions,
+dnl not every word.  This closes a longstanding sh security hole.
+  ac_dummy="m4_if([$2], , $PATH, [$2])"
+  for ac_dir in $ac_dummy; do
+    IFS=$lt_save_ifs
+    test -z "$ac_dir" && ac_dir=.
+    if test -f "$ac_dir/$1"; then
+      lt_cv_path_MAGIC_CMD=$ac_dir/"$1"
+      if test -n "$file_magic_test_file"; then
+	case $deplibs_check_method in
+	"file_magic "*)
+	  file_magic_regex=`expr "$deplibs_check_method" : "file_magic \(.*\)"`
+	  MAGIC_CMD=$lt_cv_path_MAGIC_CMD
+	  if eval $file_magic_cmd \$file_magic_test_file 2> /dev/null |
+	    $EGREP "$file_magic_regex" > /dev/null; then
+	    :
+	  else
+	    cat <<_LT_EOF 1>&2
+
+*** Warning: the command libtool uses to detect shared libraries,
+*** $file_magic_cmd, produces output that libtool cannot recognize.
+*** The result is that libtool may fail to recognize shared libraries
+*** as such.  This will affect the creation of libtool libraries that
+*** depend on shared libraries, but programs linked with such libtool
+*** libraries will work regardless of this problem.  Nevertheless, you
+*** may want to report the problem to your system manager and/or to
+*** bug-libtool@gnu.org
+
+_LT_EOF
+	  fi ;;
+	esac
+      fi
+      break
+    fi
+  done
+  IFS=$lt_save_ifs
+  MAGIC_CMD=$lt_save_MAGIC_CMD
+  ;;
+esac])
+MAGIC_CMD=$lt_cv_path_MAGIC_CMD
+if test -n "$MAGIC_CMD"; then
+  AC_MSG_RESULT($MAGIC_CMD)
+else
+  AC_MSG_RESULT(no)
+fi
+_LT_DECL([], [MAGIC_CMD], [0],
+	 [Used to examine libraries when file_magic_cmd begins with "file"])dnl
+])# _LT_PATH_TOOL_PREFIX
+
+# Old name:
+AU_ALIAS([AC_PATH_TOOL_PREFIX], [_LT_PATH_TOOL_PREFIX])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_PATH_TOOL_PREFIX], [])
+
+
+# _LT_PATH_MAGIC
+# --------------
+# find a file program that can recognize a shared library
+m4_defun([_LT_PATH_MAGIC],
+[_LT_PATH_TOOL_PREFIX(${ac_tool_prefix}file, /usr/bin$PATH_SEPARATOR$PATH)
+if test -z "$lt_cv_path_MAGIC_CMD"; then
+  if test -n "$ac_tool_prefix"; then
+    _LT_PATH_TOOL_PREFIX(file, /usr/bin$PATH_SEPARATOR$PATH)
+  else
+    MAGIC_CMD=:
+  fi
+fi
+])# _LT_PATH_MAGIC
+
+
+# LT_PATH_LD
+# ----------
+# find the pathname to the GNU or non-GNU linker
+AC_DEFUN([LT_PATH_LD],
+[AC_REQUIRE([AC_PROG_CC])dnl
+AC_REQUIRE([AC_CANONICAL_HOST])dnl
+AC_REQUIRE([AC_CANONICAL_BUILD])dnl
+m4_require([_LT_DECL_SED])dnl
+m4_require([_LT_DECL_EGREP])dnl
+m4_require([_LT_PROG_ECHO_BACKSLASH])dnl
+
+AC_ARG_WITH([gnu-ld],
+    [AS_HELP_STRING([--with-gnu-ld],
+	[assume the C compiler uses GNU ld @<:@default=no@:>@])],
+    [test no = "$withval" || with_gnu_ld=yes],
+    [with_gnu_ld=no])dnl
+
+ac_prog=ld
+if test yes = "$GCC"; then
+  # Check if gcc -print-prog-name=ld gives a path.
+  AC_MSG_CHECKING([for ld used by $CC])
+  case $host in
+  *-*-mingw*)
+    # gcc leaves a trailing carriage return, which upsets mingw
+    ac_prog=`($CC -print-prog-name=ld) 2>&5 | tr -d '\015'` ;;
+  *)
+    ac_prog=`($CC -print-prog-name=ld) 2>&5` ;;
+  esac
+  case $ac_prog in
+    # Accept absolute paths.
+    [[\\/]]* | ?:[[\\/]]*)
+      re_direlt='/[[^/]][[^/]]*/\.\./'
+      # Canonicalize the pathname of ld
+      ac_prog=`$ECHO "$ac_prog"| $SED 's%\\\\%/%g'`
+      while $ECHO "$ac_prog" | $GREP "$re_direlt" > /dev/null 2>&1; do
+	ac_prog=`$ECHO $ac_prog| $SED "s%$re_direlt%/%"`
+      done
+      test -z "$LD" && LD=$ac_prog
+      ;;
+  "")
+    # If it fails, then pretend we aren't using GCC.
+    ac_prog=ld
+    ;;
+  *)
+    # If it is relative, then search for the first ld in PATH.
+    with_gnu_ld=unknown
+    ;;
+  esac
+elif test yes = "$with_gnu_ld"; then
+  AC_MSG_CHECKING([for GNU ld])
+else
+  AC_MSG_CHECKING([for non-GNU ld])
+fi
+AC_CACHE_VAL(lt_cv_path_LD,
+[if test -z "$LD"; then
+  lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR
+  for ac_dir in $PATH; do
+    IFS=$lt_save_ifs
+    test -z "$ac_dir" && ac_dir=.
+    if test -f "$ac_dir/$ac_prog" || test -f "$ac_dir/$ac_prog$ac_exeext"; then
+      lt_cv_path_LD=$ac_dir/$ac_prog
+      # Check to see if the program is GNU ld.  I'd rather use --version,
+      # but apparently some variants of GNU ld only accept -v.
+      # Break only if it was the GNU/non-GNU ld that we prefer.
+      case `"$lt_cv_path_LD" -v 2>&1 </dev/null` in
+      *GNU* | *'with BFD'*)
+	test no != "$with_gnu_ld" && break
+	;;
+      *)
+	test yes != "$with_gnu_ld" && break
+	;;
+      esac
+    fi
+  done
+  IFS=$lt_save_ifs
+else
+  lt_cv_path_LD=$LD # Let the user override the test with a path.
+fi])
+LD=$lt_cv_path_LD
+if test -n "$LD"; then
+  AC_MSG_RESULT($LD)
+else
+  AC_MSG_RESULT(no)
+fi
+test -z "$LD" && AC_MSG_ERROR([no acceptable ld found in \$PATH])
+_LT_PATH_LD_GNU
+AC_SUBST([LD])
+
+_LT_TAGDECL([], [LD], [1], [The linker used to build libraries])
+])# LT_PATH_LD
+
+# Old names:
+AU_ALIAS([AM_PROG_LD], [LT_PATH_LD])
+AU_ALIAS([AC_PROG_LD], [LT_PATH_LD])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AM_PROG_LD], [])
+dnl AC_DEFUN([AC_PROG_LD], [])
+
+
+# _LT_PATH_LD_GNU
+#- --------------
+m4_defun([_LT_PATH_LD_GNU],
+[AC_CACHE_CHECK([if the linker ($LD) is GNU ld], lt_cv_prog_gnu_ld,
+[# I'd rather use --version here, but apparently some GNU lds only accept -v.
+case `$LD -v 2>&1 </dev/null` in
+*GNU* | *'with BFD'*)
+  lt_cv_prog_gnu_ld=yes
+  ;;
+*)
+  lt_cv_prog_gnu_ld=no
+  ;;
+esac])
+with_gnu_ld=$lt_cv_prog_gnu_ld
+])# _LT_PATH_LD_GNU
+
+
+# _LT_CMD_RELOAD
+# --------------
+# find reload flag for linker
+#   -- PORTME Some linkers may need a different reload flag.
+m4_defun([_LT_CMD_RELOAD],
+[AC_CACHE_CHECK([for $LD option to reload object files],
+  lt_cv_ld_reload_flag,
+  [lt_cv_ld_reload_flag='-r'])
+reload_flag=$lt_cv_ld_reload_flag
+case $reload_flag in
+"" | " "*) ;;
+*) reload_flag=" $reload_flag" ;;
+esac
+reload_cmds='$LD$reload_flag -o $output$reload_objs'
+case $host_os in
+  cygwin* | mingw* | pw32* | cegcc*)
+    if test yes != "$GCC"; then
+      reload_cmds=false
+    fi
+    ;;
+  darwin*)
+    if test yes = "$GCC"; then
+      reload_cmds='$LTCC $LTCFLAGS -nostdlib $wl-r -o $output$reload_objs'
+    else
+      reload_cmds='$LD$reload_flag -o $output$reload_objs'
+    fi
+    ;;
+esac
+_LT_TAGDECL([], [reload_flag], [1], [How to create reloadable object files])dnl
+_LT_TAGDECL([], [reload_cmds], [2])dnl
+])# _LT_CMD_RELOAD
+
+
+# _LT_PATH_DD
+# -----------
+# find a working dd
+m4_defun([_LT_PATH_DD],
+[AC_CACHE_CHECK([for a working dd], [ac_cv_path_lt_DD],
+[printf 0123456789abcdef0123456789abcdef >conftest.i
+cat conftest.i conftest.i >conftest2.i
+: ${lt_DD:=$DD}
+AC_PATH_PROGS_FEATURE_CHECK([lt_DD], [dd],
+[if "$ac_path_lt_DD" bs=32 count=1 <conftest2.i >conftest.out 2>/dev/null; then
+  cmp -s conftest.i conftest.out \
+  && ac_cv_path_lt_DD="$ac_path_lt_DD" ac_path_lt_DD_found=:
+fi])
+rm -f conftest.i conftest2.i conftest.out])
+])# _LT_PATH_DD
+
+
+# _LT_CMD_TRUNCATE
+# ----------------
+# find command to truncate a binary pipe
+m4_defun([_LT_CMD_TRUNCATE],
+[m4_require([_LT_PATH_DD])
+AC_CACHE_CHECK([how to truncate binary pipes], [lt_cv_truncate_bin],
+[printf 0123456789abcdef0123456789abcdef >conftest.i
+cat conftest.i conftest.i >conftest2.i
+lt_cv_truncate_bin=
+if "$ac_cv_path_lt_DD" bs=32 count=1 <conftest2.i >conftest.out 2>/dev/null; then
+  cmp -s conftest.i conftest.out \
+  && lt_cv_truncate_bin="$ac_cv_path_lt_DD bs=4096 count=1"
+fi
+rm -f conftest.i conftest2.i conftest.out
+test -z "$lt_cv_truncate_bin" && lt_cv_truncate_bin="$SED -e 4q"])
+_LT_DECL([lt_truncate_bin], [lt_cv_truncate_bin], [1],
+  [Command to truncate a binary pipe])
+])# _LT_CMD_TRUNCATE
+
+
+# _LT_CHECK_MAGIC_METHOD
+# ----------------------
+# how to check for library dependencies
+#  -- PORTME fill in with the dynamic library characteristics
+m4_defun([_LT_CHECK_MAGIC_METHOD],
+[m4_require([_LT_DECL_EGREP])
+m4_require([_LT_DECL_OBJDUMP])
+AC_CACHE_CHECK([how to recognize dependent libraries],
+lt_cv_deplibs_check_method,
+[lt_cv_file_magic_cmd='$MAGIC_CMD'
+lt_cv_file_magic_test_file=
+lt_cv_deplibs_check_method='unknown'
+# Need to set the preceding variable on all platforms that support
+# interlibrary dependencies.
+# 'none' -- dependencies not supported.
+# 'unknown' -- same as none, but documents that we really don't know.
+# 'pass_all' -- all dependencies passed with no checks.
+# 'test_compile' -- check by making test program.
+# 'file_magic [[regex]]' -- check by looking for files in library path
+# that responds to the $file_magic_cmd with a given extended regex.
+# If you have 'file' or equivalent on your system and you're not sure
+# whether 'pass_all' will *always* work, you probably want this one.
+
+case $host_os in
+aix[[4-9]]*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+beos*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+bsdi[[45]]*)
+  lt_cv_deplibs_check_method='file_magic ELF [[0-9]][[0-9]]*-bit [[ML]]SB (shared object|dynamic lib)'
+  lt_cv_file_magic_cmd='$FILECMD -L'
+  lt_cv_file_magic_test_file=/shlib/libc.so
+  ;;
+
+cygwin*)
+  # func_win32_libid is a shell function defined in ltmain.sh
+  lt_cv_deplibs_check_method='file_magic ^x86 archive import|^x86 DLL'
+  lt_cv_file_magic_cmd='func_win32_libid'
+  ;;
+
+mingw* | pw32*)
+  # Base MSYS/MinGW do not provide the 'file' command needed by
+  # func_win32_libid shell function, so use a weaker test based on 'objdump',
+  # unless we find 'file', for example because we are cross-compiling.
+  if ( file / ) >/dev/null 2>&1; then
+    lt_cv_deplibs_check_method='file_magic ^x86 archive import|^x86 DLL'
+    lt_cv_file_magic_cmd='func_win32_libid'
+  else
+    # Keep this pattern in sync with the one in func_win32_libid.
+    lt_cv_deplibs_check_method='file_magic file format (pei*-i386(.*architecture: i386)?|pe-arm-wince|pe-x86-64)'
+    lt_cv_file_magic_cmd='$OBJDUMP -f'
+  fi
+  ;;
+
+cegcc*)
+  # use the weaker test based on 'objdump'. See mingw*.
+  lt_cv_deplibs_check_method='file_magic file format pe-arm-.*little(.*architecture: arm)?'
+  lt_cv_file_magic_cmd='$OBJDUMP -f'
+  ;;
+
+darwin* | rhapsody*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+freebsd* | dragonfly* | midnightbsd*)
+  if echo __ELF__ | $CC -E - | $GREP __ELF__ > /dev/null; then
+    case $host_cpu in
+    i*86 )
+      # Not sure whether the presence of OpenBSD here was a mistake.
+      # Let's accept both of them until this is cleared up.
+      lt_cv_deplibs_check_method='file_magic (FreeBSD|OpenBSD|DragonFly)/i[[3-9]]86 (compact )?demand paged shared library'
+      lt_cv_file_magic_cmd=$FILECMD
+      lt_cv_file_magic_test_file=`echo /usr/lib/libc.so.*`
+      ;;
+    esac
+  else
+    lt_cv_deplibs_check_method=pass_all
+  fi
+  ;;
+
+haiku*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+hpux10.20* | hpux11*)
+  lt_cv_file_magic_cmd=$FILECMD
+  case $host_cpu in
+  ia64*)
+    lt_cv_deplibs_check_method='file_magic (s[[0-9]][[0-9]][[0-9]]|ELF-[[0-9]][[0-9]]) shared object file - IA64'
+    lt_cv_file_magic_test_file=/usr/lib/hpux32/libc.so
+    ;;
+  hppa*64*)
+    [lt_cv_deplibs_check_method='file_magic (s[0-9][0-9][0-9]|ELF[ -][0-9][0-9])(-bit)?( [LM]SB)? shared object( file)?[, -]* PA-RISC [0-9]\.[0-9]']
+    lt_cv_file_magic_test_file=/usr/lib/pa20_64/libc.sl
+    ;;
+  *)
+    lt_cv_deplibs_check_method='file_magic (s[[0-9]][[0-9]][[0-9]]|PA-RISC[[0-9]]\.[[0-9]]) shared library'
+    lt_cv_file_magic_test_file=/usr/lib/libc.sl
+    ;;
+  esac
+  ;;
+
+interix[[3-9]]*)
+  # PIC code is broken on Interix 3.x, that's why |\.a not |_pic\.a here
+  lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so|\.a)$'
+  ;;
+
+irix5* | irix6* | nonstopux*)
+  case $LD in
+  *-32|*"-32 ") libmagic=32-bit;;
+  *-n32|*"-n32 ") libmagic=N32;;
+  *-64|*"-64 ") libmagic=64-bit;;
+  *) libmagic=never-match;;
+  esac
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+# This must be glibc/ELF.
+linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+netbsd* | netbsdelf*-gnu)
+  if echo __ELF__ | $CC -E - | $GREP __ELF__ > /dev/null; then
+    lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so\.[[0-9]]+\.[[0-9]]+|_pic\.a)$'
+  else
+    lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so|_pic\.a)$'
+  fi
+  ;;
+
+newos6*)
+  lt_cv_deplibs_check_method='file_magic ELF [[0-9]][[0-9]]*-bit [[ML]]SB (executable|dynamic lib)'
+  lt_cv_file_magic_cmd=$FILECMD
+  lt_cv_file_magic_test_file=/usr/lib/libnls.so
+  ;;
+
+*nto* | *qnx*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+openbsd* | bitrig*)
+  if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then
+    lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so\.[[0-9]]+\.[[0-9]]+|\.so|_pic\.a)$'
+  else
+    lt_cv_deplibs_check_method='match_pattern /lib[[^/]]+(\.so\.[[0-9]]+\.[[0-9]]+|_pic\.a)$'
+  fi
+  ;;
+
+osf3* | osf4* | osf5*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+rdos*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+solaris*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX* | sysv4*uw2*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+sysv4 | sysv4.3*)
+  case $host_vendor in
+  motorola)
+    lt_cv_deplibs_check_method='file_magic ELF [[0-9]][[0-9]]*-bit [[ML]]SB (shared object|dynamic lib) M[[0-9]][[0-9]]* Version [[0-9]]'
+    lt_cv_file_magic_test_file=`echo /usr/lib/libc.so*`
+    ;;
+  ncr)
+    lt_cv_deplibs_check_method=pass_all
+    ;;
+  sequent)
+    lt_cv_file_magic_cmd='/bin/file'
+    lt_cv_deplibs_check_method='file_magic ELF [[0-9]][[0-9]]*-bit [[LM]]SB (shared object|dynamic lib )'
+    ;;
+  sni)
+    lt_cv_file_magic_cmd='/bin/file'
+    lt_cv_deplibs_check_method="file_magic ELF [[0-9]][[0-9]]*-bit [[LM]]SB dynamic lib"
+    lt_cv_file_magic_test_file=/lib/libc.so
+    ;;
+  siemens)
+    lt_cv_deplibs_check_method=pass_all
+    ;;
+  pc)
+    lt_cv_deplibs_check_method=pass_all
+    ;;
+  esac
+  ;;
+
+tpf*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+os2*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+esac
+])
+
+file_magic_glob=
+want_nocaseglob=no
+if test "$build" = "$host"; then
+  case $host_os in
+  mingw* | pw32*)
+    if ( shopt | grep nocaseglob ) >/dev/null 2>&1; then
+      want_nocaseglob=yes
+    else
+      file_magic_glob=`echo aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ | $SED -e "s/\(..\)/s\/[[\1]]\/[[\1]]\/g;/g"`
+    fi
+    ;;
+  esac
+fi
+
+file_magic_cmd=$lt_cv_file_magic_cmd
+deplibs_check_method=$lt_cv_deplibs_check_method
+test -z "$deplibs_check_method" && deplibs_check_method=unknown
+
+_LT_DECL([], [deplibs_check_method], [1],
+    [Method to check whether dependent libraries are shared objects])
+_LT_DECL([], [file_magic_cmd], [1],
+    [Command to use when deplibs_check_method = "file_magic"])
+_LT_DECL([], [file_magic_glob], [1],
+    [How to find potential files when deplibs_check_method = "file_magic"])
+_LT_DECL([], [want_nocaseglob], [1],
+    [Find potential files using nocaseglob when deplibs_check_method = "file_magic"])
+])# _LT_CHECK_MAGIC_METHOD
+
+
+# LT_PATH_NM
+# ----------
+# find the pathname to a BSD- or MS-compatible name lister
+AC_DEFUN([LT_PATH_NM],
+[AC_REQUIRE([AC_PROG_CC])dnl
+AC_CACHE_CHECK([for BSD- or MS-compatible name lister (nm)], lt_cv_path_NM,
+[if test -n "$NM"; then
+  # Let the user override the test.
+  lt_cv_path_NM=$NM
+else
+  lt_nm_to_check=${ac_tool_prefix}nm
+  if test -n "$ac_tool_prefix" && test "$build" = "$host"; then
+    lt_nm_to_check="$lt_nm_to_check nm"
+  fi
+  for lt_tmp_nm in $lt_nm_to_check; do
+    lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR
+    for ac_dir in $PATH /usr/ccs/bin/elf /usr/ccs/bin /usr/ucb /bin; do
+      IFS=$lt_save_ifs
+      test -z "$ac_dir" && ac_dir=.
+      tmp_nm=$ac_dir/$lt_tmp_nm
+      if test -f "$tmp_nm" || test -f "$tmp_nm$ac_exeext"; then
+	# Check to see if the nm accepts a BSD-compat flag.
+	# Adding the 'sed 1q' prevents false positives on HP-UX, which says:
+	#   nm: unknown option "B" ignored
+	# Tru64's nm complains that /dev/null is an invalid object file
+	# MSYS converts /dev/null to NUL, MinGW nm treats NUL as empty
+	case $build_os in
+	mingw*) lt_bad_file=conftest.nm/nofile ;;
+	*) lt_bad_file=/dev/null ;;
+	esac
+	case `"$tmp_nm" -B $lt_bad_file 2>&1 | $SED '1q'` in
+	*$lt_bad_file* | *'Invalid file or object type'*)
+	  lt_cv_path_NM="$tmp_nm -B"
+	  break 2
+	  ;;
+	*)
+	  case `"$tmp_nm" -p /dev/null 2>&1 | $SED '1q'` in
+	  */dev/null*)
+	    lt_cv_path_NM="$tmp_nm -p"
+	    break 2
+	    ;;
+	  *)
+	    lt_cv_path_NM=${lt_cv_path_NM="$tmp_nm"} # keep the first match, but
+	    continue # so that we can try to find one that supports BSD flags
+	    ;;
+	  esac
+	  ;;
+	esac
+      fi
+    done
+    IFS=$lt_save_ifs
+  done
+  : ${lt_cv_path_NM=no}
+fi])
+if test no != "$lt_cv_path_NM"; then
+  NM=$lt_cv_path_NM
+else
+  # Didn't find any BSD compatible name lister, look for dumpbin.
+  if test -n "$DUMPBIN"; then :
+    # Let the user override the test.
+  else
+    AC_CHECK_TOOLS(DUMPBIN, [dumpbin "link -dump"], :)
+    case `$DUMPBIN -symbols -headers /dev/null 2>&1 | $SED '1q'` in
+    *COFF*)
+      DUMPBIN="$DUMPBIN -symbols -headers"
+      ;;
+    *)
+      DUMPBIN=:
+      ;;
+    esac
+  fi
+  AC_SUBST([DUMPBIN])
+  if test : != "$DUMPBIN"; then
+    NM=$DUMPBIN
+  fi
+fi
+test -z "$NM" && NM=nm
+AC_SUBST([NM])
+_LT_DECL([], [NM], [1], [A BSD- or MS-compatible name lister])dnl
+
+AC_CACHE_CHECK([the name lister ($NM) interface], [lt_cv_nm_interface],
+  [lt_cv_nm_interface="BSD nm"
+  echo "int some_variable = 0;" > conftest.$ac_ext
+  (eval echo "\"\$as_me:$LINENO: $ac_compile\"" >&AS_MESSAGE_LOG_FD)
+  (eval "$ac_compile" 2>conftest.err)
+  cat conftest.err >&AS_MESSAGE_LOG_FD
+  (eval echo "\"\$as_me:$LINENO: $NM \\\"conftest.$ac_objext\\\"\"" >&AS_MESSAGE_LOG_FD)
+  (eval "$NM \"conftest.$ac_objext\"" 2>conftest.err > conftest.out)
+  cat conftest.err >&AS_MESSAGE_LOG_FD
+  (eval echo "\"\$as_me:$LINENO: output\"" >&AS_MESSAGE_LOG_FD)
+  cat conftest.out >&AS_MESSAGE_LOG_FD
+  if $GREP 'External.*some_variable' conftest.out > /dev/null; then
+    lt_cv_nm_interface="MS dumpbin"
+  fi
+  rm -f conftest*])
+])# LT_PATH_NM
+
+# Old names:
+AU_ALIAS([AM_PROG_NM], [LT_PATH_NM])
+AU_ALIAS([AC_PROG_NM], [LT_PATH_NM])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AM_PROG_NM], [])
+dnl AC_DEFUN([AC_PROG_NM], [])
+
+# _LT_CHECK_SHAREDLIB_FROM_LINKLIB
+# --------------------------------
+# how to determine the name of the shared library
+# associated with a specific link library.
+#  -- PORTME fill in with the dynamic library characteristics
+m4_defun([_LT_CHECK_SHAREDLIB_FROM_LINKLIB],
+[m4_require([_LT_DECL_EGREP])
+m4_require([_LT_DECL_OBJDUMP])
+m4_require([_LT_DECL_DLLTOOL])
+AC_CACHE_CHECK([how to associate runtime and link libraries],
+lt_cv_sharedlib_from_linklib_cmd,
+[lt_cv_sharedlib_from_linklib_cmd='unknown'
+
+case $host_os in
+cygwin* | mingw* | pw32* | cegcc*)
+  # two different shell functions defined in ltmain.sh;
+  # decide which one to use based on capabilities of $DLLTOOL
+  case `$DLLTOOL --help 2>&1` in
+  *--identify-strict*)
+    lt_cv_sharedlib_from_linklib_cmd=func_cygming_dll_for_implib
+    ;;
+  *)
+    lt_cv_sharedlib_from_linklib_cmd=func_cygming_dll_for_implib_fallback
+    ;;
+  esac
+  ;;
+*)
+  # fallback: assume linklib IS sharedlib
+  lt_cv_sharedlib_from_linklib_cmd=$ECHO
+  ;;
+esac
+])
+sharedlib_from_linklib_cmd=$lt_cv_sharedlib_from_linklib_cmd
+test -z "$sharedlib_from_linklib_cmd" && sharedlib_from_linklib_cmd=$ECHO
+
+_LT_DECL([], [sharedlib_from_linklib_cmd], [1],
+    [Command to associate shared and link libraries])
+])# _LT_CHECK_SHAREDLIB_FROM_LINKLIB
+
+
+# _LT_PATH_MANIFEST_TOOL
+# ----------------------
+# locate the manifest tool
+m4_defun([_LT_PATH_MANIFEST_TOOL],
+[AC_CHECK_TOOL(MANIFEST_TOOL, mt, :)
+test -z "$MANIFEST_TOOL" && MANIFEST_TOOL=mt
+AC_CACHE_CHECK([if $MANIFEST_TOOL is a manifest tool], [lt_cv_path_mainfest_tool],
+  [lt_cv_path_mainfest_tool=no
+  echo "$as_me:$LINENO: $MANIFEST_TOOL '-?'" >&AS_MESSAGE_LOG_FD
+  $MANIFEST_TOOL '-?' 2>conftest.err > conftest.out
+  cat conftest.err >&AS_MESSAGE_LOG_FD
+  if $GREP 'Manifest Tool' conftest.out > /dev/null; then
+    lt_cv_path_mainfest_tool=yes
+  fi
+  rm -f conftest*])
+if test yes != "$lt_cv_path_mainfest_tool"; then
+  MANIFEST_TOOL=:
+fi
+_LT_DECL([], [MANIFEST_TOOL], [1], [Manifest tool])dnl
+])# _LT_PATH_MANIFEST_TOOL
+
+
+# _LT_DLL_DEF_P([FILE])
+# ---------------------
+# True iff FILE is a Windows DLL '.def' file.
+# Keep in sync with func_dll_def_p in the libtool script
+AC_DEFUN([_LT_DLL_DEF_P],
+[dnl
+  test DEF = "`$SED -n dnl
+    -e '\''s/^[[	 ]]*//'\'' dnl Strip leading whitespace
+    -e '\''/^\(;.*\)*$/d'\'' dnl      Delete empty lines and comments
+    -e '\''s/^\(EXPORTS\|LIBRARY\)\([[	 ]].*\)*$/DEF/p'\'' dnl
+    -e q dnl                          Only consider the first "real" line
+    $1`" dnl
+])# _LT_DLL_DEF_P
+
+
+# LT_LIB_M
+# --------
+# check for math library
+AC_DEFUN([LT_LIB_M],
+[AC_REQUIRE([AC_CANONICAL_HOST])dnl
+LIBM=
+case $host in
+*-*-beos* | *-*-cegcc* | *-*-cygwin* | *-*-haiku* | *-*-pw32* | *-*-darwin*)
+  # These system don't have libm, or don't need it
+  ;;
+*-ncr-sysv4.3*)
+  AC_CHECK_LIB(mw, _mwvalidcheckl, LIBM=-lmw)
+  AC_CHECK_LIB(m, cos, LIBM="$LIBM -lm")
+  ;;
+*)
+  AC_CHECK_LIB(m, cos, LIBM=-lm)
+  ;;
+esac
+AC_SUBST([LIBM])
+])# LT_LIB_M
+
+# Old name:
+AU_ALIAS([AC_CHECK_LIBM], [LT_LIB_M])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_CHECK_LIBM], [])
+
+
+# _LT_COMPILER_NO_RTTI([TAGNAME])
+# -------------------------------
+m4_defun([_LT_COMPILER_NO_RTTI],
+[m4_require([_LT_TAG_COMPILER])dnl
+
+_LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)=
+
+if test yes = "$GCC"; then
+  case $cc_basename in
+  nvcc*)
+    _LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)=' -Xcompiler -fno-builtin' ;;
+  *)
+    _LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)=' -fno-builtin' ;;
+  esac
+
+  _LT_COMPILER_OPTION([if $compiler supports -fno-rtti -fno-exceptions],
+    lt_cv_prog_compiler_rtti_exceptions,
+    [-fno-rtti -fno-exceptions], [],
+    [_LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)="$_LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1) -fno-rtti -fno-exceptions"])
+fi
+_LT_TAGDECL([no_builtin_flag], [lt_prog_compiler_no_builtin_flag], [1],
+	[Compiler flag to turn off builtin functions])
+])# _LT_COMPILER_NO_RTTI
+
+
+# _LT_CMD_GLOBAL_SYMBOLS
+# ----------------------
+m4_defun([_LT_CMD_GLOBAL_SYMBOLS],
+[AC_REQUIRE([AC_CANONICAL_HOST])dnl
+AC_REQUIRE([AC_PROG_CC])dnl
+AC_REQUIRE([AC_PROG_AWK])dnl
+AC_REQUIRE([LT_PATH_NM])dnl
+AC_REQUIRE([LT_PATH_LD])dnl
+m4_require([_LT_DECL_SED])dnl
+m4_require([_LT_DECL_EGREP])dnl
+m4_require([_LT_TAG_COMPILER])dnl
+
+# Check for command to grab the raw symbol name followed by C symbol from nm.
+AC_MSG_CHECKING([command to parse $NM output from $compiler object])
+AC_CACHE_VAL([lt_cv_sys_global_symbol_pipe],
+[
+# These are sane defaults that work on at least a few old systems.
+# [They come from Ultrix.  What could be older than Ultrix?!! ;)]
+
+# Character class describing NM global symbol codes.
+symcode='[[BCDEGRST]]'
+
+# Regexp to match symbols that can be accessed directly from C.
+sympat='\([[_A-Za-z]][[_A-Za-z0-9]]*\)'
+
+# Define system-specific variables.
+case $host_os in
+aix*)
+  symcode='[[BCDT]]'
+  ;;
+cygwin* | mingw* | pw32* | cegcc*)
+  symcode='[[ABCDGISTW]]'
+  ;;
+hpux*)
+  if test ia64 = "$host_cpu"; then
+    symcode='[[ABCDEGRST]]'
+  fi
+  ;;
+irix* | nonstopux*)
+  symcode='[[BCDEGRST]]'
+  ;;
+osf*)
+  symcode='[[BCDEGQRST]]'
+  ;;
+solaris*)
+  symcode='[[BDRT]]'
+  ;;
+sco3.2v5*)
+  symcode='[[DT]]'
+  ;;
+sysv4.2uw2*)
+  symcode='[[DT]]'
+  ;;
+sysv5* | sco5v6* | unixware* | OpenUNIX*)
+  symcode='[[ABDT]]'
+  ;;
+sysv4)
+  symcode='[[DFNSTU]]'
+  ;;
+esac
+
+# If we're using GNU nm, then use its standard symbol codes.
+case `$NM -V 2>&1` in
+*GNU* | *'with BFD'*)
+  symcode='[[ABCDGIRSTW]]' ;;
+esac
+
+if test "$lt_cv_nm_interface" = "MS dumpbin"; then
+  # Gets list of data symbols to import.
+  lt_cv_sys_global_symbol_to_import="$SED -n -e 's/^I .* \(.*\)$/\1/p'"
+  # Adjust the below global symbol transforms to fixup imported variables.
+  lt_cdecl_hook=" -e 's/^I .* \(.*\)$/extern __declspec(dllimport) char \1;/p'"
+  lt_c_name_hook=" -e 's/^I .* \(.*\)$/  {\"\1\", (void *) 0},/p'"
+  lt_c_name_lib_hook="\
+  -e 's/^I .* \(lib.*\)$/  {\"\1\", (void *) 0},/p'\
+  -e 's/^I .* \(.*\)$/  {\"lib\1\", (void *) 0},/p'"
+else
+  # Disable hooks by default.
+  lt_cv_sys_global_symbol_to_import=
+  lt_cdecl_hook=
+  lt_c_name_hook=
+  lt_c_name_lib_hook=
+fi
+
+# Transform an extracted symbol line into a proper C declaration.
+# Some systems (esp. on ia64) link data and code symbols differently,
+# so use this general approach.
+lt_cv_sys_global_symbol_to_cdecl="$SED -n"\
+$lt_cdecl_hook\
+" -e 's/^T .* \(.*\)$/extern int \1();/p'"\
+" -e 's/^$symcode$symcode* .* \(.*\)$/extern char \1;/p'"
+
+# Transform an extracted symbol line into symbol name and symbol address
+lt_cv_sys_global_symbol_to_c_name_address="$SED -n"\
+$lt_c_name_hook\
+" -e 's/^: \(.*\) .*$/  {\"\1\", (void *) 0},/p'"\
+" -e 's/^$symcode$symcode* .* \(.*\)$/  {\"\1\", (void *) \&\1},/p'"
+
+# Transform an extracted symbol line into symbol name with lib prefix and
+# symbol address.
+lt_cv_sys_global_symbol_to_c_name_address_lib_prefix="$SED -n"\
+$lt_c_name_lib_hook\
+" -e 's/^: \(.*\) .*$/  {\"\1\", (void *) 0},/p'"\
+" -e 's/^$symcode$symcode* .* \(lib.*\)$/  {\"\1\", (void *) \&\1},/p'"\
+" -e 's/^$symcode$symcode* .* \(.*\)$/  {\"lib\1\", (void *) \&\1},/p'"
+
+# Handle CRLF in mingw tool chain
+opt_cr=
+case $build_os in
+mingw*)
+  opt_cr=`$ECHO 'x\{0,1\}' | tr x '\015'` # option cr in regexp
+  ;;
+esac
+
+# Try without a prefix underscore, then with it.
+for ac_symprfx in "" "_"; do
+
+  # Transform symcode, sympat, and symprfx into a raw symbol and a C symbol.
+  symxfrm="\\1 $ac_symprfx\\2 \\2"
+
+  # Write the raw and C identifiers.
+  if test "$lt_cv_nm_interface" = "MS dumpbin"; then
+    # Fake it for dumpbin and say T for any non-static function,
+    # D for any global variable and I for any imported variable.
+    # Also find C++ and __fastcall symbols from MSVC++ or ICC,
+    # which start with @ or ?.
+    lt_cv_sys_global_symbol_pipe="$AWK ['"\
+"     {last_section=section; section=\$ 3};"\
+"     /^COFF SYMBOL TABLE/{for(i in hide) delete hide[i]};"\
+"     /Section length .*#relocs.*(pick any)/{hide[last_section]=1};"\
+"     /^ *Symbol name *: /{split(\$ 0,sn,\":\"); si=substr(sn[2],2)};"\
+"     /^ *Type *: code/{print \"T\",si,substr(si,length(prfx))};"\
+"     /^ *Type *: data/{print \"I\",si,substr(si,length(prfx))};"\
+"     \$ 0!~/External *\|/{next};"\
+"     / 0+ UNDEF /{next}; / UNDEF \([^|]\)*()/{next};"\
+"     {if(hide[section]) next};"\
+"     {f=\"D\"}; \$ 0~/\(\).*\|/{f=\"T\"};"\
+"     {split(\$ 0,a,/\||\r/); split(a[2],s)};"\
+"     s[1]~/^[@?]/{print f,s[1],s[1]; next};"\
+"     s[1]~prfx {split(s[1],t,\"@\"); print f,t[1],substr(t[1],length(prfx))}"\
+"     ' prfx=^$ac_symprfx]"
+  else
+    lt_cv_sys_global_symbol_pipe="$SED -n -e 's/^.*[[	 ]]\($symcode$symcode*\)[[	 ]][[	 ]]*$ac_symprfx$sympat$opt_cr$/$symxfrm/p'"
+  fi
+  lt_cv_sys_global_symbol_pipe="$lt_cv_sys_global_symbol_pipe | $SED '/ __gnu_lto/d'"
+
+  # Check to see that the pipe works correctly.
+  pipe_works=no
+
+  rm -f conftest*
+  cat > conftest.$ac_ext <<_LT_EOF
+#ifdef __cplusplus
+extern "C" {
+#endif
+char nm_test_var;
+void nm_test_func(void);
+void nm_test_func(void){}
+#ifdef __cplusplus
+}
+#endif
+int main(){nm_test_var='a';nm_test_func();return(0);}
+_LT_EOF
+
+  if AC_TRY_EVAL(ac_compile); then
+    # Now try to grab the symbols.
+    nlist=conftest.nm
+    $ECHO "$as_me:$LINENO: $NM conftest.$ac_objext | $lt_cv_sys_global_symbol_pipe > $nlist" >&AS_MESSAGE_LOG_FD
+    if eval "$NM" conftest.$ac_objext \| "$lt_cv_sys_global_symbol_pipe" \> $nlist 2>&AS_MESSAGE_LOG_FD && test -s "$nlist"; then
+      # Try sorting and uniquifying the output.
+      if sort "$nlist" | uniq > "$nlist"T; then
+	mv -f "$nlist"T "$nlist"
+      else
+	rm -f "$nlist"T
+      fi
+
+      # Make sure that we snagged all the symbols we need.
+      if $GREP ' nm_test_var$' "$nlist" >/dev/null; then
+	if $GREP ' nm_test_func$' "$nlist" >/dev/null; then
+	  cat <<_LT_EOF > conftest.$ac_ext
+/* Keep this code in sync between libtool.m4, ltmain, lt_system.h, and tests.  */
+#if defined _WIN32 || defined __CYGWIN__ || defined _WIN32_WCE
+/* DATA imports from DLLs on WIN32 can't be const, because runtime
+   relocations are performed -- see ld's documentation on pseudo-relocs.  */
+# define LT@&t@_DLSYM_CONST
+#elif defined __osf__
+/* This system does not cope well with relocations in const data.  */
+# define LT@&t@_DLSYM_CONST
+#else
+# define LT@&t@_DLSYM_CONST const
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+_LT_EOF
+	  # Now generate the symbol file.
+	  eval "$lt_cv_sys_global_symbol_to_cdecl"' < "$nlist" | $GREP -v main >> conftest.$ac_ext'
+
+	  cat <<_LT_EOF >> conftest.$ac_ext
+
+/* The mapping between symbol names and symbols.  */
+LT@&t@_DLSYM_CONST struct {
+  const char *name;
+  void       *address;
+}
+lt__PROGRAM__LTX_preloaded_symbols[[]] =
+{
+  { "@PROGRAM@", (void *) 0 },
+_LT_EOF
+	  $SED "s/^$symcode$symcode* .* \(.*\)$/  {\"\1\", (void *) \&\1},/" < "$nlist" | $GREP -v main >> conftest.$ac_ext
+	  cat <<\_LT_EOF >> conftest.$ac_ext
+  {0, (void *) 0}
+};
+
+/* This works around a problem in FreeBSD linker */
+#ifdef FREEBSD_WORKAROUND
+static const void *lt_preloaded_setup() {
+  return lt__PROGRAM__LTX_preloaded_symbols;
+}
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+_LT_EOF
+	  # Now try linking the two files.
+	  mv conftest.$ac_objext conftstm.$ac_objext
+	  lt_globsym_save_LIBS=$LIBS
+	  lt_globsym_save_CFLAGS=$CFLAGS
+	  LIBS=conftstm.$ac_objext
+	  CFLAGS="$CFLAGS$_LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)"
+	  if AC_TRY_EVAL(ac_link) && test -s conftest$ac_exeext; then
+	    pipe_works=yes
+	  fi
+	  LIBS=$lt_globsym_save_LIBS
+	  CFLAGS=$lt_globsym_save_CFLAGS
+	else
+	  echo "cannot find nm_test_func in $nlist" >&AS_MESSAGE_LOG_FD
+	fi
+      else
+	echo "cannot find nm_test_var in $nlist" >&AS_MESSAGE_LOG_FD
+      fi
+    else
+      echo "cannot run $lt_cv_sys_global_symbol_pipe" >&AS_MESSAGE_LOG_FD
+    fi
+  else
+    echo "$progname: failed program was:" >&AS_MESSAGE_LOG_FD
+    cat conftest.$ac_ext >&5
+  fi
+  rm -rf conftest* conftst*
+
+  # Do not use the global_symbol_pipe unless it works.
+  if test yes = "$pipe_works"; then
+    break
+  else
+    lt_cv_sys_global_symbol_pipe=
+  fi
+done
+])
+if test -z "$lt_cv_sys_global_symbol_pipe"; then
+  lt_cv_sys_global_symbol_to_cdecl=
+fi
+if test -z "$lt_cv_sys_global_symbol_pipe$lt_cv_sys_global_symbol_to_cdecl"; then
+  AC_MSG_RESULT(failed)
+else
+  AC_MSG_RESULT(ok)
+fi
+
+# Response file support.
+if test "$lt_cv_nm_interface" = "MS dumpbin"; then
+  nm_file_list_spec='@'
+elif $NM --help 2>/dev/null | grep '[[@]]FILE' >/dev/null; then
+  nm_file_list_spec='@'
+fi
+
+_LT_DECL([global_symbol_pipe], [lt_cv_sys_global_symbol_pipe], [1],
+    [Take the output of nm and produce a listing of raw symbols and C names])
+_LT_DECL([global_symbol_to_cdecl], [lt_cv_sys_global_symbol_to_cdecl], [1],
+    [Transform the output of nm in a proper C declaration])
+_LT_DECL([global_symbol_to_import], [lt_cv_sys_global_symbol_to_import], [1],
+    [Transform the output of nm into a list of symbols to manually relocate])
+_LT_DECL([global_symbol_to_c_name_address],
+    [lt_cv_sys_global_symbol_to_c_name_address], [1],
+    [Transform the output of nm in a C name address pair])
+_LT_DECL([global_symbol_to_c_name_address_lib_prefix],
+    [lt_cv_sys_global_symbol_to_c_name_address_lib_prefix], [1],
+    [Transform the output of nm in a C name address pair when lib prefix is needed])
+_LT_DECL([nm_interface], [lt_cv_nm_interface], [1],
+    [The name lister interface])
+_LT_DECL([], [nm_file_list_spec], [1],
+    [Specify filename containing input files for $NM])
+]) # _LT_CMD_GLOBAL_SYMBOLS
+
+
+# _LT_COMPILER_PIC([TAGNAME])
+# ---------------------------
+m4_defun([_LT_COMPILER_PIC],
+[m4_require([_LT_TAG_COMPILER])dnl
+_LT_TAGVAR(lt_prog_compiler_wl, $1)=
+_LT_TAGVAR(lt_prog_compiler_pic, $1)=
+_LT_TAGVAR(lt_prog_compiler_static, $1)=
+
+m4_if([$1], [CXX], [
+  # C++ specific cases for pic, static, wl, etc.
+  if test yes = "$GXX"; then
+    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+    _LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+
+    case $host_os in
+    aix*)
+      # All AIX code is PIC.
+      if test ia64 = "$host_cpu"; then
+	# AIX 5 now supports IA64 processor
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      fi
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+      ;;
+
+    amigaos*)
+      case $host_cpu in
+      powerpc)
+            # see comment about AmigaOS4 .so support
+            _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+        ;;
+      m68k)
+            # FIXME: we need at least 68020 code to build shared libraries, but
+            # adding the '-m68020' flag to GCC prevents building anything better,
+            # like '-m68040'.
+            _LT_TAGVAR(lt_prog_compiler_pic, $1)='-m68020 -resident32 -malways-restore-a4'
+        ;;
+      esac
+      ;;
+
+    beos* | irix5* | irix6* | nonstopux* | osf3* | osf4* | osf5*)
+      # PIC is the default for these OSes.
+      ;;
+    mingw* | cygwin* | os2* | pw32* | cegcc*)
+      # This hack is so that the source file can tell whether it is being
+      # built for inclusion in a dll (and should export symbols for example).
+      # Although the cygwin gcc ignores -fPIC, still need this for old-style
+      # (--disable-auto-import) libraries
+      m4_if([$1], [GCJ], [],
+	[_LT_TAGVAR(lt_prog_compiler_pic, $1)='-DDLL_EXPORT'])
+      case $host_os in
+      os2*)
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-static'
+	;;
+      esac
+      ;;
+    darwin* | rhapsody*)
+      # PIC is the default on this platform
+      # Common symbols not allowed in MH_DYLIB files
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fno-common'
+      ;;
+    *djgpp*)
+      # DJGPP does not support shared libraries at all
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)=
+      ;;
+    haiku*)
+      # PIC is the default for Haiku.
+      # The "-static" flag exists, but is broken.
+      _LT_TAGVAR(lt_prog_compiler_static, $1)=
+      ;;
+    interix[[3-9]]*)
+      # Interix 3.x gcc -fpic/-fPIC options generate broken code.
+      # Instead, we relocate shared libraries at runtime.
+      ;;
+    sysv4*MP*)
+      if test -d /usr/nec; then
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)=-Kconform_pic
+      fi
+      ;;
+    hpux*)
+      # PIC is the default for 64-bit PA HP-UX, but not for 32-bit
+      # PA HP-UX.  On IA64 HP-UX, PIC is the default but the pic flag
+      # sets the default TLS model and affects inlining.
+      case $host_cpu in
+      hppa*64*)
+	;;
+      *)
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+	;;
+      esac
+      ;;
+    *qnx* | *nto*)
+      # QNX uses GNU C++, but need to define -shared option too, otherwise
+      # it will coredump.
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC -shared'
+      ;;
+    *)
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+      ;;
+    esac
+  else
+    case $host_os in
+      aix[[4-9]]*)
+	# All AIX code is PIC.
+	if test ia64 = "$host_cpu"; then
+	  # AIX 5 now supports IA64 processor
+	  _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	else
+	  _LT_TAGVAR(lt_prog_compiler_static, $1)='-bnso -bI:/lib/syscalls.exp'
+	fi
+	;;
+      chorus*)
+	case $cc_basename in
+	cxch68*)
+	  # Green Hills C++ Compiler
+	  # _LT_TAGVAR(lt_prog_compiler_static, $1)="--no_auto_instantiation -u __main -u __premain -u _abort -r $COOL_DIR/lib/libOrb.a $MVME_DIR/lib/CC/libC.a $MVME_DIR/lib/classix/libcx.s.a"
+	  ;;
+	esac
+	;;
+      mingw* | cygwin* | os2* | pw32* | cegcc*)
+	# This hack is so that the source file can tell whether it is being
+	# built for inclusion in a dll (and should export symbols for example).
+	m4_if([$1], [GCJ], [],
+	  [_LT_TAGVAR(lt_prog_compiler_pic, $1)='-DDLL_EXPORT'])
+	;;
+      dgux*)
+	case $cc_basename in
+	  ec++*)
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	    ;;
+	  ghcx*)
+	    # Green Hills C++ Compiler
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic'
+	    ;;
+	  *)
+	    ;;
+	esac
+	;;
+      freebsd* | dragonfly* | midnightbsd*)
+	# FreeBSD uses GNU C++
+	;;
+      hpux9* | hpux10* | hpux11*)
+	case $cc_basename in
+	  CC*)
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-a ${wl}archive'
+	    if test ia64 != "$host_cpu"; then
+	      _LT_TAGVAR(lt_prog_compiler_pic, $1)='+Z'
+	    fi
+	    ;;
+	  aCC*)
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-a ${wl}archive'
+	    case $host_cpu in
+	    hppa*64*|ia64*)
+	      # +Z the default
+	      ;;
+	    *)
+	      _LT_TAGVAR(lt_prog_compiler_pic, $1)='+Z'
+	      ;;
+	    esac
+	    ;;
+	  *)
+	    ;;
+	esac
+	;;
+      interix*)
+	# This is c89, which is MS Visual C++ (no shared libs)
+	# Anyone wants to do a port?
+	;;
+      irix5* | irix6* | nonstopux*)
+	case $cc_basename in
+	  CC*)
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared'
+	    # CC pic flag -KPIC is the default.
+	    ;;
+	  *)
+	    ;;
+	esac
+	;;
+      linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*)
+	case $cc_basename in
+	  KCC*)
+	    # KAI C++ Compiler
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='--backend -Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+	    ;;
+	  ecpc* )
+	    # old Intel C++ for x86_64, which still supported -KPIC.
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+	    ;;
+	  icpc* )
+	    # Intel C++, used to be incompatible with GCC.
+	    # ICC 10 doesn't accept -KPIC any more.
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+	    ;;
+	  pgCC* | pgcpp*)
+	    # Portland Group C++ compiler
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fpic'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	    ;;
+	  cxx*)
+	    # Compaq C++
+	    # Make sure the PIC flag is empty.  It appears that all Alpha
+	    # Linux and Compaq Tru64 Unix objects are PIC.
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)=
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared'
+	    ;;
+	  xlc* | xlC* | bgxl[[cC]]* | mpixl[[cC]]*)
+	    # IBM XL 8.0, 9.0 on PPC and BlueGene
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-qpic'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-qstaticlink'
+	    ;;
+	  *)
+	    case `$CC -V 2>&1 | $SED 5q` in
+	    *Sun\ C*)
+	      # Sun C++ 5.9
+	      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	      _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld '
+	      ;;
+	    esac
+	    ;;
+	esac
+	;;
+      lynxos*)
+	;;
+      m88k*)
+	;;
+      mvs*)
+	case $cc_basename in
+	  cxx*)
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-W c,exportall'
+	    ;;
+	  *)
+	    ;;
+	esac
+	;;
+      netbsd* | netbsdelf*-gnu)
+	;;
+      *qnx* | *nto*)
+        # QNX uses GNU C++, but need to define -shared option too, otherwise
+        # it will coredump.
+        _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC -shared'
+        ;;
+      osf3* | osf4* | osf5*)
+	case $cc_basename in
+	  KCC*)
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='--backend -Wl,'
+	    ;;
+	  RCC*)
+	    # Rational C++ 2.4.1
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic'
+	    ;;
+	  cxx*)
+	    # Digital/Compaq C++
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    # Make sure the PIC flag is empty.  It appears that all Alpha
+	    # Linux and Compaq Tru64 Unix objects are PIC.
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)=
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared'
+	    ;;
+	  *)
+	    ;;
+	esac
+	;;
+      psos*)
+	;;
+      solaris*)
+	case $cc_basename in
+	  CC* | sunCC*)
+	    # Sun C++ 4.2, 5.x and Centerline C++
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld '
+	    ;;
+	  gcx*)
+	    # Green Hills C++ Compiler
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-PIC'
+	    ;;
+	  *)
+	    ;;
+	esac
+	;;
+      sunos4*)
+	case $cc_basename in
+	  CC*)
+	    # Sun C++ 4.x
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	    ;;
+	  lcc*)
+	    # Lucid
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic'
+	    ;;
+	  *)
+	    ;;
+	esac
+	;;
+      sysv5* | unixware* | sco3.2v5* | sco5v6* | OpenUNIX*)
+	case $cc_basename in
+	  CC*)
+	    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	    _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	    ;;
+	esac
+	;;
+      tandem*)
+	case $cc_basename in
+	  NCC*)
+	    # NonStop-UX NCC 3.20
+	    _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	    ;;
+	  *)
+	    ;;
+	esac
+	;;
+      vxworks*)
+	;;
+      *)
+	_LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no
+	;;
+    esac
+  fi
+],
+[
+  if test yes = "$GCC"; then
+    _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+    _LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+
+    case $host_os in
+      aix*)
+      # All AIX code is PIC.
+      if test ia64 = "$host_cpu"; then
+	# AIX 5 now supports IA64 processor
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      fi
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+      ;;
+
+    amigaos*)
+      case $host_cpu in
+      powerpc)
+            # see comment about AmigaOS4 .so support
+            _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+        ;;
+      m68k)
+            # FIXME: we need at least 68020 code to build shared libraries, but
+            # adding the '-m68020' flag to GCC prevents building anything better,
+            # like '-m68040'.
+            _LT_TAGVAR(lt_prog_compiler_pic, $1)='-m68020 -resident32 -malways-restore-a4'
+        ;;
+      esac
+      ;;
+
+    beos* | irix5* | irix6* | nonstopux* | osf3* | osf4* | osf5*)
+      # PIC is the default for these OSes.
+      ;;
+
+    mingw* | cygwin* | pw32* | os2* | cegcc*)
+      # This hack is so that the source file can tell whether it is being
+      # built for inclusion in a dll (and should export symbols for example).
+      # Although the cygwin gcc ignores -fPIC, still need this for old-style
+      # (--disable-auto-import) libraries
+      m4_if([$1], [GCJ], [],
+	[_LT_TAGVAR(lt_prog_compiler_pic, $1)='-DDLL_EXPORT'])
+      case $host_os in
+      os2*)
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-static'
+	;;
+      esac
+      ;;
+
+    darwin* | rhapsody*)
+      # PIC is the default on this platform
+      # Common symbols not allowed in MH_DYLIB files
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fno-common'
+      ;;
+
+    haiku*)
+      # PIC is the default for Haiku.
+      # The "-static" flag exists, but is broken.
+      _LT_TAGVAR(lt_prog_compiler_static, $1)=
+      ;;
+
+    hpux*)
+      # PIC is the default for 64-bit PA HP-UX, but not for 32-bit
+      # PA HP-UX.  On IA64 HP-UX, PIC is the default but the pic flag
+      # sets the default TLS model and affects inlining.
+      case $host_cpu in
+      hppa*64*)
+	# +Z the default
+	;;
+      *)
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+	;;
+      esac
+      ;;
+
+    interix[[3-9]]*)
+      # Interix 3.x gcc -fpic/-fPIC options generate broken code.
+      # Instead, we relocate shared libraries at runtime.
+      ;;
+
+    msdosdjgpp*)
+      # Just because we use GCC doesn't mean we suddenly get shared libraries
+      # on systems that don't support them.
+      _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no
+      enable_shared=no
+      ;;
+
+    *nto* | *qnx*)
+      # QNX uses GNU C++, but need to define -shared option too, otherwise
+      # it will coredump.
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC -shared'
+      ;;
+
+    sysv4*MP*)
+      if test -d /usr/nec; then
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)=-Kconform_pic
+      fi
+      ;;
+
+    *)
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+      ;;
+    esac
+
+    case $cc_basename in
+    nvcc*) # Cuda Compiler Driver 2.2
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Xlinker '
+      if test -n "$_LT_TAGVAR(lt_prog_compiler_pic, $1)"; then
+        _LT_TAGVAR(lt_prog_compiler_pic, $1)="-Xcompiler $_LT_TAGVAR(lt_prog_compiler_pic, $1)"
+      fi
+      ;;
+    esac
+  else
+    # PORTME Check for flag to pass linker flags through the system compiler.
+    case $host_os in
+    aix*)
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+      if test ia64 = "$host_cpu"; then
+	# AIX 5 now supports IA64 processor
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      else
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-bnso -bI:/lib/syscalls.exp'
+      fi
+      ;;
+
+    darwin* | rhapsody*)
+      # PIC is the default on this platform
+      # Common symbols not allowed in MH_DYLIB files
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fno-common'
+      case $cc_basename in
+      nagfor*)
+        # NAG Fortran compiler
+        _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,-Wl,,'
+        _LT_TAGVAR(lt_prog_compiler_pic, $1)='-PIC'
+        _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+        ;;
+      esac
+      ;;
+
+    mingw* | cygwin* | pw32* | os2* | cegcc*)
+      # This hack is so that the source file can tell whether it is being
+      # built for inclusion in a dll (and should export symbols for example).
+      m4_if([$1], [GCJ], [],
+	[_LT_TAGVAR(lt_prog_compiler_pic, $1)='-DDLL_EXPORT'])
+      case $host_os in
+      os2*)
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-static'
+	;;
+      esac
+      ;;
+
+    hpux9* | hpux10* | hpux11*)
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+      # PIC is the default for IA64 HP-UX and 64-bit HP-UX, but
+      # not for PA HP-UX.
+      case $host_cpu in
+      hppa*64*|ia64*)
+	# +Z the default
+	;;
+      *)
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='+Z'
+	;;
+      esac
+      # Is there a better lt_prog_compiler_static that works with the bundled CC?
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='$wl-a ${wl}archive'
+      ;;
+
+    irix5* | irix6* | nonstopux*)
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+      # PIC (with -KPIC) is the default.
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared'
+      ;;
+
+    linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*)
+      case $cc_basename in
+      # old Intel for x86_64, which still supported -KPIC.
+      ecc*)
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+        ;;
+      # flang / f18. f95 an alias for gfortran or flang on Debian
+      flang* | f18* | f95*)
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+        ;;
+      # icc used to be incompatible with GCC.
+      # ICC 10 doesn't accept -KPIC any more.
+      icc* | ifort*)
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+        ;;
+      # Lahey Fortran 8.1.
+      lf95*)
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='--shared'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='--static'
+	;;
+      nagfor*)
+	# NAG Fortran compiler
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,-Wl,,'
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-PIC'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	;;
+      tcc*)
+	# Fabrice Bellard et al's Tiny C Compiler
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+	;;
+      pgcc* | pgf77* | pgf90* | pgf95* | pgfortran*)
+        # Portland Group compilers (*not* the Pentium gcc compiler,
+	# which looks to be a dead project)
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-fpic'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+        ;;
+      ccc*)
+        _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+        # All Alpha code is PIC.
+        _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared'
+        ;;
+      xl* | bgxl* | bgf* | mpixl*)
+	# IBM XL C 8.0/Fortran 10.1, 11.1 on PPC and BlueGene
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-qpic'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-qstaticlink'
+	;;
+      *)
+	case `$CC -V 2>&1 | $SED 5q` in
+	*Sun\ Ceres\ Fortran* | *Sun*Fortran*\ [[1-7]].* | *Sun*Fortran*\ 8.[[0-3]]*)
+	  # Sun Fortran 8.3 passes all unrecognized flags to the linker
+	  _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	  _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	  _LT_TAGVAR(lt_prog_compiler_wl, $1)=''
+	  ;;
+	*Sun\ F* | *Sun*Fortran*)
+	  _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	  _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	  _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld '
+	  ;;
+	*Sun\ C*)
+	  # Sun C 5.9
+	  _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+	  _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	  _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	  ;;
+        *Intel*\ [[CF]]*Compiler*)
+	  _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	  _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC'
+	  _LT_TAGVAR(lt_prog_compiler_static, $1)='-static'
+	  ;;
+	*Portland\ Group*)
+	  _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+	  _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fpic'
+	  _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+	  ;;
+	esac
+	;;
+      esac
+      ;;
+
+    newsos6)
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      ;;
+
+    *nto* | *qnx*)
+      # QNX uses GNU C++, but need to define -shared option too, otherwise
+      # it will coredump.
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-fPIC -shared'
+      ;;
+
+    osf3* | osf4* | osf5*)
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+      # All OSF/1 code is PIC.
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared'
+      ;;
+
+    rdos*)
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-non_shared'
+      ;;
+
+    solaris*)
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      case $cc_basename in
+      f77* | f90* | f95* | sunf77* | sunf90* | sunf95*)
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld ';;
+      *)
+	_LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,';;
+      esac
+      ;;
+
+    sunos4*)
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Qoption ld '
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-PIC'
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      ;;
+
+    sysv4 | sysv4.2uw2* | sysv4.3*)
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      ;;
+
+    sysv4*MP*)
+      if test -d /usr/nec; then
+	_LT_TAGVAR(lt_prog_compiler_pic, $1)='-Kconform_pic'
+	_LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      fi
+      ;;
+
+    sysv5* | unixware* | sco3.2v5* | sco5v6* | OpenUNIX*)
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-KPIC'
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      ;;
+
+    unicos*)
+      _LT_TAGVAR(lt_prog_compiler_wl, $1)='-Wl,'
+      _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no
+      ;;
+
+    uts4*)
+      _LT_TAGVAR(lt_prog_compiler_pic, $1)='-pic'
+      _LT_TAGVAR(lt_prog_compiler_static, $1)='-Bstatic'
+      ;;
+
+    *)
+      _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no
+      ;;
+    esac
+  fi
+])
+case $host_os in
+  # For platforms that do not support PIC, -DPIC is meaningless:
+  *djgpp*)
+    _LT_TAGVAR(lt_prog_compiler_pic, $1)=
+    ;;
+  *)
+    _LT_TAGVAR(lt_prog_compiler_pic, $1)="$_LT_TAGVAR(lt_prog_compiler_pic, $1)@&t@m4_if([$1],[],[ -DPIC],[m4_if([$1],[CXX],[ -DPIC],[])])"
+    ;;
+esac
+
+AC_CACHE_CHECK([for $compiler option to produce PIC],
+  [_LT_TAGVAR(lt_cv_prog_compiler_pic, $1)],
+  [_LT_TAGVAR(lt_cv_prog_compiler_pic, $1)=$_LT_TAGVAR(lt_prog_compiler_pic, $1)])
+_LT_TAGVAR(lt_prog_compiler_pic, $1)=$_LT_TAGVAR(lt_cv_prog_compiler_pic, $1)
+
+#
+# Check to make sure the PIC flag actually works.
+#
+if test -n "$_LT_TAGVAR(lt_prog_compiler_pic, $1)"; then
+  _LT_COMPILER_OPTION([if $compiler PIC flag $_LT_TAGVAR(lt_prog_compiler_pic, $1) works],
+    [_LT_TAGVAR(lt_cv_prog_compiler_pic_works, $1)],
+    [$_LT_TAGVAR(lt_prog_compiler_pic, $1)@&t@m4_if([$1],[],[ -DPIC],[m4_if([$1],[CXX],[ -DPIC],[])])], [],
+    [case $_LT_TAGVAR(lt_prog_compiler_pic, $1) in
+     "" | " "*) ;;
+     *) _LT_TAGVAR(lt_prog_compiler_pic, $1)=" $_LT_TAGVAR(lt_prog_compiler_pic, $1)" ;;
+     esac],
+    [_LT_TAGVAR(lt_prog_compiler_pic, $1)=
+     _LT_TAGVAR(lt_prog_compiler_can_build_shared, $1)=no])
+fi
+_LT_TAGDECL([pic_flag], [lt_prog_compiler_pic], [1],
+	[Additional compiler flags for building library objects])
+
+_LT_TAGDECL([wl], [lt_prog_compiler_wl], [1],
+	[How to pass a linker flag through the compiler])
+#
+# Check to make sure the static flag actually works.
+#
+wl=$_LT_TAGVAR(lt_prog_compiler_wl, $1) eval lt_tmp_static_flag=\"$_LT_TAGVAR(lt_prog_compiler_static, $1)\"
+_LT_LINKER_OPTION([if $compiler static flag $lt_tmp_static_flag works],
+  _LT_TAGVAR(lt_cv_prog_compiler_static_works, $1),
+  $lt_tmp_static_flag,
+  [],
+  [_LT_TAGVAR(lt_prog_compiler_static, $1)=])
+_LT_TAGDECL([link_static_flag], [lt_prog_compiler_static], [1],
+	[Compiler flag to prevent dynamic linking])
+])# _LT_COMPILER_PIC
+
+
+# _LT_LINKER_SHLIBS([TAGNAME])
+# ----------------------------
+# See if the linker supports building shared libraries.
+m4_defun([_LT_LINKER_SHLIBS],
+[AC_REQUIRE([LT_PATH_LD])dnl
+AC_REQUIRE([LT_PATH_NM])dnl
+m4_require([_LT_PATH_MANIFEST_TOOL])dnl
+m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+m4_require([_LT_DECL_EGREP])dnl
+m4_require([_LT_DECL_SED])dnl
+m4_require([_LT_CMD_GLOBAL_SYMBOLS])dnl
+m4_require([_LT_TAG_COMPILER])dnl
+AC_MSG_CHECKING([whether the $compiler linker ($LD) supports shared libraries])
+m4_if([$1], [CXX], [
+  _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED '\''s/.* //'\'' | sort | uniq > $export_symbols'
+  _LT_TAGVAR(exclude_expsyms, $1)=['_GLOBAL_OFFSET_TABLE_|_GLOBAL__F[ID]_.*']
+  case $host_os in
+  aix[[4-9]]*)
+    # If we're using GNU nm, then we don't want the "-C" option.
+    # -C means demangle to GNU nm, but means don't demangle to AIX nm.
+    # Without the "-l" option, or with the "-B" option, AIX nm treats
+    # weak defined symbols like other global defined symbols, whereas
+    # GNU nm marks them as "W".
+    # While the 'weak' keyword is ignored in the Export File, we need
+    # it in the Import File for the 'aix-soname' feature, so we have
+    # to replace the "-B" option with "-P" for AIX nm.
+    if $NM -V 2>&1 | $GREP 'GNU' > /dev/null; then
+      _LT_TAGVAR(export_symbols_cmds, $1)='$NM -Bpg $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W")) && ([substr](\$ 3,1,1) != ".")) { if (\$ 2 == "W") { print \$ 3 " weak" } else { print \$ 3 } } }'\'' | sort -u > $export_symbols'
+    else
+      _LT_TAGVAR(export_symbols_cmds, $1)='`func_echo_all $NM | $SED -e '\''s/B\([[^B]]*\)$/P\1/'\''` -PCpgl $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "L") || (\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) && ([substr](\$ 1,1,1) != ".")) { if ((\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) { print \$ 1 " weak" } else { print \$ 1 } } }'\'' | sort -u > $export_symbols'
+    fi
+    ;;
+  pw32*)
+    _LT_TAGVAR(export_symbols_cmds, $1)=$ltdll_cmds
+    ;;
+  cygwin* | mingw* | cegcc*)
+    case $cc_basename in
+    cl* | icl*)
+      _LT_TAGVAR(exclude_expsyms, $1)='_NULL_IMPORT_DESCRIPTOR|_IMPORT_DESCRIPTOR_.*'
+      ;;
+    *)
+      _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[[BCDGRS]][[ ]]/s/.*[[ ]]\([[^ ]]*\)/\1 DATA/;s/^.*[[ ]]__nm__\([[^ ]]*\)[[ ]][[^ ]]*/\1 DATA/;/^I[[ ]]/d;/^[[AITW]][[ ]]/s/.* //'\'' | sort | uniq > $export_symbols'
+      _LT_TAGVAR(exclude_expsyms, $1)=['[_]+GLOBAL_OFFSET_TABLE_|[_]+GLOBAL__[FID]_.*|[_]+head_[A-Za-z0-9_]+_dll|[A-Za-z0-9_]+_dll_iname']
+      ;;
+    esac
+    ;;
+  linux* | k*bsd*-gnu | gnu*)
+    _LT_TAGVAR(link_all_deplibs, $1)=no
+    ;;
+  *)
+    _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED '\''s/.* //'\'' | sort | uniq > $export_symbols'
+    ;;
+  esac
+], [
+  runpath_var=
+  _LT_TAGVAR(allow_undefined_flag, $1)=
+  _LT_TAGVAR(always_export_symbols, $1)=no
+  _LT_TAGVAR(archive_cmds, $1)=
+  _LT_TAGVAR(archive_expsym_cmds, $1)=
+  _LT_TAGVAR(compiler_needs_object, $1)=no
+  _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=no
+  _LT_TAGVAR(export_dynamic_flag_spec, $1)=
+  _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED '\''s/.* //'\'' | sort | uniq > $export_symbols'
+  _LT_TAGVAR(hardcode_automatic, $1)=no
+  _LT_TAGVAR(hardcode_direct, $1)=no
+  _LT_TAGVAR(hardcode_direct_absolute, $1)=no
+  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)=
+  _LT_TAGVAR(hardcode_libdir_separator, $1)=
+  _LT_TAGVAR(hardcode_minus_L, $1)=no
+  _LT_TAGVAR(hardcode_shlibpath_var, $1)=unsupported
+  _LT_TAGVAR(inherit_rpath, $1)=no
+  _LT_TAGVAR(link_all_deplibs, $1)=unknown
+  _LT_TAGVAR(module_cmds, $1)=
+  _LT_TAGVAR(module_expsym_cmds, $1)=
+  _LT_TAGVAR(old_archive_from_new_cmds, $1)=
+  _LT_TAGVAR(old_archive_from_expsyms_cmds, $1)=
+  _LT_TAGVAR(thread_safe_flag_spec, $1)=
+  _LT_TAGVAR(whole_archive_flag_spec, $1)=
+  # include_expsyms should be a list of space-separated symbols to be *always*
+  # included in the symbol list
+  _LT_TAGVAR(include_expsyms, $1)=
+  # exclude_expsyms can be an extended regexp of symbols to exclude
+  # it will be wrapped by ' (' and ')$', so one must not match beginning or
+  # end of line.  Example: 'a|bc|.*d.*' will exclude the symbols 'a' and 'bc',
+  # as well as any symbol that contains 'd'.
+  _LT_TAGVAR(exclude_expsyms, $1)=['_GLOBAL_OFFSET_TABLE_|_GLOBAL__F[ID]_.*']
+  # Although _GLOBAL_OFFSET_TABLE_ is a valid symbol C name, most a.out
+  # platforms (ab)use it in PIC code, but their linkers get confused if
+  # the symbol is explicitly referenced.  Since portable code cannot
+  # rely on this symbol name, it's probably fine to never include it in
+  # preloaded symbol tables.
+  # Exclude shared library initialization/finalization symbols.
+dnl Note also adjust exclude_expsyms for C++ above.
+  extract_expsyms_cmds=
+
+  case $host_os in
+  cygwin* | mingw* | pw32* | cegcc*)
+    # FIXME: the MSVC++ and ICC port hasn't been tested in a loooong time
+    # When not using gcc, we currently assume that we are using
+    # Microsoft Visual C++ or Intel C++ Compiler.
+    if test yes != "$GCC"; then
+      with_gnu_ld=no
+    fi
+    ;;
+  interix*)
+    # we just hope/assume this is gcc and not c89 (= MSVC++ or ICC)
+    with_gnu_ld=yes
+    ;;
+  openbsd* | bitrig*)
+    with_gnu_ld=no
+    ;;
+  linux* | k*bsd*-gnu | gnu*)
+    _LT_TAGVAR(link_all_deplibs, $1)=no
+    ;;
+  esac
+
+  _LT_TAGVAR(ld_shlibs, $1)=yes
+
+  # On some targets, GNU ld is compatible enough with the native linker
+  # that we're better off using the native interface for both.
+  lt_use_gnu_ld_interface=no
+  if test yes = "$with_gnu_ld"; then
+    case $host_os in
+      aix*)
+	# The AIX port of GNU ld has always aspired to compatibility
+	# with the native linker.  However, as the warning in the GNU ld
+	# block says, versions before 2.19.5* couldn't really create working
+	# shared libraries, regardless of the interface used.
+	case `$LD -v 2>&1` in
+	  *\ \(GNU\ Binutils\)\ 2.19.5*) ;;
+	  *\ \(GNU\ Binutils\)\ 2.[[2-9]]*) ;;
+	  *\ \(GNU\ Binutils\)\ [[3-9]]*) ;;
+	  *)
+	    lt_use_gnu_ld_interface=yes
+	    ;;
+	esac
+	;;
+      *)
+	lt_use_gnu_ld_interface=yes
+	;;
+    esac
+  fi
+
+  if test yes = "$lt_use_gnu_ld_interface"; then
+    # If archive_cmds runs LD, not CC, wlarc should be empty
+    wlarc='$wl'
+
+    # Set some defaults for GNU ld with shared library support. These
+    # are reset later if shared libraries are not supported. Putting them
+    # here allows them to be overridden if necessary.
+    runpath_var=LD_RUN_PATH
+    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+    _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic'
+    # ancient GNU ld didn't support --whole-archive et. al.
+    if $LD --help 2>&1 | $GREP 'no-whole-archive' > /dev/null; then
+      _LT_TAGVAR(whole_archive_flag_spec, $1)=$wlarc'--whole-archive$convenience '$wlarc'--no-whole-archive'
+    else
+      _LT_TAGVAR(whole_archive_flag_spec, $1)=
+    fi
+    supports_anon_versioning=no
+    case `$LD -v | $SED -e 's/([[^)]]\+)\s\+//' 2>&1` in
+      *GNU\ gold*) supports_anon_versioning=yes ;;
+      *\ [[01]].* | *\ 2.[[0-9]].* | *\ 2.10.*) ;; # catch versions < 2.11
+      *\ 2.11.93.0.2\ *) supports_anon_versioning=yes ;; # RH7.3 ...
+      *\ 2.11.92.0.12\ *) supports_anon_versioning=yes ;; # Mandrake 8.2 ...
+      *\ 2.11.*) ;; # other 2.11 versions
+      *) supports_anon_versioning=yes ;;
+    esac
+
+    # See if GNU ld supports shared libraries.
+    case $host_os in
+    aix[[3-9]]*)
+      # On AIX/PPC, the GNU linker is very broken
+      if test ia64 != "$host_cpu"; then
+	_LT_TAGVAR(ld_shlibs, $1)=no
+	cat <<_LT_EOF 1>&2
+
+*** Warning: the GNU linker, at least up to release 2.19, is reported
+*** to be unable to reliably create shared libraries on AIX.
+*** Therefore, libtool is disabling shared libraries support.  If you
+*** really care for shared libraries, you may want to install binutils
+*** 2.20 or above, or modify your PATH so that a non-GNU linker is found.
+*** You will then need to restart the configuration process.
+
+_LT_EOF
+      fi
+      ;;
+
+    amigaos*)
+      case $host_cpu in
+      powerpc)
+            # see comment about AmigaOS4 .so support
+            _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+            _LT_TAGVAR(archive_expsym_cmds, $1)=''
+        ;;
+      m68k)
+            _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/a2ixlibrary.data~$ECHO "#define NAME $libname" > $output_objdir/a2ixlibrary.data~$ECHO "#define LIBRARY_ID 1" >> $output_objdir/a2ixlibrary.data~$ECHO "#define VERSION $major" >> $output_objdir/a2ixlibrary.data~$ECHO "#define REVISION $revision" >> $output_objdir/a2ixlibrary.data~$AR $AR_FLAGS $lib $libobjs~$RANLIB $lib~(cd $output_objdir && a2ixlibrary -32)'
+            _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+            _LT_TAGVAR(hardcode_minus_L, $1)=yes
+        ;;
+      esac
+      ;;
+
+    beos*)
+      if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	_LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+	# Joseph Beckenbach <jrb3@best.com> says some releases of gcc
+	# support --undefined.  This deserves some investigation.  FIXME
+	_LT_TAGVAR(archive_cmds, $1)='$CC -nostart $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+      else
+	_LT_TAGVAR(ld_shlibs, $1)=no
+      fi
+      ;;
+
+    cygwin* | mingw* | pw32* | cegcc*)
+      # _LT_TAGVAR(hardcode_libdir_flag_spec, $1) is actually meaningless,
+      # as there is no search path for DLLs.
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+      _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-all-symbols'
+      _LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+      _LT_TAGVAR(always_export_symbols, $1)=no
+      _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes
+      _LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[[BCDGRS]][[ ]]/s/.*[[ ]]\([[^ ]]*\)/\1 DATA/;s/^.*[[ ]]__nm__\([[^ ]]*\)[[ ]][[^ ]]*/\1 DATA/;/^I[[ ]]/d;/^[[AITW]][[ ]]/s/.* //'\'' | sort | uniq > $export_symbols'
+      _LT_TAGVAR(exclude_expsyms, $1)=['[_]+GLOBAL_OFFSET_TABLE_|[_]+GLOBAL__[FID]_.*|[_]+head_[A-Za-z0-9_]+_dll|[A-Za-z0-9_]+_dll_iname']
+
+      if $LD --help 2>&1 | $GREP 'auto-import' > /dev/null; then
+        _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib'
+	# If the export-symbols file already is a .def file, use it as
+	# is; otherwise, prepend EXPORTS...
+	_LT_TAGVAR(archive_expsym_cmds, $1)='if _LT_DLL_DEF_P([$export_symbols]); then
+          cp $export_symbols $output_objdir/$soname.def;
+        else
+          echo EXPORTS > $output_objdir/$soname.def;
+          cat $export_symbols >> $output_objdir/$soname.def;
+        fi~
+        $CC -shared $output_objdir/$soname.def $libobjs $deplibs $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib'
+      else
+	_LT_TAGVAR(ld_shlibs, $1)=no
+      fi
+      ;;
+
+    haiku*)
+      _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+      _LT_TAGVAR(link_all_deplibs, $1)=yes
+      ;;
+
+    os2*)
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+      _LT_TAGVAR(hardcode_minus_L, $1)=yes
+      _LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+      shrext_cmds=.dll
+      _LT_TAGVAR(archive_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	$ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	$ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	$ECHO EXPORTS >> $output_objdir/$libname.def~
+	emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~
+	$CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	emximp -o $lib $output_objdir/$libname.def'
+      _LT_TAGVAR(archive_expsym_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	$ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	$ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	$ECHO EXPORTS >> $output_objdir/$libname.def~
+	prefix_cmds="$SED"~
+	if test EXPORTS = "`$SED 1q $export_symbols`"; then
+	  prefix_cmds="$prefix_cmds -e 1d";
+	fi~
+	prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~
+	cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~
+	$CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	emximp -o $lib $output_objdir/$libname.def'
+      _LT_TAGVAR(old_archive_From_new_cmds, $1)='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def'
+      _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes
+      _LT_TAGVAR(file_list_spec, $1)='@'
+      ;;
+
+    interix[[3-9]]*)
+      _LT_TAGVAR(hardcode_direct, $1)=no
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir'
+      _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+      # Hack: On Interix 3.x, we cannot compile PIC because of a broken gcc.
+      # Instead, shared libraries are loaded at an image base (0x10000000 by
+      # default) and relocated if they conflict, which is a slow very memory
+      # consuming and fragmenting process.  To avoid this, we pick a random,
+      # 256 KiB-aligned image base between 0x50000000 and 0x6FFC0000 at link
+      # time.  Moving up from 0x10000000 also allows more sbrk(2) space.
+      _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib'
+      _LT_TAGVAR(archive_expsym_cmds, $1)='$SED "s|^|_|" $export_symbols >$output_objdir/$soname.expsym~$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--retain-symbols-file,$output_objdir/$soname.expsym $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib'
+      ;;
+
+    gnu* | linux* | tpf* | k*bsd*-gnu | kopensolaris*-gnu)
+      tmp_diet=no
+      if test linux-dietlibc = "$host_os"; then
+	case $cc_basename in
+	  diet\ *) tmp_diet=yes;;	# linux-dietlibc with static linking (!diet-dyn)
+	esac
+      fi
+      if $LD --help 2>&1 | $EGREP ': supported targets:.* elf' > /dev/null \
+	 && test no = "$tmp_diet"
+      then
+	tmp_addflag=' $pic_flag'
+	tmp_sharedflag='-shared'
+	case $cc_basename,$host_cpu in
+        pgcc*)				# Portland Group C compiler
+	  _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	  tmp_addflag=' $pic_flag'
+	  ;;
+	pgf77* | pgf90* | pgf95* | pgfortran*)
+					# Portland Group f77 and f90 compilers
+	  _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	  tmp_addflag=' $pic_flag -Mnomain' ;;
+	ecc*,ia64* | icc*,ia64*)	# Intel C compiler on ia64
+	  tmp_addflag=' -i_dynamic' ;;
+	efc*,ia64* | ifort*,ia64*)	# Intel Fortran compiler on ia64
+	  tmp_addflag=' -i_dynamic -nofor_main' ;;
+	ifc* | ifort*)			# Intel Fortran compiler
+	  tmp_addflag=' -nofor_main' ;;
+	lf95*)				# Lahey Fortran 8.1
+	  _LT_TAGVAR(whole_archive_flag_spec, $1)=
+	  tmp_sharedflag='--shared' ;;
+        nagfor*)                        # NAGFOR 5.3
+          tmp_sharedflag='-Wl,-shared' ;;
+	xl[[cC]]* | bgxl[[cC]]* | mpixl[[cC]]*) # IBM XL C 8.0 on PPC (deal with xlf below)
+	  tmp_sharedflag='-qmkshrobj'
+	  tmp_addflag= ;;
+	nvcc*)	# Cuda Compiler Driver 2.2
+	  _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	  _LT_TAGVAR(compiler_needs_object, $1)=yes
+	  ;;
+	esac
+	case `$CC -V 2>&1 | $SED 5q` in
+	*Sun\ C*)			# Sun C 5.9
+	  _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`new_convenience=; for conv in $convenience\"\"; do test -z \"$conv\" || new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	  _LT_TAGVAR(compiler_needs_object, $1)=yes
+	  tmp_sharedflag='-G' ;;
+	*Sun\ F*)			# Sun Fortran 8.3
+	  tmp_sharedflag='-G' ;;
+	esac
+	_LT_TAGVAR(archive_cmds, $1)='$CC '"$tmp_sharedflag""$tmp_addflag"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+
+        if test yes = "$supports_anon_versioning"; then
+          _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $output_objdir/$libname.ver~
+            cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~
+            echo "local: *; };" >> $output_objdir/$libname.ver~
+            $CC '"$tmp_sharedflag""$tmp_addflag"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-version-script $wl$output_objdir/$libname.ver -o $lib'
+        fi
+
+	case $cc_basename in
+	tcc*)
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+	  _LT_TAGVAR(export_dynamic_flag_spec, $1)='-rdynamic'
+	  ;;
+	xlf* | bgf* | bgxlf* | mpixlf*)
+	  # IBM XL Fortran 10.1 on PPC cannot create shared libs itself
+	  _LT_TAGVAR(whole_archive_flag_spec, $1)='--whole-archive$convenience --no-whole-archive'
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+	  _LT_TAGVAR(archive_cmds, $1)='$LD -shared $libobjs $deplibs $linker_flags -soname $soname -o $lib'
+	  if test yes = "$supports_anon_versioning"; then
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $output_objdir/$libname.ver~
+              cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~
+              echo "local: *; };" >> $output_objdir/$libname.ver~
+              $LD -shared $libobjs $deplibs $linker_flags -soname $soname -version-script $output_objdir/$libname.ver -o $lib'
+	  fi
+	  ;;
+	esac
+      else
+        _LT_TAGVAR(ld_shlibs, $1)=no
+      fi
+      ;;
+
+    netbsd* | netbsdelf*-gnu)
+      if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then
+	_LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable $libobjs $deplibs $linker_flags -o $lib'
+	wlarc=
+      else
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+      fi
+      ;;
+
+    solaris*)
+      if $LD -v 2>&1 | $GREP 'BFD 2\.8' > /dev/null; then
+	_LT_TAGVAR(ld_shlibs, $1)=no
+	cat <<_LT_EOF 1>&2
+
+*** Warning: The releases 2.8.* of the GNU linker cannot reliably
+*** create shared libraries on Solaris systems.  Therefore, libtool
+*** is disabling shared libraries support.  We urge you to upgrade GNU
+*** binutils to release 2.9.1 or newer.  Another option is to modify
+*** your PATH or compiler configuration so that the native linker is
+*** used, and then restart.
+
+_LT_EOF
+      elif $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+      else
+	_LT_TAGVAR(ld_shlibs, $1)=no
+      fi
+      ;;
+
+    sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX*)
+      case `$LD -v 2>&1` in
+        *\ [[01]].* | *\ 2.[[0-9]].* | *\ 2.1[[0-5]].*)
+	_LT_TAGVAR(ld_shlibs, $1)=no
+	cat <<_LT_EOF 1>&2
+
+*** Warning: Releases of the GNU linker prior to 2.16.91.0.3 cannot
+*** reliably create shared libraries on SCO systems.  Therefore, libtool
+*** is disabling shared libraries support.  We urge you to upgrade GNU
+*** binutils to release 2.16.91.0.3 or newer.  Another option is to modify
+*** your PATH or compiler configuration so that the native linker is
+*** used, and then restart.
+
+_LT_EOF
+	;;
+	*)
+	  # For security reasons, it is highly recommended that you always
+	  # use absolute paths for naming shared libraries, and exclude the
+	  # DT_RUNPATH tag from executables and libraries.  But doing so
+	  # requires that you compile everything twice, which is a pain.
+	  if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+	  else
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	  fi
+	;;
+      esac
+      ;;
+
+    sunos4*)
+      _LT_TAGVAR(archive_cmds, $1)='$LD -assert pure-text -Bshareable -o $lib $libobjs $deplibs $linker_flags'
+      wlarc=
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    *)
+      if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+      else
+	_LT_TAGVAR(ld_shlibs, $1)=no
+      fi
+      ;;
+    esac
+
+    if test no = "$_LT_TAGVAR(ld_shlibs, $1)"; then
+      runpath_var=
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)=
+      _LT_TAGVAR(export_dynamic_flag_spec, $1)=
+      _LT_TAGVAR(whole_archive_flag_spec, $1)=
+    fi
+  else
+    # PORTME fill in a description of your system's linker (not GNU ld)
+    case $host_os in
+    aix3*)
+      _LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+      _LT_TAGVAR(always_export_symbols, $1)=yes
+      _LT_TAGVAR(archive_expsym_cmds, $1)='$LD -o $output_objdir/$soname $libobjs $deplibs $linker_flags -bE:$export_symbols -T512 -H512 -bM:SRE~$AR $AR_FLAGS $lib $output_objdir/$soname'
+      # Note: this linker hardcodes the directories in LIBPATH if there
+      # are no directories specified by -L.
+      _LT_TAGVAR(hardcode_minus_L, $1)=yes
+      if test yes = "$GCC" && test -z "$lt_prog_compiler_static"; then
+	# Neither direct hardcoding nor static linking is supported with a
+	# broken collect2.
+	_LT_TAGVAR(hardcode_direct, $1)=unsupported
+      fi
+      ;;
+
+    aix[[4-9]]*)
+      if test ia64 = "$host_cpu"; then
+	# On IA64, the linker does run time linking by default, so we don't
+	# have to do anything special.
+	aix_use_runtimelinking=no
+	exp_sym_flag='-Bexport'
+	no_entry_flag=
+      else
+	# If we're using GNU nm, then we don't want the "-C" option.
+	# -C means demangle to GNU nm, but means don't demangle to AIX nm.
+	# Without the "-l" option, or with the "-B" option, AIX nm treats
+	# weak defined symbols like other global defined symbols, whereas
+	# GNU nm marks them as "W".
+	# While the 'weak' keyword is ignored in the Export File, we need
+	# it in the Import File for the 'aix-soname' feature, so we have
+	# to replace the "-B" option with "-P" for AIX nm.
+	if $NM -V 2>&1 | $GREP 'GNU' > /dev/null; then
+	  _LT_TAGVAR(export_symbols_cmds, $1)='$NM -Bpg $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W")) && ([substr](\$ 3,1,1) != ".")) { if (\$ 2 == "W") { print \$ 3 " weak" } else { print \$ 3 } } }'\'' | sort -u > $export_symbols'
+	else
+	  _LT_TAGVAR(export_symbols_cmds, $1)='`func_echo_all $NM | $SED -e '\''s/B\([[^B]]*\)$/P\1/'\''` -PCpgl $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "L") || (\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) && ([substr](\$ 1,1,1) != ".")) { if ((\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) { print \$ 1 " weak" } else { print \$ 1 } } }'\'' | sort -u > $export_symbols'
+	fi
+	aix_use_runtimelinking=no
+
+	# Test if we are trying to use run time linking or normal
+	# AIX style linking. If -brtl is somewhere in LDFLAGS, we
+	# have runtime linking enabled, and use it for executables.
+	# For shared libraries, we enable/disable runtime linking
+	# depending on the kind of the shared library created -
+	# when "with_aix_soname,aix_use_runtimelinking" is:
+	# "aix,no"   lib.a(lib.so.V) shared, rtl:no,  for executables
+	# "aix,yes"  lib.so          shared, rtl:yes, for executables
+	#            lib.a           static archive
+	# "both,no"  lib.so.V(shr.o) shared, rtl:yes
+	#            lib.a(lib.so.V) shared, rtl:no,  for executables
+	# "both,yes" lib.so.V(shr.o) shared, rtl:yes, for executables
+	#            lib.a(lib.so.V) shared, rtl:no
+	# "svr4,*"   lib.so.V(shr.o) shared, rtl:yes, for executables
+	#            lib.a           static archive
+	case $host_os in aix4.[[23]]|aix4.[[23]].*|aix[[5-9]]*)
+	  for ld_flag in $LDFLAGS; do
+	  if (test x-brtl = "x$ld_flag" || test x-Wl,-brtl = "x$ld_flag"); then
+	    aix_use_runtimelinking=yes
+	    break
+	  fi
+	  done
+	  if test svr4,no = "$with_aix_soname,$aix_use_runtimelinking"; then
+	    # With aix-soname=svr4, we create the lib.so.V shared archives only,
+	    # so we don't have lib.a shared libs to link our executables.
+	    # We have to force runtime linking in this case.
+	    aix_use_runtimelinking=yes
+	    LDFLAGS="$LDFLAGS -Wl,-brtl"
+	  fi
+	  ;;
+	esac
+
+	exp_sym_flag='-bexport'
+	no_entry_flag='-bnoentry'
+      fi
+
+      # When large executables or shared objects are built, AIX ld can
+      # have problems creating the table of contents.  If linking a library
+      # or program results in "error TOC overflow" add -mminimal-toc to
+      # CXXFLAGS/CFLAGS for g++/gcc.  In the cases where that is not
+      # enough to fix the problem, add -Wl,-bbigtoc to LDFLAGS.
+
+      _LT_TAGVAR(archive_cmds, $1)=''
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+      _LT_TAGVAR(hardcode_direct_absolute, $1)=yes
+      _LT_TAGVAR(hardcode_libdir_separator, $1)=':'
+      _LT_TAGVAR(link_all_deplibs, $1)=yes
+      _LT_TAGVAR(file_list_spec, $1)='$wl-f,'
+      case $with_aix_soname,$aix_use_runtimelinking in
+      aix,*) ;; # traditional, no import file
+      svr4,* | *,yes) # use import file
+	# The Import File defines what to hardcode.
+	_LT_TAGVAR(hardcode_direct, $1)=no
+	_LT_TAGVAR(hardcode_direct_absolute, $1)=no
+	;;
+      esac
+
+      if test yes = "$GCC"; then
+	case $host_os in aix4.[[012]]|aix4.[[012]].*)
+	# We only want to do this on AIX 4.2 and lower, the check
+	# below for broken collect2 doesn't work under 4.3+
+	  collect2name=`$CC -print-prog-name=collect2`
+	  if test -f "$collect2name" &&
+	   strings "$collect2name" | $GREP resolve_lib_name >/dev/null
+	  then
+	  # We have reworked collect2
+	  :
+	  else
+	  # We have old collect2
+	  _LT_TAGVAR(hardcode_direct, $1)=unsupported
+	  # It fails to find uninstalled libraries when the uninstalled
+	  # path is not listed in the libpath.  Setting hardcode_minus_L
+	  # to unsupported forces relinking
+	  _LT_TAGVAR(hardcode_minus_L, $1)=yes
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+	  _LT_TAGVAR(hardcode_libdir_separator, $1)=
+	  fi
+	  ;;
+	esac
+	shared_flag='-shared'
+	if test yes = "$aix_use_runtimelinking"; then
+	  shared_flag="$shared_flag "'$wl-G'
+	fi
+	# Need to ensure runtime linking is disabled for the traditional
+	# shared library, or the linker may eventually find shared libraries
+	# /with/ Import File - we do not want to mix them.
+	shared_flag_aix='-shared'
+	shared_flag_svr4='-shared $wl-G'
+      else
+	# not using gcc
+	if test ia64 = "$host_cpu"; then
+	# VisualAge C++, Version 5.5 for AIX 5L for IA-64, Beta 3 Release
+	# chokes on -Wl,-G. The following line is correct:
+	  shared_flag='-G'
+	else
+	  if test yes = "$aix_use_runtimelinking"; then
+	    shared_flag='$wl-G'
+	  else
+	    shared_flag='$wl-bM:SRE'
+	  fi
+	  shared_flag_aix='$wl-bM:SRE'
+	  shared_flag_svr4='$wl-G'
+	fi
+      fi
+
+      _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-bexpall'
+      # It seems that -bexpall does not export symbols beginning with
+      # underscore (_), so it is better to generate a list of symbols to export.
+      _LT_TAGVAR(always_export_symbols, $1)=yes
+      if test aix,yes = "$with_aix_soname,$aix_use_runtimelinking"; then
+	# Warning - without using the other runtime loading flags (-brtl),
+	# -berok will link without error, but may produce a broken library.
+	_LT_TAGVAR(allow_undefined_flag, $1)='-berok'
+        # Determine the default libpath from the value encoded in an
+        # empty executable.
+        _LT_SYS_MODULE_PATH_AIX([$1])
+        _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-blibpath:$libdir:'"$aix_libpath"
+        _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -o $output_objdir/$soname $libobjs $deplibs $wl'$no_entry_flag' $compiler_flags `if test -n "$allow_undefined_flag"; then func_echo_all "$wl$allow_undefined_flag"; else :; fi` $wl'$exp_sym_flag:\$export_symbols' '$shared_flag
+      else
+	if test ia64 = "$host_cpu"; then
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R $libdir:/usr/lib:/lib'
+	  _LT_TAGVAR(allow_undefined_flag, $1)="-z nodefs"
+	  _LT_TAGVAR(archive_expsym_cmds, $1)="\$CC $shared_flag"' -o $output_objdir/$soname $libobjs $deplibs '"\$wl$no_entry_flag"' $compiler_flags $wl$allow_undefined_flag '"\$wl$exp_sym_flag:\$export_symbols"
+	else
+	 # Determine the default libpath from the value encoded in an
+	 # empty executable.
+	 _LT_SYS_MODULE_PATH_AIX([$1])
+	 _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-blibpath:$libdir:'"$aix_libpath"
+	  # Warning - without using the other run time loading flags,
+	  # -berok will link without error, but may produce a broken library.
+	  _LT_TAGVAR(no_undefined_flag, $1)=' $wl-bernotok'
+	  _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-berok'
+	  if test yes = "$with_gnu_ld"; then
+	    # We only use this code for GNU lds that support --whole-archive.
+	    _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive$convenience $wl--no-whole-archive'
+	  else
+	    # Exported symbols can be pulled into shared objects from archives
+	    _LT_TAGVAR(whole_archive_flag_spec, $1)='$convenience'
+	  fi
+	  _LT_TAGVAR(archive_cmds_need_lc, $1)=yes
+	  _LT_TAGVAR(archive_expsym_cmds, $1)='$RM -r $output_objdir/$realname.d~$MKDIR $output_objdir/$realname.d'
+	  # -brtl affects multiple linker settings, -berok does not and is overridden later
+	  compiler_flags_filtered='`func_echo_all "$compiler_flags " | $SED -e "s%-brtl\\([[, ]]\\)%-berok\\1%g"`'
+	  if test svr4 != "$with_aix_soname"; then
+	    # This is similar to how AIX traditionally builds its shared libraries.
+	    _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$CC '$shared_flag_aix' -o $output_objdir/$realname.d/$soname $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$AR $AR_FLAGS $output_objdir/$libname$release.a $output_objdir/$realname.d/$soname'
+	  fi
+	  if test aix != "$with_aix_soname"; then
+	    _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$CC '$shared_flag_svr4' -o $output_objdir/$realname.d/$shared_archive_member_spec.o $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$STRIP -e $output_objdir/$realname.d/$shared_archive_member_spec.o~( func_echo_all "#! $soname($shared_archive_member_spec.o)"; if test shr_64 = "$shared_archive_member_spec"; then func_echo_all "# 64"; else func_echo_all "# 32"; fi; cat $export_symbols ) > $output_objdir/$realname.d/$shared_archive_member_spec.imp~$AR $AR_FLAGS $output_objdir/$soname $output_objdir/$realname.d/$shared_archive_member_spec.o $output_objdir/$realname.d/$shared_archive_member_spec.imp'
+	  else
+	    # used by -dlpreopen to get the symbols
+	    _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$MV  $output_objdir/$realname.d/$soname $output_objdir'
+	  fi
+	  _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$RM -r $output_objdir/$realname.d'
+	fi
+      fi
+      ;;
+
+    amigaos*)
+      case $host_cpu in
+      powerpc)
+            # see comment about AmigaOS4 .so support
+            _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+            _LT_TAGVAR(archive_expsym_cmds, $1)=''
+        ;;
+      m68k)
+            _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/a2ixlibrary.data~$ECHO "#define NAME $libname" > $output_objdir/a2ixlibrary.data~$ECHO "#define LIBRARY_ID 1" >> $output_objdir/a2ixlibrary.data~$ECHO "#define VERSION $major" >> $output_objdir/a2ixlibrary.data~$ECHO "#define REVISION $revision" >> $output_objdir/a2ixlibrary.data~$AR $AR_FLAGS $lib $libobjs~$RANLIB $lib~(cd $output_objdir && a2ixlibrary -32)'
+            _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+            _LT_TAGVAR(hardcode_minus_L, $1)=yes
+        ;;
+      esac
+      ;;
+
+    bsdi[[45]]*)
+      _LT_TAGVAR(export_dynamic_flag_spec, $1)=-rdynamic
+      ;;
+
+    cygwin* | mingw* | pw32* | cegcc*)
+      # When not using gcc, we currently assume that we are using
+      # Microsoft Visual C++ or Intel C++ Compiler.
+      # hardcode_libdir_flag_spec is actually meaningless, as there is
+      # no search path for DLLs.
+      case $cc_basename in
+      cl* | icl*)
+	# Native MSVC or ICC
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)=' '
+	_LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+	_LT_TAGVAR(always_export_symbols, $1)=yes
+	_LT_TAGVAR(file_list_spec, $1)='@'
+	# Tell ltmain to make .lib files, not .a files.
+	libext=lib
+	# Tell ltmain to make .dll files, not .so files.
+	shrext_cmds=.dll
+	# FIXME: Setting linknames here is a bad hack.
+	_LT_TAGVAR(archive_cmds, $1)='$CC -o $output_objdir/$soname $libobjs $compiler_flags $deplibs -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~linknames='
+	_LT_TAGVAR(archive_expsym_cmds, $1)='if _LT_DLL_DEF_P([$export_symbols]); then
+            cp "$export_symbols" "$output_objdir/$soname.def";
+            echo "$tool_output_objdir$soname.def" > "$output_objdir/$soname.exp";
+          else
+            $SED -e '\''s/^/-link -EXPORT:/'\'' < $export_symbols > $output_objdir/$soname.exp;
+          fi~
+          $CC -o $tool_output_objdir$soname $libobjs $compiler_flags $deplibs "@$tool_output_objdir$soname.exp" -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~
+          linknames='
+	# The linker will not automatically build a static lib if we build a DLL.
+	# _LT_TAGVAR(old_archive_from_new_cmds, $1)='true'
+	_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes
+	_LT_TAGVAR(exclude_expsyms, $1)='_NULL_IMPORT_DESCRIPTOR|_IMPORT_DESCRIPTOR_.*'
+	_LT_TAGVAR(export_symbols_cmds, $1)='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[[BCDGRS]][[ ]]/s/.*[[ ]]\([[^ ]]*\)/\1,DATA/'\'' | $SED -e '\''/^[[AITW]][[ ]]/s/.*[[ ]]//'\'' | sort | uniq > $export_symbols'
+	# Don't use ranlib
+	_LT_TAGVAR(old_postinstall_cmds, $1)='chmod 644 $oldlib'
+	_LT_TAGVAR(postlink_cmds, $1)='lt_outputfile="@OUTPUT@"~
+          lt_tool_outputfile="@TOOL_OUTPUT@"~
+          case $lt_outputfile in
+            *.exe|*.EXE) ;;
+            *)
+              lt_outputfile=$lt_outputfile.exe
+              lt_tool_outputfile=$lt_tool_outputfile.exe
+              ;;
+          esac~
+          if test : != "$MANIFEST_TOOL" && test -f "$lt_outputfile.manifest"; then
+            $MANIFEST_TOOL -manifest "$lt_tool_outputfile.manifest" -outputresource:"$lt_tool_outputfile" || exit 1;
+            $RM "$lt_outputfile.manifest";
+          fi'
+	;;
+      *)
+	# Assume MSVC and ICC wrapper
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)=' '
+	_LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+	# Tell ltmain to make .lib files, not .a files.
+	libext=lib
+	# Tell ltmain to make .dll files, not .so files.
+	shrext_cmds=.dll
+	# FIXME: Setting linknames here is a bad hack.
+	_LT_TAGVAR(archive_cmds, $1)='$CC -o $lib $libobjs $compiler_flags `func_echo_all "$deplibs" | $SED '\''s/ -lc$//'\''` -link -dll~linknames='
+	# The linker will automatically build a .lib file if we build a DLL.
+	_LT_TAGVAR(old_archive_from_new_cmds, $1)='true'
+	# FIXME: Should let the user specify the lib program.
+	_LT_TAGVAR(old_archive_cmds, $1)='lib -OUT:$oldlib$oldobjs$old_deplibs'
+	_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes
+	;;
+      esac
+      ;;
+
+    darwin* | rhapsody*)
+      _LT_DARWIN_LINKER_FEATURES($1)
+      ;;
+
+    dgux*)
+      _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    # FreeBSD 2.2.[012] allows us to include c++rt0.o to get C++ constructor
+    # support.  Future versions do this automatically, but an explicit c++rt0.o
+    # does not break anything, and helps significantly (at the cost of a little
+    # extra space).
+    freebsd2.2*)
+      _LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags /usr/lib/c++rt0.o'
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir'
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    # Unfortunately, older versions of FreeBSD 2 do not have this feature.
+    freebsd2.*)
+      _LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags'
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+      _LT_TAGVAR(hardcode_minus_L, $1)=yes
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    # FreeBSD 3 and greater uses gcc -shared to do shared libraries.
+    freebsd* | dragonfly* | midnightbsd*)
+      _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags'
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir'
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    hpux9*)
+      if test yes = "$GCC"; then
+	_LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/$soname~$CC -shared $pic_flag $wl+b $wl$install_libdir -o $output_objdir/$soname $libobjs $deplibs $compiler_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib'
+      else
+	_LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/$soname~$LD -b +b $install_libdir -o $output_objdir/$soname $libobjs $deplibs $linker_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib'
+      fi
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir'
+      _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+
+      # hardcode_minus_L: Not really in the search PATH,
+      # but as the default location of the library.
+      _LT_TAGVAR(hardcode_minus_L, $1)=yes
+      _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+      ;;
+
+    hpux10*)
+      if test yes,no = "$GCC,$with_gnu_ld"; then
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags'
+      else
+	_LT_TAGVAR(archive_cmds, $1)='$LD -b +h $soname +b $install_libdir -o $lib $libobjs $deplibs $linker_flags'
+      fi
+      if test no = "$with_gnu_ld"; then
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir'
+	_LT_TAGVAR(hardcode_libdir_separator, $1)=:
+	_LT_TAGVAR(hardcode_direct, $1)=yes
+	_LT_TAGVAR(hardcode_direct_absolute, $1)=yes
+	_LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+	# hardcode_minus_L: Not really in the search PATH,
+	# but as the default location of the library.
+	_LT_TAGVAR(hardcode_minus_L, $1)=yes
+      fi
+      ;;
+
+    hpux11*)
+      if test yes,no = "$GCC,$with_gnu_ld"; then
+	case $host_cpu in
+	hppa*64*)
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl+h $wl$soname -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	ia64*)
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $wl+h $wl$soname $wl+nodefaultrpath -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	*)
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	esac
+      else
+	case $host_cpu in
+	hppa*64*)
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	ia64*)
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+nodefaultrpath -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	*)
+	m4_if($1, [], [
+	  # Older versions of the 11.00 compiler do not understand -b yet
+	  # (HP92453-01 A.11.01.20 doesn't, HP92453-01 B.11.X.35175-35176.GP does)
+	  _LT_LINKER_OPTION([if $CC understands -b],
+	    _LT_TAGVAR(lt_cv_prog_compiler__b, $1), [-b],
+	    [_LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags'],
+	    [_LT_TAGVAR(archive_cmds, $1)='$LD -b +h $soname +b $install_libdir -o $lib $libobjs $deplibs $linker_flags'])],
+	  [_LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags'])
+	  ;;
+	esac
+      fi
+      if test no = "$with_gnu_ld"; then
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir'
+	_LT_TAGVAR(hardcode_libdir_separator, $1)=:
+
+	case $host_cpu in
+	hppa*64*|ia64*)
+	  _LT_TAGVAR(hardcode_direct, $1)=no
+	  _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+	  ;;
+	*)
+	  _LT_TAGVAR(hardcode_direct, $1)=yes
+	  _LT_TAGVAR(hardcode_direct_absolute, $1)=yes
+	  _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+
+	  # hardcode_minus_L: Not really in the search PATH,
+	  # but as the default location of the library.
+	  _LT_TAGVAR(hardcode_minus_L, $1)=yes
+	  ;;
+	esac
+      fi
+      ;;
+
+    irix5* | irix6* | nonstopux*)
+      if test yes = "$GCC"; then
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+	# Try to use the -exported_symbol ld option, if it does not
+	# work, assume that -exports_file does not work either and
+	# implicitly export all symbols.
+	# This should be the same for all languages, so no per-tag cache variable.
+	AC_CACHE_CHECK([whether the $host_os linker accepts -exported_symbol],
+	  [lt_cv_irix_exported_symbol],
+	  [save_LDFLAGS=$LDFLAGS
+	   LDFLAGS="$LDFLAGS -shared $wl-exported_symbol ${wl}foo $wl-update_registry $wl/dev/null"
+	   AC_LINK_IFELSE(
+	     [AC_LANG_SOURCE(
+	        [AC_LANG_CASE([C], [[int foo (void) { return 0; }]],
+			      [C++], [[int foo (void) { return 0; }]],
+			      [Fortran 77], [[
+      subroutine foo
+      end]],
+			      [Fortran], [[
+      subroutine foo
+      end]])])],
+	      [lt_cv_irix_exported_symbol=yes],
+	      [lt_cv_irix_exported_symbol=no])
+           LDFLAGS=$save_LDFLAGS])
+	if test yes = "$lt_cv_irix_exported_symbol"; then
+          _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations $wl-exports_file $wl$export_symbols -o $lib'
+	fi
+	_LT_TAGVAR(link_all_deplibs, $1)=no
+      else
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -exports_file $export_symbols -o $lib'
+      fi
+      _LT_TAGVAR(archive_cmds_need_lc, $1)='no'
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+      _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+      _LT_TAGVAR(inherit_rpath, $1)=yes
+      _LT_TAGVAR(link_all_deplibs, $1)=yes
+      ;;
+
+    linux*)
+      case $cc_basename in
+      tcc*)
+	# Fabrice Bellard et al's Tiny C Compiler
+	_LT_TAGVAR(ld_shlibs, $1)=yes
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags'
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+	;;
+      esac
+      ;;
+
+    netbsd* | netbsdelf*-gnu)
+      if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then
+	_LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags'  # a.out
+      else
+	_LT_TAGVAR(archive_cmds, $1)='$LD -shared -o $lib $libobjs $deplibs $linker_flags'      # ELF
+      fi
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir'
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    newsos6)
+      _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+      _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    *nto* | *qnx*)
+      ;;
+
+    openbsd* | bitrig*)
+      if test -f /usr/libexec/ld.so; then
+	_LT_TAGVAR(hardcode_direct, $1)=yes
+	_LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+	_LT_TAGVAR(hardcode_direct_absolute, $1)=yes
+	if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags'
+	  _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags $wl-retain-symbols-file,$export_symbols'
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir'
+	  _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+	else
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags'
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir'
+	fi
+      else
+	_LT_TAGVAR(ld_shlibs, $1)=no
+      fi
+      ;;
+
+    os2*)
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+      _LT_TAGVAR(hardcode_minus_L, $1)=yes
+      _LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+      shrext_cmds=.dll
+      _LT_TAGVAR(archive_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	$ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	$ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	$ECHO EXPORTS >> $output_objdir/$libname.def~
+	emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~
+	$CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	emximp -o $lib $output_objdir/$libname.def'
+      _LT_TAGVAR(archive_expsym_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	$ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	$ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	$ECHO EXPORTS >> $output_objdir/$libname.def~
+	prefix_cmds="$SED"~
+	if test EXPORTS = "`$SED 1q $export_symbols`"; then
+	  prefix_cmds="$prefix_cmds -e 1d";
+	fi~
+	prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~
+	cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~
+	$CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	emximp -o $lib $output_objdir/$libname.def'
+      _LT_TAGVAR(old_archive_From_new_cmds, $1)='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def'
+      _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes
+      _LT_TAGVAR(file_list_spec, $1)='@'
+      ;;
+
+    osf3*)
+      if test yes = "$GCC"; then
+	_LT_TAGVAR(allow_undefined_flag, $1)=' $wl-expect_unresolved $wl\*'
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+      else
+	_LT_TAGVAR(allow_undefined_flag, $1)=' -expect_unresolved \*'
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+      fi
+      _LT_TAGVAR(archive_cmds_need_lc, $1)='no'
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+      _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+      ;;
+
+    osf4* | osf5*)	# as osf3* with the addition of -msym flag
+      if test yes = "$GCC"; then
+	_LT_TAGVAR(allow_undefined_flag, $1)=' $wl-expect_unresolved $wl\*'
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $pic_flag $libobjs $deplibs $compiler_flags $wl-msym $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+      else
+	_LT_TAGVAR(allow_undefined_flag, $1)=' -expect_unresolved \*'
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags -msym -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='for i in `cat $export_symbols`; do printf "%s %s\\n" -exported_symbol "\$i" >> $lib.exp; done; printf "%s\\n" "-hidden">> $lib.exp~
+          $CC -shared$allow_undefined_flag $wl-input $wl$lib.exp $compiler_flags $libobjs $deplibs -soname $soname `test -n "$verstring" && $ECHO "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib~$RM $lib.exp'
+
+	# Both c and cxx compiler support -rpath directly
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-rpath $libdir'
+      fi
+      _LT_TAGVAR(archive_cmds_need_lc, $1)='no'
+      _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+      ;;
+
+    solaris*)
+      _LT_TAGVAR(no_undefined_flag, $1)=' -z defs'
+      if test yes = "$GCC"; then
+	wlarc='$wl'
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $wl-z ${wl}text $wl-h $wl$soname -o $lib $libobjs $deplibs $compiler_flags'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+          $CC -shared $pic_flag $wl-z ${wl}text $wl-M $wl$lib.exp $wl-h $wl$soname -o $lib $libobjs $deplibs $compiler_flags~$RM $lib.exp'
+      else
+	case `$CC -V 2>&1` in
+	*"Compilers 5.0"*)
+	  wlarc=''
+	  _LT_TAGVAR(archive_cmds, $1)='$LD -G$allow_undefined_flag -h $soname -o $lib $libobjs $deplibs $linker_flags'
+	  _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+            $LD -G$allow_undefined_flag -M $lib.exp -h $soname -o $lib $libobjs $deplibs $linker_flags~$RM $lib.exp'
+	  ;;
+	*)
+	  wlarc='$wl'
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -G$allow_undefined_flag -h $soname -o $lib $libobjs $deplibs $compiler_flags'
+	  _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+            $CC -G$allow_undefined_flag -M $lib.exp -h $soname -o $lib $libobjs $deplibs $compiler_flags~$RM $lib.exp'
+	  ;;
+	esac
+      fi
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir'
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      case $host_os in
+      solaris2.[[0-5]] | solaris2.[[0-5]].*) ;;
+      *)
+	# The compiler driver will combine and reorder linker options,
+	# but understands '-z linker_flag'.  GCC discards it without '$wl',
+	# but is careful enough not to reorder.
+	# Supported since Solaris 2.6 (maybe 2.5.1?)
+	if test yes = "$GCC"; then
+	  _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl-z ${wl}allextract$convenience $wl-z ${wl}defaultextract'
+	else
+	  _LT_TAGVAR(whole_archive_flag_spec, $1)='-z allextract$convenience -z defaultextract'
+	fi
+	;;
+      esac
+      _LT_TAGVAR(link_all_deplibs, $1)=yes
+      ;;
+
+    sunos4*)
+      if test sequent = "$host_vendor"; then
+	# Use $CC to link under sequent, because it throws in some extra .o
+	# files that make .init and .fini sections work.
+	_LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h $soname -o $lib $libobjs $deplibs $compiler_flags'
+      else
+	_LT_TAGVAR(archive_cmds, $1)='$LD -assert pure-text -Bstatic -o $lib $libobjs $deplibs $linker_flags'
+      fi
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+      _LT_TAGVAR(hardcode_direct, $1)=yes
+      _LT_TAGVAR(hardcode_minus_L, $1)=yes
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    sysv4)
+      case $host_vendor in
+	sni)
+	  _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+	  _LT_TAGVAR(hardcode_direct, $1)=yes # is this really true???
+	;;
+	siemens)
+	  ## LD is ld it makes a PLAMLIB
+	  ## CC just makes a GrossModule.
+	  _LT_TAGVAR(archive_cmds, $1)='$LD -G -o $lib $libobjs $deplibs $linker_flags'
+	  _LT_TAGVAR(reload_cmds, $1)='$CC -r -o $output$reload_objs'
+	  _LT_TAGVAR(hardcode_direct, $1)=no
+        ;;
+	motorola)
+	  _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+	  _LT_TAGVAR(hardcode_direct, $1)=no #Motorola manual says yes, but my tests say they lie
+	;;
+      esac
+      runpath_var='LD_RUN_PATH'
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    sysv4.3*)
+      _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      _LT_TAGVAR(export_dynamic_flag_spec, $1)='-Bexport'
+      ;;
+
+    sysv4*MP*)
+      if test -d /usr/nec; then
+	_LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+	_LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+	runpath_var=LD_RUN_PATH
+	hardcode_runpath_var=yes
+	_LT_TAGVAR(ld_shlibs, $1)=yes
+      fi
+      ;;
+
+    sysv4*uw2* | sysv5OpenUNIX* | sysv5UnixWare7.[[01]].[[10]]* | unixware7* | sco3.2v5.0.[[024]]*)
+      _LT_TAGVAR(no_undefined_flag, $1)='$wl-z,text'
+      _LT_TAGVAR(archive_cmds_need_lc, $1)=no
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      runpath_var='LD_RUN_PATH'
+
+      if test yes = "$GCC"; then
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+      else
+	_LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+      fi
+      ;;
+
+    sysv5* | sco3.2v5* | sco5v6*)
+      # Note: We CANNOT use -z defs as we might desire, because we do not
+      # link with -lc, and that would cause any symbols used from libc to
+      # always be unresolved, which means just about no library would
+      # ever link correctly.  If we're not using GNU ld we use -z text
+      # though, which does catch some bad symbols but isn't as heavy-handed
+      # as -z defs.
+      _LT_TAGVAR(no_undefined_flag, $1)='$wl-z,text'
+      _LT_TAGVAR(allow_undefined_flag, $1)='$wl-z,nodefs'
+      _LT_TAGVAR(archive_cmds_need_lc, $1)=no
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R,$libdir'
+      _LT_TAGVAR(hardcode_libdir_separator, $1)=':'
+      _LT_TAGVAR(link_all_deplibs, $1)=yes
+      _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-Bexport'
+      runpath_var='LD_RUN_PATH'
+
+      if test yes = "$GCC"; then
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+      else
+	_LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+      fi
+      ;;
+
+    uts4*)
+      _LT_TAGVAR(archive_cmds, $1)='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      ;;
+
+    *)
+      _LT_TAGVAR(ld_shlibs, $1)=no
+      ;;
+    esac
+
+    if test sni = "$host_vendor"; then
+      case $host in
+      sysv4 | sysv4.2uw2* | sysv4.3* | sysv5*)
+	_LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-Blargedynsym'
+	;;
+      esac
+    fi
+  fi
+])
+AC_MSG_RESULT([$_LT_TAGVAR(ld_shlibs, $1)])
+test no = "$_LT_TAGVAR(ld_shlibs, $1)" && can_build_shared=no
+
+_LT_TAGVAR(with_gnu_ld, $1)=$with_gnu_ld
+
+_LT_DECL([], [libext], [0], [Old archive suffix (normally "a")])dnl
+_LT_DECL([], [shrext_cmds], [1], [Shared library suffix (normally ".so")])dnl
+_LT_DECL([], [extract_expsyms_cmds], [2],
+    [The commands to extract the exported symbol list from a shared archive])
+
+#
+# Do we need to explicitly link libc?
+#
+case "x$_LT_TAGVAR(archive_cmds_need_lc, $1)" in
+x|xyes)
+  # Assume -lc should be added
+  _LT_TAGVAR(archive_cmds_need_lc, $1)=yes
+
+  if test yes,yes = "$GCC,$enable_shared"; then
+    case $_LT_TAGVAR(archive_cmds, $1) in
+    *'~'*)
+      # FIXME: we may have to deal with multi-command sequences.
+      ;;
+    '$CC '*)
+      # Test whether the compiler implicitly links with -lc since on some
+      # systems, -lgcc has to come before -lc. If gcc already passes -lc
+      # to ld, don't add -lc before -lgcc.
+      AC_CACHE_CHECK([whether -lc should be explicitly linked in],
+	[lt_cv_]_LT_TAGVAR(archive_cmds_need_lc, $1),
+	[$RM conftest*
+	echo "$lt_simple_compile_test_code" > conftest.$ac_ext
+
+	if AC_TRY_EVAL(ac_compile) 2>conftest.err; then
+	  soname=conftest
+	  lib=conftest
+	  libobjs=conftest.$ac_objext
+	  deplibs=
+	  wl=$_LT_TAGVAR(lt_prog_compiler_wl, $1)
+	  pic_flag=$_LT_TAGVAR(lt_prog_compiler_pic, $1)
+	  compiler_flags=-v
+	  linker_flags=-v
+	  verstring=
+	  output_objdir=.
+	  libname=conftest
+	  lt_save_allow_undefined_flag=$_LT_TAGVAR(allow_undefined_flag, $1)
+	  _LT_TAGVAR(allow_undefined_flag, $1)=
+	  if AC_TRY_EVAL(_LT_TAGVAR(archive_cmds, $1) 2\>\&1 \| $GREP \" -lc \" \>/dev/null 2\>\&1)
+	  then
+	    lt_cv_[]_LT_TAGVAR(archive_cmds_need_lc, $1)=no
+	  else
+	    lt_cv_[]_LT_TAGVAR(archive_cmds_need_lc, $1)=yes
+	  fi
+	  _LT_TAGVAR(allow_undefined_flag, $1)=$lt_save_allow_undefined_flag
+	else
+	  cat conftest.err 1>&5
+	fi
+	$RM conftest*
+	])
+      _LT_TAGVAR(archive_cmds_need_lc, $1)=$lt_cv_[]_LT_TAGVAR(archive_cmds_need_lc, $1)
+      ;;
+    esac
+  fi
+  ;;
+esac
+
+_LT_TAGDECL([build_libtool_need_lc], [archive_cmds_need_lc], [0],
+    [Whether or not to add -lc for building shared libraries])
+_LT_TAGDECL([allow_libtool_libs_with_static_runtimes],
+    [enable_shared_with_static_runtimes], [0],
+    [Whether or not to disallow shared libs when runtime libs are static])
+_LT_TAGDECL([], [export_dynamic_flag_spec], [1],
+    [Compiler flag to allow reflexive dlopens])
+_LT_TAGDECL([], [whole_archive_flag_spec], [1],
+    [Compiler flag to generate shared objects directly from archives])
+_LT_TAGDECL([], [compiler_needs_object], [1],
+    [Whether the compiler copes with passing no objects directly])
+_LT_TAGDECL([], [old_archive_from_new_cmds], [2],
+    [Create an old-style archive from a shared archive])
+_LT_TAGDECL([], [old_archive_from_expsyms_cmds], [2],
+    [Create a temporary old-style archive to link instead of a shared archive])
+_LT_TAGDECL([], [archive_cmds], [2], [Commands used to build a shared archive])
+_LT_TAGDECL([], [archive_expsym_cmds], [2])
+_LT_TAGDECL([], [module_cmds], [2],
+    [Commands used to build a loadable module if different from building
+    a shared archive.])
+_LT_TAGDECL([], [module_expsym_cmds], [2])
+_LT_TAGDECL([], [with_gnu_ld], [1],
+    [Whether we are building with GNU ld or not])
+_LT_TAGDECL([], [allow_undefined_flag], [1],
+    [Flag that allows shared libraries with undefined symbols to be built])
+_LT_TAGDECL([], [no_undefined_flag], [1],
+    [Flag that enforces no undefined symbols])
+_LT_TAGDECL([], [hardcode_libdir_flag_spec], [1],
+    [Flag to hardcode $libdir into a binary during linking.
+    This must work even if $libdir does not exist])
+_LT_TAGDECL([], [hardcode_libdir_separator], [1],
+    [Whether we need a single "-rpath" flag with a separated argument])
+_LT_TAGDECL([], [hardcode_direct], [0],
+    [Set to "yes" if using DIR/libNAME$shared_ext during linking hardcodes
+    DIR into the resulting binary])
+_LT_TAGDECL([], [hardcode_direct_absolute], [0],
+    [Set to "yes" if using DIR/libNAME$shared_ext during linking hardcodes
+    DIR into the resulting binary and the resulting library dependency is
+    "absolute", i.e impossible to change by setting $shlibpath_var if the
+    library is relocated])
+_LT_TAGDECL([], [hardcode_minus_L], [0],
+    [Set to "yes" if using the -LDIR flag during linking hardcodes DIR
+    into the resulting binary])
+_LT_TAGDECL([], [hardcode_shlibpath_var], [0],
+    [Set to "yes" if using SHLIBPATH_VAR=DIR during linking hardcodes DIR
+    into the resulting binary])
+_LT_TAGDECL([], [hardcode_automatic], [0],
+    [Set to "yes" if building a shared library automatically hardcodes DIR
+    into the library and all subsequent libraries and executables linked
+    against it])
+_LT_TAGDECL([], [inherit_rpath], [0],
+    [Set to yes if linker adds runtime paths of dependent libraries
+    to runtime path list])
+_LT_TAGDECL([], [link_all_deplibs], [0],
+    [Whether libtool must link a program against all its dependency libraries])
+_LT_TAGDECL([], [always_export_symbols], [0],
+    [Set to "yes" if exported symbols are required])
+_LT_TAGDECL([], [export_symbols_cmds], [2],
+    [The commands to list exported symbols])
+_LT_TAGDECL([], [exclude_expsyms], [1],
+    [Symbols that should not be listed in the preloaded symbols])
+_LT_TAGDECL([], [include_expsyms], [1],
+    [Symbols that must always be exported])
+_LT_TAGDECL([], [prelink_cmds], [2],
+    [Commands necessary for linking programs (against libraries) with templates])
+_LT_TAGDECL([], [postlink_cmds], [2],
+    [Commands necessary for finishing linking programs])
+_LT_TAGDECL([], [file_list_spec], [1],
+    [Specify filename containing input files])
+dnl FIXME: Not yet implemented
+dnl _LT_TAGDECL([], [thread_safe_flag_spec], [1],
+dnl    [Compiler flag to generate thread safe objects])
+])# _LT_LINKER_SHLIBS
+
+
+# _LT_LANG_C_CONFIG([TAG])
+# ------------------------
+# Ensure that the configuration variables for a C compiler are suitably
+# defined.  These variables are subsequently used by _LT_CONFIG to write
+# the compiler configuration to 'libtool'.
+m4_defun([_LT_LANG_C_CONFIG],
+[m4_require([_LT_DECL_EGREP])dnl
+lt_save_CC=$CC
+AC_LANG_PUSH(C)
+
+# Source file extension for C test sources.
+ac_ext=c
+
+# Object file extension for compiled C test sources.
+objext=o
+_LT_TAGVAR(objext, $1)=$objext
+
+# Code to be used in simple compile tests
+lt_simple_compile_test_code="int some_variable = 0;"
+
+# Code to be used in simple link tests
+lt_simple_link_test_code='int main(){return(0);}'
+
+_LT_TAG_COMPILER
+# Save the default compiler, since it gets overwritten when the other
+# tags are being tested, and _LT_TAGVAR(compiler, []) is a NOP.
+compiler_DEFAULT=$CC
+
+# save warnings/boilerplate of simple test code
+_LT_COMPILER_BOILERPLATE
+_LT_LINKER_BOILERPLATE
+
+if test -n "$compiler"; then
+  _LT_COMPILER_NO_RTTI($1)
+  _LT_COMPILER_PIC($1)
+  _LT_COMPILER_C_O($1)
+  _LT_COMPILER_FILE_LOCKS($1)
+  _LT_LINKER_SHLIBS($1)
+  _LT_SYS_DYNAMIC_LINKER($1)
+  _LT_LINKER_HARDCODE_LIBPATH($1)
+  LT_SYS_DLOPEN_SELF
+  _LT_CMD_STRIPLIB
+
+  # Report what library types will actually be built
+  AC_MSG_CHECKING([if libtool supports shared libraries])
+  AC_MSG_RESULT([$can_build_shared])
+
+  AC_MSG_CHECKING([whether to build shared libraries])
+  test no = "$can_build_shared" && enable_shared=no
+
+  # On AIX, shared libraries and static libraries use the same namespace, and
+  # are all built from PIC.
+  case $host_os in
+  aix3*)
+    test yes = "$enable_shared" && enable_static=no
+    if test -n "$RANLIB"; then
+      archive_cmds="$archive_cmds~\$RANLIB \$lib"
+      postinstall_cmds='$RANLIB $lib'
+    fi
+    ;;
+
+  aix[[4-9]]*)
+    if test ia64 != "$host_cpu"; then
+      case $enable_shared,$with_aix_soname,$aix_use_runtimelinking in
+      yes,aix,yes) ;;			# shared object as lib.so file only
+      yes,svr4,*) ;;			# shared object as lib.so archive member only
+      yes,*) enable_static=no ;;	# shared object in lib.a archive as well
+      esac
+    fi
+    ;;
+  esac
+  AC_MSG_RESULT([$enable_shared])
+
+  AC_MSG_CHECKING([whether to build static libraries])
+  # Make sure either enable_shared or enable_static is yes.
+  test yes = "$enable_shared" || enable_static=yes
+  AC_MSG_RESULT([$enable_static])
+
+  _LT_CONFIG($1)
+fi
+AC_LANG_POP
+CC=$lt_save_CC
+])# _LT_LANG_C_CONFIG
+
+
+# _LT_LANG_CXX_CONFIG([TAG])
+# --------------------------
+# Ensure that the configuration variables for a C++ compiler are suitably
+# defined.  These variables are subsequently used by _LT_CONFIG to write
+# the compiler configuration to 'libtool'.
+m4_defun([_LT_LANG_CXX_CONFIG],
+[m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+m4_require([_LT_DECL_EGREP])dnl
+m4_require([_LT_PATH_MANIFEST_TOOL])dnl
+if test -n "$CXX" && ( test no != "$CXX" &&
+    ( (test g++ = "$CXX" && `g++ -v >/dev/null 2>&1` ) ||
+    (test g++ != "$CXX"))); then
+  AC_PROG_CXXCPP
+else
+  _lt_caught_CXX_error=yes
+fi
+
+AC_LANG_PUSH(C++)
+_LT_TAGVAR(archive_cmds_need_lc, $1)=no
+_LT_TAGVAR(allow_undefined_flag, $1)=
+_LT_TAGVAR(always_export_symbols, $1)=no
+_LT_TAGVAR(archive_expsym_cmds, $1)=
+_LT_TAGVAR(compiler_needs_object, $1)=no
+_LT_TAGVAR(export_dynamic_flag_spec, $1)=
+_LT_TAGVAR(hardcode_direct, $1)=no
+_LT_TAGVAR(hardcode_direct_absolute, $1)=no
+_LT_TAGVAR(hardcode_libdir_flag_spec, $1)=
+_LT_TAGVAR(hardcode_libdir_separator, $1)=
+_LT_TAGVAR(hardcode_minus_L, $1)=no
+_LT_TAGVAR(hardcode_shlibpath_var, $1)=unsupported
+_LT_TAGVAR(hardcode_automatic, $1)=no
+_LT_TAGVAR(inherit_rpath, $1)=no
+_LT_TAGVAR(module_cmds, $1)=
+_LT_TAGVAR(module_expsym_cmds, $1)=
+_LT_TAGVAR(link_all_deplibs, $1)=unknown
+_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds
+_LT_TAGVAR(reload_flag, $1)=$reload_flag
+_LT_TAGVAR(reload_cmds, $1)=$reload_cmds
+_LT_TAGVAR(no_undefined_flag, $1)=
+_LT_TAGVAR(whole_archive_flag_spec, $1)=
+_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=no
+
+# Source file extension for C++ test sources.
+ac_ext=cpp
+
+# Object file extension for compiled C++ test sources.
+objext=o
+_LT_TAGVAR(objext, $1)=$objext
+
+# No sense in running all these tests if we already determined that
+# the CXX compiler isn't working.  Some variables (like enable_shared)
+# are currently assumed to apply to all compilers on this platform,
+# and will be corrupted by setting them based on a non-working compiler.
+if test yes != "$_lt_caught_CXX_error"; then
+  # Code to be used in simple compile tests
+  lt_simple_compile_test_code="int some_variable = 0;"
+
+  # Code to be used in simple link tests
+  lt_simple_link_test_code='int main(int, char *[[]]) { return(0); }'
+
+  # ltmain only uses $CC for tagged configurations so make sure $CC is set.
+  _LT_TAG_COMPILER
+
+  # save warnings/boilerplate of simple test code
+  _LT_COMPILER_BOILERPLATE
+  _LT_LINKER_BOILERPLATE
+
+  # Allow CC to be a program name with arguments.
+  lt_save_CC=$CC
+  lt_save_CFLAGS=$CFLAGS
+  lt_save_LD=$LD
+  lt_save_GCC=$GCC
+  GCC=$GXX
+  lt_save_with_gnu_ld=$with_gnu_ld
+  lt_save_path_LD=$lt_cv_path_LD
+  if test -n "${lt_cv_prog_gnu_ldcxx+set}"; then
+    lt_cv_prog_gnu_ld=$lt_cv_prog_gnu_ldcxx
+  else
+    $as_unset lt_cv_prog_gnu_ld
+  fi
+  if test -n "${lt_cv_path_LDCXX+set}"; then
+    lt_cv_path_LD=$lt_cv_path_LDCXX
+  else
+    $as_unset lt_cv_path_LD
+  fi
+  test -z "${LDCXX+set}" || LD=$LDCXX
+  CC=${CXX-"c++"}
+  CFLAGS=$CXXFLAGS
+  compiler=$CC
+  _LT_TAGVAR(compiler, $1)=$CC
+  _LT_CC_BASENAME([$compiler])
+
+  if test -n "$compiler"; then
+    # We don't want -fno-exception when compiling C++ code, so set the
+    # no_builtin_flag separately
+    if test yes = "$GXX"; then
+      _LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)=' -fno-builtin'
+    else
+      _LT_TAGVAR(lt_prog_compiler_no_builtin_flag, $1)=
+    fi
+
+    if test yes = "$GXX"; then
+      # Set up default GNU C++ configuration
+
+      LT_PATH_LD
+
+      # Check if GNU C++ uses GNU ld as the underlying linker, since the
+      # archiving commands below assume that GNU ld is being used.
+      if test yes = "$with_gnu_ld"; then
+        _LT_TAGVAR(archive_cmds, $1)='$CC $pic_flag -shared -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib'
+        _LT_TAGVAR(archive_expsym_cmds, $1)='$CC $pic_flag -shared -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+
+        _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+        _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic'
+
+        # If archive_cmds runs LD, not CC, wlarc should be empty
+        # XXX I think wlarc can be eliminated in ltcf-cxx, but I need to
+        #     investigate it a little bit more. (MM)
+        wlarc='$wl'
+
+        # ancient GNU ld didn't support --whole-archive et. al.
+        if eval "`$CC -print-prog-name=ld` --help 2>&1" |
+	  $GREP 'no-whole-archive' > /dev/null; then
+          _LT_TAGVAR(whole_archive_flag_spec, $1)=$wlarc'--whole-archive$convenience '$wlarc'--no-whole-archive'
+        else
+          _LT_TAGVAR(whole_archive_flag_spec, $1)=
+        fi
+      else
+        with_gnu_ld=no
+        wlarc=
+
+        # A generic and very simple default shared library creation
+        # command for GNU C++ for the case where it uses the native
+        # linker, instead of GNU ld.  If possible, this setting should
+        # overridden to take advantage of the native linker features on
+        # the platform it is being used on.
+        _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -o $lib'
+      fi
+
+      # Commands to make compiler produce verbose output that lists
+      # what "hidden" libraries, object files and flags are used when
+      # linking a shared library.
+      output_verbose_link_cmd='$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP -v "^Configured with:" | $GREP " \-L"'
+
+    else
+      GXX=no
+      with_gnu_ld=no
+      wlarc=
+    fi
+
+    # PORTME: fill in a description of your system's C++ link characteristics
+    AC_MSG_CHECKING([whether the $compiler linker ($LD) supports shared libraries])
+    _LT_TAGVAR(ld_shlibs, $1)=yes
+    case $host_os in
+      aix3*)
+        # FIXME: insert proper C++ library support
+        _LT_TAGVAR(ld_shlibs, $1)=no
+        ;;
+      aix[[4-9]]*)
+        if test ia64 = "$host_cpu"; then
+          # On IA64, the linker does run time linking by default, so we don't
+          # have to do anything special.
+          aix_use_runtimelinking=no
+          exp_sym_flag='-Bexport'
+          no_entry_flag=
+        else
+          aix_use_runtimelinking=no
+
+          # Test if we are trying to use run time linking or normal
+          # AIX style linking. If -brtl is somewhere in LDFLAGS, we
+          # have runtime linking enabled, and use it for executables.
+          # For shared libraries, we enable/disable runtime linking
+          # depending on the kind of the shared library created -
+          # when "with_aix_soname,aix_use_runtimelinking" is:
+          # "aix,no"   lib.a(lib.so.V) shared, rtl:no,  for executables
+          # "aix,yes"  lib.so          shared, rtl:yes, for executables
+          #            lib.a           static archive
+          # "both,no"  lib.so.V(shr.o) shared, rtl:yes
+          #            lib.a(lib.so.V) shared, rtl:no,  for executables
+          # "both,yes" lib.so.V(shr.o) shared, rtl:yes, for executables
+          #            lib.a(lib.so.V) shared, rtl:no
+          # "svr4,*"   lib.so.V(shr.o) shared, rtl:yes, for executables
+          #            lib.a           static archive
+          case $host_os in aix4.[[23]]|aix4.[[23]].*|aix[[5-9]]*)
+	    for ld_flag in $LDFLAGS; do
+	      case $ld_flag in
+	      *-brtl*)
+	        aix_use_runtimelinking=yes
+	        break
+	        ;;
+	      esac
+	    done
+	    if test svr4,no = "$with_aix_soname,$aix_use_runtimelinking"; then
+	      # With aix-soname=svr4, we create the lib.so.V shared archives only,
+	      # so we don't have lib.a shared libs to link our executables.
+	      # We have to force runtime linking in this case.
+	      aix_use_runtimelinking=yes
+	      LDFLAGS="$LDFLAGS -Wl,-brtl"
+	    fi
+	    ;;
+          esac
+
+          exp_sym_flag='-bexport'
+          no_entry_flag='-bnoentry'
+        fi
+
+        # When large executables or shared objects are built, AIX ld can
+        # have problems creating the table of contents.  If linking a library
+        # or program results in "error TOC overflow" add -mminimal-toc to
+        # CXXFLAGS/CFLAGS for g++/gcc.  In the cases where that is not
+        # enough to fix the problem, add -Wl,-bbigtoc to LDFLAGS.
+
+        _LT_TAGVAR(archive_cmds, $1)=''
+        _LT_TAGVAR(hardcode_direct, $1)=yes
+        _LT_TAGVAR(hardcode_direct_absolute, $1)=yes
+        _LT_TAGVAR(hardcode_libdir_separator, $1)=':'
+        _LT_TAGVAR(link_all_deplibs, $1)=yes
+        _LT_TAGVAR(file_list_spec, $1)='$wl-f,'
+        case $with_aix_soname,$aix_use_runtimelinking in
+        aix,*) ;;	# no import file
+        svr4,* | *,yes) # use import file
+          # The Import File defines what to hardcode.
+          _LT_TAGVAR(hardcode_direct, $1)=no
+          _LT_TAGVAR(hardcode_direct_absolute, $1)=no
+          ;;
+        esac
+
+        if test yes = "$GXX"; then
+          case $host_os in aix4.[[012]]|aix4.[[012]].*)
+          # We only want to do this on AIX 4.2 and lower, the check
+          # below for broken collect2 doesn't work under 4.3+
+	  collect2name=`$CC -print-prog-name=collect2`
+	  if test -f "$collect2name" &&
+	     strings "$collect2name" | $GREP resolve_lib_name >/dev/null
+	  then
+	    # We have reworked collect2
+	    :
+	  else
+	    # We have old collect2
+	    _LT_TAGVAR(hardcode_direct, $1)=unsupported
+	    # It fails to find uninstalled libraries when the uninstalled
+	    # path is not listed in the libpath.  Setting hardcode_minus_L
+	    # to unsupported forces relinking
+	    _LT_TAGVAR(hardcode_minus_L, $1)=yes
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+	    _LT_TAGVAR(hardcode_libdir_separator, $1)=
+	  fi
+          esac
+          shared_flag='-shared'
+	  if test yes = "$aix_use_runtimelinking"; then
+	    shared_flag=$shared_flag' $wl-G'
+	  fi
+	  # Need to ensure runtime linking is disabled for the traditional
+	  # shared library, or the linker may eventually find shared libraries
+	  # /with/ Import File - we do not want to mix them.
+	  shared_flag_aix='-shared'
+	  shared_flag_svr4='-shared $wl-G'
+        else
+          # not using gcc
+          if test ia64 = "$host_cpu"; then
+	  # VisualAge C++, Version 5.5 for AIX 5L for IA-64, Beta 3 Release
+	  # chokes on -Wl,-G. The following line is correct:
+	  shared_flag='-G'
+          else
+	    if test yes = "$aix_use_runtimelinking"; then
+	      shared_flag='$wl-G'
+	    else
+	      shared_flag='$wl-bM:SRE'
+	    fi
+	    shared_flag_aix='$wl-bM:SRE'
+	    shared_flag_svr4='$wl-G'
+          fi
+        fi
+
+        _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-bexpall'
+        # It seems that -bexpall does not export symbols beginning with
+        # underscore (_), so it is better to generate a list of symbols to
+	# export.
+        _LT_TAGVAR(always_export_symbols, $1)=yes
+	if test aix,yes = "$with_aix_soname,$aix_use_runtimelinking"; then
+          # Warning - without using the other runtime loading flags (-brtl),
+          # -berok will link without error, but may produce a broken library.
+          # The "-G" linker flag allows undefined symbols.
+          _LT_TAGVAR(no_undefined_flag, $1)='-bernotok'
+          # Determine the default libpath from the value encoded in an empty
+          # executable.
+          _LT_SYS_MODULE_PATH_AIX([$1])
+          _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-blibpath:$libdir:'"$aix_libpath"
+
+          _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -o $output_objdir/$soname $libobjs $deplibs $wl'$no_entry_flag' $compiler_flags `if test -n "$allow_undefined_flag"; then func_echo_all "$wl$allow_undefined_flag"; else :; fi` $wl'$exp_sym_flag:\$export_symbols' '$shared_flag
+        else
+          if test ia64 = "$host_cpu"; then
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R $libdir:/usr/lib:/lib'
+	    _LT_TAGVAR(allow_undefined_flag, $1)="-z nodefs"
+	    _LT_TAGVAR(archive_expsym_cmds, $1)="\$CC $shared_flag"' -o $output_objdir/$soname $libobjs $deplibs '"\$wl$no_entry_flag"' $compiler_flags $wl$allow_undefined_flag '"\$wl$exp_sym_flag:\$export_symbols"
+          else
+	    # Determine the default libpath from the value encoded in an
+	    # empty executable.
+	    _LT_SYS_MODULE_PATH_AIX([$1])
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-blibpath:$libdir:'"$aix_libpath"
+	    # Warning - without using the other run time loading flags,
+	    # -berok will link without error, but may produce a broken library.
+	    _LT_TAGVAR(no_undefined_flag, $1)=' $wl-bernotok'
+	    _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-berok'
+	    if test yes = "$with_gnu_ld"; then
+	      # We only use this code for GNU lds that support --whole-archive.
+	      _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive$convenience $wl--no-whole-archive'
+	    else
+	      # Exported symbols can be pulled into shared objects from archives
+	      _LT_TAGVAR(whole_archive_flag_spec, $1)='$convenience'
+	    fi
+	    _LT_TAGVAR(archive_cmds_need_lc, $1)=yes
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='$RM -r $output_objdir/$realname.d~$MKDIR $output_objdir/$realname.d'
+	    # -brtl affects multiple linker settings, -berok does not and is overridden later
+	    compiler_flags_filtered='`func_echo_all "$compiler_flags " | $SED -e "s%-brtl\\([[, ]]\\)%-berok\\1%g"`'
+	    if test svr4 != "$with_aix_soname"; then
+	      # This is similar to how AIX traditionally builds its shared
+	      # libraries. Need -bnortl late, we may have -brtl in LDFLAGS.
+	      _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$CC '$shared_flag_aix' -o $output_objdir/$realname.d/$soname $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$AR $AR_FLAGS $output_objdir/$libname$release.a $output_objdir/$realname.d/$soname'
+	    fi
+	    if test aix != "$with_aix_soname"; then
+	      _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$CC '$shared_flag_svr4' -o $output_objdir/$realname.d/$shared_archive_member_spec.o $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$STRIP -e $output_objdir/$realname.d/$shared_archive_member_spec.o~( func_echo_all "#! $soname($shared_archive_member_spec.o)"; if test shr_64 = "$shared_archive_member_spec"; then func_echo_all "# 64"; else func_echo_all "# 32"; fi; cat $export_symbols ) > $output_objdir/$realname.d/$shared_archive_member_spec.imp~$AR $AR_FLAGS $output_objdir/$soname $output_objdir/$realname.d/$shared_archive_member_spec.o $output_objdir/$realname.d/$shared_archive_member_spec.imp'
+	    else
+	      # used by -dlpreopen to get the symbols
+	      _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$MV  $output_objdir/$realname.d/$soname $output_objdir'
+	    fi
+	    _LT_TAGVAR(archive_expsym_cmds, $1)="$_LT_TAGVAR(archive_expsym_cmds, $1)"'~$RM -r $output_objdir/$realname.d'
+          fi
+        fi
+        ;;
+
+      beos*)
+	if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	  _LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+	  # Joseph Beckenbach <jrb3@best.com> says some releases of gcc
+	  # support --undefined.  This deserves some investigation.  FIXME
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -nostart $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	else
+	  _LT_TAGVAR(ld_shlibs, $1)=no
+	fi
+	;;
+
+      chorus*)
+        case $cc_basename in
+          *)
+	  # FIXME: insert proper C++ library support
+	  _LT_TAGVAR(ld_shlibs, $1)=no
+	  ;;
+        esac
+        ;;
+
+      cygwin* | mingw* | pw32* | cegcc*)
+	case $GXX,$cc_basename in
+	,cl* | no,cl* | ,icl* | no,icl*)
+	  # Native MSVC or ICC
+	  # hardcode_libdir_flag_spec is actually meaningless, as there is
+	  # no search path for DLLs.
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)=' '
+	  _LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+	  _LT_TAGVAR(always_export_symbols, $1)=yes
+	  _LT_TAGVAR(file_list_spec, $1)='@'
+	  # Tell ltmain to make .lib files, not .a files.
+	  libext=lib
+	  # Tell ltmain to make .dll files, not .so files.
+	  shrext_cmds=.dll
+	  # FIXME: Setting linknames here is a bad hack.
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -o $output_objdir/$soname $libobjs $compiler_flags $deplibs -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~linknames='
+	  _LT_TAGVAR(archive_expsym_cmds, $1)='if _LT_DLL_DEF_P([$export_symbols]); then
+              cp "$export_symbols" "$output_objdir/$soname.def";
+              echo "$tool_output_objdir$soname.def" > "$output_objdir/$soname.exp";
+            else
+              $SED -e '\''s/^/-link -EXPORT:/'\'' < $export_symbols > $output_objdir/$soname.exp;
+            fi~
+            $CC -o $tool_output_objdir$soname $libobjs $compiler_flags $deplibs "@$tool_output_objdir$soname.exp" -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~
+            linknames='
+	  # The linker will not automatically build a static lib if we build a DLL.
+	  # _LT_TAGVAR(old_archive_from_new_cmds, $1)='true'
+	  _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes
+	  # Don't use ranlib
+	  _LT_TAGVAR(old_postinstall_cmds, $1)='chmod 644 $oldlib'
+	  _LT_TAGVAR(postlink_cmds, $1)='lt_outputfile="@OUTPUT@"~
+            lt_tool_outputfile="@TOOL_OUTPUT@"~
+            case $lt_outputfile in
+              *.exe|*.EXE) ;;
+              *)
+                lt_outputfile=$lt_outputfile.exe
+                lt_tool_outputfile=$lt_tool_outputfile.exe
+                ;;
+            esac~
+            func_to_tool_file "$lt_outputfile"~
+            if test : != "$MANIFEST_TOOL" && test -f "$lt_outputfile.manifest"; then
+              $MANIFEST_TOOL -manifest "$lt_tool_outputfile.manifest" -outputresource:"$lt_tool_outputfile" || exit 1;
+              $RM "$lt_outputfile.manifest";
+            fi'
+	  ;;
+	*)
+	  # g++
+	  # _LT_TAGVAR(hardcode_libdir_flag_spec, $1) is actually meaningless,
+	  # as there is no search path for DLLs.
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+	  _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-all-symbols'
+	  _LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+	  _LT_TAGVAR(always_export_symbols, $1)=no
+	  _LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes
+
+	  if $LD --help 2>&1 | $GREP 'auto-import' > /dev/null; then
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib'
+	    # If the export-symbols file already is a .def file, use it as
+	    # is; otherwise, prepend EXPORTS...
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='if _LT_DLL_DEF_P([$export_symbols]); then
+              cp $export_symbols $output_objdir/$soname.def;
+            else
+              echo EXPORTS > $output_objdir/$soname.def;
+              cat $export_symbols >> $output_objdir/$soname.def;
+            fi~
+            $CC -shared -nostdlib $output_objdir/$soname.def $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib'
+	  else
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	  fi
+	  ;;
+	esac
+	;;
+      darwin* | rhapsody*)
+        _LT_DARWIN_LINKER_FEATURES($1)
+	;;
+
+      os2*)
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-L$libdir'
+	_LT_TAGVAR(hardcode_minus_L, $1)=yes
+	_LT_TAGVAR(allow_undefined_flag, $1)=unsupported
+	shrext_cmds=.dll
+	_LT_TAGVAR(archive_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	  $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	  $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	  $ECHO EXPORTS >> $output_objdir/$libname.def~
+	  emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~
+	  $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	  emximp -o $lib $output_objdir/$libname.def'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	  $ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	  $ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	  $ECHO EXPORTS >> $output_objdir/$libname.def~
+	  prefix_cmds="$SED"~
+	  if test EXPORTS = "`$SED 1q $export_symbols`"; then
+	    prefix_cmds="$prefix_cmds -e 1d";
+	  fi~
+	  prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~
+	  cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~
+	  $CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	  emximp -o $lib $output_objdir/$libname.def'
+	_LT_TAGVAR(old_archive_From_new_cmds, $1)='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def'
+	_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=yes
+	_LT_TAGVAR(file_list_spec, $1)='@'
+	;;
+
+      dgux*)
+        case $cc_basename in
+          ec++*)
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+          ghcx*)
+	    # Green Hills C++ Compiler
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+          *)
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+        esac
+        ;;
+
+      freebsd2.*)
+        # C++ shared libraries reported to be fairly broken before
+	# switch to ELF
+        _LT_TAGVAR(ld_shlibs, $1)=no
+        ;;
+
+      freebsd-elf*)
+        _LT_TAGVAR(archive_cmds_need_lc, $1)=no
+        ;;
+
+      freebsd* | dragonfly* | midnightbsd*)
+        # FreeBSD 3 and later use GNU C++ and GNU ld with standard ELF
+        # conventions
+        _LT_TAGVAR(ld_shlibs, $1)=yes
+        ;;
+
+      haiku*)
+        _LT_TAGVAR(archive_cmds, $1)='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+        _LT_TAGVAR(link_all_deplibs, $1)=yes
+        ;;
+
+      hpux9*)
+        _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir'
+        _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+        _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+        _LT_TAGVAR(hardcode_direct, $1)=yes
+        _LT_TAGVAR(hardcode_minus_L, $1)=yes # Not in the search PATH,
+				             # but as the default
+				             # location of the library.
+
+        case $cc_basename in
+          CC*)
+            # FIXME: insert proper C++ library support
+            _LT_TAGVAR(ld_shlibs, $1)=no
+            ;;
+          aCC*)
+            _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/$soname~$CC -b $wl+b $wl$install_libdir -o $output_objdir/$soname $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib'
+            # Commands to make compiler produce verbose output that lists
+            # what "hidden" libraries, object files and flags are used when
+            # linking a shared library.
+            #
+            # There doesn't appear to be a way to prevent this compiler from
+            # explicitly linking system object files so we need to strip them
+            # from the output so that they don't get included in the library
+            # dependencies.
+            output_verbose_link_cmd='templist=`($CC -b $CFLAGS -v conftest.$objext 2>&1) | $EGREP " \-L"`; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "$list"'
+            ;;
+          *)
+            if test yes = "$GXX"; then
+              _LT_TAGVAR(archive_cmds, $1)='$RM $output_objdir/$soname~$CC -shared -nostdlib $pic_flag $wl+b $wl$install_libdir -o $output_objdir/$soname $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib'
+            else
+              # FIXME: insert proper C++ library support
+              _LT_TAGVAR(ld_shlibs, $1)=no
+            fi
+            ;;
+        esac
+        ;;
+
+      hpux10*|hpux11*)
+        if test no = "$with_gnu_ld"; then
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl+b $wl$libdir'
+	  _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+
+          case $host_cpu in
+            hppa*64*|ia64*)
+              ;;
+            *)
+	      _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+              ;;
+          esac
+        fi
+        case $host_cpu in
+          hppa*64*|ia64*)
+            _LT_TAGVAR(hardcode_direct, $1)=no
+            _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+            ;;
+          *)
+            _LT_TAGVAR(hardcode_direct, $1)=yes
+            _LT_TAGVAR(hardcode_direct_absolute, $1)=yes
+            _LT_TAGVAR(hardcode_minus_L, $1)=yes # Not in the search PATH,
+					         # but as the default
+					         # location of the library.
+            ;;
+        esac
+
+        case $cc_basename in
+          CC*)
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+          aCC*)
+	    case $host_cpu in
+	      hppa*64*)
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags'
+	        ;;
+	      ia64*)
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+nodefaultrpath -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags'
+	        ;;
+	      *)
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -b $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags'
+	        ;;
+	    esac
+	    # Commands to make compiler produce verbose output that lists
+	    # what "hidden" libraries, object files and flags are used when
+	    # linking a shared library.
+	    #
+	    # There doesn't appear to be a way to prevent this compiler from
+	    # explicitly linking system object files so we need to strip them
+	    # from the output so that they don't get included in the library
+	    # dependencies.
+	    output_verbose_link_cmd='templist=`($CC -b $CFLAGS -v conftest.$objext 2>&1) | $GREP " \-L"`; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "$list"'
+	    ;;
+          *)
+	    if test yes = "$GXX"; then
+	      if test no = "$with_gnu_ld"; then
+	        case $host_cpu in
+	          hppa*64*)
+	            _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib -fPIC $wl+h $wl$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags'
+	            ;;
+	          ia64*)
+	            _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $pic_flag $wl+h $wl$soname $wl+nodefaultrpath -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags'
+	            ;;
+	          *)
+	            _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags'
+	            ;;
+	        esac
+	      fi
+	    else
+	      # FIXME: insert proper C++ library support
+	      _LT_TAGVAR(ld_shlibs, $1)=no
+	    fi
+	    ;;
+        esac
+        ;;
+
+      interix[[3-9]]*)
+	_LT_TAGVAR(hardcode_direct, $1)=no
+	_LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir'
+	_LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+	# Hack: On Interix 3.x, we cannot compile PIC because of a broken gcc.
+	# Instead, shared libraries are loaded at an image base (0x10000000 by
+	# default) and relocated if they conflict, which is a slow very memory
+	# consuming and fragmenting process.  To avoid this, we pick a random,
+	# 256 KiB-aligned image base between 0x50000000 and 0x6FFC0000 at link
+	# time.  Moving up from 0x10000000 also allows more sbrk(2) space.
+	_LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib'
+	_LT_TAGVAR(archive_expsym_cmds, $1)='$SED "s|^|_|" $export_symbols >$output_objdir/$soname.expsym~$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--retain-symbols-file,$output_objdir/$soname.expsym $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib'
+	;;
+      irix5* | irix6*)
+        case $cc_basename in
+          CC*)
+	    # SGI C++
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -shared -all -multigot $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+
+	    # Archives containing C++ object files must be created using
+	    # "CC -ar", where "CC" is the IRIX C++ compiler.  This is
+	    # necessary to make sure instantiated templates are included
+	    # in the archive.
+	    _LT_TAGVAR(old_archive_cmds, $1)='$CC -ar -WR,-u -o $oldlib $oldobjs'
+	    ;;
+          *)
+	    if test yes = "$GXX"; then
+	      if test no = "$with_gnu_ld"; then
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+	      else
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` -o $lib'
+	      fi
+	    fi
+	    _LT_TAGVAR(link_all_deplibs, $1)=yes
+	    ;;
+        esac
+        _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+        _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+        _LT_TAGVAR(inherit_rpath, $1)=yes
+        ;;
+
+      linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*)
+        case $cc_basename in
+          KCC*)
+	    # Kuck and Associates, Inc. (KAI) C++ Compiler
+
+	    # KCC will only create a shared library if the output file
+	    # ends with ".so" (or ".sl" for HP-UX), so rename the library
+	    # to its proper name (with version) after linking.
+	    _LT_TAGVAR(archive_cmds, $1)='tempext=`echo $shared_ext | $SED -e '\''s/\([[^()0-9A-Za-z{}]]\)/\\\\\1/g'\''`; templib=`echo $lib | $SED -e "s/\$tempext\..*/.so/"`; $CC $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags --soname $soname -o \$templib; mv \$templib $lib'
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='tempext=`echo $shared_ext | $SED -e '\''s/\([[^()0-9A-Za-z{}]]\)/\\\\\1/g'\''`; templib=`echo $lib | $SED -e "s/\$tempext\..*/.so/"`; $CC $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags --soname $soname -o \$templib $wl-retain-symbols-file,$export_symbols; mv \$templib $lib'
+	    # Commands to make compiler produce verbose output that lists
+	    # what "hidden" libraries, object files and flags are used when
+	    # linking a shared library.
+	    #
+	    # There doesn't appear to be a way to prevent this compiler from
+	    # explicitly linking system object files so we need to strip them
+	    # from the output so that they don't get included in the library
+	    # dependencies.
+	    output_verbose_link_cmd='templist=`$CC $CFLAGS -v conftest.$objext -o libconftest$shared_ext 2>&1 | $GREP "ld"`; rm -f libconftest$shared_ext; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "$list"'
+
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir'
+	    _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic'
+
+	    # Archives containing C++ object files must be created using
+	    # "CC -Bstatic", where "CC" is the KAI C++ compiler.
+	    _LT_TAGVAR(old_archive_cmds, $1)='$CC -Bstatic -o $oldlib $oldobjs'
+	    ;;
+	  icpc* | ecpc* )
+	    # Intel C++
+	    with_gnu_ld=yes
+	    # version 8.0 and above of icpc choke on multiply defined symbols
+	    # if we add $predep_objects and $postdep_objects, however 7.1 and
+	    # earlier do not add the objects themselves.
+	    case `$CC -V 2>&1` in
+	      *"Version 7."*)
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib'
+		_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+		;;
+	      *)  # Version 8.0 or newer
+	        tmp_idyn=
+	        case $host_cpu in
+		  ia64*) tmp_idyn=' -i_dynamic';;
+		esac
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -shared'"$tmp_idyn"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+		_LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared'"$tmp_idyn"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+		;;
+	    esac
+	    _LT_TAGVAR(archive_cmds_need_lc, $1)=no
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir'
+	    _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic'
+	    _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive$convenience $wl--no-whole-archive'
+	    ;;
+          pgCC* | pgcpp*)
+            # Portland Group C++ compiler
+	    case `$CC -V` in
+	    *pgCC\ [[1-5]].* | *pgcpp\ [[1-5]].*)
+	      _LT_TAGVAR(prelink_cmds, $1)='tpldir=Template.dir~
+               rm -rf $tpldir~
+               $CC --prelink_objects --instantiation_dir $tpldir $objs $libobjs $compile_deplibs~
+               compile_command="$compile_command `find $tpldir -name \*.o | sort | $NL2SP`"'
+	      _LT_TAGVAR(old_archive_cmds, $1)='tpldir=Template.dir~
+                rm -rf $tpldir~
+                $CC --prelink_objects --instantiation_dir $tpldir $oldobjs$old_deplibs~
+                $AR $AR_FLAGS $oldlib$oldobjs$old_deplibs `find $tpldir -name \*.o | sort | $NL2SP`~
+                $RANLIB $oldlib'
+	      _LT_TAGVAR(archive_cmds, $1)='tpldir=Template.dir~
+                rm -rf $tpldir~
+                $CC --prelink_objects --instantiation_dir $tpldir $predep_objects $libobjs $deplibs $convenience $postdep_objects~
+                $CC -shared $pic_flag $predep_objects $libobjs $deplibs `find $tpldir -name \*.o | sort | $NL2SP` $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib'
+	      _LT_TAGVAR(archive_expsym_cmds, $1)='tpldir=Template.dir~
+                rm -rf $tpldir~
+                $CC --prelink_objects --instantiation_dir $tpldir $predep_objects $libobjs $deplibs $convenience $postdep_objects~
+                $CC -shared $pic_flag $predep_objects $libobjs $deplibs `find $tpldir -name \*.o | sort | $NL2SP` $postdep_objects $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+	      ;;
+	    *) # Version 6 and above use weak symbols
+	      _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib'
+	      _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+	      ;;
+	    esac
+
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl--rpath $wl$libdir'
+	    _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic'
+	    _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+            ;;
+	  cxx*)
+	    # Compaq C++
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname -o $lib'
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname  -o $lib $wl-retain-symbols-file $wl$export_symbols'
+
+	    runpath_var=LD_RUN_PATH
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-rpath $libdir'
+	    _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+
+	    # Commands to make compiler produce verbose output that lists
+	    # what "hidden" libraries, object files and flags are used when
+	    # linking a shared library.
+	    #
+	    # There doesn't appear to be a way to prevent this compiler from
+	    # explicitly linking system object files so we need to strip them
+	    # from the output so that they don't get included in the library
+	    # dependencies.
+	    output_verbose_link_cmd='templist=`$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP "ld"`; templist=`func_echo_all "$templist" | $SED "s/\(^.*ld.*\)\( .*ld .*$\)/\1/"`; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "X$list" | $Xsed'
+	    ;;
+	  xl* | mpixl* | bgxl*)
+	    # IBM XL 8.0 on PPC, with GNU ld
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+	    _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl--export-dynamic'
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -qmkshrobj $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	    if test yes = "$supports_anon_versioning"; then
+	      _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $output_objdir/$libname.ver~
+                cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~
+                echo "local: *; };" >> $output_objdir/$libname.ver~
+                $CC -qmkshrobj $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-version-script $wl$output_objdir/$libname.ver -o $lib'
+	    fi
+	    ;;
+	  *)
+	    case `$CC -V 2>&1 | $SED 5q` in
+	    *Sun\ C*)
+	      # Sun C++ 5.9
+	      _LT_TAGVAR(no_undefined_flag, $1)=' -zdefs'
+	      _LT_TAGVAR(archive_cmds, $1)='$CC -G$allow_undefined_flag -h$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags'
+	      _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G$allow_undefined_flag -h$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-retain-symbols-file $wl$export_symbols'
+	      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir'
+	      _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl--whole-archive`new_convenience=; for conv in $convenience\"\"; do test -z \"$conv\" || new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	      _LT_TAGVAR(compiler_needs_object, $1)=yes
+
+	      # Not sure whether something based on
+	      # $CC $CFLAGS -v conftest.$objext -o libconftest$shared_ext 2>&1
+	      # would be better.
+	      output_verbose_link_cmd='func_echo_all'
+
+	      # Archives containing C++ object files must be created using
+	      # "CC -xar", where "CC" is the Sun C++ compiler.  This is
+	      # necessary to make sure instantiated templates are included
+	      # in the archive.
+	      _LT_TAGVAR(old_archive_cmds, $1)='$CC -xar -o $oldlib $oldobjs'
+	      ;;
+	    esac
+	    ;;
+	esac
+	;;
+
+      lynxos*)
+        # FIXME: insert proper C++ library support
+	_LT_TAGVAR(ld_shlibs, $1)=no
+	;;
+
+      m88k*)
+        # FIXME: insert proper C++ library support
+        _LT_TAGVAR(ld_shlibs, $1)=no
+	;;
+
+      mvs*)
+        case $cc_basename in
+          cxx*)
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+	  *)
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+	esac
+	;;
+
+      netbsd*)
+        if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then
+	  _LT_TAGVAR(archive_cmds, $1)='$LD -Bshareable  -o $lib $predep_objects $libobjs $deplibs $postdep_objects $linker_flags'
+	  wlarc=
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir'
+	  _LT_TAGVAR(hardcode_direct, $1)=yes
+	  _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+	fi
+	# Workaround some broken pre-1.5 toolchains
+	output_verbose_link_cmd='$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP conftest.$objext | $SED -e "s:-lgcc -lc -lgcc::"'
+	;;
+
+      *nto* | *qnx*)
+        _LT_TAGVAR(ld_shlibs, $1)=yes
+	;;
+
+      openbsd* | bitrig*)
+	if test -f /usr/libexec/ld.so; then
+	  _LT_TAGVAR(hardcode_direct, $1)=yes
+	  _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+	  _LT_TAGVAR(hardcode_direct_absolute, $1)=yes
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -o $lib'
+	  _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir'
+	  if test -z "`echo __ELF__ | $CC -E - | grep __ELF__`"; then
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $pic_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-retain-symbols-file,$export_symbols -o $lib'
+	    _LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-E'
+	    _LT_TAGVAR(whole_archive_flag_spec, $1)=$wlarc'--whole-archive$convenience '$wlarc'--no-whole-archive'
+	  fi
+	  output_verbose_link_cmd=func_echo_all
+	else
+	  _LT_TAGVAR(ld_shlibs, $1)=no
+	fi
+	;;
+
+      osf3* | osf4* | osf5*)
+        case $cc_basename in
+          KCC*)
+	    # Kuck and Associates, Inc. (KAI) C++ Compiler
+
+	    # KCC will only create a shared library if the output file
+	    # ends with ".so" (or ".sl" for HP-UX), so rename the library
+	    # to its proper name (with version) after linking.
+	    _LT_TAGVAR(archive_cmds, $1)='tempext=`echo $shared_ext | $SED -e '\''s/\([[^()0-9A-Za-z{}]]\)/\\\\\1/g'\''`; templib=`echo "$lib" | $SED -e "s/\$tempext\..*/.so/"`; $CC $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags --soname $soname -o \$templib; mv \$templib $lib'
+
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath,$libdir'
+	    _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+
+	    # Archives containing C++ object files must be created using
+	    # the KAI C++ compiler.
+	    case $host in
+	      osf3*) _LT_TAGVAR(old_archive_cmds, $1)='$CC -Bstatic -o $oldlib $oldobjs' ;;
+	      *) _LT_TAGVAR(old_archive_cmds, $1)='$CC -o $oldlib $oldobjs' ;;
+	    esac
+	    ;;
+          RCC*)
+	    # Rational C++ 2.4.1
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+          cxx*)
+	    case $host in
+	      osf3*)
+	        _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-expect_unresolved $wl\*'
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $soname `test -n "$verstring" && func_echo_all "$wl-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+	        _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+		;;
+	      *)
+	        _LT_TAGVAR(allow_undefined_flag, $1)=' -expect_unresolved \*'
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -shared$allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -msym -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+	        _LT_TAGVAR(archive_expsym_cmds, $1)='for i in `cat $export_symbols`; do printf "%s %s\\n" -exported_symbol "\$i" >> $lib.exp; done~
+                  echo "-hidden">> $lib.exp~
+                  $CC -shared$allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags -msym -soname $soname $wl-input $wl$lib.exp  `test -n "$verstring" && $ECHO "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib~
+                  $RM $lib.exp'
+	        _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-rpath $libdir'
+		;;
+	    esac
+
+	    _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+
+	    # Commands to make compiler produce verbose output that lists
+	    # what "hidden" libraries, object files and flags are used when
+	    # linking a shared library.
+	    #
+	    # There doesn't appear to be a way to prevent this compiler from
+	    # explicitly linking system object files so we need to strip them
+	    # from the output so that they don't get included in the library
+	    # dependencies.
+	    output_verbose_link_cmd='templist=`$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP "ld" | $GREP -v "ld:"`; templist=`func_echo_all "$templist" | $SED "s/\(^.*ld.*\)\( .*ld.*$\)/\1/"`; list= ; for z in $templist; do case $z in conftest.$objext) list="$list $z";; *.$objext);; *) list="$list $z";;esac; done; func_echo_all "$list"'
+	    ;;
+	  *)
+	    if test yes,no = "$GXX,$with_gnu_ld"; then
+	      _LT_TAGVAR(allow_undefined_flag, $1)=' $wl-expect_unresolved $wl\*'
+	      case $host in
+	        osf3*)
+	          _LT_TAGVAR(archive_cmds, $1)='$CC -shared -nostdlib $allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+		  ;;
+	        *)
+	          _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -nostdlib $allow_undefined_flag $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-msym $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+		  ;;
+	      esac
+
+	      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-rpath $wl$libdir'
+	      _LT_TAGVAR(hardcode_libdir_separator, $1)=:
+
+	      # Commands to make compiler produce verbose output that lists
+	      # what "hidden" libraries, object files and flags are used when
+	      # linking a shared library.
+	      output_verbose_link_cmd='$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP -v "^Configured with:" | $GREP " \-L"'
+
+	    else
+	      # FIXME: insert proper C++ library support
+	      _LT_TAGVAR(ld_shlibs, $1)=no
+	    fi
+	    ;;
+        esac
+        ;;
+
+      psos*)
+        # FIXME: insert proper C++ library support
+        _LT_TAGVAR(ld_shlibs, $1)=no
+        ;;
+
+      sunos4*)
+        case $cc_basename in
+          CC*)
+	    # Sun C++ 4.x
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+          lcc*)
+	    # Lucid
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+          *)
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+        esac
+        ;;
+
+      solaris*)
+        case $cc_basename in
+          CC* | sunCC*)
+	    # Sun C++ 4.2, 5.x and Centerline C++
+            _LT_TAGVAR(archive_cmds_need_lc,$1)=yes
+	    _LT_TAGVAR(no_undefined_flag, $1)=' -zdefs'
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -G$allow_undefined_flag -h$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags'
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+              $CC -G$allow_undefined_flag $wl-M $wl$lib.exp -h$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~$RM $lib.exp'
+
+	    _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='-R$libdir'
+	    _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+	    case $host_os in
+	      solaris2.[[0-5]] | solaris2.[[0-5]].*) ;;
+	      *)
+		# The compiler driver will combine and reorder linker options,
+		# but understands '-z linker_flag'.
+	        # Supported since Solaris 2.6 (maybe 2.5.1?)
+		_LT_TAGVAR(whole_archive_flag_spec, $1)='-z allextract$convenience -z defaultextract'
+	        ;;
+	    esac
+	    _LT_TAGVAR(link_all_deplibs, $1)=yes
+
+	    output_verbose_link_cmd='func_echo_all'
+
+	    # Archives containing C++ object files must be created using
+	    # "CC -xar", where "CC" is the Sun C++ compiler.  This is
+	    # necessary to make sure instantiated templates are included
+	    # in the archive.
+	    _LT_TAGVAR(old_archive_cmds, $1)='$CC -xar -o $oldlib $oldobjs'
+	    ;;
+          gcx*)
+	    # Green Hills C++ Compiler
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -shared $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-h $wl$soname -o $lib'
+
+	    # The C++ compiler must be used to create the archive.
+	    _LT_TAGVAR(old_archive_cmds, $1)='$CC $LDFLAGS -archive -o $oldlib $oldobjs'
+	    ;;
+          *)
+	    # GNU C++ compiler with Solaris linker
+	    if test yes,no = "$GXX,$with_gnu_ld"; then
+	      _LT_TAGVAR(no_undefined_flag, $1)=' $wl-z ${wl}defs'
+	      if $CC --version | $GREP -v '^2\.7' > /dev/null; then
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -shared $pic_flag -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-h $wl$soname -o $lib'
+	        _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+                  $CC -shared $pic_flag -nostdlib $wl-M $wl$lib.exp $wl-h $wl$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~$RM $lib.exp'
+
+	        # Commands to make compiler produce verbose output that lists
+	        # what "hidden" libraries, object files and flags are used when
+	        # linking a shared library.
+	        output_verbose_link_cmd='$CC -shared $CFLAGS -v conftest.$objext 2>&1 | $GREP -v "^Configured with:" | $GREP " \-L"'
+	      else
+	        # g++ 2.7 appears to require '-G' NOT '-shared' on this
+	        # platform.
+	        _LT_TAGVAR(archive_cmds, $1)='$CC -G -nostdlib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags $wl-h $wl$soname -o $lib'
+	        _LT_TAGVAR(archive_expsym_cmds, $1)='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+                  $CC -G -nostdlib $wl-M $wl$lib.exp $wl-h $wl$soname -o $lib $predep_objects $libobjs $deplibs $postdep_objects $compiler_flags~$RM $lib.exp'
+
+	        # Commands to make compiler produce verbose output that lists
+	        # what "hidden" libraries, object files and flags are used when
+	        # linking a shared library.
+	        output_verbose_link_cmd='$CC -G $CFLAGS -v conftest.$objext 2>&1 | $GREP -v "^Configured with:" | $GREP " \-L"'
+	      fi
+
+	      _LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R $wl$libdir'
+	      case $host_os in
+		solaris2.[[0-5]] | solaris2.[[0-5]].*) ;;
+		*)
+		  _LT_TAGVAR(whole_archive_flag_spec, $1)='$wl-z ${wl}allextract$convenience $wl-z ${wl}defaultextract'
+		  ;;
+	      esac
+	    fi
+	    ;;
+        esac
+        ;;
+
+    sysv4*uw2* | sysv5OpenUNIX* | sysv5UnixWare7.[[01]].[[10]]* | unixware7* | sco3.2v5.0.[[024]]*)
+      _LT_TAGVAR(no_undefined_flag, $1)='$wl-z,text'
+      _LT_TAGVAR(archive_cmds_need_lc, $1)=no
+      _LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+      runpath_var='LD_RUN_PATH'
+
+      case $cc_basename in
+        CC*)
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	  _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	*)
+	  _LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	  _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+      esac
+      ;;
+
+      sysv5* | sco3.2v5* | sco5v6*)
+	# Note: We CANNOT use -z defs as we might desire, because we do not
+	# link with -lc, and that would cause any symbols used from libc to
+	# always be unresolved, which means just about no library would
+	# ever link correctly.  If we're not using GNU ld we use -z text
+	# though, which does catch some bad symbols but isn't as heavy-handed
+	# as -z defs.
+	_LT_TAGVAR(no_undefined_flag, $1)='$wl-z,text'
+	_LT_TAGVAR(allow_undefined_flag, $1)='$wl-z,nodefs'
+	_LT_TAGVAR(archive_cmds_need_lc, $1)=no
+	_LT_TAGVAR(hardcode_shlibpath_var, $1)=no
+	_LT_TAGVAR(hardcode_libdir_flag_spec, $1)='$wl-R,$libdir'
+	_LT_TAGVAR(hardcode_libdir_separator, $1)=':'
+	_LT_TAGVAR(link_all_deplibs, $1)=yes
+	_LT_TAGVAR(export_dynamic_flag_spec, $1)='$wl-Bexport'
+	runpath_var='LD_RUN_PATH'
+
+	case $cc_basename in
+          CC*)
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	    _LT_TAGVAR(old_archive_cmds, $1)='$CC -Tprelink_objects $oldobjs~
+              '"$_LT_TAGVAR(old_archive_cmds, $1)"
+	    _LT_TAGVAR(reload_cmds, $1)='$CC -Tprelink_objects $reload_objs~
+              '"$_LT_TAGVAR(reload_cmds, $1)"
+	    ;;
+	  *)
+	    _LT_TAGVAR(archive_cmds, $1)='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	    _LT_TAGVAR(archive_expsym_cmds, $1)='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	    ;;
+	esac
+      ;;
+
+      tandem*)
+        case $cc_basename in
+          NCC*)
+	    # NonStop-UX NCC 3.20
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+          *)
+	    # FIXME: insert proper C++ library support
+	    _LT_TAGVAR(ld_shlibs, $1)=no
+	    ;;
+        esac
+        ;;
+
+      vxworks*)
+        # FIXME: insert proper C++ library support
+        _LT_TAGVAR(ld_shlibs, $1)=no
+        ;;
+
+      *)
+        # FIXME: insert proper C++ library support
+        _LT_TAGVAR(ld_shlibs, $1)=no
+        ;;
+    esac
+
+    AC_MSG_RESULT([$_LT_TAGVAR(ld_shlibs, $1)])
+    test no = "$_LT_TAGVAR(ld_shlibs, $1)" && can_build_shared=no
+
+    _LT_TAGVAR(GCC, $1)=$GXX
+    _LT_TAGVAR(LD, $1)=$LD
+
+    ## CAVEAT EMPTOR:
+    ## There is no encapsulation within the following macros, do not change
+    ## the running order or otherwise move them around unless you know exactly
+    ## what you are doing...
+    _LT_SYS_HIDDEN_LIBDEPS($1)
+    _LT_COMPILER_PIC($1)
+    _LT_COMPILER_C_O($1)
+    _LT_COMPILER_FILE_LOCKS($1)
+    _LT_LINKER_SHLIBS($1)
+    _LT_SYS_DYNAMIC_LINKER($1)
+    _LT_LINKER_HARDCODE_LIBPATH($1)
+
+    _LT_CONFIG($1)
+  fi # test -n "$compiler"
+
+  CC=$lt_save_CC
+  CFLAGS=$lt_save_CFLAGS
+  LDCXX=$LD
+  LD=$lt_save_LD
+  GCC=$lt_save_GCC
+  with_gnu_ld=$lt_save_with_gnu_ld
+  lt_cv_path_LDCXX=$lt_cv_path_LD
+  lt_cv_path_LD=$lt_save_path_LD
+  lt_cv_prog_gnu_ldcxx=$lt_cv_prog_gnu_ld
+  lt_cv_prog_gnu_ld=$lt_save_with_gnu_ld
+fi # test yes != "$_lt_caught_CXX_error"
+
+AC_LANG_POP
+])# _LT_LANG_CXX_CONFIG
+
+
+# _LT_FUNC_STRIPNAME_CNF
+# ----------------------
+# func_stripname_cnf prefix suffix name
+# strip PREFIX and SUFFIX off of NAME.
+# PREFIX and SUFFIX must not contain globbing or regex special
+# characters, hashes, percent signs, but SUFFIX may contain a leading
+# dot (in which case that matches only a dot).
+#
+# This function is identical to the (non-XSI) version of func_stripname,
+# except this one can be used by m4 code that may be executed by configure,
+# rather than the libtool script.
+m4_defun([_LT_FUNC_STRIPNAME_CNF],[dnl
+AC_REQUIRE([_LT_DECL_SED])
+AC_REQUIRE([_LT_PROG_ECHO_BACKSLASH])
+func_stripname_cnf ()
+{
+  case @S|@2 in
+  .*) func_stripname_result=`$ECHO "@S|@3" | $SED "s%^@S|@1%%; s%\\\\@S|@2\$%%"`;;
+  *)  func_stripname_result=`$ECHO "@S|@3" | $SED "s%^@S|@1%%; s%@S|@2\$%%"`;;
+  esac
+} # func_stripname_cnf
+])# _LT_FUNC_STRIPNAME_CNF
+
+
+# _LT_SYS_HIDDEN_LIBDEPS([TAGNAME])
+# ---------------------------------
+# Figure out "hidden" library dependencies from verbose
+# compiler output when linking a shared library.
+# Parse the compiler output and extract the necessary
+# objects, libraries and library flags.
+m4_defun([_LT_SYS_HIDDEN_LIBDEPS],
+[m4_require([_LT_FILEUTILS_DEFAULTS])dnl
+AC_REQUIRE([_LT_FUNC_STRIPNAME_CNF])dnl
+# Dependencies to place before and after the object being linked:
+_LT_TAGVAR(predep_objects, $1)=
+_LT_TAGVAR(postdep_objects, $1)=
+_LT_TAGVAR(predeps, $1)=
+_LT_TAGVAR(postdeps, $1)=
+_LT_TAGVAR(compiler_lib_search_path, $1)=
+
+dnl we can't use the lt_simple_compile_test_code here,
+dnl because it contains code intended for an executable,
+dnl not a library.  It's possible we should let each
+dnl tag define a new lt_????_link_test_code variable,
+dnl but it's only used here...
+m4_if([$1], [], [cat > conftest.$ac_ext <<_LT_EOF
+int a;
+void foo (void) { a = 0; }
+_LT_EOF
+], [$1], [CXX], [cat > conftest.$ac_ext <<_LT_EOF
+class Foo
+{
+public:
+  Foo (void) { a = 0; }
+private:
+  int a;
+};
+_LT_EOF
+], [$1], [F77], [cat > conftest.$ac_ext <<_LT_EOF
+      subroutine foo
+      implicit none
+      integer*4 a
+      a=0
+      return
+      end
+_LT_EOF
+], [$1], [FC], [cat > conftest.$ac_ext <<_LT_EOF
+      subroutine foo
+      implicit none
+      integer a
+      a=0
+      return
+      end
+_LT_EOF
+], [$1], [GCJ], [cat > conftest.$ac_ext <<_LT_EOF
+public class foo {
+  private int a;
+  public void bar (void) {
+    a = 0;
+  }
+};
+_LT_EOF
+], [$1], [GO], [cat > conftest.$ac_ext <<_LT_EOF
+package foo
+func foo() {
+}
+_LT_EOF
+])
+
+_lt_libdeps_save_CFLAGS=$CFLAGS
+case "$CC $CFLAGS " in #(
+*\ -flto*\ *) CFLAGS="$CFLAGS -fno-lto" ;;
+*\ -fwhopr*\ *) CFLAGS="$CFLAGS -fno-whopr" ;;
+*\ -fuse-linker-plugin*\ *) CFLAGS="$CFLAGS -fno-use-linker-plugin" ;;
+esac
+
+dnl Parse the compiler output and extract the necessary
+dnl objects, libraries and library flags.
+if AC_TRY_EVAL(ac_compile); then
+  # Parse the compiler output and extract the necessary
+  # objects, libraries and library flags.
+
+  # Sentinel used to keep track of whether or not we are before
+  # the conftest object file.
+  pre_test_object_deps_done=no
+
+  for p in `eval "$output_verbose_link_cmd"`; do
+    case $prev$p in
+
+    -L* | -R* | -l*)
+       # Some compilers place space between "-{L,R}" and the path.
+       # Remove the space.
+       if test x-L = "$p" ||
+          test x-R = "$p"; then
+	 prev=$p
+	 continue
+       fi
+
+       # Expand the sysroot to ease extracting the directories later.
+       if test -z "$prev"; then
+         case $p in
+         -L*) func_stripname_cnf '-L' '' "$p"; prev=-L; p=$func_stripname_result ;;
+         -R*) func_stripname_cnf '-R' '' "$p"; prev=-R; p=$func_stripname_result ;;
+         -l*) func_stripname_cnf '-l' '' "$p"; prev=-l; p=$func_stripname_result ;;
+         esac
+       fi
+       case $p in
+       =*) func_stripname_cnf '=' '' "$p"; p=$lt_sysroot$func_stripname_result ;;
+       esac
+       if test no = "$pre_test_object_deps_done"; then
+	 case $prev in
+	 -L | -R)
+	   # Internal compiler library paths should come after those
+	   # provided the user.  The postdeps already come after the
+	   # user supplied libs so there is no need to process them.
+	   if test -z "$_LT_TAGVAR(compiler_lib_search_path, $1)"; then
+	     _LT_TAGVAR(compiler_lib_search_path, $1)=$prev$p
+	   else
+	     _LT_TAGVAR(compiler_lib_search_path, $1)="${_LT_TAGVAR(compiler_lib_search_path, $1)} $prev$p"
+	   fi
+	   ;;
+	 # The "-l" case would never come before the object being
+	 # linked, so don't bother handling this case.
+	 esac
+       else
+	 if test -z "$_LT_TAGVAR(postdeps, $1)"; then
+	   _LT_TAGVAR(postdeps, $1)=$prev$p
+	 else
+	   _LT_TAGVAR(postdeps, $1)="${_LT_TAGVAR(postdeps, $1)} $prev$p"
+	 fi
+       fi
+       prev=
+       ;;
+
+    *.lto.$objext) ;; # Ignore GCC LTO objects
+    *.$objext)
+       # This assumes that the test object file only shows up
+       # once in the compiler output.
+       if test "$p" = "conftest.$objext"; then
+	 pre_test_object_deps_done=yes
+	 continue
+       fi
+
+       if test no = "$pre_test_object_deps_done"; then
+	 if test -z "$_LT_TAGVAR(predep_objects, $1)"; then
+	   _LT_TAGVAR(predep_objects, $1)=$p
+	 else
+	   _LT_TAGVAR(predep_objects, $1)="$_LT_TAGVAR(predep_objects, $1) $p"
+	 fi
+       else
+	 if test -z "$_LT_TAGVAR(postdep_objects, $1)"; then
+	   _LT_TAGVAR(postdep_objects, $1)=$p
+	 else
+	   _LT_TAGVAR(postdep_objects, $1)="$_LT_TAGVAR(postdep_objects, $1) $p"
+	 fi
+       fi
+       ;;
+
+    *) ;; # Ignore the rest.
+
+    esac
+  done
+
+  # Clean up.
+  rm -f a.out a.exe
+else
+  echo "libtool.m4: error: problem compiling $1 test program"
+fi
+
+$RM -f confest.$objext
+CFLAGS=$_lt_libdeps_save_CFLAGS
+
+# PORTME: override above test on systems where it is broken
+m4_if([$1], [CXX],
+[case $host_os in
+interix[[3-9]]*)
+  # Interix 3.5 installs completely hosed .la files for C++, so rather than
+  # hack all around it, let's just trust "g++" to DTRT.
+  _LT_TAGVAR(predep_objects,$1)=
+  _LT_TAGVAR(postdep_objects,$1)=
+  _LT_TAGVAR(postdeps,$1)=
+  ;;
+esac
+])
+
+case " $_LT_TAGVAR(postdeps, $1) " in
+*" -lc "*) _LT_TAGVAR(archive_cmds_need_lc, $1)=no ;;
+esac
+ _LT_TAGVAR(compiler_lib_search_dirs, $1)=
+if test -n "${_LT_TAGVAR(compiler_lib_search_path, $1)}"; then
+ _LT_TAGVAR(compiler_lib_search_dirs, $1)=`echo " ${_LT_TAGVAR(compiler_lib_search_path, $1)}" | $SED -e 's! -L! !g' -e 's!^ !!'`
+fi
+_LT_TAGDECL([], [compiler_lib_search_dirs], [1],
+    [The directories searched by this compiler when creating a shared library])
+_LT_TAGDECL([], [predep_objects], [1],
+    [Dependencies to place before and after the objects being linked to
+    create a shared library])
+_LT_TAGDECL([], [postdep_objects], [1])
+_LT_TAGDECL([], [predeps], [1])
+_LT_TAGDECL([], [postdeps], [1])
+_LT_TAGDECL([], [compiler_lib_search_path], [1],
+    [The library search path used internally by the compiler when linking
+    a shared library])
+])# _LT_SYS_HIDDEN_LIBDEPS
+
+
+# _LT_LANG_F77_CONFIG([TAG])
+# --------------------------
+# Ensure that the configuration variables for a Fortran 77 compiler are
+# suitably defined.  These variables are subsequently used by _LT_CONFIG
+# to write the compiler configuration to 'libtool'.
+m4_defun([_LT_LANG_F77_CONFIG],
+[AC_LANG_PUSH(Fortran 77)
+if test -z "$F77" || test no = "$F77"; then
+  _lt_disable_F77=yes
+fi
+
+_LT_TAGVAR(archive_cmds_need_lc, $1)=no
+_LT_TAGVAR(allow_undefined_flag, $1)=
+_LT_TAGVAR(always_export_symbols, $1)=no
+_LT_TAGVAR(archive_expsym_cmds, $1)=
+_LT_TAGVAR(export_dynamic_flag_spec, $1)=
+_LT_TAGVAR(hardcode_direct, $1)=no
+_LT_TAGVAR(hardcode_direct_absolute, $1)=no
+_LT_TAGVAR(hardcode_libdir_flag_spec, $1)=
+_LT_TAGVAR(hardcode_libdir_separator, $1)=
+_LT_TAGVAR(hardcode_minus_L, $1)=no
+_LT_TAGVAR(hardcode_automatic, $1)=no
+_LT_TAGVAR(inherit_rpath, $1)=no
+_LT_TAGVAR(module_cmds, $1)=
+_LT_TAGVAR(module_expsym_cmds, $1)=
+_LT_TAGVAR(link_all_deplibs, $1)=unknown
+_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds
+_LT_TAGVAR(reload_flag, $1)=$reload_flag
+_LT_TAGVAR(reload_cmds, $1)=$reload_cmds
+_LT_TAGVAR(no_undefined_flag, $1)=
+_LT_TAGVAR(whole_archive_flag_spec, $1)=
+_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=no
+
+# Source file extension for f77 test sources.
+ac_ext=f
+
+# Object file extension for compiled f77 test sources.
+objext=o
+_LT_TAGVAR(objext, $1)=$objext
+
+# No sense in running all these tests if we already determined that
+# the F77 compiler isn't working.  Some variables (like enable_shared)
+# are currently assumed to apply to all compilers on this platform,
+# and will be corrupted by setting them based on a non-working compiler.
+if test yes != "$_lt_disable_F77"; then
+  # Code to be used in simple compile tests
+  lt_simple_compile_test_code="\
+      subroutine t
+      return
+      end
+"
+
+  # Code to be used in simple link tests
+  lt_simple_link_test_code="\
+      program t
+      end
+"
+
+  # ltmain only uses $CC for tagged configurations so make sure $CC is set.
+  _LT_TAG_COMPILER
+
+  # save warnings/boilerplate of simple test code
+  _LT_COMPILER_BOILERPLATE
+  _LT_LINKER_BOILERPLATE
+
+  # Allow CC to be a program name with arguments.
+  lt_save_CC=$CC
+  lt_save_GCC=$GCC
+  lt_save_CFLAGS=$CFLAGS
+  CC=${F77-"f77"}
+  CFLAGS=$FFLAGS
+  compiler=$CC
+  _LT_TAGVAR(compiler, $1)=$CC
+  _LT_CC_BASENAME([$compiler])
+  GCC=$G77
+  if test -n "$compiler"; then
+    AC_MSG_CHECKING([if libtool supports shared libraries])
+    AC_MSG_RESULT([$can_build_shared])
+
+    AC_MSG_CHECKING([whether to build shared libraries])
+    test no = "$can_build_shared" && enable_shared=no
+
+    # On AIX, shared libraries and static libraries use the same namespace, and
+    # are all built from PIC.
+    case $host_os in
+      aix3*)
+        test yes = "$enable_shared" && enable_static=no
+        if test -n "$RANLIB"; then
+          archive_cmds="$archive_cmds~\$RANLIB \$lib"
+          postinstall_cmds='$RANLIB $lib'
+        fi
+        ;;
+      aix[[4-9]]*)
+	if test ia64 != "$host_cpu"; then
+	  case $enable_shared,$with_aix_soname,$aix_use_runtimelinking in
+	  yes,aix,yes) ;;		# shared object as lib.so file only
+	  yes,svr4,*) ;;		# shared object as lib.so archive member only
+	  yes,*) enable_static=no ;;	# shared object in lib.a archive as well
+	  esac
+	fi
+        ;;
+    esac
+    AC_MSG_RESULT([$enable_shared])
+
+    AC_MSG_CHECKING([whether to build static libraries])
+    # Make sure either enable_shared or enable_static is yes.
+    test yes = "$enable_shared" || enable_static=yes
+    AC_MSG_RESULT([$enable_static])
+
+    _LT_TAGVAR(GCC, $1)=$G77
+    _LT_TAGVAR(LD, $1)=$LD
+
+    ## CAVEAT EMPTOR:
+    ## There is no encapsulation within the following macros, do not change
+    ## the running order or otherwise move them around unless you know exactly
+    ## what you are doing...
+    _LT_COMPILER_PIC($1)
+    _LT_COMPILER_C_O($1)
+    _LT_COMPILER_FILE_LOCKS($1)
+    _LT_LINKER_SHLIBS($1)
+    _LT_SYS_DYNAMIC_LINKER($1)
+    _LT_LINKER_HARDCODE_LIBPATH($1)
+
+    _LT_CONFIG($1)
+  fi # test -n "$compiler"
+
+  GCC=$lt_save_GCC
+  CC=$lt_save_CC
+  CFLAGS=$lt_save_CFLAGS
+fi # test yes != "$_lt_disable_F77"
+
+AC_LANG_POP
+])# _LT_LANG_F77_CONFIG
+
+
+# _LT_LANG_FC_CONFIG([TAG])
+# -------------------------
+# Ensure that the configuration variables for a Fortran compiler are
+# suitably defined.  These variables are subsequently used by _LT_CONFIG
+# to write the compiler configuration to 'libtool'.
+m4_defun([_LT_LANG_FC_CONFIG],
+[AC_LANG_PUSH(Fortran)
+
+if test -z "$FC" || test no = "$FC"; then
+  _lt_disable_FC=yes
+fi
+
+_LT_TAGVAR(archive_cmds_need_lc, $1)=no
+_LT_TAGVAR(allow_undefined_flag, $1)=
+_LT_TAGVAR(always_export_symbols, $1)=no
+_LT_TAGVAR(archive_expsym_cmds, $1)=
+_LT_TAGVAR(export_dynamic_flag_spec, $1)=
+_LT_TAGVAR(hardcode_direct, $1)=no
+_LT_TAGVAR(hardcode_direct_absolute, $1)=no
+_LT_TAGVAR(hardcode_libdir_flag_spec, $1)=
+_LT_TAGVAR(hardcode_libdir_separator, $1)=
+_LT_TAGVAR(hardcode_minus_L, $1)=no
+_LT_TAGVAR(hardcode_automatic, $1)=no
+_LT_TAGVAR(inherit_rpath, $1)=no
+_LT_TAGVAR(module_cmds, $1)=
+_LT_TAGVAR(module_expsym_cmds, $1)=
+_LT_TAGVAR(link_all_deplibs, $1)=unknown
+_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds
+_LT_TAGVAR(reload_flag, $1)=$reload_flag
+_LT_TAGVAR(reload_cmds, $1)=$reload_cmds
+_LT_TAGVAR(no_undefined_flag, $1)=
+_LT_TAGVAR(whole_archive_flag_spec, $1)=
+_LT_TAGVAR(enable_shared_with_static_runtimes, $1)=no
+
+# Source file extension for fc test sources.
+ac_ext=${ac_fc_srcext-f}
+
+# Object file extension for compiled fc test sources.
+objext=o
+_LT_TAGVAR(objext, $1)=$objext
+
+# No sense in running all these tests if we already determined that
+# the FC compiler isn't working.  Some variables (like enable_shared)
+# are currently assumed to apply to all compilers on this platform,
+# and will be corrupted by setting them based on a non-working compiler.
+if test yes != "$_lt_disable_FC"; then
+  # Code to be used in simple compile tests
+  lt_simple_compile_test_code="\
+      subroutine t
+      return
+      end
+"
+
+  # Code to be used in simple link tests
+  lt_simple_link_test_code="\
+      program t
+      end
+"
+
+  # ltmain only uses $CC for tagged configurations so make sure $CC is set.
+  _LT_TAG_COMPILER
+
+  # save warnings/boilerplate of simple test code
+  _LT_COMPILER_BOILERPLATE
+  _LT_LINKER_BOILERPLATE
+
+  # Allow CC to be a program name with arguments.
+  lt_save_CC=$CC
+  lt_save_GCC=$GCC
+  lt_save_CFLAGS=$CFLAGS
+  CC=${FC-"f95"}
+  CFLAGS=$FCFLAGS
+  compiler=$CC
+  GCC=$ac_cv_fc_compiler_gnu
+
+  _LT_TAGVAR(compiler, $1)=$CC
+  _LT_CC_BASENAME([$compiler])
+
+  if test -n "$compiler"; then
+    AC_MSG_CHECKING([if libtool supports shared libraries])
+    AC_MSG_RESULT([$can_build_shared])
+
+    AC_MSG_CHECKING([whether to build shared libraries])
+    test no = "$can_build_shared" && enable_shared=no
+
+    # On AIX, shared libraries and static libraries use the same namespace, and
+    # are all built from PIC.
+    case $host_os in
+      aix3*)
+        test yes = "$enable_shared" && enable_static=no
+        if test -n "$RANLIB"; then
+          archive_cmds="$archive_cmds~\$RANLIB \$lib"
+          postinstall_cmds='$RANLIB $lib'
+        fi
+        ;;
+      aix[[4-9]]*)
+	if test ia64 != "$host_cpu"; then
+	  case $enable_shared,$with_aix_soname,$aix_use_runtimelinking in
+	  yes,aix,yes) ;;		# shared object as lib.so file only
+	  yes,svr4,*) ;;		# shared object as lib.so archive member only
+	  yes,*) enable_static=no ;;	# shared object in lib.a archive as well
+	  esac
+	fi
+        ;;
+    esac
+    AC_MSG_RESULT([$enable_shared])
+
+    AC_MSG_CHECKING([whether to build static libraries])
+    # Make sure either enable_shared or enable_static is yes.
+    test yes = "$enable_shared" || enable_static=yes
+    AC_MSG_RESULT([$enable_static])
+
+    _LT_TAGVAR(GCC, $1)=$ac_cv_fc_compiler_gnu
+    _LT_TAGVAR(LD, $1)=$LD
+
+    ## CAVEAT EMPTOR:
+    ## There is no encapsulation within the following macros, do not change
+    ## the running order or otherwise move them around unless you know exactly
+    ## what you are doing...
+    _LT_SYS_HIDDEN_LIBDEPS($1)
+    _LT_COMPILER_PIC($1)
+    _LT_COMPILER_C_O($1)
+    _LT_COMPILER_FILE_LOCKS($1)
+    _LT_LINKER_SHLIBS($1)
+    _LT_SYS_DYNAMIC_LINKER($1)
+    _LT_LINKER_HARDCODE_LIBPATH($1)
+
+    _LT_CONFIG($1)
+  fi # test -n "$compiler"
+
+  GCC=$lt_save_GCC
+  CC=$lt_save_CC
+  CFLAGS=$lt_save_CFLAGS
+fi # test yes != "$_lt_disable_FC"
+
+AC_LANG_POP
+])# _LT_LANG_FC_CONFIG
+
+
+# _LT_LANG_GCJ_CONFIG([TAG])
+# --------------------------
+# Ensure that the configuration variables for the GNU Java Compiler compiler
+# are suitably defined.  These variables are subsequently used by _LT_CONFIG
+# to write the compiler configuration to 'libtool'.
+m4_defun([_LT_LANG_GCJ_CONFIG],
+[AC_REQUIRE([LT_PROG_GCJ])dnl
+AC_LANG_SAVE
+
+# Source file extension for Java test sources.
+ac_ext=java
+
+# Object file extension for compiled Java test sources.
+objext=o
+_LT_TAGVAR(objext, $1)=$objext
+
+# Code to be used in simple compile tests
+lt_simple_compile_test_code="class foo {}"
+
+# Code to be used in simple link tests
+lt_simple_link_test_code='public class conftest { public static void main(String[[]] argv) {}; }'
+
+# ltmain only uses $CC for tagged configurations so make sure $CC is set.
+_LT_TAG_COMPILER
+
+# save warnings/boilerplate of simple test code
+_LT_COMPILER_BOILERPLATE
+_LT_LINKER_BOILERPLATE
+
+# Allow CC to be a program name with arguments.
+lt_save_CC=$CC
+lt_save_CFLAGS=$CFLAGS
+lt_save_GCC=$GCC
+GCC=yes
+CC=${GCJ-"gcj"}
+CFLAGS=$GCJFLAGS
+compiler=$CC
+_LT_TAGVAR(compiler, $1)=$CC
+_LT_TAGVAR(LD, $1)=$LD
+_LT_CC_BASENAME([$compiler])
+
+# GCJ did not exist at the time GCC didn't implicitly link libc in.
+_LT_TAGVAR(archive_cmds_need_lc, $1)=no
+
+_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds
+_LT_TAGVAR(reload_flag, $1)=$reload_flag
+_LT_TAGVAR(reload_cmds, $1)=$reload_cmds
+
+if test -n "$compiler"; then
+  _LT_COMPILER_NO_RTTI($1)
+  _LT_COMPILER_PIC($1)
+  _LT_COMPILER_C_O($1)
+  _LT_COMPILER_FILE_LOCKS($1)
+  _LT_LINKER_SHLIBS($1)
+  _LT_LINKER_HARDCODE_LIBPATH($1)
+
+  _LT_CONFIG($1)
+fi
+
+AC_LANG_RESTORE
+
+GCC=$lt_save_GCC
+CC=$lt_save_CC
+CFLAGS=$lt_save_CFLAGS
+])# _LT_LANG_GCJ_CONFIG
+
+
+# _LT_LANG_GO_CONFIG([TAG])
+# --------------------------
+# Ensure that the configuration variables for the GNU Go compiler
+# are suitably defined.  These variables are subsequently used by _LT_CONFIG
+# to write the compiler configuration to 'libtool'.
+m4_defun([_LT_LANG_GO_CONFIG],
+[AC_REQUIRE([LT_PROG_GO])dnl
+AC_LANG_SAVE
+
+# Source file extension for Go test sources.
+ac_ext=go
+
+# Object file extension for compiled Go test sources.
+objext=o
+_LT_TAGVAR(objext, $1)=$objext
+
+# Code to be used in simple compile tests
+lt_simple_compile_test_code="package main; func main() { }"
+
+# Code to be used in simple link tests
+lt_simple_link_test_code='package main; func main() { }'
+
+# ltmain only uses $CC for tagged configurations so make sure $CC is set.
+_LT_TAG_COMPILER
+
+# save warnings/boilerplate of simple test code
+_LT_COMPILER_BOILERPLATE
+_LT_LINKER_BOILERPLATE
+
+# Allow CC to be a program name with arguments.
+lt_save_CC=$CC
+lt_save_CFLAGS=$CFLAGS
+lt_save_GCC=$GCC
+GCC=yes
+CC=${GOC-"gccgo"}
+CFLAGS=$GOFLAGS
+compiler=$CC
+_LT_TAGVAR(compiler, $1)=$CC
+_LT_TAGVAR(LD, $1)=$LD
+_LT_CC_BASENAME([$compiler])
+
+# Go did not exist at the time GCC didn't implicitly link libc in.
+_LT_TAGVAR(archive_cmds_need_lc, $1)=no
+
+_LT_TAGVAR(old_archive_cmds, $1)=$old_archive_cmds
+_LT_TAGVAR(reload_flag, $1)=$reload_flag
+_LT_TAGVAR(reload_cmds, $1)=$reload_cmds
+
+if test -n "$compiler"; then
+  _LT_COMPILER_NO_RTTI($1)
+  _LT_COMPILER_PIC($1)
+  _LT_COMPILER_C_O($1)
+  _LT_COMPILER_FILE_LOCKS($1)
+  _LT_LINKER_SHLIBS($1)
+  _LT_LINKER_HARDCODE_LIBPATH($1)
+
+  _LT_CONFIG($1)
+fi
+
+AC_LANG_RESTORE
+
+GCC=$lt_save_GCC
+CC=$lt_save_CC
+CFLAGS=$lt_save_CFLAGS
+])# _LT_LANG_GO_CONFIG
+
+
+# _LT_LANG_RC_CONFIG([TAG])
+# -------------------------
+# Ensure that the configuration variables for the Windows resource compiler
+# are suitably defined.  These variables are subsequently used by _LT_CONFIG
+# to write the compiler configuration to 'libtool'.
+m4_defun([_LT_LANG_RC_CONFIG],
+[AC_REQUIRE([LT_PROG_RC])dnl
+AC_LANG_SAVE
+
+# Source file extension for RC test sources.
+ac_ext=rc
+
+# Object file extension for compiled RC test sources.
+objext=o
+_LT_TAGVAR(objext, $1)=$objext
+
+# Code to be used in simple compile tests
+lt_simple_compile_test_code='sample MENU { MENUITEM "&Soup", 100, CHECKED }'
+
+# Code to be used in simple link tests
+lt_simple_link_test_code=$lt_simple_compile_test_code
+
+# ltmain only uses $CC for tagged configurations so make sure $CC is set.
+_LT_TAG_COMPILER
+
+# save warnings/boilerplate of simple test code
+_LT_COMPILER_BOILERPLATE
+_LT_LINKER_BOILERPLATE
+
+# Allow CC to be a program name with arguments.
+lt_save_CC=$CC
+lt_save_CFLAGS=$CFLAGS
+lt_save_GCC=$GCC
+GCC=
+CC=${RC-"windres"}
+CFLAGS=
+compiler=$CC
+_LT_TAGVAR(compiler, $1)=$CC
+_LT_CC_BASENAME([$compiler])
+_LT_TAGVAR(lt_cv_prog_compiler_c_o, $1)=yes
+
+if test -n "$compiler"; then
+  :
+  _LT_CONFIG($1)
+fi
+
+GCC=$lt_save_GCC
+AC_LANG_RESTORE
+CC=$lt_save_CC
+CFLAGS=$lt_save_CFLAGS
+])# _LT_LANG_RC_CONFIG
+
+
+# LT_PROG_GCJ
+# -----------
+AC_DEFUN([LT_PROG_GCJ],
+[m4_ifdef([AC_PROG_GCJ], [AC_PROG_GCJ],
+  [m4_ifdef([A][M_PROG_GCJ], [A][M_PROG_GCJ],
+    [AC_CHECK_TOOL(GCJ, gcj,)
+      test set = "${GCJFLAGS+set}" || GCJFLAGS="-g -O2"
+      AC_SUBST(GCJFLAGS)])])[]dnl
+])
+
+# Old name:
+AU_ALIAS([LT_AC_PROG_GCJ], [LT_PROG_GCJ])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([LT_AC_PROG_GCJ], [])
+
+
+# LT_PROG_GO
+# ----------
+AC_DEFUN([LT_PROG_GO],
+[AC_CHECK_TOOL(GOC, gccgo,)
+])
+
+
+# LT_PROG_RC
+# ----------
+AC_DEFUN([LT_PROG_RC],
+[AC_CHECK_TOOL(RC, windres,)
+])
+
+# Old name:
+AU_ALIAS([LT_AC_PROG_RC], [LT_PROG_RC])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([LT_AC_PROG_RC], [])
+
+
+# _LT_DECL_EGREP
+# --------------
+# If we don't have a new enough Autoconf to choose the best grep
+# available, choose the one first in the user's PATH.
+m4_defun([_LT_DECL_EGREP],
+[AC_REQUIRE([AC_PROG_EGREP])dnl
+AC_REQUIRE([AC_PROG_FGREP])dnl
+test -z "$GREP" && GREP=grep
+_LT_DECL([], [GREP], [1], [A grep program that handles long lines])
+_LT_DECL([], [EGREP], [1], [An ERE matcher])
+_LT_DECL([], [FGREP], [1], [A literal string matcher])
+dnl Non-bleeding-edge autoconf doesn't subst GREP, so do it here too
+AC_SUBST([GREP])
+])
+
+
+# _LT_DECL_OBJDUMP
+# --------------
+# If we don't have a new enough Autoconf to choose the best objdump
+# available, choose the one first in the user's PATH.
+m4_defun([_LT_DECL_OBJDUMP],
+[AC_CHECK_TOOL(OBJDUMP, objdump, false)
+test -z "$OBJDUMP" && OBJDUMP=objdump
+_LT_DECL([], [OBJDUMP], [1], [An object symbol dumper])
+AC_SUBST([OBJDUMP])
+])
+
+# _LT_DECL_DLLTOOL
+# ----------------
+# Ensure DLLTOOL variable is set.
+m4_defun([_LT_DECL_DLLTOOL],
+[AC_CHECK_TOOL(DLLTOOL, dlltool, false)
+test -z "$DLLTOOL" && DLLTOOL=dlltool
+_LT_DECL([], [DLLTOOL], [1], [DLL creation program])
+AC_SUBST([DLLTOOL])
+])
+
+# _LT_DECL_FILECMD
+# ----------------
+# Check for a file(cmd) program that can be used to detect file type and magic
+m4_defun([_LT_DECL_FILECMD],
+[AC_CHECK_TOOL([FILECMD], [file], [:])
+_LT_DECL([], [FILECMD], [1], [A file(cmd) program that detects file types])
+])# _LD_DECL_FILECMD
+
+# _LT_DECL_SED
+# ------------
+# Check for a fully-functional sed program, that truncates
+# as few characters as possible.  Prefer GNU sed if found.
+m4_defun([_LT_DECL_SED],
+[AC_PROG_SED
+test -z "$SED" && SED=sed
+Xsed="$SED -e 1s/^X//"
+_LT_DECL([], [SED], [1], [A sed program that does not truncate output])
+_LT_DECL([], [Xsed], ["\$SED -e 1s/^X//"],
+    [Sed that helps us avoid accidentally triggering echo(1) options like -n])
+])# _LT_DECL_SED
+
+m4_ifndef([AC_PROG_SED], [
+# NOTE: This macro has been submitted for inclusion into   #
+#  GNU Autoconf as AC_PROG_SED.  When it is available in   #
+#  a released version of Autoconf we should remove this    #
+#  macro and use it instead.                               #
+
+m4_defun([AC_PROG_SED],
+[AC_MSG_CHECKING([for a sed that does not truncate output])
+AC_CACHE_VAL(lt_cv_path_SED,
+[# Loop through the user's path and test for sed and gsed.
+# Then use that list of sed's as ones to test for truncation.
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  test -z "$as_dir" && as_dir=.
+  for lt_ac_prog in sed gsed; do
+    for ac_exec_ext in '' $ac_executable_extensions; do
+      if $as_executable_p "$as_dir/$lt_ac_prog$ac_exec_ext"; then
+        lt_ac_sed_list="$lt_ac_sed_list $as_dir/$lt_ac_prog$ac_exec_ext"
+      fi
+    done
+  done
+done
+IFS=$as_save_IFS
+lt_ac_max=0
+lt_ac_count=0
+# Add /usr/xpg4/bin/sed as it is typically found on Solaris
+# along with /bin/sed that truncates output.
+for lt_ac_sed in $lt_ac_sed_list /usr/xpg4/bin/sed; do
+  test ! -f "$lt_ac_sed" && continue
+  cat /dev/null > conftest.in
+  lt_ac_count=0
+  echo $ECHO_N "0123456789$ECHO_C" >conftest.in
+  # Check for GNU sed and select it if it is found.
+  if "$lt_ac_sed" --version 2>&1 < /dev/null | grep 'GNU' > /dev/null; then
+    lt_cv_path_SED=$lt_ac_sed
+    break
+  fi
+  while true; do
+    cat conftest.in conftest.in >conftest.tmp
+    mv conftest.tmp conftest.in
+    cp conftest.in conftest.nl
+    echo >>conftest.nl
+    $lt_ac_sed -e 's/a$//' < conftest.nl >conftest.out || break
+    cmp -s conftest.out conftest.nl || break
+    # 10000 chars as input seems more than enough
+    test 10 -lt "$lt_ac_count" && break
+    lt_ac_count=`expr $lt_ac_count + 1`
+    if test "$lt_ac_count" -gt "$lt_ac_max"; then
+      lt_ac_max=$lt_ac_count
+      lt_cv_path_SED=$lt_ac_sed
+    fi
+  done
+done
+])
+SED=$lt_cv_path_SED
+AC_SUBST([SED])
+AC_MSG_RESULT([$SED])
+])#AC_PROG_SED
+])#m4_ifndef
+
+# Old name:
+AU_ALIAS([LT_AC_PROG_SED], [AC_PROG_SED])
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([LT_AC_PROG_SED], [])
+
+
+# _LT_CHECK_SHELL_FEATURES
+# ------------------------
+# Find out whether the shell is Bourne or XSI compatible,
+# or has some other useful features.
+m4_defun([_LT_CHECK_SHELL_FEATURES],
+[if ( (MAIL=60; unset MAIL) || exit) >/dev/null 2>&1; then
+  lt_unset=unset
+else
+  lt_unset=false
+fi
+_LT_DECL([], [lt_unset], [0], [whether the shell understands "unset"])dnl
+
+# test EBCDIC or ASCII
+case `echo X|tr X '\101'` in
+ A) # ASCII based system
+    # \n is not interpreted correctly by Solaris 8 /usr/ucb/tr
+  lt_SP2NL='tr \040 \012'
+  lt_NL2SP='tr \015\012 \040\040'
+  ;;
+ *) # EBCDIC based system
+  lt_SP2NL='tr \100 \n'
+  lt_NL2SP='tr \r\n \100\100'
+  ;;
+esac
+_LT_DECL([SP2NL], [lt_SP2NL], [1], [turn spaces into newlines])dnl
+_LT_DECL([NL2SP], [lt_NL2SP], [1], [turn newlines into spaces])dnl
+])# _LT_CHECK_SHELL_FEATURES
+
+
+# _LT_PATH_CONVERSION_FUNCTIONS
+# -----------------------------
+# Determine what file name conversion functions should be used by
+# func_to_host_file (and, implicitly, by func_to_host_path).  These are needed
+# for certain cross-compile configurations and native mingw.
+m4_defun([_LT_PATH_CONVERSION_FUNCTIONS],
+[AC_REQUIRE([AC_CANONICAL_HOST])dnl
+AC_REQUIRE([AC_CANONICAL_BUILD])dnl
+AC_MSG_CHECKING([how to convert $build file names to $host format])
+AC_CACHE_VAL(lt_cv_to_host_file_cmd,
+[case $host in
+  *-*-mingw* )
+    case $build in
+      *-*-mingw* ) # actually msys
+        lt_cv_to_host_file_cmd=func_convert_file_msys_to_w32
+        ;;
+      *-*-cygwin* )
+        lt_cv_to_host_file_cmd=func_convert_file_cygwin_to_w32
+        ;;
+      * ) # otherwise, assume *nix
+        lt_cv_to_host_file_cmd=func_convert_file_nix_to_w32
+        ;;
+    esac
+    ;;
+  *-*-cygwin* )
+    case $build in
+      *-*-mingw* ) # actually msys
+        lt_cv_to_host_file_cmd=func_convert_file_msys_to_cygwin
+        ;;
+      *-*-cygwin* )
+        lt_cv_to_host_file_cmd=func_convert_file_noop
+        ;;
+      * ) # otherwise, assume *nix
+        lt_cv_to_host_file_cmd=func_convert_file_nix_to_cygwin
+        ;;
+    esac
+    ;;
+  * ) # unhandled hosts (and "normal" native builds)
+    lt_cv_to_host_file_cmd=func_convert_file_noop
+    ;;
+esac
+])
+to_host_file_cmd=$lt_cv_to_host_file_cmd
+AC_MSG_RESULT([$lt_cv_to_host_file_cmd])
+_LT_DECL([to_host_file_cmd], [lt_cv_to_host_file_cmd],
+         [0], [convert $build file names to $host format])dnl
+
+AC_MSG_CHECKING([how to convert $build file names to toolchain format])
+AC_CACHE_VAL(lt_cv_to_tool_file_cmd,
+[#assume ordinary cross tools, or native build.
+lt_cv_to_tool_file_cmd=func_convert_file_noop
+case $host in
+  *-*-mingw* )
+    case $build in
+      *-*-mingw* ) # actually msys
+        lt_cv_to_tool_file_cmd=func_convert_file_msys_to_w32
+        ;;
+    esac
+    ;;
+esac
+])
+to_tool_file_cmd=$lt_cv_to_tool_file_cmd
+AC_MSG_RESULT([$lt_cv_to_tool_file_cmd])
+_LT_DECL([to_tool_file_cmd], [lt_cv_to_tool_file_cmd],
+         [0], [convert $build files to toolchain format])dnl
+])# _LT_PATH_CONVERSION_FUNCTIONS
+
+# Helper functions for option handling.                    -*- Autoconf -*-
+#
+#   Copyright (C) 2004-2005, 2007-2009, 2011-2019, 2021-2022 Free
+#   Software Foundation, Inc.
+#   Written by Gary V. Vaughan, 2004
+#
+# This file is free software; the Free Software Foundation gives
+# unlimited permission to copy and/or distribute it, with or without
+# modifications, as long as this notice is preserved.
+
+# serial 8 ltoptions.m4
+
+# This is to help aclocal find these macros, as it can't see m4_define.
+AC_DEFUN([LTOPTIONS_VERSION], [m4_if([1])])
+
+
+# _LT_MANGLE_OPTION(MACRO-NAME, OPTION-NAME)
+# ------------------------------------------
+m4_define([_LT_MANGLE_OPTION],
+[[_LT_OPTION_]m4_bpatsubst($1__$2, [[^a-zA-Z0-9_]], [_])])
+
+
+# _LT_SET_OPTION(MACRO-NAME, OPTION-NAME)
+# ---------------------------------------
+# Set option OPTION-NAME for macro MACRO-NAME, and if there is a
+# matching handler defined, dispatch to it.  Other OPTION-NAMEs are
+# saved as a flag.
+m4_define([_LT_SET_OPTION],
+[m4_define(_LT_MANGLE_OPTION([$1], [$2]))dnl
+m4_ifdef(_LT_MANGLE_DEFUN([$1], [$2]),
+        _LT_MANGLE_DEFUN([$1], [$2]),
+    [m4_warning([Unknown $1 option '$2'])])[]dnl
+])
+
+
+# _LT_IF_OPTION(MACRO-NAME, OPTION-NAME, IF-SET, [IF-NOT-SET])
+# ------------------------------------------------------------
+# Execute IF-SET if OPTION is set, IF-NOT-SET otherwise.
+m4_define([_LT_IF_OPTION],
+[m4_ifdef(_LT_MANGLE_OPTION([$1], [$2]), [$3], [$4])])
+
+
+# _LT_UNLESS_OPTIONS(MACRO-NAME, OPTION-LIST, IF-NOT-SET)
+# -------------------------------------------------------
+# Execute IF-NOT-SET unless all options in OPTION-LIST for MACRO-NAME
+# are set.
+m4_define([_LT_UNLESS_OPTIONS],
+[m4_foreach([_LT_Option], m4_split(m4_normalize([$2])),
+	    [m4_ifdef(_LT_MANGLE_OPTION([$1], _LT_Option),
+		      [m4_define([$0_found])])])[]dnl
+m4_ifdef([$0_found], [m4_undefine([$0_found])], [$3
+])[]dnl
+])
+
+
+# _LT_SET_OPTIONS(MACRO-NAME, OPTION-LIST)
+# ----------------------------------------
+# OPTION-LIST is a space-separated list of Libtool options associated
+# with MACRO-NAME.  If any OPTION has a matching handler declared with
+# LT_OPTION_DEFINE, dispatch to that macro; otherwise complain about
+# the unknown option and exit.
+m4_defun([_LT_SET_OPTIONS],
+[# Set options
+m4_foreach([_LT_Option], m4_split(m4_normalize([$2])),
+    [_LT_SET_OPTION([$1], _LT_Option)])
+
+m4_if([$1],[LT_INIT],[
+  dnl
+  dnl Simply set some default values (i.e off) if boolean options were not
+  dnl specified:
+  _LT_UNLESS_OPTIONS([LT_INIT], [dlopen], [enable_dlopen=no
+  ])
+  _LT_UNLESS_OPTIONS([LT_INIT], [win32-dll], [enable_win32_dll=no
+  ])
+  dnl
+  dnl If no reference was made to various pairs of opposing options, then
+  dnl we run the default mode handler for the pair.  For example, if neither
+  dnl 'shared' nor 'disable-shared' was passed, we enable building of shared
+  dnl archives by default:
+  _LT_UNLESS_OPTIONS([LT_INIT], [shared disable-shared], [_LT_ENABLE_SHARED])
+  _LT_UNLESS_OPTIONS([LT_INIT], [static disable-static], [_LT_ENABLE_STATIC])
+  _LT_UNLESS_OPTIONS([LT_INIT], [pic-only no-pic], [_LT_WITH_PIC])
+  _LT_UNLESS_OPTIONS([LT_INIT], [fast-install disable-fast-install],
+		   [_LT_ENABLE_FAST_INSTALL])
+  _LT_UNLESS_OPTIONS([LT_INIT], [aix-soname=aix aix-soname=both aix-soname=svr4],
+		   [_LT_WITH_AIX_SONAME([aix])])
+  ])
+])# _LT_SET_OPTIONS
+
+
+
+# _LT_MANGLE_DEFUN(MACRO-NAME, OPTION-NAME)
+# -----------------------------------------
+m4_define([_LT_MANGLE_DEFUN],
+[[_LT_OPTION_DEFUN_]m4_bpatsubst(m4_toupper([$1__$2]), [[^A-Z0-9_]], [_])])
+
+
+# LT_OPTION_DEFINE(MACRO-NAME, OPTION-NAME, CODE)
+# -----------------------------------------------
+m4_define([LT_OPTION_DEFINE],
+[m4_define(_LT_MANGLE_DEFUN([$1], [$2]), [$3])[]dnl
+])# LT_OPTION_DEFINE
+
+
+# dlopen
+# ------
+LT_OPTION_DEFINE([LT_INIT], [dlopen], [enable_dlopen=yes
+])
+
+AU_DEFUN([AC_LIBTOOL_DLOPEN],
+[_LT_SET_OPTION([LT_INIT], [dlopen])
+AC_DIAGNOSE([obsolete],
+[$0: Remove this warning and the call to _LT_SET_OPTION when you
+put the 'dlopen' option into LT_INIT's first parameter.])
+])
+
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_LIBTOOL_DLOPEN], [])
+
+
+# win32-dll
+# ---------
+# Declare package support for building win32 dll's.
+LT_OPTION_DEFINE([LT_INIT], [win32-dll],
+[enable_win32_dll=yes
+
+case $host in
+*-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-cegcc*)
+  AC_CHECK_TOOL(AS, as, false)
+  AC_CHECK_TOOL(DLLTOOL, dlltool, false)
+  AC_CHECK_TOOL(OBJDUMP, objdump, false)
+  ;;
+esac
+
+test -z "$AS" && AS=as
+_LT_DECL([], [AS],      [1], [Assembler program])dnl
+
+test -z "$DLLTOOL" && DLLTOOL=dlltool
+_LT_DECL([], [DLLTOOL], [1], [DLL creation program])dnl
+
+test -z "$OBJDUMP" && OBJDUMP=objdump
+_LT_DECL([], [OBJDUMP], [1], [Object dumper program])dnl
+])# win32-dll
+
+AU_DEFUN([AC_LIBTOOL_WIN32_DLL],
+[AC_REQUIRE([AC_CANONICAL_HOST])dnl
+_LT_SET_OPTION([LT_INIT], [win32-dll])
+AC_DIAGNOSE([obsolete],
+[$0: Remove this warning and the call to _LT_SET_OPTION when you
+put the 'win32-dll' option into LT_INIT's first parameter.])
+])
+
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_LIBTOOL_WIN32_DLL], [])
+
+
+# _LT_ENABLE_SHARED([DEFAULT])
+# ----------------------------
+# implement the --enable-shared flag, and supports the 'shared' and
+# 'disable-shared' LT_INIT options.
+# DEFAULT is either 'yes' or 'no'.  If omitted, it defaults to 'yes'.
+m4_define([_LT_ENABLE_SHARED],
+[m4_define([_LT_ENABLE_SHARED_DEFAULT], [m4_if($1, no, no, yes)])dnl
+AC_ARG_ENABLE([shared],
+    [AS_HELP_STRING([--enable-shared@<:@=PKGS@:>@],
+	[build shared libraries @<:@default=]_LT_ENABLE_SHARED_DEFAULT[@:>@])],
+    [p=${PACKAGE-default}
+    case $enableval in
+    yes) enable_shared=yes ;;
+    no) enable_shared=no ;;
+    *)
+      enable_shared=no
+      # Look at the argument we got.  We use all the common list separators.
+      lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR,
+      for pkg in $enableval; do
+	IFS=$lt_save_ifs
+	if test "X$pkg" = "X$p"; then
+	  enable_shared=yes
+	fi
+      done
+      IFS=$lt_save_ifs
+      ;;
+    esac],
+    [enable_shared=]_LT_ENABLE_SHARED_DEFAULT)
+
+    _LT_DECL([build_libtool_libs], [enable_shared], [0],
+	[Whether or not to build shared libraries])
+])# _LT_ENABLE_SHARED
+
+LT_OPTION_DEFINE([LT_INIT], [shared], [_LT_ENABLE_SHARED([yes])])
+LT_OPTION_DEFINE([LT_INIT], [disable-shared], [_LT_ENABLE_SHARED([no])])
+
+# Old names:
+AC_DEFUN([AC_ENABLE_SHARED],
+[_LT_SET_OPTION([LT_INIT], m4_if([$1], [no], [disable-])[shared])
+])
+
+AC_DEFUN([AC_DISABLE_SHARED],
+[_LT_SET_OPTION([LT_INIT], [disable-shared])
+])
+
+AU_DEFUN([AM_ENABLE_SHARED], [AC_ENABLE_SHARED($@)])
+AU_DEFUN([AM_DISABLE_SHARED], [AC_DISABLE_SHARED($@)])
+
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AM_ENABLE_SHARED], [])
+dnl AC_DEFUN([AM_DISABLE_SHARED], [])
+
+
+
+# _LT_ENABLE_STATIC([DEFAULT])
+# ----------------------------
+# implement the --enable-static flag, and support the 'static' and
+# 'disable-static' LT_INIT options.
+# DEFAULT is either 'yes' or 'no'.  If omitted, it defaults to 'yes'.
+m4_define([_LT_ENABLE_STATIC],
+[m4_define([_LT_ENABLE_STATIC_DEFAULT], [m4_if($1, no, no, yes)])dnl
+AC_ARG_ENABLE([static],
+    [AS_HELP_STRING([--enable-static@<:@=PKGS@:>@],
+	[build static libraries @<:@default=]_LT_ENABLE_STATIC_DEFAULT[@:>@])],
+    [p=${PACKAGE-default}
+    case $enableval in
+    yes) enable_static=yes ;;
+    no) enable_static=no ;;
+    *)
+     enable_static=no
+      # Look at the argument we got.  We use all the common list separators.
+      lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR,
+      for pkg in $enableval; do
+	IFS=$lt_save_ifs
+	if test "X$pkg" = "X$p"; then
+	  enable_static=yes
+	fi
+      done
+      IFS=$lt_save_ifs
+      ;;
+    esac],
+    [enable_static=]_LT_ENABLE_STATIC_DEFAULT)
+
+    _LT_DECL([build_old_libs], [enable_static], [0],
+	[Whether or not to build static libraries])
+])# _LT_ENABLE_STATIC
+
+LT_OPTION_DEFINE([LT_INIT], [static], [_LT_ENABLE_STATIC([yes])])
+LT_OPTION_DEFINE([LT_INIT], [disable-static], [_LT_ENABLE_STATIC([no])])
+
+# Old names:
+AC_DEFUN([AC_ENABLE_STATIC],
+[_LT_SET_OPTION([LT_INIT], m4_if([$1], [no], [disable-])[static])
+])
+
+AC_DEFUN([AC_DISABLE_STATIC],
+[_LT_SET_OPTION([LT_INIT], [disable-static])
+])
+
+AU_DEFUN([AM_ENABLE_STATIC], [AC_ENABLE_STATIC($@)])
+AU_DEFUN([AM_DISABLE_STATIC], [AC_DISABLE_STATIC($@)])
+
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AM_ENABLE_STATIC], [])
+dnl AC_DEFUN([AM_DISABLE_STATIC], [])
+
+
+
+# _LT_ENABLE_FAST_INSTALL([DEFAULT])
+# ----------------------------------
+# implement the --enable-fast-install flag, and support the 'fast-install'
+# and 'disable-fast-install' LT_INIT options.
+# DEFAULT is either 'yes' or 'no'.  If omitted, it defaults to 'yes'.
+m4_define([_LT_ENABLE_FAST_INSTALL],
+[m4_define([_LT_ENABLE_FAST_INSTALL_DEFAULT], [m4_if($1, no, no, yes)])dnl
+AC_ARG_ENABLE([fast-install],
+    [AS_HELP_STRING([--enable-fast-install@<:@=PKGS@:>@],
+    [optimize for fast installation @<:@default=]_LT_ENABLE_FAST_INSTALL_DEFAULT[@:>@])],
+    [p=${PACKAGE-default}
+    case $enableval in
+    yes) enable_fast_install=yes ;;
+    no) enable_fast_install=no ;;
+    *)
+      enable_fast_install=no
+      # Look at the argument we got.  We use all the common list separators.
+      lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR,
+      for pkg in $enableval; do
+	IFS=$lt_save_ifs
+	if test "X$pkg" = "X$p"; then
+	  enable_fast_install=yes
+	fi
+      done
+      IFS=$lt_save_ifs
+      ;;
+    esac],
+    [enable_fast_install=]_LT_ENABLE_FAST_INSTALL_DEFAULT)
+
+_LT_DECL([fast_install], [enable_fast_install], [0],
+	 [Whether or not to optimize for fast installation])dnl
+])# _LT_ENABLE_FAST_INSTALL
+
+LT_OPTION_DEFINE([LT_INIT], [fast-install], [_LT_ENABLE_FAST_INSTALL([yes])])
+LT_OPTION_DEFINE([LT_INIT], [disable-fast-install], [_LT_ENABLE_FAST_INSTALL([no])])
+
+# Old names:
+AU_DEFUN([AC_ENABLE_FAST_INSTALL],
+[_LT_SET_OPTION([LT_INIT], m4_if([$1], [no], [disable-])[fast-install])
+AC_DIAGNOSE([obsolete],
+[$0: Remove this warning and the call to _LT_SET_OPTION when you put
+the 'fast-install' option into LT_INIT's first parameter.])
+])
+
+AU_DEFUN([AC_DISABLE_FAST_INSTALL],
+[_LT_SET_OPTION([LT_INIT], [disable-fast-install])
+AC_DIAGNOSE([obsolete],
+[$0: Remove this warning and the call to _LT_SET_OPTION when you put
+the 'disable-fast-install' option into LT_INIT's first parameter.])
+])
+
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_ENABLE_FAST_INSTALL], [])
+dnl AC_DEFUN([AM_DISABLE_FAST_INSTALL], [])
+
+
+# _LT_WITH_AIX_SONAME([DEFAULT])
+# ----------------------------------
+# implement the --with-aix-soname flag, and support the `aix-soname=aix'
+# and `aix-soname=both' and `aix-soname=svr4' LT_INIT options. DEFAULT
+# is either `aix', `both' or `svr4'.  If omitted, it defaults to `aix'.
+m4_define([_LT_WITH_AIX_SONAME],
+[m4_define([_LT_WITH_AIX_SONAME_DEFAULT], [m4_if($1, svr4, svr4, m4_if($1, both, both, aix))])dnl
+shared_archive_member_spec=
+case $host,$enable_shared in
+power*-*-aix[[5-9]]*,yes)
+  AC_MSG_CHECKING([which variant of shared library versioning to provide])
+  AC_ARG_WITH([aix-soname],
+    [AS_HELP_STRING([--with-aix-soname=aix|svr4|both],
+      [shared library versioning (aka "SONAME") variant to provide on AIX, @<:@default=]_LT_WITH_AIX_SONAME_DEFAULT[@:>@.])],
+    [case $withval in
+    aix|svr4|both)
+      ;;
+    *)
+      AC_MSG_ERROR([Unknown argument to --with-aix-soname])
+      ;;
+    esac
+    lt_cv_with_aix_soname=$with_aix_soname],
+    [AC_CACHE_VAL([lt_cv_with_aix_soname],
+      [lt_cv_with_aix_soname=]_LT_WITH_AIX_SONAME_DEFAULT)
+    with_aix_soname=$lt_cv_with_aix_soname])
+  AC_MSG_RESULT([$with_aix_soname])
+  if test aix != "$with_aix_soname"; then
+    # For the AIX way of multilib, we name the shared archive member
+    # based on the bitwidth used, traditionally 'shr.o' or 'shr_64.o',
+    # and 'shr.imp' or 'shr_64.imp', respectively, for the Import File.
+    # Even when GNU compilers ignore OBJECT_MODE but need '-maix64' flag,
+    # the AIX toolchain works better with OBJECT_MODE set (default 32).
+    if test 64 = "${OBJECT_MODE-32}"; then
+      shared_archive_member_spec=shr_64
+    else
+      shared_archive_member_spec=shr
+    fi
+  fi
+  ;;
+*)
+  with_aix_soname=aix
+  ;;
+esac
+
+_LT_DECL([], [shared_archive_member_spec], [0],
+    [Shared archive member basename, for filename based shared library versioning on AIX])dnl
+])# _LT_WITH_AIX_SONAME
+
+LT_OPTION_DEFINE([LT_INIT], [aix-soname=aix], [_LT_WITH_AIX_SONAME([aix])])
+LT_OPTION_DEFINE([LT_INIT], [aix-soname=both], [_LT_WITH_AIX_SONAME([both])])
+LT_OPTION_DEFINE([LT_INIT], [aix-soname=svr4], [_LT_WITH_AIX_SONAME([svr4])])
+
+
+# _LT_WITH_PIC([MODE])
+# --------------------
+# implement the --with-pic flag, and support the 'pic-only' and 'no-pic'
+# LT_INIT options.
+# MODE is either 'yes' or 'no'.  If omitted, it defaults to 'both'.
+m4_define([_LT_WITH_PIC],
+[AC_ARG_WITH([pic],
+    [AS_HELP_STRING([--with-pic@<:@=PKGS@:>@],
+	[try to use only PIC/non-PIC objects @<:@default=use both@:>@])],
+    [lt_p=${PACKAGE-default}
+    case $withval in
+    yes|no) pic_mode=$withval ;;
+    *)
+      pic_mode=default
+      # Look at the argument we got.  We use all the common list separators.
+      lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR,
+      for lt_pkg in $withval; do
+	IFS=$lt_save_ifs
+	if test "X$lt_pkg" = "X$lt_p"; then
+	  pic_mode=yes
+	fi
+      done
+      IFS=$lt_save_ifs
+      ;;
+    esac],
+    [pic_mode=m4_default([$1], [default])])
+
+_LT_DECL([], [pic_mode], [0], [What type of objects to build])dnl
+])# _LT_WITH_PIC
+
+LT_OPTION_DEFINE([LT_INIT], [pic-only], [_LT_WITH_PIC([yes])])
+LT_OPTION_DEFINE([LT_INIT], [no-pic], [_LT_WITH_PIC([no])])
+
+# Old name:
+AU_DEFUN([AC_LIBTOOL_PICMODE],
+[_LT_SET_OPTION([LT_INIT], [pic-only])
+AC_DIAGNOSE([obsolete],
+[$0: Remove this warning and the call to _LT_SET_OPTION when you
+put the 'pic-only' option into LT_INIT's first parameter.])
+])
+
+dnl aclocal-1.4 backwards compatibility:
+dnl AC_DEFUN([AC_LIBTOOL_PICMODE], [])
+
+
+m4_define([_LTDL_MODE], [])
+LT_OPTION_DEFINE([LTDL_INIT], [nonrecursive],
+		 [m4_define([_LTDL_MODE], [nonrecursive])])
+LT_OPTION_DEFINE([LTDL_INIT], [recursive],
+		 [m4_define([_LTDL_MODE], [recursive])])
+LT_OPTION_DEFINE([LTDL_INIT], [subproject],
+		 [m4_define([_LTDL_MODE], [subproject])])
+
+m4_define([_LTDL_TYPE], [])
+LT_OPTION_DEFINE([LTDL_INIT], [installable],
+		 [m4_define([_LTDL_TYPE], [installable])])
+LT_OPTION_DEFINE([LTDL_INIT], [convenience],
+		 [m4_define([_LTDL_TYPE], [convenience])])
+
+# ltsugar.m4 -- libtool m4 base layer.                         -*-Autoconf-*-
+#
+# Copyright (C) 2004-2005, 2007-2008, 2011-2019, 2021-2022 Free Software
+# Foundation, Inc.
+# Written by Gary V. Vaughan, 2004
+#
+# This file is free software; the Free Software Foundation gives
+# unlimited permission to copy and/or distribute it, with or without
+# modifications, as long as this notice is preserved.
+
+# serial 6 ltsugar.m4
+
+# This is to help aclocal find these macros, as it can't see m4_define.
+AC_DEFUN([LTSUGAR_VERSION], [m4_if([0.1])])
+
+
+# lt_join(SEP, ARG1, [ARG2...])
+# -----------------------------
+# Produce ARG1SEPARG2...SEPARGn, omitting [] arguments and their
+# associated separator.
+# Needed until we can rely on m4_join from Autoconf 2.62, since all earlier
+# versions in m4sugar had bugs.
+m4_define([lt_join],
+[m4_if([$#], [1], [],
+       [$#], [2], [[$2]],
+       [m4_if([$2], [], [], [[$2]_])$0([$1], m4_shift(m4_shift($@)))])])
+m4_define([_lt_join],
+[m4_if([$#$2], [2], [],
+       [m4_if([$2], [], [], [[$1$2]])$0([$1], m4_shift(m4_shift($@)))])])
+
+
+# lt_car(LIST)
+# lt_cdr(LIST)
+# ------------
+# Manipulate m4 lists.
+# These macros are necessary as long as will still need to support
+# Autoconf-2.59, which quotes differently.
+m4_define([lt_car], [[$1]])
+m4_define([lt_cdr],
+[m4_if([$#], 0, [m4_fatal([$0: cannot be called without arguments])],
+       [$#], 1, [],
+       [m4_dquote(m4_shift($@))])])
+m4_define([lt_unquote], $1)
+
+
+# lt_append(MACRO-NAME, STRING, [SEPARATOR])
+# ------------------------------------------
+# Redefine MACRO-NAME to hold its former content plus 'SEPARATOR''STRING'.
+# Note that neither SEPARATOR nor STRING are expanded; they are appended
+# to MACRO-NAME as is (leaving the expansion for when MACRO-NAME is invoked).
+# No SEPARATOR is output if MACRO-NAME was previously undefined (different
+# than defined and empty).
+#
+# This macro is needed until we can rely on Autoconf 2.62, since earlier
+# versions of m4sugar mistakenly expanded SEPARATOR but not STRING.
+m4_define([lt_append],
+[m4_define([$1],
+	   m4_ifdef([$1], [m4_defn([$1])[$3]])[$2])])
+
+
+
+# lt_combine(SEP, PREFIX-LIST, INFIX, SUFFIX1, [SUFFIX2...])
+# ----------------------------------------------------------
+# Produce a SEP delimited list of all paired combinations of elements of
+# PREFIX-LIST with SUFFIX1 through SUFFIXn.  Each element of the list
+# has the form PREFIXmINFIXSUFFIXn.
+# Needed until we can rely on m4_combine added in Autoconf 2.62.
+m4_define([lt_combine],
+[m4_if(m4_eval([$# > 3]), [1],
+       [m4_pushdef([_Lt_sep], [m4_define([_Lt_sep], m4_defn([lt_car]))])]]dnl
+[[m4_foreach([_Lt_prefix], [$2],
+	     [m4_foreach([_Lt_suffix],
+		]m4_dquote(m4_dquote(m4_shift(m4_shift(m4_shift($@)))))[,
+	[_Lt_sep([$1])[]m4_defn([_Lt_prefix])[$3]m4_defn([_Lt_suffix])])])])])
+
+
+# lt_if_append_uniq(MACRO-NAME, VARNAME, [SEPARATOR], [UNIQ], [NOT-UNIQ])
+# -----------------------------------------------------------------------
+# Iff MACRO-NAME does not yet contain VARNAME, then append it (delimited
+# by SEPARATOR if supplied) and expand UNIQ, else NOT-UNIQ.
+m4_define([lt_if_append_uniq],
+[m4_ifdef([$1],
+	  [m4_if(m4_index([$3]m4_defn([$1])[$3], [$3$2$3]), [-1],
+		 [lt_append([$1], [$2], [$3])$4],
+		 [$5])],
+	  [lt_append([$1], [$2], [$3])$4])])
+
+
+# lt_dict_add(DICT, KEY, VALUE)
+# -----------------------------
+m4_define([lt_dict_add],
+[m4_define([$1($2)], [$3])])
+
+
+# lt_dict_add_subkey(DICT, KEY, SUBKEY, VALUE)
+# --------------------------------------------
+m4_define([lt_dict_add_subkey],
+[m4_define([$1($2:$3)], [$4])])
+
+
+# lt_dict_fetch(DICT, KEY, [SUBKEY])
+# ----------------------------------
+m4_define([lt_dict_fetch],
+[m4_ifval([$3],
+	m4_ifdef([$1($2:$3)], [m4_defn([$1($2:$3)])]),
+    m4_ifdef([$1($2)], [m4_defn([$1($2)])]))])
+
+
+# lt_if_dict_fetch(DICT, KEY, [SUBKEY], VALUE, IF-TRUE, [IF-FALSE])
+# -----------------------------------------------------------------
+m4_define([lt_if_dict_fetch],
+[m4_if(lt_dict_fetch([$1], [$2], [$3]), [$4],
+	[$5],
+    [$6])])
+
+
+# lt_dict_filter(DICT, [SUBKEY], VALUE, [SEPARATOR], KEY, [...])
+# --------------------------------------------------------------
+m4_define([lt_dict_filter],
+[m4_if([$5], [], [],
+  [lt_join(m4_quote(m4_default([$4], [[, ]])),
+           lt_unquote(m4_split(m4_normalize(m4_foreach(_Lt_key, lt_car([m4_shiftn(4, $@)]),
+		      [lt_if_dict_fetch([$1], _Lt_key, [$2], [$3], [_Lt_key ])])))))])[]dnl
+])
+
+# ltversion.m4 -- version numbers			-*- Autoconf -*-
+#
+#   Copyright (C) 2004, 2011-2019, 2021-2022 Free Software Foundation,
+#   Inc.
+#   Written by Scott James Remnant, 2004
+#
+# This file is free software; the Free Software Foundation gives
+# unlimited permission to copy and/or distribute it, with or without
+# modifications, as long as this notice is preserved.
+
+# @configure_input@
+
+# serial 4245 ltversion.m4
+# This file is part of GNU Libtool
+
+m4_define([LT_PACKAGE_VERSION], [2.4.7])
+m4_define([LT_PACKAGE_REVISION], [2.4.7])
+
+AC_DEFUN([LTVERSION_VERSION],
+[macro_version='2.4.7'
+macro_revision='2.4.7'
+_LT_DECL(, macro_version, 0, [Which release of libtool.m4 was used?])
+_LT_DECL(, macro_revision, 0)
+])
+
+# lt~obsolete.m4 -- aclocal satisfying obsolete definitions.    -*-Autoconf-*-
+#
+#   Copyright (C) 2004-2005, 2007, 2009, 2011-2019, 2021-2022 Free
+#   Software Foundation, Inc.
+#   Written by Scott James Remnant, 2004.
+#
+# This file is free software; the Free Software Foundation gives
+# unlimited permission to copy and/or distribute it, with or without
+# modifications, as long as this notice is preserved.
+
+# serial 5 lt~obsolete.m4
+
+# These exist entirely to fool aclocal when bootstrapping libtool.
+#
+# In the past libtool.m4 has provided macros via AC_DEFUN (or AU_DEFUN),
+# which have later been changed to m4_define as they aren't part of the
+# exported API, or moved to Autoconf or Automake where they belong.
+#
+# The trouble is, aclocal is a bit thick.  It'll see the old AC_DEFUN
+# in /usr/share/aclocal/libtool.m4 and remember it, then when it sees us
+# using a macro with the same name in our local m4/libtool.m4 it'll
+# pull the old libtool.m4 in (it doesn't see our shiny new m4_define
+# and doesn't know about Autoconf macros at all.)
+#
+# So we provide this file, which has a silly filename so it's always
+# included after everything else.  This provides aclocal with the
+# AC_DEFUNs it wants, but when m4 processes it, it doesn't do anything
+# because those macros already exist, or will be overwritten later.
+# We use AC_DEFUN over AU_DEFUN for compatibility with aclocal-1.6.
+#
+# Anytime we withdraw an AC_DEFUN or AU_DEFUN, remember to add it here.
+# Yes, that means every name once taken will need to remain here until
+# we give up compatibility with versions before 1.7, at which point
+# we need to keep only those names which we still refer to.
+
+# This is to help aclocal find these macros, as it can't see m4_define.
+AC_DEFUN([LTOBSOLETE_VERSION], [m4_if([1])])
+
+m4_ifndef([AC_LIBTOOL_LINKER_OPTION],	[AC_DEFUN([AC_LIBTOOL_LINKER_OPTION])])
+m4_ifndef([AC_PROG_EGREP],		[AC_DEFUN([AC_PROG_EGREP])])
+m4_ifndef([_LT_AC_PROG_ECHO_BACKSLASH],	[AC_DEFUN([_LT_AC_PROG_ECHO_BACKSLASH])])
+m4_ifndef([_LT_AC_SHELL_INIT],		[AC_DEFUN([_LT_AC_SHELL_INIT])])
+m4_ifndef([_LT_AC_SYS_LIBPATH_AIX],	[AC_DEFUN([_LT_AC_SYS_LIBPATH_AIX])])
+m4_ifndef([_LT_PROG_LTMAIN],		[AC_DEFUN([_LT_PROG_LTMAIN])])
+m4_ifndef([_LT_AC_TAGVAR],		[AC_DEFUN([_LT_AC_TAGVAR])])
+m4_ifndef([AC_LTDL_ENABLE_INSTALL],	[AC_DEFUN([AC_LTDL_ENABLE_INSTALL])])
+m4_ifndef([AC_LTDL_PREOPEN],		[AC_DEFUN([AC_LTDL_PREOPEN])])
+m4_ifndef([_LT_AC_SYS_COMPILER],	[AC_DEFUN([_LT_AC_SYS_COMPILER])])
+m4_ifndef([_LT_AC_LOCK],		[AC_DEFUN([_LT_AC_LOCK])])
+m4_ifndef([AC_LIBTOOL_SYS_OLD_ARCHIVE],	[AC_DEFUN([AC_LIBTOOL_SYS_OLD_ARCHIVE])])
+m4_ifndef([_LT_AC_TRY_DLOPEN_SELF],	[AC_DEFUN([_LT_AC_TRY_DLOPEN_SELF])])
+m4_ifndef([AC_LIBTOOL_PROG_CC_C_O],	[AC_DEFUN([AC_LIBTOOL_PROG_CC_C_O])])
+m4_ifndef([AC_LIBTOOL_SYS_HARD_LINK_LOCKS], [AC_DEFUN([AC_LIBTOOL_SYS_HARD_LINK_LOCKS])])
+m4_ifndef([AC_LIBTOOL_OBJDIR],		[AC_DEFUN([AC_LIBTOOL_OBJDIR])])
+m4_ifndef([AC_LTDL_OBJDIR],		[AC_DEFUN([AC_LTDL_OBJDIR])])
+m4_ifndef([AC_LIBTOOL_PROG_LD_HARDCODE_LIBPATH], [AC_DEFUN([AC_LIBTOOL_PROG_LD_HARDCODE_LIBPATH])])
+m4_ifndef([AC_LIBTOOL_SYS_LIB_STRIP],	[AC_DEFUN([AC_LIBTOOL_SYS_LIB_STRIP])])
+m4_ifndef([AC_PATH_MAGIC],		[AC_DEFUN([AC_PATH_MAGIC])])
+m4_ifndef([AC_PROG_LD_GNU],		[AC_DEFUN([AC_PROG_LD_GNU])])
+m4_ifndef([AC_PROG_LD_RELOAD_FLAG],	[AC_DEFUN([AC_PROG_LD_RELOAD_FLAG])])
+m4_ifndef([AC_DEPLIBS_CHECK_METHOD],	[AC_DEFUN([AC_DEPLIBS_CHECK_METHOD])])
+m4_ifndef([AC_LIBTOOL_PROG_COMPILER_NO_RTTI], [AC_DEFUN([AC_LIBTOOL_PROG_COMPILER_NO_RTTI])])
+m4_ifndef([AC_LIBTOOL_SYS_GLOBAL_SYMBOL_PIPE], [AC_DEFUN([AC_LIBTOOL_SYS_GLOBAL_SYMBOL_PIPE])])
+m4_ifndef([AC_LIBTOOL_PROG_COMPILER_PIC], [AC_DEFUN([AC_LIBTOOL_PROG_COMPILER_PIC])])
+m4_ifndef([AC_LIBTOOL_PROG_LD_SHLIBS],	[AC_DEFUN([AC_LIBTOOL_PROG_LD_SHLIBS])])
+m4_ifndef([AC_LIBTOOL_POSTDEP_PREDEP],	[AC_DEFUN([AC_LIBTOOL_POSTDEP_PREDEP])])
+m4_ifndef([LT_AC_PROG_EGREP],		[AC_DEFUN([LT_AC_PROG_EGREP])])
+m4_ifndef([LT_AC_PROG_SED],		[AC_DEFUN([LT_AC_PROG_SED])])
+m4_ifndef([_LT_CC_BASENAME],		[AC_DEFUN([_LT_CC_BASENAME])])
+m4_ifndef([_LT_COMPILER_BOILERPLATE],	[AC_DEFUN([_LT_COMPILER_BOILERPLATE])])
+m4_ifndef([_LT_LINKER_BOILERPLATE],	[AC_DEFUN([_LT_LINKER_BOILERPLATE])])
+m4_ifndef([_AC_PROG_LIBTOOL],		[AC_DEFUN([_AC_PROG_LIBTOOL])])
+m4_ifndef([AC_LIBTOOL_SETUP],		[AC_DEFUN([AC_LIBTOOL_SETUP])])
+m4_ifndef([_LT_AC_CHECK_DLFCN],		[AC_DEFUN([_LT_AC_CHECK_DLFCN])])
+m4_ifndef([AC_LIBTOOL_SYS_DYNAMIC_LINKER],	[AC_DEFUN([AC_LIBTOOL_SYS_DYNAMIC_LINKER])])
+m4_ifndef([_LT_AC_TAGCONFIG],		[AC_DEFUN([_LT_AC_TAGCONFIG])])
+m4_ifndef([AC_DISABLE_FAST_INSTALL],	[AC_DEFUN([AC_DISABLE_FAST_INSTALL])])
+m4_ifndef([_LT_AC_LANG_CXX],		[AC_DEFUN([_LT_AC_LANG_CXX])])
+m4_ifndef([_LT_AC_LANG_F77],		[AC_DEFUN([_LT_AC_LANG_F77])])
+m4_ifndef([_LT_AC_LANG_GCJ],		[AC_DEFUN([_LT_AC_LANG_GCJ])])
+m4_ifndef([AC_LIBTOOL_LANG_C_CONFIG],	[AC_DEFUN([AC_LIBTOOL_LANG_C_CONFIG])])
+m4_ifndef([_LT_AC_LANG_C_CONFIG],	[AC_DEFUN([_LT_AC_LANG_C_CONFIG])])
+m4_ifndef([AC_LIBTOOL_LANG_CXX_CONFIG],	[AC_DEFUN([AC_LIBTOOL_LANG_CXX_CONFIG])])
+m4_ifndef([_LT_AC_LANG_CXX_CONFIG],	[AC_DEFUN([_LT_AC_LANG_CXX_CONFIG])])
+m4_ifndef([AC_LIBTOOL_LANG_F77_CONFIG],	[AC_DEFUN([AC_LIBTOOL_LANG_F77_CONFIG])])
+m4_ifndef([_LT_AC_LANG_F77_CONFIG],	[AC_DEFUN([_LT_AC_LANG_F77_CONFIG])])
+m4_ifndef([AC_LIBTOOL_LANG_GCJ_CONFIG],	[AC_DEFUN([AC_LIBTOOL_LANG_GCJ_CONFIG])])
+m4_ifndef([_LT_AC_LANG_GCJ_CONFIG],	[AC_DEFUN([_LT_AC_LANG_GCJ_CONFIG])])
+m4_ifndef([AC_LIBTOOL_LANG_RC_CONFIG],	[AC_DEFUN([AC_LIBTOOL_LANG_RC_CONFIG])])
+m4_ifndef([_LT_AC_LANG_RC_CONFIG],	[AC_DEFUN([_LT_AC_LANG_RC_CONFIG])])
+m4_ifndef([AC_LIBTOOL_CONFIG],		[AC_DEFUN([AC_LIBTOOL_CONFIG])])
+m4_ifndef([_LT_AC_FILE_LTDLL_C],	[AC_DEFUN([_LT_AC_FILE_LTDLL_C])])
+m4_ifndef([_LT_REQUIRED_DARWIN_CHECKS],	[AC_DEFUN([_LT_REQUIRED_DARWIN_CHECKS])])
+m4_ifndef([_LT_AC_PROG_CXXCPP],		[AC_DEFUN([_LT_AC_PROG_CXXCPP])])
+m4_ifndef([_LT_PREPARE_SED_QUOTE_VARS],	[AC_DEFUN([_LT_PREPARE_SED_QUOTE_VARS])])
+m4_ifndef([_LT_PROG_ECHO_BACKSLASH],	[AC_DEFUN([_LT_PROG_ECHO_BACKSLASH])])
+m4_ifndef([_LT_PROG_F77],		[AC_DEFUN([_LT_PROG_F77])])
+m4_ifndef([_LT_PROG_FC],		[AC_DEFUN([_LT_PROG_FC])])
+m4_ifndef([_LT_PROG_CXX],		[AC_DEFUN([_LT_PROG_CXX])])
+
+# Copyright (C) 2002-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_AUTOMAKE_VERSION(VERSION)
+# ----------------------------
+# Automake X.Y traces this macro to ensure aclocal.m4 has been
+# generated from the m4 files accompanying Automake X.Y.
+# (This private macro should not be called outside this file.)
+AC_DEFUN([AM_AUTOMAKE_VERSION],
+[am__api_version='1.16'
+dnl Some users find AM_AUTOMAKE_VERSION and mistake it for a way to
+dnl require some minimum version.  Point them to the right macro.
+m4_if([$1], [1.16.5], [],
+      [AC_FATAL([Do not call $0, use AM_INIT_AUTOMAKE([$1]).])])dnl
+])
+
+# _AM_AUTOCONF_VERSION(VERSION)
+# -----------------------------
+# aclocal traces this macro to find the Autoconf version.
+# This is a private macro too.  Using m4_define simplifies
+# the logic in aclocal, which can simply ignore this definition.
+m4_define([_AM_AUTOCONF_VERSION], [])
+
+# AM_SET_CURRENT_AUTOMAKE_VERSION
+# -------------------------------
+# Call AM_AUTOMAKE_VERSION and AM_AUTOMAKE_VERSION so they can be traced.
+# This function is AC_REQUIREd by AM_INIT_AUTOMAKE.
+AC_DEFUN([AM_SET_CURRENT_AUTOMAKE_VERSION],
+[AM_AUTOMAKE_VERSION([1.16.5])dnl
+m4_ifndef([AC_AUTOCONF_VERSION],
+  [m4_copy([m4_PACKAGE_VERSION], [AC_AUTOCONF_VERSION])])dnl
+_AM_AUTOCONF_VERSION(m4_defn([AC_AUTOCONF_VERSION]))])
+
+# Copyright (C) 2011-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_PROG_AR([ACT-IF-FAIL])
+# -------------------------
+# Try to determine the archiver interface, and trigger the ar-lib wrapper
+# if it is needed.  If the detection of archiver interface fails, run
+# ACT-IF-FAIL (default is to abort configure with a proper error message).
+AC_DEFUN([AM_PROG_AR],
+[AC_BEFORE([$0], [LT_INIT])dnl
+AC_BEFORE([$0], [AC_PROG_LIBTOOL])dnl
+AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl
+AC_REQUIRE_AUX_FILE([ar-lib])dnl
+AC_CHECK_TOOLS([AR], [ar lib "link -lib"], [false])
+: ${AR=ar}
+
+AC_CACHE_CHECK([the archiver ($AR) interface], [am_cv_ar_interface],
+  [AC_LANG_PUSH([C])
+   am_cv_ar_interface=ar
+   AC_COMPILE_IFELSE([AC_LANG_SOURCE([[int some_variable = 0;]])],
+     [am_ar_try='$AR cru libconftest.a conftest.$ac_objext >&AS_MESSAGE_LOG_FD'
+      AC_TRY_EVAL([am_ar_try])
+      if test "$ac_status" -eq 0; then
+        am_cv_ar_interface=ar
+      else
+        am_ar_try='$AR -NOLOGO -OUT:conftest.lib conftest.$ac_objext >&AS_MESSAGE_LOG_FD'
+        AC_TRY_EVAL([am_ar_try])
+        if test "$ac_status" -eq 0; then
+          am_cv_ar_interface=lib
+        else
+          am_cv_ar_interface=unknown
+        fi
+      fi
+      rm -f conftest.lib libconftest.a
+     ])
+   AC_LANG_POP([C])])
+
+case $am_cv_ar_interface in
+ar)
+  ;;
+lib)
+  # Microsoft lib, so override with the ar-lib wrapper script.
+  # FIXME: It is wrong to rewrite AR.
+  # But if we don't then we get into trouble of one sort or another.
+  # A longer-term fix would be to have automake use am__AR in this case,
+  # and then we could set am__AR="$am_aux_dir/ar-lib \$(AR)" or something
+  # similar.
+  AR="$am_aux_dir/ar-lib $AR"
+  ;;
+unknown)
+  m4_default([$1],
+             [AC_MSG_ERROR([could not determine $AR interface])])
+  ;;
+esac
+AC_SUBST([AR])dnl
+])
+
+# AM_AUX_DIR_EXPAND                                         -*- Autoconf -*-
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# For projects using AC_CONFIG_AUX_DIR([foo]), Autoconf sets
+# $ac_aux_dir to '$srcdir/foo'.  In other projects, it is set to
+# '$srcdir', '$srcdir/..', or '$srcdir/../..'.
+#
+# Of course, Automake must honor this variable whenever it calls a
+# tool from the auxiliary directory.  The problem is that $srcdir (and
+# therefore $ac_aux_dir as well) can be either absolute or relative,
+# depending on how configure is run.  This is pretty annoying, since
+# it makes $ac_aux_dir quite unusable in subdirectories: in the top
+# source directory, any form will work fine, but in subdirectories a
+# relative path needs to be adjusted first.
+#
+# $ac_aux_dir/missing
+#    fails when called from a subdirectory if $ac_aux_dir is relative
+# $top_srcdir/$ac_aux_dir/missing
+#    fails if $ac_aux_dir is absolute,
+#    fails when called from a subdirectory in a VPATH build with
+#          a relative $ac_aux_dir
+#
+# The reason of the latter failure is that $top_srcdir and $ac_aux_dir
+# are both prefixed by $srcdir.  In an in-source build this is usually
+# harmless because $srcdir is '.', but things will broke when you
+# start a VPATH build or use an absolute $srcdir.
+#
+# So we could use something similar to $top_srcdir/$ac_aux_dir/missing,
+# iff we strip the leading $srcdir from $ac_aux_dir.  That would be:
+#   am_aux_dir='\$(top_srcdir)/'`expr "$ac_aux_dir" : "$srcdir//*\(.*\)"`
+# and then we would define $MISSING as
+#   MISSING="\${SHELL} $am_aux_dir/missing"
+# This will work as long as MISSING is not called from configure, because
+# unfortunately $(top_srcdir) has no meaning in configure.
+# However there are other variables, like CC, which are often used in
+# configure, and could therefore not use this "fixed" $ac_aux_dir.
+#
+# Another solution, used here, is to always expand $ac_aux_dir to an
+# absolute PATH.  The drawback is that using absolute paths prevent a
+# configured tree to be moved without reconfiguration.
+
+AC_DEFUN([AM_AUX_DIR_EXPAND],
+[AC_REQUIRE([AC_CONFIG_AUX_DIR_DEFAULT])dnl
+# Expand $ac_aux_dir to an absolute path.
+am_aux_dir=`cd "$ac_aux_dir" && pwd`
+])
+
+# AM_CONDITIONAL                                            -*- Autoconf -*-
+
+# Copyright (C) 1997-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_CONDITIONAL(NAME, SHELL-CONDITION)
+# -------------------------------------
+# Define a conditional.
+AC_DEFUN([AM_CONDITIONAL],
+[AC_PREREQ([2.52])dnl
+ m4_if([$1], [TRUE],  [AC_FATAL([$0: invalid condition: $1])],
+       [$1], [FALSE], [AC_FATAL([$0: invalid condition: $1])])dnl
+AC_SUBST([$1_TRUE])dnl
+AC_SUBST([$1_FALSE])dnl
+_AM_SUBST_NOTMAKE([$1_TRUE])dnl
+_AM_SUBST_NOTMAKE([$1_FALSE])dnl
+m4_define([_AM_COND_VALUE_$1], [$2])dnl
+if $2; then
+  $1_TRUE=
+  $1_FALSE='#'
+else
+  $1_TRUE='#'
+  $1_FALSE=
+fi
+AC_CONFIG_COMMANDS_PRE(
+[if test -z "${$1_TRUE}" && test -z "${$1_FALSE}"; then
+  AC_MSG_ERROR([[conditional "$1" was never defined.
+Usually this means the macro was only invoked conditionally.]])
+fi])])
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+
+# There are a few dirty hacks below to avoid letting 'AC_PROG_CC' be
+# written in clear, in which case automake, when reading aclocal.m4,
+# will think it sees a *use*, and therefore will trigger all it's
+# C support machinery.  Also note that it means that autoscan, seeing
+# CC etc. in the Makefile, will ask for an AC_PROG_CC use...
+
+
+# _AM_DEPENDENCIES(NAME)
+# ----------------------
+# See how the compiler implements dependency checking.
+# NAME is "CC", "CXX", "OBJC", "OBJCXX", "UPC", or "GJC".
+# We try a few techniques and use that to set a single cache variable.
+#
+# We don't AC_REQUIRE the corresponding AC_PROG_CC since the latter was
+# modified to invoke _AM_DEPENDENCIES(CC); we would have a circular
+# dependency, and given that the user is not expected to run this macro,
+# just rely on AC_PROG_CC.
+AC_DEFUN([_AM_DEPENDENCIES],
+[AC_REQUIRE([AM_SET_DEPDIR])dnl
+AC_REQUIRE([AM_OUTPUT_DEPENDENCY_COMMANDS])dnl
+AC_REQUIRE([AM_MAKE_INCLUDE])dnl
+AC_REQUIRE([AM_DEP_TRACK])dnl
+
+m4_if([$1], [CC],   [depcc="$CC"   am_compiler_list=],
+      [$1], [CXX],  [depcc="$CXX"  am_compiler_list=],
+      [$1], [OBJC], [depcc="$OBJC" am_compiler_list='gcc3 gcc'],
+      [$1], [OBJCXX], [depcc="$OBJCXX" am_compiler_list='gcc3 gcc'],
+      [$1], [UPC],  [depcc="$UPC"  am_compiler_list=],
+      [$1], [GCJ],  [depcc="$GCJ"  am_compiler_list='gcc3 gcc'],
+                    [depcc="$$1"   am_compiler_list=])
+
+AC_CACHE_CHECK([dependency style of $depcc],
+               [am_cv_$1_dependencies_compiler_type],
+[if test -z "$AMDEP_TRUE" && test -f "$am_depcomp"; then
+  # We make a subdir and do the tests there.  Otherwise we can end up
+  # making bogus files that we don't know about and never remove.  For
+  # instance it was reported that on HP-UX the gcc test will end up
+  # making a dummy file named 'D' -- because '-MD' means "put the output
+  # in D".
+  rm -rf conftest.dir
+  mkdir conftest.dir
+  # Copy depcomp to subdir because otherwise we won't find it if we're
+  # using a relative directory.
+  cp "$am_depcomp" conftest.dir
+  cd conftest.dir
+  # We will build objects and dependencies in a subdirectory because
+  # it helps to detect inapplicable dependency modes.  For instance
+  # both Tru64's cc and ICC support -MD to output dependencies as a
+  # side effect of compilation, but ICC will put the dependencies in
+  # the current directory while Tru64 will put them in the object
+  # directory.
+  mkdir sub
+
+  am_cv_$1_dependencies_compiler_type=none
+  if test "$am_compiler_list" = ""; then
+     am_compiler_list=`sed -n ['s/^#*\([a-zA-Z0-9]*\))$/\1/p'] < ./depcomp`
+  fi
+  am__universal=false
+  m4_case([$1], [CC],
+    [case " $depcc " in #(
+     *\ -arch\ *\ -arch\ *) am__universal=true ;;
+     esac],
+    [CXX],
+    [case " $depcc " in #(
+     *\ -arch\ *\ -arch\ *) am__universal=true ;;
+     esac])
+
+  for depmode in $am_compiler_list; do
+    # Setup a source with many dependencies, because some compilers
+    # like to wrap large dependency lists on column 80 (with \), and
+    # we should not choose a depcomp mode which is confused by this.
+    #
+    # We need to recreate these files for each test, as the compiler may
+    # overwrite some of them when testing with obscure command lines.
+    # This happens at least with the AIX C compiler.
+    : > sub/conftest.c
+    for i in 1 2 3 4 5 6; do
+      echo '#include "conftst'$i'.h"' >> sub/conftest.c
+      # Using ": > sub/conftst$i.h" creates only sub/conftst1.h with
+      # Solaris 10 /bin/sh.
+      echo '/* dummy */' > sub/conftst$i.h
+    done
+    echo "${am__include} ${am__quote}sub/conftest.Po${am__quote}" > confmf
+
+    # We check with '-c' and '-o' for the sake of the "dashmstdout"
+    # mode.  It turns out that the SunPro C++ compiler does not properly
+    # handle '-M -o', and we need to detect this.  Also, some Intel
+    # versions had trouble with output in subdirs.
+    am__obj=sub/conftest.${OBJEXT-o}
+    am__minus_obj="-o $am__obj"
+    case $depmode in
+    gcc)
+      # This depmode causes a compiler race in universal mode.
+      test "$am__universal" = false || continue
+      ;;
+    nosideeffect)
+      # After this tag, mechanisms are not by side-effect, so they'll
+      # only be used when explicitly requested.
+      if test "x$enable_dependency_tracking" = xyes; then
+	continue
+      else
+	break
+      fi
+      ;;
+    msvc7 | msvc7msys | msvisualcpp | msvcmsys)
+      # This compiler won't grok '-c -o', but also, the minuso test has
+      # not run yet.  These depmodes are late enough in the game, and
+      # so weak that their functioning should not be impacted.
+      am__obj=conftest.${OBJEXT-o}
+      am__minus_obj=
+      ;;
+    none) break ;;
+    esac
+    if depmode=$depmode \
+       source=sub/conftest.c object=$am__obj \
+       depfile=sub/conftest.Po tmpdepfile=sub/conftest.TPo \
+       $SHELL ./depcomp $depcc -c $am__minus_obj sub/conftest.c \
+         >/dev/null 2>conftest.err &&
+       grep sub/conftst1.h sub/conftest.Po > /dev/null 2>&1 &&
+       grep sub/conftst6.h sub/conftest.Po > /dev/null 2>&1 &&
+       grep $am__obj sub/conftest.Po > /dev/null 2>&1 &&
+       ${MAKE-make} -s -f confmf > /dev/null 2>&1; then
+      # icc doesn't choke on unknown options, it will just issue warnings
+      # or remarks (even with -Werror).  So we grep stderr for any message
+      # that says an option was ignored or not supported.
+      # When given -MP, icc 7.0 and 7.1 complain thusly:
+      #   icc: Command line warning: ignoring option '-M'; no argument required
+      # The diagnosis changed in icc 8.0:
+      #   icc: Command line remark: option '-MP' not supported
+      if (grep 'ignoring option' conftest.err ||
+          grep 'not supported' conftest.err) >/dev/null 2>&1; then :; else
+        am_cv_$1_dependencies_compiler_type=$depmode
+        break
+      fi
+    fi
+  done
+
+  cd ..
+  rm -rf conftest.dir
+else
+  am_cv_$1_dependencies_compiler_type=none
+fi
+])
+AC_SUBST([$1DEPMODE], [depmode=$am_cv_$1_dependencies_compiler_type])
+AM_CONDITIONAL([am__fastdep$1], [
+  test "x$enable_dependency_tracking" != xno \
+  && test "$am_cv_$1_dependencies_compiler_type" = gcc3])
+])
+
+
+# AM_SET_DEPDIR
+# -------------
+# Choose a directory name for dependency files.
+# This macro is AC_REQUIREd in _AM_DEPENDENCIES.
+AC_DEFUN([AM_SET_DEPDIR],
+[AC_REQUIRE([AM_SET_LEADING_DOT])dnl
+AC_SUBST([DEPDIR], ["${am__leading_dot}deps"])dnl
+])
+
+
+# AM_DEP_TRACK
+# ------------
+AC_DEFUN([AM_DEP_TRACK],
+[AC_ARG_ENABLE([dependency-tracking], [dnl
+AS_HELP_STRING(
+  [--enable-dependency-tracking],
+  [do not reject slow dependency extractors])
+AS_HELP_STRING(
+  [--disable-dependency-tracking],
+  [speeds up one-time build])])
+if test "x$enable_dependency_tracking" != xno; then
+  am_depcomp="$ac_aux_dir/depcomp"
+  AMDEPBACKSLASH='\'
+  am__nodep='_no'
+fi
+AM_CONDITIONAL([AMDEP], [test "x$enable_dependency_tracking" != xno])
+AC_SUBST([AMDEPBACKSLASH])dnl
+_AM_SUBST_NOTMAKE([AMDEPBACKSLASH])dnl
+AC_SUBST([am__nodep])dnl
+_AM_SUBST_NOTMAKE([am__nodep])dnl
+])
+
+# Generate code to set up dependency tracking.              -*- Autoconf -*-
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_OUTPUT_DEPENDENCY_COMMANDS
+# ------------------------------
+AC_DEFUN([_AM_OUTPUT_DEPENDENCY_COMMANDS],
+[{
+  # Older Autoconf quotes --file arguments for eval, but not when files
+  # are listed without --file.  Let's play safe and only enable the eval
+  # if we detect the quoting.
+  # TODO: see whether this extra hack can be removed once we start
+  # requiring Autoconf 2.70 or later.
+  AS_CASE([$CONFIG_FILES],
+          [*\'*], [eval set x "$CONFIG_FILES"],
+          [*], [set x $CONFIG_FILES])
+  shift
+  # Used to flag and report bootstrapping failures.
+  am_rc=0
+  for am_mf
+  do
+    # Strip MF so we end up with the name of the file.
+    am_mf=`AS_ECHO(["$am_mf"]) | sed -e 's/:.*$//'`
+    # Check whether this is an Automake generated Makefile which includes
+    # dependency-tracking related rules and includes.
+    # Grep'ing the whole file directly is not great: AIX grep has a line
+    # limit of 2048, but all sed's we know have understand at least 4000.
+    sed -n 's,^am--depfiles:.*,X,p' "$am_mf" | grep X >/dev/null 2>&1 \
+      || continue
+    am_dirpart=`AS_DIRNAME(["$am_mf"])`
+    am_filepart=`AS_BASENAME(["$am_mf"])`
+    AM_RUN_LOG([cd "$am_dirpart" \
+      && sed -e '/# am--include-marker/d' "$am_filepart" \
+        | $MAKE -f - am--depfiles]) || am_rc=$?
+  done
+  if test $am_rc -ne 0; then
+    AC_MSG_FAILURE([Something went wrong bootstrapping makefile fragments
+    for automatic dependency tracking.  If GNU make was not used, consider
+    re-running the configure script with MAKE="gmake" (or whatever is
+    necessary).  You can also try re-running configure with the
+    '--disable-dependency-tracking' option to at least be able to build
+    the package (albeit without support for automatic dependency tracking).])
+  fi
+  AS_UNSET([am_dirpart])
+  AS_UNSET([am_filepart])
+  AS_UNSET([am_mf])
+  AS_UNSET([am_rc])
+  rm -f conftest-deps.mk
+}
+])# _AM_OUTPUT_DEPENDENCY_COMMANDS
+
+
+# AM_OUTPUT_DEPENDENCY_COMMANDS
+# -----------------------------
+# This macro should only be invoked once -- use via AC_REQUIRE.
+#
+# This code is only required when automatic dependency tracking is enabled.
+# This creates each '.Po' and '.Plo' makefile fragment that we'll need in
+# order to bootstrap the dependency handling code.
+AC_DEFUN([AM_OUTPUT_DEPENDENCY_COMMANDS],
+[AC_CONFIG_COMMANDS([depfiles],
+     [test x"$AMDEP_TRUE" != x"" || _AM_OUTPUT_DEPENDENCY_COMMANDS],
+     [AMDEP_TRUE="$AMDEP_TRUE" MAKE="${MAKE-make}"])])
+
+# Do all the work for Automake.                             -*- Autoconf -*-
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This macro actually does too much.  Some checks are only needed if
+# your package does certain things.  But this isn't really a big deal.
+
+dnl Redefine AC_PROG_CC to automatically invoke _AM_PROG_CC_C_O.
+m4_define([AC_PROG_CC],
+m4_defn([AC_PROG_CC])
+[_AM_PROG_CC_C_O
+])
+
+# AM_INIT_AUTOMAKE(PACKAGE, VERSION, [NO-DEFINE])
+# AM_INIT_AUTOMAKE([OPTIONS])
+# -----------------------------------------------
+# The call with PACKAGE and VERSION arguments is the old style
+# call (pre autoconf-2.50), which is being phased out.  PACKAGE
+# and VERSION should now be passed to AC_INIT and removed from
+# the call to AM_INIT_AUTOMAKE.
+# We support both call styles for the transition.  After
+# the next Automake release, Autoconf can make the AC_INIT
+# arguments mandatory, and then we can depend on a new Autoconf
+# release and drop the old call support.
+AC_DEFUN([AM_INIT_AUTOMAKE],
+[AC_PREREQ([2.65])dnl
+m4_ifdef([_$0_ALREADY_INIT],
+  [m4_fatal([$0 expanded multiple times
+]m4_defn([_$0_ALREADY_INIT]))],
+  [m4_define([_$0_ALREADY_INIT], m4_expansion_stack)])dnl
+dnl Autoconf wants to disallow AM_ names.  We explicitly allow
+dnl the ones we care about.
+m4_pattern_allow([^AM_[A-Z]+FLAGS$])dnl
+AC_REQUIRE([AM_SET_CURRENT_AUTOMAKE_VERSION])dnl
+AC_REQUIRE([AC_PROG_INSTALL])dnl
+if test "`cd $srcdir && pwd`" != "`pwd`"; then
+  # Use -I$(srcdir) only when $(srcdir) != ., so that make's output
+  # is not polluted with repeated "-I."
+  AC_SUBST([am__isrc], [' -I$(srcdir)'])_AM_SUBST_NOTMAKE([am__isrc])dnl
+  # test to see if srcdir already configured
+  if test -f $srcdir/config.status; then
+    AC_MSG_ERROR([source directory already configured; run "make distclean" there first])
+  fi
+fi
+
+# test whether we have cygpath
+if test -z "$CYGPATH_W"; then
+  if (cygpath --version) >/dev/null 2>/dev/null; then
+    CYGPATH_W='cygpath -w'
+  else
+    CYGPATH_W=echo
+  fi
+fi
+AC_SUBST([CYGPATH_W])
+
+# Define the identity of the package.
+dnl Distinguish between old-style and new-style calls.
+m4_ifval([$2],
+[AC_DIAGNOSE([obsolete],
+             [$0: two- and three-arguments forms are deprecated.])
+m4_ifval([$3], [_AM_SET_OPTION([no-define])])dnl
+ AC_SUBST([PACKAGE], [$1])dnl
+ AC_SUBST([VERSION], [$2])],
+[_AM_SET_OPTIONS([$1])dnl
+dnl Diagnose old-style AC_INIT with new-style AM_AUTOMAKE_INIT.
+m4_if(
+  m4_ifset([AC_PACKAGE_NAME], [ok]):m4_ifset([AC_PACKAGE_VERSION], [ok]),
+  [ok:ok],,
+  [m4_fatal([AC_INIT should be called with package and version arguments])])dnl
+ AC_SUBST([PACKAGE], ['AC_PACKAGE_TARNAME'])dnl
+ AC_SUBST([VERSION], ['AC_PACKAGE_VERSION'])])dnl
+
+_AM_IF_OPTION([no-define],,
+[AC_DEFINE_UNQUOTED([PACKAGE], ["$PACKAGE"], [Name of package])
+ AC_DEFINE_UNQUOTED([VERSION], ["$VERSION"], [Version number of package])])dnl
+
+# Some tools Automake needs.
+AC_REQUIRE([AM_SANITY_CHECK])dnl
+AC_REQUIRE([AC_ARG_PROGRAM])dnl
+AM_MISSING_PROG([ACLOCAL], [aclocal-${am__api_version}])
+AM_MISSING_PROG([AUTOCONF], [autoconf])
+AM_MISSING_PROG([AUTOMAKE], [automake-${am__api_version}])
+AM_MISSING_PROG([AUTOHEADER], [autoheader])
+AM_MISSING_PROG([MAKEINFO], [makeinfo])
+AC_REQUIRE([AM_PROG_INSTALL_SH])dnl
+AC_REQUIRE([AM_PROG_INSTALL_STRIP])dnl
+AC_REQUIRE([AC_PROG_MKDIR_P])dnl
+# For better backward compatibility.  To be removed once Automake 1.9.x
+# dies out for good.  For more background, see:
+# <https://lists.gnu.org/archive/html/automake/2012-07/msg00001.html>
+# <https://lists.gnu.org/archive/html/automake/2012-07/msg00014.html>
+AC_SUBST([mkdir_p], ['$(MKDIR_P)'])
+# We need awk for the "check" target (and possibly the TAP driver).  The
+# system "awk" is bad on some platforms.
+AC_REQUIRE([AC_PROG_AWK])dnl
+AC_REQUIRE([AC_PROG_MAKE_SET])dnl
+AC_REQUIRE([AM_SET_LEADING_DOT])dnl
+_AM_IF_OPTION([tar-ustar], [_AM_PROG_TAR([ustar])],
+	      [_AM_IF_OPTION([tar-pax], [_AM_PROG_TAR([pax])],
+			     [_AM_PROG_TAR([v7])])])
+_AM_IF_OPTION([no-dependencies],,
+[AC_PROVIDE_IFELSE([AC_PROG_CC],
+		  [_AM_DEPENDENCIES([CC])],
+		  [m4_define([AC_PROG_CC],
+			     m4_defn([AC_PROG_CC])[_AM_DEPENDENCIES([CC])])])dnl
+AC_PROVIDE_IFELSE([AC_PROG_CXX],
+		  [_AM_DEPENDENCIES([CXX])],
+		  [m4_define([AC_PROG_CXX],
+			     m4_defn([AC_PROG_CXX])[_AM_DEPENDENCIES([CXX])])])dnl
+AC_PROVIDE_IFELSE([AC_PROG_OBJC],
+		  [_AM_DEPENDENCIES([OBJC])],
+		  [m4_define([AC_PROG_OBJC],
+			     m4_defn([AC_PROG_OBJC])[_AM_DEPENDENCIES([OBJC])])])dnl
+AC_PROVIDE_IFELSE([AC_PROG_OBJCXX],
+		  [_AM_DEPENDENCIES([OBJCXX])],
+		  [m4_define([AC_PROG_OBJCXX],
+			     m4_defn([AC_PROG_OBJCXX])[_AM_DEPENDENCIES([OBJCXX])])])dnl
+])
+# Variables for tags utilities; see am/tags.am
+if test -z "$CTAGS"; then
+  CTAGS=ctags
+fi
+AC_SUBST([CTAGS])
+if test -z "$ETAGS"; then
+  ETAGS=etags
+fi
+AC_SUBST([ETAGS])
+if test -z "$CSCOPE"; then
+  CSCOPE=cscope
+fi
+AC_SUBST([CSCOPE])
+
+AC_REQUIRE([AM_SILENT_RULES])dnl
+dnl The testsuite driver may need to know about EXEEXT, so add the
+dnl 'am__EXEEXT' conditional if _AM_COMPILER_EXEEXT was seen.  This
+dnl macro is hooked onto _AC_COMPILER_EXEEXT early, see below.
+AC_CONFIG_COMMANDS_PRE(dnl
+[m4_provide_if([_AM_COMPILER_EXEEXT],
+  [AM_CONDITIONAL([am__EXEEXT], [test -n "$EXEEXT"])])])dnl
+
+# POSIX will say in a future version that running "rm -f" with no argument
+# is OK; and we want to be able to make that assumption in our Makefile
+# recipes.  So use an aggressive probe to check that the usage we want is
+# actually supported "in the wild" to an acceptable degree.
+# See automake bug#10828.
+# To make any issue more visible, cause the running configure to be aborted
+# by default if the 'rm' program in use doesn't match our expectations; the
+# user can still override this though.
+if rm -f && rm -fr && rm -rf; then : OK; else
+  cat >&2 <<'END'
+Oops!
+
+Your 'rm' program seems unable to run without file operands specified
+on the command line, even when the '-f' option is present.  This is contrary
+to the behaviour of most rm programs out there, and not conforming with
+the upcoming POSIX standard: <http://austingroupbugs.net/view.php?id=542>
+
+Please tell bug-automake@gnu.org about your system, including the value
+of your $PATH and any error possibly output before this message.  This
+can help us improve future automake versions.
+
+END
+  if test x"$ACCEPT_INFERIOR_RM_PROGRAM" = x"yes"; then
+    echo 'Configuration will proceed anyway, since you have set the' >&2
+    echo 'ACCEPT_INFERIOR_RM_PROGRAM variable to "yes"' >&2
+    echo >&2
+  else
+    cat >&2 <<'END'
+Aborting the configuration process, to ensure you take notice of the issue.
+
+You can download and install GNU coreutils to get an 'rm' implementation
+that behaves properly: <https://www.gnu.org/software/coreutils/>.
+
+If you want to complete the configuration process using your problematic
+'rm' anyway, export the environment variable ACCEPT_INFERIOR_RM_PROGRAM
+to "yes", and re-run configure.
+
+END
+    AC_MSG_ERROR([Your 'rm' program is bad, sorry.])
+  fi
+fi
+dnl The trailing newline in this macro's definition is deliberate, for
+dnl backward compatibility and to allow trailing 'dnl'-style comments
+dnl after the AM_INIT_AUTOMAKE invocation. See automake bug#16841.
+])
+
+dnl Hook into '_AC_COMPILER_EXEEXT' early to learn its expansion.  Do not
+dnl add the conditional right here, as _AC_COMPILER_EXEEXT may be further
+dnl mangled by Autoconf and run in a shell conditional statement.
+m4_define([_AC_COMPILER_EXEEXT],
+m4_defn([_AC_COMPILER_EXEEXT])[m4_provide([_AM_COMPILER_EXEEXT])])
+
+# When config.status generates a header, we must update the stamp-h file.
+# This file resides in the same directory as the config header
+# that is generated.  The stamp files are numbered to have different names.
+
+# Autoconf calls _AC_AM_CONFIG_HEADER_HOOK (when defined) in the
+# loop where config.status creates the headers, so we can generate
+# our stamp files there.
+AC_DEFUN([_AC_AM_CONFIG_HEADER_HOOK],
+[# Compute $1's index in $config_headers.
+_am_arg=$1
+_am_stamp_count=1
+for _am_header in $config_headers :; do
+  case $_am_header in
+    $_am_arg | $_am_arg:* )
+      break ;;
+    * )
+      _am_stamp_count=`expr $_am_stamp_count + 1` ;;
+  esac
+done
+echo "timestamp for $_am_arg" >`AS_DIRNAME(["$_am_arg"])`/stamp-h[]$_am_stamp_count])
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_PROG_INSTALL_SH
+# ------------------
+# Define $install_sh.
+AC_DEFUN([AM_PROG_INSTALL_SH],
+[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl
+if test x"${install_sh+set}" != xset; then
+  case $am_aux_dir in
+  *\ * | *\	*)
+    install_sh="\${SHELL} '$am_aux_dir/install-sh'" ;;
+  *)
+    install_sh="\${SHELL} $am_aux_dir/install-sh"
+  esac
+fi
+AC_SUBST([install_sh])])
+
+# Copyright (C) 2003-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# Check whether the underlying file-system supports filenames
+# with a leading dot.  For instance MS-DOS doesn't.
+AC_DEFUN([AM_SET_LEADING_DOT],
+[rm -rf .tst 2>/dev/null
+mkdir .tst 2>/dev/null
+if test -d .tst; then
+  am__leading_dot=.
+else
+  am__leading_dot=_
+fi
+rmdir .tst 2>/dev/null
+AC_SUBST([am__leading_dot])])
+
+# Add --enable-maintainer-mode option to configure.         -*- Autoconf -*-
+# From Jim Meyering
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_MAINTAINER_MODE([DEFAULT-MODE])
+# ----------------------------------
+# Control maintainer-specific portions of Makefiles.
+# Default is to disable them, unless 'enable' is passed literally.
+# For symmetry, 'disable' may be passed as well.  Anyway, the user
+# can override the default with the --enable/--disable switch.
+AC_DEFUN([AM_MAINTAINER_MODE],
+[m4_case(m4_default([$1], [disable]),
+       [enable], [m4_define([am_maintainer_other], [disable])],
+       [disable], [m4_define([am_maintainer_other], [enable])],
+       [m4_define([am_maintainer_other], [enable])
+        m4_warn([syntax], [unexpected argument to AM@&t@_MAINTAINER_MODE: $1])])
+AC_MSG_CHECKING([whether to enable maintainer-specific portions of Makefiles])
+  dnl maintainer-mode's default is 'disable' unless 'enable' is passed
+  AC_ARG_ENABLE([maintainer-mode],
+    [AS_HELP_STRING([--]am_maintainer_other[-maintainer-mode],
+      am_maintainer_other[ make rules and dependencies not useful
+      (and sometimes confusing) to the casual installer])],
+    [USE_MAINTAINER_MODE=$enableval],
+    [USE_MAINTAINER_MODE=]m4_if(am_maintainer_other, [enable], [no], [yes]))
+  AC_MSG_RESULT([$USE_MAINTAINER_MODE])
+  AM_CONDITIONAL([MAINTAINER_MODE], [test $USE_MAINTAINER_MODE = yes])
+  MAINT=$MAINTAINER_MODE_TRUE
+  AC_SUBST([MAINT])dnl
+]
+)
+
+# Check to see how 'make' treats includes.	            -*- Autoconf -*-
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_MAKE_INCLUDE()
+# -----------------
+# Check whether make has an 'include' directive that can support all
+# the idioms we need for our automatic dependency tracking code.
+AC_DEFUN([AM_MAKE_INCLUDE],
+[AC_MSG_CHECKING([whether ${MAKE-make} supports the include directive])
+cat > confinc.mk << 'END'
+am__doit:
+	@echo this is the am__doit target >confinc.out
+.PHONY: am__doit
+END
+am__include="#"
+am__quote=
+# BSD make does it like this.
+echo '.include "confinc.mk" # ignored' > confmf.BSD
+# Other make implementations (GNU, Solaris 10, AIX) do it like this.
+echo 'include confinc.mk # ignored' > confmf.GNU
+_am_result=no
+for s in GNU BSD; do
+  AM_RUN_LOG([${MAKE-make} -f confmf.$s && cat confinc.out])
+  AS_CASE([$?:`cat confinc.out 2>/dev/null`],
+      ['0:this is the am__doit target'],
+      [AS_CASE([$s],
+          [BSD], [am__include='.include' am__quote='"'],
+          [am__include='include' am__quote=''])])
+  if test "$am__include" != "#"; then
+    _am_result="yes ($s style)"
+    break
+  fi
+done
+rm -f confinc.* confmf.*
+AC_MSG_RESULT([${_am_result}])
+AC_SUBST([am__include])])
+AC_SUBST([am__quote])])
+
+# Fake the existence of programs that GNU maintainers use.  -*- Autoconf -*-
+
+# Copyright (C) 1997-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_MISSING_PROG(NAME, PROGRAM)
+# ------------------------------
+AC_DEFUN([AM_MISSING_PROG],
+[AC_REQUIRE([AM_MISSING_HAS_RUN])
+$1=${$1-"${am_missing_run}$2"}
+AC_SUBST($1)])
+
+# AM_MISSING_HAS_RUN
+# ------------------
+# Define MISSING if not defined so far and test if it is modern enough.
+# If it is, set am_missing_run to use it, otherwise, to nothing.
+AC_DEFUN([AM_MISSING_HAS_RUN],
+[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl
+AC_REQUIRE_AUX_FILE([missing])dnl
+if test x"${MISSING+set}" != xset; then
+  MISSING="\${SHELL} '$am_aux_dir/missing'"
+fi
+# Use eval to expand $SHELL
+if eval "$MISSING --is-lightweight"; then
+  am_missing_run="$MISSING "
+else
+  am_missing_run=
+  AC_MSG_WARN(['missing' script is too old or missing])
+fi
+])
+
+#  -*- Autoconf -*-
+# Obsolete and "removed" macros, that must however still report explicit
+# error messages when used, to smooth transition.
+#
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+AC_DEFUN([AM_CONFIG_HEADER],
+[AC_DIAGNOSE([obsolete],
+['$0': this macro is obsolete.
+You should use the 'AC][_CONFIG_HEADERS' macro instead.])dnl
+AC_CONFIG_HEADERS($@)])
+
+AC_DEFUN([AM_PROG_CC_STDC],
+[AC_PROG_CC
+am_cv_prog_cc_stdc=$ac_cv_prog_cc_stdc
+AC_DIAGNOSE([obsolete],
+['$0': this macro is obsolete.
+You should simply use the 'AC][_PROG_CC' macro instead.
+Also, your code should no longer depend upon 'am_cv_prog_cc_stdc',
+but upon 'ac_cv_prog_cc_stdc'.])])
+
+AC_DEFUN([AM_C_PROTOTYPES],
+         [AC_FATAL([automatic de-ANSI-fication support has been removed])])
+AU_DEFUN([fp_C_PROTOTYPES], [AM_C_PROTOTYPES])
+
+# Helper functions for option handling.                     -*- Autoconf -*-
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_MANGLE_OPTION(NAME)
+# -----------------------
+AC_DEFUN([_AM_MANGLE_OPTION],
+[[_AM_OPTION_]m4_bpatsubst($1, [[^a-zA-Z0-9_]], [_])])
+
+# _AM_SET_OPTION(NAME)
+# --------------------
+# Set option NAME.  Presently that only means defining a flag for this option.
+AC_DEFUN([_AM_SET_OPTION],
+[m4_define(_AM_MANGLE_OPTION([$1]), [1])])
+
+# _AM_SET_OPTIONS(OPTIONS)
+# ------------------------
+# OPTIONS is a space-separated list of Automake options.
+AC_DEFUN([_AM_SET_OPTIONS],
+[m4_foreach_w([_AM_Option], [$1], [_AM_SET_OPTION(_AM_Option)])])
+
+# _AM_IF_OPTION(OPTION, IF-SET, [IF-NOT-SET])
+# -------------------------------------------
+# Execute IF-SET if OPTION is set, IF-NOT-SET otherwise.
+AC_DEFUN([_AM_IF_OPTION],
+[m4_ifset(_AM_MANGLE_OPTION([$1]), [$2], [$3])])
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_PROG_CC_C_O
+# ---------------
+# Like AC_PROG_CC_C_O, but changed for automake.  We rewrite AC_PROG_CC
+# to automatically call this.
+AC_DEFUN([_AM_PROG_CC_C_O],
+[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl
+AC_REQUIRE_AUX_FILE([compile])dnl
+AC_LANG_PUSH([C])dnl
+AC_CACHE_CHECK(
+  [whether $CC understands -c and -o together],
+  [am_cv_prog_cc_c_o],
+  [AC_LANG_CONFTEST([AC_LANG_PROGRAM([])])
+  # Make sure it works both with $CC and with simple cc.
+  # Following AC_PROG_CC_C_O, we do the test twice because some
+  # compilers refuse to overwrite an existing .o file with -o,
+  # though they will create one.
+  am_cv_prog_cc_c_o=yes
+  for am_i in 1 2; do
+    if AM_RUN_LOG([$CC -c conftest.$ac_ext -o conftest2.$ac_objext]) \
+         && test -f conftest2.$ac_objext; then
+      : OK
+    else
+      am_cv_prog_cc_c_o=no
+      break
+    fi
+  done
+  rm -f core conftest*
+  unset am_i])
+if test "$am_cv_prog_cc_c_o" != yes; then
+   # Losing compiler, so override with the script.
+   # FIXME: It is wrong to rewrite CC.
+   # But if we don't then we get into trouble of one sort or another.
+   # A longer-term fix would be to have automake use am__CC in this case,
+   # and then we could set am__CC="\$(top_srcdir)/compile \$(CC)"
+   CC="$am_aux_dir/compile $CC"
+fi
+AC_LANG_POP([C])])
+
+# For backward compatibility.
+AC_DEFUN_ONCE([AM_PROG_CC_C_O], [AC_REQUIRE([AC_PROG_CC])])
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_RUN_LOG(COMMAND)
+# -------------------
+# Run COMMAND, save the exit status in ac_status, and log it.
+# (This has been adapted from Autoconf's _AC_RUN_LOG macro.)
+AC_DEFUN([AM_RUN_LOG],
+[{ echo "$as_me:$LINENO: $1" >&AS_MESSAGE_LOG_FD
+   ($1) >&AS_MESSAGE_LOG_FD 2>&AS_MESSAGE_LOG_FD
+   ac_status=$?
+   echo "$as_me:$LINENO: \$? = $ac_status" >&AS_MESSAGE_LOG_FD
+   (exit $ac_status); }])
+
+# Check to make sure that the build environment is sane.    -*- Autoconf -*-
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_SANITY_CHECK
+# ---------------
+AC_DEFUN([AM_SANITY_CHECK],
+[AC_MSG_CHECKING([whether build environment is sane])
+# Reject unsafe characters in $srcdir or the absolute working directory
+# name.  Accept space and tab only in the latter.
+am_lf='
+'
+case `pwd` in
+  *[[\\\"\#\$\&\'\`$am_lf]]*)
+    AC_MSG_ERROR([unsafe absolute working directory name]);;
+esac
+case $srcdir in
+  *[[\\\"\#\$\&\'\`$am_lf\ \	]]*)
+    AC_MSG_ERROR([unsafe srcdir value: '$srcdir']);;
+esac
+
+# Do 'set' in a subshell so we don't clobber the current shell's
+# arguments.  Must try -L first in case configure is actually a
+# symlink; some systems play weird games with the mod time of symlinks
+# (eg FreeBSD returns the mod time of the symlink's containing
+# directory).
+if (
+   am_has_slept=no
+   for am_try in 1 2; do
+     echo "timestamp, slept: $am_has_slept" > conftest.file
+     set X `ls -Lt "$srcdir/configure" conftest.file 2> /dev/null`
+     if test "$[*]" = "X"; then
+	# -L didn't work.
+	set X `ls -t "$srcdir/configure" conftest.file`
+     fi
+     if test "$[*]" != "X $srcdir/configure conftest.file" \
+	&& test "$[*]" != "X conftest.file $srcdir/configure"; then
+
+	# If neither matched, then we have a broken ls.  This can happen
+	# if, for instance, CONFIG_SHELL is bash and it inherits a
+	# broken ls alias from the environment.  This has actually
+	# happened.  Such a system could not be considered "sane".
+	AC_MSG_ERROR([ls -t appears to fail.  Make sure there is not a broken
+  alias in your environment])
+     fi
+     if test "$[2]" = conftest.file || test $am_try -eq 2; then
+       break
+     fi
+     # Just in case.
+     sleep 1
+     am_has_slept=yes
+   done
+   test "$[2]" = conftest.file
+   )
+then
+   # Ok.
+   :
+else
+   AC_MSG_ERROR([newly created file is older than distributed files!
+Check your system clock])
+fi
+AC_MSG_RESULT([yes])
+# If we didn't sleep, we still need to ensure time stamps of config.status and
+# generated files are strictly newer.
+am_sleep_pid=
+if grep 'slept: no' conftest.file >/dev/null 2>&1; then
+  ( sleep 1 ) &
+  am_sleep_pid=$!
+fi
+AC_CONFIG_COMMANDS_PRE(
+  [AC_MSG_CHECKING([that generated files are newer than configure])
+   if test -n "$am_sleep_pid"; then
+     # Hide warnings about reused PIDs.
+     wait $am_sleep_pid 2>/dev/null
+   fi
+   AC_MSG_RESULT([done])])
+rm -f conftest.file
+])
+
+# Copyright (C) 2009-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_SILENT_RULES([DEFAULT])
+# --------------------------
+# Enable less verbose build rules; with the default set to DEFAULT
+# ("yes" being less verbose, "no" or empty being verbose).
+AC_DEFUN([AM_SILENT_RULES],
+[AC_ARG_ENABLE([silent-rules], [dnl
+AS_HELP_STRING(
+  [--enable-silent-rules],
+  [less verbose build output (undo: "make V=1")])
+AS_HELP_STRING(
+  [--disable-silent-rules],
+  [verbose build output (undo: "make V=0")])dnl
+])
+case $enable_silent_rules in @%:@ (((
+  yes) AM_DEFAULT_VERBOSITY=0;;
+   no) AM_DEFAULT_VERBOSITY=1;;
+    *) AM_DEFAULT_VERBOSITY=m4_if([$1], [yes], [0], [1]);;
+esac
+dnl
+dnl A few 'make' implementations (e.g., NonStop OS and NextStep)
+dnl do not support nested variable expansions.
+dnl See automake bug#9928 and bug#10237.
+am_make=${MAKE-make}
+AC_CACHE_CHECK([whether $am_make supports nested variables],
+   [am_cv_make_support_nested_variables],
+   [if AS_ECHO([['TRUE=$(BAR$(V))
+BAR0=false
+BAR1=true
+V=1
+am__doit:
+	@$(TRUE)
+.PHONY: am__doit']]) | $am_make -f - >/dev/null 2>&1; then
+  am_cv_make_support_nested_variables=yes
+else
+  am_cv_make_support_nested_variables=no
+fi])
+if test $am_cv_make_support_nested_variables = yes; then
+  dnl Using '$V' instead of '$(V)' breaks IRIX make.
+  AM_V='$(V)'
+  AM_DEFAULT_V='$(AM_DEFAULT_VERBOSITY)'
+else
+  AM_V=$AM_DEFAULT_VERBOSITY
+  AM_DEFAULT_V=$AM_DEFAULT_VERBOSITY
+fi
+AC_SUBST([AM_V])dnl
+AM_SUBST_NOTMAKE([AM_V])dnl
+AC_SUBST([AM_DEFAULT_V])dnl
+AM_SUBST_NOTMAKE([AM_DEFAULT_V])dnl
+AC_SUBST([AM_DEFAULT_VERBOSITY])dnl
+AM_BACKSLASH='\'
+AC_SUBST([AM_BACKSLASH])dnl
+_AM_SUBST_NOTMAKE([AM_BACKSLASH])dnl
+])
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_PROG_INSTALL_STRIP
+# ---------------------
+# One issue with vendor 'install' (even GNU) is that you can't
+# specify the program used to strip binaries.  This is especially
+# annoying in cross-compiling environments, where the build's strip
+# is unlikely to handle the host's binaries.
+# Fortunately install-sh will honor a STRIPPROG variable, so we
+# always use install-sh in "make install-strip", and initialize
+# STRIPPROG with the value of the STRIP variable (set by the user).
+AC_DEFUN([AM_PROG_INSTALL_STRIP],
+[AC_REQUIRE([AM_PROG_INSTALL_SH])dnl
+# Installed binaries are usually stripped using 'strip' when the user
+# run "make install-strip".  However 'strip' might not be the right
+# tool to use in cross-compilation environments, therefore Automake
+# will honor the 'STRIP' environment variable to overrule this program.
+dnl Don't test for $cross_compiling = yes, because it might be 'maybe'.
+if test "$cross_compiling" != no; then
+  AC_CHECK_TOOL([STRIP], [strip], :)
+fi
+INSTALL_STRIP_PROGRAM="\$(install_sh) -c -s"
+AC_SUBST([INSTALL_STRIP_PROGRAM])])
+
+# Copyright (C) 2006-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_SUBST_NOTMAKE(VARIABLE)
+# ---------------------------
+# Prevent Automake from outputting VARIABLE = @VARIABLE@ in Makefile.in.
+# This macro is traced by Automake.
+AC_DEFUN([_AM_SUBST_NOTMAKE])
+
+# AM_SUBST_NOTMAKE(VARIABLE)
+# --------------------------
+# Public sister of _AM_SUBST_NOTMAKE.
+AC_DEFUN([AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE($@)])
+
+# Check how to create a tarball.                            -*- Autoconf -*-
+
+# Copyright (C) 2004-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_PROG_TAR(FORMAT)
+# --------------------
+# Check how to create a tarball in format FORMAT.
+# FORMAT should be one of 'v7', 'ustar', or 'pax'.
+#
+# Substitute a variable $(am__tar) that is a command
+# writing to stdout a FORMAT-tarball containing the directory
+# $tardir.
+#     tardir=directory && $(am__tar) > result.tar
+#
+# Substitute a variable $(am__untar) that extract such
+# a tarball read from stdin.
+#     $(am__untar) < result.tar
+#
+AC_DEFUN([_AM_PROG_TAR],
+[# Always define AMTAR for backward compatibility.  Yes, it's still used
+# in the wild :-(  We should find a proper way to deprecate it ...
+AC_SUBST([AMTAR], ['$${TAR-tar}'])
+
+# We'll loop over all known methods to create a tar archive until one works.
+_am_tools='gnutar m4_if([$1], [ustar], [plaintar]) pax cpio none'
+
+m4_if([$1], [v7],
+  [am__tar='$${TAR-tar} chof - "$$tardir"' am__untar='$${TAR-tar} xf -'],
+
+  [m4_case([$1],
+    [ustar],
+     [# The POSIX 1988 'ustar' format is defined with fixed-size fields.
+      # There is notably a 21 bits limit for the UID and the GID.  In fact,
+      # the 'pax' utility can hang on bigger UID/GID (see automake bug#8343
+      # and bug#13588).
+      am_max_uid=2097151 # 2^21 - 1
+      am_max_gid=$am_max_uid
+      # The $UID and $GID variables are not portable, so we need to resort
+      # to the POSIX-mandated id(1) utility.  Errors in the 'id' calls
+      # below are definitely unexpected, so allow the users to see them
+      # (that is, avoid stderr redirection).
+      am_uid=`id -u || echo unknown`
+      am_gid=`id -g || echo unknown`
+      AC_MSG_CHECKING([whether UID '$am_uid' is supported by ustar format])
+      if test $am_uid -le $am_max_uid; then
+         AC_MSG_RESULT([yes])
+      else
+         AC_MSG_RESULT([no])
+         _am_tools=none
+      fi
+      AC_MSG_CHECKING([whether GID '$am_gid' is supported by ustar format])
+      if test $am_gid -le $am_max_gid; then
+         AC_MSG_RESULT([yes])
+      else
+        AC_MSG_RESULT([no])
+        _am_tools=none
+      fi],
+
+  [pax],
+    [],
+
+  [m4_fatal([Unknown tar format])])
+
+  AC_MSG_CHECKING([how to create a $1 tar archive])
+
+  # Go ahead even if we have the value already cached.  We do so because we
+  # need to set the values for the 'am__tar' and 'am__untar' variables.
+  _am_tools=${am_cv_prog_tar_$1-$_am_tools}
+
+  for _am_tool in $_am_tools; do
+    case $_am_tool in
+    gnutar)
+      for _am_tar in tar gnutar gtar; do
+        AM_RUN_LOG([$_am_tar --version]) && break
+      done
+      am__tar="$_am_tar --format=m4_if([$1], [pax], [posix], [$1]) -chf - "'"$$tardir"'
+      am__tar_="$_am_tar --format=m4_if([$1], [pax], [posix], [$1]) -chf - "'"$tardir"'
+      am__untar="$_am_tar -xf -"
+      ;;
+    plaintar)
+      # Must skip GNU tar: if it does not support --format= it doesn't create
+      # ustar tarball either.
+      (tar --version) >/dev/null 2>&1 && continue
+      am__tar='tar chf - "$$tardir"'
+      am__tar_='tar chf - "$tardir"'
+      am__untar='tar xf -'
+      ;;
+    pax)
+      am__tar='pax -L -x $1 -w "$$tardir"'
+      am__tar_='pax -L -x $1 -w "$tardir"'
+      am__untar='pax -r'
+      ;;
+    cpio)
+      am__tar='find "$$tardir" -print | cpio -o -H $1 -L'
+      am__tar_='find "$tardir" -print | cpio -o -H $1 -L'
+      am__untar='cpio -i -H $1 -d'
+      ;;
+    none)
+      am__tar=false
+      am__tar_=false
+      am__untar=false
+      ;;
+    esac
+
+    # If the value was cached, stop now.  We just wanted to have am__tar
+    # and am__untar set.
+    test -n "${am_cv_prog_tar_$1}" && break
+
+    # tar/untar a dummy directory, and stop if the command works.
+    rm -rf conftest.dir
+    mkdir conftest.dir
+    echo GrepMe > conftest.dir/file
+    AM_RUN_LOG([tardir=conftest.dir && eval $am__tar_ >conftest.tar])
+    rm -rf conftest.dir
+    if test -s conftest.tar; then
+      AM_RUN_LOG([$am__untar <conftest.tar])
+      AM_RUN_LOG([cat conftest.dir/file])
+      grep GrepMe conftest.dir/file >/dev/null 2>&1 && break
+    fi
+  done
+  rm -rf conftest.dir
+
+  AC_CACHE_VAL([am_cv_prog_tar_$1], [am_cv_prog_tar_$1=$_am_tool])
+  AC_MSG_RESULT([$am_cv_prog_tar_$1])])
+
+AC_SUBST([am__tar])
+AC_SUBST([am__untar])
+]) # _AM_PROG_TAR
+
diff --git a/ar-lib b/ar-lib
new file mode 100755
index 0000000..c349042
--- /dev/null
+++ b/ar-lib
@@ -0,0 +1,271 @@
+#! /bin/sh
+# Wrapper for Microsoft lib.exe
+
+me=ar-lib
+scriptversion=2019-07-04.01; # UTC
+
+# Copyright (C) 2010-2021 Free Software Foundation, Inc.
+# Written by Peter Rosin <peda@lysator.liu.se>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# This file is maintained in Automake, please report
+# bugs to <bug-automake@gnu.org> or send patches to
+# <automake-patches@gnu.org>.
+
+
+# func_error message
+func_error ()
+{
+  echo "$me: $1" 1>&2
+  exit 1
+}
+
+file_conv=
+
+# func_file_conv build_file
+# Convert a $build file to $host form and store it in $file
+# Currently only supports Windows hosts.
+func_file_conv ()
+{
+  file=$1
+  case $file in
+    / | /[!/]*) # absolute file, and not a UNC file
+      if test -z "$file_conv"; then
+	# lazily determine how to convert abs files
+	case `uname -s` in
+	  MINGW*)
+	    file_conv=mingw
+	    ;;
+	  CYGWIN* | MSYS*)
+	    file_conv=cygwin
+	    ;;
+	  *)
+	    file_conv=wine
+	    ;;
+	esac
+      fi
+      case $file_conv in
+	mingw)
+	  file=`cmd //C echo "$file " | sed -e 's/"\(.*\) " *$/\1/'`
+	  ;;
+	cygwin | msys)
+	  file=`cygpath -m "$file" || echo "$file"`
+	  ;;
+	wine)
+	  file=`winepath -w "$file" || echo "$file"`
+	  ;;
+      esac
+      ;;
+  esac
+}
+
+# func_at_file at_file operation archive
+# Iterate over all members in AT_FILE performing OPERATION on ARCHIVE
+# for each of them.
+# When interpreting the content of the @FILE, do NOT use func_file_conv,
+# since the user would need to supply preconverted file names to
+# binutils ar, at least for MinGW.
+func_at_file ()
+{
+  operation=$2
+  archive=$3
+  at_file_contents=`cat "$1"`
+  eval set x "$at_file_contents"
+  shift
+
+  for member
+  do
+    $AR -NOLOGO $operation:"$member" "$archive" || exit $?
+  done
+}
+
+case $1 in
+  '')
+     func_error "no command.  Try '$0 --help' for more information."
+     ;;
+  -h | --h*)
+    cat <<EOF
+Usage: $me [--help] [--version] PROGRAM ACTION ARCHIVE [MEMBER...]
+
+Members may be specified in a file named with @FILE.
+EOF
+    exit $?
+    ;;
+  -v | --v*)
+    echo "$me, version $scriptversion"
+    exit $?
+    ;;
+esac
+
+if test $# -lt 3; then
+  func_error "you must specify a program, an action and an archive"
+fi
+
+AR=$1
+shift
+while :
+do
+  if test $# -lt 2; then
+    func_error "you must specify a program, an action and an archive"
+  fi
+  case $1 in
+    -lib | -LIB \
+    | -ltcg | -LTCG \
+    | -machine* | -MACHINE* \
+    | -subsystem* | -SUBSYSTEM* \
+    | -verbose | -VERBOSE \
+    | -wx* | -WX* )
+      AR="$AR $1"
+      shift
+      ;;
+    *)
+      action=$1
+      shift
+      break
+      ;;
+  esac
+done
+orig_archive=$1
+shift
+func_file_conv "$orig_archive"
+archive=$file
+
+# strip leading dash in $action
+action=${action#-}
+
+delete=
+extract=
+list=
+quick=
+replace=
+index=
+create=
+
+while test -n "$action"
+do
+  case $action in
+    d*) delete=yes  ;;
+    x*) extract=yes ;;
+    t*) list=yes    ;;
+    q*) quick=yes   ;;
+    r*) replace=yes ;;
+    s*) index=yes   ;;
+    S*)             ;; # the index is always updated implicitly
+    c*) create=yes  ;;
+    u*)             ;; # TODO: don't ignore the update modifier
+    v*)             ;; # TODO: don't ignore the verbose modifier
+    *)
+      func_error "unknown action specified"
+      ;;
+  esac
+  action=${action#?}
+done
+
+case $delete$extract$list$quick$replace,$index in
+  yes,* | ,yes)
+    ;;
+  yesyes*)
+    func_error "more than one action specified"
+    ;;
+  *)
+    func_error "no action specified"
+    ;;
+esac
+
+if test -n "$delete"; then
+  if test ! -f "$orig_archive"; then
+    func_error "archive not found"
+  fi
+  for member
+  do
+    case $1 in
+      @*)
+        func_at_file "${1#@}" -REMOVE "$archive"
+        ;;
+      *)
+        func_file_conv "$1"
+        $AR -NOLOGO -REMOVE:"$file" "$archive" || exit $?
+        ;;
+    esac
+  done
+
+elif test -n "$extract"; then
+  if test ! -f "$orig_archive"; then
+    func_error "archive not found"
+  fi
+  if test $# -gt 0; then
+    for member
+    do
+      case $1 in
+        @*)
+          func_at_file "${1#@}" -EXTRACT "$archive"
+          ;;
+        *)
+          func_file_conv "$1"
+          $AR -NOLOGO -EXTRACT:"$file" "$archive" || exit $?
+          ;;
+      esac
+    done
+  else
+    $AR -NOLOGO -LIST "$archive" | tr -d '\r' | sed -e 's/\\/\\\\/g' \
+      | while read member
+        do
+          $AR -NOLOGO -EXTRACT:"$member" "$archive" || exit $?
+        done
+  fi
+
+elif test -n "$quick$replace"; then
+  if test ! -f "$orig_archive"; then
+    if test -z "$create"; then
+      echo "$me: creating $orig_archive"
+    fi
+    orig_archive=
+  else
+    orig_archive=$archive
+  fi
+
+  for member
+  do
+    case $1 in
+    @*)
+      func_file_conv "${1#@}"
+      set x "$@" "@$file"
+      ;;
+    *)
+      func_file_conv "$1"
+      set x "$@" "$file"
+      ;;
+    esac
+    shift
+    shift
+  done
+
+  if test -n "$orig_archive"; then
+    $AR -NOLOGO -OUT:"$archive" "$orig_archive" "$@" || exit $?
+  else
+    $AR -NOLOGO -OUT:"$archive" "$@" || exit $?
+  fi
+
+elif test -n "$list"; then
+  if test ! -f "$orig_archive"; then
+    func_error "archive not found"
+  fi
+  $AR -NOLOGO -LIST "$archive" || exit $?
+fi
diff --git a/autogen.sh b/autogen.sh
new file mode 100755
index 0000000..5bce9ba
--- /dev/null
+++ b/autogen.sh
@@ -0,0 +1,1578 @@
+#!/bin/sh
+#                        a u t o g e n . s h
+#
+# Copyright (c) 2005-2009 United States Government as represented by
+# the U.S. Army Research Laboratory.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+#
+# 3. The name of the author may not be used to endorse or promote
+# products derived from this software without specific prior written
+# permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+###
+#
+# Script for automatically preparing the sources for compilation by
+# performing the myriad of necessary steps.  The script attempts to
+# detect proper version support, and outputs warnings about particular
+# systems that have autotool peculiarities.
+#
+# Basically, if everything is set up and installed correctly, the
+# script will validate that minimum versions of the GNU Build System
+# tools are installed, account for several common configuration
+# issues, and then simply run autoreconf for you.
+#
+# If autoreconf fails, which can happen for many valid configurations,
+# this script proceeds to run manual preparation steps effectively
+# providing a POSIX shell script (mostly complete) reimplementation of
+# autoreconf.
+#
+# The AUTORECONF, AUTOCONF, AUTOMAKE, LIBTOOLIZE, ACLOCAL, AUTOHEADER
+# environment variables and corresponding _OPTIONS variables (e.g.
+# AUTORECONF_OPTIONS) may be used to override the default automatic
+# detection behaviors.  Similarly the _VERSION variables will override
+# the minimum required version numbers.
+#
+# Examples:
+#
+#   To obtain help on usage:
+#     ./autogen.sh --help
+#
+#   To obtain verbose output:
+#     ./autogen.sh --verbose
+#
+#   To skip autoreconf and prepare manually:
+#     AUTORECONF=false ./autogen.sh
+#
+#   To verbosely try running with an older (unsupported) autoconf:
+#     AUTOCONF_VERSION=2.50 ./autogen.sh --verbose
+#
+# Author:
+#   Christopher Sean Morrison <morrison@brlcad.org>
+#
+# Patches:
+#   Sebastian Pipping <sebastian@pipping.org>
+#
+######################################################################
+
+# set to minimum acceptable version of autoconf
+if [ "x$AUTOCONF_VERSION" = "x" ] ; then
+    AUTOCONF_VERSION=2.52
+fi
+# set to minimum acceptable version of automake
+if [ "x$AUTOMAKE_VERSION" = "x" ] ; then
+    AUTOMAKE_VERSION=1.6.0
+fi
+# set to minimum acceptable version of libtool
+if [ "x$LIBTOOL_VERSION" = "x" ] ; then
+    LIBTOOL_VERSION=1.4.2
+fi
+
+
+##################
+# ident function #
+##################
+ident ( ) {
+    # extract copyright from header
+    __copyright="`grep Copyright $AUTOGEN_SH | head -${HEAD_N}1 | awk '{print $4}'`"
+    if [ "x$__copyright" = "x" ] ; then
+	__copyright="`date +%Y`"
+    fi
+
+    # extract version from CVS Id string
+    __id="$Id: autogen.sh 33925 2009-03-01 23:27:06Z brlcad $"
+    __version="`echo $__id | sed 's/.*\([0-9][0-9][0-9][0-9]\)[-\/]\([0-9][0-9]\)[-\/]\([0-9][0-9]\).*/\1\2\3/'`"
+    if [ "x$__version" = "x" ] ; then
+	__version=""
+    fi
+
+    echo "autogen.sh build preparation script by Christopher Sean Morrison"
+    echo "  + config.guess download patch by Sebastian Pipping (2008-12-03)"
+    echo "revised 3-clause BSD-style license, copyright (c) $__copyright"
+    echo "script version $__version, ISO/IEC 9945 POSIX shell script"
+}
+
+
+##################
+# USAGE FUNCTION #
+##################
+usage ( ) {
+    echo "Usage: $AUTOGEN_SH [-h|--help] [-v|--verbose] [-q|--quiet] [-d|--download] [--version]"
+    echo "    --help      Help on $NAME_OF_AUTOGEN usage"
+    echo "    --verbose   Verbose progress output"
+    echo "    --quiet     Quiet suppressed progress output"
+    echo "    --download  Download the latest config.guess from gnulib"
+    echo "    --version   Only perform GNU Build System version checks"
+    echo
+    echo "Description: This script will validate that minimum versions of the"
+    echo "GNU Build System tools are installed and then run autoreconf for you."
+    echo "Should autoreconf fail, manual preparation steps will be run"
+    echo "potentially accounting for several common preparation issues.  The"
+
+    echo "AUTORECONF, AUTOCONF, AUTOMAKE, LIBTOOLIZE, ACLOCAL, AUTOHEADER,"
+    echo "PROJECT, & CONFIGURE environment variables and corresponding _OPTIONS"
+    echo "variables (e.g. AUTORECONF_OPTIONS) may be used to override the"
+    echo "default automatic detection behavior."
+    echo
+
+    ident
+
+    return 0
+}
+
+
+##########################
+# VERSION_ERROR FUNCTION #
+##########################
+version_error ( ) {
+    if [ "x$1" = "x" ] ; then
+	echo "INTERNAL ERROR: version_error was not provided a version"
+	exit 1
+    fi
+    if [ "x$2" = "x" ] ; then
+	echo "INTERNAL ERROR: version_error was not provided an application name"
+	exit 1
+    fi
+    $ECHO
+    $ECHO "ERROR:  To prepare the ${PROJECT} build system from scratch,"
+    $ECHO "        at least version $1 of $2 must be installed."
+    $ECHO
+    $ECHO "$NAME_OF_AUTOGEN does not need to be run on the same machine that will"
+    $ECHO "run configure or make.  Either the GNU Autotools will need to be installed"
+    $ECHO "or upgraded on this system, or $NAME_OF_AUTOGEN must be run on the source"
+    $ECHO "code on another system and then transferred to here. -- Cheers!"
+    $ECHO
+}
+
+##########################
+# VERSION_CHECK FUNCTION #
+##########################
+version_check ( ) {
+    if [ "x$1" = "x" ] ; then
+	echo "INTERNAL ERROR: version_check was not provided a minimum version"
+	exit 1
+    fi
+    _min="$1"
+    if [ "x$2" = "x" ] ; then
+	echo "INTERNAL ERROR: version check was not provided a comparison version"
+	exit 1
+    fi
+    _cur="$2"
+
+    # needed to handle versions like 1.10 and 1.4-p6
+    _min="`echo ${_min}. | sed 's/[^0-9]/./g' | sed 's/\.\././g'`"
+    _cur="`echo ${_cur}. | sed 's/[^0-9]/./g' | sed 's/\.\././g'`"
+
+    _min_major="`echo $_min | cut -d. -f1`"
+    _min_minor="`echo $_min | cut -d. -f2`"
+    _min_patch="`echo $_min | cut -d. -f3`"
+
+    _cur_major="`echo $_cur | cut -d. -f1`"
+    _cur_minor="`echo $_cur | cut -d. -f2`"
+    _cur_patch="`echo $_cur | cut -d. -f3`"
+
+    if [ "x$_min_major" = "x" ] ; then
+	_min_major=0
+    fi
+    if [ "x$_min_minor" = "x" ] ; then
+	_min_minor=0
+    fi
+    if [ "x$_min_patch" = "x" ] ; then
+	_min_patch=0
+    fi
+    if [ "x$_cur_minor" = "x" ] ; then
+	_cur_major=0
+    fi
+    if [ "x$_cur_minor" = "x" ] ; then
+	_cur_minor=0
+    fi
+    if [ "x$_cur_patch" = "x" ] ; then
+	_cur_patch=0
+    fi
+
+    $VERBOSE_ECHO "Checking if ${_cur_major}.${_cur_minor}.${_cur_patch} is greater than ${_min_major}.${_min_minor}.${_min_patch}"
+
+    if [ $_min_major -lt $_cur_major ] ; then
+	return 0
+    elif [ $_min_major -eq $_cur_major ] ; then
+	if [ $_min_minor -lt $_cur_minor ] ; then
+	    return 0
+	elif [ $_min_minor -eq $_cur_minor ] ; then
+	    if [ $_min_patch -lt $_cur_patch ] ; then
+		return 0
+	    elif [ $_min_patch -eq $_cur_patch ] ; then
+		return 0
+	    fi
+	fi
+    fi
+    return 1
+}
+
+
+######################################
+# LOCATE_CONFIGURE_TEMPLATE FUNCTION #
+######################################
+locate_configure_template ( ) {
+    _pwd="`pwd`"
+    if test -f "./configure.ac" ; then
+	echo "./configure.ac"
+    elif test -f "./configure.in" ; then
+	echo "./configure.in"
+    elif test -f "$_pwd/configure.ac" ; then
+	echo "$_pwd/configure.ac"
+    elif test -f "$_pwd/configure.in" ; then
+	echo "$_pwd/configure.in"
+    elif test -f "$PATH_TO_AUTOGEN/configure.ac" ; then
+	echo "$PATH_TO_AUTOGEN/configure.ac"
+    elif test -f "$PATH_TO_AUTOGEN/configure.in" ; then
+	echo "$PATH_TO_AUTOGEN/configure.in"
+    fi
+}
+
+
+##################
+# argument check #
+##################
+ARGS="$*"
+PATH_TO_AUTOGEN="`dirname $0`"
+NAME_OF_AUTOGEN="`basename $0`"
+AUTOGEN_SH="$PATH_TO_AUTOGEN/$NAME_OF_AUTOGEN"
+
+LIBTOOL_M4="${PATH_TO_AUTOGEN}/misc/libtool.m4"
+
+if [ "x$HELP" = "x" ] ; then
+    HELP=no
+fi
+if [ "x$QUIET" = "x" ] ; then
+    QUIET=no
+fi
+if [ "x$VERBOSE" = "x" ] ; then
+    VERBOSE=no
+fi
+if [ "x$VERSION_ONLY" = "x" ] ; then
+    VERSION_ONLY=no
+fi
+if [ "x$DOWNLOAD" = "x" ] ; then
+    DOWNLOAD=no
+fi
+if [ "x$AUTORECONF_OPTIONS" = "x" ] ; then
+    AUTORECONF_OPTIONS="-i -f"
+fi
+if [ "x$AUTOCONF_OPTIONS" = "x" ] ; then
+    AUTOCONF_OPTIONS="-f"
+fi
+if [ "x$AUTOMAKE_OPTIONS" = "x" ] ; then
+    AUTOMAKE_OPTIONS="-a -c -f"
+fi
+ALT_AUTOMAKE_OPTIONS="-a -c"
+if [ "x$LIBTOOLIZE_OPTIONS" = "x" ] ; then
+    LIBTOOLIZE_OPTIONS="--automake -c -f"
+fi
+ALT_LIBTOOLIZE_OPTIONS="--automake --copy --force"
+if [ "x$ACLOCAL_OPTIONS" = "x" ] ; then
+    ACLOCAL_OPTIONS=""
+fi
+if [ "x$AUTOHEADER_OPTIONS" = "x" ] ; then
+    AUTOHEADER_OPTIONS=""
+fi
+if [ "x$CONFIG_GUESS_URL" = "x" ] ; then
+    CONFIG_GUESS_URL="http://git.savannah.gnu.org/gitweb/?p=gnulib.git;a=blob_plain;f=build-aux/config.guess;hb=HEAD"
+fi
+for arg in $ARGS ; do
+    case "x$arg" in
+	x--help) HELP=yes ;;
+	x-[hH]) HELP=yes ;;
+	x--quiet) QUIET=yes ;;
+	x-[qQ]) QUIET=yes ;;
+	x--verbose) VERBOSE=yes ;;
+	x-[dD]) DOWNLOAD=yes ;;
+	x--download) DOWNLOAD=yes ;;
+	x-[vV]) VERBOSE=yes ;;
+	x--version) VERSION_ONLY=yes ;;
+	*)
+	    echo "Unknown option: $arg"
+	    echo
+	    usage
+	    exit 1
+	    ;;
+    esac
+done
+
+
+#####################
+# environment check #
+#####################
+
+# sanity check before recursions potentially begin
+if [ ! -f "$AUTOGEN_SH" ] ; then
+    echo "INTERNAL ERROR: $AUTOGEN_SH does not exist"
+    if [ ! "x$0" = "x$AUTOGEN_SH" ] ; then
+	echo "INTERNAL ERROR: dirname/basename inconsistency: $0 != $AUTOGEN_SH"
+    fi
+    exit 1
+fi
+
+# force locale setting to C so things like date output as expected
+LC_ALL=C
+
+# commands that this script expects
+for __cmd in echo head tail pwd ; do
+    echo "test" | $__cmd > /dev/null 2>&1
+    if [ $? != 0 ] ; then
+	echo "INTERNAL ERROR: '${__cmd}' command is required"
+	exit 2
+    fi
+done
+echo "test" | grep "test" > /dev/null 2>&1
+if test ! x$? = x0 ; then
+    echo "INTERNAL ERROR: grep command is required"
+    exit 1
+fi
+echo "test" | sed "s/test/test/" > /dev/null 2>&1
+if test ! x$? = x0 ; then
+    echo "INTERNAL ERROR: sed command is required"
+    exit 1
+fi
+
+
+# determine the behavior of echo
+case `echo "testing\c"; echo 1,2,3`,`echo -n testing; echo 1,2,3` in
+    *c*,-n*) ECHO_N= ECHO_C='
+' ECHO_T='	' ;;
+    *c*,*  ) ECHO_N=-n ECHO_C= ECHO_T= ;;
+    *)       ECHO_N= ECHO_C='\c' ECHO_T= ;;
+esac
+
+# determine the behavior of head
+case "x`echo 'head' | head -n 1 2>&1`" in
+    *xhead*) HEAD_N="n " ;;
+    *) HEAD_N="" ;;
+esac
+
+# determine the behavior of tail
+case "x`echo 'tail' | tail -n 1 2>&1`" in
+    *xtail*) TAIL_N="n " ;;
+    *) TAIL_N="" ;;
+esac
+
+VERBOSE_ECHO=:
+ECHO=:
+if [ "x$QUIET" = "xyes" ] ; then
+    if [ "x$VERBOSE" = "xyes" ] ; then
+	echo "Verbose output quelled by quiet option.  Further output disabled."
+    fi
+else
+    ECHO=echo
+    if [ "x$VERBOSE" = "xyes" ] ; then
+	echo "Verbose output enabled"
+	VERBOSE_ECHO=echo
+    fi
+fi
+
+
+# allow a recursive run to disable further recursions
+if [ "x$RUN_RECURSIVE" = "x" ] ; then
+    RUN_RECURSIVE=yes
+fi
+
+
+################################################
+# check for help arg and bypass version checks #
+################################################
+if [ "x`echo $ARGS | sed 's/.*[hH][eE][lL][pP].*/help/'`" = "xhelp" ] ; then
+    HELP=yes
+fi
+if [ "x$HELP" = "xyes" ] ; then
+    usage
+    $ECHO "---"
+    $ECHO "Help was requested.  No preparation or configuration will be performed."
+    exit 0
+fi
+
+
+#######################
+# set up signal traps #
+#######################
+untrap_abnormal ( ) {
+    for sig in 1 2 13 15; do
+	trap - $sig
+    done
+}
+
+# do this cleanup whenever we exit.
+trap '
+    # start from the root
+    if test -d "$START_PATH" ; then
+	cd "$START_PATH"
+    fi
+
+    # restore/delete backup files
+    if test "x$PFC_INIT" = "x1" ; then
+	recursive_restore
+    fi
+' 0
+
+# trap SIGHUP (1), SIGINT (2), SIGPIPE (13), SIGTERM (15)
+for sig in 1 2 13 15; do
+    trap '
+	$ECHO ""
+	$ECHO "Aborting $NAME_OF_AUTOGEN: caught signal '$sig'"
+
+	# start from the root
+	if test -d "$START_PATH" ; then
+	    cd "$START_PATH"
+	fi
+
+	# clean up on abnormal exit
+	$VERBOSE_ECHO "rm -rf autom4te.cache"
+	rm -rf autom4te.cache
+
+	if test -f "acinclude.m4.$$.backup" ; then
+	    $VERBOSE_ECHO "cat acinclude.m4.$$.backup > acinclude.m4"
+	    chmod u+w acinclude.m4
+	    cat acinclude.m4.$$.backup > acinclude.m4
+
+	    $VERBOSE_ECHO "rm -f acinclude.m4.$$.backup"
+	    rm -f acinclude.m4.$$.backup
+        fi
+
+	{ (exit 1); exit 1; }
+' $sig
+done
+
+
+#############################
+# look for a configure file #
+#############################
+if [ "x$CONFIGURE" = "x" ] ; then
+    CONFIGURE="`locate_configure_template`"
+    if [ ! "x$CONFIGURE" = "x" ] ; then
+	$VERBOSE_ECHO "Found a configure template: $CONFIGURE"
+    fi
+else
+    $ECHO "Using CONFIGURE environment variable override: $CONFIGURE"
+fi
+if [ "x$CONFIGURE" = "x" ] ; then
+    if [ "x$VERSION_ONLY" = "xyes" ] ; then
+	CONFIGURE=/dev/null
+    else
+	$ECHO
+	$ECHO "A configure.ac or configure.in file could not be located implying"
+	$ECHO "that the GNU Build System is at least not used in this directory.  In"
+	$ECHO "any case, there is nothing to do here without one of those files."
+	$ECHO
+	$ECHO "ERROR: No configure.in or configure.ac file found in `pwd`"
+	exit 1
+    fi
+fi
+
+####################
+# get project name #
+####################
+if [ "x$PROJECT" = "x" ] ; then
+    PROJECT="`grep AC_INIT $CONFIGURE | grep -v '.*#.*AC_INIT' | tail -${TAIL_N}1 | sed 's/^[ 	]*AC_INIT(\([^,)]*\).*/\1/' | sed 's/.*\[\(.*\)\].*/\1/'`"
+    if [ "x$PROJECT" = "xAC_INIT" ] ; then
+	# projects might be using the older/deprecated arg-less AC_INIT .. look for AM_INIT_AUTOMAKE instead
+	PROJECT="`grep AM_INIT_AUTOMAKE $CONFIGURE | grep -v '.*#.*AM_INIT_AUTOMAKE' | tail -${TAIL_N}1 | sed 's/^[ 	]*AM_INIT_AUTOMAKE(\([^,)]*\).*/\1/' | sed 's/.*\[\(.*\)\].*/\1/'`"
+    fi
+    if [ "x$PROJECT" = "xAM_INIT_AUTOMAKE" ] ; then
+	PROJECT="project"
+    fi
+    if [ "x$PROJECT" = "x" ] ; then
+	PROJECT="project"
+    fi
+else
+    $ECHO "Using PROJECT environment variable override: $PROJECT"
+fi
+$ECHO "Preparing the $PROJECT build system...please wait"
+$ECHO
+
+
+########################
+# check for autoreconf #
+########################
+HAVE_AUTORECONF=no
+if [ "x$AUTORECONF" = "x" ] ; then
+    for AUTORECONF in autoreconf ; do
+	$VERBOSE_ECHO "Checking autoreconf version: $AUTORECONF --version"
+	$AUTORECONF --version > /dev/null 2>&1
+	if [ $? = 0 ] ; then
+	    HAVE_AUTORECONF=yes
+	    break
+	fi
+    done
+else
+    HAVE_AUTORECONF=yes
+    $ECHO "Using AUTORECONF environment variable override: $AUTORECONF"
+fi
+
+
+##########################
+# autoconf version check #
+##########################
+_acfound=no
+if [ "x$AUTOCONF" = "x" ] ; then
+    for AUTOCONF in autoconf ; do
+	$VERBOSE_ECHO "Checking autoconf version: $AUTOCONF --version"
+	$AUTOCONF --version > /dev/null 2>&1
+	if [ $? = 0 ] ; then
+	    _acfound=yes
+	    break
+	fi
+    done
+else
+    _acfound=yes
+    $ECHO "Using AUTOCONF environment variable override: $AUTOCONF"
+fi
+
+_report_error=no
+if [ ! "x$_acfound" = "xyes" ] ; then
+    $ECHO "ERROR:  Unable to locate GNU Autoconf."
+    _report_error=yes
+else
+    _version="`$AUTOCONF --version | head -${HEAD_N}1 | sed 's/[^0-9]*\([0-9\.][0-9\.]*\)/\1/'`"
+    if [ "x$_version" = "x" ] ; then
+	_version="0.0.0"
+    fi
+    $ECHO "Found GNU Autoconf version $_version"
+    version_check "$AUTOCONF_VERSION" "$_version"
+    if [ $? -ne 0 ] ; then
+	_report_error=yes
+    fi
+fi
+if [ "x$_report_error" = "xyes" ] ; then
+    version_error "$AUTOCONF_VERSION" "GNU Autoconf"
+    exit 1
+fi
+
+
+##########################
+# automake version check #
+##########################
+_amfound=no
+if [ "x$AUTOMAKE" = "x" ] ; then
+    for AUTOMAKE in automake ; do
+	$VERBOSE_ECHO "Checking automake version: $AUTOMAKE --version"
+	$AUTOMAKE --version > /dev/null 2>&1
+	if [ $? = 0 ] ; then
+	    _amfound=yes
+	    break
+	fi
+    done
+else
+    _amfound=yes
+    $ECHO "Using AUTOMAKE environment variable override: $AUTOMAKE"
+fi
+
+
+_report_error=no
+if [ ! "x$_amfound" = "xyes" ] ; then
+    $ECHO
+    $ECHO "ERROR: Unable to locate GNU Automake."
+    _report_error=yes
+else
+    _version="`$AUTOMAKE --version | head -${HEAD_N}1 | sed 's/[^0-9]*\([0-9\.][0-9\.]*\)/\1/'`"
+    if [ "x$_version" = "x" ] ; then
+	_version="0.0.0"
+    fi
+    $ECHO "Found GNU Automake version $_version"
+    version_check "$AUTOMAKE_VERSION" "$_version"
+    if [ $? -ne 0 ] ; then
+	_report_error=yes
+    fi
+fi
+if [ "x$_report_error" = "xyes" ] ; then
+    version_error "$AUTOMAKE_VERSION" "GNU Automake"
+    exit 1
+fi
+
+
+########################
+# check for libtoolize #
+########################
+HAVE_LIBTOOLIZE=yes
+HAVE_ALT_LIBTOOLIZE=no
+_ltfound=no
+if [ "x$LIBTOOLIZE" = "x" ] ; then
+    LIBTOOLIZE=libtoolize
+    $VERBOSE_ECHO "Checking libtoolize version: $LIBTOOLIZE --version"
+    $LIBTOOLIZE --version > /dev/null 2>&1
+    if [ ! $? = 0 ] ; then
+	HAVE_LIBTOOLIZE=no
+	$ECHO
+	if [ "x$HAVE_AUTORECONF" = "xno" ] ; then
+	    $ECHO "Warning:  libtoolize does not appear to be available."
+	else
+	    $ECHO "Warning:  libtoolize does not appear to be available.  This means that"
+	    $ECHO "the automatic build preparation via autoreconf will probably not work."
+	    $ECHO "Preparing the build by running each step individually, however, should"
+	    $ECHO "work and will be done automatically for you if autoreconf fails."
+	fi
+
+	# look for some alternates
+	for tool in glibtoolize libtoolize15 libtoolize14 libtoolize13 ; do
+	    $VERBOSE_ECHO "Checking libtoolize alternate: $tool --version"
+	    _glibtoolize="`$tool --version > /dev/null 2>&1`"
+	    if [ $? = 0 ] ; then
+		$VERBOSE_ECHO "Found $tool --version"
+		_glti="`which $tool`"
+		if [ "x$_glti" = "x" ] ; then
+		    $VERBOSE_ECHO "Cannot find $tool with which"
+		    continue;
+		fi
+		if test ! -f "$_glti" ; then
+		    $VERBOSE_ECHO "Cannot use $tool, $_glti is not a file"
+		    continue;
+		fi
+		_gltidir="`dirname $_glti`"
+		if [ "x$_gltidir" = "x" ] ; then
+		    $VERBOSE_ECHO "Cannot find $tool path with dirname of $_glti"
+		    continue;
+		fi
+		if test ! -d "$_gltidir" ; then
+		    $VERBOSE_ECHO "Cannot use $tool, $_gltidir is not a directory"
+		    continue;
+		fi
+		HAVE_ALT_LIBTOOLIZE=yes
+		LIBTOOLIZE="$tool"
+		$ECHO
+		$ECHO "Fortunately, $tool was found which means that your system may simply"
+		$ECHO "have a non-standard or incomplete GNU Autotools install.  If you have"
+		$ECHO "sufficient system access, it may be possible to quell this warning by"
+		$ECHO "running:"
+		$ECHO
+		sudo -V > /dev/null 2>&1
+		if [ $? = 0 ] ; then
+		    $ECHO "   sudo ln -s $_glti $_gltidir/libtoolize"
+		    $ECHO
+		else
+		    $ECHO "   ln -s $_glti $_gltidir/libtoolize"
+		    $ECHO
+		    $ECHO "Run that as root or with proper permissions to the $_gltidir directory"
+		    $ECHO
+		fi
+		_ltfound=yes
+		break
+	    fi
+	done
+    else
+	_ltfound=yes
+    fi
+else
+    _ltfound=yes
+    $ECHO "Using LIBTOOLIZE environment variable override: $LIBTOOLIZE"
+fi
+
+
+############################
+# libtoolize version check #
+############################
+_report_error=no
+if [ ! "x$_ltfound" = "xyes" ] ; then
+    $ECHO
+    $ECHO "ERROR: Unable to locate GNU Libtool."
+    _report_error=yes
+else
+    _version="`$LIBTOOLIZE --version | head -${HEAD_N}1 | sed 's/[^0-9]*\([0-9\.][0-9\.]*\)/\1/'`"
+    if [ "x$_version" = "x" ] ; then
+	_version="0.0.0"
+    fi
+    $ECHO "Found GNU Libtool version $_version"
+    version_check "$LIBTOOL_VERSION" "$_version"
+    if [ $? -ne 0 ] ; then
+	_report_error=yes
+    fi
+fi
+if [ "x$_report_error" = "xyes" ] ; then
+    version_error "$LIBTOOL_VERSION" "GNU Libtool"
+    exit 1
+fi
+
+
+#####################
+# check for aclocal #
+#####################
+if [ "x$ACLOCAL" = "x" ] ; then
+    for ACLOCAL in aclocal ; do
+	$VERBOSE_ECHO "Checking aclocal version: $ACLOCAL --version"
+	$ACLOCAL --version > /dev/null 2>&1
+	if [ $? = 0 ] ; then
+	    break
+	fi
+    done
+else
+    $ECHO "Using ACLOCAL environment variable override: $ACLOCAL"
+fi
+
+
+########################
+# check for autoheader #
+########################
+if [ "x$AUTOHEADER" = "x" ] ; then
+    for AUTOHEADER in autoheader ; do
+	$VERBOSE_ECHO "Checking autoheader version: $AUTOHEADER --version"
+	$AUTOHEADER --version > /dev/null 2>&1
+	if [ $? = 0 ] ; then
+	    break
+	fi
+    done
+else
+    $ECHO "Using AUTOHEADER environment variable override: $AUTOHEADER"
+fi
+
+
+#########################
+# check if version only #
+#########################
+$VERBOSE_ECHO "Checking whether to only output version information"
+if [ "x$VERSION_ONLY" = "xyes" ] ; then
+    $ECHO
+    ident
+    $ECHO "---"
+    $ECHO "Version requested.  No preparation or configuration will be performed."
+    exit 0
+fi
+
+
+#################################
+# PROTECT_FROM_CLOBBER FUNCTION #
+#################################
+protect_from_clobber ( ) {
+    PFC_INIT=1
+
+    # protect COPYING & INSTALL from overwrite by automake.  the
+    # automake force option will (inappropriately) ignore the existing
+    # contents of a COPYING and/or INSTALL files (depending on the
+    # version) instead of just forcing *missing* files like it does
+    # for AUTHORS, NEWS, and README. this is broken but extremely
+    # prevalent behavior, so we protect against it by keeping a backup
+    # of the file that can later be restored.
+
+    for file in COPYING INSTALL ; do
+	if test -f ${file} ; then
+	    if test -f ${file}.$$.protect_from_automake.backup ; then
+		$VERBOSE_ECHO "Already backed up ${file} in `pwd`"
+	    else
+		$VERBOSE_ECHO "Backing up ${file} in `pwd`"
+		$VERBOSE_ECHO "cp -p ${file} ${file}.$$.protect_from_automake.backup"
+		cp -p ${file} ${file}.$$.protect_from_automake.backup
+	    fi
+	fi
+    done
+}
+
+
+##############################
+# RECURSIVE_PROTECT FUNCTION #
+##############################
+recursive_protect ( ) {
+
+    # for projects using recursive configure, run the build
+    # preparation steps for the subdirectories.  this function assumes
+    # START_PATH was set to pwd before recursion begins so that
+    # relative paths work.
+
+    # git 'r done, protect COPYING and INSTALL from being clobbered
+    protect_from_clobber
+
+    if test -d autom4te.cache ; then
+	$VERBOSE_ECHO "Found an autom4te.cache directory, deleting it"
+	$VERBOSE_ECHO "rm -rf autom4te.cache"
+	rm -rf autom4te.cache
+    fi
+
+    # find configure template
+    _configure="`locate_configure_template`"
+    if [ "x$_configure" = "x" ] ; then
+	return
+    fi
+    # $VERBOSE_ECHO "Looking for configure template found `pwd`/$_configure"
+
+    # look for subdirs
+    # $VERBOSE_ECHO "Looking for subdirs in `pwd`"
+    _det_config_subdirs="`grep AC_CONFIG_SUBDIRS $_configure | grep -v '.*#.*AC_CONFIG_SUBDIRS' | sed 's/^[ 	]*AC_CONFIG_SUBDIRS(\(.*\)).*/\1/' | sed 's/.*\[\(.*\)\].*/\1/'`"
+    CHECK_DIRS=""
+    for dir in $_det_config_subdirs ; do
+	if test -d "`pwd`/$dir" ; then
+	    CHECK_DIRS="$CHECK_DIRS \"`pwd`/$dir\""
+	fi
+    done
+
+    # process subdirs
+    if [ ! "x$CHECK_DIRS" = "x" ] ; then
+	$VERBOSE_ECHO "Recursively scanning the following directories:"
+	$VERBOSE_ECHO "  $CHECK_DIRS"
+	for dir in $CHECK_DIRS ; do
+	    $VERBOSE_ECHO "Protecting files from automake in $dir"
+	    cd "$START_PATH"
+	    eval "cd $dir"
+
+	    # recursively git 'r done
+	    recursive_protect
+	done
+    fi
+} # end of recursive_protect
+
+
+#############################
+# RESTORE_CLOBBERED FUNCION #
+#############################
+restore_clobbered ( ) {
+
+    # The automake (and autoreconf by extension) -f/--force-missing
+    # option may overwrite COPYING and INSTALL even if they do exist.
+    # Here we restore the files if necessary.
+
+    spacer=no
+
+    for file in COPYING INSTALL ; do
+	if test -f ${file}.$$.protect_from_automake.backup ; then
+	    if test -f ${file} ; then
+	    # compare entire content, restore if needed
+	    if test "x`cat ${file}`" != "x`cat ${file}.$$.protect_from_automake.backup`" ; then
+		if test "x$spacer" = "xno" ; then
+		    $VERBOSE_ECHO
+		    spacer=yes
+		fi
+		# restore the backup
+		$VERBOSE_ECHO "Restoring ${file} from backup (automake -f likely clobbered it)"
+		$VERBOSE_ECHO "rm -f ${file}"
+		rm -f ${file}
+		$VERBOSE_ECHO "mv ${file}.$$.protect_from_automake.backup ${file}"
+		mv ${file}.$$.protect_from_automake.backup ${file}
+	    fi # check contents
+	    elif test -f ${file}.$$.protect_from_automake.backup ; then
+		$VERBOSE_ECHO "mv ${file}.$$.protect_from_automake.backup ${file}"
+		mv ${file}.$$.protect_from_automake.backup ${file}
+	    fi # -f ${file}
+	
+	    # just in case
+	    $VERBOSE_ECHO "rm -f ${file}.$$.protect_from_automake.backup"
+	    rm -f ${file}.$$.protect_from_automake.backup
+	fi # -f ${file}.$$.protect_from_automake.backup
+    done
+
+    CONFIGURE="`locate_configure_template`"
+    if [ "x$CONFIGURE" = "x" ] ; then
+	return
+    fi
+
+    _aux_dir="`grep AC_CONFIG_AUX_DIR $CONFIGURE | grep -v '.*#.*AC_CONFIG_AUX_DIR' | tail -${TAIL_N}1 | sed 's/^[ 	]*AC_CONFIG_AUX_DIR(\(.*\)).*/\1/' | sed 's/.*\[\(.*\)\].*/\1/'`"
+    if test ! -d "$_aux_dir" ; then
+	_aux_dir=.
+    fi
+
+    for file in config.guess config.sub ltmain.sh ; do
+	if test -f "${_aux_dir}/${file}" ; then
+	    $VERBOSE_ECHO "rm -f \"${_aux_dir}/${file}.backup\""
+	    rm -f "${_aux_dir}/${file}.backup"
+	fi
+    done
+} # end of restore_clobbered
+
+
+##############################
+# RECURSIVE_RESTORE FUNCTION #
+##############################
+recursive_restore ( ) {
+
+    # restore COPYING and INSTALL from backup if they were clobbered
+    # for each directory recursively.
+
+    # git 'r undone
+    restore_clobbered
+
+    # find configure template
+    _configure="`locate_configure_template`"
+    if [ "x$_configure" = "x" ] ; then
+	return
+    fi
+
+    # look for subdirs
+    _det_config_subdirs="`grep AC_CONFIG_SUBDIRS $_configure | grep -v '.*#.*AC_CONFIG_SUBDIRS' | sed 's/^[ 	]*AC_CONFIG_SUBDIRS(\(.*\)).*/\1/' | sed 's/.*\[\(.*\)\].*/\1/'`"
+    CHECK_DIRS=""
+    for dir in $_det_config_subdirs ; do
+	if test -d "`pwd`/$dir" ; then
+	    CHECK_DIRS="$CHECK_DIRS \"`pwd`/$dir\""
+	fi
+    done
+
+    # process subdirs
+    if [ ! "x$CHECK_DIRS" = "x" ] ; then
+	$VERBOSE_ECHO "Recursively scanning the following directories:"
+	$VERBOSE_ECHO "  $CHECK_DIRS"
+	for dir in $CHECK_DIRS ; do
+	    $VERBOSE_ECHO "Checking files for automake damage in $dir"
+	    cd "$START_PATH"
+	    eval "cd $dir"
+
+	    # recursively git 'r undone
+	    recursive_restore
+	done
+    fi
+} # end of recursive_restore
+
+
+#######################
+# INITIALIZE FUNCTION #
+#######################
+initialize ( ) {
+
+    # this routine performs a variety of directory-specific
+    # initializations.  some are sanity checks, some are preventive,
+    # and some are necessary setup detection.
+    #
+    # this function sets:
+    #   CONFIGURE
+    #   SEARCH_DIRS
+    #   CONFIG_SUBDIRS
+
+    ##################################
+    # check for a configure template #
+    ##################################
+    CONFIGURE="`locate_configure_template`"
+    if [ "x$CONFIGURE" = "x" ] ; then
+	$ECHO
+	$ECHO "A configure.ac or configure.in file could not be located implying"
+	$ECHO "that the GNU Build System is at least not used in this directory.  In"
+	$ECHO "any case, there is nothing to do here without one of those files."
+	$ECHO
+	$ECHO "ERROR: No configure.in or configure.ac file found in `pwd`"
+	exit 1
+    fi
+
+    #####################
+    # detect an aux dir #
+    #####################
+    _aux_dir="`grep AC_CONFIG_AUX_DIR $CONFIGURE | grep -v '.*#.*AC_CONFIG_AUX_DIR' | tail -${TAIL_N}1 | sed 's/^[ 	]*AC_CONFIG_AUX_DIR(\(.*\)).*/\1/' | sed 's/.*\[\(.*\)\].*/\1/'`"
+    if test ! -d "$_aux_dir" ; then
+	_aux_dir=.
+    else
+	$VERBOSE_ECHO "Detected auxillary directory: $_aux_dir"
+    fi
+
+    ################################
+    # detect a recursive configure #
+    ################################
+    CONFIG_SUBDIRS=""
+    _det_config_subdirs="`grep AC_CONFIG_SUBDIRS $CONFIGURE | grep -v '.*#.*AC_CONFIG_SUBDIRS' | sed 's/^[ 	]*AC_CONFIG_SUBDIRS(\(.*\)).*/\1/' | sed 's/.*\[\(.*\)\].*/\1/'`"
+    for dir in $_det_config_subdirs ; do
+	if test -d "`pwd`/$dir" ; then
+	    $VERBOSE_ECHO "Detected recursive configure directory: `pwd`/$dir"
+	    CONFIG_SUBDIRS="$CONFIG_SUBDIRS `pwd`/$dir"
+	fi
+    done
+
+    ###########################################################
+    # make sure certain required files exist for GNU projects #
+    ###########################################################
+    _marker_found=""
+    _marker_found_message_intro='Detected non-GNU marker "'
+    _marker_found_message_mid='" in '
+    for marker in foreign cygnus ; do
+	_marker_found_message=${_marker_found_message_intro}${marker}${_marker_found_message_mid}
+	_marker_found="`grep 'AM_INIT_AUTOMAKE.*'${marker} $CONFIGURE`"
+	if [ ! "x$_marker_found" = "x" ] ; then
+	    $VERBOSE_ECHO "${_marker_found_message}`basename \"$CONFIGURE\"`"
+	    break
+	fi
+	if test -f "`dirname \"$CONFIGURE\"/Makefile.am`" ; then
+	    _marker_found="`grep 'AUTOMAKE_OPTIONS.*'${marker} Makefile.am`"
+	    if [ ! "x$_marker_found" = "x" ] ; then
+		$VERBOSE_ECHO "${_marker_found_message}Makefile.am"
+		break
+	    fi
+	fi
+    done
+    if [ "x${_marker_found}" = "x" ] ; then
+	_suggest_foreign=no
+	for file in AUTHORS COPYING ChangeLog INSTALL NEWS README ; do
+	    if [ ! -f $file ] ; then
+		$VERBOSE_ECHO "Touching ${file} since it does not exist"
+		_suggest_foreign=yes
+		touch $file
+	    fi
+	done
+
+	if [ "x${_suggest_foreign}" = "xyes" ] ; then
+	    $ECHO
+	    $ECHO "Warning: Several files expected of projects that conform to the GNU"
+	    $ECHO "coding standards were not found.  The files were automatically added"
+	    $ECHO "for you since you do not have a 'foreign' declaration specified."
+	    $ECHO
+	    $ECHO "Considered adding 'foreign' to AM_INIT_AUTOMAKE in `basename \"$CONFIGURE\"`"
+	    if test -f "`dirname \"$CONFIGURE\"/Makefile.am`" ; then
+		$ECHO "or to AUTOMAKE_OPTIONS in your top-level Makefile.am file."
+	    fi
+	    $ECHO
+	fi
+    fi
+
+    ##################################################
+    # make sure certain generated files do not exist #
+    ##################################################
+    for file in config.guess config.sub ltmain.sh ; do
+	if test -f "${_aux_dir}/${file}" ; then
+	    $VERBOSE_ECHO "mv -f \"${_aux_dir}/${file}\" \"${_aux_dir}/${file}.backup\""
+	    mv -f "${_aux_dir}/${file}" "${_aux_dir}/${file}.backup"
+	fi
+    done
+
+    ############################
+    # search alternate m4 dirs #
+    ############################
+    SEARCH_DIRS=""
+    for dir in m4 ; do
+	if [ -d $dir ] ; then
+	    $VERBOSE_ECHO "Found extra aclocal search directory: $dir"
+	    SEARCH_DIRS="$SEARCH_DIRS -I $dir"
+	fi
+    done
+
+    ######################################
+    # remove any previous build products #
+    ######################################
+    if test -d autom4te.cache ; then
+	$VERBOSE_ECHO "Found an autom4te.cache directory, deleting it"
+	$VERBOSE_ECHO "rm -rf autom4te.cache"
+	rm -rf autom4te.cache
+    fi
+# tcl/tk (and probably others) have a customized aclocal.m4, so can't delete it
+#     if test -f aclocal.m4 ; then
+# 	$VERBOSE_ECHO "Found an aclocal.m4 file, deleting it"
+# 	$VERBOSE_ECHO "rm -f aclocal.m4"
+# 	rm -f aclocal.m4
+#     fi
+
+} # end of initialize()
+
+
+##############
+# initialize #
+##############
+
+# stash path
+START_PATH="`pwd`"
+
+# Before running autoreconf or manual steps, some prep detection work
+# is necessary or useful.  Only needs to occur once per directory, but
+# does need to traverse the entire subconfigure hierarchy to protect
+# files from being clobbered even by autoreconf.
+recursive_protect
+
+# start from where we started
+cd "$START_PATH"
+
+# get ready to process
+initialize
+
+
+#########################################
+# DOWNLOAD_GNULIB_CONFIG_GUESS FUNCTION #
+#########################################
+
+# TODO - should make sure wget/curl exist and/or work before trying to
+# use them.
+
+download_gnulib_config_guess () {
+    # abuse gitweb to download gnulib's latest config.guess via HTTP
+    config_guess_temp="config.guess.$$.download"
+    ret=1
+    for __cmd in wget curl fetch ; do
+	$VERBOSE_ECHO "Checking for command ${__cmd}"
+	${__cmd} --version > /dev/null 2>&1
+	ret=$?
+	if [ ! $ret = 0 ] ; then
+	    continue
+        fi
+
+	__cmd_version=`${__cmd} --version | head -n 1 | sed -e 's/^[^0-9]\+//' -e 's/ .*//'`
+	$VERBOSE_ECHO "Found ${__cmd} ${__cmd_version}"
+
+	opts=""
+	case ${__cmd} in
+	    wget)
+		opts="-O" 
+		;;
+	    curl)
+		opts="-o"
+		;;
+	    fetch)
+		opts="-t 5 -f"
+		;;
+	esac
+
+	$VERBOSE_ECHO "Running $__cmd \"${CONFIG_GUESS_URL}\" $opts \"${config_guess_temp}\""
+	eval "$__cmd \"${CONFIG_GUESS_URL}\" $opts \"${config_guess_temp}\"" > /dev/null 2>&1
+	if [ $? = 0 ] ; then
+	    mv -f "${config_guess_temp}" ${_aux_dir}/config.guess
+	    ret=0
+	    break
+	fi
+    done
+
+    if [ ! $ret = 0 ] ; then
+	$ECHO "Warning: config.guess download failed from: $CONFIG_GUESS_URL"
+	rm -f "${config_guess_temp}"
+    fi
+}
+
+
+##############################
+# LIBTOOLIZE_NEEDED FUNCTION #
+##############################
+libtoolize_needed () {
+    ret=1 # means no, don't need libtoolize
+    for feature in AC_PROG_LIBTOOL AM_PROG_LIBTOOL LT_INIT ; do
+	$VERBOSE_ECHO "Searching for $feature in $CONFIGURE"
+	found="`grep \"^$feature.*\" $CONFIGURE`"
+	if [ ! "x$found" = "x" ] ; then
+	    ret=0 # means yes, need to run libtoolize
+	    break
+	fi
+    done
+    return ${ret}
+}
+
+
+
+############################################
+# prepare build via autoreconf or manually #
+############################################
+reconfigure_manually=no
+if [ "x$HAVE_AUTORECONF" = "xyes" ] ; then
+    $ECHO
+    $ECHO $ECHO_N "Automatically preparing build ... $ECHO_C"
+
+    $VERBOSE_ECHO "$AUTORECONF $SEARCH_DIRS $AUTORECONF_OPTIONS"
+    autoreconf_output="`$AUTORECONF $SEARCH_DIRS $AUTORECONF_OPTIONS 2>&1`"
+    ret=$?
+    $VERBOSE_ECHO "$autoreconf_output"
+
+    if [ ! $ret = 0 ] ; then
+	if [ "x$HAVE_ALT_LIBTOOLIZE" = "xyes" ] ; then
+	    if [ ! "x`echo \"$autoreconf_output\" | grep libtoolize | grep \"No such file or directory\"`" = "x" ] ; then
+		$ECHO
+		$ECHO "Warning: autoreconf failed but due to what is usually a common libtool"
+		$ECHO "misconfiguration issue.  This problem is encountered on systems that"
+		$ECHO "have installed libtoolize under a different name without providing a"
+		$ECHO "symbolic link or without setting the LIBTOOLIZE environment variable."
+		$ECHO
+		$ECHO "Restarting the preparation steps with LIBTOOLIZE set to $LIBTOOLIZE"
+
+		export LIBTOOLIZE
+		RUN_RECURSIVE=no
+		export RUN_RECURSIVE
+		untrap_abnormal
+
+		$VERBOSE_ECHO sh $AUTOGEN_SH "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9"
+		sh "$AUTOGEN_SH" "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9"
+		exit $?
+	    fi
+	fi
+
+	$ECHO "Warning: $AUTORECONF failed"
+
+	if test -f ltmain.sh ; then
+	    $ECHO "libtoolize being run by autoreconf is not creating ltmain.sh in the auxillary directory like it should"
+	fi
+
+	$ECHO "Attempting to run the preparation steps individually"
+	reconfigure_manually=yes
+    else
+	if [ "x$DOWNLOAD" = "xyes" ] ; then
+	    if libtoolize_needed ; then
+		download_gnulib_config_guess
+	    fi
+	fi
+    fi
+else
+    reconfigure_manually=yes
+fi
+
+
+############################
+# LIBTOOL_FAILURE FUNCTION #
+############################
+libtool_failure ( ) {
+
+    # libtool is rather error-prone in comparison to the other
+    # autotools and this routine attempts to compensate for some
+    # common failures.  the output after a libtoolize failure is
+    # parsed for an error related to AC_PROG_LIBTOOL and if found, we
+    # attempt to inject a project-provided libtool.m4 file.
+
+    _autoconf_output="$1"
+
+    if [ "x$RUN_RECURSIVE" = "xno" ] ; then
+	# we already tried the libtool.m4, don't try again
+	return 1
+    fi
+
+    if test -f "$LIBTOOL_M4" ; then
+	found_libtool="`$ECHO $_autoconf_output | grep AC_PROG_LIBTOOL`"
+	if test ! "x$found_libtool" = "x" ; then
+	    if test -f acinclude.m4 ; then
+		rm -f acinclude.m4.$$.backup
+		$VERBOSE_ECHO "cat acinclude.m4 > acinclude.m4.$$.backup"
+		cat acinclude.m4 > acinclude.m4.$$.backup
+	    fi
+	    $VERBOSE_ECHO "cat \"$LIBTOOL_M4\" >> acinclude.m4"
+	    chmod u+w acinclude.m4
+	    cat "$LIBTOOL_M4" >> acinclude.m4
+
+	    # don't keep doing this
+	    RUN_RECURSIVE=no
+	    export RUN_RECURSIVE
+	    untrap_abnormal
+
+	    $ECHO
+	    $ECHO "Restarting the preparation steps with libtool macros in acinclude.m4"
+	    $VERBOSE_ECHO sh $AUTOGEN_SH "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9"
+	    sh "$AUTOGEN_SH" "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9"
+	    exit $?
+	fi
+    fi
+}
+
+
+###########################
+# MANUAL_AUTOGEN FUNCTION #
+###########################
+manual_autogen ( ) {
+
+    ##################################################
+    # Manual preparation steps taken are as follows: #
+    #   aclocal [-I m4]                              #
+    #   libtoolize --automake -c -f                  #
+    #   aclocal [-I m4]                              #
+    #   autoconf -f                                  #
+    #   autoheader                                   #
+    #   automake -a -c -f                            #
+    ##################################################
+
+    ###########
+    # aclocal #
+    ###########
+    $VERBOSE_ECHO "$ACLOCAL $SEARCH_DIRS $ACLOCAL_OPTIONS"
+    aclocal_output="`$ACLOCAL $SEARCH_DIRS $ACLOCAL_OPTIONS 2>&1`"
+    ret=$?
+    $VERBOSE_ECHO "$aclocal_output"
+    if [ ! $ret = 0 ] ; then $ECHO "ERROR: $ACLOCAL failed" && exit 2 ; fi
+
+    ##############
+    # libtoolize #
+    ##############
+    if libtoolize_needed ; then
+	if [ "x$HAVE_LIBTOOLIZE" = "xyes" ] ; then
+	    $VERBOSE_ECHO "$LIBTOOLIZE $LIBTOOLIZE_OPTIONS"
+	    libtoolize_output="`$LIBTOOLIZE $LIBTOOLIZE_OPTIONS 2>&1`"
+	    ret=$?
+	    $VERBOSE_ECHO "$libtoolize_output"
+
+	    if [ ! $ret = 0 ] ; then $ECHO "ERROR: $LIBTOOLIZE failed" && exit 2 ; fi
+	else
+	    if [ "x$HAVE_ALT_LIBTOOLIZE" = "xyes" ] ; then
+		$VERBOSE_ECHO "$LIBTOOLIZE $ALT_LIBTOOLIZE_OPTIONS"
+		libtoolize_output="`$LIBTOOLIZE $ALT_LIBTOOLIZE_OPTIONS 2>&1`"
+		ret=$?
+		$VERBOSE_ECHO "$libtoolize_output"
+
+		if [ ! $ret = 0 ] ; then $ECHO "ERROR: $LIBTOOLIZE failed" && exit 2 ; fi
+	    fi
+	fi
+
+	###########
+	# aclocal #
+	###########
+	# re-run again as instructed by libtoolize
+	$VERBOSE_ECHO "$ACLOCAL $SEARCH_DIRS $ACLOCAL_OPTIONS"
+	aclocal_output="`$ACLOCAL $SEARCH_DIRS $ACLOCAL_OPTIONS 2>&1`"
+	ret=$?
+	$VERBOSE_ECHO "$aclocal_output"
+
+	# libtoolize might put ltmain.sh in the wrong place
+	if test -f ltmain.sh ; then
+	    if test ! -f "${_aux_dir}/ltmain.sh" ; then
+		$ECHO
+		$ECHO "Warning:  $LIBTOOLIZE is creating ltmain.sh in the wrong directory"
+		$ECHO
+		$ECHO "Fortunately, the problem can be worked around by simply copying the"
+		$ECHO "file to the appropriate location (${_aux_dir}/).  This has been done for you."
+		$ECHO
+		$VERBOSE_ECHO "cp -p ltmain.sh \"${_aux_dir}/ltmain.sh\""
+		cp -p ltmain.sh "${_aux_dir}/ltmain.sh"
+		$ECHO $ECHO_N "Continuing build preparation ... $ECHO_C"
+	    fi
+	fi # ltmain.sh
+
+	if [ "x$DOWNLOAD" = "xyes" ] ; then
+	    download_gnulib_config_guess
+	fi
+    fi # libtoolize_needed
+
+    ############
+    # autoconf #
+    ############
+    $VERBOSE_ECHO
+    $VERBOSE_ECHO "$AUTOCONF $AUTOCONF_OPTIONS"
+    autoconf_output="`$AUTOCONF $AUTOCONF_OPTIONS 2>&1`"
+    ret=$?
+    $VERBOSE_ECHO "$autoconf_output"
+
+    if [ ! $ret = 0 ] ; then
+	# retry without the -f and check for usage of macros that are too new
+	ac2_59_macros="AC_C_RESTRICT AC_INCLUDES_DEFAULT AC_LANG_ASSERT AC_LANG_WERROR AS_SET_CATFILE"
+	ac2_55_macros="AC_COMPILER_IFELSE AC_FUNC_MBRTOWC AC_HEADER_STDBOOL AC_LANG_CONFTEST AC_LANG_SOURCE AC_LANG_PROGRAM AC_LANG_CALL AC_LANG_FUNC_TRY_LINK AC_MSG_FAILURE AC_PREPROC_IFELSE"
+	ac2_54_macros="AC_C_BACKSLASH_A AC_CONFIG_LIBOBJ_DIR AC_GNU_SOURCE AC_PROG_EGREP AC_PROG_FGREP AC_REPLACE_FNMATCH AC_FUNC_FNMATCH_GNU AC_FUNC_REALLOC AC_TYPE_MBSTATE_T"
+
+	macros_to_search=""
+	ac_major="`echo ${AUTOCONF_VERSION}. | cut -d. -f1 | sed 's/[^0-9]//g'`"
+	ac_minor="`echo ${AUTOCONF_VERSION}. | cut -d. -f2 | sed 's/[^0-9]//g'`"
+
+	if [ $ac_major -lt 2 ] ; then
+	    macros_to_search="$ac2_59_macros $ac2_55_macros $ac2_54_macros"
+	else
+	    if [ $ac_minor -lt 54 ] ; then
+		macros_to_search="$ac2_59_macros $ac2_55_macros $ac2_54_macros"
+	    elif [ $ac_minor -lt 55 ] ; then
+		macros_to_search="$ac2_59_macros $ac2_55_macros"
+	    elif [ $ac_minor -lt 59 ] ; then
+		macros_to_search="$ac2_59_macros"
+	    fi
+	fi
+
+	configure_ac_macros=__none__
+	for feature in $macros_to_search ; do
+	    $VERBOSE_ECHO "Searching for $feature in $CONFIGURE"
+	    found="`grep \"^$feature.*\" $CONFIGURE`"
+	    if [ ! "x$found" = "x" ] ; then
+		if [ "x$configure_ac_macros" = "x__none__" ] ; then
+		    configure_ac_macros="$feature"
+		else
+		    configure_ac_macros="$feature $configure_ac_macros"
+		fi
+	    fi
+	done
+	if [ ! "x$configure_ac_macros" = "x__none__" ] ; then
+	    $ECHO
+	    $ECHO "Warning:  Unsupported macros were found in $CONFIGURE"
+	    $ECHO
+	    $ECHO "The `basename \"$CONFIGURE\"` file was scanned in order to determine if any"
+	    $ECHO "unsupported macros are used that exceed the minimum version"
+	    $ECHO "settings specified within this file.  As such, the following macros"
+	    $ECHO "should be removed from configure.ac or the version numbers in this"
+	    $ECHO "file should be increased:"
+	    $ECHO
+	    $ECHO "$configure_ac_macros"
+	    $ECHO
+	    $ECHO $ECHO_N "Ignorantly continuing build preparation ... $ECHO_C"
+	fi
+
+	###################
+	# autoconf, retry #
+	###################
+	$VERBOSE_ECHO
+	$VERBOSE_ECHO "$AUTOCONF"
+	autoconf_output="`$AUTOCONF 2>&1`"
+	ret=$?
+	$VERBOSE_ECHO "$autoconf_output"
+
+	if [ ! $ret = 0 ] ; then
+	    # test if libtool is busted
+	    libtool_failure "$autoconf_output"
+
+	    # let the user know what went wrong
+	    cat <<EOF
+$autoconf_output
+EOF
+	    $ECHO "ERROR: $AUTOCONF failed"
+	    exit 2
+	else
+	    # autoconf sans -f and possibly sans unsupported options succeed so warn verbosely
+	    $ECHO
+	    $ECHO "Warning: autoconf seems to have succeeded by removing the following options:"
+	    $ECHO "	AUTOCONF_OPTIONS=\"$AUTOCONF_OPTIONS\""
+	    $ECHO
+	    $ECHO "Removing those options should not be necessary and indicate some other"
+	    $ECHO "problem with the build system.  The build preparation is highly suspect"
+	    $ECHO "and may result in configuration or compilation errors.  Consider"
+	    if [ "x$VERBOSE_ECHO" = "x:" ] ; then
+		$ECHO "rerunning the build preparation with verbose output enabled."
+		$ECHO "	$AUTOGEN_SH --verbose"
+	    else
+		$ECHO "reviewing the minimum GNU Autotools version settings contained in"
+		$ECHO "this script along with the macros being used in your `basename \"$CONFIGURE\"` file."
+	    fi
+	    $ECHO
+	    $ECHO $ECHO_N "Continuing build preparation ... $ECHO_C"
+	fi # autoconf ret = 0
+    fi # autoconf ret = 0
+
+    ##############
+    # autoheader #
+    ##############
+    need_autoheader=no
+    for feature in AM_CONFIG_HEADER AC_CONFIG_HEADER ; do
+	$VERBOSE_ECHO "Searching for $feature in $CONFIGURE"
+	found="`grep \"^$feature.*\" $CONFIGURE`"
+	if [ ! "x$found" = "x" ] ; then
+	    need_autoheader=yes
+	    break
+	fi
+    done
+    if [ "x$need_autoheader" = "xyes" ] ; then
+	$VERBOSE_ECHO "$AUTOHEADER $AUTOHEADER_OPTIONS"
+	autoheader_output="`$AUTOHEADER $AUTOHEADER_OPTIONS 2>&1`"
+	ret=$?
+	$VERBOSE_ECHO "$autoheader_output"
+	if [ ! $ret = 0 ] ; then $ECHO "ERROR: $AUTOHEADER failed" && exit 2 ; fi
+    fi # need_autoheader
+
+    ############
+    # automake #
+    ############
+    need_automake=no
+    for feature in AM_INIT_AUTOMAKE ; do
+	$VERBOSE_ECHO "Searching for $feature in $CONFIGURE"
+	found="`grep \"^$feature.*\" $CONFIGURE`"
+	if [ ! "x$found" = "x" ] ; then
+	    need_automake=yes
+	    break
+	fi
+    done
+
+    if [ "x$need_automake" = "xyes" ] ; then
+	$VERBOSE_ECHO "$AUTOMAKE $AUTOMAKE_OPTIONS"
+	automake_output="`$AUTOMAKE $AUTOMAKE_OPTIONS 2>&1`"
+	ret=$?
+	$VERBOSE_ECHO "$automake_output"
+
+	if [ ! $ret = 0 ] ; then
+
+	    ###################
+	    # automake, retry #
+	    ###################
+	    $VERBOSE_ECHO
+	    $VERBOSE_ECHO "$AUTOMAKE $ALT_AUTOMAKE_OPTIONS"
+	    # retry without the -f
+	    automake_output="`$AUTOMAKE $ALT_AUTOMAKE_OPTIONS 2>&1`"
+	    ret=$?
+	    $VERBOSE_ECHO "$automake_output"
+
+	    if [ ! $ret = 0 ] ; then
+	 	# test if libtool is busted
+		libtool_failure "$automake_output"
+
+		# let the user know what went wrong
+		cat <<EOF
+$automake_output
+EOF
+		$ECHO "ERROR: $AUTOMAKE failed"
+		exit 2
+	    fi # automake retry
+	fi # automake ret = 0
+    fi # need_automake
+} # end of manual_autogen
+
+
+#####################################
+# RECURSIVE_MANUAL_AUTOGEN FUNCTION #
+#####################################
+recursive_manual_autogen ( ) {
+
+    # run the build preparation steps manually for this directory
+    manual_autogen
+
+    # for projects using recursive configure, run the build
+    # preparation steps for the subdirectories.
+    if [ ! "x$CONFIG_SUBDIRS" = "x" ] ; then
+	$VERBOSE_ECHO "Recursively configuring the following directories:"
+	$VERBOSE_ECHO "  $CONFIG_SUBDIRS"
+	for dir in $CONFIG_SUBDIRS ; do
+	    $VERBOSE_ECHO "Processing recursive configure in $dir"
+	    cd "$START_PATH"
+	    cd "$dir"
+
+	    # new directory, prepare
+	    initialize
+
+	    # run manual steps for the subdir and any others below
+	    recursive_manual_autogen
+	done
+    fi
+}
+
+
+################################
+# run manual preparation steps #
+################################
+if [ "x$reconfigure_manually" = "xyes" ] ; then
+    $ECHO
+    $ECHO $ECHO_N "Preparing build ... $ECHO_C"
+
+    recursive_manual_autogen
+fi
+
+
+#########################
+# restore and summarize #
+#########################
+cd "$START_PATH"
+
+# restore COPYING and INSTALL from backup if necessary
+recursive_restore
+
+# make sure we end up with a configure script
+config_ac="`locate_configure_template`"
+config="`echo $config_ac | sed 's/\.ac$//' | sed 's/\.in$//'`"
+if [ "x$config" = "x" ] ; then
+    $VERBOSE_ECHO "Could not locate the configure template (from `pwd`)"
+fi
+
+# summarize
+$ECHO "done"
+$ECHO
+if test "x$config" = "x" -o ! -f "$config" ; then
+    $ECHO "WARNING: The $PROJECT build system should now be prepared but there"
+    $ECHO "does not seem to be a resulting configure file.  This is unexpected"
+    $ECHO "and likely the result of an error.  You should run $NAME_OF_AUTOGEN"
+    $ECHO "with the --verbose option to get more details on a potential"
+    $ECHO "misconfiguration."
+else
+    $ECHO "The $PROJECT build system is now prepared.  To build here, run:"
+    $ECHO "  $config"
+    $ECHO "  make"
+fi
+
+
+# Local Variables:
+# mode: sh
+# tab-width: 8
+# sh-basic-offset: 4
+# sh-indentation: 4
+# indent-tabs-mode: t
+# End:
+# ex: shiftwidth=4 tabstop=8
diff --git a/build_debian.sh b/build_debian.sh
new file mode 100755
index 0000000..72dcfff
--- /dev/null
+++ b/build_debian.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# If this script fails on a Debian 4.0 ("etch") system then read
+# the debian/README.debian4 file.
+
+echo "chmod +x debian/rules"
+chmod +x debian/rules
+
+# in some environments the '-rfakeroot' can cause a failure (e.g. when
+# building as root). If so, remove that argument from the following:
+echo "dpkg-buildpackage -b -rfakeroot -us -uc"
+dpkg-buildpackage -b -rfakeroot -us -uc
+
+# If the above succeeds then the ".deb" binary package is placed in the
+# parent directory.
diff --git a/compile b/compile
new file mode 100755
index 0000000..df363c8
--- /dev/null
+++ b/compile
@@ -0,0 +1,348 @@
+#! /bin/sh
+# Wrapper for compilers which do not understand '-c -o'.
+
+scriptversion=2018-03-07.03; # UTC
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+# Written by Tom Tromey <tromey@cygnus.com>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# This file is maintained in Automake, please report
+# bugs to <bug-automake@gnu.org> or send patches to
+# <automake-patches@gnu.org>.
+
+nl='
+'
+
+# We need space, tab and new line, in precisely that order.  Quoting is
+# there to prevent tools from complaining about whitespace usage.
+IFS=" ""	$nl"
+
+file_conv=
+
+# func_file_conv build_file lazy
+# Convert a $build file to $host form and store it in $file
+# Currently only supports Windows hosts. If the determined conversion
+# type is listed in (the comma separated) LAZY, no conversion will
+# take place.
+func_file_conv ()
+{
+  file=$1
+  case $file in
+    / | /[!/]*) # absolute file, and not a UNC file
+      if test -z "$file_conv"; then
+	# lazily determine how to convert abs files
+	case `uname -s` in
+	  MINGW*)
+	    file_conv=mingw
+	    ;;
+	  CYGWIN* | MSYS*)
+	    file_conv=cygwin
+	    ;;
+	  *)
+	    file_conv=wine
+	    ;;
+	esac
+      fi
+      case $file_conv/,$2, in
+	*,$file_conv,*)
+	  ;;
+	mingw/*)
+	  file=`cmd //C echo "$file " | sed -e 's/"\(.*\) " *$/\1/'`
+	  ;;
+	cygwin/* | msys/*)
+	  file=`cygpath -m "$file" || echo "$file"`
+	  ;;
+	wine/*)
+	  file=`winepath -w "$file" || echo "$file"`
+	  ;;
+      esac
+      ;;
+  esac
+}
+
+# func_cl_dashL linkdir
+# Make cl look for libraries in LINKDIR
+func_cl_dashL ()
+{
+  func_file_conv "$1"
+  if test -z "$lib_path"; then
+    lib_path=$file
+  else
+    lib_path="$lib_path;$file"
+  fi
+  linker_opts="$linker_opts -LIBPATH:$file"
+}
+
+# func_cl_dashl library
+# Do a library search-path lookup for cl
+func_cl_dashl ()
+{
+  lib=$1
+  found=no
+  save_IFS=$IFS
+  IFS=';'
+  for dir in $lib_path $LIB
+  do
+    IFS=$save_IFS
+    if $shared && test -f "$dir/$lib.dll.lib"; then
+      found=yes
+      lib=$dir/$lib.dll.lib
+      break
+    fi
+    if test -f "$dir/$lib.lib"; then
+      found=yes
+      lib=$dir/$lib.lib
+      break
+    fi
+    if test -f "$dir/lib$lib.a"; then
+      found=yes
+      lib=$dir/lib$lib.a
+      break
+    fi
+  done
+  IFS=$save_IFS
+
+  if test "$found" != yes; then
+    lib=$lib.lib
+  fi
+}
+
+# func_cl_wrapper cl arg...
+# Adjust compile command to suit cl
+func_cl_wrapper ()
+{
+  # Assume a capable shell
+  lib_path=
+  shared=:
+  linker_opts=
+  for arg
+  do
+    if test -n "$eat"; then
+      eat=
+    else
+      case $1 in
+	-o)
+	  # configure might choose to run compile as 'compile cc -o foo foo.c'.
+	  eat=1
+	  case $2 in
+	    *.o | *.[oO][bB][jJ])
+	      func_file_conv "$2"
+	      set x "$@" -Fo"$file"
+	      shift
+	      ;;
+	    *)
+	      func_file_conv "$2"
+	      set x "$@" -Fe"$file"
+	      shift
+	      ;;
+	  esac
+	  ;;
+	-I)
+	  eat=1
+	  func_file_conv "$2" mingw
+	  set x "$@" -I"$file"
+	  shift
+	  ;;
+	-I*)
+	  func_file_conv "${1#-I}" mingw
+	  set x "$@" -I"$file"
+	  shift
+	  ;;
+	-l)
+	  eat=1
+	  func_cl_dashl "$2"
+	  set x "$@" "$lib"
+	  shift
+	  ;;
+	-l*)
+	  func_cl_dashl "${1#-l}"
+	  set x "$@" "$lib"
+	  shift
+	  ;;
+	-L)
+	  eat=1
+	  func_cl_dashL "$2"
+	  ;;
+	-L*)
+	  func_cl_dashL "${1#-L}"
+	  ;;
+	-static)
+	  shared=false
+	  ;;
+	-Wl,*)
+	  arg=${1#-Wl,}
+	  save_ifs="$IFS"; IFS=','
+	  for flag in $arg; do
+	    IFS="$save_ifs"
+	    linker_opts="$linker_opts $flag"
+	  done
+	  IFS="$save_ifs"
+	  ;;
+	-Xlinker)
+	  eat=1
+	  linker_opts="$linker_opts $2"
+	  ;;
+	-*)
+	  set x "$@" "$1"
+	  shift
+	  ;;
+	*.cc | *.CC | *.cxx | *.CXX | *.[cC]++)
+	  func_file_conv "$1"
+	  set x "$@" -Tp"$file"
+	  shift
+	  ;;
+	*.c | *.cpp | *.CPP | *.lib | *.LIB | *.Lib | *.OBJ | *.obj | *.[oO])
+	  func_file_conv "$1" mingw
+	  set x "$@" "$file"
+	  shift
+	  ;;
+	*)
+	  set x "$@" "$1"
+	  shift
+	  ;;
+      esac
+    fi
+    shift
+  done
+  if test -n "$linker_opts"; then
+    linker_opts="-link$linker_opts"
+  fi
+  exec "$@" $linker_opts
+  exit 1
+}
+
+eat=
+
+case $1 in
+  '')
+     echo "$0: No command.  Try '$0 --help' for more information." 1>&2
+     exit 1;
+     ;;
+  -h | --h*)
+    cat <<\EOF
+Usage: compile [--help] [--version] PROGRAM [ARGS]
+
+Wrapper for compilers which do not understand '-c -o'.
+Remove '-o dest.o' from ARGS, run PROGRAM with the remaining
+arguments, and rename the output as expected.
+
+If you are trying to build a whole package this is not the
+right script to run: please start by reading the file 'INSTALL'.
+
+Report bugs to <bug-automake@gnu.org>.
+EOF
+    exit $?
+    ;;
+  -v | --v*)
+    echo "compile $scriptversion"
+    exit $?
+    ;;
+  cl | *[/\\]cl | cl.exe | *[/\\]cl.exe | \
+  icl | *[/\\]icl | icl.exe | *[/\\]icl.exe )
+    func_cl_wrapper "$@"      # Doesn't return...
+    ;;
+esac
+
+ofile=
+cfile=
+
+for arg
+do
+  if test -n "$eat"; then
+    eat=
+  else
+    case $1 in
+      -o)
+	# configure might choose to run compile as 'compile cc -o foo foo.c'.
+	# So we strip '-o arg' only if arg is an object.
+	eat=1
+	case $2 in
+	  *.o | *.obj)
+	    ofile=$2
+	    ;;
+	  *)
+	    set x "$@" -o "$2"
+	    shift
+	    ;;
+	esac
+	;;
+      *.c)
+	cfile=$1
+	set x "$@" "$1"
+	shift
+	;;
+      *)
+	set x "$@" "$1"
+	shift
+	;;
+    esac
+  fi
+  shift
+done
+
+if test -z "$ofile" || test -z "$cfile"; then
+  # If no '-o' option was seen then we might have been invoked from a
+  # pattern rule where we don't need one.  That is ok -- this is a
+  # normal compilation that the losing compiler can handle.  If no
+  # '.c' file was seen then we are probably linking.  That is also
+  # ok.
+  exec "$@"
+fi
+
+# Name of file we expect compiler to create.
+cofile=`echo "$cfile" | sed 's|^.*[\\/]||; s|^[a-zA-Z]:||; s/\.c$/.o/'`
+
+# Create the lock directory.
+# Note: use '[/\\:.-]' here to ensure that we don't use the same name
+# that we are using for the .o file.  Also, base the name on the expected
+# object file name, since that is what matters with a parallel build.
+lockdir=`echo "$cofile" | sed -e 's|[/\\:.-]|_|g'`.d
+while true; do
+  if mkdir "$lockdir" >/dev/null 2>&1; then
+    break
+  fi
+  sleep 1
+done
+# FIXME: race condition here if user kills between mkdir and trap.
+trap "rmdir '$lockdir'; exit 1" 1 2 15
+
+# Run the compile.
+"$@"
+ret=$?
+
+if test -f "$cofile"; then
+  test "$cofile" = "$ofile" || mv "$cofile" "$ofile"
+elif test -f "${cofile}bj"; then
+  test "${cofile}bj" = "$ofile" || mv "${cofile}bj" "$ofile"
+fi
+
+rmdir "$lockdir"
+exit $ret
+
+# Local Variables:
+# mode: shell-script
+# sh-indentation: 2
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/config.guess b/config.guess
new file mode 100755
index 0000000..7f76b62
--- /dev/null
+++ b/config.guess
@@ -0,0 +1,1754 @@
+#! /bin/sh
+# Attempt to guess a canonical system name.
+#   Copyright 1992-2022 Free Software Foundation, Inc.
+
+# shellcheck disable=SC2006,SC2268 # see below for rationale
+
+timestamp='2022-01-09'
+
+# This file is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses/>.
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that
+# program.  This Exception is an additional permission under section 7
+# of the GNU General Public License, version 3 ("GPLv3").
+#
+# Originally written by Per Bothner; maintained since 2000 by Ben Elliston.
+#
+# You can get the latest version of this script from:
+# https://git.savannah.gnu.org/cgit/config.git/plain/config.guess
+#
+# Please send patches to <config-patches@gnu.org>.
+
+
+# The "shellcheck disable" line above the timestamp inhibits complaints
+# about features and limitations of the classic Bourne shell that were
+# superseded or lifted in POSIX.  However, this script identifies a wide
+# variety of pre-POSIX systems that do not have POSIX shells at all, and
+# even some reasonably current systems (Solaris 10 as case-in-point) still
+# have a pre-POSIX /bin/sh.
+
+
+me=`echo "$0" | sed -e 's,.*/,,'`
+
+usage="\
+Usage: $0 [OPTION]
+
+Output the configuration name of the system \`$me' is run on.
+
+Options:
+  -h, --help         print this help, then exit
+  -t, --time-stamp   print date of last modification, then exit
+  -v, --version      print version number, then exit
+
+Report bugs and patches to <config-patches@gnu.org>."
+
+version="\
+GNU config.guess ($timestamp)
+
+Originally written by Per Bothner.
+Copyright 1992-2022 Free Software Foundation, Inc.
+
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
+
+help="
+Try \`$me --help' for more information."
+
+# Parse command line
+while test $# -gt 0 ; do
+  case $1 in
+    --time-stamp | --time* | -t )
+       echo "$timestamp" ; exit ;;
+    --version | -v )
+       echo "$version" ; exit ;;
+    --help | --h* | -h )
+       echo "$usage"; exit ;;
+    -- )     # Stop option processing
+       shift; break ;;
+    - )	# Use stdin as input.
+       break ;;
+    -* )
+       echo "$me: invalid option $1$help" >&2
+       exit 1 ;;
+    * )
+       break ;;
+  esac
+done
+
+if test $# != 0; then
+  echo "$me: too many arguments$help" >&2
+  exit 1
+fi
+
+# Just in case it came from the environment.
+GUESS=
+
+# CC_FOR_BUILD -- compiler used by this script. Note that the use of a
+# compiler to aid in system detection is discouraged as it requires
+# temporary files to be created and, as you can see below, it is a
+# headache to deal with in a portable fashion.
+
+# Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still
+# use `HOST_CC' if defined, but it is deprecated.
+
+# Portable tmp directory creation inspired by the Autoconf team.
+
+tmp=
+# shellcheck disable=SC2172
+trap 'test -z "$tmp" || rm -fr "$tmp"' 0 1 2 13 15
+
+set_cc_for_build() {
+    # prevent multiple calls if $tmp is already set
+    test "$tmp" && return 0
+    : "${TMPDIR=/tmp}"
+    # shellcheck disable=SC2039,SC3028
+    { tmp=`(umask 077 && mktemp -d "$TMPDIR/cgXXXXXX") 2>/dev/null` && test -n "$tmp" && test -d "$tmp" ; } ||
+	{ test -n "$RANDOM" && tmp=$TMPDIR/cg$$-$RANDOM && (umask 077 && mkdir "$tmp" 2>/dev/null) ; } ||
+	{ tmp=$TMPDIR/cg-$$ && (umask 077 && mkdir "$tmp" 2>/dev/null) && echo "Warning: creating insecure temp directory" >&2 ; } ||
+	{ echo "$me: cannot create a temporary directory in $TMPDIR" >&2 ; exit 1 ; }
+    dummy=$tmp/dummy
+    case ${CC_FOR_BUILD-},${HOST_CC-},${CC-} in
+	,,)    echo "int x;" > "$dummy.c"
+	       for driver in cc gcc c89 c99 ; do
+		   if ($driver -c -o "$dummy.o" "$dummy.c") >/dev/null 2>&1 ; then
+		       CC_FOR_BUILD=$driver
+		       break
+		   fi
+	       done
+	       if test x"$CC_FOR_BUILD" = x ; then
+		   CC_FOR_BUILD=no_compiler_found
+	       fi
+	       ;;
+	,,*)   CC_FOR_BUILD=$CC ;;
+	,*,*)  CC_FOR_BUILD=$HOST_CC ;;
+    esac
+}
+
+# This is needed to find uname on a Pyramid OSx when run in the BSD universe.
+# (ghazi@noc.rutgers.edu 1994-08-24)
+if test -f /.attbin/uname ; then
+	PATH=$PATH:/.attbin ; export PATH
+fi
+
+UNAME_MACHINE=`(uname -m) 2>/dev/null` || UNAME_MACHINE=unknown
+UNAME_RELEASE=`(uname -r) 2>/dev/null` || UNAME_RELEASE=unknown
+UNAME_SYSTEM=`(uname -s) 2>/dev/null` || UNAME_SYSTEM=unknown
+UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown
+
+case $UNAME_SYSTEM in
+Linux|GNU|GNU/*)
+	LIBC=unknown
+
+	set_cc_for_build
+	cat <<-EOF > "$dummy.c"
+	#include <features.h>
+	#if defined(__UCLIBC__)
+	LIBC=uclibc
+	#elif defined(__dietlibc__)
+	LIBC=dietlibc
+	#elif defined(__GLIBC__)
+	LIBC=gnu
+	#else
+	#include <stdarg.h>
+	/* First heuristic to detect musl libc.  */
+	#ifdef __DEFINED_va_list
+	LIBC=musl
+	#endif
+	#endif
+	EOF
+	cc_set_libc=`$CC_FOR_BUILD -E "$dummy.c" 2>/dev/null | grep '^LIBC' | sed 's, ,,g'`
+	eval "$cc_set_libc"
+
+	# Second heuristic to detect musl libc.
+	if [ "$LIBC" = unknown ] &&
+	   command -v ldd >/dev/null &&
+	   ldd --version 2>&1 | grep -q ^musl; then
+		LIBC=musl
+	fi
+
+	# If the system lacks a compiler, then just pick glibc.
+	# We could probably try harder.
+	if [ "$LIBC" = unknown ]; then
+		LIBC=gnu
+	fi
+	;;
+esac
+
+# Note: order is significant - the case branches are not exclusive.
+
+case $UNAME_MACHINE:$UNAME_SYSTEM:$UNAME_RELEASE:$UNAME_VERSION in
+    *:NetBSD:*:*)
+	# NetBSD (nbsd) targets should (where applicable) match one or
+	# more of the tuples: *-*-netbsdelf*, *-*-netbsdaout*,
+	# *-*-netbsdecoff* and *-*-netbsd*.  For targets that recently
+	# switched to ELF, *-*-netbsd* would select the old
+	# object file format.  This provides both forward
+	# compatibility and a consistent mechanism for selecting the
+	# object file format.
+	#
+	# Note: NetBSD doesn't particularly care about the vendor
+	# portion of the name.  We always set it to "unknown".
+	UNAME_MACHINE_ARCH=`(uname -p 2>/dev/null || \
+	    /sbin/sysctl -n hw.machine_arch 2>/dev/null || \
+	    /usr/sbin/sysctl -n hw.machine_arch 2>/dev/null || \
+	    echo unknown)`
+	case $UNAME_MACHINE_ARCH in
+	    aarch64eb) machine=aarch64_be-unknown ;;
+	    armeb) machine=armeb-unknown ;;
+	    arm*) machine=arm-unknown ;;
+	    sh3el) machine=shl-unknown ;;
+	    sh3eb) machine=sh-unknown ;;
+	    sh5el) machine=sh5le-unknown ;;
+	    earmv*)
+		arch=`echo "$UNAME_MACHINE_ARCH" | sed -e 's,^e\(armv[0-9]\).*$,\1,'`
+		endian=`echo "$UNAME_MACHINE_ARCH" | sed -ne 's,^.*\(eb\)$,\1,p'`
+		machine=${arch}${endian}-unknown
+		;;
+	    *) machine=$UNAME_MACHINE_ARCH-unknown ;;
+	esac
+	# The Operating System including object format, if it has switched
+	# to ELF recently (or will in the future) and ABI.
+	case $UNAME_MACHINE_ARCH in
+	    earm*)
+		os=netbsdelf
+		;;
+	    arm*|i386|m68k|ns32k|sh3*|sparc|vax)
+		set_cc_for_build
+		if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \
+			| grep -q __ELF__
+		then
+		    # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout).
+		    # Return netbsd for either.  FIX?
+		    os=netbsd
+		else
+		    os=netbsdelf
+		fi
+		;;
+	    *)
+		os=netbsd
+		;;
+	esac
+	# Determine ABI tags.
+	case $UNAME_MACHINE_ARCH in
+	    earm*)
+		expr='s/^earmv[0-9]/-eabi/;s/eb$//'
+		abi=`echo "$UNAME_MACHINE_ARCH" | sed -e "$expr"`
+		;;
+	esac
+	# The OS release
+	# Debian GNU/NetBSD machines have a different userland, and
+	# thus, need a distinct triplet. However, they do not need
+	# kernel version information, so it can be replaced with a
+	# suitable tag, in the style of linux-gnu.
+	case $UNAME_VERSION in
+	    Debian*)
+		release='-gnu'
+		;;
+	    *)
+		release=`echo "$UNAME_RELEASE" | sed -e 's/[-_].*//' | cut -d. -f1,2`
+		;;
+	esac
+	# Since CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM:
+	# contains redundant information, the shorter form:
+	# CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used.
+	GUESS=$machine-${os}${release}${abi-}
+	;;
+    *:Bitrig:*:*)
+	UNAME_MACHINE_ARCH=`arch | sed 's/Bitrig.//'`
+	GUESS=$UNAME_MACHINE_ARCH-unknown-bitrig$UNAME_RELEASE
+	;;
+    *:OpenBSD:*:*)
+	UNAME_MACHINE_ARCH=`arch | sed 's/OpenBSD.//'`
+	GUESS=$UNAME_MACHINE_ARCH-unknown-openbsd$UNAME_RELEASE
+	;;
+    *:SecBSD:*:*)
+	UNAME_MACHINE_ARCH=`arch | sed 's/SecBSD.//'`
+	GUESS=$UNAME_MACHINE_ARCH-unknown-secbsd$UNAME_RELEASE
+	;;
+    *:LibertyBSD:*:*)
+	UNAME_MACHINE_ARCH=`arch | sed 's/^.*BSD\.//'`
+	GUESS=$UNAME_MACHINE_ARCH-unknown-libertybsd$UNAME_RELEASE
+	;;
+    *:MidnightBSD:*:*)
+	GUESS=$UNAME_MACHINE-unknown-midnightbsd$UNAME_RELEASE
+	;;
+    *:ekkoBSD:*:*)
+	GUESS=$UNAME_MACHINE-unknown-ekkobsd$UNAME_RELEASE
+	;;
+    *:SolidBSD:*:*)
+	GUESS=$UNAME_MACHINE-unknown-solidbsd$UNAME_RELEASE
+	;;
+    *:OS108:*:*)
+	GUESS=$UNAME_MACHINE-unknown-os108_$UNAME_RELEASE
+	;;
+    macppc:MirBSD:*:*)
+	GUESS=powerpc-unknown-mirbsd$UNAME_RELEASE
+	;;
+    *:MirBSD:*:*)
+	GUESS=$UNAME_MACHINE-unknown-mirbsd$UNAME_RELEASE
+	;;
+    *:Sortix:*:*)
+	GUESS=$UNAME_MACHINE-unknown-sortix
+	;;
+    *:Twizzler:*:*)
+	GUESS=$UNAME_MACHINE-unknown-twizzler
+	;;
+    *:Redox:*:*)
+	GUESS=$UNAME_MACHINE-unknown-redox
+	;;
+    mips:OSF1:*.*)
+	GUESS=mips-dec-osf1
+	;;
+    alpha:OSF1:*:*)
+	# Reset EXIT trap before exiting to avoid spurious non-zero exit code.
+	trap '' 0
+	case $UNAME_RELEASE in
+	*4.0)
+		UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'`
+		;;
+	*5.*)
+		UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $4}'`
+		;;
+	esac
+	# According to Compaq, /usr/sbin/psrinfo has been available on
+	# OSF/1 and Tru64 systems produced since 1995.  I hope that
+	# covers most systems running today.  This code pipes the CPU
+	# types through head -n 1, so we only detect the type of CPU 0.
+	ALPHA_CPU_TYPE=`/usr/sbin/psrinfo -v | sed -n -e 's/^  The alpha \(.*\) processor.*$/\1/p' | head -n 1`
+	case $ALPHA_CPU_TYPE in
+	    "EV4 (21064)")
+		UNAME_MACHINE=alpha ;;
+	    "EV4.5 (21064)")
+		UNAME_MACHINE=alpha ;;
+	    "LCA4 (21066/21068)")
+		UNAME_MACHINE=alpha ;;
+	    "EV5 (21164)")
+		UNAME_MACHINE=alphaev5 ;;
+	    "EV5.6 (21164A)")
+		UNAME_MACHINE=alphaev56 ;;
+	    "EV5.6 (21164PC)")
+		UNAME_MACHINE=alphapca56 ;;
+	    "EV5.7 (21164PC)")
+		UNAME_MACHINE=alphapca57 ;;
+	    "EV6 (21264)")
+		UNAME_MACHINE=alphaev6 ;;
+	    "EV6.7 (21264A)")
+		UNAME_MACHINE=alphaev67 ;;
+	    "EV6.8CB (21264C)")
+		UNAME_MACHINE=alphaev68 ;;
+	    "EV6.8AL (21264B)")
+		UNAME_MACHINE=alphaev68 ;;
+	    "EV6.8CX (21264D)")
+		UNAME_MACHINE=alphaev68 ;;
+	    "EV6.9A (21264/EV69A)")
+		UNAME_MACHINE=alphaev69 ;;
+	    "EV7 (21364)")
+		UNAME_MACHINE=alphaev7 ;;
+	    "EV7.9 (21364A)")
+		UNAME_MACHINE=alphaev79 ;;
+	esac
+	# A Pn.n version is a patched version.
+	# A Vn.n version is a released version.
+	# A Tn.n version is a released field test version.
+	# A Xn.n version is an unreleased experimental baselevel.
+	# 1.2 uses "1.2" for uname -r.
+	OSF_REL=`echo "$UNAME_RELEASE" | sed -e 's/^[PVTX]//' | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz`
+	GUESS=$UNAME_MACHINE-dec-osf$OSF_REL
+	;;
+    Amiga*:UNIX_System_V:4.0:*)
+	GUESS=m68k-unknown-sysv4
+	;;
+    *:[Aa]miga[Oo][Ss]:*:*)
+	GUESS=$UNAME_MACHINE-unknown-amigaos
+	;;
+    *:[Mm]orph[Oo][Ss]:*:*)
+	GUESS=$UNAME_MACHINE-unknown-morphos
+	;;
+    *:OS/390:*:*)
+	GUESS=i370-ibm-openedition
+	;;
+    *:z/VM:*:*)
+	GUESS=s390-ibm-zvmoe
+	;;
+    *:OS400:*:*)
+	GUESS=powerpc-ibm-os400
+	;;
+    arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*)
+	GUESS=arm-acorn-riscix$UNAME_RELEASE
+	;;
+    arm*:riscos:*:*|arm*:RISCOS:*:*)
+	GUESS=arm-unknown-riscos
+	;;
+    SR2?01:HI-UX/MPP:*:* | SR8000:HI-UX/MPP:*:*)
+	GUESS=hppa1.1-hitachi-hiuxmpp
+	;;
+    Pyramid*:OSx*:*:* | MIS*:OSx*:*:* | MIS*:SMP_DC-OSx*:*:*)
+	# akee@wpdis03.wpafb.af.mil (Earle F. Ake) contributed MIS and NILE.
+	case `(/bin/universe) 2>/dev/null` in
+	    att) GUESS=pyramid-pyramid-sysv3 ;;
+	    *)   GUESS=pyramid-pyramid-bsd   ;;
+	esac
+	;;
+    NILE*:*:*:dcosx)
+	GUESS=pyramid-pyramid-svr4
+	;;
+    DRS?6000:unix:4.0:6*)
+	GUESS=sparc-icl-nx6
+	;;
+    DRS?6000:UNIX_SV:4.2*:7* | DRS?6000:isis:4.2*:7*)
+	case `/usr/bin/uname -p` in
+	    sparc) GUESS=sparc-icl-nx7 ;;
+	esac
+	;;
+    s390x:SunOS:*:*)
+	SUN_REL=`echo "$UNAME_RELEASE" | sed -e 's/[^.]*//'`
+	GUESS=$UNAME_MACHINE-ibm-solaris2$SUN_REL
+	;;
+    sun4H:SunOS:5.*:*)
+	SUN_REL=`echo "$UNAME_RELEASE" | sed -e 's/[^.]*//'`
+	GUESS=sparc-hal-solaris2$SUN_REL
+	;;
+    sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*)
+	SUN_REL=`echo "$UNAME_RELEASE" | sed -e 's/[^.]*//'`
+	GUESS=sparc-sun-solaris2$SUN_REL
+	;;
+    i86pc:AuroraUX:5.*:* | i86xen:AuroraUX:5.*:*)
+	GUESS=i386-pc-auroraux$UNAME_RELEASE
+	;;
+    i86pc:SunOS:5.*:* | i86xen:SunOS:5.*:*)
+	set_cc_for_build
+	SUN_ARCH=i386
+	# If there is a compiler, see if it is configured for 64-bit objects.
+	# Note that the Sun cc does not turn __LP64__ into 1 like gcc does.
+	# This test works for both compilers.
+	if test "$CC_FOR_BUILD" != no_compiler_found; then
+	    if (echo '#ifdef __amd64'; echo IS_64BIT_ARCH; echo '#endif') | \
+		(CCOPTS="" $CC_FOR_BUILD -m64 -E - 2>/dev/null) | \
+		grep IS_64BIT_ARCH >/dev/null
+	    then
+		SUN_ARCH=x86_64
+	    fi
+	fi
+	SUN_REL=`echo "$UNAME_RELEASE" | sed -e 's/[^.]*//'`
+	GUESS=$SUN_ARCH-pc-solaris2$SUN_REL
+	;;
+    sun4*:SunOS:6*:*)
+	# According to config.sub, this is the proper way to canonicalize
+	# SunOS6.  Hard to guess exactly what SunOS6 will be like, but
+	# it's likely to be more like Solaris than SunOS4.
+	SUN_REL=`echo "$UNAME_RELEASE" | sed -e 's/[^.]*//'`
+	GUESS=sparc-sun-solaris3$SUN_REL
+	;;
+    sun4*:SunOS:*:*)
+	case `/usr/bin/arch -k` in
+	    Series*|S4*)
+		UNAME_RELEASE=`uname -v`
+		;;
+	esac
+	# Japanese Language versions have a version number like `4.1.3-JL'.
+	SUN_REL=`echo "$UNAME_RELEASE" | sed -e 's/-/_/'`
+	GUESS=sparc-sun-sunos$SUN_REL
+	;;
+    sun3*:SunOS:*:*)
+	GUESS=m68k-sun-sunos$UNAME_RELEASE
+	;;
+    sun*:*:4.2BSD:*)
+	UNAME_RELEASE=`(sed 1q /etc/motd | awk '{print substr($5,1,3)}') 2>/dev/null`
+	test "x$UNAME_RELEASE" = x && UNAME_RELEASE=3
+	case `/bin/arch` in
+	    sun3)
+		GUESS=m68k-sun-sunos$UNAME_RELEASE
+		;;
+	    sun4)
+		GUESS=sparc-sun-sunos$UNAME_RELEASE
+		;;
+	esac
+	;;
+    aushp:SunOS:*:*)
+	GUESS=sparc-auspex-sunos$UNAME_RELEASE
+	;;
+    # The situation for MiNT is a little confusing.  The machine name
+    # can be virtually everything (everything which is not
+    # "atarist" or "atariste" at least should have a processor
+    # > m68000).  The system name ranges from "MiNT" over "FreeMiNT"
+    # to the lowercase version "mint" (or "freemint").  Finally
+    # the system name "TOS" denotes a system which is actually not
+    # MiNT.  But MiNT is downward compatible to TOS, so this should
+    # be no problem.
+    atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*)
+	GUESS=m68k-atari-mint$UNAME_RELEASE
+	;;
+    atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*)
+	GUESS=m68k-atari-mint$UNAME_RELEASE
+	;;
+    *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*)
+	GUESS=m68k-atari-mint$UNAME_RELEASE
+	;;
+    milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*)
+	GUESS=m68k-milan-mint$UNAME_RELEASE
+	;;
+    hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*)
+	GUESS=m68k-hades-mint$UNAME_RELEASE
+	;;
+    *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*)
+	GUESS=m68k-unknown-mint$UNAME_RELEASE
+	;;
+    m68k:machten:*:*)
+	GUESS=m68k-apple-machten$UNAME_RELEASE
+	;;
+    powerpc:machten:*:*)
+	GUESS=powerpc-apple-machten$UNAME_RELEASE
+	;;
+    RISC*:Mach:*:*)
+	GUESS=mips-dec-mach_bsd4.3
+	;;
+    RISC*:ULTRIX:*:*)
+	GUESS=mips-dec-ultrix$UNAME_RELEASE
+	;;
+    VAX*:ULTRIX*:*:*)
+	GUESS=vax-dec-ultrix$UNAME_RELEASE
+	;;
+    2020:CLIX:*:* | 2430:CLIX:*:*)
+	GUESS=clipper-intergraph-clix$UNAME_RELEASE
+	;;
+    mips:*:*:UMIPS | mips:*:*:RISCos)
+	set_cc_for_build
+	sed 's/^	//' << EOF > "$dummy.c"
+#ifdef __cplusplus
+#include <stdio.h>  /* for printf() prototype */
+	int main (int argc, char *argv[]) {
+#else
+	int main (argc, argv) int argc; char *argv[]; {
+#endif
+	#if defined (host_mips) && defined (MIPSEB)
+	#if defined (SYSTYPE_SYSV)
+	  printf ("mips-mips-riscos%ssysv\\n", argv[1]); exit (0);
+	#endif
+	#if defined (SYSTYPE_SVR4)
+	  printf ("mips-mips-riscos%ssvr4\\n", argv[1]); exit (0);
+	#endif
+	#if defined (SYSTYPE_BSD43) || defined(SYSTYPE_BSD)
+	  printf ("mips-mips-riscos%sbsd\\n", argv[1]); exit (0);
+	#endif
+	#endif
+	  exit (-1);
+	}
+EOF
+	$CC_FOR_BUILD -o "$dummy" "$dummy.c" &&
+	  dummyarg=`echo "$UNAME_RELEASE" | sed -n 's/\([0-9]*\).*/\1/p'` &&
+	  SYSTEM_NAME=`"$dummy" "$dummyarg"` &&
+	    { echo "$SYSTEM_NAME"; exit; }
+	GUESS=mips-mips-riscos$UNAME_RELEASE
+	;;
+    Motorola:PowerMAX_OS:*:*)
+	GUESS=powerpc-motorola-powermax
+	;;
+    Motorola:*:4.3:PL8-*)
+	GUESS=powerpc-harris-powermax
+	;;
+    Night_Hawk:*:*:PowerMAX_OS | Synergy:PowerMAX_OS:*:*)
+	GUESS=powerpc-harris-powermax
+	;;
+    Night_Hawk:Power_UNIX:*:*)
+	GUESS=powerpc-harris-powerunix
+	;;
+    m88k:CX/UX:7*:*)
+	GUESS=m88k-harris-cxux7
+	;;
+    m88k:*:4*:R4*)
+	GUESS=m88k-motorola-sysv4
+	;;
+    m88k:*:3*:R3*)
+	GUESS=m88k-motorola-sysv3
+	;;
+    AViiON:dgux:*:*)
+	# DG/UX returns AViiON for all architectures
+	UNAME_PROCESSOR=`/usr/bin/uname -p`
+	if test "$UNAME_PROCESSOR" = mc88100 || test "$UNAME_PROCESSOR" = mc88110
+	then
+	    if test "$TARGET_BINARY_INTERFACE"x = m88kdguxelfx || \
+	       test "$TARGET_BINARY_INTERFACE"x = x
+	    then
+		GUESS=m88k-dg-dgux$UNAME_RELEASE
+	    else
+		GUESS=m88k-dg-dguxbcs$UNAME_RELEASE
+	    fi
+	else
+	    GUESS=i586-dg-dgux$UNAME_RELEASE
+	fi
+	;;
+    M88*:DolphinOS:*:*)	# DolphinOS (SVR3)
+	GUESS=m88k-dolphin-sysv3
+	;;
+    M88*:*:R3*:*)
+	# Delta 88k system running SVR3
+	GUESS=m88k-motorola-sysv3
+	;;
+    XD88*:*:*:*) # Tektronix XD88 system running UTekV (SVR3)
+	GUESS=m88k-tektronix-sysv3
+	;;
+    Tek43[0-9][0-9]:UTek:*:*) # Tektronix 4300 system running UTek (BSD)
+	GUESS=m68k-tektronix-bsd
+	;;
+    *:IRIX*:*:*)
+	IRIX_REL=`echo "$UNAME_RELEASE" | sed -e 's/-/_/g'`
+	GUESS=mips-sgi-irix$IRIX_REL
+	;;
+    ????????:AIX?:[12].1:2)   # AIX 2.2.1 or AIX 2.1.1 is RT/PC AIX.
+	GUESS=romp-ibm-aix    # uname -m gives an 8 hex-code CPU id
+	;;                    # Note that: echo "'`uname -s`'" gives 'AIX '
+    i*86:AIX:*:*)
+	GUESS=i386-ibm-aix
+	;;
+    ia64:AIX:*:*)
+	if test -x /usr/bin/oslevel ; then
+		IBM_REV=`/usr/bin/oslevel`
+	else
+		IBM_REV=$UNAME_VERSION.$UNAME_RELEASE
+	fi
+	GUESS=$UNAME_MACHINE-ibm-aix$IBM_REV
+	;;
+    *:AIX:2:3)
+	if grep bos325 /usr/include/stdio.h >/dev/null 2>&1; then
+		set_cc_for_build
+		sed 's/^		//' << EOF > "$dummy.c"
+		#include <sys/systemcfg.h>
+
+		main()
+			{
+			if (!__power_pc())
+				exit(1);
+			puts("powerpc-ibm-aix3.2.5");
+			exit(0);
+			}
+EOF
+		if $CC_FOR_BUILD -o "$dummy" "$dummy.c" && SYSTEM_NAME=`"$dummy"`
+		then
+			GUESS=$SYSTEM_NAME
+		else
+			GUESS=rs6000-ibm-aix3.2.5
+		fi
+	elif grep bos324 /usr/include/stdio.h >/dev/null 2>&1; then
+		GUESS=rs6000-ibm-aix3.2.4
+	else
+		GUESS=rs6000-ibm-aix3.2
+	fi
+	;;
+    *:AIX:*:[4567])
+	IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | sed 1q | awk '{ print $1 }'`
+	if /usr/sbin/lsattr -El "$IBM_CPU_ID" | grep ' POWER' >/dev/null 2>&1; then
+		IBM_ARCH=rs6000
+	else
+		IBM_ARCH=powerpc
+	fi
+	if test -x /usr/bin/lslpp ; then
+		IBM_REV=`/usr/bin/lslpp -Lqc bos.rte.libc | \
+			   awk -F: '{ print $3 }' | sed s/[0-9]*$/0/`
+	else
+		IBM_REV=$UNAME_VERSION.$UNAME_RELEASE
+	fi
+	GUESS=$IBM_ARCH-ibm-aix$IBM_REV
+	;;
+    *:AIX:*:*)
+	GUESS=rs6000-ibm-aix
+	;;
+    ibmrt:4.4BSD:*|romp-ibm:4.4BSD:*)
+	GUESS=romp-ibm-bsd4.4
+	;;
+    ibmrt:*BSD:*|romp-ibm:BSD:*)            # covers RT/PC BSD and
+	GUESS=romp-ibm-bsd$UNAME_RELEASE    # 4.3 with uname added to
+	;;                                  # report: romp-ibm BSD 4.3
+    *:BOSX:*:*)
+	GUESS=rs6000-bull-bosx
+	;;
+    DPX/2?00:B.O.S.:*:*)
+	GUESS=m68k-bull-sysv3
+	;;
+    9000/[34]??:4.3bsd:1.*:*)
+	GUESS=m68k-hp-bsd
+	;;
+    hp300:4.4BSD:*:* | 9000/[34]??:4.3bsd:2.*:*)
+	GUESS=m68k-hp-bsd4.4
+	;;
+    9000/[34678]??:HP-UX:*:*)
+	HPUX_REV=`echo "$UNAME_RELEASE" | sed -e 's/[^.]*.[0B]*//'`
+	case $UNAME_MACHINE in
+	    9000/31?)            HP_ARCH=m68000 ;;
+	    9000/[34]??)         HP_ARCH=m68k ;;
+	    9000/[678][0-9][0-9])
+		if test -x /usr/bin/getconf; then
+		    sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null`
+		    sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null`
+		    case $sc_cpu_version in
+		      523) HP_ARCH=hppa1.0 ;; # CPU_PA_RISC1_0
+		      528) HP_ARCH=hppa1.1 ;; # CPU_PA_RISC1_1
+		      532)                      # CPU_PA_RISC2_0
+			case $sc_kernel_bits in
+			  32) HP_ARCH=hppa2.0n ;;
+			  64) HP_ARCH=hppa2.0w ;;
+			  '') HP_ARCH=hppa2.0 ;;   # HP-UX 10.20
+			esac ;;
+		    esac
+		fi
+		if test "$HP_ARCH" = ""; then
+		    set_cc_for_build
+		    sed 's/^		//' << EOF > "$dummy.c"
+
+		#define _HPUX_SOURCE
+		#include <stdlib.h>
+		#include <unistd.h>
+
+		int main ()
+		{
+		#if defined(_SC_KERNEL_BITS)
+		    long bits = sysconf(_SC_KERNEL_BITS);
+		#endif
+		    long cpu  = sysconf (_SC_CPU_VERSION);
+
+		    switch (cpu)
+			{
+			case CPU_PA_RISC1_0: puts ("hppa1.0"); break;
+			case CPU_PA_RISC1_1: puts ("hppa1.1"); break;
+			case CPU_PA_RISC2_0:
+		#if defined(_SC_KERNEL_BITS)
+			    switch (bits)
+				{
+				case 64: puts ("hppa2.0w"); break;
+				case 32: puts ("hppa2.0n"); break;
+				default: puts ("hppa2.0"); break;
+				} break;
+		#else  /* !defined(_SC_KERNEL_BITS) */
+			    puts ("hppa2.0"); break;
+		#endif
+			default: puts ("hppa1.0"); break;
+			}
+		    exit (0);
+		}
+EOF
+		    (CCOPTS="" $CC_FOR_BUILD -o "$dummy" "$dummy.c" 2>/dev/null) && HP_ARCH=`"$dummy"`
+		    test -z "$HP_ARCH" && HP_ARCH=hppa
+		fi ;;
+	esac
+	if test "$HP_ARCH" = hppa2.0w
+	then
+	    set_cc_for_build
+
+	    # hppa2.0w-hp-hpux* has a 64-bit kernel and a compiler generating
+	    # 32-bit code.  hppa64-hp-hpux* has the same kernel and a compiler
+	    # generating 64-bit code.  GNU and HP use different nomenclature:
+	    #
+	    # $ CC_FOR_BUILD=cc ./config.guess
+	    # => hppa2.0w-hp-hpux11.23
+	    # $ CC_FOR_BUILD="cc +DA2.0w" ./config.guess
+	    # => hppa64-hp-hpux11.23
+
+	    if echo __LP64__ | (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) |
+		grep -q __LP64__
+	    then
+		HP_ARCH=hppa2.0w
+	    else
+		HP_ARCH=hppa64
+	    fi
+	fi
+	GUESS=$HP_ARCH-hp-hpux$HPUX_REV
+	;;
+    ia64:HP-UX:*:*)
+	HPUX_REV=`echo "$UNAME_RELEASE" | sed -e 's/[^.]*.[0B]*//'`
+	GUESS=ia64-hp-hpux$HPUX_REV
+	;;
+    3050*:HI-UX:*:*)
+	set_cc_for_build
+	sed 's/^	//' << EOF > "$dummy.c"
+	#include <unistd.h>
+	int
+	main ()
+	{
+	  long cpu = sysconf (_SC_CPU_VERSION);
+	  /* The order matters, because CPU_IS_HP_MC68K erroneously returns
+	     true for CPU_PA_RISC1_0.  CPU_IS_PA_RISC returns correct
+	     results, however.  */
+	  if (CPU_IS_PA_RISC (cpu))
+	    {
+	      switch (cpu)
+		{
+		  case CPU_PA_RISC1_0: puts ("hppa1.0-hitachi-hiuxwe2"); break;
+		  case CPU_PA_RISC1_1: puts ("hppa1.1-hitachi-hiuxwe2"); break;
+		  case CPU_PA_RISC2_0: puts ("hppa2.0-hitachi-hiuxwe2"); break;
+		  default: puts ("hppa-hitachi-hiuxwe2"); break;
+		}
+	    }
+	  else if (CPU_IS_HP_MC68K (cpu))
+	    puts ("m68k-hitachi-hiuxwe2");
+	  else puts ("unknown-hitachi-hiuxwe2");
+	  exit (0);
+	}
+EOF
+	$CC_FOR_BUILD -o "$dummy" "$dummy.c" && SYSTEM_NAME=`"$dummy"` &&
+		{ echo "$SYSTEM_NAME"; exit; }
+	GUESS=unknown-hitachi-hiuxwe2
+	;;
+    9000/7??:4.3bsd:*:* | 9000/8?[79]:4.3bsd:*:*)
+	GUESS=hppa1.1-hp-bsd
+	;;
+    9000/8??:4.3bsd:*:*)
+	GUESS=hppa1.0-hp-bsd
+	;;
+    *9??*:MPE/iX:*:* | *3000*:MPE/iX:*:*)
+	GUESS=hppa1.0-hp-mpeix
+	;;
+    hp7??:OSF1:*:* | hp8?[79]:OSF1:*:*)
+	GUESS=hppa1.1-hp-osf
+	;;
+    hp8??:OSF1:*:*)
+	GUESS=hppa1.0-hp-osf
+	;;
+    i*86:OSF1:*:*)
+	if test -x /usr/sbin/sysversion ; then
+	    GUESS=$UNAME_MACHINE-unknown-osf1mk
+	else
+	    GUESS=$UNAME_MACHINE-unknown-osf1
+	fi
+	;;
+    parisc*:Lites*:*:*)
+	GUESS=hppa1.1-hp-lites
+	;;
+    C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*)
+	GUESS=c1-convex-bsd
+	;;
+    C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*)
+	if getsysinfo -f scalar_acc
+	then echo c32-convex-bsd
+	else echo c2-convex-bsd
+	fi
+	exit ;;
+    C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*)
+	GUESS=c34-convex-bsd
+	;;
+    C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*)
+	GUESS=c38-convex-bsd
+	;;
+    C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*)
+	GUESS=c4-convex-bsd
+	;;
+    CRAY*Y-MP:*:*:*)
+	CRAY_REL=`echo "$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/'`
+	GUESS=ymp-cray-unicos$CRAY_REL
+	;;
+    CRAY*[A-Z]90:*:*:*)
+	echo "$UNAME_MACHINE"-cray-unicos"$UNAME_RELEASE" \
+	| sed -e 's/CRAY.*\([A-Z]90\)/\1/' \
+	      -e y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/ \
+	      -e 's/\.[^.]*$/.X/'
+	exit ;;
+    CRAY*TS:*:*:*)
+	CRAY_REL=`echo "$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/'`
+	GUESS=t90-cray-unicos$CRAY_REL
+	;;
+    CRAY*T3E:*:*:*)
+	CRAY_REL=`echo "$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/'`
+	GUESS=alphaev5-cray-unicosmk$CRAY_REL
+	;;
+    CRAY*SV1:*:*:*)
+	CRAY_REL=`echo "$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/'`
+	GUESS=sv1-cray-unicos$CRAY_REL
+	;;
+    *:UNICOS/mp:*:*)
+	CRAY_REL=`echo "$UNAME_RELEASE" | sed -e 's/\.[^.]*$/.X/'`
+	GUESS=craynv-cray-unicosmp$CRAY_REL
+	;;
+    F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*)
+	FUJITSU_PROC=`uname -m | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz`
+	FUJITSU_SYS=`uname -p | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/\///'`
+	FUJITSU_REL=`echo "$UNAME_RELEASE" | sed -e 's/ /_/'`
+	GUESS=${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}
+	;;
+    5000:UNIX_System_V:4.*:*)
+	FUJITSU_SYS=`uname -p | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/\///'`
+	FUJITSU_REL=`echo "$UNAME_RELEASE" | tr ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz | sed -e 's/ /_/'`
+	GUESS=sparc-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}
+	;;
+    i*86:BSD/386:*:* | i*86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*)
+	GUESS=$UNAME_MACHINE-pc-bsdi$UNAME_RELEASE
+	;;
+    sparc*:BSD/OS:*:*)
+	GUESS=sparc-unknown-bsdi$UNAME_RELEASE
+	;;
+    *:BSD/OS:*:*)
+	GUESS=$UNAME_MACHINE-unknown-bsdi$UNAME_RELEASE
+	;;
+    arm:FreeBSD:*:*)
+	UNAME_PROCESSOR=`uname -p`
+	set_cc_for_build
+	if echo __ARM_PCS_VFP | $CC_FOR_BUILD -E - 2>/dev/null \
+	    | grep -q __ARM_PCS_VFP
+	then
+	    FREEBSD_REL=`echo "$UNAME_RELEASE" | sed -e 's/[-(].*//'`
+	    GUESS=$UNAME_PROCESSOR-unknown-freebsd$FREEBSD_REL-gnueabi
+	else
+	    FREEBSD_REL=`echo "$UNAME_RELEASE" | sed -e 's/[-(].*//'`
+	    GUESS=$UNAME_PROCESSOR-unknown-freebsd$FREEBSD_REL-gnueabihf
+	fi
+	;;
+    *:FreeBSD:*:*)
+	UNAME_PROCESSOR=`/usr/bin/uname -p`
+	case $UNAME_PROCESSOR in
+	    amd64)
+		UNAME_PROCESSOR=x86_64 ;;
+	    i386)
+		UNAME_PROCESSOR=i586 ;;
+	esac
+	FREEBSD_REL=`echo "$UNAME_RELEASE" | sed -e 's/[-(].*//'`
+	GUESS=$UNAME_PROCESSOR-unknown-freebsd$FREEBSD_REL
+	;;
+    i*:CYGWIN*:*)
+	GUESS=$UNAME_MACHINE-pc-cygwin
+	;;
+    *:MINGW64*:*)
+	GUESS=$UNAME_MACHINE-pc-mingw64
+	;;
+    *:MINGW*:*)
+	GUESS=$UNAME_MACHINE-pc-mingw32
+	;;
+    *:MSYS*:*)
+	GUESS=$UNAME_MACHINE-pc-msys
+	;;
+    i*:PW*:*)
+	GUESS=$UNAME_MACHINE-pc-pw32
+	;;
+    *:SerenityOS:*:*)
+        GUESS=$UNAME_MACHINE-pc-serenity
+        ;;
+    *:Interix*:*)
+	case $UNAME_MACHINE in
+	    x86)
+		GUESS=i586-pc-interix$UNAME_RELEASE
+		;;
+	    authenticamd | genuineintel | EM64T)
+		GUESS=x86_64-unknown-interix$UNAME_RELEASE
+		;;
+	    IA64)
+		GUESS=ia64-unknown-interix$UNAME_RELEASE
+		;;
+	esac ;;
+    i*:UWIN*:*)
+	GUESS=$UNAME_MACHINE-pc-uwin
+	;;
+    amd64:CYGWIN*:*:* | x86_64:CYGWIN*:*:*)
+	GUESS=x86_64-pc-cygwin
+	;;
+    prep*:SunOS:5.*:*)
+	SUN_REL=`echo "$UNAME_RELEASE" | sed -e 's/[^.]*//'`
+	GUESS=powerpcle-unknown-solaris2$SUN_REL
+	;;
+    *:GNU:*:*)
+	# the GNU system
+	GNU_ARCH=`echo "$UNAME_MACHINE" | sed -e 's,[-/].*$,,'`
+	GNU_REL=`echo "$UNAME_RELEASE" | sed -e 's,/.*$,,'`
+	GUESS=$GNU_ARCH-unknown-$LIBC$GNU_REL
+	;;
+    *:GNU/*:*:*)
+	# other systems with GNU libc and userland
+	GNU_SYS=`echo "$UNAME_SYSTEM" | sed 's,^[^/]*/,,' | tr "[:upper:]" "[:lower:]"`
+	GNU_REL=`echo "$UNAME_RELEASE" | sed -e 's/[-(].*//'`
+	GUESS=$UNAME_MACHINE-unknown-$GNU_SYS$GNU_REL-$LIBC
+	;;
+    *:Minix:*:*)
+	GUESS=$UNAME_MACHINE-unknown-minix
+	;;
+    aarch64:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    aarch64_be:Linux:*:*)
+	UNAME_MACHINE=aarch64_be
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    alpha:Linux:*:*)
+	case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' /proc/cpuinfo 2>/dev/null` in
+	  EV5)   UNAME_MACHINE=alphaev5 ;;
+	  EV56)  UNAME_MACHINE=alphaev56 ;;
+	  PCA56) UNAME_MACHINE=alphapca56 ;;
+	  PCA57) UNAME_MACHINE=alphapca56 ;;
+	  EV6)   UNAME_MACHINE=alphaev6 ;;
+	  EV67)  UNAME_MACHINE=alphaev67 ;;
+	  EV68*) UNAME_MACHINE=alphaev68 ;;
+	esac
+	objdump --private-headers /bin/sh | grep -q ld.so.1
+	if test "$?" = 0 ; then LIBC=gnulibc1 ; fi
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    arc:Linux:*:* | arceb:Linux:*:* | arc32:Linux:*:* | arc64:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    arm*:Linux:*:*)
+	set_cc_for_build
+	if echo __ARM_EABI__ | $CC_FOR_BUILD -E - 2>/dev/null \
+	    | grep -q __ARM_EABI__
+	then
+	    GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	else
+	    if echo __ARM_PCS_VFP | $CC_FOR_BUILD -E - 2>/dev/null \
+		| grep -q __ARM_PCS_VFP
+	    then
+		GUESS=$UNAME_MACHINE-unknown-linux-${LIBC}eabi
+	    else
+		GUESS=$UNAME_MACHINE-unknown-linux-${LIBC}eabihf
+	    fi
+	fi
+	;;
+    avr32*:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    cris:Linux:*:*)
+	GUESS=$UNAME_MACHINE-axis-linux-$LIBC
+	;;
+    crisv32:Linux:*:*)
+	GUESS=$UNAME_MACHINE-axis-linux-$LIBC
+	;;
+    e2k:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    frv:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    hexagon:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    i*86:Linux:*:*)
+	GUESS=$UNAME_MACHINE-pc-linux-$LIBC
+	;;
+    ia64:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    k1om:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    loongarch32:Linux:*:* | loongarch64:Linux:*:* | loongarchx32:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    m32r*:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    m68*:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    mips:Linux:*:* | mips64:Linux:*:*)
+	set_cc_for_build
+	IS_GLIBC=0
+	test x"${LIBC}" = xgnu && IS_GLIBC=1
+	sed 's/^	//' << EOF > "$dummy.c"
+	#undef CPU
+	#undef mips
+	#undef mipsel
+	#undef mips64
+	#undef mips64el
+	#if ${IS_GLIBC} && defined(_ABI64)
+	LIBCABI=gnuabi64
+	#else
+	#if ${IS_GLIBC} && defined(_ABIN32)
+	LIBCABI=gnuabin32
+	#else
+	LIBCABI=${LIBC}
+	#endif
+	#endif
+
+	#if ${IS_GLIBC} && defined(__mips64) && defined(__mips_isa_rev) && __mips_isa_rev>=6
+	CPU=mipsisa64r6
+	#else
+	#if ${IS_GLIBC} && !defined(__mips64) && defined(__mips_isa_rev) && __mips_isa_rev>=6
+	CPU=mipsisa32r6
+	#else
+	#if defined(__mips64)
+	CPU=mips64
+	#else
+	CPU=mips
+	#endif
+	#endif
+	#endif
+
+	#if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL)
+	MIPS_ENDIAN=el
+	#else
+	#if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB)
+	MIPS_ENDIAN=
+	#else
+	MIPS_ENDIAN=
+	#endif
+	#endif
+EOF
+	cc_set_vars=`$CC_FOR_BUILD -E "$dummy.c" 2>/dev/null | grep '^CPU\|^MIPS_ENDIAN\|^LIBCABI'`
+	eval "$cc_set_vars"
+	test "x$CPU" != x && { echo "$CPU${MIPS_ENDIAN}-unknown-linux-$LIBCABI"; exit; }
+	;;
+    mips64el:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    openrisc*:Linux:*:*)
+	GUESS=or1k-unknown-linux-$LIBC
+	;;
+    or32:Linux:*:* | or1k*:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    padre:Linux:*:*)
+	GUESS=sparc-unknown-linux-$LIBC
+	;;
+    parisc64:Linux:*:* | hppa64:Linux:*:*)
+	GUESS=hppa64-unknown-linux-$LIBC
+	;;
+    parisc:Linux:*:* | hppa:Linux:*:*)
+	# Look for CPU level
+	case `grep '^cpu[^a-z]*:' /proc/cpuinfo 2>/dev/null | cut -d' ' -f2` in
+	  PA7*) GUESS=hppa1.1-unknown-linux-$LIBC ;;
+	  PA8*) GUESS=hppa2.0-unknown-linux-$LIBC ;;
+	  *)    GUESS=hppa-unknown-linux-$LIBC ;;
+	esac
+	;;
+    ppc64:Linux:*:*)
+	GUESS=powerpc64-unknown-linux-$LIBC
+	;;
+    ppc:Linux:*:*)
+	GUESS=powerpc-unknown-linux-$LIBC
+	;;
+    ppc64le:Linux:*:*)
+	GUESS=powerpc64le-unknown-linux-$LIBC
+	;;
+    ppcle:Linux:*:*)
+	GUESS=powerpcle-unknown-linux-$LIBC
+	;;
+    riscv32:Linux:*:* | riscv32be:Linux:*:* | riscv64:Linux:*:* | riscv64be:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    s390:Linux:*:* | s390x:Linux:*:*)
+	GUESS=$UNAME_MACHINE-ibm-linux-$LIBC
+	;;
+    sh64*:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    sh*:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    sparc:Linux:*:* | sparc64:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    tile*:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    vax:Linux:*:*)
+	GUESS=$UNAME_MACHINE-dec-linux-$LIBC
+	;;
+    x86_64:Linux:*:*)
+	set_cc_for_build
+	LIBCABI=$LIBC
+	if test "$CC_FOR_BUILD" != no_compiler_found; then
+	    if (echo '#ifdef __ILP32__'; echo IS_X32; echo '#endif') | \
+		(CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \
+		grep IS_X32 >/dev/null
+	    then
+		LIBCABI=${LIBC}x32
+	    fi
+	fi
+	GUESS=$UNAME_MACHINE-pc-linux-$LIBCABI
+	;;
+    xtensa*:Linux:*:*)
+	GUESS=$UNAME_MACHINE-unknown-linux-$LIBC
+	;;
+    i*86:DYNIX/ptx:4*:*)
+	# ptx 4.0 does uname -s correctly, with DYNIX/ptx in there.
+	# earlier versions are messed up and put the nodename in both
+	# sysname and nodename.
+	GUESS=i386-sequent-sysv4
+	;;
+    i*86:UNIX_SV:4.2MP:2.*)
+	# Unixware is an offshoot of SVR4, but it has its own version
+	# number series starting with 2...
+	# I am not positive that other SVR4 systems won't match this,
+	# I just have to hope.  -- rms.
+	# Use sysv4.2uw... so that sysv4* matches it.
+	GUESS=$UNAME_MACHINE-pc-sysv4.2uw$UNAME_VERSION
+	;;
+    i*86:OS/2:*:*)
+	# If we were able to find `uname', then EMX Unix compatibility
+	# is probably installed.
+	GUESS=$UNAME_MACHINE-pc-os2-emx
+	;;
+    i*86:XTS-300:*:STOP)
+	GUESS=$UNAME_MACHINE-unknown-stop
+	;;
+    i*86:atheos:*:*)
+	GUESS=$UNAME_MACHINE-unknown-atheos
+	;;
+    i*86:syllable:*:*)
+	GUESS=$UNAME_MACHINE-pc-syllable
+	;;
+    i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.[02]*:*)
+	GUESS=i386-unknown-lynxos$UNAME_RELEASE
+	;;
+    i*86:*DOS:*:*)
+	GUESS=$UNAME_MACHINE-pc-msdosdjgpp
+	;;
+    i*86:*:4.*:*)
+	UNAME_REL=`echo "$UNAME_RELEASE" | sed 's/\/MP$//'`
+	if grep Novell /usr/include/link.h >/dev/null 2>/dev/null; then
+		GUESS=$UNAME_MACHINE-univel-sysv$UNAME_REL
+	else
+		GUESS=$UNAME_MACHINE-pc-sysv$UNAME_REL
+	fi
+	;;
+    i*86:*:5:[678]*)
+	# UnixWare 7.x, OpenUNIX and OpenServer 6.
+	case `/bin/uname -X | grep "^Machine"` in
+	    *486*)	     UNAME_MACHINE=i486 ;;
+	    *Pentium)	     UNAME_MACHINE=i586 ;;
+	    *Pent*|*Celeron) UNAME_MACHINE=i686 ;;
+	esac
+	GUESS=$UNAME_MACHINE-unknown-sysv${UNAME_RELEASE}${UNAME_SYSTEM}${UNAME_VERSION}
+	;;
+    i*86:*:3.2:*)
+	if test -f /usr/options/cb.name; then
+		UNAME_REL=`sed -n 's/.*Version //p' </usr/options/cb.name`
+		GUESS=$UNAME_MACHINE-pc-isc$UNAME_REL
+	elif /bin/uname -X 2>/dev/null >/dev/null ; then
+		UNAME_REL=`(/bin/uname -X|grep Release|sed -e 's/.*= //')`
+		(/bin/uname -X|grep i80486 >/dev/null) && UNAME_MACHINE=i486
+		(/bin/uname -X|grep '^Machine.*Pentium' >/dev/null) \
+			&& UNAME_MACHINE=i586
+		(/bin/uname -X|grep '^Machine.*Pent *II' >/dev/null) \
+			&& UNAME_MACHINE=i686
+		(/bin/uname -X|grep '^Machine.*Pentium Pro' >/dev/null) \
+			&& UNAME_MACHINE=i686
+		GUESS=$UNAME_MACHINE-pc-sco$UNAME_REL
+	else
+		GUESS=$UNAME_MACHINE-pc-sysv32
+	fi
+	;;
+    pc:*:*:*)
+	# Left here for compatibility:
+	# uname -m prints for DJGPP always 'pc', but it prints nothing about
+	# the processor, so we play safe by assuming i586.
+	# Note: whatever this is, it MUST be the same as what config.sub
+	# prints for the "djgpp" host, or else GDB configure will decide that
+	# this is a cross-build.
+	GUESS=i586-pc-msdosdjgpp
+	;;
+    Intel:Mach:3*:*)
+	GUESS=i386-pc-mach3
+	;;
+    paragon:*:*:*)
+	GUESS=i860-intel-osf1
+	;;
+    i860:*:4.*:*) # i860-SVR4
+	if grep Stardent /usr/include/sys/uadmin.h >/dev/null 2>&1 ; then
+	  GUESS=i860-stardent-sysv$UNAME_RELEASE    # Stardent Vistra i860-SVR4
+	else # Add other i860-SVR4 vendors below as they are discovered.
+	  GUESS=i860-unknown-sysv$UNAME_RELEASE     # Unknown i860-SVR4
+	fi
+	;;
+    mini*:CTIX:SYS*5:*)
+	# "miniframe"
+	GUESS=m68010-convergent-sysv
+	;;
+    mc68k:UNIX:SYSTEM5:3.51m)
+	GUESS=m68k-convergent-sysv
+	;;
+    M680?0:D-NIX:5.3:*)
+	GUESS=m68k-diab-dnix
+	;;
+    M68*:*:R3V[5678]*:*)
+	test -r /sysV68 && { echo 'm68k-motorola-sysv'; exit; } ;;
+    3[345]??:*:4.0:3.0 | 3[34]??A:*:4.0:3.0 | 3[34]??,*:*:4.0:3.0 | 3[34]??/*:*:4.0:3.0 | 4400:*:4.0:3.0 | 4850:*:4.0:3.0 | SKA40:*:4.0:3.0 | SDS2:*:4.0:3.0 | SHG2:*:4.0:3.0 | S7501*:*:4.0:3.0)
+	OS_REL=''
+	test -r /etc/.relid \
+	&& OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid`
+	/bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+	  && { echo i486-ncr-sysv4.3"$OS_REL"; exit; }
+	/bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \
+	  && { echo i586-ncr-sysv4.3"$OS_REL"; exit; } ;;
+    3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*)
+	/bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+	  && { echo i486-ncr-sysv4; exit; } ;;
+    NCR*:*:4.2:* | MPRAS*:*:4.2:*)
+	OS_REL='.3'
+	test -r /etc/.relid \
+	    && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid`
+	/bin/uname -p 2>/dev/null | grep 86 >/dev/null \
+	    && { echo i486-ncr-sysv4.3"$OS_REL"; exit; }
+	/bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \
+	    && { echo i586-ncr-sysv4.3"$OS_REL"; exit; }
+	/bin/uname -p 2>/dev/null | /bin/grep pteron >/dev/null \
+	    && { echo i586-ncr-sysv4.3"$OS_REL"; exit; } ;;
+    m68*:LynxOS:2.*:* | m68*:LynxOS:3.0*:*)
+	GUESS=m68k-unknown-lynxos$UNAME_RELEASE
+	;;
+    mc68030:UNIX_System_V:4.*:*)
+	GUESS=m68k-atari-sysv4
+	;;
+    TSUNAMI:LynxOS:2.*:*)
+	GUESS=sparc-unknown-lynxos$UNAME_RELEASE
+	;;
+    rs6000:LynxOS:2.*:*)
+	GUESS=rs6000-unknown-lynxos$UNAME_RELEASE
+	;;
+    PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.[02]*:*)
+	GUESS=powerpc-unknown-lynxos$UNAME_RELEASE
+	;;
+    SM[BE]S:UNIX_SV:*:*)
+	GUESS=mips-dde-sysv$UNAME_RELEASE
+	;;
+    RM*:ReliantUNIX-*:*:*)
+	GUESS=mips-sni-sysv4
+	;;
+    RM*:SINIX-*:*:*)
+	GUESS=mips-sni-sysv4
+	;;
+    *:SINIX-*:*:*)
+	if uname -p 2>/dev/null >/dev/null ; then
+		UNAME_MACHINE=`(uname -p) 2>/dev/null`
+		GUESS=$UNAME_MACHINE-sni-sysv4
+	else
+		GUESS=ns32k-sni-sysv
+	fi
+	;;
+    PENTIUM:*:4.0*:*)	# Unisys `ClearPath HMP IX 4000' SVR4/MP effort
+			# says <Richard.M.Bartel@ccMail.Census.GOV>
+	GUESS=i586-unisys-sysv4
+	;;
+    *:UNIX_System_V:4*:FTX*)
+	# From Gerald Hewes <hewes@openmarket.com>.
+	# How about differentiating between stratus architectures? -djm
+	GUESS=hppa1.1-stratus-sysv4
+	;;
+    *:*:*:FTX*)
+	# From seanf@swdc.stratus.com.
+	GUESS=i860-stratus-sysv4
+	;;
+    i*86:VOS:*:*)
+	# From Paul.Green@stratus.com.
+	GUESS=$UNAME_MACHINE-stratus-vos
+	;;
+    *:VOS:*:*)
+	# From Paul.Green@stratus.com.
+	GUESS=hppa1.1-stratus-vos
+	;;
+    mc68*:A/UX:*:*)
+	GUESS=m68k-apple-aux$UNAME_RELEASE
+	;;
+    news*:NEWS-OS:6*:*)
+	GUESS=mips-sony-newsos6
+	;;
+    R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*)
+	if test -d /usr/nec; then
+		GUESS=mips-nec-sysv$UNAME_RELEASE
+	else
+		GUESS=mips-unknown-sysv$UNAME_RELEASE
+	fi
+	;;
+    BeBox:BeOS:*:*)	# BeOS running on hardware made by Be, PPC only.
+	GUESS=powerpc-be-beos
+	;;
+    BeMac:BeOS:*:*)	# BeOS running on Mac or Mac clone, PPC only.
+	GUESS=powerpc-apple-beos
+	;;
+    BePC:BeOS:*:*)	# BeOS running on Intel PC compatible.
+	GUESS=i586-pc-beos
+	;;
+    BePC:Haiku:*:*)	# Haiku running on Intel PC compatible.
+	GUESS=i586-pc-haiku
+	;;
+    x86_64:Haiku:*:*)
+	GUESS=x86_64-unknown-haiku
+	;;
+    SX-4:SUPER-UX:*:*)
+	GUESS=sx4-nec-superux$UNAME_RELEASE
+	;;
+    SX-5:SUPER-UX:*:*)
+	GUESS=sx5-nec-superux$UNAME_RELEASE
+	;;
+    SX-6:SUPER-UX:*:*)
+	GUESS=sx6-nec-superux$UNAME_RELEASE
+	;;
+    SX-7:SUPER-UX:*:*)
+	GUESS=sx7-nec-superux$UNAME_RELEASE
+	;;
+    SX-8:SUPER-UX:*:*)
+	GUESS=sx8-nec-superux$UNAME_RELEASE
+	;;
+    SX-8R:SUPER-UX:*:*)
+	GUESS=sx8r-nec-superux$UNAME_RELEASE
+	;;
+    SX-ACE:SUPER-UX:*:*)
+	GUESS=sxace-nec-superux$UNAME_RELEASE
+	;;
+    Power*:Rhapsody:*:*)
+	GUESS=powerpc-apple-rhapsody$UNAME_RELEASE
+	;;
+    *:Rhapsody:*:*)
+	GUESS=$UNAME_MACHINE-apple-rhapsody$UNAME_RELEASE
+	;;
+    arm64:Darwin:*:*)
+	GUESS=aarch64-apple-darwin$UNAME_RELEASE
+	;;
+    *:Darwin:*:*)
+	UNAME_PROCESSOR=`uname -p`
+	case $UNAME_PROCESSOR in
+	    unknown) UNAME_PROCESSOR=powerpc ;;
+	esac
+	if command -v xcode-select > /dev/null 2> /dev/null && \
+		! xcode-select --print-path > /dev/null 2> /dev/null ; then
+	    # Avoid executing cc if there is no toolchain installed as
+	    # cc will be a stub that puts up a graphical alert
+	    # prompting the user to install developer tools.
+	    CC_FOR_BUILD=no_compiler_found
+	else
+	    set_cc_for_build
+	fi
+	if test "$CC_FOR_BUILD" != no_compiler_found; then
+	    if (echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | \
+		   (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \
+		   grep IS_64BIT_ARCH >/dev/null
+	    then
+		case $UNAME_PROCESSOR in
+		    i386) UNAME_PROCESSOR=x86_64 ;;
+		    powerpc) UNAME_PROCESSOR=powerpc64 ;;
+		esac
+	    fi
+	    # On 10.4-10.6 one might compile for PowerPC via gcc -arch ppc
+	    if (echo '#ifdef __POWERPC__'; echo IS_PPC; echo '#endif') | \
+		   (CCOPTS="" $CC_FOR_BUILD -E - 2>/dev/null) | \
+		   grep IS_PPC >/dev/null
+	    then
+		UNAME_PROCESSOR=powerpc
+	    fi
+	elif test "$UNAME_PROCESSOR" = i386 ; then
+	    # uname -m returns i386 or x86_64
+	    UNAME_PROCESSOR=$UNAME_MACHINE
+	fi
+	GUESS=$UNAME_PROCESSOR-apple-darwin$UNAME_RELEASE
+	;;
+    *:procnto*:*:* | *:QNX:[0123456789]*:*)
+	UNAME_PROCESSOR=`uname -p`
+	if test "$UNAME_PROCESSOR" = x86; then
+		UNAME_PROCESSOR=i386
+		UNAME_MACHINE=pc
+	fi
+	GUESS=$UNAME_PROCESSOR-$UNAME_MACHINE-nto-qnx$UNAME_RELEASE
+	;;
+    *:QNX:*:4*)
+	GUESS=i386-pc-qnx
+	;;
+    NEO-*:NONSTOP_KERNEL:*:*)
+	GUESS=neo-tandem-nsk$UNAME_RELEASE
+	;;
+    NSE-*:NONSTOP_KERNEL:*:*)
+	GUESS=nse-tandem-nsk$UNAME_RELEASE
+	;;
+    NSR-*:NONSTOP_KERNEL:*:*)
+	GUESS=nsr-tandem-nsk$UNAME_RELEASE
+	;;
+    NSV-*:NONSTOP_KERNEL:*:*)
+	GUESS=nsv-tandem-nsk$UNAME_RELEASE
+	;;
+    NSX-*:NONSTOP_KERNEL:*:*)
+	GUESS=nsx-tandem-nsk$UNAME_RELEASE
+	;;
+    *:NonStop-UX:*:*)
+	GUESS=mips-compaq-nonstopux
+	;;
+    BS2000:POSIX*:*:*)
+	GUESS=bs2000-siemens-sysv
+	;;
+    DS/*:UNIX_System_V:*:*)
+	GUESS=$UNAME_MACHINE-$UNAME_SYSTEM-$UNAME_RELEASE
+	;;
+    *:Plan9:*:*)
+	# "uname -m" is not consistent, so use $cputype instead. 386
+	# is converted to i386 for consistency with other x86
+	# operating systems.
+	if test "${cputype-}" = 386; then
+	    UNAME_MACHINE=i386
+	elif test "x${cputype-}" != x; then
+	    UNAME_MACHINE=$cputype
+	fi
+	GUESS=$UNAME_MACHINE-unknown-plan9
+	;;
+    *:TOPS-10:*:*)
+	GUESS=pdp10-unknown-tops10
+	;;
+    *:TENEX:*:*)
+	GUESS=pdp10-unknown-tenex
+	;;
+    KS10:TOPS-20:*:* | KL10:TOPS-20:*:* | TYPE4:TOPS-20:*:*)
+	GUESS=pdp10-dec-tops20
+	;;
+    XKL-1:TOPS-20:*:* | TYPE5:TOPS-20:*:*)
+	GUESS=pdp10-xkl-tops20
+	;;
+    *:TOPS-20:*:*)
+	GUESS=pdp10-unknown-tops20
+	;;
+    *:ITS:*:*)
+	GUESS=pdp10-unknown-its
+	;;
+    SEI:*:*:SEIUX)
+	GUESS=mips-sei-seiux$UNAME_RELEASE
+	;;
+    *:DragonFly:*:*)
+	DRAGONFLY_REL=`echo "$UNAME_RELEASE" | sed -e 's/[-(].*//'`
+	GUESS=$UNAME_MACHINE-unknown-dragonfly$DRAGONFLY_REL
+	;;
+    *:*VMS:*:*)
+	UNAME_MACHINE=`(uname -p) 2>/dev/null`
+	case $UNAME_MACHINE in
+	    A*) GUESS=alpha-dec-vms ;;
+	    I*) GUESS=ia64-dec-vms ;;
+	    V*) GUESS=vax-dec-vms ;;
+	esac ;;
+    *:XENIX:*:SysV)
+	GUESS=i386-pc-xenix
+	;;
+    i*86:skyos:*:*)
+	SKYOS_REL=`echo "$UNAME_RELEASE" | sed -e 's/ .*$//'`
+	GUESS=$UNAME_MACHINE-pc-skyos$SKYOS_REL
+	;;
+    i*86:rdos:*:*)
+	GUESS=$UNAME_MACHINE-pc-rdos
+	;;
+    i*86:Fiwix:*:*)
+	GUESS=$UNAME_MACHINE-pc-fiwix
+	;;
+    *:AROS:*:*)
+	GUESS=$UNAME_MACHINE-unknown-aros
+	;;
+    x86_64:VMkernel:*:*)
+	GUESS=$UNAME_MACHINE-unknown-esx
+	;;
+    amd64:Isilon\ OneFS:*:*)
+	GUESS=x86_64-unknown-onefs
+	;;
+    *:Unleashed:*:*)
+	GUESS=$UNAME_MACHINE-unknown-unleashed$UNAME_RELEASE
+	;;
+esac
+
+# Do we have a guess based on uname results?
+if test "x$GUESS" != x; then
+    echo "$GUESS"
+    exit
+fi
+
+# No uname command or uname output not recognized.
+set_cc_for_build
+cat > "$dummy.c" <<EOF
+#ifdef _SEQUENT_
+#include <sys/types.h>
+#include <sys/utsname.h>
+#endif
+#if defined(ultrix) || defined(_ultrix) || defined(__ultrix) || defined(__ultrix__)
+#if defined (vax) || defined (__vax) || defined (__vax__) || defined(mips) || defined(__mips) || defined(__mips__) || defined(MIPS) || defined(__MIPS__)
+#include <signal.h>
+#if defined(_SIZE_T_) || defined(SIGLOST)
+#include <sys/utsname.h>
+#endif
+#endif
+#endif
+main ()
+{
+#if defined (sony)
+#if defined (MIPSEB)
+  /* BFD wants "bsd" instead of "newsos".  Perhaps BFD should be changed,
+     I don't know....  */
+  printf ("mips-sony-bsd\n"); exit (0);
+#else
+#include <sys/param.h>
+  printf ("m68k-sony-newsos%s\n",
+#ifdef NEWSOS4
+  "4"
+#else
+  ""
+#endif
+  ); exit (0);
+#endif
+#endif
+
+#if defined (NeXT)
+#if !defined (__ARCHITECTURE__)
+#define __ARCHITECTURE__ "m68k"
+#endif
+  int version;
+  version=`(hostinfo | sed -n 's/.*NeXT Mach \([0-9]*\).*/\1/p') 2>/dev/null`;
+  if (version < 4)
+    printf ("%s-next-nextstep%d\n", __ARCHITECTURE__, version);
+  else
+    printf ("%s-next-openstep%d\n", __ARCHITECTURE__, version);
+  exit (0);
+#endif
+
+#if defined (MULTIMAX) || defined (n16)
+#if defined (UMAXV)
+  printf ("ns32k-encore-sysv\n"); exit (0);
+#else
+#if defined (CMU)
+  printf ("ns32k-encore-mach\n"); exit (0);
+#else
+  printf ("ns32k-encore-bsd\n"); exit (0);
+#endif
+#endif
+#endif
+
+#if defined (__386BSD__)
+  printf ("i386-pc-bsd\n"); exit (0);
+#endif
+
+#if defined (sequent)
+#if defined (i386)
+  printf ("i386-sequent-dynix\n"); exit (0);
+#endif
+#if defined (ns32000)
+  printf ("ns32k-sequent-dynix\n"); exit (0);
+#endif
+#endif
+
+#if defined (_SEQUENT_)
+  struct utsname un;
+
+  uname(&un);
+  if (strncmp(un.version, "V2", 2) == 0) {
+    printf ("i386-sequent-ptx2\n"); exit (0);
+  }
+  if (strncmp(un.version, "V1", 2) == 0) { /* XXX is V1 correct? */
+    printf ("i386-sequent-ptx1\n"); exit (0);
+  }
+  printf ("i386-sequent-ptx\n"); exit (0);
+#endif
+
+#if defined (vax)
+#if !defined (ultrix)
+#include <sys/param.h>
+#if defined (BSD)
+#if BSD == 43
+  printf ("vax-dec-bsd4.3\n"); exit (0);
+#else
+#if BSD == 199006
+  printf ("vax-dec-bsd4.3reno\n"); exit (0);
+#else
+  printf ("vax-dec-bsd\n"); exit (0);
+#endif
+#endif
+#else
+  printf ("vax-dec-bsd\n"); exit (0);
+#endif
+#else
+#if defined(_SIZE_T_) || defined(SIGLOST)
+  struct utsname un;
+  uname (&un);
+  printf ("vax-dec-ultrix%s\n", un.release); exit (0);
+#else
+  printf ("vax-dec-ultrix\n"); exit (0);
+#endif
+#endif
+#endif
+#if defined(ultrix) || defined(_ultrix) || defined(__ultrix) || defined(__ultrix__)
+#if defined(mips) || defined(__mips) || defined(__mips__) || defined(MIPS) || defined(__MIPS__)
+#if defined(_SIZE_T_) || defined(SIGLOST)
+  struct utsname *un;
+  uname (&un);
+  printf ("mips-dec-ultrix%s\n", un.release); exit (0);
+#else
+  printf ("mips-dec-ultrix\n"); exit (0);
+#endif
+#endif
+#endif
+
+#if defined (alliant) && defined (i860)
+  printf ("i860-alliant-bsd\n"); exit (0);
+#endif
+
+  exit (1);
+}
+EOF
+
+$CC_FOR_BUILD -o "$dummy" "$dummy.c" 2>/dev/null && SYSTEM_NAME=`"$dummy"` &&
+	{ echo "$SYSTEM_NAME"; exit; }
+
+# Apollos put the system type in the environment.
+test -d /usr/apollo && { echo "$ISP-apollo-$SYSTYPE"; exit; }
+
+echo "$0: unable to guess system type" >&2
+
+case $UNAME_MACHINE:$UNAME_SYSTEM in
+    mips:Linux | mips64:Linux)
+	# If we got here on MIPS GNU/Linux, output extra information.
+	cat >&2 <<EOF
+
+NOTE: MIPS GNU/Linux systems require a C compiler to fully recognize
+the system type. Please install a C compiler and try again.
+EOF
+	;;
+esac
+
+cat >&2 <<EOF
+
+This script (version $timestamp), has failed to recognize the
+operating system you are using. If your script is old, overwrite *all*
+copies of config.guess and config.sub with the latest versions from:
+
+  https://git.savannah.gnu.org/cgit/config.git/plain/config.guess
+and
+  https://git.savannah.gnu.org/cgit/config.git/plain/config.sub
+EOF
+
+our_year=`echo $timestamp | sed 's,-.*,,'`
+thisyear=`date +%Y`
+# shellcheck disable=SC2003
+script_age=`expr "$thisyear" - "$our_year"`
+if test "$script_age" -lt 3 ; then
+   cat >&2 <<EOF
+
+If $0 has already been updated, send the following data and any
+information you think might be pertinent to config-patches@gnu.org to
+provide the necessary information to handle your system.
+
+config.guess timestamp = $timestamp
+
+uname -m = `(uname -m) 2>/dev/null || echo unknown`
+uname -r = `(uname -r) 2>/dev/null || echo unknown`
+uname -s = `(uname -s) 2>/dev/null || echo unknown`
+uname -v = `(uname -v) 2>/dev/null || echo unknown`
+
+/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null`
+/bin/uname -X     = `(/bin/uname -X) 2>/dev/null`
+
+hostinfo               = `(hostinfo) 2>/dev/null`
+/bin/universe          = `(/bin/universe) 2>/dev/null`
+/usr/bin/arch -k       = `(/usr/bin/arch -k) 2>/dev/null`
+/bin/arch              = `(/bin/arch) 2>/dev/null`
+/usr/bin/oslevel       = `(/usr/bin/oslevel) 2>/dev/null`
+/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null`
+
+UNAME_MACHINE = "$UNAME_MACHINE"
+UNAME_RELEASE = "$UNAME_RELEASE"
+UNAME_SYSTEM  = "$UNAME_SYSTEM"
+UNAME_VERSION = "$UNAME_VERSION"
+EOF
+fi
+
+exit 1
+
+# Local variables:
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "timestamp='"
+# time-stamp-format: "%:y-%02m-%02d"
+# time-stamp-end: "'"
+# End:
diff --git a/config.h.in b/config.h.in
new file mode 100644
index 0000000..cb34f65
--- /dev/null
+++ b/config.h.in
@@ -0,0 +1,177 @@
+/* config.h.in.  Generated from configure.ac by autoheader.  */
+
+/* Define to 1 if you have the <byteswap.h> header file. */
+#undef HAVE_BYTESWAP_H
+
+/* Define to 1 if you have the `clock_gettime' function. */
+#undef HAVE_CLOCK_GETTIME
+
+/* Define to 1 if you have the <dlfcn.h> header file. */
+#undef HAVE_DLFCN_H
+
+/* Define to 1 if you have the `getopt_long' function. */
+#undef HAVE_GETOPT_LONG
+
+/* Found sys/random.h */
+#undef HAVE_GETRANDOM
+
+/* Define to 1 if you have the `gettimeofday' function. */
+#undef HAVE_GETTIMEOFDAY
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#undef HAVE_INTTYPES_H
+
+/* Define to 1 if you have the <linux/bsg.h> header file. */
+#undef HAVE_LINUX_BSG_H
+
+/* Define to 1 if you have the <linux/kdev_t.h> header file. */
+#undef HAVE_LINUX_KDEV_T_H
+
+/* Define to 1 if you have the <linux/nvme_ioctl.h> header file. */
+#undef HAVE_LINUX_NVME_IOCTL_H
+
+/* Have Linux sg v4 header */
+#undef HAVE_LINUX_SG_V4_HDR
+
+/* Define to 1 if you have the <linux/types.h> header file. */
+#undef HAVE_LINUX_TYPES_H
+
+/* Define to 1 if you have the `lseek64' function. */
+#undef HAVE_LSEEK64
+
+/* Found NVMe */
+#undef HAVE_NVME
+
+/* Define to 1 if you have the `posix_fadvise' function. */
+#undef HAVE_POSIX_FADVISE
+
+/* Define to 1 if you have the `posix_memalign' function. */
+#undef HAVE_POSIX_MEMALIGN
+
+/* Define to 1 if you have the `pthread_cancel' function. */
+#undef HAVE_PTHREAD_CANCEL
+
+/* Define to 1 if you have the `pthread_kill' function. */
+#undef HAVE_PTHREAD_KILL
+
+/* Define to 1 if you have the `srand48_r' function. */
+#undef HAVE_SRAND48_R
+
+/* Define to 1 if you have the <stdatomic.h> header file. */
+#undef HAVE_STDATOMIC_H
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#undef HAVE_STDINT_H
+
+/* Define to 1 if you have the <stdio.h> header file. */
+#undef HAVE_STDIO_H
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#undef HAVE_STDLIB_H
+
+/* Define to 1 if you have the <strings.h> header file. */
+#undef HAVE_STRINGS_H
+
+/* Define to 1 if you have the <string.h> header file. */
+#undef HAVE_STRING_H
+
+/* Define to 1 if you have the `sysconf' function. */
+#undef HAVE_SYSCONF
+
+/* Define to 1 if you have the <sys/random.h> header file. */
+#undef HAVE_SYS_RANDOM_H
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#undef HAVE_SYS_STAT_H
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#undef HAVE_SYS_TYPES_H
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#undef HAVE_UNISTD_H
+
+/* use generic little-endian/big-endian instead */
+#undef IGNORE_FAST_LEBE
+
+/* option ignored */
+#undef IGNORE_LINUX_BSG
+
+/* even if Linux sg v4 available, use v3 instead */
+#undef IGNORE_LINUX_SGV4
+
+/* compile out NVMe support */
+#undef IGNORE_NVME
+
+/* Define to the sub-directory where libtool stores uninstalled libraries. */
+#undef LT_OBJDIR
+
+/* Name of package */
+#undef PACKAGE
+
+/* Define to the address where bug reports for this package should be sent. */
+#undef PACKAGE_BUGREPORT
+
+/* Define to the full name of this package. */
+#undef PACKAGE_NAME
+
+/* Define to the full name and version of this package. */
+#undef PACKAGE_STRING
+
+/* Define to the one symbol short name of this package. */
+#undef PACKAGE_TARNAME
+
+/* Define to the home page for this package. */
+#undef PACKAGE_URL
+
+/* Define to the version of this package. */
+#undef PACKAGE_VERSION
+
+/* sg3_utils on android */
+#undef SG_LIB_ANDROID
+
+/* sg3_utils Build Host */
+#undef SG_LIB_BUILD_HOST
+
+/* sg3_utils on FreeBSD */
+#undef SG_LIB_FREEBSD
+
+/* sg3_utils on Haiku */
+#undef SG_LIB_HAIKU
+
+/* sg3_utils on linux */
+#undef SG_LIB_LINUX
+
+/* also MinGW environment */
+#undef SG_LIB_MINGW
+
+/* sg3_utils on NetBSD */
+#undef SG_LIB_NETBSD
+
+/* sg3_utils on OpenBSD */
+#undef SG_LIB_OPENBSD
+
+/* sg3_utils on Tru64 UNIX */
+#undef SG_LIB_OSF1
+
+/* sg3_utils on other */
+#undef SG_LIB_OTHER
+
+/* sg3_utils on Solaris */
+#undef SG_LIB_SOLARIS
+
+/* sg3_utils on Win32 */
+#undef SG_LIB_WIN32
+
+/* full SCSI sense strings and NVMe status strings */
+#undef SG_SCSI_STRINGS
+
+/* Define to 1 if all of the C90 standard headers exist (not just the ones
+   required in a freestanding environment). This macro is provided for
+   backward compatibility; new code need not use it. */
+#undef STDC_HEADERS
+
+/* Version number of package */
+#undef VERSION
+
+/* enable Win32 SPT Direct */
+#undef WIN32_SPT_DIRECT
diff --git a/config.sub b/config.sub
new file mode 100755
index 0000000..dba16e8
--- /dev/null
+++ b/config.sub
@@ -0,0 +1,1890 @@
+#! /bin/sh
+# Configuration validation subroutine script.
+#   Copyright 1992-2022 Free Software Foundation, Inc.
+
+# shellcheck disable=SC2006,SC2268 # see below for rationale
+
+timestamp='2022-01-03'
+
+# This file is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses/>.
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that
+# program.  This Exception is an additional permission under section 7
+# of the GNU General Public License, version 3 ("GPLv3").
+
+
+# Please send patches to <config-patches@gnu.org>.
+#
+# Configuration subroutine to validate and canonicalize a configuration type.
+# Supply the specified configuration type as an argument.
+# If it is invalid, we print an error message on stderr and exit with code 1.
+# Otherwise, we print the canonical config type on stdout and succeed.
+
+# You can get the latest version of this script from:
+# https://git.savannah.gnu.org/cgit/config.git/plain/config.sub
+
+# This file is supposed to be the same for all GNU packages
+# and recognize all the CPU types, system types and aliases
+# that are meaningful with *any* GNU software.
+# Each package is responsible for reporting which valid configurations
+# it does not support.  The user should be able to distinguish
+# a failure to support a valid configuration from a meaningless
+# configuration.
+
+# The goal of this file is to map all the various variations of a given
+# machine specification into a single specification in the form:
+#	CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM
+# or in some cases, the newer four-part form:
+#	CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM
+# It is wrong to echo any other type of specification.
+
+# The "shellcheck disable" line above the timestamp inhibits complaints
+# about features and limitations of the classic Bourne shell that were
+# superseded or lifted in POSIX.  However, this script identifies a wide
+# variety of pre-POSIX systems that do not have POSIX shells at all, and
+# even some reasonably current systems (Solaris 10 as case-in-point) still
+# have a pre-POSIX /bin/sh.
+
+me=`echo "$0" | sed -e 's,.*/,,'`
+
+usage="\
+Usage: $0 [OPTION] CPU-MFR-OPSYS or ALIAS
+
+Canonicalize a configuration name.
+
+Options:
+  -h, --help         print this help, then exit
+  -t, --time-stamp   print date of last modification, then exit
+  -v, --version      print version number, then exit
+
+Report bugs and patches to <config-patches@gnu.org>."
+
+version="\
+GNU config.sub ($timestamp)
+
+Copyright 1992-2022 Free Software Foundation, Inc.
+
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
+
+help="
+Try \`$me --help' for more information."
+
+# Parse command line
+while test $# -gt 0 ; do
+  case $1 in
+    --time-stamp | --time* | -t )
+       echo "$timestamp" ; exit ;;
+    --version | -v )
+       echo "$version" ; exit ;;
+    --help | --h* | -h )
+       echo "$usage"; exit ;;
+    -- )     # Stop option processing
+       shift; break ;;
+    - )	# Use stdin as input.
+       break ;;
+    -* )
+       echo "$me: invalid option $1$help" >&2
+       exit 1 ;;
+
+    *local*)
+       # First pass through any local machine types.
+       echo "$1"
+       exit ;;
+
+    * )
+       break ;;
+  esac
+done
+
+case $# in
+ 0) echo "$me: missing argument$help" >&2
+    exit 1;;
+ 1) ;;
+ *) echo "$me: too many arguments$help" >&2
+    exit 1;;
+esac
+
+# Split fields of configuration type
+# shellcheck disable=SC2162
+saved_IFS=$IFS
+IFS="-" read field1 field2 field3 field4 <<EOF
+$1
+EOF
+IFS=$saved_IFS
+
+# Separate into logical components for further validation
+case $1 in
+	*-*-*-*-*)
+		echo Invalid configuration \`"$1"\': more than four components >&2
+		exit 1
+		;;
+	*-*-*-*)
+		basic_machine=$field1-$field2
+		basic_os=$field3-$field4
+		;;
+	*-*-*)
+		# Ambiguous whether COMPANY is present, or skipped and KERNEL-OS is two
+		# parts
+		maybe_os=$field2-$field3
+		case $maybe_os in
+			nto-qnx* | linux-* | uclinux-uclibc* \
+			| uclinux-gnu* | kfreebsd*-gnu* | knetbsd*-gnu* | netbsd*-gnu* \
+			| netbsd*-eabi* | kopensolaris*-gnu* | cloudabi*-eabi* \
+			| storm-chaos* | os2-emx* | rtmk-nova*)
+				basic_machine=$field1
+				basic_os=$maybe_os
+				;;
+			android-linux)
+				basic_machine=$field1-unknown
+				basic_os=linux-android
+				;;
+			*)
+				basic_machine=$field1-$field2
+				basic_os=$field3
+				;;
+		esac
+		;;
+	*-*)
+		# A lone config we happen to match not fitting any pattern
+		case $field1-$field2 in
+			decstation-3100)
+				basic_machine=mips-dec
+				basic_os=
+				;;
+			*-*)
+				# Second component is usually, but not always the OS
+				case $field2 in
+					# Prevent following clause from handling this valid os
+					sun*os*)
+						basic_machine=$field1
+						basic_os=$field2
+						;;
+					zephyr*)
+						basic_machine=$field1-unknown
+						basic_os=$field2
+						;;
+					# Manufacturers
+					dec* | mips* | sequent* | encore* | pc533* | sgi* | sony* \
+					| att* | 7300* | 3300* | delta* | motorola* | sun[234]* \
+					| unicom* | ibm* | next | hp | isi* | apollo | altos* \
+					| convergent* | ncr* | news | 32* | 3600* | 3100* \
+					| hitachi* | c[123]* | convex* | sun | crds | omron* | dg \
+					| ultra | tti* | harris | dolphin | highlevel | gould \
+					| cbm | ns | masscomp | apple | axis | knuth | cray \
+					| microblaze* | sim | cisco \
+					| oki | wec | wrs | winbond)
+						basic_machine=$field1-$field2
+						basic_os=
+						;;
+					*)
+						basic_machine=$field1
+						basic_os=$field2
+						;;
+				esac
+			;;
+		esac
+		;;
+	*)
+		# Convert single-component short-hands not valid as part of
+		# multi-component configurations.
+		case $field1 in
+			386bsd)
+				basic_machine=i386-pc
+				basic_os=bsd
+				;;
+			a29khif)
+				basic_machine=a29k-amd
+				basic_os=udi
+				;;
+			adobe68k)
+				basic_machine=m68010-adobe
+				basic_os=scout
+				;;
+			alliant)
+				basic_machine=fx80-alliant
+				basic_os=
+				;;
+			altos | altos3068)
+				basic_machine=m68k-altos
+				basic_os=
+				;;
+			am29k)
+				basic_machine=a29k-none
+				basic_os=bsd
+				;;
+			amdahl)
+				basic_machine=580-amdahl
+				basic_os=sysv
+				;;
+			amiga)
+				basic_machine=m68k-unknown
+				basic_os=
+				;;
+			amigaos | amigados)
+				basic_machine=m68k-unknown
+				basic_os=amigaos
+				;;
+			amigaunix | amix)
+				basic_machine=m68k-unknown
+				basic_os=sysv4
+				;;
+			apollo68)
+				basic_machine=m68k-apollo
+				basic_os=sysv
+				;;
+			apollo68bsd)
+				basic_machine=m68k-apollo
+				basic_os=bsd
+				;;
+			aros)
+				basic_machine=i386-pc
+				basic_os=aros
+				;;
+			aux)
+				basic_machine=m68k-apple
+				basic_os=aux
+				;;
+			balance)
+				basic_machine=ns32k-sequent
+				basic_os=dynix
+				;;
+			blackfin)
+				basic_machine=bfin-unknown
+				basic_os=linux
+				;;
+			cegcc)
+				basic_machine=arm-unknown
+				basic_os=cegcc
+				;;
+			convex-c1)
+				basic_machine=c1-convex
+				basic_os=bsd
+				;;
+			convex-c2)
+				basic_machine=c2-convex
+				basic_os=bsd
+				;;
+			convex-c32)
+				basic_machine=c32-convex
+				basic_os=bsd
+				;;
+			convex-c34)
+				basic_machine=c34-convex
+				basic_os=bsd
+				;;
+			convex-c38)
+				basic_machine=c38-convex
+				basic_os=bsd
+				;;
+			cray)
+				basic_machine=j90-cray
+				basic_os=unicos
+				;;
+			crds | unos)
+				basic_machine=m68k-crds
+				basic_os=
+				;;
+			da30)
+				basic_machine=m68k-da30
+				basic_os=
+				;;
+			decstation | pmax | pmin | dec3100 | decstatn)
+				basic_machine=mips-dec
+				basic_os=
+				;;
+			delta88)
+				basic_machine=m88k-motorola
+				basic_os=sysv3
+				;;
+			dicos)
+				basic_machine=i686-pc
+				basic_os=dicos
+				;;
+			djgpp)
+				basic_machine=i586-pc
+				basic_os=msdosdjgpp
+				;;
+			ebmon29k)
+				basic_machine=a29k-amd
+				basic_os=ebmon
+				;;
+			es1800 | OSE68k | ose68k | ose | OSE)
+				basic_machine=m68k-ericsson
+				basic_os=ose
+				;;
+			gmicro)
+				basic_machine=tron-gmicro
+				basic_os=sysv
+				;;
+			go32)
+				basic_machine=i386-pc
+				basic_os=go32
+				;;
+			h8300hms)
+				basic_machine=h8300-hitachi
+				basic_os=hms
+				;;
+			h8300xray)
+				basic_machine=h8300-hitachi
+				basic_os=xray
+				;;
+			h8500hms)
+				basic_machine=h8500-hitachi
+				basic_os=hms
+				;;
+			harris)
+				basic_machine=m88k-harris
+				basic_os=sysv3
+				;;
+			hp300 | hp300hpux)
+				basic_machine=m68k-hp
+				basic_os=hpux
+				;;
+			hp300bsd)
+				basic_machine=m68k-hp
+				basic_os=bsd
+				;;
+			hppaosf)
+				basic_machine=hppa1.1-hp
+				basic_os=osf
+				;;
+			hppro)
+				basic_machine=hppa1.1-hp
+				basic_os=proelf
+				;;
+			i386mach)
+				basic_machine=i386-mach
+				basic_os=mach
+				;;
+			isi68 | isi)
+				basic_machine=m68k-isi
+				basic_os=sysv
+				;;
+			m68knommu)
+				basic_machine=m68k-unknown
+				basic_os=linux
+				;;
+			magnum | m3230)
+				basic_machine=mips-mips
+				basic_os=sysv
+				;;
+			merlin)
+				basic_machine=ns32k-utek
+				basic_os=sysv
+				;;
+			mingw64)
+				basic_machine=x86_64-pc
+				basic_os=mingw64
+				;;
+			mingw32)
+				basic_machine=i686-pc
+				basic_os=mingw32
+				;;
+			mingw32ce)
+				basic_machine=arm-unknown
+				basic_os=mingw32ce
+				;;
+			monitor)
+				basic_machine=m68k-rom68k
+				basic_os=coff
+				;;
+			morphos)
+				basic_machine=powerpc-unknown
+				basic_os=morphos
+				;;
+			moxiebox)
+				basic_machine=moxie-unknown
+				basic_os=moxiebox
+				;;
+			msdos)
+				basic_machine=i386-pc
+				basic_os=msdos
+				;;
+			msys)
+				basic_machine=i686-pc
+				basic_os=msys
+				;;
+			mvs)
+				basic_machine=i370-ibm
+				basic_os=mvs
+				;;
+			nacl)
+				basic_machine=le32-unknown
+				basic_os=nacl
+				;;
+			ncr3000)
+				basic_machine=i486-ncr
+				basic_os=sysv4
+				;;
+			netbsd386)
+				basic_machine=i386-pc
+				basic_os=netbsd
+				;;
+			netwinder)
+				basic_machine=armv4l-rebel
+				basic_os=linux
+				;;
+			news | news700 | news800 | news900)
+				basic_machine=m68k-sony
+				basic_os=newsos
+				;;
+			news1000)
+				basic_machine=m68030-sony
+				basic_os=newsos
+				;;
+			necv70)
+				basic_machine=v70-nec
+				basic_os=sysv
+				;;
+			nh3000)
+				basic_machine=m68k-harris
+				basic_os=cxux
+				;;
+			nh[45]000)
+				basic_machine=m88k-harris
+				basic_os=cxux
+				;;
+			nindy960)
+				basic_machine=i960-intel
+				basic_os=nindy
+				;;
+			mon960)
+				basic_machine=i960-intel
+				basic_os=mon960
+				;;
+			nonstopux)
+				basic_machine=mips-compaq
+				basic_os=nonstopux
+				;;
+			os400)
+				basic_machine=powerpc-ibm
+				basic_os=os400
+				;;
+			OSE68000 | ose68000)
+				basic_machine=m68000-ericsson
+				basic_os=ose
+				;;
+			os68k)
+				basic_machine=m68k-none
+				basic_os=os68k
+				;;
+			paragon)
+				basic_machine=i860-intel
+				basic_os=osf
+				;;
+			parisc)
+				basic_machine=hppa-unknown
+				basic_os=linux
+				;;
+			psp)
+				basic_machine=mipsallegrexel-sony
+				basic_os=psp
+				;;
+			pw32)
+				basic_machine=i586-unknown
+				basic_os=pw32
+				;;
+			rdos | rdos64)
+				basic_machine=x86_64-pc
+				basic_os=rdos
+				;;
+			rdos32)
+				basic_machine=i386-pc
+				basic_os=rdos
+				;;
+			rom68k)
+				basic_machine=m68k-rom68k
+				basic_os=coff
+				;;
+			sa29200)
+				basic_machine=a29k-amd
+				basic_os=udi
+				;;
+			sei)
+				basic_machine=mips-sei
+				basic_os=seiux
+				;;
+			sequent)
+				basic_machine=i386-sequent
+				basic_os=
+				;;
+			sps7)
+				basic_machine=m68k-bull
+				basic_os=sysv2
+				;;
+			st2000)
+				basic_machine=m68k-tandem
+				basic_os=
+				;;
+			stratus)
+				basic_machine=i860-stratus
+				basic_os=sysv4
+				;;
+			sun2)
+				basic_machine=m68000-sun
+				basic_os=
+				;;
+			sun2os3)
+				basic_machine=m68000-sun
+				basic_os=sunos3
+				;;
+			sun2os4)
+				basic_machine=m68000-sun
+				basic_os=sunos4
+				;;
+			sun3)
+				basic_machine=m68k-sun
+				basic_os=
+				;;
+			sun3os3)
+				basic_machine=m68k-sun
+				basic_os=sunos3
+				;;
+			sun3os4)
+				basic_machine=m68k-sun
+				basic_os=sunos4
+				;;
+			sun4)
+				basic_machine=sparc-sun
+				basic_os=
+				;;
+			sun4os3)
+				basic_machine=sparc-sun
+				basic_os=sunos3
+				;;
+			sun4os4)
+				basic_machine=sparc-sun
+				basic_os=sunos4
+				;;
+			sun4sol2)
+				basic_machine=sparc-sun
+				basic_os=solaris2
+				;;
+			sun386 | sun386i | roadrunner)
+				basic_machine=i386-sun
+				basic_os=
+				;;
+			sv1)
+				basic_machine=sv1-cray
+				basic_os=unicos
+				;;
+			symmetry)
+				basic_machine=i386-sequent
+				basic_os=dynix
+				;;
+			t3e)
+				basic_machine=alphaev5-cray
+				basic_os=unicos
+				;;
+			t90)
+				basic_machine=t90-cray
+				basic_os=unicos
+				;;
+			toad1)
+				basic_machine=pdp10-xkl
+				basic_os=tops20
+				;;
+			tpf)
+				basic_machine=s390x-ibm
+				basic_os=tpf
+				;;
+			udi29k)
+				basic_machine=a29k-amd
+				basic_os=udi
+				;;
+			ultra3)
+				basic_machine=a29k-nyu
+				basic_os=sym1
+				;;
+			v810 | necv810)
+				basic_machine=v810-nec
+				basic_os=none
+				;;
+			vaxv)
+				basic_machine=vax-dec
+				basic_os=sysv
+				;;
+			vms)
+				basic_machine=vax-dec
+				basic_os=vms
+				;;
+			vsta)
+				basic_machine=i386-pc
+				basic_os=vsta
+				;;
+			vxworks960)
+				basic_machine=i960-wrs
+				basic_os=vxworks
+				;;
+			vxworks68)
+				basic_machine=m68k-wrs
+				basic_os=vxworks
+				;;
+			vxworks29k)
+				basic_machine=a29k-wrs
+				basic_os=vxworks
+				;;
+			xbox)
+				basic_machine=i686-pc
+				basic_os=mingw32
+				;;
+			ymp)
+				basic_machine=ymp-cray
+				basic_os=unicos
+				;;
+			*)
+				basic_machine=$1
+				basic_os=
+				;;
+		esac
+		;;
+esac
+
+# Decode 1-component or ad-hoc basic machines
+case $basic_machine in
+	# Here we handle the default manufacturer of certain CPU types.  It is in
+	# some cases the only manufacturer, in others, it is the most popular.
+	w89k)
+		cpu=hppa1.1
+		vendor=winbond
+		;;
+	op50n)
+		cpu=hppa1.1
+		vendor=oki
+		;;
+	op60c)
+		cpu=hppa1.1
+		vendor=oki
+		;;
+	ibm*)
+		cpu=i370
+		vendor=ibm
+		;;
+	orion105)
+		cpu=clipper
+		vendor=highlevel
+		;;
+	mac | mpw | mac-mpw)
+		cpu=m68k
+		vendor=apple
+		;;
+	pmac | pmac-mpw)
+		cpu=powerpc
+		vendor=apple
+		;;
+
+	# Recognize the various machine names and aliases which stand
+	# for a CPU type and a company and sometimes even an OS.
+	3b1 | 7300 | 7300-att | att-7300 | pc7300 | safari | unixpc)
+		cpu=m68000
+		vendor=att
+		;;
+	3b*)
+		cpu=we32k
+		vendor=att
+		;;
+	bluegene*)
+		cpu=powerpc
+		vendor=ibm
+		basic_os=cnk
+		;;
+	decsystem10* | dec10*)
+		cpu=pdp10
+		vendor=dec
+		basic_os=tops10
+		;;
+	decsystem20* | dec20*)
+		cpu=pdp10
+		vendor=dec
+		basic_os=tops20
+		;;
+	delta | 3300 | motorola-3300 | motorola-delta \
+	      | 3300-motorola | delta-motorola)
+		cpu=m68k
+		vendor=motorola
+		;;
+	dpx2*)
+		cpu=m68k
+		vendor=bull
+		basic_os=sysv3
+		;;
+	encore | umax | mmax)
+		cpu=ns32k
+		vendor=encore
+		;;
+	elxsi)
+		cpu=elxsi
+		vendor=elxsi
+		basic_os=${basic_os:-bsd}
+		;;
+	fx2800)
+		cpu=i860
+		vendor=alliant
+		;;
+	genix)
+		cpu=ns32k
+		vendor=ns
+		;;
+	h3050r* | hiux*)
+		cpu=hppa1.1
+		vendor=hitachi
+		basic_os=hiuxwe2
+		;;
+	hp3k9[0-9][0-9] | hp9[0-9][0-9])
+		cpu=hppa1.0
+		vendor=hp
+		;;
+	hp9k2[0-9][0-9] | hp9k31[0-9])
+		cpu=m68000
+		vendor=hp
+		;;
+	hp9k3[2-9][0-9])
+		cpu=m68k
+		vendor=hp
+		;;
+	hp9k6[0-9][0-9] | hp6[0-9][0-9])
+		cpu=hppa1.0
+		vendor=hp
+		;;
+	hp9k7[0-79][0-9] | hp7[0-79][0-9])
+		cpu=hppa1.1
+		vendor=hp
+		;;
+	hp9k78[0-9] | hp78[0-9])
+		# FIXME: really hppa2.0-hp
+		cpu=hppa1.1
+		vendor=hp
+		;;
+	hp9k8[67]1 | hp8[67]1 | hp9k80[24] | hp80[24] | hp9k8[78]9 | hp8[78]9 | hp9k893 | hp893)
+		# FIXME: really hppa2.0-hp
+		cpu=hppa1.1
+		vendor=hp
+		;;
+	hp9k8[0-9][13679] | hp8[0-9][13679])
+		cpu=hppa1.1
+		vendor=hp
+		;;
+	hp9k8[0-9][0-9] | hp8[0-9][0-9])
+		cpu=hppa1.0
+		vendor=hp
+		;;
+	i*86v32)
+		cpu=`echo "$1" | sed -e 's/86.*/86/'`
+		vendor=pc
+		basic_os=sysv32
+		;;
+	i*86v4*)
+		cpu=`echo "$1" | sed -e 's/86.*/86/'`
+		vendor=pc
+		basic_os=sysv4
+		;;
+	i*86v)
+		cpu=`echo "$1" | sed -e 's/86.*/86/'`
+		vendor=pc
+		basic_os=sysv
+		;;
+	i*86sol2)
+		cpu=`echo "$1" | sed -e 's/86.*/86/'`
+		vendor=pc
+		basic_os=solaris2
+		;;
+	j90 | j90-cray)
+		cpu=j90
+		vendor=cray
+		basic_os=${basic_os:-unicos}
+		;;
+	iris | iris4d)
+		cpu=mips
+		vendor=sgi
+		case $basic_os in
+		    irix*)
+			;;
+		    *)
+			basic_os=irix4
+			;;
+		esac
+		;;
+	miniframe)
+		cpu=m68000
+		vendor=convergent
+		;;
+	*mint | mint[0-9]* | *MiNT | *MiNT[0-9]*)
+		cpu=m68k
+		vendor=atari
+		basic_os=mint
+		;;
+	news-3600 | risc-news)
+		cpu=mips
+		vendor=sony
+		basic_os=newsos
+		;;
+	next | m*-next)
+		cpu=m68k
+		vendor=next
+		case $basic_os in
+		    openstep*)
+		        ;;
+		    nextstep*)
+			;;
+		    ns2*)
+		      basic_os=nextstep2
+			;;
+		    *)
+		      basic_os=nextstep3
+			;;
+		esac
+		;;
+	np1)
+		cpu=np1
+		vendor=gould
+		;;
+	op50n-* | op60c-*)
+		cpu=hppa1.1
+		vendor=oki
+		basic_os=proelf
+		;;
+	pa-hitachi)
+		cpu=hppa1.1
+		vendor=hitachi
+		basic_os=hiuxwe2
+		;;
+	pbd)
+		cpu=sparc
+		vendor=tti
+		;;
+	pbb)
+		cpu=m68k
+		vendor=tti
+		;;
+	pc532)
+		cpu=ns32k
+		vendor=pc532
+		;;
+	pn)
+		cpu=pn
+		vendor=gould
+		;;
+	power)
+		cpu=power
+		vendor=ibm
+		;;
+	ps2)
+		cpu=i386
+		vendor=ibm
+		;;
+	rm[46]00)
+		cpu=mips
+		vendor=siemens
+		;;
+	rtpc | rtpc-*)
+		cpu=romp
+		vendor=ibm
+		;;
+	sde)
+		cpu=mipsisa32
+		vendor=sde
+		basic_os=${basic_os:-elf}
+		;;
+	simso-wrs)
+		cpu=sparclite
+		vendor=wrs
+		basic_os=vxworks
+		;;
+	tower | tower-32)
+		cpu=m68k
+		vendor=ncr
+		;;
+	vpp*|vx|vx-*)
+		cpu=f301
+		vendor=fujitsu
+		;;
+	w65)
+		cpu=w65
+		vendor=wdc
+		;;
+	w89k-*)
+		cpu=hppa1.1
+		vendor=winbond
+		basic_os=proelf
+		;;
+	none)
+		cpu=none
+		vendor=none
+		;;
+	leon|leon[3-9])
+		cpu=sparc
+		vendor=$basic_machine
+		;;
+	leon-*|leon[3-9]-*)
+		cpu=sparc
+		vendor=`echo "$basic_machine" | sed 's/-.*//'`
+		;;
+
+	*-*)
+		# shellcheck disable=SC2162
+		saved_IFS=$IFS
+		IFS="-" read cpu vendor <<EOF
+$basic_machine
+EOF
+		IFS=$saved_IFS
+		;;
+	# We use `pc' rather than `unknown'
+	# because (1) that's what they normally are, and
+	# (2) the word "unknown" tends to confuse beginning users.
+	i*86 | x86_64)
+		cpu=$basic_machine
+		vendor=pc
+		;;
+	# These rules are duplicated from below for sake of the special case above;
+	# i.e. things that normalized to x86 arches should also default to "pc"
+	pc98)
+		cpu=i386
+		vendor=pc
+		;;
+	x64 | amd64)
+		cpu=x86_64
+		vendor=pc
+		;;
+	# Recognize the basic CPU types without company name.
+	*)
+		cpu=$basic_machine
+		vendor=unknown
+		;;
+esac
+
+unset -v basic_machine
+
+# Decode basic machines in the full and proper CPU-Company form.
+case $cpu-$vendor in
+	# Here we handle the default manufacturer of certain CPU types in canonical form. It is in
+	# some cases the only manufacturer, in others, it is the most popular.
+	craynv-unknown)
+		vendor=cray
+		basic_os=${basic_os:-unicosmp}
+		;;
+	c90-unknown | c90-cray)
+		vendor=cray
+		basic_os=${Basic_os:-unicos}
+		;;
+	fx80-unknown)
+		vendor=alliant
+		;;
+	romp-unknown)
+		vendor=ibm
+		;;
+	mmix-unknown)
+		vendor=knuth
+		;;
+	microblaze-unknown | microblazeel-unknown)
+		vendor=xilinx
+		;;
+	rs6000-unknown)
+		vendor=ibm
+		;;
+	vax-unknown)
+		vendor=dec
+		;;
+	pdp11-unknown)
+		vendor=dec
+		;;
+	we32k-unknown)
+		vendor=att
+		;;
+	cydra-unknown)
+		vendor=cydrome
+		;;
+	i370-ibm*)
+		vendor=ibm
+		;;
+	orion-unknown)
+		vendor=highlevel
+		;;
+	xps-unknown | xps100-unknown)
+		cpu=xps100
+		vendor=honeywell
+		;;
+
+	# Here we normalize CPU types with a missing or matching vendor
+	armh-unknown | armh-alt)
+		cpu=armv7l
+		vendor=alt
+		basic_os=${basic_os:-linux-gnueabihf}
+		;;
+	dpx20-unknown | dpx20-bull)
+		cpu=rs6000
+		vendor=bull
+		basic_os=${basic_os:-bosx}
+		;;
+
+	# Here we normalize CPU types irrespective of the vendor
+	amd64-*)
+		cpu=x86_64
+		;;
+	blackfin-*)
+		cpu=bfin
+		basic_os=linux
+		;;
+	c54x-*)
+		cpu=tic54x
+		;;
+	c55x-*)
+		cpu=tic55x
+		;;
+	c6x-*)
+		cpu=tic6x
+		;;
+	e500v[12]-*)
+		cpu=powerpc
+		basic_os=${basic_os}"spe"
+		;;
+	mips3*-*)
+		cpu=mips64
+		;;
+	ms1-*)
+		cpu=mt
+		;;
+	m68knommu-*)
+		cpu=m68k
+		basic_os=linux
+		;;
+	m9s12z-* | m68hcs12z-* | hcs12z-* | s12z-*)
+		cpu=s12z
+		;;
+	openrisc-*)
+		cpu=or32
+		;;
+	parisc-*)
+		cpu=hppa
+		basic_os=linux
+		;;
+	pentium-* | p5-* | k5-* | k6-* | nexgen-* | viac3-*)
+		cpu=i586
+		;;
+	pentiumpro-* | p6-* | 6x86-* | athlon-* | athalon_*-*)
+		cpu=i686
+		;;
+	pentiumii-* | pentium2-* | pentiumiii-* | pentium3-*)
+		cpu=i686
+		;;
+	pentium4-*)
+		cpu=i786
+		;;
+	pc98-*)
+		cpu=i386
+		;;
+	ppc-* | ppcbe-*)
+		cpu=powerpc
+		;;
+	ppcle-* | powerpclittle-*)
+		cpu=powerpcle
+		;;
+	ppc64-*)
+		cpu=powerpc64
+		;;
+	ppc64le-* | powerpc64little-*)
+		cpu=powerpc64le
+		;;
+	sb1-*)
+		cpu=mipsisa64sb1
+		;;
+	sb1el-*)
+		cpu=mipsisa64sb1el
+		;;
+	sh5e[lb]-*)
+		cpu=`echo "$cpu" | sed 's/^\(sh.\)e\(.\)$/\1\2e/'`
+		;;
+	spur-*)
+		cpu=spur
+		;;
+	strongarm-* | thumb-*)
+		cpu=arm
+		;;
+	tx39-*)
+		cpu=mipstx39
+		;;
+	tx39el-*)
+		cpu=mipstx39el
+		;;
+	x64-*)
+		cpu=x86_64
+		;;
+	xscale-* | xscalee[bl]-*)
+		cpu=`echo "$cpu" | sed 's/^xscale/arm/'`
+		;;
+	arm64-* | aarch64le-*)
+		cpu=aarch64
+		;;
+
+	# Recognize the canonical CPU Types that limit and/or modify the
+	# company names they are paired with.
+	cr16-*)
+		basic_os=${basic_os:-elf}
+		;;
+	crisv32-* | etraxfs*-*)
+		cpu=crisv32
+		vendor=axis
+		;;
+	cris-* | etrax*-*)
+		cpu=cris
+		vendor=axis
+		;;
+	crx-*)
+		basic_os=${basic_os:-elf}
+		;;
+	neo-tandem)
+		cpu=neo
+		vendor=tandem
+		;;
+	nse-tandem)
+		cpu=nse
+		vendor=tandem
+		;;
+	nsr-tandem)
+		cpu=nsr
+		vendor=tandem
+		;;
+	nsv-tandem)
+		cpu=nsv
+		vendor=tandem
+		;;
+	nsx-tandem)
+		cpu=nsx
+		vendor=tandem
+		;;
+	mipsallegrexel-sony)
+		cpu=mipsallegrexel
+		vendor=sony
+		;;
+	tile*-*)
+		basic_os=${basic_os:-linux-gnu}
+		;;
+
+	*)
+		# Recognize the canonical CPU types that are allowed with any
+		# company name.
+		case $cpu in
+			1750a | 580 \
+			| a29k \
+			| aarch64 | aarch64_be \
+			| abacus \
+			| alpha | alphaev[4-8] | alphaev56 | alphaev6[78] \
+			| alpha64 | alpha64ev[4-8] | alpha64ev56 | alpha64ev6[78] \
+			| alphapca5[67] | alpha64pca5[67] \
+			| am33_2.0 \
+			| amdgcn \
+			| arc | arceb | arc32 | arc64 \
+			| arm | arm[lb]e | arme[lb] | armv* \
+			| avr | avr32 \
+			| asmjs \
+			| ba \
+			| be32 | be64 \
+			| bfin | bpf | bs2000 \
+			| c[123]* | c30 | [cjt]90 | c4x \
+			| c8051 | clipper | craynv | csky | cydra \
+			| d10v | d30v | dlx | dsp16xx \
+			| e2k | elxsi | epiphany \
+			| f30[01] | f700 | fido | fr30 | frv | ft32 | fx80 \
+			| h8300 | h8500 \
+			| hppa | hppa1.[01] | hppa2.0 | hppa2.0[nw] | hppa64 \
+			| hexagon \
+			| i370 | i*86 | i860 | i960 | ia16 | ia64 \
+			| ip2k | iq2000 \
+			| k1om \
+			| le32 | le64 \
+			| lm32 \
+			| loongarch32 | loongarch64 | loongarchx32 \
+			| m32c | m32r | m32rle \
+			| m5200 | m68000 | m680[012346]0 | m68360 | m683?2 | m68k \
+			| m6811 | m68hc11 | m6812 | m68hc12 | m68hcs12x \
+			| m88110 | m88k | maxq | mb | mcore | mep | metag \
+			| microblaze | microblazeel \
+			| mips | mipsbe | mipseb | mipsel | mipsle \
+			| mips16 \
+			| mips64 | mips64eb | mips64el \
+			| mips64octeon | mips64octeonel \
+			| mips64orion | mips64orionel \
+			| mips64r5900 | mips64r5900el \
+			| mips64vr | mips64vrel \
+			| mips64vr4100 | mips64vr4100el \
+			| mips64vr4300 | mips64vr4300el \
+			| mips64vr5000 | mips64vr5000el \
+			| mips64vr5900 | mips64vr5900el \
+			| mipsisa32 | mipsisa32el \
+			| mipsisa32r2 | mipsisa32r2el \
+			| mipsisa32r3 | mipsisa32r3el \
+			| mipsisa32r5 | mipsisa32r5el \
+			| mipsisa32r6 | mipsisa32r6el \
+			| mipsisa64 | mipsisa64el \
+			| mipsisa64r2 | mipsisa64r2el \
+			| mipsisa64r3 | mipsisa64r3el \
+			| mipsisa64r5 | mipsisa64r5el \
+			| mipsisa64r6 | mipsisa64r6el \
+			| mipsisa64sb1 | mipsisa64sb1el \
+			| mipsisa64sr71k | mipsisa64sr71kel \
+			| mipsr5900 | mipsr5900el \
+			| mipstx39 | mipstx39el \
+			| mmix \
+			| mn10200 | mn10300 \
+			| moxie \
+			| mt \
+			| msp430 \
+			| nds32 | nds32le | nds32be \
+			| nfp \
+			| nios | nios2 | nios2eb | nios2el \
+			| none | np1 | ns16k | ns32k | nvptx \
+			| open8 \
+			| or1k* \
+			| or32 \
+			| orion \
+			| picochip \
+			| pdp10 | pdp11 | pj | pjl | pn | power \
+			| powerpc | powerpc64 | powerpc64le | powerpcle | powerpcspe \
+			| pru \
+			| pyramid \
+			| riscv | riscv32 | riscv32be | riscv64 | riscv64be \
+			| rl78 | romp | rs6000 | rx \
+			| s390 | s390x \
+			| score \
+			| sh | shl \
+			| sh[1234] | sh[24]a | sh[24]ae[lb] | sh[23]e | she[lb] | sh[lb]e \
+			| sh[1234]e[lb] |  sh[12345][lb]e | sh[23]ele | sh64 | sh64le \
+			| sparc | sparc64 | sparc64b | sparc64v | sparc86x | sparclet \
+			| sparclite \
+			| sparcv8 | sparcv9 | sparcv9b | sparcv9v | sv1 | sx* \
+			| spu \
+			| tahoe \
+			| thumbv7* \
+			| tic30 | tic4x | tic54x | tic55x | tic6x | tic80 \
+			| tron \
+			| ubicom32 \
+			| v70 | v850 | v850e | v850e1 | v850es | v850e2 | v850e2v3 \
+			| vax \
+			| visium \
+			| w65 \
+			| wasm32 | wasm64 \
+			| we32k \
+			| x86 | x86_64 | xc16x | xgate | xps100 \
+			| xstormy16 | xtensa* \
+			| ymp \
+			| z8k | z80)
+				;;
+
+			*)
+				echo Invalid configuration \`"$1"\': machine \`"$cpu-$vendor"\' not recognized 1>&2
+				exit 1
+				;;
+		esac
+		;;
+esac
+
+# Here we canonicalize certain aliases for manufacturers.
+case $vendor in
+	digital*)
+		vendor=dec
+		;;
+	commodore*)
+		vendor=cbm
+		;;
+	*)
+		;;
+esac
+
+# Decode manufacturer-specific aliases for certain operating systems.
+
+if test x$basic_os != x
+then
+
+# First recognize some ad-hoc cases, or perhaps split kernel-os, or else just
+# set os.
+case $basic_os in
+	gnu/linux*)
+		kernel=linux
+		os=`echo "$basic_os" | sed -e 's|gnu/linux|gnu|'`
+		;;
+	os2-emx)
+		kernel=os2
+		os=`echo "$basic_os" | sed -e 's|os2-emx|emx|'`
+		;;
+	nto-qnx*)
+		kernel=nto
+		os=`echo "$basic_os" | sed -e 's|nto-qnx|qnx|'`
+		;;
+	*-*)
+		# shellcheck disable=SC2162
+		saved_IFS=$IFS
+		IFS="-" read kernel os <<EOF
+$basic_os
+EOF
+		IFS=$saved_IFS
+		;;
+	# Default OS when just kernel was specified
+	nto*)
+		kernel=nto
+		os=`echo "$basic_os" | sed -e 's|nto|qnx|'`
+		;;
+	linux*)
+		kernel=linux
+		os=`echo "$basic_os" | sed -e 's|linux|gnu|'`
+		;;
+	*)
+		kernel=
+		os=$basic_os
+		;;
+esac
+
+# Now, normalize the OS (knowing we just have one component, it's not a kernel,
+# etc.)
+case $os in
+	# First match some system type aliases that might get confused
+	# with valid system types.
+	# solaris* is a basic system type, with this one exception.
+	auroraux)
+		os=auroraux
+		;;
+	bluegene*)
+		os=cnk
+		;;
+	solaris1 | solaris1.*)
+		os=`echo "$os" | sed -e 's|solaris1|sunos4|'`
+		;;
+	solaris)
+		os=solaris2
+		;;
+	unixware*)
+		os=sysv4.2uw
+		;;
+	# es1800 is here to avoid being matched by es* (a different OS)
+	es1800*)
+		os=ose
+		;;
+	# Some version numbers need modification
+	chorusos*)
+		os=chorusos
+		;;
+	isc)
+		os=isc2.2
+		;;
+	sco6)
+		os=sco5v6
+		;;
+	sco5)
+		os=sco3.2v5
+		;;
+	sco4)
+		os=sco3.2v4
+		;;
+	sco3.2.[4-9]*)
+		os=`echo "$os" | sed -e 's/sco3.2./sco3.2v/'`
+		;;
+	sco*v* | scout)
+		# Don't match below
+		;;
+	sco*)
+		os=sco3.2v2
+		;;
+	psos*)
+		os=psos
+		;;
+	qnx*)
+		os=qnx
+		;;
+	hiux*)
+		os=hiuxwe2
+		;;
+	lynx*178)
+		os=lynxos178
+		;;
+	lynx*5)
+		os=lynxos5
+		;;
+	lynxos*)
+		# don't get caught up in next wildcard
+		;;
+	lynx*)
+		os=lynxos
+		;;
+	mac[0-9]*)
+		os=`echo "$os" | sed -e 's|mac|macos|'`
+		;;
+	opened*)
+		os=openedition
+		;;
+	os400*)
+		os=os400
+		;;
+	sunos5*)
+		os=`echo "$os" | sed -e 's|sunos5|solaris2|'`
+		;;
+	sunos6*)
+		os=`echo "$os" | sed -e 's|sunos6|solaris3|'`
+		;;
+	wince*)
+		os=wince
+		;;
+	utek*)
+		os=bsd
+		;;
+	dynix*)
+		os=bsd
+		;;
+	acis*)
+		os=aos
+		;;
+	atheos*)
+		os=atheos
+		;;
+	syllable*)
+		os=syllable
+		;;
+	386bsd)
+		os=bsd
+		;;
+	ctix* | uts*)
+		os=sysv
+		;;
+	nova*)
+		os=rtmk-nova
+		;;
+	ns2)
+		os=nextstep2
+		;;
+	# Preserve the version number of sinix5.
+	sinix5.*)
+		os=`echo "$os" | sed -e 's|sinix|sysv|'`
+		;;
+	sinix*)
+		os=sysv4
+		;;
+	tpf*)
+		os=tpf
+		;;
+	triton*)
+		os=sysv3
+		;;
+	oss*)
+		os=sysv3
+		;;
+	svr4*)
+		os=sysv4
+		;;
+	svr3)
+		os=sysv3
+		;;
+	sysvr4)
+		os=sysv4
+		;;
+	ose*)
+		os=ose
+		;;
+	*mint | mint[0-9]* | *MiNT | MiNT[0-9]*)
+		os=mint
+		;;
+	dicos*)
+		os=dicos
+		;;
+	pikeos*)
+		# Until real need of OS specific support for
+		# particular features comes up, bare metal
+		# configurations are quite functional.
+		case $cpu in
+		    arm*)
+			os=eabi
+			;;
+		    *)
+			os=elf
+			;;
+		esac
+		;;
+	*)
+		# No normalization, but not necessarily accepted, that comes below.
+		;;
+esac
+
+else
+
+# Here we handle the default operating systems that come with various machines.
+# The value should be what the vendor currently ships out the door with their
+# machine or put another way, the most popular os provided with the machine.
+
+# Note that if you're going to try to match "-MANUFACTURER" here (say,
+# "-sun"), then you have to tell the case statement up towards the top
+# that MANUFACTURER isn't an operating system.  Otherwise, code above
+# will signal an error saying that MANUFACTURER isn't an operating
+# system, and we'll never get to this point.
+
+kernel=
+case $cpu-$vendor in
+	score-*)
+		os=elf
+		;;
+	spu-*)
+		os=elf
+		;;
+	*-acorn)
+		os=riscix1.2
+		;;
+	arm*-rebel)
+		kernel=linux
+		os=gnu
+		;;
+	arm*-semi)
+		os=aout
+		;;
+	c4x-* | tic4x-*)
+		os=coff
+		;;
+	c8051-*)
+		os=elf
+		;;
+	clipper-intergraph)
+		os=clix
+		;;
+	hexagon-*)
+		os=elf
+		;;
+	tic54x-*)
+		os=coff
+		;;
+	tic55x-*)
+		os=coff
+		;;
+	tic6x-*)
+		os=coff
+		;;
+	# This must come before the *-dec entry.
+	pdp10-*)
+		os=tops20
+		;;
+	pdp11-*)
+		os=none
+		;;
+	*-dec | vax-*)
+		os=ultrix4.2
+		;;
+	m68*-apollo)
+		os=domain
+		;;
+	i386-sun)
+		os=sunos4.0.2
+		;;
+	m68000-sun)
+		os=sunos3
+		;;
+	m68*-cisco)
+		os=aout
+		;;
+	mep-*)
+		os=elf
+		;;
+	mips*-cisco)
+		os=elf
+		;;
+	mips*-*)
+		os=elf
+		;;
+	or32-*)
+		os=coff
+		;;
+	*-tti)	# must be before sparc entry or we get the wrong os.
+		os=sysv3
+		;;
+	sparc-* | *-sun)
+		os=sunos4.1.1
+		;;
+	pru-*)
+		os=elf
+		;;
+	*-be)
+		os=beos
+		;;
+	*-ibm)
+		os=aix
+		;;
+	*-knuth)
+		os=mmixware
+		;;
+	*-wec)
+		os=proelf
+		;;
+	*-winbond)
+		os=proelf
+		;;
+	*-oki)
+		os=proelf
+		;;
+	*-hp)
+		os=hpux
+		;;
+	*-hitachi)
+		os=hiux
+		;;
+	i860-* | *-att | *-ncr | *-altos | *-motorola | *-convergent)
+		os=sysv
+		;;
+	*-cbm)
+		os=amigaos
+		;;
+	*-dg)
+		os=dgux
+		;;
+	*-dolphin)
+		os=sysv3
+		;;
+	m68k-ccur)
+		os=rtu
+		;;
+	m88k-omron*)
+		os=luna
+		;;
+	*-next)
+		os=nextstep
+		;;
+	*-sequent)
+		os=ptx
+		;;
+	*-crds)
+		os=unos
+		;;
+	*-ns)
+		os=genix
+		;;
+	i370-*)
+		os=mvs
+		;;
+	*-gould)
+		os=sysv
+		;;
+	*-highlevel)
+		os=bsd
+		;;
+	*-encore)
+		os=bsd
+		;;
+	*-sgi)
+		os=irix
+		;;
+	*-siemens)
+		os=sysv4
+		;;
+	*-masscomp)
+		os=rtu
+		;;
+	f30[01]-fujitsu | f700-fujitsu)
+		os=uxpv
+		;;
+	*-rom68k)
+		os=coff
+		;;
+	*-*bug)
+		os=coff
+		;;
+	*-apple)
+		os=macos
+		;;
+	*-atari*)
+		os=mint
+		;;
+	*-wrs)
+		os=vxworks
+		;;
+	*)
+		os=none
+		;;
+esac
+
+fi
+
+# Now, validate our (potentially fixed-up) OS.
+case $os in
+	# Sometimes we do "kernel-libc", so those need to count as OSes.
+	musl* | newlib* | relibc* | uclibc*)
+		;;
+	# Likewise for "kernel-abi"
+	eabi* | gnueabi*)
+		;;
+	# VxWorks passes extra cpu info in the 4th filed.
+	simlinux | simwindows | spe)
+		;;
+	# Now accept the basic system types.
+	# The portable systems comes first.
+	# Each alternative MUST end in a * to match a version number.
+	gnu* | android* | bsd* | mach* | minix* | genix* | ultrix* | irix* \
+	     | *vms* | esix* | aix* | cnk* | sunos | sunos[34]* \
+	     | hpux* | unos* | osf* | luna* | dgux* | auroraux* | solaris* \
+	     | sym* |  plan9* | psp* | sim* | xray* | os68k* | v88r* \
+	     | hiux* | abug | nacl* | netware* | windows* \
+	     | os9* | macos* | osx* | ios* \
+	     | mpw* | magic* | mmixware* | mon960* | lnews* \
+	     | amigaos* | amigados* | msdos* | newsos* | unicos* | aof* \
+	     | aos* | aros* | cloudabi* | sortix* | twizzler* \
+	     | nindy* | vxsim* | vxworks* | ebmon* | hms* | mvs* \
+	     | clix* | riscos* | uniplus* | iris* | isc* | rtu* | xenix* \
+	     | mirbsd* | netbsd* | dicos* | openedition* | ose* \
+	     | bitrig* | openbsd* | secbsd* | solidbsd* | libertybsd* | os108* \
+	     | ekkobsd* | freebsd* | riscix* | lynxos* | os400* \
+	     | bosx* | nextstep* | cxux* | aout* | elf* | oabi* \
+	     | ptx* | coff* | ecoff* | winnt* | domain* | vsta* \
+	     | udi* | lites* | ieee* | go32* | aux* | hcos* \
+	     | chorusrdb* | cegcc* | glidix* | serenity* \
+	     | cygwin* | msys* | pe* | moss* | proelf* | rtems* \
+	     | midipix* | mingw32* | mingw64* | mint* \
+	     | uxpv* | beos* | mpeix* | udk* | moxiebox* \
+	     | interix* | uwin* | mks* | rhapsody* | darwin* \
+	     | openstep* | oskit* | conix* | pw32* | nonstopux* \
+	     | storm-chaos* | tops10* | tenex* | tops20* | its* \
+	     | os2* | vos* | palmos* | uclinux* | nucleus* | morphos* \
+	     | scout* | superux* | sysv* | rtmk* | tpf* | windiss* \
+	     | powermax* | dnix* | nx6 | nx7 | sei* | dragonfly* \
+	     | skyos* | haiku* | rdos* | toppers* | drops* | es* \
+	     | onefs* | tirtos* | phoenix* | fuchsia* | redox* | bme* \
+	     | midnightbsd* | amdhsa* | unleashed* | emscripten* | wasi* \
+	     | nsk* | powerunix* | genode* | zvmoe* | qnx* | emx* | zephyr* \
+	     | fiwix* )
+		;;
+	# This one is extra strict with allowed versions
+	sco3.2v2 | sco3.2v[4-9]* | sco5v6*)
+		# Don't forget version if it is 3.2v4 or newer.
+		;;
+	none)
+		;;
+	*)
+		echo Invalid configuration \`"$1"\': OS \`"$os"\' not recognized 1>&2
+		exit 1
+		;;
+esac
+
+# As a final step for OS-related things, validate the OS-kernel combination
+# (given a valid OS), if there is a kernel.
+case $kernel-$os in
+	linux-gnu* | linux-dietlibc* | linux-android* | linux-newlib* \
+		   | linux-musl* | linux-relibc* | linux-uclibc* )
+		;;
+	uclinux-uclibc* )
+		;;
+	-dietlibc* | -newlib* | -musl* | -relibc* | -uclibc* )
+		# These are just libc implementations, not actual OSes, and thus
+		# require a kernel.
+		echo "Invalid configuration \`$1': libc \`$os' needs explicit kernel." 1>&2
+		exit 1
+		;;
+	kfreebsd*-gnu* | kopensolaris*-gnu*)
+		;;
+	vxworks-simlinux | vxworks-simwindows | vxworks-spe)
+		;;
+	nto-qnx*)
+		;;
+	os2-emx)
+		;;
+	*-eabi* | *-gnueabi*)
+		;;
+	-*)
+		# Blank kernel with real OS is always fine.
+		;;
+	*-*)
+		echo "Invalid configuration \`$1': Kernel \`$kernel' not known to work with OS \`$os'." 1>&2
+		exit 1
+		;;
+esac
+
+# Here we handle the case where we know the os, and the CPU type, but not the
+# manufacturer.  We pick the logical manufacturer.
+case $vendor in
+	unknown)
+		case $cpu-$os in
+			*-riscix*)
+				vendor=acorn
+				;;
+			*-sunos*)
+				vendor=sun
+				;;
+			*-cnk* | *-aix*)
+				vendor=ibm
+				;;
+			*-beos*)
+				vendor=be
+				;;
+			*-hpux*)
+				vendor=hp
+				;;
+			*-mpeix*)
+				vendor=hp
+				;;
+			*-hiux*)
+				vendor=hitachi
+				;;
+			*-unos*)
+				vendor=crds
+				;;
+			*-dgux*)
+				vendor=dg
+				;;
+			*-luna*)
+				vendor=omron
+				;;
+			*-genix*)
+				vendor=ns
+				;;
+			*-clix*)
+				vendor=intergraph
+				;;
+			*-mvs* | *-opened*)
+				vendor=ibm
+				;;
+			*-os400*)
+				vendor=ibm
+				;;
+			s390-* | s390x-*)
+				vendor=ibm
+				;;
+			*-ptx*)
+				vendor=sequent
+				;;
+			*-tpf*)
+				vendor=ibm
+				;;
+			*-vxsim* | *-vxworks* | *-windiss*)
+				vendor=wrs
+				;;
+			*-aux*)
+				vendor=apple
+				;;
+			*-hms*)
+				vendor=hitachi
+				;;
+			*-mpw* | *-macos*)
+				vendor=apple
+				;;
+			*-*mint | *-mint[0-9]* | *-*MiNT | *-MiNT[0-9]*)
+				vendor=atari
+				;;
+			*-vos*)
+				vendor=stratus
+				;;
+		esac
+		;;
+esac
+
+echo "$cpu-$vendor-${kernel:+$kernel-}$os"
+exit
+
+# Local variables:
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "timestamp='"
+# time-stamp-format: "%:y-%02m-%02d"
+# time-stamp-end: "'"
+# End:
diff --git a/configure b/configure
new file mode 100755
index 0000000..a3f5ce8
--- /dev/null
+++ b/configure
@@ -0,0 +1,16049 @@
+#! /bin/sh
+# Guess values for system-dependent variables and create Makefiles.
+# Generated by GNU Autoconf 2.71 for sg3_utils 1.48.
+#
+# Report bugs to <dgilbert@interlog.com>.
+#
+#
+# Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation,
+# Inc.
+#
+#
+# This configure script is free software; the Free Software Foundation
+# gives unlimited permission to copy, distribute and modify it.
+## -------------------- ##
+## M4sh Initialization. ##
+## -------------------- ##
+
+# Be more Bourne compatible
+DUALCASE=1; export DUALCASE # for MKS sh
+as_nop=:
+if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1
+then :
+  emulate sh
+  NULLCMD=:
+  # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which
+  # is contrary to our usage.  Disable this feature.
+  alias -g '${1+"$@"}'='"$@"'
+  setopt NO_GLOB_SUBST
+else $as_nop
+  case `(set -o) 2>/dev/null` in #(
+  *posix*) :
+    set -o posix ;; #(
+  *) :
+     ;;
+esac
+fi
+
+
+
+# Reset variables that may have inherited troublesome values from
+# the environment.
+
+# IFS needs to be set, to space, tab, and newline, in precisely that order.
+# (If _AS_PATH_WALK were called with IFS unset, it would have the
+# side effect of setting IFS to empty, thus disabling word splitting.)
+# Quoting is to prevent editors from complaining about space-tab.
+as_nl='
+'
+export as_nl
+IFS=" ""	$as_nl"
+
+PS1='$ '
+PS2='> '
+PS4='+ '
+
+# Ensure predictable behavior from utilities with locale-dependent output.
+LC_ALL=C
+export LC_ALL
+LANGUAGE=C
+export LANGUAGE
+
+# We cannot yet rely on "unset" to work, but we need these variables
+# to be unset--not just set to an empty or harmless value--now, to
+# avoid bugs in old shells (e.g. pre-3.0 UWIN ksh).  This construct
+# also avoids known problems related to "unset" and subshell syntax
+# in other old shells (e.g. bash 2.01 and pdksh 5.2.14).
+for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH
+do eval test \${$as_var+y} \
+  && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || :
+done
+
+# Ensure that fds 0, 1, and 2 are open.
+if (exec 3>&0) 2>/dev/null; then :; else exec 0</dev/null; fi
+if (exec 3>&1) 2>/dev/null; then :; else exec 1>/dev/null; fi
+if (exec 3>&2)            ; then :; else exec 2>/dev/null; fi
+
+# The user is always right.
+if ${PATH_SEPARATOR+false} :; then
+  PATH_SEPARATOR=:
+  (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {
+    (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||
+      PATH_SEPARATOR=';'
+  }
+fi
+
+
+# Find who we are.  Look in the path if we contain no directory separator.
+as_myself=
+case $0 in #((
+  *[\\/]* ) as_myself=$0 ;;
+  *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    test -r "$as_dir$0" && as_myself=$as_dir$0 && break
+  done
+IFS=$as_save_IFS
+
+     ;;
+esac
+# We did not find ourselves, most probably we were run as `sh COMMAND'
+# in which case we are not to be found in the path.
+if test "x$as_myself" = x; then
+  as_myself=$0
+fi
+if test ! -f "$as_myself"; then
+  printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2
+  exit 1
+fi
+
+
+# Use a proper internal environment variable to ensure we don't fall
+  # into an infinite loop, continuously re-executing ourselves.
+  if test x"${_as_can_reexec}" != xno && test "x$CONFIG_SHELL" != x; then
+    _as_can_reexec=no; export _as_can_reexec;
+    # We cannot yet assume a decent shell, so we have to provide a
+# neutralization value for shells without unset; and this also
+# works around shells that cannot unset nonexistent variables.
+# Preserve -v and -x to the replacement shell.
+BASH_ENV=/dev/null
+ENV=/dev/null
+(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV
+case $- in # ((((
+  *v*x* | *x*v* ) as_opts=-vx ;;
+  *v* ) as_opts=-v ;;
+  *x* ) as_opts=-x ;;
+  * ) as_opts= ;;
+esac
+exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"}
+# Admittedly, this is quite paranoid, since all the known shells bail
+# out after a failed `exec'.
+printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2
+exit 255
+  fi
+  # We don't want this to propagate to other subprocesses.
+          { _as_can_reexec=; unset _as_can_reexec;}
+if test "x$CONFIG_SHELL" = x; then
+  as_bourne_compatible="as_nop=:
+if test \${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1
+then :
+  emulate sh
+  NULLCMD=:
+  # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which
+  # is contrary to our usage.  Disable this feature.
+  alias -g '\${1+\"\$@\"}'='\"\$@\"'
+  setopt NO_GLOB_SUBST
+else \$as_nop
+  case \`(set -o) 2>/dev/null\` in #(
+  *posix*) :
+    set -o posix ;; #(
+  *) :
+     ;;
+esac
+fi
+"
+  as_required="as_fn_return () { (exit \$1); }
+as_fn_success () { as_fn_return 0; }
+as_fn_failure () { as_fn_return 1; }
+as_fn_ret_success () { return 0; }
+as_fn_ret_failure () { return 1; }
+
+exitcode=0
+as_fn_success || { exitcode=1; echo as_fn_success failed.; }
+as_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; }
+as_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; }
+as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; }
+if ( set x; as_fn_ret_success y && test x = \"\$1\" )
+then :
+
+else \$as_nop
+  exitcode=1; echo positional parameters were not saved.
+fi
+test x\$exitcode = x0 || exit 1
+blah=\$(echo \$(echo blah))
+test x\"\$blah\" = xblah || exit 1
+test -x / || exit 1"
+  as_suggested="  as_lineno_1=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_1a=\$LINENO
+  as_lineno_2=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_2a=\$LINENO
+  eval 'test \"x\$as_lineno_1'\$as_run'\" != \"x\$as_lineno_2'\$as_run'\" &&
+  test \"x\`expr \$as_lineno_1'\$as_run' + 1\`\" = \"x\$as_lineno_2'\$as_run'\"' || exit 1
+
+  test -n \"\${ZSH_VERSION+set}\${BASH_VERSION+set}\" || (
+    ECHO='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'
+    ECHO=\$ECHO\$ECHO\$ECHO\$ECHO\$ECHO
+    ECHO=\$ECHO\$ECHO\$ECHO\$ECHO\$ECHO\$ECHO
+    PATH=/empty FPATH=/empty; export PATH FPATH
+    test \"X\`printf %s \$ECHO\`\" = \"X\$ECHO\" \\
+      || test \"X\`print -r -- \$ECHO\`\" = \"X\$ECHO\" ) || exit 1
+test \$(( 1 + 1 )) = 2 || exit 1"
+  if (eval "$as_required") 2>/dev/null
+then :
+  as_have_required=yes
+else $as_nop
+  as_have_required=no
+fi
+  if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null
+then :
+
+else $as_nop
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+as_found=false
+for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+  as_found=:
+  case $as_dir in #(
+	 /*)
+	   for as_base in sh bash ksh sh5; do
+	     # Try only shells that exist, to save several forks.
+	     as_shell=$as_dir$as_base
+	     if { test -f "$as_shell" || test -f "$as_shell.exe"; } &&
+		    as_run=a "$as_shell" -c "$as_bourne_compatible""$as_required" 2>/dev/null
+then :
+  CONFIG_SHELL=$as_shell as_have_required=yes
+		   if as_run=a "$as_shell" -c "$as_bourne_compatible""$as_suggested" 2>/dev/null
+then :
+  break 2
+fi
+fi
+	   done;;
+       esac
+  as_found=false
+done
+IFS=$as_save_IFS
+if $as_found
+then :
+
+else $as_nop
+  if { test -f "$SHELL" || test -f "$SHELL.exe"; } &&
+	      as_run=a "$SHELL" -c "$as_bourne_compatible""$as_required" 2>/dev/null
+then :
+  CONFIG_SHELL=$SHELL as_have_required=yes
+fi
+fi
+
+
+      if test "x$CONFIG_SHELL" != x
+then :
+  export CONFIG_SHELL
+             # We cannot yet assume a decent shell, so we have to provide a
+# neutralization value for shells without unset; and this also
+# works around shells that cannot unset nonexistent variables.
+# Preserve -v and -x to the replacement shell.
+BASH_ENV=/dev/null
+ENV=/dev/null
+(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV
+case $- in # ((((
+  *v*x* | *x*v* ) as_opts=-vx ;;
+  *v* ) as_opts=-v ;;
+  *x* ) as_opts=-x ;;
+  * ) as_opts= ;;
+esac
+exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"}
+# Admittedly, this is quite paranoid, since all the known shells bail
+# out after a failed `exec'.
+printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2
+exit 255
+fi
+
+    if test x$as_have_required = xno
+then :
+  printf "%s\n" "$0: This script requires a shell more modern than all"
+  printf "%s\n" "$0: the shells that I found on your system."
+  if test ${ZSH_VERSION+y} ; then
+    printf "%s\n" "$0: In particular, zsh $ZSH_VERSION has bugs and should"
+    printf "%s\n" "$0: be upgraded to zsh 4.3.4 or later."
+  else
+    printf "%s\n" "$0: Please tell bug-autoconf@gnu.org and
+$0: dgilbert@interlog.com about your system, including any
+$0: error possibly output before this message. Then install
+$0: a modern shell, or manually run the script under such a
+$0: shell if you do have one."
+  fi
+  exit 1
+fi
+fi
+fi
+SHELL=${CONFIG_SHELL-/bin/sh}
+export SHELL
+# Unset more variables known to interfere with behavior of common tools.
+CLICOLOR_FORCE= GREP_OPTIONS=
+unset CLICOLOR_FORCE GREP_OPTIONS
+
+## --------------------- ##
+## M4sh Shell Functions. ##
+## --------------------- ##
+# as_fn_unset VAR
+# ---------------
+# Portably unset VAR.
+as_fn_unset ()
+{
+  { eval $1=; unset $1;}
+}
+as_unset=as_fn_unset
+
+
+# as_fn_set_status STATUS
+# -----------------------
+# Set $? to STATUS, without forking.
+as_fn_set_status ()
+{
+  return $1
+} # as_fn_set_status
+
+# as_fn_exit STATUS
+# -----------------
+# Exit the shell with STATUS, even in a "trap 0" or "set -e" context.
+as_fn_exit ()
+{
+  set +e
+  as_fn_set_status $1
+  exit $1
+} # as_fn_exit
+# as_fn_nop
+# ---------
+# Do nothing but, unlike ":", preserve the value of $?.
+as_fn_nop ()
+{
+  return $?
+}
+as_nop=as_fn_nop
+
+# as_fn_mkdir_p
+# -------------
+# Create "$as_dir" as a directory, including parents if necessary.
+as_fn_mkdir_p ()
+{
+
+  case $as_dir in #(
+  -*) as_dir=./$as_dir;;
+  esac
+  test -d "$as_dir" || eval $as_mkdir_p || {
+    as_dirs=
+    while :; do
+      case $as_dir in #(
+      *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'(
+      *) as_qdir=$as_dir;;
+      esac
+      as_dirs="'$as_qdir' $as_dirs"
+      as_dir=`$as_dirname -- "$as_dir" ||
+$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+	 X"$as_dir" : 'X\(//\)[^/]' \| \
+	 X"$as_dir" : 'X\(//\)$' \| \
+	 X"$as_dir" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$as_dir" |
+    sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)[^/].*/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`
+      test -d "$as_dir" && break
+    done
+    test -z "$as_dirs" || eval "mkdir $as_dirs"
+  } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir"
+
+
+} # as_fn_mkdir_p
+
+# as_fn_executable_p FILE
+# -----------------------
+# Test if FILE is an executable regular file.
+as_fn_executable_p ()
+{
+  test -f "$1" && test -x "$1"
+} # as_fn_executable_p
+# as_fn_append VAR VALUE
+# ----------------------
+# Append the text in VALUE to the end of the definition contained in VAR. Take
+# advantage of any shell optimizations that allow amortized linear growth over
+# repeated appends, instead of the typical quadratic growth present in naive
+# implementations.
+if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null
+then :
+  eval 'as_fn_append ()
+  {
+    eval $1+=\$2
+  }'
+else $as_nop
+  as_fn_append ()
+  {
+    eval $1=\$$1\$2
+  }
+fi # as_fn_append
+
+# as_fn_arith ARG...
+# ------------------
+# Perform arithmetic evaluation on the ARGs, and store the result in the
+# global $as_val. Take advantage of shells that can avoid forks. The arguments
+# must be portable across $(()) and expr.
+if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null
+then :
+  eval 'as_fn_arith ()
+  {
+    as_val=$(( $* ))
+  }'
+else $as_nop
+  as_fn_arith ()
+  {
+    as_val=`expr "$@" || test $? -eq 1`
+  }
+fi # as_fn_arith
+
+# as_fn_nop
+# ---------
+# Do nothing but, unlike ":", preserve the value of $?.
+as_fn_nop ()
+{
+  return $?
+}
+as_nop=as_fn_nop
+
+# as_fn_error STATUS ERROR [LINENO LOG_FD]
+# ----------------------------------------
+# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are
+# provided, also output the error to LOG_FD, referencing LINENO. Then exit the
+# script with STATUS, using 1 if that was 0.
+as_fn_error ()
+{
+  as_status=$1; test $as_status -eq 0 && as_status=1
+  if test "$4"; then
+    as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4
+  fi
+  printf "%s\n" "$as_me: error: $2" >&2
+  as_fn_exit $as_status
+} # as_fn_error
+
+if expr a : '\(a\)' >/dev/null 2>&1 &&
+   test "X`expr 00001 : '.*\(...\)'`" = X001; then
+  as_expr=expr
+else
+  as_expr=false
+fi
+
+if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then
+  as_basename=basename
+else
+  as_basename=false
+fi
+
+if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then
+  as_dirname=dirname
+else
+  as_dirname=false
+fi
+
+as_me=`$as_basename -- "$0" ||
+$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \
+	 X"$0" : 'X\(//\)$' \| \
+	 X"$0" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X/"$0" |
+    sed '/^.*\/\([^/][^/]*\)\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\/\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\/\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`
+
+# Avoid depending upon Character Ranges.
+as_cr_letters='abcdefghijklmnopqrstuvwxyz'
+as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+as_cr_Letters=$as_cr_letters$as_cr_LETTERS
+as_cr_digits='0123456789'
+as_cr_alnum=$as_cr_Letters$as_cr_digits
+
+
+  as_lineno_1=$LINENO as_lineno_1a=$LINENO
+  as_lineno_2=$LINENO as_lineno_2a=$LINENO
+  eval 'test "x$as_lineno_1'$as_run'" != "x$as_lineno_2'$as_run'" &&
+  test "x`expr $as_lineno_1'$as_run' + 1`" = "x$as_lineno_2'$as_run'"' || {
+  # Blame Lee E. McMahon (1931-1989) for sed's syntax.  :-)
+  sed -n '
+    p
+    /[$]LINENO/=
+  ' <$as_myself |
+    sed '
+      s/[$]LINENO.*/&-/
+      t lineno
+      b
+      :lineno
+      N
+      :loop
+      s/[$]LINENO\([^'$as_cr_alnum'_].*\n\)\(.*\)/\2\1\2/
+      t loop
+      s/-\n.*//
+    ' >$as_me.lineno &&
+  chmod +x "$as_me.lineno" ||
+    { printf "%s\n" "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; }
+
+  # If we had to re-execute with $CONFIG_SHELL, we're ensured to have
+  # already done that, so ensure we don't try to do so again and fall
+  # in an infinite loop.  This has already happened in practice.
+  _as_can_reexec=no; export _as_can_reexec
+  # Don't try to exec as it changes $[0], causing all sort of problems
+  # (the dirname of $[0] is not the place where we might find the
+  # original and so on.  Autoconf is especially sensitive to this).
+  . "./$as_me.lineno"
+  # Exit status is that of the last command.
+  exit
+}
+
+
+# Determine whether it's possible to make 'echo' print without a newline.
+# These variables are no longer used directly by Autoconf, but are AC_SUBSTed
+# for compatibility with existing Makefiles.
+ECHO_C= ECHO_N= ECHO_T=
+case `echo -n x` in #(((((
+-n*)
+  case `echo 'xy\c'` in
+  *c*) ECHO_T='	';;	# ECHO_T is single tab character.
+  xy)  ECHO_C='\c';;
+  *)   echo `echo ksh88 bug on AIX 6.1` > /dev/null
+       ECHO_T='	';;
+  esac;;
+*)
+  ECHO_N='-n';;
+esac
+
+# For backward compatibility with old third-party macros, we provide
+# the shell variables $as_echo and $as_echo_n.  New code should use
+# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively.
+as_echo='printf %s\n'
+as_echo_n='printf %s'
+
+
+rm -f conf$$ conf$$.exe conf$$.file
+if test -d conf$$.dir; then
+  rm -f conf$$.dir/conf$$.file
+else
+  rm -f conf$$.dir
+  mkdir conf$$.dir 2>/dev/null
+fi
+if (echo >conf$$.file) 2>/dev/null; then
+  if ln -s conf$$.file conf$$ 2>/dev/null; then
+    as_ln_s='ln -s'
+    # ... but there are two gotchas:
+    # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail.
+    # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable.
+    # In both cases, we have to default to `cp -pR'.
+    ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe ||
+      as_ln_s='cp -pR'
+  elif ln conf$$.file conf$$ 2>/dev/null; then
+    as_ln_s=ln
+  else
+    as_ln_s='cp -pR'
+  fi
+else
+  as_ln_s='cp -pR'
+fi
+rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file
+rmdir conf$$.dir 2>/dev/null
+
+if mkdir -p . 2>/dev/null; then
+  as_mkdir_p='mkdir -p "$as_dir"'
+else
+  test -d ./-p && rmdir ./-p
+  as_mkdir_p=false
+fi
+
+as_test_x='test -x'
+as_executable_p=as_fn_executable_p
+
+# Sed expression to map a string onto a valid CPP name.
+as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'"
+
+# Sed expression to map a string onto a valid variable name.
+as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'"
+
+SHELL=${CONFIG_SHELL-/bin/sh}
+
+
+test -n "$DJDIR" || exec 7<&0 </dev/null
+exec 6>&1
+
+# Name of the host.
+# hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status,
+# so uname gets run too.
+ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q`
+
+#
+# Initializations.
+#
+ac_default_prefix=/usr/local
+ac_clean_files=
+ac_config_libobj_dir=.
+LIBOBJS=
+cross_compiling=no
+subdirs=
+MFLAGS=
+MAKEFLAGS=
+
+# Identity of this package.
+PACKAGE_NAME='sg3_utils'
+PACKAGE_TARNAME='sg3_utils'
+PACKAGE_VERSION='1.48'
+PACKAGE_STRING='sg3_utils 1.48'
+PACKAGE_BUGREPORT='dgilbert@interlog.com'
+PACKAGE_URL=''
+
+# Factoring default headers for most tests.
+ac_includes_default="\
+#include <stddef.h>
+#ifdef HAVE_STDIO_H
+# include <stdio.h>
+#endif
+#ifdef HAVE_STDLIB_H
+# include <stdlib.h>
+#endif
+#ifdef HAVE_STRING_H
+# include <string.h>
+#endif
+#ifdef HAVE_INTTYPES_H
+# include <inttypes.h>
+#endif
+#ifdef HAVE_STDINT_H
+# include <stdint.h>
+#endif
+#ifdef HAVE_STRINGS_H
+# include <strings.h>
+#endif
+#ifdef HAVE_SYS_TYPES_H
+# include <sys/types.h>
+#endif
+#ifdef HAVE_SYS_STAT_H
+# include <sys/stat.h>
+#endif
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif"
+
+ac_header_c_list=
+ac_subst_vars='am__EXEEXT_FALSE
+am__EXEEXT_TRUE
+LTLIBOBJS
+LIBOBJS
+PT_DUMMY_FALSE
+PT_DUMMY_TRUE
+DEBUG_FALSE
+DEBUG_TRUE
+OS_OTHER_FALSE
+OS_OTHER_TRUE
+OS_HAIKU_FALSE
+OS_HAIKU_TRUE
+OS_OPENBSD_FALSE
+OS_OPENBSD_TRUE
+OS_NETBSD_FALSE
+OS_NETBSD_TRUE
+OS_ANDROID_FALSE
+OS_ANDROID_TRUE
+OS_WIN32_CYGWIN_FALSE
+OS_WIN32_CYGWIN_TRUE
+OS_WIN32_MINGW_FALSE
+OS_WIN32_MINGW_TRUE
+OS_SOLARIS_FALSE
+OS_SOLARIS_TRUE
+OS_OSF_FALSE
+OS_OSF_TRUE
+OS_LINUX_FALSE
+OS_LINUX_TRUE
+OS_FREEBSD_FALSE
+OS_FREEBSD_TRUE
+os_libs
+os_cflags
+CPP
+GETOPT_O_FILES
+RT_LIB
+PTHREAD_LIB
+LT_SYS_LIBRARY_PATH
+OTOOL64
+OTOOL
+LIPO
+NMEDIT
+DSYMUTIL
+MANIFEST_TOOL
+RANLIB
+DLLTOOL
+OBJDUMP
+FILECMD
+LN_S
+NM
+ac_ct_DUMPBIN
+DUMPBIN
+LD
+FGREP
+EGREP
+GREP
+SED
+host_os
+host_vendor
+host_cpu
+host
+build_os
+build_vendor
+build_cpu
+build
+LIBTOOL
+ac_ct_AR
+AR
+am__fastdepCC_FALSE
+am__fastdepCC_TRUE
+CCDEPMODE
+am__nodep
+AMDEPBACKSLASH
+AMDEP_FALSE
+AMDEP_TRUE
+am__include
+DEPDIR
+OBJEXT
+EXEEXT
+ac_ct_CC
+CPPFLAGS
+LDFLAGS
+CFLAGS
+CC
+MAINT
+MAINTAINER_MODE_FALSE
+MAINTAINER_MODE_TRUE
+AM_BACKSLASH
+AM_DEFAULT_VERBOSITY
+AM_DEFAULT_V
+AM_V
+CSCOPE
+ETAGS
+CTAGS
+am__untar
+am__tar
+AMTAR
+am__leading_dot
+SET_MAKE
+AWK
+mkdir_p
+MKDIR_P
+INSTALL_STRIP_PROGRAM
+STRIP
+install_sh
+MAKEINFO
+AUTOHEADER
+AUTOMAKE
+AUTOCONF
+ACLOCAL
+VERSION
+PACKAGE
+CYGPATH_W
+am__isrc
+INSTALL_DATA
+INSTALL_SCRIPT
+INSTALL_PROGRAM
+target_alias
+host_alias
+build_alias
+LIBS
+ECHO_T
+ECHO_N
+ECHO_C
+DEFS
+mandir
+localedir
+libdir
+psdir
+pdfdir
+dvidir
+htmldir
+infodir
+docdir
+oldincludedir
+includedir
+runstatedir
+localstatedir
+sharedstatedir
+sysconfdir
+datadir
+datarootdir
+libexecdir
+sbindir
+bindir
+program_transform_name
+prefix
+exec_prefix
+PACKAGE_URL
+PACKAGE_BUGREPORT
+PACKAGE_STRING
+PACKAGE_VERSION
+PACKAGE_TARNAME
+PACKAGE_NAME
+PATH_SEPARATOR
+SHELL
+am__quote'
+ac_subst_files=''
+ac_user_opts='
+enable_option_checking
+enable_silent_rules
+enable_maintainer_mode
+enable_dependency_tracking
+enable_shared
+enable_static
+with_pic
+enable_fast_install
+with_aix_soname
+with_gnu_ld
+with_sysroot
+enable_libtool_lock
+enable_debug
+enable_pt_dummy
+enable_linuxbsg
+enable_win32_spt_direct
+enable_scsistrings
+enable_nvme_supp
+enable_fast_lebe
+enable_linux_sgv4
+'
+      ac_precious_vars='build_alias
+host_alias
+target_alias
+CC
+CFLAGS
+LDFLAGS
+LIBS
+CPPFLAGS
+LT_SYS_LIBRARY_PATH
+CPP'
+
+
+# Initialize some variables set by options.
+ac_init_help=
+ac_init_version=false
+ac_unrecognized_opts=
+ac_unrecognized_sep=
+# The variables have the same names as the options, with
+# dashes changed to underlines.
+cache_file=/dev/null
+exec_prefix=NONE
+no_create=
+no_recursion=
+prefix=NONE
+program_prefix=NONE
+program_suffix=NONE
+program_transform_name=s,x,x,
+silent=
+site=
+srcdir=
+verbose=
+x_includes=NONE
+x_libraries=NONE
+
+# Installation directory options.
+# These are left unexpanded so users can "make install exec_prefix=/foo"
+# and all the variables that are supposed to be based on exec_prefix
+# by default will actually change.
+# Use braces instead of parens because sh, perl, etc. also accept them.
+# (The list follows the same order as the GNU Coding Standards.)
+bindir='${exec_prefix}/bin'
+sbindir='${exec_prefix}/sbin'
+libexecdir='${exec_prefix}/libexec'
+datarootdir='${prefix}/share'
+datadir='${datarootdir}'
+sysconfdir='${prefix}/etc'
+sharedstatedir='${prefix}/com'
+localstatedir='${prefix}/var'
+runstatedir='${localstatedir}/run'
+includedir='${prefix}/include'
+oldincludedir='/usr/include'
+docdir='${datarootdir}/doc/${PACKAGE_TARNAME}'
+infodir='${datarootdir}/info'
+htmldir='${docdir}'
+dvidir='${docdir}'
+pdfdir='${docdir}'
+psdir='${docdir}'
+libdir='${exec_prefix}/lib'
+localedir='${datarootdir}/locale'
+mandir='${datarootdir}/man'
+
+ac_prev=
+ac_dashdash=
+for ac_option
+do
+  # If the previous option needs an argument, assign it.
+  if test -n "$ac_prev"; then
+    eval $ac_prev=\$ac_option
+    ac_prev=
+    continue
+  fi
+
+  case $ac_option in
+  *=?*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;;
+  *=)   ac_optarg= ;;
+  *)    ac_optarg=yes ;;
+  esac
+
+  case $ac_dashdash$ac_option in
+  --)
+    ac_dashdash=yes ;;
+
+  -bindir | --bindir | --bindi | --bind | --bin | --bi)
+    ac_prev=bindir ;;
+  -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*)
+    bindir=$ac_optarg ;;
+
+  -build | --build | --buil | --bui | --bu)
+    ac_prev=build_alias ;;
+  -build=* | --build=* | --buil=* | --bui=* | --bu=*)
+    build_alias=$ac_optarg ;;
+
+  -cache-file | --cache-file | --cache-fil | --cache-fi \
+  | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c)
+    ac_prev=cache_file ;;
+  -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \
+  | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*)
+    cache_file=$ac_optarg ;;
+
+  --config-cache | -C)
+    cache_file=config.cache ;;
+
+  -datadir | --datadir | --datadi | --datad)
+    ac_prev=datadir ;;
+  -datadir=* | --datadir=* | --datadi=* | --datad=*)
+    datadir=$ac_optarg ;;
+
+  -datarootdir | --datarootdir | --datarootdi | --datarootd | --dataroot \
+  | --dataroo | --dataro | --datar)
+    ac_prev=datarootdir ;;
+  -datarootdir=* | --datarootdir=* | --datarootdi=* | --datarootd=* \
+  | --dataroot=* | --dataroo=* | --dataro=* | --datar=*)
+    datarootdir=$ac_optarg ;;
+
+  -disable-* | --disable-*)
+    ac_useropt=`expr "x$ac_option" : 'x-*disable-\(.*\)'`
+    # Reject names that are not valid shell variable names.
+    expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null &&
+      as_fn_error $? "invalid feature name: \`$ac_useropt'"
+    ac_useropt_orig=$ac_useropt
+    ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'`
+    case $ac_user_opts in
+      *"
+"enable_$ac_useropt"
+"*) ;;
+      *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--disable-$ac_useropt_orig"
+	 ac_unrecognized_sep=', ';;
+    esac
+    eval enable_$ac_useropt=no ;;
+
+  -docdir | --docdir | --docdi | --doc | --do)
+    ac_prev=docdir ;;
+  -docdir=* | --docdir=* | --docdi=* | --doc=* | --do=*)
+    docdir=$ac_optarg ;;
+
+  -dvidir | --dvidir | --dvidi | --dvid | --dvi | --dv)
+    ac_prev=dvidir ;;
+  -dvidir=* | --dvidir=* | --dvidi=* | --dvid=* | --dvi=* | --dv=*)
+    dvidir=$ac_optarg ;;
+
+  -enable-* | --enable-*)
+    ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'`
+    # Reject names that are not valid shell variable names.
+    expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null &&
+      as_fn_error $? "invalid feature name: \`$ac_useropt'"
+    ac_useropt_orig=$ac_useropt
+    ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'`
+    case $ac_user_opts in
+      *"
+"enable_$ac_useropt"
+"*) ;;
+      *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--enable-$ac_useropt_orig"
+	 ac_unrecognized_sep=', ';;
+    esac
+    eval enable_$ac_useropt=\$ac_optarg ;;
+
+  -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \
+  | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \
+  | --exec | --exe | --ex)
+    ac_prev=exec_prefix ;;
+  -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \
+  | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \
+  | --exec=* | --exe=* | --ex=*)
+    exec_prefix=$ac_optarg ;;
+
+  -gas | --gas | --ga | --g)
+    # Obsolete; use --with-gas.
+    with_gas=yes ;;
+
+  -help | --help | --hel | --he | -h)
+    ac_init_help=long ;;
+  -help=r* | --help=r* | --hel=r* | --he=r* | -hr*)
+    ac_init_help=recursive ;;
+  -help=s* | --help=s* | --hel=s* | --he=s* | -hs*)
+    ac_init_help=short ;;
+
+  -host | --host | --hos | --ho)
+    ac_prev=host_alias ;;
+  -host=* | --host=* | --hos=* | --ho=*)
+    host_alias=$ac_optarg ;;
+
+  -htmldir | --htmldir | --htmldi | --htmld | --html | --htm | --ht)
+    ac_prev=htmldir ;;
+  -htmldir=* | --htmldir=* | --htmldi=* | --htmld=* | --html=* | --htm=* \
+  | --ht=*)
+    htmldir=$ac_optarg ;;
+
+  -includedir | --includedir | --includedi | --included | --include \
+  | --includ | --inclu | --incl | --inc)
+    ac_prev=includedir ;;
+  -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \
+  | --includ=* | --inclu=* | --incl=* | --inc=*)
+    includedir=$ac_optarg ;;
+
+  -infodir | --infodir | --infodi | --infod | --info | --inf)
+    ac_prev=infodir ;;
+  -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*)
+    infodir=$ac_optarg ;;
+
+  -libdir | --libdir | --libdi | --libd)
+    ac_prev=libdir ;;
+  -libdir=* | --libdir=* | --libdi=* | --libd=*)
+    libdir=$ac_optarg ;;
+
+  -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \
+  | --libexe | --libex | --libe)
+    ac_prev=libexecdir ;;
+  -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \
+  | --libexe=* | --libex=* | --libe=*)
+    libexecdir=$ac_optarg ;;
+
+  -localedir | --localedir | --localedi | --localed | --locale)
+    ac_prev=localedir ;;
+  -localedir=* | --localedir=* | --localedi=* | --localed=* | --locale=*)
+    localedir=$ac_optarg ;;
+
+  -localstatedir | --localstatedir | --localstatedi | --localstated \
+  | --localstate | --localstat | --localsta | --localst | --locals)
+    ac_prev=localstatedir ;;
+  -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \
+  | --localstate=* | --localstat=* | --localsta=* | --localst=* | --locals=*)
+    localstatedir=$ac_optarg ;;
+
+  -mandir | --mandir | --mandi | --mand | --man | --ma | --m)
+    ac_prev=mandir ;;
+  -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*)
+    mandir=$ac_optarg ;;
+
+  -nfp | --nfp | --nf)
+    # Obsolete; use --without-fp.
+    with_fp=no ;;
+
+  -no-create | --no-create | --no-creat | --no-crea | --no-cre \
+  | --no-cr | --no-c | -n)
+    no_create=yes ;;
+
+  -no-recursion | --no-recursion | --no-recursio | --no-recursi \
+  | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r)
+    no_recursion=yes ;;
+
+  -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \
+  | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \
+  | --oldin | --oldi | --old | --ol | --o)
+    ac_prev=oldincludedir ;;
+  -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \
+  | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \
+  | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*)
+    oldincludedir=$ac_optarg ;;
+
+  -prefix | --prefix | --prefi | --pref | --pre | --pr | --p)
+    ac_prev=prefix ;;
+  -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*)
+    prefix=$ac_optarg ;;
+
+  -program-prefix | --program-prefix | --program-prefi | --program-pref \
+  | --program-pre | --program-pr | --program-p)
+    ac_prev=program_prefix ;;
+  -program-prefix=* | --program-prefix=* | --program-prefi=* \
+  | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*)
+    program_prefix=$ac_optarg ;;
+
+  -program-suffix | --program-suffix | --program-suffi | --program-suff \
+  | --program-suf | --program-su | --program-s)
+    ac_prev=program_suffix ;;
+  -program-suffix=* | --program-suffix=* | --program-suffi=* \
+  | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*)
+    program_suffix=$ac_optarg ;;
+
+  -program-transform-name | --program-transform-name \
+  | --program-transform-nam | --program-transform-na \
+  | --program-transform-n | --program-transform- \
+  | --program-transform | --program-transfor \
+  | --program-transfo | --program-transf \
+  | --program-trans | --program-tran \
+  | --progr-tra | --program-tr | --program-t)
+    ac_prev=program_transform_name ;;
+  -program-transform-name=* | --program-transform-name=* \
+  | --program-transform-nam=* | --program-transform-na=* \
+  | --program-transform-n=* | --program-transform-=* \
+  | --program-transform=* | --program-transfor=* \
+  | --program-transfo=* | --program-transf=* \
+  | --program-trans=* | --program-tran=* \
+  | --progr-tra=* | --program-tr=* | --program-t=*)
+    program_transform_name=$ac_optarg ;;
+
+  -pdfdir | --pdfdir | --pdfdi | --pdfd | --pdf | --pd)
+    ac_prev=pdfdir ;;
+  -pdfdir=* | --pdfdir=* | --pdfdi=* | --pdfd=* | --pdf=* | --pd=*)
+    pdfdir=$ac_optarg ;;
+
+  -psdir | --psdir | --psdi | --psd | --ps)
+    ac_prev=psdir ;;
+  -psdir=* | --psdir=* | --psdi=* | --psd=* | --ps=*)
+    psdir=$ac_optarg ;;
+
+  -q | -quiet | --quiet | --quie | --qui | --qu | --q \
+  | -silent | --silent | --silen | --sile | --sil)
+    silent=yes ;;
+
+  -runstatedir | --runstatedir | --runstatedi | --runstated \
+  | --runstate | --runstat | --runsta | --runst | --runs \
+  | --run | --ru | --r)
+    ac_prev=runstatedir ;;
+  -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \
+  | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \
+  | --run=* | --ru=* | --r=*)
+    runstatedir=$ac_optarg ;;
+
+  -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb)
+    ac_prev=sbindir ;;
+  -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \
+  | --sbi=* | --sb=*)
+    sbindir=$ac_optarg ;;
+
+  -sharedstatedir | --sharedstatedir | --sharedstatedi \
+  | --sharedstated | --sharedstate | --sharedstat | --sharedsta \
+  | --sharedst | --shareds | --shared | --share | --shar \
+  | --sha | --sh)
+    ac_prev=sharedstatedir ;;
+  -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \
+  | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \
+  | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \
+  | --sha=* | --sh=*)
+    sharedstatedir=$ac_optarg ;;
+
+  -site | --site | --sit)
+    ac_prev=site ;;
+  -site=* | --site=* | --sit=*)
+    site=$ac_optarg ;;
+
+  -srcdir | --srcdir | --srcdi | --srcd | --src | --sr)
+    ac_prev=srcdir ;;
+  -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*)
+    srcdir=$ac_optarg ;;
+
+  -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \
+  | --syscon | --sysco | --sysc | --sys | --sy)
+    ac_prev=sysconfdir ;;
+  -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \
+  | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*)
+    sysconfdir=$ac_optarg ;;
+
+  -target | --target | --targe | --targ | --tar | --ta | --t)
+    ac_prev=target_alias ;;
+  -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*)
+    target_alias=$ac_optarg ;;
+
+  -v | -verbose | --verbose | --verbos | --verbo | --verb)
+    verbose=yes ;;
+
+  -version | --version | --versio | --versi | --vers | -V)
+    ac_init_version=: ;;
+
+  -with-* | --with-*)
+    ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'`
+    # Reject names that are not valid shell variable names.
+    expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null &&
+      as_fn_error $? "invalid package name: \`$ac_useropt'"
+    ac_useropt_orig=$ac_useropt
+    ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'`
+    case $ac_user_opts in
+      *"
+"with_$ac_useropt"
+"*) ;;
+      *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--with-$ac_useropt_orig"
+	 ac_unrecognized_sep=', ';;
+    esac
+    eval with_$ac_useropt=\$ac_optarg ;;
+
+  -without-* | --without-*)
+    ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'`
+    # Reject names that are not valid shell variable names.
+    expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null &&
+      as_fn_error $? "invalid package name: \`$ac_useropt'"
+    ac_useropt_orig=$ac_useropt
+    ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'`
+    case $ac_user_opts in
+      *"
+"with_$ac_useropt"
+"*) ;;
+      *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--without-$ac_useropt_orig"
+	 ac_unrecognized_sep=', ';;
+    esac
+    eval with_$ac_useropt=no ;;
+
+  --x)
+    # Obsolete; use --with-x.
+    with_x=yes ;;
+
+  -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \
+  | --x-incl | --x-inc | --x-in | --x-i)
+    ac_prev=x_includes ;;
+  -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \
+  | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*)
+    x_includes=$ac_optarg ;;
+
+  -x-libraries | --x-libraries | --x-librarie | --x-librari \
+  | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l)
+    ac_prev=x_libraries ;;
+  -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \
+  | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*)
+    x_libraries=$ac_optarg ;;
+
+  -*) as_fn_error $? "unrecognized option: \`$ac_option'
+Try \`$0 --help' for more information"
+    ;;
+
+  *=*)
+    ac_envvar=`expr "x$ac_option" : 'x\([^=]*\)='`
+    # Reject names that are not valid shell variable names.
+    case $ac_envvar in #(
+      '' | [0-9]* | *[!_$as_cr_alnum]* )
+      as_fn_error $? "invalid variable name: \`$ac_envvar'" ;;
+    esac
+    eval $ac_envvar=\$ac_optarg
+    export $ac_envvar ;;
+
+  *)
+    # FIXME: should be removed in autoconf 3.0.
+    printf "%s\n" "$as_me: WARNING: you should use --build, --host, --target" >&2
+    expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null &&
+      printf "%s\n" "$as_me: WARNING: invalid host type: $ac_option" >&2
+    : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}"
+    ;;
+
+  esac
+done
+
+if test -n "$ac_prev"; then
+  ac_option=--`echo $ac_prev | sed 's/_/-/g'`
+  as_fn_error $? "missing argument to $ac_option"
+fi
+
+if test -n "$ac_unrecognized_opts"; then
+  case $enable_option_checking in
+    no) ;;
+    fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;;
+    *)     printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;;
+  esac
+fi
+
+# Check all directory arguments for consistency.
+for ac_var in	exec_prefix prefix bindir sbindir libexecdir datarootdir \
+		datadir sysconfdir sharedstatedir localstatedir includedir \
+		oldincludedir docdir infodir htmldir dvidir pdfdir psdir \
+		libdir localedir mandir runstatedir
+do
+  eval ac_val=\$$ac_var
+  # Remove trailing slashes.
+  case $ac_val in
+    */ )
+      ac_val=`expr "X$ac_val" : 'X\(.*[^/]\)' \| "X$ac_val" : 'X\(.*\)'`
+      eval $ac_var=\$ac_val;;
+  esac
+  # Be sure to have absolute directory names.
+  case $ac_val in
+    [\\/$]* | ?:[\\/]* )  continue;;
+    NONE | '' ) case $ac_var in *prefix ) continue;; esac;;
+  esac
+  as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val"
+done
+
+# There might be people who depend on the old broken behavior: `$host'
+# used to hold the argument of --host etc.
+# FIXME: To remove some day.
+build=$build_alias
+host=$host_alias
+target=$target_alias
+
+# FIXME: To remove some day.
+if test "x$host_alias" != x; then
+  if test "x$build_alias" = x; then
+    cross_compiling=maybe
+  elif test "x$build_alias" != "x$host_alias"; then
+    cross_compiling=yes
+  fi
+fi
+
+ac_tool_prefix=
+test -n "$host_alias" && ac_tool_prefix=$host_alias-
+
+test "$silent" = yes && exec 6>/dev/null
+
+
+ac_pwd=`pwd` && test -n "$ac_pwd" &&
+ac_ls_di=`ls -di .` &&
+ac_pwd_ls_di=`cd "$ac_pwd" && ls -di .` ||
+  as_fn_error $? "working directory cannot be determined"
+test "X$ac_ls_di" = "X$ac_pwd_ls_di" ||
+  as_fn_error $? "pwd does not report name of working directory"
+
+
+# Find the source files, if location was not specified.
+if test -z "$srcdir"; then
+  ac_srcdir_defaulted=yes
+  # Try the directory containing this script, then the parent directory.
+  ac_confdir=`$as_dirname -- "$as_myself" ||
+$as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+	 X"$as_myself" : 'X\(//\)[^/]' \| \
+	 X"$as_myself" : 'X\(//\)$' \| \
+	 X"$as_myself" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$as_myself" |
+    sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)[^/].*/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`
+  srcdir=$ac_confdir
+  if test ! -r "$srcdir/$ac_unique_file"; then
+    srcdir=..
+  fi
+else
+  ac_srcdir_defaulted=no
+fi
+if test ! -r "$srcdir/$ac_unique_file"; then
+  test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .."
+  as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir"
+fi
+ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work"
+ac_abs_confdir=`(
+	cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg"
+	pwd)`
+# When building in place, set srcdir=.
+if test "$ac_abs_confdir" = "$ac_pwd"; then
+  srcdir=.
+fi
+# Remove unnecessary trailing slashes from srcdir.
+# Double slashes in file names in object file debugging info
+# mess up M-x gdb in Emacs.
+case $srcdir in
+*/) srcdir=`expr "X$srcdir" : 'X\(.*[^/]\)' \| "X$srcdir" : 'X\(.*\)'`;;
+esac
+for ac_var in $ac_precious_vars; do
+  eval ac_env_${ac_var}_set=\${${ac_var}+set}
+  eval ac_env_${ac_var}_value=\$${ac_var}
+  eval ac_cv_env_${ac_var}_set=\${${ac_var}+set}
+  eval ac_cv_env_${ac_var}_value=\$${ac_var}
+done
+
+#
+# Report the --help message.
+#
+if test "$ac_init_help" = "long"; then
+  # Omit some internal or obsolete options to make the list less imposing.
+  # This message is too long to be a string in the A/UX 3.1 sh.
+  cat <<_ACEOF
+\`configure' configures sg3_utils 1.48 to adapt to many kinds of systems.
+
+Usage: $0 [OPTION]... [VAR=VALUE]...
+
+To assign environment variables (e.g., CC, CFLAGS...), specify them as
+VAR=VALUE.  See below for descriptions of some of the useful variables.
+
+Defaults for the options are specified in brackets.
+
+Configuration:
+  -h, --help              display this help and exit
+      --help=short        display options specific to this package
+      --help=recursive    display the short help of all the included packages
+  -V, --version           display version information and exit
+  -q, --quiet, --silent   do not print \`checking ...' messages
+      --cache-file=FILE   cache test results in FILE [disabled]
+  -C, --config-cache      alias for \`--cache-file=config.cache'
+  -n, --no-create         do not create output files
+      --srcdir=DIR        find the sources in DIR [configure dir or \`..']
+
+Installation directories:
+  --prefix=PREFIX         install architecture-independent files in PREFIX
+                          [$ac_default_prefix]
+  --exec-prefix=EPREFIX   install architecture-dependent files in EPREFIX
+                          [PREFIX]
+
+By default, \`make install' will install all the files in
+\`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc.  You can specify
+an installation prefix other than \`$ac_default_prefix' using \`--prefix',
+for instance \`--prefix=\$HOME'.
+
+For better control, use the options below.
+
+Fine tuning of the installation directories:
+  --bindir=DIR            user executables [EPREFIX/bin]
+  --sbindir=DIR           system admin executables [EPREFIX/sbin]
+  --libexecdir=DIR        program executables [EPREFIX/libexec]
+  --sysconfdir=DIR        read-only single-machine data [PREFIX/etc]
+  --sharedstatedir=DIR    modifiable architecture-independent data [PREFIX/com]
+  --localstatedir=DIR     modifiable single-machine data [PREFIX/var]
+  --runstatedir=DIR       modifiable per-process data [LOCALSTATEDIR/run]
+  --libdir=DIR            object code libraries [EPREFIX/lib]
+  --includedir=DIR        C header files [PREFIX/include]
+  --oldincludedir=DIR     C header files for non-gcc [/usr/include]
+  --datarootdir=DIR       read-only arch.-independent data root [PREFIX/share]
+  --datadir=DIR           read-only architecture-independent data [DATAROOTDIR]
+  --infodir=DIR           info documentation [DATAROOTDIR/info]
+  --localedir=DIR         locale-dependent data [DATAROOTDIR/locale]
+  --mandir=DIR            man documentation [DATAROOTDIR/man]
+  --docdir=DIR            documentation root [DATAROOTDIR/doc/sg3_utils]
+  --htmldir=DIR           html documentation [DOCDIR]
+  --dvidir=DIR            dvi documentation [DOCDIR]
+  --pdfdir=DIR            pdf documentation [DOCDIR]
+  --psdir=DIR             ps documentation [DOCDIR]
+_ACEOF
+
+  cat <<\_ACEOF
+
+Program names:
+  --program-prefix=PREFIX            prepend PREFIX to installed program names
+  --program-suffix=SUFFIX            append SUFFIX to installed program names
+  --program-transform-name=PROGRAM   run sed PROGRAM on installed program names
+
+System types:
+  --build=BUILD     configure for building on BUILD [guessed]
+  --host=HOST       cross-compile to build programs to run on HOST [BUILD]
+_ACEOF
+fi
+
+if test -n "$ac_init_help"; then
+  case $ac_init_help in
+     short | recursive ) echo "Configuration of sg3_utils 1.48:";;
+   esac
+  cat <<\_ACEOF
+
+Optional Features:
+  --disable-option-checking  ignore unrecognized --enable/--with options
+  --disable-FEATURE       do not include FEATURE (same as --enable-FEATURE=no)
+  --enable-FEATURE[=ARG]  include FEATURE [ARG=yes]
+  --enable-silent-rules   less verbose build output (undo: "make V=1")
+  --disable-silent-rules  verbose build output (undo: "make V=0")
+  --enable-maintainer-mode
+                          enable make rules and dependencies not useful (and
+                          sometimes confusing) to the casual installer
+  --enable-dependency-tracking
+                          do not reject slow dependency extractors
+  --disable-dependency-tracking
+                          speeds up one-time build
+  --enable-shared[=PKGS]  build shared libraries [default=yes]
+  --enable-static[=PKGS]  build static libraries [default=yes]
+  --enable-fast-install[=PKGS]
+                          optimize for fast installation [default=yes]
+  --disable-libtool-lock  avoid locking (might break parallel builds)
+  --enable-debug          Turn on debugging
+  --enable-pt_dummy       pass-through codes compiles, does nothing
+  --disable-linuxbsg      option ignored, this is placeholder
+  --enable-win32-spt-direct
+                          enable Win32 SPT Direct
+  --disable-scsistrings   Disable full SCSI sense strings and NVMe status
+                          strings
+  --disable-nvme-supp     remove all or most NVMe code
+  --disable-fast-lebe     use generic little-endian/big-endian code instead
+  --disable-linux-sgv4    for Linux sg driver avoid v4 interface even if
+                          available
+
+Optional Packages:
+  --with-PACKAGE[=ARG]    use PACKAGE [ARG=yes]
+  --without-PACKAGE       do not use PACKAGE (same as --with-PACKAGE=no)
+  --with-pic[=PKGS]       try to use only PIC/non-PIC objects [default=use
+                          both]
+  --with-aix-soname=aix|svr4|both
+                          shared library versioning (aka "SONAME") variant to
+                          provide on AIX, [default=aix].
+  --with-gnu-ld           assume the C compiler uses GNU ld [default=no]
+  --with-sysroot[=DIR]    Search for dependent libraries within DIR (or the
+                          compiler's sysroot if not specified).
+
+Some influential environment variables:
+  CC          C compiler command
+  CFLAGS      C compiler flags
+  LDFLAGS     linker flags, e.g. -L<lib dir> if you have libraries in a
+              nonstandard directory <lib dir>
+  LIBS        libraries to pass to the linker, e.g. -l<library>
+  CPPFLAGS    (Objective) C/C++ preprocessor flags, e.g. -I<include dir> if
+              you have headers in a nonstandard directory <include dir>
+  LT_SYS_LIBRARY_PATH
+              User-defined run-time library search path.
+  CPP         C preprocessor
+
+Use these variables to override the choices made by `configure' or to help
+it to find libraries and programs with nonstandard names/locations.
+
+Report bugs to <dgilbert@interlog.com>.
+_ACEOF
+ac_status=$?
+fi
+
+if test "$ac_init_help" = "recursive"; then
+  # If there are subdirs, report their specific --help.
+  for ac_dir in : $ac_subdirs_all; do test "x$ac_dir" = x: && continue
+    test -d "$ac_dir" ||
+      { cd "$srcdir" && ac_pwd=`pwd` && srcdir=. && test -d "$ac_dir"; } ||
+      continue
+    ac_builddir=.
+
+case "$ac_dir" in
+.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;;
+*)
+  ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'`
+  # A ".." for each directory in $ac_dir_suffix.
+  ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'`
+  case $ac_top_builddir_sub in
+  "") ac_top_builddir_sub=. ac_top_build_prefix= ;;
+  *)  ac_top_build_prefix=$ac_top_builddir_sub/ ;;
+  esac ;;
+esac
+ac_abs_top_builddir=$ac_pwd
+ac_abs_builddir=$ac_pwd$ac_dir_suffix
+# for backward compatibility:
+ac_top_builddir=$ac_top_build_prefix
+
+case $srcdir in
+  .)  # We are building in place.
+    ac_srcdir=.
+    ac_top_srcdir=$ac_top_builddir_sub
+    ac_abs_top_srcdir=$ac_pwd ;;
+  [\\/]* | ?:[\\/]* )  # Absolute name.
+    ac_srcdir=$srcdir$ac_dir_suffix;
+    ac_top_srcdir=$srcdir
+    ac_abs_top_srcdir=$srcdir ;;
+  *) # Relative name.
+    ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix
+    ac_top_srcdir=$ac_top_build_prefix$srcdir
+    ac_abs_top_srcdir=$ac_pwd/$srcdir ;;
+esac
+ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix
+
+    cd "$ac_dir" || { ac_status=$?; continue; }
+    # Check for configure.gnu first; this name is used for a wrapper for
+    # Metaconfig's "Configure" on case-insensitive file systems.
+    if test -f "$ac_srcdir/configure.gnu"; then
+      echo &&
+      $SHELL "$ac_srcdir/configure.gnu" --help=recursive
+    elif test -f "$ac_srcdir/configure"; then
+      echo &&
+      $SHELL "$ac_srcdir/configure" --help=recursive
+    else
+      printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2
+    fi || ac_status=$?
+    cd "$ac_pwd" || { ac_status=$?; break; }
+  done
+fi
+
+test -n "$ac_init_help" && exit $ac_status
+if $ac_init_version; then
+  cat <<\_ACEOF
+sg3_utils configure 1.48
+generated by GNU Autoconf 2.71
+
+Copyright (C) 2021 Free Software Foundation, Inc.
+This configure script is free software; the Free Software Foundation
+gives unlimited permission to copy, distribute and modify it.
+_ACEOF
+  exit
+fi
+
+## ------------------------ ##
+## Autoconf initialization. ##
+## ------------------------ ##
+
+# ac_fn_c_try_compile LINENO
+# --------------------------
+# Try to compile conftest.$ac_ext, and return whether this succeeded.
+ac_fn_c_try_compile ()
+{
+  as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+  rm -f conftest.$ac_objext conftest.beam
+  if { { ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_compile") 2>conftest.err
+  ac_status=$?
+  if test -s conftest.err; then
+    grep -v '^ *+' conftest.err >conftest.er1
+    cat conftest.er1 >&5
+    mv -f conftest.er1 conftest.err
+  fi
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest.$ac_objext
+then :
+  ac_retval=0
+else $as_nop
+  printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_retval=1
+fi
+  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+  as_fn_set_status $ac_retval
+
+} # ac_fn_c_try_compile
+
+# ac_fn_c_try_link LINENO
+# -----------------------
+# Try to link conftest.$ac_ext, and return whether this succeeded.
+ac_fn_c_try_link ()
+{
+  as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+  rm -f conftest.$ac_objext conftest.beam conftest$ac_exeext
+  if { { ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_link") 2>conftest.err
+  ac_status=$?
+  if test -s conftest.err; then
+    grep -v '^ *+' conftest.err >conftest.er1
+    cat conftest.er1 >&5
+    mv -f conftest.er1 conftest.err
+  fi
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; } && {
+	 test -z "$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       } && test -s conftest$ac_exeext && {
+	 test "$cross_compiling" = yes ||
+	 test -x conftest$ac_exeext
+       }
+then :
+  ac_retval=0
+else $as_nop
+  printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+	ac_retval=1
+fi
+  # Delete the IPA/IPO (Inter Procedural Analysis/Optimization) information
+  # created by the PGI compiler (conftest_ipa8_conftest.oo), as it would
+  # interfere with the next link command; also delete a directory that is
+  # left behind by Apple's compiler.  We do this before executing the actions.
+  rm -rf conftest.dSYM conftest_ipa8_conftest.oo
+  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+  as_fn_set_status $ac_retval
+
+} # ac_fn_c_try_link
+
+# ac_fn_c_check_header_compile LINENO HEADER VAR INCLUDES
+# -------------------------------------------------------
+# Tests whether HEADER exists and can be compiled using the include files in
+# INCLUDES, setting the cache variable VAR accordingly.
+ac_fn_c_check_header_compile ()
+{
+  as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5
+printf %s "checking for $2... " >&6; }
+if eval test \${$3+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+$4
+#include <$2>
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+  eval "$3=yes"
+else $as_nop
+  eval "$3=no"
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+eval ac_res=\$$3
+	       { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
+printf "%s\n" "$ac_res" >&6; }
+  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+
+} # ac_fn_c_check_header_compile
+
+# ac_fn_c_check_func LINENO FUNC VAR
+# ----------------------------------
+# Tests whether FUNC exists, setting the cache variable VAR accordingly
+ac_fn_c_check_func ()
+{
+  as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5
+printf %s "checking for $2... " >&6; }
+if eval test \${$3+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+/* Define $2 to an innocuous variant, in case <limits.h> declares $2.
+   For example, HP-UX 11i <limits.h> declares gettimeofday.  */
+#define $2 innocuous_$2
+
+/* System header to define __stub macros and hopefully few prototypes,
+   which can conflict with char $2 (); below.  */
+
+#include <limits.h>
+#undef $2
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char $2 ();
+/* The GNU C library defines this for functions which it implements
+    to always fail with ENOSYS.  Some functions are actually named
+    something starting with __ and the normal name is an alias.  */
+#if defined __stub_$2 || defined __stub___$2
+choke me
+#endif
+
+int
+main (void)
+{
+return $2 ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  eval "$3=yes"
+else $as_nop
+  eval "$3=no"
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+fi
+eval ac_res=\$$3
+	       { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
+printf "%s\n" "$ac_res" >&6; }
+  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+
+} # ac_fn_c_check_func
+
+# ac_fn_c_try_cpp LINENO
+# ----------------------
+# Try to preprocess conftest.$ac_ext, and return whether this succeeded.
+ac_fn_c_try_cpp ()
+{
+  as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+  if { { ac_try="$ac_cpp conftest.$ac_ext"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_cpp conftest.$ac_ext") 2>conftest.err
+  ac_status=$?
+  if test -s conftest.err; then
+    grep -v '^ *+' conftest.err >conftest.er1
+    cat conftest.er1 >&5
+    mv -f conftest.er1 conftest.err
+  fi
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; } > conftest.i && {
+	 test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" ||
+	 test ! -s conftest.err
+       }
+then :
+  ac_retval=0
+else $as_nop
+  printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+    ac_retval=1
+fi
+  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+  as_fn_set_status $ac_retval
+
+} # ac_fn_c_try_cpp
+ac_configure_args_raw=
+for ac_arg
+do
+  case $ac_arg in
+  *\'*)
+    ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;;
+  esac
+  as_fn_append ac_configure_args_raw " '$ac_arg'"
+done
+
+case $ac_configure_args_raw in
+  *$as_nl*)
+    ac_safe_unquote= ;;
+  *)
+    ac_unsafe_z='|&;<>()$`\\"*?[ ''	' # This string ends in space, tab.
+    ac_unsafe_a="$ac_unsafe_z#~"
+    ac_safe_unquote="s/ '\\([^$ac_unsafe_a][^$ac_unsafe_z]*\\)'/ \\1/g"
+    ac_configure_args_raw=`      printf "%s\n" "$ac_configure_args_raw" | sed "$ac_safe_unquote"`;;
+esac
+
+cat >config.log <<_ACEOF
+This file contains any messages produced by compilers while
+running configure, to aid debugging if configure makes a mistake.
+
+It was created by sg3_utils $as_me 1.48, which was
+generated by GNU Autoconf 2.71.  Invocation command line was
+
+  $ $0$ac_configure_args_raw
+
+_ACEOF
+exec 5>>config.log
+{
+cat <<_ASUNAME
+## --------- ##
+## Platform. ##
+## --------- ##
+
+hostname = `(hostname || uname -n) 2>/dev/null | sed 1q`
+uname -m = `(uname -m) 2>/dev/null || echo unknown`
+uname -r = `(uname -r) 2>/dev/null || echo unknown`
+uname -s = `(uname -s) 2>/dev/null || echo unknown`
+uname -v = `(uname -v) 2>/dev/null || echo unknown`
+
+/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null || echo unknown`
+/bin/uname -X     = `(/bin/uname -X) 2>/dev/null     || echo unknown`
+
+/bin/arch              = `(/bin/arch) 2>/dev/null              || echo unknown`
+/usr/bin/arch -k       = `(/usr/bin/arch -k) 2>/dev/null       || echo unknown`
+/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null || echo unknown`
+/usr/bin/hostinfo      = `(/usr/bin/hostinfo) 2>/dev/null      || echo unknown`
+/bin/machine           = `(/bin/machine) 2>/dev/null           || echo unknown`
+/usr/bin/oslevel       = `(/usr/bin/oslevel) 2>/dev/null       || echo unknown`
+/bin/universe          = `(/bin/universe) 2>/dev/null          || echo unknown`
+
+_ASUNAME
+
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    printf "%s\n" "PATH: $as_dir"
+  done
+IFS=$as_save_IFS
+
+} >&5
+
+cat >&5 <<_ACEOF
+
+
+## ----------- ##
+## Core tests. ##
+## ----------- ##
+
+_ACEOF
+
+
+# Keep a trace of the command line.
+# Strip out --no-create and --no-recursion so they do not pile up.
+# Strip out --silent because we don't want to record it for future runs.
+# Also quote any args containing shell meta-characters.
+# Make two passes to allow for proper duplicate-argument suppression.
+ac_configure_args=
+ac_configure_args0=
+ac_configure_args1=
+ac_must_keep_next=false
+for ac_pass in 1 2
+do
+  for ac_arg
+  do
+    case $ac_arg in
+    -no-create | --no-c* | -n | -no-recursion | --no-r*) continue ;;
+    -q | -quiet | --quiet | --quie | --qui | --qu | --q \
+    | -silent | --silent | --silen | --sile | --sil)
+      continue ;;
+    *\'*)
+      ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;;
+    esac
+    case $ac_pass in
+    1) as_fn_append ac_configure_args0 " '$ac_arg'" ;;
+    2)
+      as_fn_append ac_configure_args1 " '$ac_arg'"
+      if test $ac_must_keep_next = true; then
+	ac_must_keep_next=false # Got value, back to normal.
+      else
+	case $ac_arg in
+	  *=* | --config-cache | -C | -disable-* | --disable-* \
+	  | -enable-* | --enable-* | -gas | --g* | -nfp | --nf* \
+	  | -q | -quiet | --q* | -silent | --sil* | -v | -verb* \
+	  | -with-* | --with-* | -without-* | --without-* | --x)
+	    case "$ac_configure_args0 " in
+	      "$ac_configure_args1"*" '$ac_arg' "* ) continue ;;
+	    esac
+	    ;;
+	  -* ) ac_must_keep_next=true ;;
+	esac
+      fi
+      as_fn_append ac_configure_args " '$ac_arg'"
+      ;;
+    esac
+  done
+done
+{ ac_configure_args0=; unset ac_configure_args0;}
+{ ac_configure_args1=; unset ac_configure_args1;}
+
+# When interrupted or exit'd, cleanup temporary files, and complete
+# config.log.  We remove comments because anyway the quotes in there
+# would cause problems or look ugly.
+# WARNING: Use '\'' to represent an apostrophe within the trap.
+# WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug.
+trap 'exit_status=$?
+  # Sanitize IFS.
+  IFS=" ""	$as_nl"
+  # Save into config.log some information that might help in debugging.
+  {
+    echo
+
+    printf "%s\n" "## ---------------- ##
+## Cache variables. ##
+## ---------------- ##"
+    echo
+    # The following way of writing the cache mishandles newlines in values,
+(
+  for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do
+    eval ac_val=\$$ac_var
+    case $ac_val in #(
+    *${as_nl}*)
+      case $ac_var in #(
+      *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5
+printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;;
+      esac
+      case $ac_var in #(
+      _ | IFS | as_nl) ;; #(
+      BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #(
+      *) { eval $ac_var=; unset $ac_var;} ;;
+      esac ;;
+    esac
+  done
+  (set) 2>&1 |
+    case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #(
+    *${as_nl}ac_space=\ *)
+      sed -n \
+	"s/'\''/'\''\\\\'\'''\''/g;
+	  s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p"
+      ;; #(
+    *)
+      sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p"
+      ;;
+    esac |
+    sort
+)
+    echo
+
+    printf "%s\n" "## ----------------- ##
+## Output variables. ##
+## ----------------- ##"
+    echo
+    for ac_var in $ac_subst_vars
+    do
+      eval ac_val=\$$ac_var
+      case $ac_val in
+      *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;;
+      esac
+      printf "%s\n" "$ac_var='\''$ac_val'\''"
+    done | sort
+    echo
+
+    if test -n "$ac_subst_files"; then
+      printf "%s\n" "## ------------------- ##
+## File substitutions. ##
+## ------------------- ##"
+      echo
+      for ac_var in $ac_subst_files
+      do
+	eval ac_val=\$$ac_var
+	case $ac_val in
+	*\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;;
+	esac
+	printf "%s\n" "$ac_var='\''$ac_val'\''"
+      done | sort
+      echo
+    fi
+
+    if test -s confdefs.h; then
+      printf "%s\n" "## ----------- ##
+## confdefs.h. ##
+## ----------- ##"
+      echo
+      cat confdefs.h
+      echo
+    fi
+    test "$ac_signal" != 0 &&
+      printf "%s\n" "$as_me: caught signal $ac_signal"
+    printf "%s\n" "$as_me: exit $exit_status"
+  } >&5
+  rm -f core *.core core.conftest.* &&
+    rm -f -r conftest* confdefs* conf$$* $ac_clean_files &&
+    exit $exit_status
+' 0
+for ac_signal in 1 2 13 15; do
+  trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal
+done
+ac_signal=0
+
+# confdefs.h avoids OS command line length limits that DEFS can exceed.
+rm -f -r conftest* confdefs.h
+
+printf "%s\n" "/* confdefs.h */" > confdefs.h
+
+# Predefined preprocessor variables.
+
+printf "%s\n" "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h
+
+
+# Let the site file select an alternate cache file if it wants to.
+# Prefer an explicitly selected file to automatically selected ones.
+if test -n "$CONFIG_SITE"; then
+  ac_site_files="$CONFIG_SITE"
+elif test "x$prefix" != xNONE; then
+  ac_site_files="$prefix/share/config.site $prefix/etc/config.site"
+else
+  ac_site_files="$ac_default_prefix/share/config.site $ac_default_prefix/etc/config.site"
+fi
+
+for ac_site_file in $ac_site_files
+do
+  case $ac_site_file in #(
+  */*) :
+     ;; #(
+  *) :
+    ac_site_file=./$ac_site_file ;;
+esac
+  if test -f "$ac_site_file" && test -r "$ac_site_file"; then
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5
+printf "%s\n" "$as_me: loading site script $ac_site_file" >&6;}
+    sed 's/^/| /' "$ac_site_file" >&5
+    . "$ac_site_file" \
+      || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "failed to load site script $ac_site_file
+See \`config.log' for more details" "$LINENO" 5; }
+  fi
+done
+
+if test -r "$cache_file"; then
+  # Some versions of bash will fail to source /dev/null (special files
+  # actually), so we avoid doing that.  DJGPP emulates it as a regular file.
+  if test /dev/null != "$cache_file" && test -f "$cache_file"; then
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5
+printf "%s\n" "$as_me: loading cache $cache_file" >&6;}
+    case $cache_file in
+      [\\/]* | ?:[\\/]* ) . "$cache_file";;
+      *)                      . "./$cache_file";;
+    esac
+  fi
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5
+printf "%s\n" "$as_me: creating cache $cache_file" >&6;}
+  >$cache_file
+fi
+
+# Test code for whether the C compiler supports C89 (global declarations)
+ac_c_conftest_c89_globals='
+/* Does the compiler advertise C89 conformance?
+   Do not test the value of __STDC__, because some compilers set it to 0
+   while being otherwise adequately conformant. */
+#if !defined __STDC__
+# error "Compiler does not advertise C89 conformance"
+#endif
+
+#include <stddef.h>
+#include <stdarg.h>
+struct stat;
+/* Most of the following tests are stolen from RCS 5.7 src/conf.sh.  */
+struct buf { int x; };
+struct buf * (*rcsopen) (struct buf *, struct stat *, int);
+static char *e (p, i)
+     char **p;
+     int i;
+{
+  return p[i];
+}
+static char *f (char * (*g) (char **, int), char **p, ...)
+{
+  char *s;
+  va_list v;
+  va_start (v,p);
+  s = g (p, va_arg (v,int));
+  va_end (v);
+  return s;
+}
+
+/* OSF 4.0 Compaq cc is some sort of almost-ANSI by default.  It has
+   function prototypes and stuff, but not \xHH hex character constants.
+   These do not provoke an error unfortunately, instead are silently treated
+   as an "x".  The following induces an error, until -std is added to get
+   proper ANSI mode.  Curiously \x00 != x always comes out true, for an
+   array size at least.  It is necessary to write \x00 == 0 to get something
+   that is true only with -std.  */
+int osf4_cc_array ['\''\x00'\'' == 0 ? 1 : -1];
+
+/* IBM C 6 for AIX is almost-ANSI by default, but it replaces macro parameters
+   inside strings and character constants.  */
+#define FOO(x) '\''x'\''
+int xlc6_cc_array[FOO(a) == '\''x'\'' ? 1 : -1];
+
+int test (int i, double x);
+struct s1 {int (*f) (int a);};
+struct s2 {int (*f) (double a);};
+int pairnames (int, char **, int *(*)(struct buf *, struct stat *, int),
+               int, int);'
+
+# Test code for whether the C compiler supports C89 (body of main).
+ac_c_conftest_c89_main='
+ok |= (argc == 0 || f (e, argv, 0) != argv[0] || f (e, argv, 1) != argv[1]);
+'
+
+# Test code for whether the C compiler supports C99 (global declarations)
+ac_c_conftest_c99_globals='
+// Does the compiler advertise C99 conformance?
+#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 199901L
+# error "Compiler does not advertise C99 conformance"
+#endif
+
+#include <stdbool.h>
+extern int puts (const char *);
+extern int printf (const char *, ...);
+extern int dprintf (int, const char *, ...);
+extern void *malloc (size_t);
+
+// Check varargs macros.  These examples are taken from C99 6.10.3.5.
+// dprintf is used instead of fprintf to avoid needing to declare
+// FILE and stderr.
+#define debug(...) dprintf (2, __VA_ARGS__)
+#define showlist(...) puts (#__VA_ARGS__)
+#define report(test,...) ((test) ? puts (#test) : printf (__VA_ARGS__))
+static void
+test_varargs_macros (void)
+{
+  int x = 1234;
+  int y = 5678;
+  debug ("Flag");
+  debug ("X = %d\n", x);
+  showlist (The first, second, and third items.);
+  report (x>y, "x is %d but y is %d", x, y);
+}
+
+// Check long long types.
+#define BIG64 18446744073709551615ull
+#define BIG32 4294967295ul
+#define BIG_OK (BIG64 / BIG32 == 4294967297ull && BIG64 % BIG32 == 0)
+#if !BIG_OK
+  #error "your preprocessor is broken"
+#endif
+#if BIG_OK
+#else
+  #error "your preprocessor is broken"
+#endif
+static long long int bignum = -9223372036854775807LL;
+static unsigned long long int ubignum = BIG64;
+
+struct incomplete_array
+{
+  int datasize;
+  double data[];
+};
+
+struct named_init {
+  int number;
+  const wchar_t *name;
+  double average;
+};
+
+typedef const char *ccp;
+
+static inline int
+test_restrict (ccp restrict text)
+{
+  // See if C++-style comments work.
+  // Iterate through items via the restricted pointer.
+  // Also check for declarations in for loops.
+  for (unsigned int i = 0; *(text+i) != '\''\0'\''; ++i)
+    continue;
+  return 0;
+}
+
+// Check varargs and va_copy.
+static bool
+test_varargs (const char *format, ...)
+{
+  va_list args;
+  va_start (args, format);
+  va_list args_copy;
+  va_copy (args_copy, args);
+
+  const char *str = "";
+  int number = 0;
+  float fnumber = 0;
+
+  while (*format)
+    {
+      switch (*format++)
+	{
+	case '\''s'\'': // string
+	  str = va_arg (args_copy, const char *);
+	  break;
+	case '\''d'\'': // int
+	  number = va_arg (args_copy, int);
+	  break;
+	case '\''f'\'': // float
+	  fnumber = va_arg (args_copy, double);
+	  break;
+	default:
+	  break;
+	}
+    }
+  va_end (args_copy);
+  va_end (args);
+
+  return *str && number && fnumber;
+}
+'
+
+# Test code for whether the C compiler supports C99 (body of main).
+ac_c_conftest_c99_main='
+  // Check bool.
+  _Bool success = false;
+  success |= (argc != 0);
+
+  // Check restrict.
+  if (test_restrict ("String literal") == 0)
+    success = true;
+  char *restrict newvar = "Another string";
+
+  // Check varargs.
+  success &= test_varargs ("s, d'\'' f .", "string", 65, 34.234);
+  test_varargs_macros ();
+
+  // Check flexible array members.
+  struct incomplete_array *ia =
+    malloc (sizeof (struct incomplete_array) + (sizeof (double) * 10));
+  ia->datasize = 10;
+  for (int i = 0; i < ia->datasize; ++i)
+    ia->data[i] = i * 1.234;
+
+  // Check named initializers.
+  struct named_init ni = {
+    .number = 34,
+    .name = L"Test wide string",
+    .average = 543.34343,
+  };
+
+  ni.number = 58;
+
+  int dynamic_array[ni.number];
+  dynamic_array[0] = argv[0][0];
+  dynamic_array[ni.number - 1] = 543;
+
+  // work around unused variable warnings
+  ok |= (!success || bignum == 0LL || ubignum == 0uLL || newvar[0] == '\''x'\''
+	 || dynamic_array[ni.number - 1] != 543);
+'
+
+# Test code for whether the C compiler supports C11 (global declarations)
+ac_c_conftest_c11_globals='
+// Does the compiler advertise C11 conformance?
+#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112L
+# error "Compiler does not advertise C11 conformance"
+#endif
+
+// Check _Alignas.
+char _Alignas (double) aligned_as_double;
+char _Alignas (0) no_special_alignment;
+extern char aligned_as_int;
+char _Alignas (0) _Alignas (int) aligned_as_int;
+
+// Check _Alignof.
+enum
+{
+  int_alignment = _Alignof (int),
+  int_array_alignment = _Alignof (int[100]),
+  char_alignment = _Alignof (char)
+};
+_Static_assert (0 < -_Alignof (int), "_Alignof is signed");
+
+// Check _Noreturn.
+int _Noreturn does_not_return (void) { for (;;) continue; }
+
+// Check _Static_assert.
+struct test_static_assert
+{
+  int x;
+  _Static_assert (sizeof (int) <= sizeof (long int),
+                  "_Static_assert does not work in struct");
+  long int y;
+};
+
+// Check UTF-8 literals.
+#define u8 syntax error!
+char const utf8_literal[] = u8"happens to be ASCII" "another string";
+
+// Check duplicate typedefs.
+typedef long *long_ptr;
+typedef long int *long_ptr;
+typedef long_ptr long_ptr;
+
+// Anonymous structures and unions -- taken from C11 6.7.2.1 Example 1.
+struct anonymous
+{
+  union {
+    struct { int i; int j; };
+    struct { int k; long int l; } w;
+  };
+  int m;
+} v1;
+'
+
+# Test code for whether the C compiler supports C11 (body of main).
+ac_c_conftest_c11_main='
+  _Static_assert ((offsetof (struct anonymous, i)
+		   == offsetof (struct anonymous, w.k)),
+		  "Anonymous union alignment botch");
+  v1.i = 2;
+  v1.w.k = 5;
+  ok |= v1.i != 5;
+'
+
+# Test code for whether the C compiler supports C11 (complete).
+ac_c_conftest_c11_program="${ac_c_conftest_c89_globals}
+${ac_c_conftest_c99_globals}
+${ac_c_conftest_c11_globals}
+
+int
+main (int argc, char **argv)
+{
+  int ok = 0;
+  ${ac_c_conftest_c89_main}
+  ${ac_c_conftest_c99_main}
+  ${ac_c_conftest_c11_main}
+  return ok;
+}
+"
+
+# Test code for whether the C compiler supports C99 (complete).
+ac_c_conftest_c99_program="${ac_c_conftest_c89_globals}
+${ac_c_conftest_c99_globals}
+
+int
+main (int argc, char **argv)
+{
+  int ok = 0;
+  ${ac_c_conftest_c89_main}
+  ${ac_c_conftest_c99_main}
+  return ok;
+}
+"
+
+# Test code for whether the C compiler supports C89 (complete).
+ac_c_conftest_c89_program="${ac_c_conftest_c89_globals}
+
+int
+main (int argc, char **argv)
+{
+  int ok = 0;
+  ${ac_c_conftest_c89_main}
+  return ok;
+}
+"
+
+as_fn_append ac_header_c_list " stdio.h stdio_h HAVE_STDIO_H"
+as_fn_append ac_header_c_list " stdlib.h stdlib_h HAVE_STDLIB_H"
+as_fn_append ac_header_c_list " string.h string_h HAVE_STRING_H"
+as_fn_append ac_header_c_list " inttypes.h inttypes_h HAVE_INTTYPES_H"
+as_fn_append ac_header_c_list " stdint.h stdint_h HAVE_STDINT_H"
+as_fn_append ac_header_c_list " strings.h strings_h HAVE_STRINGS_H"
+as_fn_append ac_header_c_list " sys/stat.h sys_stat_h HAVE_SYS_STAT_H"
+as_fn_append ac_header_c_list " sys/types.h sys_types_h HAVE_SYS_TYPES_H"
+as_fn_append ac_header_c_list " unistd.h unistd_h HAVE_UNISTD_H"
+
+# Auxiliary files required by this configure script.
+ac_aux_files="config.guess config.sub ltmain.sh ar-lib compile missing install-sh"
+
+# Locations in which to look for auxiliary files.
+ac_aux_dir_candidates="${srcdir}${PATH_SEPARATOR}${srcdir}/..${PATH_SEPARATOR}${srcdir}/../.."
+
+# Search for a directory containing all of the required auxiliary files,
+# $ac_aux_files, from the $PATH-style list $ac_aux_dir_candidates.
+# If we don't find one directory that contains all the files we need,
+# we report the set of missing files from the *first* directory in
+# $ac_aux_dir_candidates and give up.
+ac_missing_aux_files=""
+ac_first_candidate=:
+printf "%s\n" "$as_me:${as_lineno-$LINENO}: looking for aux files: $ac_aux_files" >&5
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+as_found=false
+for as_dir in $ac_aux_dir_candidates
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+  as_found=:
+
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}:  trying $as_dir" >&5
+  ac_aux_dir_found=yes
+  ac_install_sh=
+  for ac_aux in $ac_aux_files
+  do
+    # As a special case, if "install-sh" is required, that requirement
+    # can be satisfied by any of "install-sh", "install.sh", or "shtool",
+    # and $ac_install_sh is set appropriately for whichever one is found.
+    if test x"$ac_aux" = x"install-sh"
+    then
+      if test -f "${as_dir}install-sh"; then
+        printf "%s\n" "$as_me:${as_lineno-$LINENO}:   ${as_dir}install-sh found" >&5
+        ac_install_sh="${as_dir}install-sh -c"
+      elif test -f "${as_dir}install.sh"; then
+        printf "%s\n" "$as_me:${as_lineno-$LINENO}:   ${as_dir}install.sh found" >&5
+        ac_install_sh="${as_dir}install.sh -c"
+      elif test -f "${as_dir}shtool"; then
+        printf "%s\n" "$as_me:${as_lineno-$LINENO}:   ${as_dir}shtool found" >&5
+        ac_install_sh="${as_dir}shtool install -c"
+      else
+        ac_aux_dir_found=no
+        if $ac_first_candidate; then
+          ac_missing_aux_files="${ac_missing_aux_files} install-sh"
+        else
+          break
+        fi
+      fi
+    else
+      if test -f "${as_dir}${ac_aux}"; then
+        printf "%s\n" "$as_me:${as_lineno-$LINENO}:   ${as_dir}${ac_aux} found" >&5
+      else
+        ac_aux_dir_found=no
+        if $ac_first_candidate; then
+          ac_missing_aux_files="${ac_missing_aux_files} ${ac_aux}"
+        else
+          break
+        fi
+      fi
+    fi
+  done
+  if test "$ac_aux_dir_found" = yes; then
+    ac_aux_dir="$as_dir"
+    break
+  fi
+  ac_first_candidate=false
+
+  as_found=false
+done
+IFS=$as_save_IFS
+if $as_found
+then :
+
+else $as_nop
+  as_fn_error $? "cannot find required auxiliary files:$ac_missing_aux_files" "$LINENO" 5
+fi
+
+
+# These three variables are undocumented and unsupported,
+# and are intended to be withdrawn in a future Autoconf release.
+# They can cause serious problems if a builder's source tree is in a directory
+# whose full name contains unusual characters.
+if test -f "${ac_aux_dir}config.guess"; then
+  ac_config_guess="$SHELL ${ac_aux_dir}config.guess"
+fi
+if test -f "${ac_aux_dir}config.sub"; then
+  ac_config_sub="$SHELL ${ac_aux_dir}config.sub"
+fi
+if test -f "$ac_aux_dir/configure"; then
+  ac_configure="$SHELL ${ac_aux_dir}configure"
+fi
+
+# Check that the precious variables saved in the cache have kept the same
+# value.
+ac_cache_corrupted=false
+for ac_var in $ac_precious_vars; do
+  eval ac_old_set=\$ac_cv_env_${ac_var}_set
+  eval ac_new_set=\$ac_env_${ac_var}_set
+  eval ac_old_val=\$ac_cv_env_${ac_var}_value
+  eval ac_new_val=\$ac_env_${ac_var}_value
+  case $ac_old_set,$ac_new_set in
+    set,)
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5
+printf "%s\n" "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;}
+      ac_cache_corrupted=: ;;
+    ,set)
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5
+printf "%s\n" "$as_me: error: \`$ac_var' was not set in the previous run" >&2;}
+      ac_cache_corrupted=: ;;
+    ,);;
+    *)
+      if test "x$ac_old_val" != "x$ac_new_val"; then
+	# differences in whitespace do not lead to failure.
+	ac_old_val_w=`echo x $ac_old_val`
+	ac_new_val_w=`echo x $ac_new_val`
+	if test "$ac_old_val_w" != "$ac_new_val_w"; then
+	  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5
+printf "%s\n" "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;}
+	  ac_cache_corrupted=:
+	else
+	  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5
+printf "%s\n" "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;}
+	  eval $ac_var=\$ac_old_val
+	fi
+	{ printf "%s\n" "$as_me:${as_lineno-$LINENO}:   former value:  \`$ac_old_val'" >&5
+printf "%s\n" "$as_me:   former value:  \`$ac_old_val'" >&2;}
+	{ printf "%s\n" "$as_me:${as_lineno-$LINENO}:   current value: \`$ac_new_val'" >&5
+printf "%s\n" "$as_me:   current value: \`$ac_new_val'" >&2;}
+      fi;;
+  esac
+  # Pass precious variables to config.status.
+  if test "$ac_new_set" = set; then
+    case $ac_new_val in
+    *\'*) ac_arg=$ac_var=`printf "%s\n" "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;;
+    *) ac_arg=$ac_var=$ac_new_val ;;
+    esac
+    case " $ac_configure_args " in
+      *" '$ac_arg' "*) ;; # Avoid dups.  Use of quotes ensures accuracy.
+      *) as_fn_append ac_configure_args " '$ac_arg'" ;;
+    esac
+  fi
+done
+if $ac_cache_corrupted; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5
+printf "%s\n" "$as_me: error: changes in the environment can compromise the build" >&2;}
+  as_fn_error $? "run \`${MAKE-make} distclean' and/or \`rm $cache_file'
+	    and start over" "$LINENO" 5
+fi
+## -------------------- ##
+## Main body of script. ##
+## -------------------- ##
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
+
+am__api_version='1.16'
+
+
+
+  # Find a good install program.  We prefer a C program (faster),
+# so one script is as good as another.  But avoid the broken or
+# incompatible versions:
+# SysV /etc/install, /usr/sbin/install
+# SunOS /usr/etc/install
+# IRIX /sbin/install
+# AIX /bin/install
+# AmigaOS /C/install, which installs bootblocks on floppy discs
+# AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag
+# AFS /usr/afsws/bin/install, which mishandles nonexistent args
+# SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff"
+# OS/2's system install, which has a completely different semantic
+# ./install, which can be erroneously created by make from ./install.sh.
+# Reject install programs that cannot install multiple files.
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5
+printf %s "checking for a BSD-compatible install... " >&6; }
+if test -z "$INSTALL"; then
+if test ${ac_cv_path_install+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    # Account for fact that we put trailing slashes in our PATH walk.
+case $as_dir in #((
+  ./ | /[cC]/* | \
+  /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \
+  ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \
+  /usr/ucb/* ) ;;
+  *)
+    # OSF1 and SCO ODT 3.0 have their own names for install.
+    # Don't use installbsd from OSF since it installs stuff as root
+    # by default.
+    for ac_prog in ginstall scoinst install; do
+      for ac_exec_ext in '' $ac_executable_extensions; do
+	if as_fn_executable_p "$as_dir$ac_prog$ac_exec_ext"; then
+	  if test $ac_prog = install &&
+	    grep dspmsg "$as_dir$ac_prog$ac_exec_ext" >/dev/null 2>&1; then
+	    # AIX install.  It has an incompatible calling convention.
+	    :
+	  elif test $ac_prog = install &&
+	    grep pwplus "$as_dir$ac_prog$ac_exec_ext" >/dev/null 2>&1; then
+	    # program-specific install script used by HP pwplus--don't use.
+	    :
+	  else
+	    rm -rf conftest.one conftest.two conftest.dir
+	    echo one > conftest.one
+	    echo two > conftest.two
+	    mkdir conftest.dir
+	    if "$as_dir$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir/" &&
+	      test -s conftest.one && test -s conftest.two &&
+	      test -s conftest.dir/conftest.one &&
+	      test -s conftest.dir/conftest.two
+	    then
+	      ac_cv_path_install="$as_dir$ac_prog$ac_exec_ext -c"
+	      break 3
+	    fi
+	  fi
+	fi
+      done
+    done
+    ;;
+esac
+
+  done
+IFS=$as_save_IFS
+
+rm -rf conftest.one conftest.two conftest.dir
+
+fi
+  if test ${ac_cv_path_install+y}; then
+    INSTALL=$ac_cv_path_install
+  else
+    # As a last resort, use the slow shell script.  Don't cache a
+    # value for INSTALL within a source directory, because that will
+    # break other packages using the cache if that directory is
+    # removed, or if the value is a relative name.
+    INSTALL=$ac_install_sh
+  fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5
+printf "%s\n" "$INSTALL" >&6; }
+
+# Use test -z because SunOS4 sh mishandles braces in ${var-val}.
+# It thinks the first close brace ends the variable substitution.
+test -z "$INSTALL_PROGRAM" && INSTALL_PROGRAM='${INSTALL}'
+
+test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}'
+
+test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644'
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether build environment is sane" >&5
+printf %s "checking whether build environment is sane... " >&6; }
+# Reject unsafe characters in $srcdir or the absolute working directory
+# name.  Accept space and tab only in the latter.
+am_lf='
+'
+case `pwd` in
+  *[\\\"\#\$\&\'\`$am_lf]*)
+    as_fn_error $? "unsafe absolute working directory name" "$LINENO" 5;;
+esac
+case $srcdir in
+  *[\\\"\#\$\&\'\`$am_lf\ \	]*)
+    as_fn_error $? "unsafe srcdir value: '$srcdir'" "$LINENO" 5;;
+esac
+
+# Do 'set' in a subshell so we don't clobber the current shell's
+# arguments.  Must try -L first in case configure is actually a
+# symlink; some systems play weird games with the mod time of symlinks
+# (eg FreeBSD returns the mod time of the symlink's containing
+# directory).
+if (
+   am_has_slept=no
+   for am_try in 1 2; do
+     echo "timestamp, slept: $am_has_slept" > conftest.file
+     set X `ls -Lt "$srcdir/configure" conftest.file 2> /dev/null`
+     if test "$*" = "X"; then
+	# -L didn't work.
+	set X `ls -t "$srcdir/configure" conftest.file`
+     fi
+     if test "$*" != "X $srcdir/configure conftest.file" \
+	&& test "$*" != "X conftest.file $srcdir/configure"; then
+
+	# If neither matched, then we have a broken ls.  This can happen
+	# if, for instance, CONFIG_SHELL is bash and it inherits a
+	# broken ls alias from the environment.  This has actually
+	# happened.  Such a system could not be considered "sane".
+	as_fn_error $? "ls -t appears to fail.  Make sure there is not a broken
+  alias in your environment" "$LINENO" 5
+     fi
+     if test "$2" = conftest.file || test $am_try -eq 2; then
+       break
+     fi
+     # Just in case.
+     sleep 1
+     am_has_slept=yes
+   done
+   test "$2" = conftest.file
+   )
+then
+   # Ok.
+   :
+else
+   as_fn_error $? "newly created file is older than distributed files!
+Check your system clock" "$LINENO" 5
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+# If we didn't sleep, we still need to ensure time stamps of config.status and
+# generated files are strictly newer.
+am_sleep_pid=
+if grep 'slept: no' conftest.file >/dev/null 2>&1; then
+  ( sleep 1 ) &
+  am_sleep_pid=$!
+fi
+
+rm -f conftest.file
+
+test "$program_prefix" != NONE &&
+  program_transform_name="s&^&$program_prefix&;$program_transform_name"
+# Use a double $ so make ignores it.
+test "$program_suffix" != NONE &&
+  program_transform_name="s&\$&$program_suffix&;$program_transform_name"
+# Double any \ or $.
+# By default was `s,x,x', remove it if useless.
+ac_script='s/[\\$]/&&/g;s/;s,x,x,$//'
+program_transform_name=`printf "%s\n" "$program_transform_name" | sed "$ac_script"`
+
+
+# Expand $ac_aux_dir to an absolute path.
+am_aux_dir=`cd "$ac_aux_dir" && pwd`
+
+
+  if test x"${MISSING+set}" != xset; then
+  MISSING="\${SHELL} '$am_aux_dir/missing'"
+fi
+# Use eval to expand $SHELL
+if eval "$MISSING --is-lightweight"; then
+  am_missing_run="$MISSING "
+else
+  am_missing_run=
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: 'missing' script is too old or missing" >&5
+printf "%s\n" "$as_me: WARNING: 'missing' script is too old or missing" >&2;}
+fi
+
+if test x"${install_sh+set}" != xset; then
+  case $am_aux_dir in
+  *\ * | *\	*)
+    install_sh="\${SHELL} '$am_aux_dir/install-sh'" ;;
+  *)
+    install_sh="\${SHELL} $am_aux_dir/install-sh"
+  esac
+fi
+
+# Installed binaries are usually stripped using 'strip' when the user
+# run "make install-strip".  However 'strip' might not be the right
+# tool to use in cross-compilation environments, therefore Automake
+# will honor the 'STRIP' environment variable to overrule this program.
+if test "$cross_compiling" != no; then
+  if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}strip", so it can be a program name with args.
+set dummy ${ac_tool_prefix}strip; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_STRIP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$STRIP"; then
+  ac_cv_prog_STRIP="$STRIP" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_STRIP="${ac_tool_prefix}strip"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+STRIP=$ac_cv_prog_STRIP
+if test -n "$STRIP"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $STRIP" >&5
+printf "%s\n" "$STRIP" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_STRIP"; then
+  ac_ct_STRIP=$STRIP
+  # Extract the first word of "strip", so it can be a program name with args.
+set dummy strip; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_STRIP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_STRIP"; then
+  ac_cv_prog_ac_ct_STRIP="$ac_ct_STRIP" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_STRIP="strip"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_STRIP=$ac_cv_prog_ac_ct_STRIP
+if test -n "$ac_ct_STRIP"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_STRIP" >&5
+printf "%s\n" "$ac_ct_STRIP" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_STRIP" = x; then
+    STRIP=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    STRIP=$ac_ct_STRIP
+  fi
+else
+  STRIP="$ac_cv_prog_STRIP"
+fi
+
+fi
+INSTALL_STRIP_PROGRAM="\$(install_sh) -c -s"
+
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a race-free mkdir -p" >&5
+printf %s "checking for a race-free mkdir -p... " >&6; }
+if test -z "$MKDIR_P"; then
+  if test ${ac_cv_path_mkdir+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH$PATH_SEPARATOR/opt/sfw/bin
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_prog in mkdir gmkdir; do
+	 for ac_exec_ext in '' $ac_executable_extensions; do
+	   as_fn_executable_p "$as_dir$ac_prog$ac_exec_ext" || continue
+	   case `"$as_dir$ac_prog$ac_exec_ext" --version 2>&1` in #(
+	     'mkdir ('*'coreutils) '* | \
+	     'BusyBox '* | \
+	     'mkdir (fileutils) '4.1*)
+	       ac_cv_path_mkdir=$as_dir$ac_prog$ac_exec_ext
+	       break 3;;
+	   esac
+	 done
+       done
+  done
+IFS=$as_save_IFS
+
+fi
+
+  test -d ./--version && rmdir ./--version
+  if test ${ac_cv_path_mkdir+y}; then
+    MKDIR_P="$ac_cv_path_mkdir -p"
+  else
+    # As a last resort, use the slow shell script.  Don't cache a
+    # value for MKDIR_P within a source directory, because that will
+    # break other packages using the cache if that directory is
+    # removed, or if the value is a relative name.
+    MKDIR_P="$ac_install_sh -d"
+  fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $MKDIR_P" >&5
+printf "%s\n" "$MKDIR_P" >&6; }
+
+for ac_prog in gawk mawk nawk awk
+do
+  # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_AWK+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$AWK"; then
+  ac_cv_prog_AWK="$AWK" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_AWK="$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+AWK=$ac_cv_prog_AWK
+if test -n "$AWK"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $AWK" >&5
+printf "%s\n" "$AWK" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+  test -n "$AWK" && break
+done
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} sets \$(MAKE)" >&5
+printf %s "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; }
+set x ${MAKE-make}
+ac_make=`printf "%s\n" "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'`
+if eval test \${ac_cv_prog_make_${ac_make}_set+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  cat >conftest.make <<\_ACEOF
+SHELL = /bin/sh
+all:
+	@echo '@@@%%%=$(MAKE)=@@@%%%'
+_ACEOF
+# GNU make sometimes prints "make[1]: Entering ...", which would confuse us.
+case `${MAKE-make} -f conftest.make 2>/dev/null` in
+  *@@@%%%=?*=@@@%%%*)
+    eval ac_cv_prog_make_${ac_make}_set=yes;;
+  *)
+    eval ac_cv_prog_make_${ac_make}_set=no;;
+esac
+rm -f conftest.make
+fi
+if eval test \$ac_cv_prog_make_${ac_make}_set = yes; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+  SET_MAKE=
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+  SET_MAKE="MAKE=${MAKE-make}"
+fi
+
+rm -rf .tst 2>/dev/null
+mkdir .tst 2>/dev/null
+if test -d .tst; then
+  am__leading_dot=.
+else
+  am__leading_dot=_
+fi
+rmdir .tst 2>/dev/null
+
+# Check whether --enable-silent-rules was given.
+if test ${enable_silent_rules+y}
+then :
+  enableval=$enable_silent_rules;
+fi
+
+case $enable_silent_rules in # (((
+  yes) AM_DEFAULT_VERBOSITY=0;;
+   no) AM_DEFAULT_VERBOSITY=1;;
+    *) AM_DEFAULT_VERBOSITY=1;;
+esac
+am_make=${MAKE-make}
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $am_make supports nested variables" >&5
+printf %s "checking whether $am_make supports nested variables... " >&6; }
+if test ${am_cv_make_support_nested_variables+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if printf "%s\n" 'TRUE=$(BAR$(V))
+BAR0=false
+BAR1=true
+V=1
+am__doit:
+	@$(TRUE)
+.PHONY: am__doit' | $am_make -f - >/dev/null 2>&1; then
+  am_cv_make_support_nested_variables=yes
+else
+  am_cv_make_support_nested_variables=no
+fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_make_support_nested_variables" >&5
+printf "%s\n" "$am_cv_make_support_nested_variables" >&6; }
+if test $am_cv_make_support_nested_variables = yes; then
+    AM_V='$(V)'
+  AM_DEFAULT_V='$(AM_DEFAULT_VERBOSITY)'
+else
+  AM_V=$AM_DEFAULT_VERBOSITY
+  AM_DEFAULT_V=$AM_DEFAULT_VERBOSITY
+fi
+AM_BACKSLASH='\'
+
+if test "`cd $srcdir && pwd`" != "`pwd`"; then
+  # Use -I$(srcdir) only when $(srcdir) != ., so that make's output
+  # is not polluted with repeated "-I."
+  am__isrc=' -I$(srcdir)'
+  # test to see if srcdir already configured
+  if test -f $srcdir/config.status; then
+    as_fn_error $? "source directory already configured; run \"make distclean\" there first" "$LINENO" 5
+  fi
+fi
+
+# test whether we have cygpath
+if test -z "$CYGPATH_W"; then
+  if (cygpath --version) >/dev/null 2>/dev/null; then
+    CYGPATH_W='cygpath -w'
+  else
+    CYGPATH_W=echo
+  fi
+fi
+
+
+# Define the identity of the package.
+ PACKAGE='sg3_utils'
+ VERSION='1.48'
+
+
+printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h
+
+
+printf "%s\n" "#define VERSION \"$VERSION\"" >>confdefs.h
+
+# Some tools Automake needs.
+
+ACLOCAL=${ACLOCAL-"${am_missing_run}aclocal-${am__api_version}"}
+
+
+AUTOCONF=${AUTOCONF-"${am_missing_run}autoconf"}
+
+
+AUTOMAKE=${AUTOMAKE-"${am_missing_run}automake-${am__api_version}"}
+
+
+AUTOHEADER=${AUTOHEADER-"${am_missing_run}autoheader"}
+
+
+MAKEINFO=${MAKEINFO-"${am_missing_run}makeinfo"}
+
+# For better backward compatibility.  To be removed once Automake 1.9.x
+# dies out for good.  For more background, see:
+# <https://lists.gnu.org/archive/html/automake/2012-07/msg00001.html>
+# <https://lists.gnu.org/archive/html/automake/2012-07/msg00014.html>
+mkdir_p='$(MKDIR_P)'
+
+# We need awk for the "check" target (and possibly the TAP driver).  The
+# system "awk" is bad on some platforms.
+# Always define AMTAR for backward compatibility.  Yes, it's still used
+# in the wild :-(  We should find a proper way to deprecate it ...
+AMTAR='$${TAR-tar}'
+
+
+# We'll loop over all known methods to create a tar archive until one works.
+_am_tools='gnutar  pax cpio none'
+
+am__tar='$${TAR-tar} chof - "$$tardir"' am__untar='$${TAR-tar} xf -'
+
+
+
+
+
+# Variables for tags utilities; see am/tags.am
+if test -z "$CTAGS"; then
+  CTAGS=ctags
+fi
+
+if test -z "$ETAGS"; then
+  ETAGS=etags
+fi
+
+if test -z "$CSCOPE"; then
+  CSCOPE=cscope
+fi
+
+
+
+# POSIX will say in a future version that running "rm -f" with no argument
+# is OK; and we want to be able to make that assumption in our Makefile
+# recipes.  So use an aggressive probe to check that the usage we want is
+# actually supported "in the wild" to an acceptable degree.
+# See automake bug#10828.
+# To make any issue more visible, cause the running configure to be aborted
+# by default if the 'rm' program in use doesn't match our expectations; the
+# user can still override this though.
+if rm -f && rm -fr && rm -rf; then : OK; else
+  cat >&2 <<'END'
+Oops!
+
+Your 'rm' program seems unable to run without file operands specified
+on the command line, even when the '-f' option is present.  This is contrary
+to the behaviour of most rm programs out there, and not conforming with
+the upcoming POSIX standard: <http://austingroupbugs.net/view.php?id=542>
+
+Please tell bug-automake@gnu.org about your system, including the value
+of your $PATH and any error possibly output before this message.  This
+can help us improve future automake versions.
+
+END
+  if test x"$ACCEPT_INFERIOR_RM_PROGRAM" = x"yes"; then
+    echo 'Configuration will proceed anyway, since you have set the' >&2
+    echo 'ACCEPT_INFERIOR_RM_PROGRAM variable to "yes"' >&2
+    echo >&2
+  else
+    cat >&2 <<'END'
+Aborting the configuration process, to ensure you take notice of the issue.
+
+You can download and install GNU coreutils to get an 'rm' implementation
+that behaves properly: <https://www.gnu.org/software/coreutils/>.
+
+If you want to complete the configuration process using your problematic
+'rm' anyway, export the environment variable ACCEPT_INFERIOR_RM_PROGRAM
+to "yes", and re-run configure.
+
+END
+    as_fn_error $? "Your 'rm' program is bad, sorry." "$LINENO" 5
+  fi
+fi
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to enable maintainer-specific portions of Makefiles" >&5
+printf %s "checking whether to enable maintainer-specific portions of Makefiles... " >&6; }
+    # Check whether --enable-maintainer-mode was given.
+if test ${enable_maintainer_mode+y}
+then :
+  enableval=$enable_maintainer_mode; USE_MAINTAINER_MODE=$enableval
+else $as_nop
+  USE_MAINTAINER_MODE=no
+fi
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $USE_MAINTAINER_MODE" >&5
+printf "%s\n" "$USE_MAINTAINER_MODE" >&6; }
+   if test $USE_MAINTAINER_MODE = yes; then
+  MAINTAINER_MODE_TRUE=
+  MAINTAINER_MODE_FALSE='#'
+else
+  MAINTAINER_MODE_TRUE='#'
+  MAINTAINER_MODE_FALSE=
+fi
+
+  MAINT=$MAINTAINER_MODE_TRUE
+
+
+ac_config_headers="$ac_config_headers config.h"
+
+
+
+
+
+
+
+
+
+
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}gcc", so it can be a program name with args.
+set dummy ${ac_tool_prefix}gcc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$CC"; then
+  ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_CC="${ac_tool_prefix}gcc"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_CC"; then
+  ac_ct_CC=$CC
+  # Extract the first word of "gcc", so it can be a program name with args.
+set dummy gcc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_CC+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_CC"; then
+  ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_CC="gcc"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_CC=$ac_cv_prog_ac_ct_CC
+if test -n "$ac_ct_CC"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5
+printf "%s\n" "$ac_ct_CC" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_CC" = x; then
+    CC=""
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    CC=$ac_ct_CC
+  fi
+else
+  CC="$ac_cv_prog_CC"
+fi
+
+if test -z "$CC"; then
+          if test -n "$ac_tool_prefix"; then
+    # Extract the first word of "${ac_tool_prefix}cc", so it can be a program name with args.
+set dummy ${ac_tool_prefix}cc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$CC"; then
+  ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_CC="${ac_tool_prefix}cc"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+  fi
+fi
+if test -z "$CC"; then
+  # Extract the first word of "cc", so it can be a program name with args.
+set dummy cc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$CC"; then
+  ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+  ac_prog_rejected=no
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    if test "$as_dir$ac_word$ac_exec_ext" = "/usr/ucb/cc"; then
+       ac_prog_rejected=yes
+       continue
+     fi
+    ac_cv_prog_CC="cc"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+if test $ac_prog_rejected = yes; then
+  # We found a bogon in the path, so make sure we never use it.
+  set dummy $ac_cv_prog_CC
+  shift
+  if test $# != 0; then
+    # We chose a different compiler from the bogus one.
+    # However, it has the same basename, so the bogon will be chosen
+    # first if we set CC to just the basename; use the full file name.
+    shift
+    ac_cv_prog_CC="$as_dir$ac_word${1+' '}$@"
+  fi
+fi
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$CC"; then
+  if test -n "$ac_tool_prefix"; then
+  for ac_prog in cl.exe
+  do
+    # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args.
+set dummy $ac_tool_prefix$ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$CC"; then
+  ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_CC="$ac_tool_prefix$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+    test -n "$CC" && break
+  done
+fi
+if test -z "$CC"; then
+  ac_ct_CC=$CC
+  for ac_prog in cl.exe
+do
+  # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_CC+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_CC"; then
+  ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_CC="$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_CC=$ac_cv_prog_ac_ct_CC
+if test -n "$ac_ct_CC"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5
+printf "%s\n" "$ac_ct_CC" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+  test -n "$ac_ct_CC" && break
+done
+
+  if test "x$ac_ct_CC" = x; then
+    CC=""
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    CC=$ac_ct_CC
+  fi
+fi
+
+fi
+if test -z "$CC"; then
+  if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}clang", so it can be a program name with args.
+set dummy ${ac_tool_prefix}clang; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$CC"; then
+  ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_CC="${ac_tool_prefix}clang"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_CC"; then
+  ac_ct_CC=$CC
+  # Extract the first word of "clang", so it can be a program name with args.
+set dummy clang; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_CC+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_CC"; then
+  ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_CC="clang"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_CC=$ac_cv_prog_ac_ct_CC
+if test -n "$ac_ct_CC"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5
+printf "%s\n" "$ac_ct_CC" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_CC" = x; then
+    CC=""
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    CC=$ac_ct_CC
+  fi
+else
+  CC="$ac_cv_prog_CC"
+fi
+
+fi
+
+
+test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "no acceptable C compiler found in \$PATH
+See \`config.log' for more details" "$LINENO" 5; }
+
+# Provide some information about the compiler.
+printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5
+set X $ac_compile
+ac_compiler=$2
+for ac_option in --version -v -V -qversion -version; do
+  { { ac_try="$ac_compiler $ac_option >&5"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_compiler $ac_option >&5") 2>conftest.err
+  ac_status=$?
+  if test -s conftest.err; then
+    sed '10a\
+... rest of stderr output deleted ...
+         10q' conftest.err >conftest.er1
+    cat conftest.er1 >&5
+  fi
+  rm -f conftest.er1 conftest.err
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+done
+
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+ac_clean_files_save=$ac_clean_files
+ac_clean_files="$ac_clean_files a.out a.out.dSYM a.exe b.out"
+# Try to create an executable without -o first, disregard a.out.
+# It will help us diagnose broken compilers, and finding out an intuition
+# of exeext.
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5
+printf %s "checking whether the C compiler works... " >&6; }
+ac_link_default=`printf "%s\n" "$ac_link" | sed 's/ -o *conftest[^ ]*//'`
+
+# The possible output files:
+ac_files="a.out conftest.exe conftest a.exe a_out.exe b.out conftest.*"
+
+ac_rmfiles=
+for ac_file in $ac_files
+do
+  case $ac_file in
+    *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj ) ;;
+    * ) ac_rmfiles="$ac_rmfiles $ac_file";;
+  esac
+done
+rm -f $ac_rmfiles
+
+if { { ac_try="$ac_link_default"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_link_default") 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+then :
+  # Autoconf-2.13 could set the ac_cv_exeext variable to `no'.
+# So ignore a value of `no', otherwise this would lead to `EXEEXT = no'
+# in a Makefile.  We should not override ac_cv_exeext if it was cached,
+# so that the user can short-circuit this test for compilers unknown to
+# Autoconf.
+for ac_file in $ac_files ''
+do
+  test -f "$ac_file" || continue
+  case $ac_file in
+    *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj )
+	;;
+    [ab].out )
+	# We found the default executable, but exeext='' is most
+	# certainly right.
+	break;;
+    *.* )
+	if test ${ac_cv_exeext+y} && test "$ac_cv_exeext" != no;
+	then :; else
+	   ac_cv_exeext=`expr "$ac_file" : '[^.]*\(\..*\)'`
+	fi
+	# We set ac_cv_exeext here because the later test for it is not
+	# safe: cross compilers may not add the suffix if given an `-o'
+	# argument, so we may need to know it at that point already.
+	# Even if this section looks crufty: it has the advantage of
+	# actually working.
+	break;;
+    * )
+	break;;
+  esac
+done
+test "$ac_cv_exeext" = no && ac_cv_exeext=
+
+else $as_nop
+  ac_file=''
+fi
+if test -z "$ac_file"
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error 77 "C compiler cannot create executables
+See \`config.log' for more details" "$LINENO" 5; }
+else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5
+printf %s "checking for C compiler default output file name... " >&6; }
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5
+printf "%s\n" "$ac_file" >&6; }
+ac_exeext=$ac_cv_exeext
+
+rm -f -r a.out a.out.dSYM a.exe conftest$ac_cv_exeext b.out
+ac_clean_files=$ac_clean_files_save
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5
+printf %s "checking for suffix of executables... " >&6; }
+if { { ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_link") 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+then :
+  # If both `conftest.exe' and `conftest' are `present' (well, observable)
+# catch `conftest.exe'.  For instance with Cygwin, `ls conftest' will
+# work properly (i.e., refer to `conftest.exe'), while it won't with
+# `rm'.
+for ac_file in conftest.exe conftest conftest.*; do
+  test -f "$ac_file" || continue
+  case $ac_file in
+    *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj ) ;;
+    *.* ) ac_cv_exeext=`expr "$ac_file" : '[^.]*\(\..*\)'`
+	  break;;
+    * ) break;;
+  esac
+done
+else $as_nop
+  { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "cannot compute suffix of executables: cannot compile and link
+See \`config.log' for more details" "$LINENO" 5; }
+fi
+rm -f conftest conftest$ac_cv_exeext
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5
+printf "%s\n" "$ac_cv_exeext" >&6; }
+
+rm -f conftest.$ac_ext
+EXEEXT=$ac_cv_exeext
+ac_exeext=$EXEEXT
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <stdio.h>
+int
+main (void)
+{
+FILE *f = fopen ("conftest.out", "w");
+ return ferror (f) || fclose (f) != 0;
+
+  ;
+  return 0;
+}
+_ACEOF
+ac_clean_files="$ac_clean_files conftest.out"
+# Check that the compiler produces executables we can run.  If not, either
+# the compiler is broken, or we cross compile.
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5
+printf %s "checking whether we are cross compiling... " >&6; }
+if test "$cross_compiling" != yes; then
+  { { ac_try="$ac_link"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_link") 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+  if { ac_try='./conftest$ac_cv_exeext'
+  { { case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_try") 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; }; then
+    cross_compiling=no
+  else
+    if test "$cross_compiling" = maybe; then
+	cross_compiling=yes
+    else
+	{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error 77 "cannot run C compiled programs.
+If you meant to cross compile, use \`--host'.
+See \`config.log' for more details" "$LINENO" 5; }
+    fi
+  fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5
+printf "%s\n" "$cross_compiling" >&6; }
+
+rm -f conftest.$ac_ext conftest$ac_cv_exeext conftest.out
+ac_clean_files=$ac_clean_files_save
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5
+printf %s "checking for suffix of object files... " >&6; }
+if test ${ac_cv_objext+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+rm -f conftest.o conftest.obj
+if { { ac_try="$ac_compile"
+case "(($ac_try" in
+  *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+  *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+  (eval "$ac_compile") 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+then :
+  for ac_file in conftest.o conftest.obj conftest.*; do
+  test -f "$ac_file" || continue;
+  case $ac_file in
+    *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM ) ;;
+    *) ac_cv_objext=`expr "$ac_file" : '.*\.\(.*\)'`
+       break;;
+  esac
+done
+else $as_nop
+  printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "cannot compute suffix of object files: cannot compile
+See \`config.log' for more details" "$LINENO" 5; }
+fi
+rm -f conftest.$ac_cv_objext conftest.$ac_ext
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5
+printf "%s\n" "$ac_cv_objext" >&6; }
+OBJEXT=$ac_cv_objext
+ac_objext=$OBJEXT
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the compiler supports GNU C" >&5
+printf %s "checking whether the compiler supports GNU C... " >&6; }
+if test ${ac_cv_c_compiler_gnu+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+#ifndef __GNUC__
+       choke me
+#endif
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+  ac_compiler_gnu=yes
+else $as_nop
+  ac_compiler_gnu=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+ac_cv_c_compiler_gnu=$ac_compiler_gnu
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5
+printf "%s\n" "$ac_cv_c_compiler_gnu" >&6; }
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+if test $ac_compiler_gnu = yes; then
+  GCC=yes
+else
+  GCC=
+fi
+ac_test_CFLAGS=${CFLAGS+y}
+ac_save_CFLAGS=$CFLAGS
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5
+printf %s "checking whether $CC accepts -g... " >&6; }
+if test ${ac_cv_prog_cc_g+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_save_c_werror_flag=$ac_c_werror_flag
+   ac_c_werror_flag=yes
+   ac_cv_prog_cc_g=no
+   CFLAGS="-g"
+   cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+  ac_cv_prog_cc_g=yes
+else $as_nop
+  CFLAGS=""
+      cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+
+else $as_nop
+  ac_c_werror_flag=$ac_save_c_werror_flag
+	 CFLAGS="-g"
+	 cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+  ac_cv_prog_cc_g=yes
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+   ac_c_werror_flag=$ac_save_c_werror_flag
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5
+printf "%s\n" "$ac_cv_prog_cc_g" >&6; }
+if test $ac_test_CFLAGS; then
+  CFLAGS=$ac_save_CFLAGS
+elif test $ac_cv_prog_cc_g = yes; then
+  if test "$GCC" = yes; then
+    CFLAGS="-g -O2"
+  else
+    CFLAGS="-g"
+  fi
+else
+  if test "$GCC" = yes; then
+    CFLAGS="-O2"
+  else
+    CFLAGS=
+  fi
+fi
+ac_prog_cc_stdc=no
+if test x$ac_prog_cc_stdc = xno
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C11 features" >&5
+printf %s "checking for $CC option to enable C11 features... " >&6; }
+if test ${ac_cv_prog_cc_c11+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_cv_prog_cc_c11=no
+ac_save_CC=$CC
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+$ac_c_conftest_c11_program
+_ACEOF
+for ac_arg in '' -std=gnu11
+do
+  CC="$ac_save_CC $ac_arg"
+  if ac_fn_c_try_compile "$LINENO"
+then :
+  ac_cv_prog_cc_c11=$ac_arg
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam
+  test "x$ac_cv_prog_cc_c11" != "xno" && break
+done
+rm -f conftest.$ac_ext
+CC=$ac_save_CC
+fi
+
+if test "x$ac_cv_prog_cc_c11" = xno
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5
+printf "%s\n" "unsupported" >&6; }
+else $as_nop
+  if test "x$ac_cv_prog_cc_c11" = x
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5
+printf "%s\n" "none needed" >&6; }
+else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5
+printf "%s\n" "$ac_cv_prog_cc_c11" >&6; }
+     CC="$CC $ac_cv_prog_cc_c11"
+fi
+  ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c11
+  ac_prog_cc_stdc=c11
+fi
+fi
+if test x$ac_prog_cc_stdc = xno
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C99 features" >&5
+printf %s "checking for $CC option to enable C99 features... " >&6; }
+if test ${ac_cv_prog_cc_c99+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_cv_prog_cc_c99=no
+ac_save_CC=$CC
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+$ac_c_conftest_c99_program
+_ACEOF
+for ac_arg in '' -std=gnu99 -std=c99 -c99 -qlanglvl=extc1x -qlanglvl=extc99 -AC99 -D_STDC_C99=
+do
+  CC="$ac_save_CC $ac_arg"
+  if ac_fn_c_try_compile "$LINENO"
+then :
+  ac_cv_prog_cc_c99=$ac_arg
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam
+  test "x$ac_cv_prog_cc_c99" != "xno" && break
+done
+rm -f conftest.$ac_ext
+CC=$ac_save_CC
+fi
+
+if test "x$ac_cv_prog_cc_c99" = xno
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5
+printf "%s\n" "unsupported" >&6; }
+else $as_nop
+  if test "x$ac_cv_prog_cc_c99" = x
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5
+printf "%s\n" "none needed" >&6; }
+else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5
+printf "%s\n" "$ac_cv_prog_cc_c99" >&6; }
+     CC="$CC $ac_cv_prog_cc_c99"
+fi
+  ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c99
+  ac_prog_cc_stdc=c99
+fi
+fi
+if test x$ac_prog_cc_stdc = xno
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C89 features" >&5
+printf %s "checking for $CC option to enable C89 features... " >&6; }
+if test ${ac_cv_prog_cc_c89+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_cv_prog_cc_c89=no
+ac_save_CC=$CC
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+$ac_c_conftest_c89_program
+_ACEOF
+for ac_arg in '' -qlanglvl=extc89 -qlanglvl=ansi -std -Ae "-Aa -D_HPUX_SOURCE" "-Xc -D__EXTENSIONS__"
+do
+  CC="$ac_save_CC $ac_arg"
+  if ac_fn_c_try_compile "$LINENO"
+then :
+  ac_cv_prog_cc_c89=$ac_arg
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam
+  test "x$ac_cv_prog_cc_c89" != "xno" && break
+done
+rm -f conftest.$ac_ext
+CC=$ac_save_CC
+fi
+
+if test "x$ac_cv_prog_cc_c89" = xno
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5
+printf "%s\n" "unsupported" >&6; }
+else $as_nop
+  if test "x$ac_cv_prog_cc_c89" = x
+then :
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5
+printf "%s\n" "none needed" >&6; }
+else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5
+printf "%s\n" "$ac_cv_prog_cc_c89" >&6; }
+     CC="$CC $ac_cv_prog_cc_c89"
+fi
+  ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c89
+  ac_prog_cc_stdc=c89
+fi
+fi
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
+  ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC understands -c and -o together" >&5
+printf %s "checking whether $CC understands -c and -o together... " >&6; }
+if test ${am_cv_prog_cc_c_o+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+  # Make sure it works both with $CC and with simple cc.
+  # Following AC_PROG_CC_C_O, we do the test twice because some
+  # compilers refuse to overwrite an existing .o file with -o,
+  # though they will create one.
+  am_cv_prog_cc_c_o=yes
+  for am_i in 1 2; do
+    if { echo "$as_me:$LINENO: $CC -c conftest.$ac_ext -o conftest2.$ac_objext" >&5
+   ($CC -c conftest.$ac_ext -o conftest2.$ac_objext) >&5 2>&5
+   ac_status=$?
+   echo "$as_me:$LINENO: \$? = $ac_status" >&5
+   (exit $ac_status); } \
+         && test -f conftest2.$ac_objext; then
+      : OK
+    else
+      am_cv_prog_cc_c_o=no
+      break
+    fi
+  done
+  rm -f core conftest*
+  unset am_i
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_prog_cc_c_o" >&5
+printf "%s\n" "$am_cv_prog_cc_c_o" >&6; }
+if test "$am_cv_prog_cc_c_o" != yes; then
+   # Losing compiler, so override with the script.
+   # FIXME: It is wrong to rewrite CC.
+   # But if we don't then we get into trouble of one sort or another.
+   # A longer-term fix would be to have automake use am__CC in this case,
+   # and then we could set am__CC="\$(top_srcdir)/compile \$(CC)"
+   CC="$am_aux_dir/compile $CC"
+fi
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+DEPDIR="${am__leading_dot}deps"
+
+ac_config_commands="$ac_config_commands depfiles"
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} supports the include directive" >&5
+printf %s "checking whether ${MAKE-make} supports the include directive... " >&6; }
+cat > confinc.mk << 'END'
+am__doit:
+	@echo this is the am__doit target >confinc.out
+.PHONY: am__doit
+END
+am__include="#"
+am__quote=
+# BSD make does it like this.
+echo '.include "confinc.mk" # ignored' > confmf.BSD
+# Other make implementations (GNU, Solaris 10, AIX) do it like this.
+echo 'include confinc.mk # ignored' > confmf.GNU
+_am_result=no
+for s in GNU BSD; do
+  { echo "$as_me:$LINENO: ${MAKE-make} -f confmf.$s && cat confinc.out" >&5
+   (${MAKE-make} -f confmf.$s && cat confinc.out) >&5 2>&5
+   ac_status=$?
+   echo "$as_me:$LINENO: \$? = $ac_status" >&5
+   (exit $ac_status); }
+  case $?:`cat confinc.out 2>/dev/null` in #(
+  '0:this is the am__doit target') :
+    case $s in #(
+  BSD) :
+    am__include='.include' am__quote='"' ;; #(
+  *) :
+    am__include='include' am__quote='' ;;
+esac ;; #(
+  *) :
+     ;;
+esac
+  if test "$am__include" != "#"; then
+    _am_result="yes ($s style)"
+    break
+  fi
+done
+rm -f confinc.* confmf.*
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${_am_result}" >&5
+printf "%s\n" "${_am_result}" >&6; }
+
+# Check whether --enable-dependency-tracking was given.
+if test ${enable_dependency_tracking+y}
+then :
+  enableval=$enable_dependency_tracking;
+fi
+
+if test "x$enable_dependency_tracking" != xno; then
+  am_depcomp="$ac_aux_dir/depcomp"
+  AMDEPBACKSLASH='\'
+  am__nodep='_no'
+fi
+ if test "x$enable_dependency_tracking" != xno; then
+  AMDEP_TRUE=
+  AMDEP_FALSE='#'
+else
+  AMDEP_TRUE='#'
+  AMDEP_FALSE=
+fi
+
+
+
+depcc="$CC"   am_compiler_list=
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking dependency style of $depcc" >&5
+printf %s "checking dependency style of $depcc... " >&6; }
+if test ${am_cv_CC_dependencies_compiler_type+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -z "$AMDEP_TRUE" && test -f "$am_depcomp"; then
+  # We make a subdir and do the tests there.  Otherwise we can end up
+  # making bogus files that we don't know about and never remove.  For
+  # instance it was reported that on HP-UX the gcc test will end up
+  # making a dummy file named 'D' -- because '-MD' means "put the output
+  # in D".
+  rm -rf conftest.dir
+  mkdir conftest.dir
+  # Copy depcomp to subdir because otherwise we won't find it if we're
+  # using a relative directory.
+  cp "$am_depcomp" conftest.dir
+  cd conftest.dir
+  # We will build objects and dependencies in a subdirectory because
+  # it helps to detect inapplicable dependency modes.  For instance
+  # both Tru64's cc and ICC support -MD to output dependencies as a
+  # side effect of compilation, but ICC will put the dependencies in
+  # the current directory while Tru64 will put them in the object
+  # directory.
+  mkdir sub
+
+  am_cv_CC_dependencies_compiler_type=none
+  if test "$am_compiler_list" = ""; then
+     am_compiler_list=`sed -n 's/^#*\([a-zA-Z0-9]*\))$/\1/p' < ./depcomp`
+  fi
+  am__universal=false
+  case " $depcc " in #(
+     *\ -arch\ *\ -arch\ *) am__universal=true ;;
+     esac
+
+  for depmode in $am_compiler_list; do
+    # Setup a source with many dependencies, because some compilers
+    # like to wrap large dependency lists on column 80 (with \), and
+    # we should not choose a depcomp mode which is confused by this.
+    #
+    # We need to recreate these files for each test, as the compiler may
+    # overwrite some of them when testing with obscure command lines.
+    # This happens at least with the AIX C compiler.
+    : > sub/conftest.c
+    for i in 1 2 3 4 5 6; do
+      echo '#include "conftst'$i'.h"' >> sub/conftest.c
+      # Using ": > sub/conftst$i.h" creates only sub/conftst1.h with
+      # Solaris 10 /bin/sh.
+      echo '/* dummy */' > sub/conftst$i.h
+    done
+    echo "${am__include} ${am__quote}sub/conftest.Po${am__quote}" > confmf
+
+    # We check with '-c' and '-o' for the sake of the "dashmstdout"
+    # mode.  It turns out that the SunPro C++ compiler does not properly
+    # handle '-M -o', and we need to detect this.  Also, some Intel
+    # versions had trouble with output in subdirs.
+    am__obj=sub/conftest.${OBJEXT-o}
+    am__minus_obj="-o $am__obj"
+    case $depmode in
+    gcc)
+      # This depmode causes a compiler race in universal mode.
+      test "$am__universal" = false || continue
+      ;;
+    nosideeffect)
+      # After this tag, mechanisms are not by side-effect, so they'll
+      # only be used when explicitly requested.
+      if test "x$enable_dependency_tracking" = xyes; then
+	continue
+      else
+	break
+      fi
+      ;;
+    msvc7 | msvc7msys | msvisualcpp | msvcmsys)
+      # This compiler won't grok '-c -o', but also, the minuso test has
+      # not run yet.  These depmodes are late enough in the game, and
+      # so weak that their functioning should not be impacted.
+      am__obj=conftest.${OBJEXT-o}
+      am__minus_obj=
+      ;;
+    none) break ;;
+    esac
+    if depmode=$depmode \
+       source=sub/conftest.c object=$am__obj \
+       depfile=sub/conftest.Po tmpdepfile=sub/conftest.TPo \
+       $SHELL ./depcomp $depcc -c $am__minus_obj sub/conftest.c \
+         >/dev/null 2>conftest.err &&
+       grep sub/conftst1.h sub/conftest.Po > /dev/null 2>&1 &&
+       grep sub/conftst6.h sub/conftest.Po > /dev/null 2>&1 &&
+       grep $am__obj sub/conftest.Po > /dev/null 2>&1 &&
+       ${MAKE-make} -s -f confmf > /dev/null 2>&1; then
+      # icc doesn't choke on unknown options, it will just issue warnings
+      # or remarks (even with -Werror).  So we grep stderr for any message
+      # that says an option was ignored or not supported.
+      # When given -MP, icc 7.0 and 7.1 complain thusly:
+      #   icc: Command line warning: ignoring option '-M'; no argument required
+      # The diagnosis changed in icc 8.0:
+      #   icc: Command line remark: option '-MP' not supported
+      if (grep 'ignoring option' conftest.err ||
+          grep 'not supported' conftest.err) >/dev/null 2>&1; then :; else
+        am_cv_CC_dependencies_compiler_type=$depmode
+        break
+      fi
+    fi
+  done
+
+  cd ..
+  rm -rf conftest.dir
+else
+  am_cv_CC_dependencies_compiler_type=none
+fi
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_CC_dependencies_compiler_type" >&5
+printf "%s\n" "$am_cv_CC_dependencies_compiler_type" >&6; }
+CCDEPMODE=depmode=$am_cv_CC_dependencies_compiler_type
+
+ if
+  test "x$enable_dependency_tracking" != xno \
+  && test "$am_cv_CC_dependencies_compiler_type" = gcc3; then
+  am__fastdepCC_TRUE=
+  am__fastdepCC_FALSE='#'
+else
+  am__fastdepCC_TRUE='#'
+  am__fastdepCC_FALSE=
+fi
+
+
+# AC_PROG_CXX
+
+
+# AM_PROG_AR is supported and needed since automake v1.12+
+
+
+  if test -n "$ac_tool_prefix"; then
+  for ac_prog in ar lib "link -lib"
+  do
+    # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args.
+set dummy $ac_tool_prefix$ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_AR+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$AR"; then
+  ac_cv_prog_AR="$AR" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_AR="$ac_tool_prefix$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+AR=$ac_cv_prog_AR
+if test -n "$AR"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $AR" >&5
+printf "%s\n" "$AR" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+    test -n "$AR" && break
+  done
+fi
+if test -z "$AR"; then
+  ac_ct_AR=$AR
+  for ac_prog in ar lib "link -lib"
+do
+  # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_AR+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_AR"; then
+  ac_cv_prog_ac_ct_AR="$ac_ct_AR" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_AR="$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_AR=$ac_cv_prog_ac_ct_AR
+if test -n "$ac_ct_AR"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_AR" >&5
+printf "%s\n" "$ac_ct_AR" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+  test -n "$ac_ct_AR" && break
+done
+
+  if test "x$ac_ct_AR" = x; then
+    AR="false"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    AR=$ac_ct_AR
+  fi
+fi
+
+: ${AR=ar}
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking the archiver ($AR) interface" >&5
+printf %s "checking the archiver ($AR) interface... " >&6; }
+if test ${am_cv_ar_interface+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+   am_cv_ar_interface=ar
+   cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+int some_variable = 0;
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+  am_ar_try='$AR cru libconftest.a conftest.$ac_objext >&5'
+      { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$am_ar_try\""; } >&5
+  (eval $am_ar_try) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+      if test "$ac_status" -eq 0; then
+        am_cv_ar_interface=ar
+      else
+        am_ar_try='$AR -NOLOGO -OUT:conftest.lib conftest.$ac_objext >&5'
+        { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$am_ar_try\""; } >&5
+  (eval $am_ar_try) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+        if test "$ac_status" -eq 0; then
+          am_cv_ar_interface=lib
+        else
+          am_cv_ar_interface=unknown
+        fi
+      fi
+      rm -f conftest.lib libconftest.a
+
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+   ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_ar_interface" >&5
+printf "%s\n" "$am_cv_ar_interface" >&6; }
+
+case $am_cv_ar_interface in
+ar)
+  ;;
+lib)
+  # Microsoft lib, so override with the ar-lib wrapper script.
+  # FIXME: It is wrong to rewrite AR.
+  # But if we don't then we get into trouble of one sort or another.
+  # A longer-term fix would be to have automake use am__AR in this case,
+  # and then we could set am__AR="$am_aux_dir/ar-lib \$(AR)" or something
+  # similar.
+  AR="$am_aux_dir/ar-lib $AR"
+  ;;
+unknown)
+  as_fn_error $? "could not determine $AR interface" "$LINENO" 5
+  ;;
+esac
+
+
+# Adding libtools to the build seems to bring in C++ environment
+case `pwd` in
+  *\ * | *\	*)
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Libtool does not cope well with whitespace in \`pwd\`" >&5
+printf "%s\n" "$as_me: WARNING: Libtool does not cope well with whitespace in \`pwd\`" >&2;} ;;
+esac
+
+
+
+macro_version='2.4.7'
+macro_revision='2.4.7'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ltmain=$ac_aux_dir/ltmain.sh
+
+
+
+  # Make sure we can run config.sub.
+$SHELL "${ac_aux_dir}config.sub" sun4 >/dev/null 2>&1 ||
+  as_fn_error $? "cannot run $SHELL ${ac_aux_dir}config.sub" "$LINENO" 5
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking build system type" >&5
+printf %s "checking build system type... " >&6; }
+if test ${ac_cv_build+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_build_alias=$build_alias
+test "x$ac_build_alias" = x &&
+  ac_build_alias=`$SHELL "${ac_aux_dir}config.guess"`
+test "x$ac_build_alias" = x &&
+  as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5
+ac_cv_build=`$SHELL "${ac_aux_dir}config.sub" $ac_build_alias` ||
+  as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $ac_build_alias failed" "$LINENO" 5
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5
+printf "%s\n" "$ac_cv_build" >&6; }
+case $ac_cv_build in
+*-*-*) ;;
+*) as_fn_error $? "invalid value of canonical build" "$LINENO" 5;;
+esac
+build=$ac_cv_build
+ac_save_IFS=$IFS; IFS='-'
+set x $ac_cv_build
+shift
+build_cpu=$1
+build_vendor=$2
+shift; shift
+# Remember, the first character of IFS is used to create $*,
+# except with old shells:
+build_os=$*
+IFS=$ac_save_IFS
+case $build_os in *\ *) build_os=`echo "$build_os" | sed 's/ /-/g'`;; esac
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking host system type" >&5
+printf %s "checking host system type... " >&6; }
+if test ${ac_cv_host+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test "x$host_alias" = x; then
+  ac_cv_host=$ac_cv_build
+else
+  ac_cv_host=`$SHELL "${ac_aux_dir}config.sub" $host_alias` ||
+    as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $host_alias failed" "$LINENO" 5
+fi
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5
+printf "%s\n" "$ac_cv_host" >&6; }
+case $ac_cv_host in
+*-*-*) ;;
+*) as_fn_error $? "invalid value of canonical host" "$LINENO" 5;;
+esac
+host=$ac_cv_host
+ac_save_IFS=$IFS; IFS='-'
+set x $ac_cv_host
+shift
+host_cpu=$1
+host_vendor=$2
+shift; shift
+# Remember, the first character of IFS is used to create $*,
+# except with old shells:
+host_os=$*
+IFS=$ac_save_IFS
+case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac
+
+
+# Backslashify metacharacters that are still active within
+# double-quoted strings.
+sed_quote_subst='s/\(["`$\\]\)/\\\1/g'
+
+# Same as above, but do not quote variable references.
+double_quote_subst='s/\(["`\\]\)/\\\1/g'
+
+# Sed substitution to delay expansion of an escaped shell variable in a
+# double_quote_subst'ed string.
+delay_variable_subst='s/\\\\\\\\\\\$/\\\\\\$/g'
+
+# Sed substitution to delay expansion of an escaped single quote.
+delay_single_quote_subst='s/'\''/'\'\\\\\\\'\''/g'
+
+# Sed substitution to avoid accidental globbing in evaled expressions
+no_glob_subst='s/\*/\\\*/g'
+
+ECHO='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'
+ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO
+ECHO=$ECHO$ECHO$ECHO$ECHO$ECHO$ECHO
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking how to print strings" >&5
+printf %s "checking how to print strings... " >&6; }
+# Test print first, because it will be a builtin if present.
+if test "X`( print -r -- -n ) 2>/dev/null`" = X-n && \
+   test "X`print -r -- $ECHO 2>/dev/null`" = "X$ECHO"; then
+  ECHO='print -r --'
+elif test "X`printf %s $ECHO 2>/dev/null`" = "X$ECHO"; then
+  ECHO='printf %s\n'
+else
+  # Use this function as a fallback that always works.
+  func_fallback_echo ()
+  {
+    eval 'cat <<_LTECHO_EOF
+$1
+_LTECHO_EOF'
+  }
+  ECHO='func_fallback_echo'
+fi
+
+# func_echo_all arg...
+# Invoke $ECHO with all args, space-separated.
+func_echo_all ()
+{
+    $ECHO ""
+}
+
+case $ECHO in
+  printf*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: printf" >&5
+printf "%s\n" "printf" >&6; } ;;
+  print*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: print -r" >&5
+printf "%s\n" "print -r" >&6; } ;;
+  *) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: cat" >&5
+printf "%s\n" "cat" >&6; } ;;
+esac
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a sed that does not truncate output" >&5
+printf %s "checking for a sed that does not truncate output... " >&6; }
+if test ${ac_cv_path_SED+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+            ac_script=s/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/
+     for ac_i in 1 2 3 4 5 6 7; do
+       ac_script="$ac_script$as_nl$ac_script"
+     done
+     echo "$ac_script" 2>/dev/null | sed 99q >conftest.sed
+     { ac_script=; unset ac_script;}
+     if test -z "$SED"; then
+  ac_path_SED_found=false
+  # Loop through the user's path and test for each of PROGNAME-LIST
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_prog in sed gsed
+   do
+    for ac_exec_ext in '' $ac_executable_extensions; do
+      ac_path_SED="$as_dir$ac_prog$ac_exec_ext"
+      as_fn_executable_p "$ac_path_SED" || continue
+# Check for GNU ac_path_SED and select it if it is found.
+  # Check for GNU $ac_path_SED
+case `"$ac_path_SED" --version 2>&1` in
+*GNU*)
+  ac_cv_path_SED="$ac_path_SED" ac_path_SED_found=:;;
+*)
+  ac_count=0
+  printf %s 0123456789 >"conftest.in"
+  while :
+  do
+    cat "conftest.in" "conftest.in" >"conftest.tmp"
+    mv "conftest.tmp" "conftest.in"
+    cp "conftest.in" "conftest.nl"
+    printf "%s\n" '' >> "conftest.nl"
+    "$ac_path_SED" -f conftest.sed < "conftest.nl" >"conftest.out" 2>/dev/null || break
+    diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break
+    as_fn_arith $ac_count + 1 && ac_count=$as_val
+    if test $ac_count -gt ${ac_path_SED_max-0}; then
+      # Best one so far, save it but keep looking for a better one
+      ac_cv_path_SED="$ac_path_SED"
+      ac_path_SED_max=$ac_count
+    fi
+    # 10*(2^10) chars as input seems more than enough
+    test $ac_count -gt 10 && break
+  done
+  rm -f conftest.in conftest.tmp conftest.nl conftest.out;;
+esac
+
+      $ac_path_SED_found && break 3
+    done
+  done
+  done
+IFS=$as_save_IFS
+  if test -z "$ac_cv_path_SED"; then
+    as_fn_error $? "no acceptable sed could be found in \$PATH" "$LINENO" 5
+  fi
+else
+  ac_cv_path_SED=$SED
+fi
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_SED" >&5
+printf "%s\n" "$ac_cv_path_SED" >&6; }
+ SED="$ac_cv_path_SED"
+  rm -f conftest.sed
+
+test -z "$SED" && SED=sed
+Xsed="$SED -e 1s/^X//"
+
+
+
+
+
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for grep that handles long lines and -e" >&5
+printf %s "checking for grep that handles long lines and -e... " >&6; }
+if test ${ac_cv_path_GREP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -z "$GREP"; then
+  ac_path_GREP_found=false
+  # Loop through the user's path and test for each of PROGNAME-LIST
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_prog in grep ggrep
+   do
+    for ac_exec_ext in '' $ac_executable_extensions; do
+      ac_path_GREP="$as_dir$ac_prog$ac_exec_ext"
+      as_fn_executable_p "$ac_path_GREP" || continue
+# Check for GNU ac_path_GREP and select it if it is found.
+  # Check for GNU $ac_path_GREP
+case `"$ac_path_GREP" --version 2>&1` in
+*GNU*)
+  ac_cv_path_GREP="$ac_path_GREP" ac_path_GREP_found=:;;
+*)
+  ac_count=0
+  printf %s 0123456789 >"conftest.in"
+  while :
+  do
+    cat "conftest.in" "conftest.in" >"conftest.tmp"
+    mv "conftest.tmp" "conftest.in"
+    cp "conftest.in" "conftest.nl"
+    printf "%s\n" 'GREP' >> "conftest.nl"
+    "$ac_path_GREP" -e 'GREP$' -e '-(cannot match)-' < "conftest.nl" >"conftest.out" 2>/dev/null || break
+    diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break
+    as_fn_arith $ac_count + 1 && ac_count=$as_val
+    if test $ac_count -gt ${ac_path_GREP_max-0}; then
+      # Best one so far, save it but keep looking for a better one
+      ac_cv_path_GREP="$ac_path_GREP"
+      ac_path_GREP_max=$ac_count
+    fi
+    # 10*(2^10) chars as input seems more than enough
+    test $ac_count -gt 10 && break
+  done
+  rm -f conftest.in conftest.tmp conftest.nl conftest.out;;
+esac
+
+      $ac_path_GREP_found && break 3
+    done
+  done
+  done
+IFS=$as_save_IFS
+  if test -z "$ac_cv_path_GREP"; then
+    as_fn_error $? "no acceptable grep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5
+  fi
+else
+  ac_cv_path_GREP=$GREP
+fi
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_GREP" >&5
+printf "%s\n" "$ac_cv_path_GREP" >&6; }
+ GREP="$ac_cv_path_GREP"
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for egrep" >&5
+printf %s "checking for egrep... " >&6; }
+if test ${ac_cv_path_EGREP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if echo a | $GREP -E '(a|b)' >/dev/null 2>&1
+   then ac_cv_path_EGREP="$GREP -E"
+   else
+     if test -z "$EGREP"; then
+  ac_path_EGREP_found=false
+  # Loop through the user's path and test for each of PROGNAME-LIST
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_prog in egrep
+   do
+    for ac_exec_ext in '' $ac_executable_extensions; do
+      ac_path_EGREP="$as_dir$ac_prog$ac_exec_ext"
+      as_fn_executable_p "$ac_path_EGREP" || continue
+# Check for GNU ac_path_EGREP and select it if it is found.
+  # Check for GNU $ac_path_EGREP
+case `"$ac_path_EGREP" --version 2>&1` in
+*GNU*)
+  ac_cv_path_EGREP="$ac_path_EGREP" ac_path_EGREP_found=:;;
+*)
+  ac_count=0
+  printf %s 0123456789 >"conftest.in"
+  while :
+  do
+    cat "conftest.in" "conftest.in" >"conftest.tmp"
+    mv "conftest.tmp" "conftest.in"
+    cp "conftest.in" "conftest.nl"
+    printf "%s\n" 'EGREP' >> "conftest.nl"
+    "$ac_path_EGREP" 'EGREP$' < "conftest.nl" >"conftest.out" 2>/dev/null || break
+    diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break
+    as_fn_arith $ac_count + 1 && ac_count=$as_val
+    if test $ac_count -gt ${ac_path_EGREP_max-0}; then
+      # Best one so far, save it but keep looking for a better one
+      ac_cv_path_EGREP="$ac_path_EGREP"
+      ac_path_EGREP_max=$ac_count
+    fi
+    # 10*(2^10) chars as input seems more than enough
+    test $ac_count -gt 10 && break
+  done
+  rm -f conftest.in conftest.tmp conftest.nl conftest.out;;
+esac
+
+      $ac_path_EGREP_found && break 3
+    done
+  done
+  done
+IFS=$as_save_IFS
+  if test -z "$ac_cv_path_EGREP"; then
+    as_fn_error $? "no acceptable egrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5
+  fi
+else
+  ac_cv_path_EGREP=$EGREP
+fi
+
+   fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP" >&5
+printf "%s\n" "$ac_cv_path_EGREP" >&6; }
+ EGREP="$ac_cv_path_EGREP"
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for fgrep" >&5
+printf %s "checking for fgrep... " >&6; }
+if test ${ac_cv_path_FGREP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if echo 'ab*c' | $GREP -F 'ab*c' >/dev/null 2>&1
+   then ac_cv_path_FGREP="$GREP -F"
+   else
+     if test -z "$FGREP"; then
+  ac_path_FGREP_found=false
+  # Loop through the user's path and test for each of PROGNAME-LIST
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_prog in fgrep
+   do
+    for ac_exec_ext in '' $ac_executable_extensions; do
+      ac_path_FGREP="$as_dir$ac_prog$ac_exec_ext"
+      as_fn_executable_p "$ac_path_FGREP" || continue
+# Check for GNU ac_path_FGREP and select it if it is found.
+  # Check for GNU $ac_path_FGREP
+case `"$ac_path_FGREP" --version 2>&1` in
+*GNU*)
+  ac_cv_path_FGREP="$ac_path_FGREP" ac_path_FGREP_found=:;;
+*)
+  ac_count=0
+  printf %s 0123456789 >"conftest.in"
+  while :
+  do
+    cat "conftest.in" "conftest.in" >"conftest.tmp"
+    mv "conftest.tmp" "conftest.in"
+    cp "conftest.in" "conftest.nl"
+    printf "%s\n" 'FGREP' >> "conftest.nl"
+    "$ac_path_FGREP" FGREP < "conftest.nl" >"conftest.out" 2>/dev/null || break
+    diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break
+    as_fn_arith $ac_count + 1 && ac_count=$as_val
+    if test $ac_count -gt ${ac_path_FGREP_max-0}; then
+      # Best one so far, save it but keep looking for a better one
+      ac_cv_path_FGREP="$ac_path_FGREP"
+      ac_path_FGREP_max=$ac_count
+    fi
+    # 10*(2^10) chars as input seems more than enough
+    test $ac_count -gt 10 && break
+  done
+  rm -f conftest.in conftest.tmp conftest.nl conftest.out;;
+esac
+
+      $ac_path_FGREP_found && break 3
+    done
+  done
+  done
+IFS=$as_save_IFS
+  if test -z "$ac_cv_path_FGREP"; then
+    as_fn_error $? "no acceptable fgrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5
+  fi
+else
+  ac_cv_path_FGREP=$FGREP
+fi
+
+   fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_FGREP" >&5
+printf "%s\n" "$ac_cv_path_FGREP" >&6; }
+ FGREP="$ac_cv_path_FGREP"
+
+
+test -z "$GREP" && GREP=grep
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# Check whether --with-gnu-ld was given.
+if test ${with_gnu_ld+y}
+then :
+  withval=$with_gnu_ld; test no = "$withval" || with_gnu_ld=yes
+else $as_nop
+  with_gnu_ld=no
+fi
+
+ac_prog=ld
+if test yes = "$GCC"; then
+  # Check if gcc -print-prog-name=ld gives a path.
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ld used by $CC" >&5
+printf %s "checking for ld used by $CC... " >&6; }
+  case $host in
+  *-*-mingw*)
+    # gcc leaves a trailing carriage return, which upsets mingw
+    ac_prog=`($CC -print-prog-name=ld) 2>&5 | tr -d '\015'` ;;
+  *)
+    ac_prog=`($CC -print-prog-name=ld) 2>&5` ;;
+  esac
+  case $ac_prog in
+    # Accept absolute paths.
+    [\\/]* | ?:[\\/]*)
+      re_direlt='/[^/][^/]*/\.\./'
+      # Canonicalize the pathname of ld
+      ac_prog=`$ECHO "$ac_prog"| $SED 's%\\\\%/%g'`
+      while $ECHO "$ac_prog" | $GREP "$re_direlt" > /dev/null 2>&1; do
+	ac_prog=`$ECHO $ac_prog| $SED "s%$re_direlt%/%"`
+      done
+      test -z "$LD" && LD=$ac_prog
+      ;;
+  "")
+    # If it fails, then pretend we aren't using GCC.
+    ac_prog=ld
+    ;;
+  *)
+    # If it is relative, then search for the first ld in PATH.
+    with_gnu_ld=unknown
+    ;;
+  esac
+elif test yes = "$with_gnu_ld"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GNU ld" >&5
+printf %s "checking for GNU ld... " >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for non-GNU ld" >&5
+printf %s "checking for non-GNU ld... " >&6; }
+fi
+if test ${lt_cv_path_LD+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -z "$LD"; then
+  lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR
+  for ac_dir in $PATH; do
+    IFS=$lt_save_ifs
+    test -z "$ac_dir" && ac_dir=.
+    if test -f "$ac_dir/$ac_prog" || test -f "$ac_dir/$ac_prog$ac_exeext"; then
+      lt_cv_path_LD=$ac_dir/$ac_prog
+      # Check to see if the program is GNU ld.  I'd rather use --version,
+      # but apparently some variants of GNU ld only accept -v.
+      # Break only if it was the GNU/non-GNU ld that we prefer.
+      case `"$lt_cv_path_LD" -v 2>&1 </dev/null` in
+      *GNU* | *'with BFD'*)
+	test no != "$with_gnu_ld" && break
+	;;
+      *)
+	test yes != "$with_gnu_ld" && break
+	;;
+      esac
+    fi
+  done
+  IFS=$lt_save_ifs
+else
+  lt_cv_path_LD=$LD # Let the user override the test with a path.
+fi
+fi
+
+LD=$lt_cv_path_LD
+if test -n "$LD"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $LD" >&5
+printf "%s\n" "$LD" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+test -z "$LD" && as_fn_error $? "no acceptable ld found in \$PATH" "$LINENO" 5
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if the linker ($LD) is GNU ld" >&5
+printf %s "checking if the linker ($LD) is GNU ld... " >&6; }
+if test ${lt_cv_prog_gnu_ld+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  # I'd rather use --version here, but apparently some GNU lds only accept -v.
+case `$LD -v 2>&1 </dev/null` in
+*GNU* | *'with BFD'*)
+  lt_cv_prog_gnu_ld=yes
+  ;;
+*)
+  lt_cv_prog_gnu_ld=no
+  ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_gnu_ld" >&5
+printf "%s\n" "$lt_cv_prog_gnu_ld" >&6; }
+with_gnu_ld=$lt_cv_prog_gnu_ld
+
+
+
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for BSD- or MS-compatible name lister (nm)" >&5
+printf %s "checking for BSD- or MS-compatible name lister (nm)... " >&6; }
+if test ${lt_cv_path_NM+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$NM"; then
+  # Let the user override the test.
+  lt_cv_path_NM=$NM
+else
+  lt_nm_to_check=${ac_tool_prefix}nm
+  if test -n "$ac_tool_prefix" && test "$build" = "$host"; then
+    lt_nm_to_check="$lt_nm_to_check nm"
+  fi
+  for lt_tmp_nm in $lt_nm_to_check; do
+    lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR
+    for ac_dir in $PATH /usr/ccs/bin/elf /usr/ccs/bin /usr/ucb /bin; do
+      IFS=$lt_save_ifs
+      test -z "$ac_dir" && ac_dir=.
+      tmp_nm=$ac_dir/$lt_tmp_nm
+      if test -f "$tmp_nm" || test -f "$tmp_nm$ac_exeext"; then
+	# Check to see if the nm accepts a BSD-compat flag.
+	# Adding the 'sed 1q' prevents false positives on HP-UX, which says:
+	#   nm: unknown option "B" ignored
+	# Tru64's nm complains that /dev/null is an invalid object file
+	# MSYS converts /dev/null to NUL, MinGW nm treats NUL as empty
+	case $build_os in
+	mingw*) lt_bad_file=conftest.nm/nofile ;;
+	*) lt_bad_file=/dev/null ;;
+	esac
+	case `"$tmp_nm" -B $lt_bad_file 2>&1 | $SED '1q'` in
+	*$lt_bad_file* | *'Invalid file or object type'*)
+	  lt_cv_path_NM="$tmp_nm -B"
+	  break 2
+	  ;;
+	*)
+	  case `"$tmp_nm" -p /dev/null 2>&1 | $SED '1q'` in
+	  */dev/null*)
+	    lt_cv_path_NM="$tmp_nm -p"
+	    break 2
+	    ;;
+	  *)
+	    lt_cv_path_NM=${lt_cv_path_NM="$tmp_nm"} # keep the first match, but
+	    continue # so that we can try to find one that supports BSD flags
+	    ;;
+	  esac
+	  ;;
+	esac
+      fi
+    done
+    IFS=$lt_save_ifs
+  done
+  : ${lt_cv_path_NM=no}
+fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_path_NM" >&5
+printf "%s\n" "$lt_cv_path_NM" >&6; }
+if test no != "$lt_cv_path_NM"; then
+  NM=$lt_cv_path_NM
+else
+  # Didn't find any BSD compatible name lister, look for dumpbin.
+  if test -n "$DUMPBIN"; then :
+    # Let the user override the test.
+  else
+    if test -n "$ac_tool_prefix"; then
+  for ac_prog in dumpbin "link -dump"
+  do
+    # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args.
+set dummy $ac_tool_prefix$ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_DUMPBIN+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$DUMPBIN"; then
+  ac_cv_prog_DUMPBIN="$DUMPBIN" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_DUMPBIN="$ac_tool_prefix$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+DUMPBIN=$ac_cv_prog_DUMPBIN
+if test -n "$DUMPBIN"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $DUMPBIN" >&5
+printf "%s\n" "$DUMPBIN" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+    test -n "$DUMPBIN" && break
+  done
+fi
+if test -z "$DUMPBIN"; then
+  ac_ct_DUMPBIN=$DUMPBIN
+  for ac_prog in dumpbin "link -dump"
+do
+  # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_DUMPBIN+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_DUMPBIN"; then
+  ac_cv_prog_ac_ct_DUMPBIN="$ac_ct_DUMPBIN" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_DUMPBIN="$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_DUMPBIN=$ac_cv_prog_ac_ct_DUMPBIN
+if test -n "$ac_ct_DUMPBIN"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_DUMPBIN" >&5
+printf "%s\n" "$ac_ct_DUMPBIN" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+  test -n "$ac_ct_DUMPBIN" && break
+done
+
+  if test "x$ac_ct_DUMPBIN" = x; then
+    DUMPBIN=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    DUMPBIN=$ac_ct_DUMPBIN
+  fi
+fi
+
+    case `$DUMPBIN -symbols -headers /dev/null 2>&1 | $SED '1q'` in
+    *COFF*)
+      DUMPBIN="$DUMPBIN -symbols -headers"
+      ;;
+    *)
+      DUMPBIN=:
+      ;;
+    esac
+  fi
+
+  if test : != "$DUMPBIN"; then
+    NM=$DUMPBIN
+  fi
+fi
+test -z "$NM" && NM=nm
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking the name lister ($NM) interface" >&5
+printf %s "checking the name lister ($NM) interface... " >&6; }
+if test ${lt_cv_nm_interface+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_nm_interface="BSD nm"
+  echo "int some_variable = 0;" > conftest.$ac_ext
+  (eval echo "\"\$as_me:$LINENO: $ac_compile\"" >&5)
+  (eval "$ac_compile" 2>conftest.err)
+  cat conftest.err >&5
+  (eval echo "\"\$as_me:$LINENO: $NM \\\"conftest.$ac_objext\\\"\"" >&5)
+  (eval "$NM \"conftest.$ac_objext\"" 2>conftest.err > conftest.out)
+  cat conftest.err >&5
+  (eval echo "\"\$as_me:$LINENO: output\"" >&5)
+  cat conftest.out >&5
+  if $GREP 'External.*some_variable' conftest.out > /dev/null; then
+    lt_cv_nm_interface="MS dumpbin"
+  fi
+  rm -f conftest*
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_nm_interface" >&5
+printf "%s\n" "$lt_cv_nm_interface" >&6; }
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ln -s works" >&5
+printf %s "checking whether ln -s works... " >&6; }
+LN_S=$as_ln_s
+if test "$LN_S" = "ln -s"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no, using $LN_S" >&5
+printf "%s\n" "no, using $LN_S" >&6; }
+fi
+
+# find the maximum length of command line arguments
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking the maximum length of command line arguments" >&5
+printf %s "checking the maximum length of command line arguments... " >&6; }
+if test ${lt_cv_sys_max_cmd_len+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+    i=0
+  teststring=ABCD
+
+  case $build_os in
+  msdosdjgpp*)
+    # On DJGPP, this test can blow up pretty badly due to problems in libc
+    # (any single argument exceeding 2000 bytes causes a buffer overrun
+    # during glob expansion).  Even if it were fixed, the result of this
+    # check would be larger than it should be.
+    lt_cv_sys_max_cmd_len=12288;    # 12K is about right
+    ;;
+
+  gnu*)
+    # Under GNU Hurd, this test is not required because there is
+    # no limit to the length of command line arguments.
+    # Libtool will interpret -1 as no limit whatsoever
+    lt_cv_sys_max_cmd_len=-1;
+    ;;
+
+  cygwin* | mingw* | cegcc*)
+    # On Win9x/ME, this test blows up -- it succeeds, but takes
+    # about 5 minutes as the teststring grows exponentially.
+    # Worse, since 9x/ME are not pre-emptively multitasking,
+    # you end up with a "frozen" computer, even though with patience
+    # the test eventually succeeds (with a max line length of 256k).
+    # Instead, let's just punt: use the minimum linelength reported by
+    # all of the supported platforms: 8192 (on NT/2K/XP).
+    lt_cv_sys_max_cmd_len=8192;
+    ;;
+
+  mint*)
+    # On MiNT this can take a long time and run out of memory.
+    lt_cv_sys_max_cmd_len=8192;
+    ;;
+
+  amigaos*)
+    # On AmigaOS with pdksh, this test takes hours, literally.
+    # So we just punt and use a minimum line length of 8192.
+    lt_cv_sys_max_cmd_len=8192;
+    ;;
+
+  bitrig* | darwin* | dragonfly* | freebsd* | midnightbsd* | netbsd* | openbsd*)
+    # This has been around since 386BSD, at least.  Likely further.
+    if test -x /sbin/sysctl; then
+      lt_cv_sys_max_cmd_len=`/sbin/sysctl -n kern.argmax`
+    elif test -x /usr/sbin/sysctl; then
+      lt_cv_sys_max_cmd_len=`/usr/sbin/sysctl -n kern.argmax`
+    else
+      lt_cv_sys_max_cmd_len=65536	# usable default for all BSDs
+    fi
+    # And add a safety zone
+    lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 4`
+    lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \* 3`
+    ;;
+
+  interix*)
+    # We know the value 262144 and hardcode it with a safety zone (like BSD)
+    lt_cv_sys_max_cmd_len=196608
+    ;;
+
+  os2*)
+    # The test takes a long time on OS/2.
+    lt_cv_sys_max_cmd_len=8192
+    ;;
+
+  osf*)
+    # Dr. Hans Ekkehard Plesser reports seeing a kernel panic running configure
+    # due to this test when exec_disable_arg_limit is 1 on Tru64. It is not
+    # nice to cause kernel panics so lets avoid the loop below.
+    # First set a reasonable default.
+    lt_cv_sys_max_cmd_len=16384
+    #
+    if test -x /sbin/sysconfig; then
+      case `/sbin/sysconfig -q proc exec_disable_arg_limit` in
+        *1*) lt_cv_sys_max_cmd_len=-1 ;;
+      esac
+    fi
+    ;;
+  sco3.2v5*)
+    lt_cv_sys_max_cmd_len=102400
+    ;;
+  sysv5* | sco5v6* | sysv4.2uw2*)
+    kargmax=`grep ARG_MAX /etc/conf/cf.d/stune 2>/dev/null`
+    if test -n "$kargmax"; then
+      lt_cv_sys_max_cmd_len=`echo $kargmax | $SED 's/.*[	 ]//'`
+    else
+      lt_cv_sys_max_cmd_len=32768
+    fi
+    ;;
+  *)
+    lt_cv_sys_max_cmd_len=`(getconf ARG_MAX) 2> /dev/null`
+    if test -n "$lt_cv_sys_max_cmd_len" && \
+       test undefined != "$lt_cv_sys_max_cmd_len"; then
+      lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 4`
+      lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \* 3`
+    else
+      # Make teststring a little bigger before we do anything with it.
+      # a 1K string should be a reasonable start.
+      for i in 1 2 3 4 5 6 7 8; do
+        teststring=$teststring$teststring
+      done
+      SHELL=${SHELL-${CONFIG_SHELL-/bin/sh}}
+      # If test is not a shell built-in, we'll probably end up computing a
+      # maximum length that is only half of the actual maximum length, but
+      # we can't tell.
+      while { test X`env echo "$teststring$teststring" 2>/dev/null` \
+	         = "X$teststring$teststring"; } >/dev/null 2>&1 &&
+	      test 17 != "$i" # 1/2 MB should be enough
+      do
+        i=`expr $i + 1`
+        teststring=$teststring$teststring
+      done
+      # Only check the string length outside the loop.
+      lt_cv_sys_max_cmd_len=`expr "X$teststring" : ".*" 2>&1`
+      teststring=
+      # Add a significant safety factor because C++ compilers can tack on
+      # massive amounts of additional arguments before passing them to the
+      # linker.  It appears as though 1/2 is a usable value.
+      lt_cv_sys_max_cmd_len=`expr $lt_cv_sys_max_cmd_len \/ 2`
+    fi
+    ;;
+  esac
+
+fi
+
+if test -n "$lt_cv_sys_max_cmd_len"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_sys_max_cmd_len" >&5
+printf "%s\n" "$lt_cv_sys_max_cmd_len" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none" >&5
+printf "%s\n" "none" >&6; }
+fi
+max_cmd_len=$lt_cv_sys_max_cmd_len
+
+
+
+
+
+
+: ${CP="cp -f"}
+: ${MV="mv -f"}
+: ${RM="rm -f"}
+
+if ( (MAIL=60; unset MAIL) || exit) >/dev/null 2>&1; then
+  lt_unset=unset
+else
+  lt_unset=false
+fi
+
+
+
+
+
+# test EBCDIC or ASCII
+case `echo X|tr X '\101'` in
+ A) # ASCII based system
+    # \n is not interpreted correctly by Solaris 8 /usr/ucb/tr
+  lt_SP2NL='tr \040 \012'
+  lt_NL2SP='tr \015\012 \040\040'
+  ;;
+ *) # EBCDIC based system
+  lt_SP2NL='tr \100 \n'
+  lt_NL2SP='tr \r\n \100\100'
+  ;;
+esac
+
+
+
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking how to convert $build file names to $host format" >&5
+printf %s "checking how to convert $build file names to $host format... " >&6; }
+if test ${lt_cv_to_host_file_cmd+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  case $host in
+  *-*-mingw* )
+    case $build in
+      *-*-mingw* ) # actually msys
+        lt_cv_to_host_file_cmd=func_convert_file_msys_to_w32
+        ;;
+      *-*-cygwin* )
+        lt_cv_to_host_file_cmd=func_convert_file_cygwin_to_w32
+        ;;
+      * ) # otherwise, assume *nix
+        lt_cv_to_host_file_cmd=func_convert_file_nix_to_w32
+        ;;
+    esac
+    ;;
+  *-*-cygwin* )
+    case $build in
+      *-*-mingw* ) # actually msys
+        lt_cv_to_host_file_cmd=func_convert_file_msys_to_cygwin
+        ;;
+      *-*-cygwin* )
+        lt_cv_to_host_file_cmd=func_convert_file_noop
+        ;;
+      * ) # otherwise, assume *nix
+        lt_cv_to_host_file_cmd=func_convert_file_nix_to_cygwin
+        ;;
+    esac
+    ;;
+  * ) # unhandled hosts (and "normal" native builds)
+    lt_cv_to_host_file_cmd=func_convert_file_noop
+    ;;
+esac
+
+fi
+
+to_host_file_cmd=$lt_cv_to_host_file_cmd
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_to_host_file_cmd" >&5
+printf "%s\n" "$lt_cv_to_host_file_cmd" >&6; }
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking how to convert $build file names to toolchain format" >&5
+printf %s "checking how to convert $build file names to toolchain format... " >&6; }
+if test ${lt_cv_to_tool_file_cmd+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  #assume ordinary cross tools, or native build.
+lt_cv_to_tool_file_cmd=func_convert_file_noop
+case $host in
+  *-*-mingw* )
+    case $build in
+      *-*-mingw* ) # actually msys
+        lt_cv_to_tool_file_cmd=func_convert_file_msys_to_w32
+        ;;
+    esac
+    ;;
+esac
+
+fi
+
+to_tool_file_cmd=$lt_cv_to_tool_file_cmd
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_to_tool_file_cmd" >&5
+printf "%s\n" "$lt_cv_to_tool_file_cmd" >&6; }
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $LD option to reload object files" >&5
+printf %s "checking for $LD option to reload object files... " >&6; }
+if test ${lt_cv_ld_reload_flag+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_ld_reload_flag='-r'
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_ld_reload_flag" >&5
+printf "%s\n" "$lt_cv_ld_reload_flag" >&6; }
+reload_flag=$lt_cv_ld_reload_flag
+case $reload_flag in
+"" | " "*) ;;
+*) reload_flag=" $reload_flag" ;;
+esac
+reload_cmds='$LD$reload_flag -o $output$reload_objs'
+case $host_os in
+  cygwin* | mingw* | pw32* | cegcc*)
+    if test yes != "$GCC"; then
+      reload_cmds=false
+    fi
+    ;;
+  darwin*)
+    if test yes = "$GCC"; then
+      reload_cmds='$LTCC $LTCFLAGS -nostdlib $wl-r -o $output$reload_objs'
+    else
+      reload_cmds='$LD$reload_flag -o $output$reload_objs'
+    fi
+    ;;
+esac
+
+
+
+
+
+
+
+
+
+if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}file", so it can be a program name with args.
+set dummy ${ac_tool_prefix}file; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_FILECMD+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$FILECMD"; then
+  ac_cv_prog_FILECMD="$FILECMD" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_FILECMD="${ac_tool_prefix}file"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+FILECMD=$ac_cv_prog_FILECMD
+if test -n "$FILECMD"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $FILECMD" >&5
+printf "%s\n" "$FILECMD" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_FILECMD"; then
+  ac_ct_FILECMD=$FILECMD
+  # Extract the first word of "file", so it can be a program name with args.
+set dummy file; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_FILECMD+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_FILECMD"; then
+  ac_cv_prog_ac_ct_FILECMD="$ac_ct_FILECMD" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_FILECMD="file"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_FILECMD=$ac_cv_prog_ac_ct_FILECMD
+if test -n "$ac_ct_FILECMD"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_FILECMD" >&5
+printf "%s\n" "$ac_ct_FILECMD" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_FILECMD" = x; then
+    FILECMD=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    FILECMD=$ac_ct_FILECMD
+  fi
+else
+  FILECMD="$ac_cv_prog_FILECMD"
+fi
+
+
+
+
+
+
+
+if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}objdump", so it can be a program name with args.
+set dummy ${ac_tool_prefix}objdump; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_OBJDUMP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$OBJDUMP"; then
+  ac_cv_prog_OBJDUMP="$OBJDUMP" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_OBJDUMP="${ac_tool_prefix}objdump"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+OBJDUMP=$ac_cv_prog_OBJDUMP
+if test -n "$OBJDUMP"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $OBJDUMP" >&5
+printf "%s\n" "$OBJDUMP" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_OBJDUMP"; then
+  ac_ct_OBJDUMP=$OBJDUMP
+  # Extract the first word of "objdump", so it can be a program name with args.
+set dummy objdump; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_OBJDUMP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_OBJDUMP"; then
+  ac_cv_prog_ac_ct_OBJDUMP="$ac_ct_OBJDUMP" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_OBJDUMP="objdump"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_OBJDUMP=$ac_cv_prog_ac_ct_OBJDUMP
+if test -n "$ac_ct_OBJDUMP"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_OBJDUMP" >&5
+printf "%s\n" "$ac_ct_OBJDUMP" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_OBJDUMP" = x; then
+    OBJDUMP="false"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    OBJDUMP=$ac_ct_OBJDUMP
+  fi
+else
+  OBJDUMP="$ac_cv_prog_OBJDUMP"
+fi
+
+test -z "$OBJDUMP" && OBJDUMP=objdump
+
+
+
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking how to recognize dependent libraries" >&5
+printf %s "checking how to recognize dependent libraries... " >&6; }
+if test ${lt_cv_deplibs_check_method+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_file_magic_cmd='$MAGIC_CMD'
+lt_cv_file_magic_test_file=
+lt_cv_deplibs_check_method='unknown'
+# Need to set the preceding variable on all platforms that support
+# interlibrary dependencies.
+# 'none' -- dependencies not supported.
+# 'unknown' -- same as none, but documents that we really don't know.
+# 'pass_all' -- all dependencies passed with no checks.
+# 'test_compile' -- check by making test program.
+# 'file_magic [[regex]]' -- check by looking for files in library path
+# that responds to the $file_magic_cmd with a given extended regex.
+# If you have 'file' or equivalent on your system and you're not sure
+# whether 'pass_all' will *always* work, you probably want this one.
+
+case $host_os in
+aix[4-9]*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+beos*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+bsdi[45]*)
+  lt_cv_deplibs_check_method='file_magic ELF [0-9][0-9]*-bit [ML]SB (shared object|dynamic lib)'
+  lt_cv_file_magic_cmd='$FILECMD -L'
+  lt_cv_file_magic_test_file=/shlib/libc.so
+  ;;
+
+cygwin*)
+  # func_win32_libid is a shell function defined in ltmain.sh
+  lt_cv_deplibs_check_method='file_magic ^x86 archive import|^x86 DLL'
+  lt_cv_file_magic_cmd='func_win32_libid'
+  ;;
+
+mingw* | pw32*)
+  # Base MSYS/MinGW do not provide the 'file' command needed by
+  # func_win32_libid shell function, so use a weaker test based on 'objdump',
+  # unless we find 'file', for example because we are cross-compiling.
+  if ( file / ) >/dev/null 2>&1; then
+    lt_cv_deplibs_check_method='file_magic ^x86 archive import|^x86 DLL'
+    lt_cv_file_magic_cmd='func_win32_libid'
+  else
+    # Keep this pattern in sync with the one in func_win32_libid.
+    lt_cv_deplibs_check_method='file_magic file format (pei*-i386(.*architecture: i386)?|pe-arm-wince|pe-x86-64)'
+    lt_cv_file_magic_cmd='$OBJDUMP -f'
+  fi
+  ;;
+
+cegcc*)
+  # use the weaker test based on 'objdump'. See mingw*.
+  lt_cv_deplibs_check_method='file_magic file format pe-arm-.*little(.*architecture: arm)?'
+  lt_cv_file_magic_cmd='$OBJDUMP -f'
+  ;;
+
+darwin* | rhapsody*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+freebsd* | dragonfly* | midnightbsd*)
+  if echo __ELF__ | $CC -E - | $GREP __ELF__ > /dev/null; then
+    case $host_cpu in
+    i*86 )
+      # Not sure whether the presence of OpenBSD here was a mistake.
+      # Let's accept both of them until this is cleared up.
+      lt_cv_deplibs_check_method='file_magic (FreeBSD|OpenBSD|DragonFly)/i[3-9]86 (compact )?demand paged shared library'
+      lt_cv_file_magic_cmd=$FILECMD
+      lt_cv_file_magic_test_file=`echo /usr/lib/libc.so.*`
+      ;;
+    esac
+  else
+    lt_cv_deplibs_check_method=pass_all
+  fi
+  ;;
+
+haiku*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+hpux10.20* | hpux11*)
+  lt_cv_file_magic_cmd=$FILECMD
+  case $host_cpu in
+  ia64*)
+    lt_cv_deplibs_check_method='file_magic (s[0-9][0-9][0-9]|ELF-[0-9][0-9]) shared object file - IA64'
+    lt_cv_file_magic_test_file=/usr/lib/hpux32/libc.so
+    ;;
+  hppa*64*)
+    lt_cv_deplibs_check_method='file_magic (s[0-9][0-9][0-9]|ELF[ -][0-9][0-9])(-bit)?( [LM]SB)? shared object( file)?[, -]* PA-RISC [0-9]\.[0-9]'
+    lt_cv_file_magic_test_file=/usr/lib/pa20_64/libc.sl
+    ;;
+  *)
+    lt_cv_deplibs_check_method='file_magic (s[0-9][0-9][0-9]|PA-RISC[0-9]\.[0-9]) shared library'
+    lt_cv_file_magic_test_file=/usr/lib/libc.sl
+    ;;
+  esac
+  ;;
+
+interix[3-9]*)
+  # PIC code is broken on Interix 3.x, that's why |\.a not |_pic\.a here
+  lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so|\.a)$'
+  ;;
+
+irix5* | irix6* | nonstopux*)
+  case $LD in
+  *-32|*"-32 ") libmagic=32-bit;;
+  *-n32|*"-n32 ") libmagic=N32;;
+  *-64|*"-64 ") libmagic=64-bit;;
+  *) libmagic=never-match;;
+  esac
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+# This must be glibc/ELF.
+linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+netbsd* | netbsdelf*-gnu)
+  if echo __ELF__ | $CC -E - | $GREP __ELF__ > /dev/null; then
+    lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so\.[0-9]+\.[0-9]+|_pic\.a)$'
+  else
+    lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so|_pic\.a)$'
+  fi
+  ;;
+
+newos6*)
+  lt_cv_deplibs_check_method='file_magic ELF [0-9][0-9]*-bit [ML]SB (executable|dynamic lib)'
+  lt_cv_file_magic_cmd=$FILECMD
+  lt_cv_file_magic_test_file=/usr/lib/libnls.so
+  ;;
+
+*nto* | *qnx*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+openbsd* | bitrig*)
+  if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then
+    lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so\.[0-9]+\.[0-9]+|\.so|_pic\.a)$'
+  else
+    lt_cv_deplibs_check_method='match_pattern /lib[^/]+(\.so\.[0-9]+\.[0-9]+|_pic\.a)$'
+  fi
+  ;;
+
+osf3* | osf4* | osf5*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+rdos*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+solaris*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX* | sysv4*uw2*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+
+sysv4 | sysv4.3*)
+  case $host_vendor in
+  motorola)
+    lt_cv_deplibs_check_method='file_magic ELF [0-9][0-9]*-bit [ML]SB (shared object|dynamic lib) M[0-9][0-9]* Version [0-9]'
+    lt_cv_file_magic_test_file=`echo /usr/lib/libc.so*`
+    ;;
+  ncr)
+    lt_cv_deplibs_check_method=pass_all
+    ;;
+  sequent)
+    lt_cv_file_magic_cmd='/bin/file'
+    lt_cv_deplibs_check_method='file_magic ELF [0-9][0-9]*-bit [LM]SB (shared object|dynamic lib )'
+    ;;
+  sni)
+    lt_cv_file_magic_cmd='/bin/file'
+    lt_cv_deplibs_check_method="file_magic ELF [0-9][0-9]*-bit [LM]SB dynamic lib"
+    lt_cv_file_magic_test_file=/lib/libc.so
+    ;;
+  siemens)
+    lt_cv_deplibs_check_method=pass_all
+    ;;
+  pc)
+    lt_cv_deplibs_check_method=pass_all
+    ;;
+  esac
+  ;;
+
+tpf*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+os2*)
+  lt_cv_deplibs_check_method=pass_all
+  ;;
+esac
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_deplibs_check_method" >&5
+printf "%s\n" "$lt_cv_deplibs_check_method" >&6; }
+
+file_magic_glob=
+want_nocaseglob=no
+if test "$build" = "$host"; then
+  case $host_os in
+  mingw* | pw32*)
+    if ( shopt | grep nocaseglob ) >/dev/null 2>&1; then
+      want_nocaseglob=yes
+    else
+      file_magic_glob=`echo aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ | $SED -e "s/\(..\)/s\/[\1]\/[\1]\/g;/g"`
+    fi
+    ;;
+  esac
+fi
+
+file_magic_cmd=$lt_cv_file_magic_cmd
+deplibs_check_method=$lt_cv_deplibs_check_method
+test -z "$deplibs_check_method" && deplibs_check_method=unknown
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}dlltool", so it can be a program name with args.
+set dummy ${ac_tool_prefix}dlltool; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_DLLTOOL+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$DLLTOOL"; then
+  ac_cv_prog_DLLTOOL="$DLLTOOL" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_DLLTOOL="${ac_tool_prefix}dlltool"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+DLLTOOL=$ac_cv_prog_DLLTOOL
+if test -n "$DLLTOOL"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $DLLTOOL" >&5
+printf "%s\n" "$DLLTOOL" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_DLLTOOL"; then
+  ac_ct_DLLTOOL=$DLLTOOL
+  # Extract the first word of "dlltool", so it can be a program name with args.
+set dummy dlltool; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_DLLTOOL+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_DLLTOOL"; then
+  ac_cv_prog_ac_ct_DLLTOOL="$ac_ct_DLLTOOL" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_DLLTOOL="dlltool"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_DLLTOOL=$ac_cv_prog_ac_ct_DLLTOOL
+if test -n "$ac_ct_DLLTOOL"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_DLLTOOL" >&5
+printf "%s\n" "$ac_ct_DLLTOOL" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_DLLTOOL" = x; then
+    DLLTOOL="false"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    DLLTOOL=$ac_ct_DLLTOOL
+  fi
+else
+  DLLTOOL="$ac_cv_prog_DLLTOOL"
+fi
+
+test -z "$DLLTOOL" && DLLTOOL=dlltool
+
+
+
+
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking how to associate runtime and link libraries" >&5
+printf %s "checking how to associate runtime and link libraries... " >&6; }
+if test ${lt_cv_sharedlib_from_linklib_cmd+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_sharedlib_from_linklib_cmd='unknown'
+
+case $host_os in
+cygwin* | mingw* | pw32* | cegcc*)
+  # two different shell functions defined in ltmain.sh;
+  # decide which one to use based on capabilities of $DLLTOOL
+  case `$DLLTOOL --help 2>&1` in
+  *--identify-strict*)
+    lt_cv_sharedlib_from_linklib_cmd=func_cygming_dll_for_implib
+    ;;
+  *)
+    lt_cv_sharedlib_from_linklib_cmd=func_cygming_dll_for_implib_fallback
+    ;;
+  esac
+  ;;
+*)
+  # fallback: assume linklib IS sharedlib
+  lt_cv_sharedlib_from_linklib_cmd=$ECHO
+  ;;
+esac
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_sharedlib_from_linklib_cmd" >&5
+printf "%s\n" "$lt_cv_sharedlib_from_linklib_cmd" >&6; }
+sharedlib_from_linklib_cmd=$lt_cv_sharedlib_from_linklib_cmd
+test -z "$sharedlib_from_linklib_cmd" && sharedlib_from_linklib_cmd=$ECHO
+
+
+
+
+
+
+
+if test -n "$ac_tool_prefix"; then
+  for ac_prog in ar
+  do
+    # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args.
+set dummy $ac_tool_prefix$ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_AR+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$AR"; then
+  ac_cv_prog_AR="$AR" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_AR="$ac_tool_prefix$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+AR=$ac_cv_prog_AR
+if test -n "$AR"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $AR" >&5
+printf "%s\n" "$AR" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+    test -n "$AR" && break
+  done
+fi
+if test -z "$AR"; then
+  ac_ct_AR=$AR
+  for ac_prog in ar
+do
+  # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_AR+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_AR"; then
+  ac_cv_prog_ac_ct_AR="$ac_ct_AR" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_AR="$ac_prog"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_AR=$ac_cv_prog_ac_ct_AR
+if test -n "$ac_ct_AR"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_AR" >&5
+printf "%s\n" "$ac_ct_AR" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+  test -n "$ac_ct_AR" && break
+done
+
+  if test "x$ac_ct_AR" = x; then
+    AR="false"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    AR=$ac_ct_AR
+  fi
+fi
+
+: ${AR=ar}
+
+
+
+
+
+
+# Use ARFLAGS variable as AR's operation code to sync the variable naming with
+# Automake.  If both AR_FLAGS and ARFLAGS are specified, AR_FLAGS should have
+# higher priority because thats what people were doing historically (setting
+# ARFLAGS for automake and AR_FLAGS for libtool).  FIXME: Make the AR_FLAGS
+# variable obsoleted/removed.
+
+test ${AR_FLAGS+y} || AR_FLAGS=${ARFLAGS-cr}
+lt_ar_flags=$AR_FLAGS
+
+
+
+
+
+
+# Make AR_FLAGS overridable by 'make ARFLAGS='.  Don't try to run-time override
+# by AR_FLAGS because that was never working and AR_FLAGS is about to die.
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for archiver @FILE support" >&5
+printf %s "checking for archiver @FILE support... " >&6; }
+if test ${lt_cv_ar_at_file+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_ar_at_file=no
+   cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+  echo conftest.$ac_objext > conftest.lst
+      lt_ar_try='$AR $AR_FLAGS libconftest.a @conftest.lst >&5'
+      { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$lt_ar_try\""; } >&5
+  (eval $lt_ar_try) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+      if test 0 -eq "$ac_status"; then
+	# Ensure the archiver fails upon bogus file names.
+	rm -f conftest.$ac_objext libconftest.a
+	{ { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$lt_ar_try\""; } >&5
+  (eval $lt_ar_try) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+	if test 0 -ne "$ac_status"; then
+          lt_cv_ar_at_file=@
+        fi
+      fi
+      rm -f conftest.* libconftest.a
+
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_ar_at_file" >&5
+printf "%s\n" "$lt_cv_ar_at_file" >&6; }
+
+if test no = "$lt_cv_ar_at_file"; then
+  archiver_list_spec=
+else
+  archiver_list_spec=$lt_cv_ar_at_file
+fi
+
+
+
+
+
+
+
+if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}strip", so it can be a program name with args.
+set dummy ${ac_tool_prefix}strip; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_STRIP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$STRIP"; then
+  ac_cv_prog_STRIP="$STRIP" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_STRIP="${ac_tool_prefix}strip"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+STRIP=$ac_cv_prog_STRIP
+if test -n "$STRIP"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $STRIP" >&5
+printf "%s\n" "$STRIP" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_STRIP"; then
+  ac_ct_STRIP=$STRIP
+  # Extract the first word of "strip", so it can be a program name with args.
+set dummy strip; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_STRIP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_STRIP"; then
+  ac_cv_prog_ac_ct_STRIP="$ac_ct_STRIP" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_STRIP="strip"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_STRIP=$ac_cv_prog_ac_ct_STRIP
+if test -n "$ac_ct_STRIP"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_STRIP" >&5
+printf "%s\n" "$ac_ct_STRIP" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_STRIP" = x; then
+    STRIP=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    STRIP=$ac_ct_STRIP
+  fi
+else
+  STRIP="$ac_cv_prog_STRIP"
+fi
+
+test -z "$STRIP" && STRIP=:
+
+
+
+
+
+
+if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}ranlib", so it can be a program name with args.
+set dummy ${ac_tool_prefix}ranlib; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_RANLIB+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$RANLIB"; then
+  ac_cv_prog_RANLIB="$RANLIB" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_RANLIB="${ac_tool_prefix}ranlib"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+RANLIB=$ac_cv_prog_RANLIB
+if test -n "$RANLIB"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $RANLIB" >&5
+printf "%s\n" "$RANLIB" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_RANLIB"; then
+  ac_ct_RANLIB=$RANLIB
+  # Extract the first word of "ranlib", so it can be a program name with args.
+set dummy ranlib; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_RANLIB+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_RANLIB"; then
+  ac_cv_prog_ac_ct_RANLIB="$ac_ct_RANLIB" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_RANLIB="ranlib"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_RANLIB=$ac_cv_prog_ac_ct_RANLIB
+if test -n "$ac_ct_RANLIB"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_RANLIB" >&5
+printf "%s\n" "$ac_ct_RANLIB" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_RANLIB" = x; then
+    RANLIB=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    RANLIB=$ac_ct_RANLIB
+  fi
+else
+  RANLIB="$ac_cv_prog_RANLIB"
+fi
+
+test -z "$RANLIB" && RANLIB=:
+
+
+
+
+
+
+# Determine commands to create old-style static archives.
+old_archive_cmds='$AR $AR_FLAGS $oldlib$oldobjs'
+old_postinstall_cmds='chmod 644 $oldlib'
+old_postuninstall_cmds=
+
+if test -n "$RANLIB"; then
+  case $host_os in
+  bitrig* | openbsd*)
+    old_postinstall_cmds="$old_postinstall_cmds~\$RANLIB -t \$tool_oldlib"
+    ;;
+  *)
+    old_postinstall_cmds="$old_postinstall_cmds~\$RANLIB \$tool_oldlib"
+    ;;
+  esac
+  old_archive_cmds="$old_archive_cmds~\$RANLIB \$tool_oldlib"
+fi
+
+case $host_os in
+  darwin*)
+    lock_old_archive_extraction=yes ;;
+  *)
+    lock_old_archive_extraction=no ;;
+esac
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# If no C compiler was specified, use CC.
+LTCC=${LTCC-"$CC"}
+
+# If no C compiler flags were specified, use CFLAGS.
+LTCFLAGS=${LTCFLAGS-"$CFLAGS"}
+
+# Allow CC to be a program name with arguments.
+compiler=$CC
+
+
+# Check for command to grab the raw symbol name followed by C symbol from nm.
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking command to parse $NM output from $compiler object" >&5
+printf %s "checking command to parse $NM output from $compiler object... " >&6; }
+if test ${lt_cv_sys_global_symbol_pipe+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+
+# These are sane defaults that work on at least a few old systems.
+# [They come from Ultrix.  What could be older than Ultrix?!! ;)]
+
+# Character class describing NM global symbol codes.
+symcode='[BCDEGRST]'
+
+# Regexp to match symbols that can be accessed directly from C.
+sympat='\([_A-Za-z][_A-Za-z0-9]*\)'
+
+# Define system-specific variables.
+case $host_os in
+aix*)
+  symcode='[BCDT]'
+  ;;
+cygwin* | mingw* | pw32* | cegcc*)
+  symcode='[ABCDGISTW]'
+  ;;
+hpux*)
+  if test ia64 = "$host_cpu"; then
+    symcode='[ABCDEGRST]'
+  fi
+  ;;
+irix* | nonstopux*)
+  symcode='[BCDEGRST]'
+  ;;
+osf*)
+  symcode='[BCDEGQRST]'
+  ;;
+solaris*)
+  symcode='[BDRT]'
+  ;;
+sco3.2v5*)
+  symcode='[DT]'
+  ;;
+sysv4.2uw2*)
+  symcode='[DT]'
+  ;;
+sysv5* | sco5v6* | unixware* | OpenUNIX*)
+  symcode='[ABDT]'
+  ;;
+sysv4)
+  symcode='[DFNSTU]'
+  ;;
+esac
+
+# If we're using GNU nm, then use its standard symbol codes.
+case `$NM -V 2>&1` in
+*GNU* | *'with BFD'*)
+  symcode='[ABCDGIRSTW]' ;;
+esac
+
+if test "$lt_cv_nm_interface" = "MS dumpbin"; then
+  # Gets list of data symbols to import.
+  lt_cv_sys_global_symbol_to_import="$SED -n -e 's/^I .* \(.*\)$/\1/p'"
+  # Adjust the below global symbol transforms to fixup imported variables.
+  lt_cdecl_hook=" -e 's/^I .* \(.*\)$/extern __declspec(dllimport) char \1;/p'"
+  lt_c_name_hook=" -e 's/^I .* \(.*\)$/  {\"\1\", (void *) 0},/p'"
+  lt_c_name_lib_hook="\
+  -e 's/^I .* \(lib.*\)$/  {\"\1\", (void *) 0},/p'\
+  -e 's/^I .* \(.*\)$/  {\"lib\1\", (void *) 0},/p'"
+else
+  # Disable hooks by default.
+  lt_cv_sys_global_symbol_to_import=
+  lt_cdecl_hook=
+  lt_c_name_hook=
+  lt_c_name_lib_hook=
+fi
+
+# Transform an extracted symbol line into a proper C declaration.
+# Some systems (esp. on ia64) link data and code symbols differently,
+# so use this general approach.
+lt_cv_sys_global_symbol_to_cdecl="$SED -n"\
+$lt_cdecl_hook\
+" -e 's/^T .* \(.*\)$/extern int \1();/p'"\
+" -e 's/^$symcode$symcode* .* \(.*\)$/extern char \1;/p'"
+
+# Transform an extracted symbol line into symbol name and symbol address
+lt_cv_sys_global_symbol_to_c_name_address="$SED -n"\
+$lt_c_name_hook\
+" -e 's/^: \(.*\) .*$/  {\"\1\", (void *) 0},/p'"\
+" -e 's/^$symcode$symcode* .* \(.*\)$/  {\"\1\", (void *) \&\1},/p'"
+
+# Transform an extracted symbol line into symbol name with lib prefix and
+# symbol address.
+lt_cv_sys_global_symbol_to_c_name_address_lib_prefix="$SED -n"\
+$lt_c_name_lib_hook\
+" -e 's/^: \(.*\) .*$/  {\"\1\", (void *) 0},/p'"\
+" -e 's/^$symcode$symcode* .* \(lib.*\)$/  {\"\1\", (void *) \&\1},/p'"\
+" -e 's/^$symcode$symcode* .* \(.*\)$/  {\"lib\1\", (void *) \&\1},/p'"
+
+# Handle CRLF in mingw tool chain
+opt_cr=
+case $build_os in
+mingw*)
+  opt_cr=`$ECHO 'x\{0,1\}' | tr x '\015'` # option cr in regexp
+  ;;
+esac
+
+# Try without a prefix underscore, then with it.
+for ac_symprfx in "" "_"; do
+
+  # Transform symcode, sympat, and symprfx into a raw symbol and a C symbol.
+  symxfrm="\\1 $ac_symprfx\\2 \\2"
+
+  # Write the raw and C identifiers.
+  if test "$lt_cv_nm_interface" = "MS dumpbin"; then
+    # Fake it for dumpbin and say T for any non-static function,
+    # D for any global variable and I for any imported variable.
+    # Also find C++ and __fastcall symbols from MSVC++ or ICC,
+    # which start with @ or ?.
+    lt_cv_sys_global_symbol_pipe="$AWK '"\
+"     {last_section=section; section=\$ 3};"\
+"     /^COFF SYMBOL TABLE/{for(i in hide) delete hide[i]};"\
+"     /Section length .*#relocs.*(pick any)/{hide[last_section]=1};"\
+"     /^ *Symbol name *: /{split(\$ 0,sn,\":\"); si=substr(sn[2],2)};"\
+"     /^ *Type *: code/{print \"T\",si,substr(si,length(prfx))};"\
+"     /^ *Type *: data/{print \"I\",si,substr(si,length(prfx))};"\
+"     \$ 0!~/External *\|/{next};"\
+"     / 0+ UNDEF /{next}; / UNDEF \([^|]\)*()/{next};"\
+"     {if(hide[section]) next};"\
+"     {f=\"D\"}; \$ 0~/\(\).*\|/{f=\"T\"};"\
+"     {split(\$ 0,a,/\||\r/); split(a[2],s)};"\
+"     s[1]~/^[@?]/{print f,s[1],s[1]; next};"\
+"     s[1]~prfx {split(s[1],t,\"@\"); print f,t[1],substr(t[1],length(prfx))}"\
+"     ' prfx=^$ac_symprfx"
+  else
+    lt_cv_sys_global_symbol_pipe="$SED -n -e 's/^.*[	 ]\($symcode$symcode*\)[	 ][	 ]*$ac_symprfx$sympat$opt_cr$/$symxfrm/p'"
+  fi
+  lt_cv_sys_global_symbol_pipe="$lt_cv_sys_global_symbol_pipe | $SED '/ __gnu_lto/d'"
+
+  # Check to see that the pipe works correctly.
+  pipe_works=no
+
+  rm -f conftest*
+  cat > conftest.$ac_ext <<_LT_EOF
+#ifdef __cplusplus
+extern "C" {
+#endif
+char nm_test_var;
+void nm_test_func(void);
+void nm_test_func(void){}
+#ifdef __cplusplus
+}
+#endif
+int main(){nm_test_var='a';nm_test_func();return(0);}
+_LT_EOF
+
+  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
+  (eval $ac_compile) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; then
+    # Now try to grab the symbols.
+    nlist=conftest.nm
+    $ECHO "$as_me:$LINENO: $NM conftest.$ac_objext | $lt_cv_sys_global_symbol_pipe > $nlist" >&5
+    if eval "$NM" conftest.$ac_objext \| "$lt_cv_sys_global_symbol_pipe" \> $nlist 2>&5 && test -s "$nlist"; then
+      # Try sorting and uniquifying the output.
+      if sort "$nlist" | uniq > "$nlist"T; then
+	mv -f "$nlist"T "$nlist"
+      else
+	rm -f "$nlist"T
+      fi
+
+      # Make sure that we snagged all the symbols we need.
+      if $GREP ' nm_test_var$' "$nlist" >/dev/null; then
+	if $GREP ' nm_test_func$' "$nlist" >/dev/null; then
+	  cat <<_LT_EOF > conftest.$ac_ext
+/* Keep this code in sync between libtool.m4, ltmain, lt_system.h, and tests.  */
+#if defined _WIN32 || defined __CYGWIN__ || defined _WIN32_WCE
+/* DATA imports from DLLs on WIN32 can't be const, because runtime
+   relocations are performed -- see ld's documentation on pseudo-relocs.  */
+# define LT_DLSYM_CONST
+#elif defined __osf__
+/* This system does not cope well with relocations in const data.  */
+# define LT_DLSYM_CONST
+#else
+# define LT_DLSYM_CONST const
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+_LT_EOF
+	  # Now generate the symbol file.
+	  eval "$lt_cv_sys_global_symbol_to_cdecl"' < "$nlist" | $GREP -v main >> conftest.$ac_ext'
+
+	  cat <<_LT_EOF >> conftest.$ac_ext
+
+/* The mapping between symbol names and symbols.  */
+LT_DLSYM_CONST struct {
+  const char *name;
+  void       *address;
+}
+lt__PROGRAM__LTX_preloaded_symbols[] =
+{
+  { "@PROGRAM@", (void *) 0 },
+_LT_EOF
+	  $SED "s/^$symcode$symcode* .* \(.*\)$/  {\"\1\", (void *) \&\1},/" < "$nlist" | $GREP -v main >> conftest.$ac_ext
+	  cat <<\_LT_EOF >> conftest.$ac_ext
+  {0, (void *) 0}
+};
+
+/* This works around a problem in FreeBSD linker */
+#ifdef FREEBSD_WORKAROUND
+static const void *lt_preloaded_setup() {
+  return lt__PROGRAM__LTX_preloaded_symbols;
+}
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+_LT_EOF
+	  # Now try linking the two files.
+	  mv conftest.$ac_objext conftstm.$ac_objext
+	  lt_globsym_save_LIBS=$LIBS
+	  lt_globsym_save_CFLAGS=$CFLAGS
+	  LIBS=conftstm.$ac_objext
+	  CFLAGS="$CFLAGS$lt_prog_compiler_no_builtin_flag"
+	  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_link\""; } >&5
+  (eval $ac_link) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; } && test -s conftest$ac_exeext; then
+	    pipe_works=yes
+	  fi
+	  LIBS=$lt_globsym_save_LIBS
+	  CFLAGS=$lt_globsym_save_CFLAGS
+	else
+	  echo "cannot find nm_test_func in $nlist" >&5
+	fi
+      else
+	echo "cannot find nm_test_var in $nlist" >&5
+      fi
+    else
+      echo "cannot run $lt_cv_sys_global_symbol_pipe" >&5
+    fi
+  else
+    echo "$progname: failed program was:" >&5
+    cat conftest.$ac_ext >&5
+  fi
+  rm -rf conftest* conftst*
+
+  # Do not use the global_symbol_pipe unless it works.
+  if test yes = "$pipe_works"; then
+    break
+  else
+    lt_cv_sys_global_symbol_pipe=
+  fi
+done
+
+fi
+
+if test -z "$lt_cv_sys_global_symbol_pipe"; then
+  lt_cv_sys_global_symbol_to_cdecl=
+fi
+if test -z "$lt_cv_sys_global_symbol_pipe$lt_cv_sys_global_symbol_to_cdecl"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: failed" >&5
+printf "%s\n" "failed" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ok" >&5
+printf "%s\n" "ok" >&6; }
+fi
+
+# Response file support.
+if test "$lt_cv_nm_interface" = "MS dumpbin"; then
+  nm_file_list_spec='@'
+elif $NM --help 2>/dev/null | grep '[@]FILE' >/dev/null; then
+  nm_file_list_spec='@'
+fi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for sysroot" >&5
+printf %s "checking for sysroot... " >&6; }
+
+# Check whether --with-sysroot was given.
+if test ${with_sysroot+y}
+then :
+  withval=$with_sysroot;
+else $as_nop
+  with_sysroot=no
+fi
+
+
+lt_sysroot=
+case $with_sysroot in #(
+ yes)
+   if test yes = "$GCC"; then
+     lt_sysroot=`$CC --print-sysroot 2>/dev/null`
+   fi
+   ;; #(
+ /*)
+   lt_sysroot=`echo "$with_sysroot" | $SED -e "$sed_quote_subst"`
+   ;; #(
+ no|'')
+   ;; #(
+ *)
+   { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $with_sysroot" >&5
+printf "%s\n" "$with_sysroot" >&6; }
+   as_fn_error $? "The sysroot must be an absolute path." "$LINENO" 5
+   ;;
+esac
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${lt_sysroot:-no}" >&5
+printf "%s\n" "${lt_sysroot:-no}" >&6; }
+
+
+
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a working dd" >&5
+printf %s "checking for a working dd... " >&6; }
+if test ${ac_cv_path_lt_DD+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  printf 0123456789abcdef0123456789abcdef >conftest.i
+cat conftest.i conftest.i >conftest2.i
+: ${lt_DD:=$DD}
+if test -z "$lt_DD"; then
+  ac_path_lt_DD_found=false
+  # Loop through the user's path and test for each of PROGNAME-LIST
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_prog in dd
+   do
+    for ac_exec_ext in '' $ac_executable_extensions; do
+      ac_path_lt_DD="$as_dir$ac_prog$ac_exec_ext"
+      as_fn_executable_p "$ac_path_lt_DD" || continue
+if "$ac_path_lt_DD" bs=32 count=1 <conftest2.i >conftest.out 2>/dev/null; then
+  cmp -s conftest.i conftest.out \
+  && ac_cv_path_lt_DD="$ac_path_lt_DD" ac_path_lt_DD_found=:
+fi
+      $ac_path_lt_DD_found && break 3
+    done
+  done
+  done
+IFS=$as_save_IFS
+  if test -z "$ac_cv_path_lt_DD"; then
+    :
+  fi
+else
+  ac_cv_path_lt_DD=$lt_DD
+fi
+
+rm -f conftest.i conftest2.i conftest.out
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_lt_DD" >&5
+printf "%s\n" "$ac_cv_path_lt_DD" >&6; }
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking how to truncate binary pipes" >&5
+printf %s "checking how to truncate binary pipes... " >&6; }
+if test ${lt_cv_truncate_bin+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  printf 0123456789abcdef0123456789abcdef >conftest.i
+cat conftest.i conftest.i >conftest2.i
+lt_cv_truncate_bin=
+if "$ac_cv_path_lt_DD" bs=32 count=1 <conftest2.i >conftest.out 2>/dev/null; then
+  cmp -s conftest.i conftest.out \
+  && lt_cv_truncate_bin="$ac_cv_path_lt_DD bs=4096 count=1"
+fi
+rm -f conftest.i conftest2.i conftest.out
+test -z "$lt_cv_truncate_bin" && lt_cv_truncate_bin="$SED -e 4q"
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_truncate_bin" >&5
+printf "%s\n" "$lt_cv_truncate_bin" >&6; }
+
+
+
+
+
+
+
+# Calculate cc_basename.  Skip known compiler wrappers and cross-prefix.
+func_cc_basename ()
+{
+    for cc_temp in $*""; do
+      case $cc_temp in
+        compile | *[\\/]compile | ccache | *[\\/]ccache ) ;;
+        distcc | *[\\/]distcc | purify | *[\\/]purify ) ;;
+        \-*) ;;
+        *) break;;
+      esac
+    done
+    func_cc_basename_result=`$ECHO "$cc_temp" | $SED "s%.*/%%; s%^$host_alias-%%"`
+}
+
+# Check whether --enable-libtool-lock was given.
+if test ${enable_libtool_lock+y}
+then :
+  enableval=$enable_libtool_lock;
+fi
+
+test no = "$enable_libtool_lock" || enable_libtool_lock=yes
+
+# Some flags need to be propagated to the compiler or linker for good
+# libtool support.
+case $host in
+ia64-*-hpux*)
+  # Find out what ABI is being produced by ac_compile, and set mode
+  # options accordingly.
+  echo 'int i;' > conftest.$ac_ext
+  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
+  (eval $ac_compile) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; then
+    case `$FILECMD conftest.$ac_objext` in
+      *ELF-32*)
+	HPUX_IA64_MODE=32
+	;;
+      *ELF-64*)
+	HPUX_IA64_MODE=64
+	;;
+    esac
+  fi
+  rm -rf conftest*
+  ;;
+*-*-irix6*)
+  # Find out what ABI is being produced by ac_compile, and set linker
+  # options accordingly.
+  echo '#line '$LINENO' "configure"' > conftest.$ac_ext
+  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
+  (eval $ac_compile) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; then
+    if test yes = "$lt_cv_prog_gnu_ld"; then
+      case `$FILECMD conftest.$ac_objext` in
+	*32-bit*)
+	  LD="${LD-ld} -melf32bsmip"
+	  ;;
+	*N32*)
+	  LD="${LD-ld} -melf32bmipn32"
+	  ;;
+	*64-bit*)
+	  LD="${LD-ld} -melf64bmip"
+	;;
+      esac
+    else
+      case `$FILECMD conftest.$ac_objext` in
+	*32-bit*)
+	  LD="${LD-ld} -32"
+	  ;;
+	*N32*)
+	  LD="${LD-ld} -n32"
+	  ;;
+	*64-bit*)
+	  LD="${LD-ld} -64"
+	  ;;
+      esac
+    fi
+  fi
+  rm -rf conftest*
+  ;;
+
+mips64*-*linux*)
+  # Find out what ABI is being produced by ac_compile, and set linker
+  # options accordingly.
+  echo '#line '$LINENO' "configure"' > conftest.$ac_ext
+  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
+  (eval $ac_compile) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; then
+    emul=elf
+    case `$FILECMD conftest.$ac_objext` in
+      *32-bit*)
+	emul="${emul}32"
+	;;
+      *64-bit*)
+	emul="${emul}64"
+	;;
+    esac
+    case `$FILECMD conftest.$ac_objext` in
+      *MSB*)
+	emul="${emul}btsmip"
+	;;
+      *LSB*)
+	emul="${emul}ltsmip"
+	;;
+    esac
+    case `$FILECMD conftest.$ac_objext` in
+      *N32*)
+	emul="${emul}n32"
+	;;
+    esac
+    LD="${LD-ld} -m $emul"
+  fi
+  rm -rf conftest*
+  ;;
+
+x86_64-*kfreebsd*-gnu|x86_64-*linux*|powerpc*-*linux*| \
+s390*-*linux*|s390*-*tpf*|sparc*-*linux*)
+  # Find out what ABI is being produced by ac_compile, and set linker
+  # options accordingly.  Note that the listed cases only cover the
+  # situations where additional linker options are needed (such as when
+  # doing 32-bit compilation for a host where ld defaults to 64-bit, or
+  # vice versa); the common cases where no linker options are needed do
+  # not appear in the list.
+  echo 'int i;' > conftest.$ac_ext
+  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
+  (eval $ac_compile) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; then
+    case `$FILECMD conftest.o` in
+      *32-bit*)
+	case $host in
+	  x86_64-*kfreebsd*-gnu)
+	    LD="${LD-ld} -m elf_i386_fbsd"
+	    ;;
+	  x86_64-*linux*)
+	    case `$FILECMD conftest.o` in
+	      *x86-64*)
+		LD="${LD-ld} -m elf32_x86_64"
+		;;
+	      *)
+		LD="${LD-ld} -m elf_i386"
+		;;
+	    esac
+	    ;;
+	  powerpc64le-*linux*)
+	    LD="${LD-ld} -m elf32lppclinux"
+	    ;;
+	  powerpc64-*linux*)
+	    LD="${LD-ld} -m elf32ppclinux"
+	    ;;
+	  s390x-*linux*)
+	    LD="${LD-ld} -m elf_s390"
+	    ;;
+	  sparc64-*linux*)
+	    LD="${LD-ld} -m elf32_sparc"
+	    ;;
+	esac
+	;;
+      *64-bit*)
+	case $host in
+	  x86_64-*kfreebsd*-gnu)
+	    LD="${LD-ld} -m elf_x86_64_fbsd"
+	    ;;
+	  x86_64-*linux*)
+	    LD="${LD-ld} -m elf_x86_64"
+	    ;;
+	  powerpcle-*linux*)
+	    LD="${LD-ld} -m elf64lppc"
+	    ;;
+	  powerpc-*linux*)
+	    LD="${LD-ld} -m elf64ppc"
+	    ;;
+	  s390*-*linux*|s390*-*tpf*)
+	    LD="${LD-ld} -m elf64_s390"
+	    ;;
+	  sparc*-*linux*)
+	    LD="${LD-ld} -m elf64_sparc"
+	    ;;
+	esac
+	;;
+    esac
+  fi
+  rm -rf conftest*
+  ;;
+
+*-*-sco3.2v5*)
+  # On SCO OpenServer 5, we need -belf to get full-featured binaries.
+  SAVE_CFLAGS=$CFLAGS
+  CFLAGS="$CFLAGS -belf"
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the C compiler needs -belf" >&5
+printf %s "checking whether the C compiler needs -belf... " >&6; }
+if test ${lt_cv_cc_needs_belf+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+     cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  lt_cv_cc_needs_belf=yes
+else $as_nop
+  lt_cv_cc_needs_belf=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+     ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_cc_needs_belf" >&5
+printf "%s\n" "$lt_cv_cc_needs_belf" >&6; }
+  if test yes != "$lt_cv_cc_needs_belf"; then
+    # this is probably gcc 2.8.0, egcs 1.0 or newer; no need for -belf
+    CFLAGS=$SAVE_CFLAGS
+  fi
+  ;;
+*-*solaris*)
+  # Find out what ABI is being produced by ac_compile, and set linker
+  # options accordingly.
+  echo 'int i;' > conftest.$ac_ext
+  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
+  (eval $ac_compile) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; then
+    case `$FILECMD conftest.o` in
+    *64-bit*)
+      case $lt_cv_prog_gnu_ld in
+      yes*)
+        case $host in
+        i?86-*-solaris*|x86_64-*-solaris*)
+          LD="${LD-ld} -m elf_x86_64"
+          ;;
+        sparc*-*-solaris*)
+          LD="${LD-ld} -m elf64_sparc"
+          ;;
+        esac
+        # GNU ld 2.21 introduced _sol2 emulations.  Use them if available.
+        if ${LD-ld} -V | grep _sol2 >/dev/null 2>&1; then
+          LD=${LD-ld}_sol2
+        fi
+        ;;
+      *)
+	if ${LD-ld} -64 -r -o conftest2.o conftest.o >/dev/null 2>&1; then
+	  LD="${LD-ld} -64"
+	fi
+	;;
+      esac
+      ;;
+    esac
+  fi
+  rm -rf conftest*
+  ;;
+esac
+
+need_locks=$enable_libtool_lock
+
+if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}mt", so it can be a program name with args.
+set dummy ${ac_tool_prefix}mt; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_MANIFEST_TOOL+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$MANIFEST_TOOL"; then
+  ac_cv_prog_MANIFEST_TOOL="$MANIFEST_TOOL" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_MANIFEST_TOOL="${ac_tool_prefix}mt"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+MANIFEST_TOOL=$ac_cv_prog_MANIFEST_TOOL
+if test -n "$MANIFEST_TOOL"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $MANIFEST_TOOL" >&5
+printf "%s\n" "$MANIFEST_TOOL" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_MANIFEST_TOOL"; then
+  ac_ct_MANIFEST_TOOL=$MANIFEST_TOOL
+  # Extract the first word of "mt", so it can be a program name with args.
+set dummy mt; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_MANIFEST_TOOL+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_MANIFEST_TOOL"; then
+  ac_cv_prog_ac_ct_MANIFEST_TOOL="$ac_ct_MANIFEST_TOOL" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_MANIFEST_TOOL="mt"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_MANIFEST_TOOL=$ac_cv_prog_ac_ct_MANIFEST_TOOL
+if test -n "$ac_ct_MANIFEST_TOOL"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_MANIFEST_TOOL" >&5
+printf "%s\n" "$ac_ct_MANIFEST_TOOL" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_MANIFEST_TOOL" = x; then
+    MANIFEST_TOOL=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    MANIFEST_TOOL=$ac_ct_MANIFEST_TOOL
+  fi
+else
+  MANIFEST_TOOL="$ac_cv_prog_MANIFEST_TOOL"
+fi
+
+test -z "$MANIFEST_TOOL" && MANIFEST_TOOL=mt
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if $MANIFEST_TOOL is a manifest tool" >&5
+printf %s "checking if $MANIFEST_TOOL is a manifest tool... " >&6; }
+if test ${lt_cv_path_mainfest_tool+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_path_mainfest_tool=no
+  echo "$as_me:$LINENO: $MANIFEST_TOOL '-?'" >&5
+  $MANIFEST_TOOL '-?' 2>conftest.err > conftest.out
+  cat conftest.err >&5
+  if $GREP 'Manifest Tool' conftest.out > /dev/null; then
+    lt_cv_path_mainfest_tool=yes
+  fi
+  rm -f conftest*
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_path_mainfest_tool" >&5
+printf "%s\n" "$lt_cv_path_mainfest_tool" >&6; }
+if test yes != "$lt_cv_path_mainfest_tool"; then
+  MANIFEST_TOOL=:
+fi
+
+
+
+
+
+
+  case $host_os in
+    rhapsody* | darwin*)
+    if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}dsymutil", so it can be a program name with args.
+set dummy ${ac_tool_prefix}dsymutil; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_DSYMUTIL+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$DSYMUTIL"; then
+  ac_cv_prog_DSYMUTIL="$DSYMUTIL" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_DSYMUTIL="${ac_tool_prefix}dsymutil"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+DSYMUTIL=$ac_cv_prog_DSYMUTIL
+if test -n "$DSYMUTIL"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $DSYMUTIL" >&5
+printf "%s\n" "$DSYMUTIL" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_DSYMUTIL"; then
+  ac_ct_DSYMUTIL=$DSYMUTIL
+  # Extract the first word of "dsymutil", so it can be a program name with args.
+set dummy dsymutil; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_DSYMUTIL+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_DSYMUTIL"; then
+  ac_cv_prog_ac_ct_DSYMUTIL="$ac_ct_DSYMUTIL" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_DSYMUTIL="dsymutil"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_DSYMUTIL=$ac_cv_prog_ac_ct_DSYMUTIL
+if test -n "$ac_ct_DSYMUTIL"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_DSYMUTIL" >&5
+printf "%s\n" "$ac_ct_DSYMUTIL" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_DSYMUTIL" = x; then
+    DSYMUTIL=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    DSYMUTIL=$ac_ct_DSYMUTIL
+  fi
+else
+  DSYMUTIL="$ac_cv_prog_DSYMUTIL"
+fi
+
+    if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}nmedit", so it can be a program name with args.
+set dummy ${ac_tool_prefix}nmedit; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_NMEDIT+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$NMEDIT"; then
+  ac_cv_prog_NMEDIT="$NMEDIT" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_NMEDIT="${ac_tool_prefix}nmedit"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+NMEDIT=$ac_cv_prog_NMEDIT
+if test -n "$NMEDIT"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $NMEDIT" >&5
+printf "%s\n" "$NMEDIT" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_NMEDIT"; then
+  ac_ct_NMEDIT=$NMEDIT
+  # Extract the first word of "nmedit", so it can be a program name with args.
+set dummy nmedit; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_NMEDIT+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_NMEDIT"; then
+  ac_cv_prog_ac_ct_NMEDIT="$ac_ct_NMEDIT" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_NMEDIT="nmedit"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_NMEDIT=$ac_cv_prog_ac_ct_NMEDIT
+if test -n "$ac_ct_NMEDIT"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_NMEDIT" >&5
+printf "%s\n" "$ac_ct_NMEDIT" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_NMEDIT" = x; then
+    NMEDIT=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    NMEDIT=$ac_ct_NMEDIT
+  fi
+else
+  NMEDIT="$ac_cv_prog_NMEDIT"
+fi
+
+    if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}lipo", so it can be a program name with args.
+set dummy ${ac_tool_prefix}lipo; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_LIPO+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$LIPO"; then
+  ac_cv_prog_LIPO="$LIPO" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_LIPO="${ac_tool_prefix}lipo"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+LIPO=$ac_cv_prog_LIPO
+if test -n "$LIPO"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $LIPO" >&5
+printf "%s\n" "$LIPO" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_LIPO"; then
+  ac_ct_LIPO=$LIPO
+  # Extract the first word of "lipo", so it can be a program name with args.
+set dummy lipo; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_LIPO+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_LIPO"; then
+  ac_cv_prog_ac_ct_LIPO="$ac_ct_LIPO" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_LIPO="lipo"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_LIPO=$ac_cv_prog_ac_ct_LIPO
+if test -n "$ac_ct_LIPO"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_LIPO" >&5
+printf "%s\n" "$ac_ct_LIPO" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_LIPO" = x; then
+    LIPO=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    LIPO=$ac_ct_LIPO
+  fi
+else
+  LIPO="$ac_cv_prog_LIPO"
+fi
+
+    if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}otool", so it can be a program name with args.
+set dummy ${ac_tool_prefix}otool; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_OTOOL+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$OTOOL"; then
+  ac_cv_prog_OTOOL="$OTOOL" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_OTOOL="${ac_tool_prefix}otool"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+OTOOL=$ac_cv_prog_OTOOL
+if test -n "$OTOOL"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $OTOOL" >&5
+printf "%s\n" "$OTOOL" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_OTOOL"; then
+  ac_ct_OTOOL=$OTOOL
+  # Extract the first word of "otool", so it can be a program name with args.
+set dummy otool; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_OTOOL+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_OTOOL"; then
+  ac_cv_prog_ac_ct_OTOOL="$ac_ct_OTOOL" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_OTOOL="otool"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_OTOOL=$ac_cv_prog_ac_ct_OTOOL
+if test -n "$ac_ct_OTOOL"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_OTOOL" >&5
+printf "%s\n" "$ac_ct_OTOOL" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_OTOOL" = x; then
+    OTOOL=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    OTOOL=$ac_ct_OTOOL
+  fi
+else
+  OTOOL="$ac_cv_prog_OTOOL"
+fi
+
+    if test -n "$ac_tool_prefix"; then
+  # Extract the first word of "${ac_tool_prefix}otool64", so it can be a program name with args.
+set dummy ${ac_tool_prefix}otool64; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_OTOOL64+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$OTOOL64"; then
+  ac_cv_prog_OTOOL64="$OTOOL64" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_OTOOL64="${ac_tool_prefix}otool64"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+OTOOL64=$ac_cv_prog_OTOOL64
+if test -n "$OTOOL64"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $OTOOL64" >&5
+printf "%s\n" "$OTOOL64" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_OTOOL64"; then
+  ac_ct_OTOOL64=$OTOOL64
+  # Extract the first word of "otool64", so it can be a program name with args.
+set dummy otool64; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_OTOOL64+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if test -n "$ac_ct_OTOOL64"; then
+  ac_cv_prog_ac_ct_OTOOL64="$ac_ct_OTOOL64" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ac_ct_OTOOL64="otool64"
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_OTOOL64=$ac_cv_prog_ac_ct_OTOOL64
+if test -n "$ac_ct_OTOOL64"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_OTOOL64" >&5
+printf "%s\n" "$ac_ct_OTOOL64" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+  if test "x$ac_ct_OTOOL64" = x; then
+    OTOOL64=":"
+  else
+    case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+    OTOOL64=$ac_ct_OTOOL64
+  fi
+else
+  OTOOL64="$ac_cv_prog_OTOOL64"
+fi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for -single_module linker flag" >&5
+printf %s "checking for -single_module linker flag... " >&6; }
+if test ${lt_cv_apple_cc_single_mod+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_apple_cc_single_mod=no
+      if test -z "$LT_MULTI_MODULE"; then
+	# By default we will add the -single_module flag. You can override
+	# by either setting the environment variable LT_MULTI_MODULE
+	# non-empty at configure time, or by adding -multi_module to the
+	# link flags.
+	rm -rf libconftest.dylib*
+	echo "int foo(void){return 1;}" > conftest.c
+	echo "$LTCC $LTCFLAGS $LDFLAGS -o libconftest.dylib \
+-dynamiclib -Wl,-single_module conftest.c" >&5
+	$LTCC $LTCFLAGS $LDFLAGS -o libconftest.dylib \
+	  -dynamiclib -Wl,-single_module conftest.c 2>conftest.err
+        _lt_result=$?
+	# If there is a non-empty error log, and "single_module"
+	# appears in it, assume the flag caused a linker warning
+        if test -s conftest.err && $GREP single_module conftest.err; then
+	  cat conftest.err >&5
+	# Otherwise, if the output was created with a 0 exit code from
+	# the compiler, it worked.
+	elif test -f libconftest.dylib && test 0 = "$_lt_result"; then
+	  lt_cv_apple_cc_single_mod=yes
+	else
+	  cat conftest.err >&5
+	fi
+	rm -rf libconftest.dylib*
+	rm -f conftest.*
+      fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_apple_cc_single_mod" >&5
+printf "%s\n" "$lt_cv_apple_cc_single_mod" >&6; }
+
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for -exported_symbols_list linker flag" >&5
+printf %s "checking for -exported_symbols_list linker flag... " >&6; }
+if test ${lt_cv_ld_exported_symbols_list+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_ld_exported_symbols_list=no
+      save_LDFLAGS=$LDFLAGS
+      echo "_main" > conftest.sym
+      LDFLAGS="$LDFLAGS -Wl,-exported_symbols_list,conftest.sym"
+      cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  lt_cv_ld_exported_symbols_list=yes
+else $as_nop
+  lt_cv_ld_exported_symbols_list=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+	LDFLAGS=$save_LDFLAGS
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_ld_exported_symbols_list" >&5
+printf "%s\n" "$lt_cv_ld_exported_symbols_list" >&6; }
+
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for -force_load linker flag" >&5
+printf %s "checking for -force_load linker flag... " >&6; }
+if test ${lt_cv_ld_force_load+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_ld_force_load=no
+      cat > conftest.c << _LT_EOF
+int forced_loaded() { return 2;}
+_LT_EOF
+      echo "$LTCC $LTCFLAGS -c -o conftest.o conftest.c" >&5
+      $LTCC $LTCFLAGS -c -o conftest.o conftest.c 2>&5
+      echo "$AR $AR_FLAGS libconftest.a conftest.o" >&5
+      $AR $AR_FLAGS libconftest.a conftest.o 2>&5
+      echo "$RANLIB libconftest.a" >&5
+      $RANLIB libconftest.a 2>&5
+      cat > conftest.c << _LT_EOF
+int main() { return 0;}
+_LT_EOF
+      echo "$LTCC $LTCFLAGS $LDFLAGS -o conftest conftest.c -Wl,-force_load,./libconftest.a" >&5
+      $LTCC $LTCFLAGS $LDFLAGS -o conftest conftest.c -Wl,-force_load,./libconftest.a 2>conftest.err
+      _lt_result=$?
+      if test -s conftest.err && $GREP force_load conftest.err; then
+	cat conftest.err >&5
+      elif test -f conftest && test 0 = "$_lt_result" && $GREP forced_load conftest >/dev/null 2>&1; then
+	lt_cv_ld_force_load=yes
+      else
+	cat conftest.err >&5
+      fi
+        rm -f conftest.err libconftest.a conftest conftest.c
+        rm -rf conftest.dSYM
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_ld_force_load" >&5
+printf "%s\n" "$lt_cv_ld_force_load" >&6; }
+    case $host_os in
+    rhapsody* | darwin1.[012])
+      _lt_dar_allow_undefined='$wl-undefined ${wl}suppress' ;;
+    darwin1.*)
+      _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;;
+    darwin*)
+      case $MACOSX_DEPLOYMENT_TARGET,$host in
+        10.[012],*|,*powerpc*-darwin[5-8]*)
+          _lt_dar_allow_undefined='$wl-flat_namespace $wl-undefined ${wl}suppress' ;;
+        *)
+          _lt_dar_allow_undefined='$wl-undefined ${wl}dynamic_lookup' ;;
+      esac
+    ;;
+  esac
+    if test yes = "$lt_cv_apple_cc_single_mod"; then
+      _lt_dar_single_mod='$single_module'
+    fi
+    if test yes = "$lt_cv_ld_exported_symbols_list"; then
+      _lt_dar_export_syms=' $wl-exported_symbols_list,$output_objdir/$libname-symbols.expsym'
+    else
+      _lt_dar_export_syms='~$NMEDIT -s $output_objdir/$libname-symbols.expsym $lib'
+    fi
+    if test : != "$DSYMUTIL" && test no = "$lt_cv_ld_force_load"; then
+      _lt_dsymutil='~$DSYMUTIL $lib || :'
+    else
+      _lt_dsymutil=
+    fi
+    ;;
+  esac
+
+# func_munge_path_list VARIABLE PATH
+# -----------------------------------
+# VARIABLE is name of variable containing _space_ separated list of
+# directories to be munged by the contents of PATH, which is string
+# having a format:
+# "DIR[:DIR]:"
+#       string "DIR[ DIR]" will be prepended to VARIABLE
+# ":DIR[:DIR]"
+#       string "DIR[ DIR]" will be appended to VARIABLE
+# "DIRP[:DIRP]::[DIRA:]DIRA"
+#       string "DIRP[ DIRP]" will be prepended to VARIABLE and string
+#       "DIRA[ DIRA]" will be appended to VARIABLE
+# "DIR[:DIR]"
+#       VARIABLE will be replaced by "DIR[ DIR]"
+func_munge_path_list ()
+{
+    case x$2 in
+    x)
+        ;;
+    *:)
+        eval $1=\"`$ECHO $2 | $SED 's/:/ /g'` \$$1\"
+        ;;
+    x:*)
+        eval $1=\"\$$1 `$ECHO $2 | $SED 's/:/ /g'`\"
+        ;;
+    *::*)
+        eval $1=\"\$$1\ `$ECHO $2 | $SED -e 's/.*:://' -e 's/:/ /g'`\"
+        eval $1=\"`$ECHO $2 | $SED -e 's/::.*//' -e 's/:/ /g'`\ \$$1\"
+        ;;
+    *)
+        eval $1=\"`$ECHO $2 | $SED 's/:/ /g'`\"
+        ;;
+    esac
+}
+
+ac_header= ac_cache=
+for ac_item in $ac_header_c_list
+do
+  if test $ac_cache; then
+    ac_fn_c_check_header_compile "$LINENO" $ac_header ac_cv_header_$ac_cache "$ac_includes_default"
+    if eval test \"x\$ac_cv_header_$ac_cache\" = xyes; then
+      printf "%s\n" "#define $ac_item 1" >> confdefs.h
+    fi
+    ac_header= ac_cache=
+  elif test $ac_header; then
+    ac_cache=$ac_item
+  else
+    ac_header=$ac_item
+  fi
+done
+
+
+
+
+
+
+
+
+if test $ac_cv_header_stdlib_h = yes && test $ac_cv_header_string_h = yes
+then :
+
+printf "%s\n" "#define STDC_HEADERS 1" >>confdefs.h
+
+fi
+ac_fn_c_check_header_compile "$LINENO" "dlfcn.h" "ac_cv_header_dlfcn_h" "$ac_includes_default
+"
+if test "x$ac_cv_header_dlfcn_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_DLFCN_H 1" >>confdefs.h
+
+fi
+
+
+
+
+
+# Set options
+
+
+
+        enable_dlopen=no
+
+
+  enable_win32_dll=no
+
+
+            # Check whether --enable-shared was given.
+if test ${enable_shared+y}
+then :
+  enableval=$enable_shared; p=${PACKAGE-default}
+    case $enableval in
+    yes) enable_shared=yes ;;
+    no) enable_shared=no ;;
+    *)
+      enable_shared=no
+      # Look at the argument we got.  We use all the common list separators.
+      lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR,
+      for pkg in $enableval; do
+	IFS=$lt_save_ifs
+	if test "X$pkg" = "X$p"; then
+	  enable_shared=yes
+	fi
+      done
+      IFS=$lt_save_ifs
+      ;;
+    esac
+else $as_nop
+  enable_shared=yes
+fi
+
+
+
+
+
+
+
+
+
+  # Check whether --enable-static was given.
+if test ${enable_static+y}
+then :
+  enableval=$enable_static; p=${PACKAGE-default}
+    case $enableval in
+    yes) enable_static=yes ;;
+    no) enable_static=no ;;
+    *)
+     enable_static=no
+      # Look at the argument we got.  We use all the common list separators.
+      lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR,
+      for pkg in $enableval; do
+	IFS=$lt_save_ifs
+	if test "X$pkg" = "X$p"; then
+	  enable_static=yes
+	fi
+      done
+      IFS=$lt_save_ifs
+      ;;
+    esac
+else $as_nop
+  enable_static=yes
+fi
+
+
+
+
+
+
+
+
+
+
+# Check whether --with-pic was given.
+if test ${with_pic+y}
+then :
+  withval=$with_pic; lt_p=${PACKAGE-default}
+    case $withval in
+    yes|no) pic_mode=$withval ;;
+    *)
+      pic_mode=default
+      # Look at the argument we got.  We use all the common list separators.
+      lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR,
+      for lt_pkg in $withval; do
+	IFS=$lt_save_ifs
+	if test "X$lt_pkg" = "X$lt_p"; then
+	  pic_mode=yes
+	fi
+      done
+      IFS=$lt_save_ifs
+      ;;
+    esac
+else $as_nop
+  pic_mode=default
+fi
+
+
+
+
+
+
+
+
+  # Check whether --enable-fast-install was given.
+if test ${enable_fast_install+y}
+then :
+  enableval=$enable_fast_install; p=${PACKAGE-default}
+    case $enableval in
+    yes) enable_fast_install=yes ;;
+    no) enable_fast_install=no ;;
+    *)
+      enable_fast_install=no
+      # Look at the argument we got.  We use all the common list separators.
+      lt_save_ifs=$IFS; IFS=$IFS$PATH_SEPARATOR,
+      for pkg in $enableval; do
+	IFS=$lt_save_ifs
+	if test "X$pkg" = "X$p"; then
+	  enable_fast_install=yes
+	fi
+      done
+      IFS=$lt_save_ifs
+      ;;
+    esac
+else $as_nop
+  enable_fast_install=yes
+fi
+
+
+
+
+
+
+
+
+  shared_archive_member_spec=
+case $host,$enable_shared in
+power*-*-aix[5-9]*,yes)
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking which variant of shared library versioning to provide" >&5
+printf %s "checking which variant of shared library versioning to provide... " >&6; }
+
+# Check whether --with-aix-soname was given.
+if test ${with_aix_soname+y}
+then :
+  withval=$with_aix_soname; case $withval in
+    aix|svr4|both)
+      ;;
+    *)
+      as_fn_error $? "Unknown argument to --with-aix-soname" "$LINENO" 5
+      ;;
+    esac
+    lt_cv_with_aix_soname=$with_aix_soname
+else $as_nop
+  if test ${lt_cv_with_aix_soname+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_with_aix_soname=aix
+fi
+
+    with_aix_soname=$lt_cv_with_aix_soname
+fi
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $with_aix_soname" >&5
+printf "%s\n" "$with_aix_soname" >&6; }
+  if test aix != "$with_aix_soname"; then
+    # For the AIX way of multilib, we name the shared archive member
+    # based on the bitwidth used, traditionally 'shr.o' or 'shr_64.o',
+    # and 'shr.imp' or 'shr_64.imp', respectively, for the Import File.
+    # Even when GNU compilers ignore OBJECT_MODE but need '-maix64' flag,
+    # the AIX toolchain works better with OBJECT_MODE set (default 32).
+    if test 64 = "${OBJECT_MODE-32}"; then
+      shared_archive_member_spec=shr_64
+    else
+      shared_archive_member_spec=shr
+    fi
+  fi
+  ;;
+*)
+  with_aix_soname=aix
+  ;;
+esac
+
+
+
+
+
+
+
+
+
+
+# This can be used to rebuild libtool when needed
+LIBTOOL_DEPS=$ltmain
+
+# Always use our own libtool.
+LIBTOOL='$(SHELL) $(top_builddir)/libtool'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+test -z "$LN_S" && LN_S="ln -s"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+if test -n "${ZSH_VERSION+set}"; then
+   setopt NO_GLOB_SUBST
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for objdir" >&5
+printf %s "checking for objdir... " >&6; }
+if test ${lt_cv_objdir+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  rm -f .libs 2>/dev/null
+mkdir .libs 2>/dev/null
+if test -d .libs; then
+  lt_cv_objdir=.libs
+else
+  # MS-DOS does not allow filenames that begin with a dot.
+  lt_cv_objdir=_libs
+fi
+rmdir .libs 2>/dev/null
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_objdir" >&5
+printf "%s\n" "$lt_cv_objdir" >&6; }
+objdir=$lt_cv_objdir
+
+
+
+
+
+printf "%s\n" "#define LT_OBJDIR \"$lt_cv_objdir/\"" >>confdefs.h
+
+
+
+
+case $host_os in
+aix3*)
+  # AIX sometimes has problems with the GCC collect2 program.  For some
+  # reason, if we set the COLLECT_NAMES environment variable, the problems
+  # vanish in a puff of smoke.
+  if test set != "${COLLECT_NAMES+set}"; then
+    COLLECT_NAMES=
+    export COLLECT_NAMES
+  fi
+  ;;
+esac
+
+# Global variables:
+ofile=libtool
+can_build_shared=yes
+
+# All known linkers require a '.a' archive for static linking (except MSVC and
+# ICC, which need '.lib').
+libext=a
+
+with_gnu_ld=$lt_cv_prog_gnu_ld
+
+old_CC=$CC
+old_CFLAGS=$CFLAGS
+
+# Set sane defaults for various variables
+test -z "$CC" && CC=cc
+test -z "$LTCC" && LTCC=$CC
+test -z "$LTCFLAGS" && LTCFLAGS=$CFLAGS
+test -z "$LD" && LD=ld
+test -z "$ac_objext" && ac_objext=o
+
+func_cc_basename $compiler
+cc_basename=$func_cc_basename_result
+
+
+# Only perform the check for file, if the check method requires it
+test -z "$MAGIC_CMD" && MAGIC_CMD=file
+case $deplibs_check_method in
+file_magic*)
+  if test "$file_magic_cmd" = '$MAGIC_CMD'; then
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ${ac_tool_prefix}file" >&5
+printf %s "checking for ${ac_tool_prefix}file... " >&6; }
+if test ${lt_cv_path_MAGIC_CMD+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  case $MAGIC_CMD in
+[\\/*] |  ?:[\\/]*)
+  lt_cv_path_MAGIC_CMD=$MAGIC_CMD # Let the user override the test with a path.
+  ;;
+*)
+  lt_save_MAGIC_CMD=$MAGIC_CMD
+  lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR
+  ac_dummy="/usr/bin$PATH_SEPARATOR$PATH"
+  for ac_dir in $ac_dummy; do
+    IFS=$lt_save_ifs
+    test -z "$ac_dir" && ac_dir=.
+    if test -f "$ac_dir/${ac_tool_prefix}file"; then
+      lt_cv_path_MAGIC_CMD=$ac_dir/"${ac_tool_prefix}file"
+      if test -n "$file_magic_test_file"; then
+	case $deplibs_check_method in
+	"file_magic "*)
+	  file_magic_regex=`expr "$deplibs_check_method" : "file_magic \(.*\)"`
+	  MAGIC_CMD=$lt_cv_path_MAGIC_CMD
+	  if eval $file_magic_cmd \$file_magic_test_file 2> /dev/null |
+	    $EGREP "$file_magic_regex" > /dev/null; then
+	    :
+	  else
+	    cat <<_LT_EOF 1>&2
+
+*** Warning: the command libtool uses to detect shared libraries,
+*** $file_magic_cmd, produces output that libtool cannot recognize.
+*** The result is that libtool may fail to recognize shared libraries
+*** as such.  This will affect the creation of libtool libraries that
+*** depend on shared libraries, but programs linked with such libtool
+*** libraries will work regardless of this problem.  Nevertheless, you
+*** may want to report the problem to your system manager and/or to
+*** bug-libtool@gnu.org
+
+_LT_EOF
+	  fi ;;
+	esac
+      fi
+      break
+    fi
+  done
+  IFS=$lt_save_ifs
+  MAGIC_CMD=$lt_save_MAGIC_CMD
+  ;;
+esac
+fi
+
+MAGIC_CMD=$lt_cv_path_MAGIC_CMD
+if test -n "$MAGIC_CMD"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $MAGIC_CMD" >&5
+printf "%s\n" "$MAGIC_CMD" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+
+
+
+if test -z "$lt_cv_path_MAGIC_CMD"; then
+  if test -n "$ac_tool_prefix"; then
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for file" >&5
+printf %s "checking for file... " >&6; }
+if test ${lt_cv_path_MAGIC_CMD+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  case $MAGIC_CMD in
+[\\/*] |  ?:[\\/]*)
+  lt_cv_path_MAGIC_CMD=$MAGIC_CMD # Let the user override the test with a path.
+  ;;
+*)
+  lt_save_MAGIC_CMD=$MAGIC_CMD
+  lt_save_ifs=$IFS; IFS=$PATH_SEPARATOR
+  ac_dummy="/usr/bin$PATH_SEPARATOR$PATH"
+  for ac_dir in $ac_dummy; do
+    IFS=$lt_save_ifs
+    test -z "$ac_dir" && ac_dir=.
+    if test -f "$ac_dir/file"; then
+      lt_cv_path_MAGIC_CMD=$ac_dir/"file"
+      if test -n "$file_magic_test_file"; then
+	case $deplibs_check_method in
+	"file_magic "*)
+	  file_magic_regex=`expr "$deplibs_check_method" : "file_magic \(.*\)"`
+	  MAGIC_CMD=$lt_cv_path_MAGIC_CMD
+	  if eval $file_magic_cmd \$file_magic_test_file 2> /dev/null |
+	    $EGREP "$file_magic_regex" > /dev/null; then
+	    :
+	  else
+	    cat <<_LT_EOF 1>&2
+
+*** Warning: the command libtool uses to detect shared libraries,
+*** $file_magic_cmd, produces output that libtool cannot recognize.
+*** The result is that libtool may fail to recognize shared libraries
+*** as such.  This will affect the creation of libtool libraries that
+*** depend on shared libraries, but programs linked with such libtool
+*** libraries will work regardless of this problem.  Nevertheless, you
+*** may want to report the problem to your system manager and/or to
+*** bug-libtool@gnu.org
+
+_LT_EOF
+	  fi ;;
+	esac
+      fi
+      break
+    fi
+  done
+  IFS=$lt_save_ifs
+  MAGIC_CMD=$lt_save_MAGIC_CMD
+  ;;
+esac
+fi
+
+MAGIC_CMD=$lt_cv_path_MAGIC_CMD
+if test -n "$MAGIC_CMD"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $MAGIC_CMD" >&5
+printf "%s\n" "$MAGIC_CMD" >&6; }
+else
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+  else
+    MAGIC_CMD=:
+  fi
+fi
+
+  fi
+  ;;
+esac
+
+# Use C for the default configuration in the libtool script
+
+lt_save_CC=$CC
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
+# Source file extension for C test sources.
+ac_ext=c
+
+# Object file extension for compiled C test sources.
+objext=o
+objext=$objext
+
+# Code to be used in simple compile tests
+lt_simple_compile_test_code="int some_variable = 0;"
+
+# Code to be used in simple link tests
+lt_simple_link_test_code='int main(){return(0);}'
+
+
+
+
+
+
+
+# If no C compiler was specified, use CC.
+LTCC=${LTCC-"$CC"}
+
+# If no C compiler flags were specified, use CFLAGS.
+LTCFLAGS=${LTCFLAGS-"$CFLAGS"}
+
+# Allow CC to be a program name with arguments.
+compiler=$CC
+
+# Save the default compiler, since it gets overwritten when the other
+# tags are being tested, and _LT_TAGVAR(compiler, []) is a NOP.
+compiler_DEFAULT=$CC
+
+# save warnings/boilerplate of simple test code
+ac_outfile=conftest.$ac_objext
+echo "$lt_simple_compile_test_code" >conftest.$ac_ext
+eval "$ac_compile" 2>&1 >/dev/null | $SED '/^$/d; /^ *+/d' >conftest.err
+_lt_compiler_boilerplate=`cat conftest.err`
+$RM conftest*
+
+ac_outfile=conftest.$ac_objext
+echo "$lt_simple_link_test_code" >conftest.$ac_ext
+eval "$ac_link" 2>&1 >/dev/null | $SED '/^$/d; /^ *+/d' >conftest.err
+_lt_linker_boilerplate=`cat conftest.err`
+$RM -r conftest*
+
+
+if test -n "$compiler"; then
+
+lt_prog_compiler_no_builtin_flag=
+
+if test yes = "$GCC"; then
+  case $cc_basename in
+  nvcc*)
+    lt_prog_compiler_no_builtin_flag=' -Xcompiler -fno-builtin' ;;
+  *)
+    lt_prog_compiler_no_builtin_flag=' -fno-builtin' ;;
+  esac
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if $compiler supports -fno-rtti -fno-exceptions" >&5
+printf %s "checking if $compiler supports -fno-rtti -fno-exceptions... " >&6; }
+if test ${lt_cv_prog_compiler_rtti_exceptions+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_prog_compiler_rtti_exceptions=no
+   ac_outfile=conftest.$ac_objext
+   echo "$lt_simple_compile_test_code" > conftest.$ac_ext
+   lt_compiler_flag="-fno-rtti -fno-exceptions"  ## exclude from sc_useless_quotes_in_assignment
+   # Insert the option either (1) after the last *FLAGS variable, or
+   # (2) before a word containing "conftest.", or (3) at the end.
+   # Note that $ac_compile itself does not contain backslashes and begins
+   # with a dollar sign (not a hyphen), so the echo should work correctly.
+   # The option is referenced via a variable to avoid confusing sed.
+   lt_compile=`echo "$ac_compile" | $SED \
+   -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
+   -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
+   -e 's:$: $lt_compiler_flag:'`
+   (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&5)
+   (eval "$lt_compile" 2>conftest.err)
+   ac_status=$?
+   cat conftest.err >&5
+   echo "$as_me:$LINENO: \$? = $ac_status" >&5
+   if (exit $ac_status) && test -s "$ac_outfile"; then
+     # The compiler can only warn and ignore the option if not recognized
+     # So say no if there are warnings other than the usual output.
+     $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' >conftest.exp
+     $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2
+     if test ! -s conftest.er2 || diff conftest.exp conftest.er2 >/dev/null; then
+       lt_cv_prog_compiler_rtti_exceptions=yes
+     fi
+   fi
+   $RM conftest*
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_rtti_exceptions" >&5
+printf "%s\n" "$lt_cv_prog_compiler_rtti_exceptions" >&6; }
+
+if test yes = "$lt_cv_prog_compiler_rtti_exceptions"; then
+    lt_prog_compiler_no_builtin_flag="$lt_prog_compiler_no_builtin_flag -fno-rtti -fno-exceptions"
+else
+    :
+fi
+
+fi
+
+
+
+
+
+
+  lt_prog_compiler_wl=
+lt_prog_compiler_pic=
+lt_prog_compiler_static=
+
+
+  if test yes = "$GCC"; then
+    lt_prog_compiler_wl='-Wl,'
+    lt_prog_compiler_static='-static'
+
+    case $host_os in
+      aix*)
+      # All AIX code is PIC.
+      if test ia64 = "$host_cpu"; then
+	# AIX 5 now supports IA64 processor
+	lt_prog_compiler_static='-Bstatic'
+      fi
+      lt_prog_compiler_pic='-fPIC'
+      ;;
+
+    amigaos*)
+      case $host_cpu in
+      powerpc)
+            # see comment about AmigaOS4 .so support
+            lt_prog_compiler_pic='-fPIC'
+        ;;
+      m68k)
+            # FIXME: we need at least 68020 code to build shared libraries, but
+            # adding the '-m68020' flag to GCC prevents building anything better,
+            # like '-m68040'.
+            lt_prog_compiler_pic='-m68020 -resident32 -malways-restore-a4'
+        ;;
+      esac
+      ;;
+
+    beos* | irix5* | irix6* | nonstopux* | osf3* | osf4* | osf5*)
+      # PIC is the default for these OSes.
+      ;;
+
+    mingw* | cygwin* | pw32* | os2* | cegcc*)
+      # This hack is so that the source file can tell whether it is being
+      # built for inclusion in a dll (and should export symbols for example).
+      # Although the cygwin gcc ignores -fPIC, still need this for old-style
+      # (--disable-auto-import) libraries
+      lt_prog_compiler_pic='-DDLL_EXPORT'
+      case $host_os in
+      os2*)
+	lt_prog_compiler_static='$wl-static'
+	;;
+      esac
+      ;;
+
+    darwin* | rhapsody*)
+      # PIC is the default on this platform
+      # Common symbols not allowed in MH_DYLIB files
+      lt_prog_compiler_pic='-fno-common'
+      ;;
+
+    haiku*)
+      # PIC is the default for Haiku.
+      # The "-static" flag exists, but is broken.
+      lt_prog_compiler_static=
+      ;;
+
+    hpux*)
+      # PIC is the default for 64-bit PA HP-UX, but not for 32-bit
+      # PA HP-UX.  On IA64 HP-UX, PIC is the default but the pic flag
+      # sets the default TLS model and affects inlining.
+      case $host_cpu in
+      hppa*64*)
+	# +Z the default
+	;;
+      *)
+	lt_prog_compiler_pic='-fPIC'
+	;;
+      esac
+      ;;
+
+    interix[3-9]*)
+      # Interix 3.x gcc -fpic/-fPIC options generate broken code.
+      # Instead, we relocate shared libraries at runtime.
+      ;;
+
+    msdosdjgpp*)
+      # Just because we use GCC doesn't mean we suddenly get shared libraries
+      # on systems that don't support them.
+      lt_prog_compiler_can_build_shared=no
+      enable_shared=no
+      ;;
+
+    *nto* | *qnx*)
+      # QNX uses GNU C++, but need to define -shared option too, otherwise
+      # it will coredump.
+      lt_prog_compiler_pic='-fPIC -shared'
+      ;;
+
+    sysv4*MP*)
+      if test -d /usr/nec; then
+	lt_prog_compiler_pic=-Kconform_pic
+      fi
+      ;;
+
+    *)
+      lt_prog_compiler_pic='-fPIC'
+      ;;
+    esac
+
+    case $cc_basename in
+    nvcc*) # Cuda Compiler Driver 2.2
+      lt_prog_compiler_wl='-Xlinker '
+      if test -n "$lt_prog_compiler_pic"; then
+        lt_prog_compiler_pic="-Xcompiler $lt_prog_compiler_pic"
+      fi
+      ;;
+    esac
+  else
+    # PORTME Check for flag to pass linker flags through the system compiler.
+    case $host_os in
+    aix*)
+      lt_prog_compiler_wl='-Wl,'
+      if test ia64 = "$host_cpu"; then
+	# AIX 5 now supports IA64 processor
+	lt_prog_compiler_static='-Bstatic'
+      else
+	lt_prog_compiler_static='-bnso -bI:/lib/syscalls.exp'
+      fi
+      ;;
+
+    darwin* | rhapsody*)
+      # PIC is the default on this platform
+      # Common symbols not allowed in MH_DYLIB files
+      lt_prog_compiler_pic='-fno-common'
+      case $cc_basename in
+      nagfor*)
+        # NAG Fortran compiler
+        lt_prog_compiler_wl='-Wl,-Wl,,'
+        lt_prog_compiler_pic='-PIC'
+        lt_prog_compiler_static='-Bstatic'
+        ;;
+      esac
+      ;;
+
+    mingw* | cygwin* | pw32* | os2* | cegcc*)
+      # This hack is so that the source file can tell whether it is being
+      # built for inclusion in a dll (and should export symbols for example).
+      lt_prog_compiler_pic='-DDLL_EXPORT'
+      case $host_os in
+      os2*)
+	lt_prog_compiler_static='$wl-static'
+	;;
+      esac
+      ;;
+
+    hpux9* | hpux10* | hpux11*)
+      lt_prog_compiler_wl='-Wl,'
+      # PIC is the default for IA64 HP-UX and 64-bit HP-UX, but
+      # not for PA HP-UX.
+      case $host_cpu in
+      hppa*64*|ia64*)
+	# +Z the default
+	;;
+      *)
+	lt_prog_compiler_pic='+Z'
+	;;
+      esac
+      # Is there a better lt_prog_compiler_static that works with the bundled CC?
+      lt_prog_compiler_static='$wl-a ${wl}archive'
+      ;;
+
+    irix5* | irix6* | nonstopux*)
+      lt_prog_compiler_wl='-Wl,'
+      # PIC (with -KPIC) is the default.
+      lt_prog_compiler_static='-non_shared'
+      ;;
+
+    linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*)
+      case $cc_basename in
+      # old Intel for x86_64, which still supported -KPIC.
+      ecc*)
+	lt_prog_compiler_wl='-Wl,'
+	lt_prog_compiler_pic='-KPIC'
+	lt_prog_compiler_static='-static'
+        ;;
+      # flang / f18. f95 an alias for gfortran or flang on Debian
+      flang* | f18* | f95*)
+	lt_prog_compiler_wl='-Wl,'
+	lt_prog_compiler_pic='-fPIC'
+	lt_prog_compiler_static='-static'
+        ;;
+      # icc used to be incompatible with GCC.
+      # ICC 10 doesn't accept -KPIC any more.
+      icc* | ifort*)
+	lt_prog_compiler_wl='-Wl,'
+	lt_prog_compiler_pic='-fPIC'
+	lt_prog_compiler_static='-static'
+        ;;
+      # Lahey Fortran 8.1.
+      lf95*)
+	lt_prog_compiler_wl='-Wl,'
+	lt_prog_compiler_pic='--shared'
+	lt_prog_compiler_static='--static'
+	;;
+      nagfor*)
+	# NAG Fortran compiler
+	lt_prog_compiler_wl='-Wl,-Wl,,'
+	lt_prog_compiler_pic='-PIC'
+	lt_prog_compiler_static='-Bstatic'
+	;;
+      tcc*)
+	# Fabrice Bellard et al's Tiny C Compiler
+	lt_prog_compiler_wl='-Wl,'
+	lt_prog_compiler_pic='-fPIC'
+	lt_prog_compiler_static='-static'
+	;;
+      pgcc* | pgf77* | pgf90* | pgf95* | pgfortran*)
+        # Portland Group compilers (*not* the Pentium gcc compiler,
+	# which looks to be a dead project)
+	lt_prog_compiler_wl='-Wl,'
+	lt_prog_compiler_pic='-fpic'
+	lt_prog_compiler_static='-Bstatic'
+        ;;
+      ccc*)
+        lt_prog_compiler_wl='-Wl,'
+        # All Alpha code is PIC.
+        lt_prog_compiler_static='-non_shared'
+        ;;
+      xl* | bgxl* | bgf* | mpixl*)
+	# IBM XL C 8.0/Fortran 10.1, 11.1 on PPC and BlueGene
+	lt_prog_compiler_wl='-Wl,'
+	lt_prog_compiler_pic='-qpic'
+	lt_prog_compiler_static='-qstaticlink'
+	;;
+      *)
+	case `$CC -V 2>&1 | $SED 5q` in
+	*Sun\ Ceres\ Fortran* | *Sun*Fortran*\ [1-7].* | *Sun*Fortran*\ 8.[0-3]*)
+	  # Sun Fortran 8.3 passes all unrecognized flags to the linker
+	  lt_prog_compiler_pic='-KPIC'
+	  lt_prog_compiler_static='-Bstatic'
+	  lt_prog_compiler_wl=''
+	  ;;
+	*Sun\ F* | *Sun*Fortran*)
+	  lt_prog_compiler_pic='-KPIC'
+	  lt_prog_compiler_static='-Bstatic'
+	  lt_prog_compiler_wl='-Qoption ld '
+	  ;;
+	*Sun\ C*)
+	  # Sun C 5.9
+	  lt_prog_compiler_pic='-KPIC'
+	  lt_prog_compiler_static='-Bstatic'
+	  lt_prog_compiler_wl='-Wl,'
+	  ;;
+        *Intel*\ [CF]*Compiler*)
+	  lt_prog_compiler_wl='-Wl,'
+	  lt_prog_compiler_pic='-fPIC'
+	  lt_prog_compiler_static='-static'
+	  ;;
+	*Portland\ Group*)
+	  lt_prog_compiler_wl='-Wl,'
+	  lt_prog_compiler_pic='-fpic'
+	  lt_prog_compiler_static='-Bstatic'
+	  ;;
+	esac
+	;;
+      esac
+      ;;
+
+    newsos6)
+      lt_prog_compiler_pic='-KPIC'
+      lt_prog_compiler_static='-Bstatic'
+      ;;
+
+    *nto* | *qnx*)
+      # QNX uses GNU C++, but need to define -shared option too, otherwise
+      # it will coredump.
+      lt_prog_compiler_pic='-fPIC -shared'
+      ;;
+
+    osf3* | osf4* | osf5*)
+      lt_prog_compiler_wl='-Wl,'
+      # All OSF/1 code is PIC.
+      lt_prog_compiler_static='-non_shared'
+      ;;
+
+    rdos*)
+      lt_prog_compiler_static='-non_shared'
+      ;;
+
+    solaris*)
+      lt_prog_compiler_pic='-KPIC'
+      lt_prog_compiler_static='-Bstatic'
+      case $cc_basename in
+      f77* | f90* | f95* | sunf77* | sunf90* | sunf95*)
+	lt_prog_compiler_wl='-Qoption ld ';;
+      *)
+	lt_prog_compiler_wl='-Wl,';;
+      esac
+      ;;
+
+    sunos4*)
+      lt_prog_compiler_wl='-Qoption ld '
+      lt_prog_compiler_pic='-PIC'
+      lt_prog_compiler_static='-Bstatic'
+      ;;
+
+    sysv4 | sysv4.2uw2* | sysv4.3*)
+      lt_prog_compiler_wl='-Wl,'
+      lt_prog_compiler_pic='-KPIC'
+      lt_prog_compiler_static='-Bstatic'
+      ;;
+
+    sysv4*MP*)
+      if test -d /usr/nec; then
+	lt_prog_compiler_pic='-Kconform_pic'
+	lt_prog_compiler_static='-Bstatic'
+      fi
+      ;;
+
+    sysv5* | unixware* | sco3.2v5* | sco5v6* | OpenUNIX*)
+      lt_prog_compiler_wl='-Wl,'
+      lt_prog_compiler_pic='-KPIC'
+      lt_prog_compiler_static='-Bstatic'
+      ;;
+
+    unicos*)
+      lt_prog_compiler_wl='-Wl,'
+      lt_prog_compiler_can_build_shared=no
+      ;;
+
+    uts4*)
+      lt_prog_compiler_pic='-pic'
+      lt_prog_compiler_static='-Bstatic'
+      ;;
+
+    *)
+      lt_prog_compiler_can_build_shared=no
+      ;;
+    esac
+  fi
+
+case $host_os in
+  # For platforms that do not support PIC, -DPIC is meaningless:
+  *djgpp*)
+    lt_prog_compiler_pic=
+    ;;
+  *)
+    lt_prog_compiler_pic="$lt_prog_compiler_pic -DPIC"
+    ;;
+esac
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $compiler option to produce PIC" >&5
+printf %s "checking for $compiler option to produce PIC... " >&6; }
+if test ${lt_cv_prog_compiler_pic+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_prog_compiler_pic=$lt_prog_compiler_pic
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_pic" >&5
+printf "%s\n" "$lt_cv_prog_compiler_pic" >&6; }
+lt_prog_compiler_pic=$lt_cv_prog_compiler_pic
+
+#
+# Check to make sure the PIC flag actually works.
+#
+if test -n "$lt_prog_compiler_pic"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if $compiler PIC flag $lt_prog_compiler_pic works" >&5
+printf %s "checking if $compiler PIC flag $lt_prog_compiler_pic works... " >&6; }
+if test ${lt_cv_prog_compiler_pic_works+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_prog_compiler_pic_works=no
+   ac_outfile=conftest.$ac_objext
+   echo "$lt_simple_compile_test_code" > conftest.$ac_ext
+   lt_compiler_flag="$lt_prog_compiler_pic -DPIC"  ## exclude from sc_useless_quotes_in_assignment
+   # Insert the option either (1) after the last *FLAGS variable, or
+   # (2) before a word containing "conftest.", or (3) at the end.
+   # Note that $ac_compile itself does not contain backslashes and begins
+   # with a dollar sign (not a hyphen), so the echo should work correctly.
+   # The option is referenced via a variable to avoid confusing sed.
+   lt_compile=`echo "$ac_compile" | $SED \
+   -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
+   -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
+   -e 's:$: $lt_compiler_flag:'`
+   (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&5)
+   (eval "$lt_compile" 2>conftest.err)
+   ac_status=$?
+   cat conftest.err >&5
+   echo "$as_me:$LINENO: \$? = $ac_status" >&5
+   if (exit $ac_status) && test -s "$ac_outfile"; then
+     # The compiler can only warn and ignore the option if not recognized
+     # So say no if there are warnings other than the usual output.
+     $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' >conftest.exp
+     $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2
+     if test ! -s conftest.er2 || diff conftest.exp conftest.er2 >/dev/null; then
+       lt_cv_prog_compiler_pic_works=yes
+     fi
+   fi
+   $RM conftest*
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_pic_works" >&5
+printf "%s\n" "$lt_cv_prog_compiler_pic_works" >&6; }
+
+if test yes = "$lt_cv_prog_compiler_pic_works"; then
+    case $lt_prog_compiler_pic in
+     "" | " "*) ;;
+     *) lt_prog_compiler_pic=" $lt_prog_compiler_pic" ;;
+     esac
+else
+    lt_prog_compiler_pic=
+     lt_prog_compiler_can_build_shared=no
+fi
+
+fi
+
+
+
+
+
+
+
+
+
+
+
+#
+# Check to make sure the static flag actually works.
+#
+wl=$lt_prog_compiler_wl eval lt_tmp_static_flag=\"$lt_prog_compiler_static\"
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if $compiler static flag $lt_tmp_static_flag works" >&5
+printf %s "checking if $compiler static flag $lt_tmp_static_flag works... " >&6; }
+if test ${lt_cv_prog_compiler_static_works+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_prog_compiler_static_works=no
+   save_LDFLAGS=$LDFLAGS
+   LDFLAGS="$LDFLAGS $lt_tmp_static_flag"
+   echo "$lt_simple_link_test_code" > conftest.$ac_ext
+   if (eval $ac_link 2>conftest.err) && test -s conftest$ac_exeext; then
+     # The linker can only warn and ignore the option if not recognized
+     # So say no if there are warnings
+     if test -s conftest.err; then
+       # Append any errors to the config.log.
+       cat conftest.err 1>&5
+       $ECHO "$_lt_linker_boilerplate" | $SED '/^$/d' > conftest.exp
+       $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2
+       if diff conftest.exp conftest.er2 >/dev/null; then
+         lt_cv_prog_compiler_static_works=yes
+       fi
+     else
+       lt_cv_prog_compiler_static_works=yes
+     fi
+   fi
+   $RM -r conftest*
+   LDFLAGS=$save_LDFLAGS
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_static_works" >&5
+printf "%s\n" "$lt_cv_prog_compiler_static_works" >&6; }
+
+if test yes = "$lt_cv_prog_compiler_static_works"; then
+    :
+else
+    lt_prog_compiler_static=
+fi
+
+
+
+
+
+
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if $compiler supports -c -o file.$ac_objext" >&5
+printf %s "checking if $compiler supports -c -o file.$ac_objext... " >&6; }
+if test ${lt_cv_prog_compiler_c_o+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_prog_compiler_c_o=no
+   $RM -r conftest 2>/dev/null
+   mkdir conftest
+   cd conftest
+   mkdir out
+   echo "$lt_simple_compile_test_code" > conftest.$ac_ext
+
+   lt_compiler_flag="-o out/conftest2.$ac_objext"
+   # Insert the option either (1) after the last *FLAGS variable, or
+   # (2) before a word containing "conftest.", or (3) at the end.
+   # Note that $ac_compile itself does not contain backslashes and begins
+   # with a dollar sign (not a hyphen), so the echo should work correctly.
+   lt_compile=`echo "$ac_compile" | $SED \
+   -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
+   -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
+   -e 's:$: $lt_compiler_flag:'`
+   (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&5)
+   (eval "$lt_compile" 2>out/conftest.err)
+   ac_status=$?
+   cat out/conftest.err >&5
+   echo "$as_me:$LINENO: \$? = $ac_status" >&5
+   if (exit $ac_status) && test -s out/conftest2.$ac_objext
+   then
+     # The compiler can only warn and ignore the option if not recognized
+     # So say no if there are warnings
+     $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' > out/conftest.exp
+     $SED '/^$/d; /^ *+/d' out/conftest.err >out/conftest.er2
+     if test ! -s out/conftest.er2 || diff out/conftest.exp out/conftest.er2 >/dev/null; then
+       lt_cv_prog_compiler_c_o=yes
+     fi
+   fi
+   chmod u+w . 2>&5
+   $RM conftest*
+   # SGI C++ compiler will create directory out/ii_files/ for
+   # template instantiation
+   test -d out/ii_files && $RM out/ii_files/* && rmdir out/ii_files
+   $RM out/* && rmdir out
+   cd ..
+   $RM -r conftest
+   $RM conftest*
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_c_o" >&5
+printf "%s\n" "$lt_cv_prog_compiler_c_o" >&6; }
+
+
+
+
+
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if $compiler supports -c -o file.$ac_objext" >&5
+printf %s "checking if $compiler supports -c -o file.$ac_objext... " >&6; }
+if test ${lt_cv_prog_compiler_c_o+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_prog_compiler_c_o=no
+   $RM -r conftest 2>/dev/null
+   mkdir conftest
+   cd conftest
+   mkdir out
+   echo "$lt_simple_compile_test_code" > conftest.$ac_ext
+
+   lt_compiler_flag="-o out/conftest2.$ac_objext"
+   # Insert the option either (1) after the last *FLAGS variable, or
+   # (2) before a word containing "conftest.", or (3) at the end.
+   # Note that $ac_compile itself does not contain backslashes and begins
+   # with a dollar sign (not a hyphen), so the echo should work correctly.
+   lt_compile=`echo "$ac_compile" | $SED \
+   -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
+   -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
+   -e 's:$: $lt_compiler_flag:'`
+   (eval echo "\"\$as_me:$LINENO: $lt_compile\"" >&5)
+   (eval "$lt_compile" 2>out/conftest.err)
+   ac_status=$?
+   cat out/conftest.err >&5
+   echo "$as_me:$LINENO: \$? = $ac_status" >&5
+   if (exit $ac_status) && test -s out/conftest2.$ac_objext
+   then
+     # The compiler can only warn and ignore the option if not recognized
+     # So say no if there are warnings
+     $ECHO "$_lt_compiler_boilerplate" | $SED '/^$/d' > out/conftest.exp
+     $SED '/^$/d; /^ *+/d' out/conftest.err >out/conftest.er2
+     if test ! -s out/conftest.er2 || diff out/conftest.exp out/conftest.er2 >/dev/null; then
+       lt_cv_prog_compiler_c_o=yes
+     fi
+   fi
+   chmod u+w . 2>&5
+   $RM conftest*
+   # SGI C++ compiler will create directory out/ii_files/ for
+   # template instantiation
+   test -d out/ii_files && $RM out/ii_files/* && rmdir out/ii_files
+   $RM out/* && rmdir out
+   cd ..
+   $RM -r conftest
+   $RM conftest*
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler_c_o" >&5
+printf "%s\n" "$lt_cv_prog_compiler_c_o" >&6; }
+
+
+
+
+hard_links=nottested
+if test no = "$lt_cv_prog_compiler_c_o" && test no != "$need_locks"; then
+  # do not overwrite the value of need_locks provided by the user
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if we can lock with hard links" >&5
+printf %s "checking if we can lock with hard links... " >&6; }
+  hard_links=yes
+  $RM conftest*
+  ln conftest.a conftest.b 2>/dev/null && hard_links=no
+  touch conftest.a
+  ln conftest.a conftest.b 2>&5 || hard_links=no
+  ln conftest.a conftest.b 2>/dev/null && hard_links=no
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $hard_links" >&5
+printf "%s\n" "$hard_links" >&6; }
+  if test no = "$hard_links"; then
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: '$CC' does not support '-c -o', so 'make -j' may be unsafe" >&5
+printf "%s\n" "$as_me: WARNING: '$CC' does not support '-c -o', so 'make -j' may be unsafe" >&2;}
+    need_locks=warn
+  fi
+else
+  need_locks=no
+fi
+
+
+
+
+
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the $compiler linker ($LD) supports shared libraries" >&5
+printf %s "checking whether the $compiler linker ($LD) supports shared libraries... " >&6; }
+
+  runpath_var=
+  allow_undefined_flag=
+  always_export_symbols=no
+  archive_cmds=
+  archive_expsym_cmds=
+  compiler_needs_object=no
+  enable_shared_with_static_runtimes=no
+  export_dynamic_flag_spec=
+  export_symbols_cmds='$NM $libobjs $convenience | $global_symbol_pipe | $SED '\''s/.* //'\'' | sort | uniq > $export_symbols'
+  hardcode_automatic=no
+  hardcode_direct=no
+  hardcode_direct_absolute=no
+  hardcode_libdir_flag_spec=
+  hardcode_libdir_separator=
+  hardcode_minus_L=no
+  hardcode_shlibpath_var=unsupported
+  inherit_rpath=no
+  link_all_deplibs=unknown
+  module_cmds=
+  module_expsym_cmds=
+  old_archive_from_new_cmds=
+  old_archive_from_expsyms_cmds=
+  thread_safe_flag_spec=
+  whole_archive_flag_spec=
+  # include_expsyms should be a list of space-separated symbols to be *always*
+  # included in the symbol list
+  include_expsyms=
+  # exclude_expsyms can be an extended regexp of symbols to exclude
+  # it will be wrapped by ' (' and ')$', so one must not match beginning or
+  # end of line.  Example: 'a|bc|.*d.*' will exclude the symbols 'a' and 'bc',
+  # as well as any symbol that contains 'd'.
+  exclude_expsyms='_GLOBAL_OFFSET_TABLE_|_GLOBAL__F[ID]_.*'
+  # Although _GLOBAL_OFFSET_TABLE_ is a valid symbol C name, most a.out
+  # platforms (ab)use it in PIC code, but their linkers get confused if
+  # the symbol is explicitly referenced.  Since portable code cannot
+  # rely on this symbol name, it's probably fine to never include it in
+  # preloaded symbol tables.
+  # Exclude shared library initialization/finalization symbols.
+  extract_expsyms_cmds=
+
+  case $host_os in
+  cygwin* | mingw* | pw32* | cegcc*)
+    # FIXME: the MSVC++ and ICC port hasn't been tested in a loooong time
+    # When not using gcc, we currently assume that we are using
+    # Microsoft Visual C++ or Intel C++ Compiler.
+    if test yes != "$GCC"; then
+      with_gnu_ld=no
+    fi
+    ;;
+  interix*)
+    # we just hope/assume this is gcc and not c89 (= MSVC++ or ICC)
+    with_gnu_ld=yes
+    ;;
+  openbsd* | bitrig*)
+    with_gnu_ld=no
+    ;;
+  linux* | k*bsd*-gnu | gnu*)
+    link_all_deplibs=no
+    ;;
+  esac
+
+  ld_shlibs=yes
+
+  # On some targets, GNU ld is compatible enough with the native linker
+  # that we're better off using the native interface for both.
+  lt_use_gnu_ld_interface=no
+  if test yes = "$with_gnu_ld"; then
+    case $host_os in
+      aix*)
+	# The AIX port of GNU ld has always aspired to compatibility
+	# with the native linker.  However, as the warning in the GNU ld
+	# block says, versions before 2.19.5* couldn't really create working
+	# shared libraries, regardless of the interface used.
+	case `$LD -v 2>&1` in
+	  *\ \(GNU\ Binutils\)\ 2.19.5*) ;;
+	  *\ \(GNU\ Binutils\)\ 2.[2-9]*) ;;
+	  *\ \(GNU\ Binutils\)\ [3-9]*) ;;
+	  *)
+	    lt_use_gnu_ld_interface=yes
+	    ;;
+	esac
+	;;
+      *)
+	lt_use_gnu_ld_interface=yes
+	;;
+    esac
+  fi
+
+  if test yes = "$lt_use_gnu_ld_interface"; then
+    # If archive_cmds runs LD, not CC, wlarc should be empty
+    wlarc='$wl'
+
+    # Set some defaults for GNU ld with shared library support. These
+    # are reset later if shared libraries are not supported. Putting them
+    # here allows them to be overridden if necessary.
+    runpath_var=LD_RUN_PATH
+    hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+    export_dynamic_flag_spec='$wl--export-dynamic'
+    # ancient GNU ld didn't support --whole-archive et. al.
+    if $LD --help 2>&1 | $GREP 'no-whole-archive' > /dev/null; then
+      whole_archive_flag_spec=$wlarc'--whole-archive$convenience '$wlarc'--no-whole-archive'
+    else
+      whole_archive_flag_spec=
+    fi
+    supports_anon_versioning=no
+    case `$LD -v | $SED -e 's/([^)]\+)\s\+//' 2>&1` in
+      *GNU\ gold*) supports_anon_versioning=yes ;;
+      *\ [01].* | *\ 2.[0-9].* | *\ 2.10.*) ;; # catch versions < 2.11
+      *\ 2.11.93.0.2\ *) supports_anon_versioning=yes ;; # RH7.3 ...
+      *\ 2.11.92.0.12\ *) supports_anon_versioning=yes ;; # Mandrake 8.2 ...
+      *\ 2.11.*) ;; # other 2.11 versions
+      *) supports_anon_versioning=yes ;;
+    esac
+
+    # See if GNU ld supports shared libraries.
+    case $host_os in
+    aix[3-9]*)
+      # On AIX/PPC, the GNU linker is very broken
+      if test ia64 != "$host_cpu"; then
+	ld_shlibs=no
+	cat <<_LT_EOF 1>&2
+
+*** Warning: the GNU linker, at least up to release 2.19, is reported
+*** to be unable to reliably create shared libraries on AIX.
+*** Therefore, libtool is disabling shared libraries support.  If you
+*** really care for shared libraries, you may want to install binutils
+*** 2.20 or above, or modify your PATH so that a non-GNU linker is found.
+*** You will then need to restart the configuration process.
+
+_LT_EOF
+      fi
+      ;;
+
+    amigaos*)
+      case $host_cpu in
+      powerpc)
+            # see comment about AmigaOS4 .so support
+            archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+            archive_expsym_cmds=''
+        ;;
+      m68k)
+            archive_cmds='$RM $output_objdir/a2ixlibrary.data~$ECHO "#define NAME $libname" > $output_objdir/a2ixlibrary.data~$ECHO "#define LIBRARY_ID 1" >> $output_objdir/a2ixlibrary.data~$ECHO "#define VERSION $major" >> $output_objdir/a2ixlibrary.data~$ECHO "#define REVISION $revision" >> $output_objdir/a2ixlibrary.data~$AR $AR_FLAGS $lib $libobjs~$RANLIB $lib~(cd $output_objdir && a2ixlibrary -32)'
+            hardcode_libdir_flag_spec='-L$libdir'
+            hardcode_minus_L=yes
+        ;;
+      esac
+      ;;
+
+    beos*)
+      if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	allow_undefined_flag=unsupported
+	# Joseph Beckenbach <jrb3@best.com> says some releases of gcc
+	# support --undefined.  This deserves some investigation.  FIXME
+	archive_cmds='$CC -nostart $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+      else
+	ld_shlibs=no
+      fi
+      ;;
+
+    cygwin* | mingw* | pw32* | cegcc*)
+      # _LT_TAGVAR(hardcode_libdir_flag_spec, ) is actually meaningless,
+      # as there is no search path for DLLs.
+      hardcode_libdir_flag_spec='-L$libdir'
+      export_dynamic_flag_spec='$wl--export-all-symbols'
+      allow_undefined_flag=unsupported
+      always_export_symbols=no
+      enable_shared_with_static_runtimes=yes
+      export_symbols_cmds='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[BCDGRS][ ]/s/.*[ ]\([^ ]*\)/\1 DATA/;s/^.*[ ]__nm__\([^ ]*\)[ ][^ ]*/\1 DATA/;/^I[ ]/d;/^[AITW][ ]/s/.* //'\'' | sort | uniq > $export_symbols'
+      exclude_expsyms='[_]+GLOBAL_OFFSET_TABLE_|[_]+GLOBAL__[FID]_.*|[_]+head_[A-Za-z0-9_]+_dll|[A-Za-z0-9_]+_dll_iname'
+
+      if $LD --help 2>&1 | $GREP 'auto-import' > /dev/null; then
+        archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib'
+	# If the export-symbols file already is a .def file, use it as
+	# is; otherwise, prepend EXPORTS...
+	archive_expsym_cmds='if   test DEF = "`$SED -n     -e '\''s/^[	 ]*//'\''     -e '\''/^\(;.*\)*$/d'\''     -e '\''s/^\(EXPORTS\|LIBRARY\)\([	 ].*\)*$/DEF/p'\''     -e q     $export_symbols`" ; then
+          cp $export_symbols $output_objdir/$soname.def;
+        else
+          echo EXPORTS > $output_objdir/$soname.def;
+          cat $export_symbols >> $output_objdir/$soname.def;
+        fi~
+        $CC -shared $output_objdir/$soname.def $libobjs $deplibs $compiler_flags -o $output_objdir/$soname $wl--enable-auto-image-base -Xlinker --out-implib -Xlinker $lib'
+      else
+	ld_shlibs=no
+      fi
+      ;;
+
+    haiku*)
+      archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+      link_all_deplibs=yes
+      ;;
+
+    os2*)
+      hardcode_libdir_flag_spec='-L$libdir'
+      hardcode_minus_L=yes
+      allow_undefined_flag=unsupported
+      shrext_cmds=.dll
+      archive_cmds='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	$ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	$ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	$ECHO EXPORTS >> $output_objdir/$libname.def~
+	emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~
+	$CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	emximp -o $lib $output_objdir/$libname.def'
+      archive_expsym_cmds='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	$ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	$ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	$ECHO EXPORTS >> $output_objdir/$libname.def~
+	prefix_cmds="$SED"~
+	if test EXPORTS = "`$SED 1q $export_symbols`"; then
+	  prefix_cmds="$prefix_cmds -e 1d";
+	fi~
+	prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~
+	cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~
+	$CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	emximp -o $lib $output_objdir/$libname.def'
+      old_archive_From_new_cmds='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def'
+      enable_shared_with_static_runtimes=yes
+      file_list_spec='@'
+      ;;
+
+    interix[3-9]*)
+      hardcode_direct=no
+      hardcode_shlibpath_var=no
+      hardcode_libdir_flag_spec='$wl-rpath,$libdir'
+      export_dynamic_flag_spec='$wl-E'
+      # Hack: On Interix 3.x, we cannot compile PIC because of a broken gcc.
+      # Instead, shared libraries are loaded at an image base (0x10000000 by
+      # default) and relocated if they conflict, which is a slow very memory
+      # consuming and fragmenting process.  To avoid this, we pick a random,
+      # 256 KiB-aligned image base between 0x50000000 and 0x6FFC0000 at link
+      # time.  Moving up from 0x10000000 also allows more sbrk(2) space.
+      archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib'
+      archive_expsym_cmds='$SED "s|^|_|" $export_symbols >$output_objdir/$soname.expsym~$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-h,$soname $wl--retain-symbols-file,$output_objdir/$soname.expsym $wl--image-base,`expr ${RANDOM-$$} % 4096 / 2 \* 262144 + 1342177280` -o $lib'
+      ;;
+
+    gnu* | linux* | tpf* | k*bsd*-gnu | kopensolaris*-gnu)
+      tmp_diet=no
+      if test linux-dietlibc = "$host_os"; then
+	case $cc_basename in
+	  diet\ *) tmp_diet=yes;;	# linux-dietlibc with static linking (!diet-dyn)
+	esac
+      fi
+      if $LD --help 2>&1 | $EGREP ': supported targets:.* elf' > /dev/null \
+	 && test no = "$tmp_diet"
+      then
+	tmp_addflag=' $pic_flag'
+	tmp_sharedflag='-shared'
+	case $cc_basename,$host_cpu in
+        pgcc*)				# Portland Group C compiler
+	  whole_archive_flag_spec='$wl--whole-archive`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	  tmp_addflag=' $pic_flag'
+	  ;;
+	pgf77* | pgf90* | pgf95* | pgfortran*)
+					# Portland Group f77 and f90 compilers
+	  whole_archive_flag_spec='$wl--whole-archive`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	  tmp_addflag=' $pic_flag -Mnomain' ;;
+	ecc*,ia64* | icc*,ia64*)	# Intel C compiler on ia64
+	  tmp_addflag=' -i_dynamic' ;;
+	efc*,ia64* | ifort*,ia64*)	# Intel Fortran compiler on ia64
+	  tmp_addflag=' -i_dynamic -nofor_main' ;;
+	ifc* | ifort*)			# Intel Fortran compiler
+	  tmp_addflag=' -nofor_main' ;;
+	lf95*)				# Lahey Fortran 8.1
+	  whole_archive_flag_spec=
+	  tmp_sharedflag='--shared' ;;
+        nagfor*)                        # NAGFOR 5.3
+          tmp_sharedflag='-Wl,-shared' ;;
+	xl[cC]* | bgxl[cC]* | mpixl[cC]*) # IBM XL C 8.0 on PPC (deal with xlf below)
+	  tmp_sharedflag='-qmkshrobj'
+	  tmp_addflag= ;;
+	nvcc*)	# Cuda Compiler Driver 2.2
+	  whole_archive_flag_spec='$wl--whole-archive`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	  compiler_needs_object=yes
+	  ;;
+	esac
+	case `$CC -V 2>&1 | $SED 5q` in
+	*Sun\ C*)			# Sun C 5.9
+	  whole_archive_flag_spec='$wl--whole-archive`new_convenience=; for conv in $convenience\"\"; do test -z \"$conv\" || new_convenience=\"$new_convenience,$conv\"; done; func_echo_all \"$new_convenience\"` $wl--no-whole-archive'
+	  compiler_needs_object=yes
+	  tmp_sharedflag='-G' ;;
+	*Sun\ F*)			# Sun Fortran 8.3
+	  tmp_sharedflag='-G' ;;
+	esac
+	archive_cmds='$CC '"$tmp_sharedflag""$tmp_addflag"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+
+        if test yes = "$supports_anon_versioning"; then
+          archive_expsym_cmds='echo "{ global:" > $output_objdir/$libname.ver~
+            cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~
+            echo "local: *; };" >> $output_objdir/$libname.ver~
+            $CC '"$tmp_sharedflag""$tmp_addflag"' $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-version-script $wl$output_objdir/$libname.ver -o $lib'
+        fi
+
+	case $cc_basename in
+	tcc*)
+	  hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+	  export_dynamic_flag_spec='-rdynamic'
+	  ;;
+	xlf* | bgf* | bgxlf* | mpixlf*)
+	  # IBM XL Fortran 10.1 on PPC cannot create shared libs itself
+	  whole_archive_flag_spec='--whole-archive$convenience --no-whole-archive'
+	  hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+	  archive_cmds='$LD -shared $libobjs $deplibs $linker_flags -soname $soname -o $lib'
+	  if test yes = "$supports_anon_versioning"; then
+	    archive_expsym_cmds='echo "{ global:" > $output_objdir/$libname.ver~
+              cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~
+              echo "local: *; };" >> $output_objdir/$libname.ver~
+              $LD -shared $libobjs $deplibs $linker_flags -soname $soname -version-script $output_objdir/$libname.ver -o $lib'
+	  fi
+	  ;;
+	esac
+      else
+        ld_shlibs=no
+      fi
+      ;;
+
+    netbsd* | netbsdelf*-gnu)
+      if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then
+	archive_cmds='$LD -Bshareable $libobjs $deplibs $linker_flags -o $lib'
+	wlarc=
+      else
+	archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	archive_expsym_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+      fi
+      ;;
+
+    solaris*)
+      if $LD -v 2>&1 | $GREP 'BFD 2\.8' > /dev/null; then
+	ld_shlibs=no
+	cat <<_LT_EOF 1>&2
+
+*** Warning: The releases 2.8.* of the GNU linker cannot reliably
+*** create shared libraries on Solaris systems.  Therefore, libtool
+*** is disabling shared libraries support.  We urge you to upgrade GNU
+*** binutils to release 2.9.1 or newer.  Another option is to modify
+*** your PATH or compiler configuration so that the native linker is
+*** used, and then restart.
+
+_LT_EOF
+      elif $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	archive_expsym_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+      else
+	ld_shlibs=no
+      fi
+      ;;
+
+    sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX*)
+      case `$LD -v 2>&1` in
+        *\ [01].* | *\ 2.[0-9].* | *\ 2.1[0-5].*)
+	ld_shlibs=no
+	cat <<_LT_EOF 1>&2
+
+*** Warning: Releases of the GNU linker prior to 2.16.91.0.3 cannot
+*** reliably create shared libraries on SCO systems.  Therefore, libtool
+*** is disabling shared libraries support.  We urge you to upgrade GNU
+*** binutils to release 2.16.91.0.3 or newer.  Another option is to modify
+*** your PATH or compiler configuration so that the native linker is
+*** used, and then restart.
+
+_LT_EOF
+	;;
+	*)
+	  # For security reasons, it is highly recommended that you always
+	  # use absolute paths for naming shared libraries, and exclude the
+	  # DT_RUNPATH tag from executables and libraries.  But doing so
+	  # requires that you compile everything twice, which is a pain.
+	  if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	    hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+	    archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	    archive_expsym_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+	  else
+	    ld_shlibs=no
+	  fi
+	;;
+      esac
+      ;;
+
+    sunos4*)
+      archive_cmds='$LD -assert pure-text -Bshareable -o $lib $libobjs $deplibs $linker_flags'
+      wlarc=
+      hardcode_direct=yes
+      hardcode_shlibpath_var=no
+      ;;
+
+    *)
+      if $LD --help 2>&1 | $GREP ': supported targets:.* elf' > /dev/null; then
+	archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+	archive_expsym_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-retain-symbols-file $wl$export_symbols -o $lib'
+      else
+	ld_shlibs=no
+      fi
+      ;;
+    esac
+
+    if test no = "$ld_shlibs"; then
+      runpath_var=
+      hardcode_libdir_flag_spec=
+      export_dynamic_flag_spec=
+      whole_archive_flag_spec=
+    fi
+  else
+    # PORTME fill in a description of your system's linker (not GNU ld)
+    case $host_os in
+    aix3*)
+      allow_undefined_flag=unsupported
+      always_export_symbols=yes
+      archive_expsym_cmds='$LD -o $output_objdir/$soname $libobjs $deplibs $linker_flags -bE:$export_symbols -T512 -H512 -bM:SRE~$AR $AR_FLAGS $lib $output_objdir/$soname'
+      # Note: this linker hardcodes the directories in LIBPATH if there
+      # are no directories specified by -L.
+      hardcode_minus_L=yes
+      if test yes = "$GCC" && test -z "$lt_prog_compiler_static"; then
+	# Neither direct hardcoding nor static linking is supported with a
+	# broken collect2.
+	hardcode_direct=unsupported
+      fi
+      ;;
+
+    aix[4-9]*)
+      if test ia64 = "$host_cpu"; then
+	# On IA64, the linker does run time linking by default, so we don't
+	# have to do anything special.
+	aix_use_runtimelinking=no
+	exp_sym_flag='-Bexport'
+	no_entry_flag=
+      else
+	# If we're using GNU nm, then we don't want the "-C" option.
+	# -C means demangle to GNU nm, but means don't demangle to AIX nm.
+	# Without the "-l" option, or with the "-B" option, AIX nm treats
+	# weak defined symbols like other global defined symbols, whereas
+	# GNU nm marks them as "W".
+	# While the 'weak' keyword is ignored in the Export File, we need
+	# it in the Import File for the 'aix-soname' feature, so we have
+	# to replace the "-B" option with "-P" for AIX nm.
+	if $NM -V 2>&1 | $GREP 'GNU' > /dev/null; then
+	  export_symbols_cmds='$NM -Bpg $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "W")) && (substr(\$ 3,1,1) != ".")) { if (\$ 2 == "W") { print \$ 3 " weak" } else { print \$ 3 } } }'\'' | sort -u > $export_symbols'
+	else
+	  export_symbols_cmds='`func_echo_all $NM | $SED -e '\''s/B\([^B]*\)$/P\1/'\''` -PCpgl $libobjs $convenience | awk '\''{ if (((\$ 2 == "T") || (\$ 2 == "D") || (\$ 2 == "B") || (\$ 2 == "L") || (\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) && (substr(\$ 1,1,1) != ".")) { if ((\$ 2 == "W") || (\$ 2 == "V") || (\$ 2 == "Z")) { print \$ 1 " weak" } else { print \$ 1 } } }'\'' | sort -u > $export_symbols'
+	fi
+	aix_use_runtimelinking=no
+
+	# Test if we are trying to use run time linking or normal
+	# AIX style linking. If -brtl is somewhere in LDFLAGS, we
+	# have runtime linking enabled, and use it for executables.
+	# For shared libraries, we enable/disable runtime linking
+	# depending on the kind of the shared library created -
+	# when "with_aix_soname,aix_use_runtimelinking" is:
+	# "aix,no"   lib.a(lib.so.V) shared, rtl:no,  for executables
+	# "aix,yes"  lib.so          shared, rtl:yes, for executables
+	#            lib.a           static archive
+	# "both,no"  lib.so.V(shr.o) shared, rtl:yes
+	#            lib.a(lib.so.V) shared, rtl:no,  for executables
+	# "both,yes" lib.so.V(shr.o) shared, rtl:yes, for executables
+	#            lib.a(lib.so.V) shared, rtl:no
+	# "svr4,*"   lib.so.V(shr.o) shared, rtl:yes, for executables
+	#            lib.a           static archive
+	case $host_os in aix4.[23]|aix4.[23].*|aix[5-9]*)
+	  for ld_flag in $LDFLAGS; do
+	  if (test x-brtl = "x$ld_flag" || test x-Wl,-brtl = "x$ld_flag"); then
+	    aix_use_runtimelinking=yes
+	    break
+	  fi
+	  done
+	  if test svr4,no = "$with_aix_soname,$aix_use_runtimelinking"; then
+	    # With aix-soname=svr4, we create the lib.so.V shared archives only,
+	    # so we don't have lib.a shared libs to link our executables.
+	    # We have to force runtime linking in this case.
+	    aix_use_runtimelinking=yes
+	    LDFLAGS="$LDFLAGS -Wl,-brtl"
+	  fi
+	  ;;
+	esac
+
+	exp_sym_flag='-bexport'
+	no_entry_flag='-bnoentry'
+      fi
+
+      # When large executables or shared objects are built, AIX ld can
+      # have problems creating the table of contents.  If linking a library
+      # or program results in "error TOC overflow" add -mminimal-toc to
+      # CXXFLAGS/CFLAGS for g++/gcc.  In the cases where that is not
+      # enough to fix the problem, add -Wl,-bbigtoc to LDFLAGS.
+
+      archive_cmds=''
+      hardcode_direct=yes
+      hardcode_direct_absolute=yes
+      hardcode_libdir_separator=':'
+      link_all_deplibs=yes
+      file_list_spec='$wl-f,'
+      case $with_aix_soname,$aix_use_runtimelinking in
+      aix,*) ;; # traditional, no import file
+      svr4,* | *,yes) # use import file
+	# The Import File defines what to hardcode.
+	hardcode_direct=no
+	hardcode_direct_absolute=no
+	;;
+      esac
+
+      if test yes = "$GCC"; then
+	case $host_os in aix4.[012]|aix4.[012].*)
+	# We only want to do this on AIX 4.2 and lower, the check
+	# below for broken collect2 doesn't work under 4.3+
+	  collect2name=`$CC -print-prog-name=collect2`
+	  if test -f "$collect2name" &&
+	   strings "$collect2name" | $GREP resolve_lib_name >/dev/null
+	  then
+	  # We have reworked collect2
+	  :
+	  else
+	  # We have old collect2
+	  hardcode_direct=unsupported
+	  # It fails to find uninstalled libraries when the uninstalled
+	  # path is not listed in the libpath.  Setting hardcode_minus_L
+	  # to unsupported forces relinking
+	  hardcode_minus_L=yes
+	  hardcode_libdir_flag_spec='-L$libdir'
+	  hardcode_libdir_separator=
+	  fi
+	  ;;
+	esac
+	shared_flag='-shared'
+	if test yes = "$aix_use_runtimelinking"; then
+	  shared_flag="$shared_flag "'$wl-G'
+	fi
+	# Need to ensure runtime linking is disabled for the traditional
+	# shared library, or the linker may eventually find shared libraries
+	# /with/ Import File - we do not want to mix them.
+	shared_flag_aix='-shared'
+	shared_flag_svr4='-shared $wl-G'
+      else
+	# not using gcc
+	if test ia64 = "$host_cpu"; then
+	# VisualAge C++, Version 5.5 for AIX 5L for IA-64, Beta 3 Release
+	# chokes on -Wl,-G. The following line is correct:
+	  shared_flag='-G'
+	else
+	  if test yes = "$aix_use_runtimelinking"; then
+	    shared_flag='$wl-G'
+	  else
+	    shared_flag='$wl-bM:SRE'
+	  fi
+	  shared_flag_aix='$wl-bM:SRE'
+	  shared_flag_svr4='$wl-G'
+	fi
+      fi
+
+      export_dynamic_flag_spec='$wl-bexpall'
+      # It seems that -bexpall does not export symbols beginning with
+      # underscore (_), so it is better to generate a list of symbols to export.
+      always_export_symbols=yes
+      if test aix,yes = "$with_aix_soname,$aix_use_runtimelinking"; then
+	# Warning - without using the other runtime loading flags (-brtl),
+	# -berok will link without error, but may produce a broken library.
+	allow_undefined_flag='-berok'
+        # Determine the default libpath from the value encoded in an
+        # empty executable.
+        if test set = "${lt_cv_aix_libpath+set}"; then
+  aix_libpath=$lt_cv_aix_libpath
+else
+  if test ${lt_cv_aix_libpath_+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+
+  lt_aix_libpath_sed='
+      /Import File Strings/,/^$/ {
+	  /^0/ {
+	      s/^0  *\([^ ]*\) *$/\1/
+	      p
+	  }
+      }'
+  lt_cv_aix_libpath_=`dump -H conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"`
+  # Check for a 64-bit object if we didn't find anything.
+  if test -z "$lt_cv_aix_libpath_"; then
+    lt_cv_aix_libpath_=`dump -HX64 conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"`
+  fi
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+  if test -z "$lt_cv_aix_libpath_"; then
+    lt_cv_aix_libpath_=/usr/lib:/lib
+  fi
+
+fi
+
+  aix_libpath=$lt_cv_aix_libpath_
+fi
+
+        hardcode_libdir_flag_spec='$wl-blibpath:$libdir:'"$aix_libpath"
+        archive_expsym_cmds='$CC -o $output_objdir/$soname $libobjs $deplibs $wl'$no_entry_flag' $compiler_flags `if test -n "$allow_undefined_flag"; then func_echo_all "$wl$allow_undefined_flag"; else :; fi` $wl'$exp_sym_flag:\$export_symbols' '$shared_flag
+      else
+	if test ia64 = "$host_cpu"; then
+	  hardcode_libdir_flag_spec='$wl-R $libdir:/usr/lib:/lib'
+	  allow_undefined_flag="-z nodefs"
+	  archive_expsym_cmds="\$CC $shared_flag"' -o $output_objdir/$soname $libobjs $deplibs '"\$wl$no_entry_flag"' $compiler_flags $wl$allow_undefined_flag '"\$wl$exp_sym_flag:\$export_symbols"
+	else
+	 # Determine the default libpath from the value encoded in an
+	 # empty executable.
+	 if test set = "${lt_cv_aix_libpath+set}"; then
+  aix_libpath=$lt_cv_aix_libpath
+else
+  if test ${lt_cv_aix_libpath_+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+
+  lt_aix_libpath_sed='
+      /Import File Strings/,/^$/ {
+	  /^0/ {
+	      s/^0  *\([^ ]*\) *$/\1/
+	      p
+	  }
+      }'
+  lt_cv_aix_libpath_=`dump -H conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"`
+  # Check for a 64-bit object if we didn't find anything.
+  if test -z "$lt_cv_aix_libpath_"; then
+    lt_cv_aix_libpath_=`dump -HX64 conftest$ac_exeext 2>/dev/null | $SED -n -e "$lt_aix_libpath_sed"`
+  fi
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+  if test -z "$lt_cv_aix_libpath_"; then
+    lt_cv_aix_libpath_=/usr/lib:/lib
+  fi
+
+fi
+
+  aix_libpath=$lt_cv_aix_libpath_
+fi
+
+	 hardcode_libdir_flag_spec='$wl-blibpath:$libdir:'"$aix_libpath"
+	  # Warning - without using the other run time loading flags,
+	  # -berok will link without error, but may produce a broken library.
+	  no_undefined_flag=' $wl-bernotok'
+	  allow_undefined_flag=' $wl-berok'
+	  if test yes = "$with_gnu_ld"; then
+	    # We only use this code for GNU lds that support --whole-archive.
+	    whole_archive_flag_spec='$wl--whole-archive$convenience $wl--no-whole-archive'
+	  else
+	    # Exported symbols can be pulled into shared objects from archives
+	    whole_archive_flag_spec='$convenience'
+	  fi
+	  archive_cmds_need_lc=yes
+	  archive_expsym_cmds='$RM -r $output_objdir/$realname.d~$MKDIR $output_objdir/$realname.d'
+	  # -brtl affects multiple linker settings, -berok does not and is overridden later
+	  compiler_flags_filtered='`func_echo_all "$compiler_flags " | $SED -e "s%-brtl\\([, ]\\)%-berok\\1%g"`'
+	  if test svr4 != "$with_aix_soname"; then
+	    # This is similar to how AIX traditionally builds its shared libraries.
+	    archive_expsym_cmds="$archive_expsym_cmds"'~$CC '$shared_flag_aix' -o $output_objdir/$realname.d/$soname $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$AR $AR_FLAGS $output_objdir/$libname$release.a $output_objdir/$realname.d/$soname'
+	  fi
+	  if test aix != "$with_aix_soname"; then
+	    archive_expsym_cmds="$archive_expsym_cmds"'~$CC '$shared_flag_svr4' -o $output_objdir/$realname.d/$shared_archive_member_spec.o $libobjs $deplibs $wl-bnoentry '$compiler_flags_filtered'$wl-bE:$export_symbols$allow_undefined_flag~$STRIP -e $output_objdir/$realname.d/$shared_archive_member_spec.o~( func_echo_all "#! $soname($shared_archive_member_spec.o)"; if test shr_64 = "$shared_archive_member_spec"; then func_echo_all "# 64"; else func_echo_all "# 32"; fi; cat $export_symbols ) > $output_objdir/$realname.d/$shared_archive_member_spec.imp~$AR $AR_FLAGS $output_objdir/$soname $output_objdir/$realname.d/$shared_archive_member_spec.o $output_objdir/$realname.d/$shared_archive_member_spec.imp'
+	  else
+	    # used by -dlpreopen to get the symbols
+	    archive_expsym_cmds="$archive_expsym_cmds"'~$MV  $output_objdir/$realname.d/$soname $output_objdir'
+	  fi
+	  archive_expsym_cmds="$archive_expsym_cmds"'~$RM -r $output_objdir/$realname.d'
+	fi
+      fi
+      ;;
+
+    amigaos*)
+      case $host_cpu in
+      powerpc)
+            # see comment about AmigaOS4 .so support
+            archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib'
+            archive_expsym_cmds=''
+        ;;
+      m68k)
+            archive_cmds='$RM $output_objdir/a2ixlibrary.data~$ECHO "#define NAME $libname" > $output_objdir/a2ixlibrary.data~$ECHO "#define LIBRARY_ID 1" >> $output_objdir/a2ixlibrary.data~$ECHO "#define VERSION $major" >> $output_objdir/a2ixlibrary.data~$ECHO "#define REVISION $revision" >> $output_objdir/a2ixlibrary.data~$AR $AR_FLAGS $lib $libobjs~$RANLIB $lib~(cd $output_objdir && a2ixlibrary -32)'
+            hardcode_libdir_flag_spec='-L$libdir'
+            hardcode_minus_L=yes
+        ;;
+      esac
+      ;;
+
+    bsdi[45]*)
+      export_dynamic_flag_spec=-rdynamic
+      ;;
+
+    cygwin* | mingw* | pw32* | cegcc*)
+      # When not using gcc, we currently assume that we are using
+      # Microsoft Visual C++ or Intel C++ Compiler.
+      # hardcode_libdir_flag_spec is actually meaningless, as there is
+      # no search path for DLLs.
+      case $cc_basename in
+      cl* | icl*)
+	# Native MSVC or ICC
+	hardcode_libdir_flag_spec=' '
+	allow_undefined_flag=unsupported
+	always_export_symbols=yes
+	file_list_spec='@'
+	# Tell ltmain to make .lib files, not .a files.
+	libext=lib
+	# Tell ltmain to make .dll files, not .so files.
+	shrext_cmds=.dll
+	# FIXME: Setting linknames here is a bad hack.
+	archive_cmds='$CC -o $output_objdir/$soname $libobjs $compiler_flags $deplibs -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~linknames='
+	archive_expsym_cmds='if   test DEF = "`$SED -n     -e '\''s/^[	 ]*//'\''     -e '\''/^\(;.*\)*$/d'\''     -e '\''s/^\(EXPORTS\|LIBRARY\)\([	 ].*\)*$/DEF/p'\''     -e q     $export_symbols`" ; then
+            cp "$export_symbols" "$output_objdir/$soname.def";
+            echo "$tool_output_objdir$soname.def" > "$output_objdir/$soname.exp";
+          else
+            $SED -e '\''s/^/-link -EXPORT:/'\'' < $export_symbols > $output_objdir/$soname.exp;
+          fi~
+          $CC -o $tool_output_objdir$soname $libobjs $compiler_flags $deplibs "@$tool_output_objdir$soname.exp" -Wl,-DLL,-IMPLIB:"$tool_output_objdir$libname.dll.lib"~
+          linknames='
+	# The linker will not automatically build a static lib if we build a DLL.
+	# _LT_TAGVAR(old_archive_from_new_cmds, )='true'
+	enable_shared_with_static_runtimes=yes
+	exclude_expsyms='_NULL_IMPORT_DESCRIPTOR|_IMPORT_DESCRIPTOR_.*'
+	export_symbols_cmds='$NM $libobjs $convenience | $global_symbol_pipe | $SED -e '\''/^[BCDGRS][ ]/s/.*[ ]\([^ ]*\)/\1,DATA/'\'' | $SED -e '\''/^[AITW][ ]/s/.*[ ]//'\'' | sort | uniq > $export_symbols'
+	# Don't use ranlib
+	old_postinstall_cmds='chmod 644 $oldlib'
+	postlink_cmds='lt_outputfile="@OUTPUT@"~
+          lt_tool_outputfile="@TOOL_OUTPUT@"~
+          case $lt_outputfile in
+            *.exe|*.EXE) ;;
+            *)
+              lt_outputfile=$lt_outputfile.exe
+              lt_tool_outputfile=$lt_tool_outputfile.exe
+              ;;
+          esac~
+          if test : != "$MANIFEST_TOOL" && test -f "$lt_outputfile.manifest"; then
+            $MANIFEST_TOOL -manifest "$lt_tool_outputfile.manifest" -outputresource:"$lt_tool_outputfile" || exit 1;
+            $RM "$lt_outputfile.manifest";
+          fi'
+	;;
+      *)
+	# Assume MSVC and ICC wrapper
+	hardcode_libdir_flag_spec=' '
+	allow_undefined_flag=unsupported
+	# Tell ltmain to make .lib files, not .a files.
+	libext=lib
+	# Tell ltmain to make .dll files, not .so files.
+	shrext_cmds=.dll
+	# FIXME: Setting linknames here is a bad hack.
+	archive_cmds='$CC -o $lib $libobjs $compiler_flags `func_echo_all "$deplibs" | $SED '\''s/ -lc$//'\''` -link -dll~linknames='
+	# The linker will automatically build a .lib file if we build a DLL.
+	old_archive_from_new_cmds='true'
+	# FIXME: Should let the user specify the lib program.
+	old_archive_cmds='lib -OUT:$oldlib$oldobjs$old_deplibs'
+	enable_shared_with_static_runtimes=yes
+	;;
+      esac
+      ;;
+
+    darwin* | rhapsody*)
+
+
+  archive_cmds_need_lc=no
+  hardcode_direct=no
+  hardcode_automatic=yes
+  hardcode_shlibpath_var=unsupported
+  if test yes = "$lt_cv_ld_force_load"; then
+    whole_archive_flag_spec='`for conv in $convenience\"\"; do test  -n \"$conv\" && new_convenience=\"$new_convenience $wl-force_load,$conv\"; done; func_echo_all \"$new_convenience\"`'
+
+  else
+    whole_archive_flag_spec=''
+  fi
+  link_all_deplibs=yes
+  allow_undefined_flag=$_lt_dar_allow_undefined
+  case $cc_basename in
+     ifort*|nagfor*) _lt_dar_can_shared=yes ;;
+     *) _lt_dar_can_shared=$GCC ;;
+  esac
+  if test yes = "$_lt_dar_can_shared"; then
+    output_verbose_link_cmd=func_echo_all
+    archive_cmds="\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$libobjs \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring $_lt_dar_single_mod$_lt_dsymutil"
+    module_cmds="\$CC \$allow_undefined_flag -o \$lib -bundle \$libobjs \$deplibs \$compiler_flags$_lt_dsymutil"
+    archive_expsym_cmds="$SED 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC -dynamiclib \$allow_undefined_flag -o \$lib \$libobjs \$deplibs \$compiler_flags -install_name \$rpath/\$soname \$verstring $_lt_dar_single_mod$_lt_dar_export_syms$_lt_dsymutil"
+    module_expsym_cmds="$SED -e 's|^|_|' < \$export_symbols > \$output_objdir/\$libname-symbols.expsym~\$CC \$allow_undefined_flag -o \$lib -bundle \$libobjs \$deplibs \$compiler_flags$_lt_dar_export_syms$_lt_dsymutil"
+
+  else
+  ld_shlibs=no
+  fi
+
+      ;;
+
+    dgux*)
+      archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+      hardcode_libdir_flag_spec='-L$libdir'
+      hardcode_shlibpath_var=no
+      ;;
+
+    # FreeBSD 2.2.[012] allows us to include c++rt0.o to get C++ constructor
+    # support.  Future versions do this automatically, but an explicit c++rt0.o
+    # does not break anything, and helps significantly (at the cost of a little
+    # extra space).
+    freebsd2.2*)
+      archive_cmds='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags /usr/lib/c++rt0.o'
+      hardcode_libdir_flag_spec='-R$libdir'
+      hardcode_direct=yes
+      hardcode_shlibpath_var=no
+      ;;
+
+    # Unfortunately, older versions of FreeBSD 2 do not have this feature.
+    freebsd2.*)
+      archive_cmds='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags'
+      hardcode_direct=yes
+      hardcode_minus_L=yes
+      hardcode_shlibpath_var=no
+      ;;
+
+    # FreeBSD 3 and greater uses gcc -shared to do shared libraries.
+    freebsd* | dragonfly* | midnightbsd*)
+      archive_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags'
+      hardcode_libdir_flag_spec='-R$libdir'
+      hardcode_direct=yes
+      hardcode_shlibpath_var=no
+      ;;
+
+    hpux9*)
+      if test yes = "$GCC"; then
+	archive_cmds='$RM $output_objdir/$soname~$CC -shared $pic_flag $wl+b $wl$install_libdir -o $output_objdir/$soname $libobjs $deplibs $compiler_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib'
+      else
+	archive_cmds='$RM $output_objdir/$soname~$LD -b +b $install_libdir -o $output_objdir/$soname $libobjs $deplibs $linker_flags~test "x$output_objdir/$soname" = "x$lib" || mv $output_objdir/$soname $lib'
+      fi
+      hardcode_libdir_flag_spec='$wl+b $wl$libdir'
+      hardcode_libdir_separator=:
+      hardcode_direct=yes
+
+      # hardcode_minus_L: Not really in the search PATH,
+      # but as the default location of the library.
+      hardcode_minus_L=yes
+      export_dynamic_flag_spec='$wl-E'
+      ;;
+
+    hpux10*)
+      if test yes,no = "$GCC,$with_gnu_ld"; then
+	archive_cmds='$CC -shared $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags'
+      else
+	archive_cmds='$LD -b +h $soname +b $install_libdir -o $lib $libobjs $deplibs $linker_flags'
+      fi
+      if test no = "$with_gnu_ld"; then
+	hardcode_libdir_flag_spec='$wl+b $wl$libdir'
+	hardcode_libdir_separator=:
+	hardcode_direct=yes
+	hardcode_direct_absolute=yes
+	export_dynamic_flag_spec='$wl-E'
+	# hardcode_minus_L: Not really in the search PATH,
+	# but as the default location of the library.
+	hardcode_minus_L=yes
+      fi
+      ;;
+
+    hpux11*)
+      if test yes,no = "$GCC,$with_gnu_ld"; then
+	case $host_cpu in
+	hppa*64*)
+	  archive_cmds='$CC -shared $wl+h $wl$soname -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	ia64*)
+	  archive_cmds='$CC -shared $pic_flag $wl+h $wl$soname $wl+nodefaultrpath -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	*)
+	  archive_cmds='$CC -shared $pic_flag $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	esac
+      else
+	case $host_cpu in
+	hppa*64*)
+	  archive_cmds='$CC -b $wl+h $wl$soname -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	ia64*)
+	  archive_cmds='$CC -b $wl+h $wl$soname $wl+nodefaultrpath -o $lib $libobjs $deplibs $compiler_flags'
+	  ;;
+	*)
+
+	  # Older versions of the 11.00 compiler do not understand -b yet
+	  # (HP92453-01 A.11.01.20 doesn't, HP92453-01 B.11.X.35175-35176.GP does)
+	  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if $CC understands -b" >&5
+printf %s "checking if $CC understands -b... " >&6; }
+if test ${lt_cv_prog_compiler__b+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_prog_compiler__b=no
+   save_LDFLAGS=$LDFLAGS
+   LDFLAGS="$LDFLAGS -b"
+   echo "$lt_simple_link_test_code" > conftest.$ac_ext
+   if (eval $ac_link 2>conftest.err) && test -s conftest$ac_exeext; then
+     # The linker can only warn and ignore the option if not recognized
+     # So say no if there are warnings
+     if test -s conftest.err; then
+       # Append any errors to the config.log.
+       cat conftest.err 1>&5
+       $ECHO "$_lt_linker_boilerplate" | $SED '/^$/d' > conftest.exp
+       $SED '/^$/d; /^ *+/d' conftest.err >conftest.er2
+       if diff conftest.exp conftest.er2 >/dev/null; then
+         lt_cv_prog_compiler__b=yes
+       fi
+     else
+       lt_cv_prog_compiler__b=yes
+     fi
+   fi
+   $RM -r conftest*
+   LDFLAGS=$save_LDFLAGS
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_prog_compiler__b" >&5
+printf "%s\n" "$lt_cv_prog_compiler__b" >&6; }
+
+if test yes = "$lt_cv_prog_compiler__b"; then
+    archive_cmds='$CC -b $wl+h $wl$soname $wl+b $wl$install_libdir -o $lib $libobjs $deplibs $compiler_flags'
+else
+    archive_cmds='$LD -b +h $soname +b $install_libdir -o $lib $libobjs $deplibs $linker_flags'
+fi
+
+	  ;;
+	esac
+      fi
+      if test no = "$with_gnu_ld"; then
+	hardcode_libdir_flag_spec='$wl+b $wl$libdir'
+	hardcode_libdir_separator=:
+
+	case $host_cpu in
+	hppa*64*|ia64*)
+	  hardcode_direct=no
+	  hardcode_shlibpath_var=no
+	  ;;
+	*)
+	  hardcode_direct=yes
+	  hardcode_direct_absolute=yes
+	  export_dynamic_flag_spec='$wl-E'
+
+	  # hardcode_minus_L: Not really in the search PATH,
+	  # but as the default location of the library.
+	  hardcode_minus_L=yes
+	  ;;
+	esac
+      fi
+      ;;
+
+    irix5* | irix6* | nonstopux*)
+      if test yes = "$GCC"; then
+	archive_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+	# Try to use the -exported_symbol ld option, if it does not
+	# work, assume that -exports_file does not work either and
+	# implicitly export all symbols.
+	# This should be the same for all languages, so no per-tag cache variable.
+	{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the $host_os linker accepts -exported_symbol" >&5
+printf %s "checking whether the $host_os linker accepts -exported_symbol... " >&6; }
+if test ${lt_cv_irix_exported_symbol+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  save_LDFLAGS=$LDFLAGS
+	   LDFLAGS="$LDFLAGS -shared $wl-exported_symbol ${wl}foo $wl-update_registry $wl/dev/null"
+	   cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+int foo (void) { return 0; }
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  lt_cv_irix_exported_symbol=yes
+else $as_nop
+  lt_cv_irix_exported_symbol=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+           LDFLAGS=$save_LDFLAGS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_irix_exported_symbol" >&5
+printf "%s\n" "$lt_cv_irix_exported_symbol" >&6; }
+	if test yes = "$lt_cv_irix_exported_symbol"; then
+          archive_expsym_cmds='$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations $wl-exports_file $wl$export_symbols -o $lib'
+	fi
+	link_all_deplibs=no
+      else
+	archive_cmds='$CC -shared $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+	archive_expsym_cmds='$CC -shared $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -exports_file $export_symbols -o $lib'
+      fi
+      archive_cmds_need_lc='no'
+      hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+      hardcode_libdir_separator=:
+      inherit_rpath=yes
+      link_all_deplibs=yes
+      ;;
+
+    linux*)
+      case $cc_basename in
+      tcc*)
+	# Fabrice Bellard et al's Tiny C Compiler
+	ld_shlibs=yes
+	archive_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags'
+	hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+	;;
+      esac
+      ;;
+
+    netbsd* | netbsdelf*-gnu)
+      if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then
+	archive_cmds='$LD -Bshareable -o $lib $libobjs $deplibs $linker_flags'  # a.out
+      else
+	archive_cmds='$LD -shared -o $lib $libobjs $deplibs $linker_flags'      # ELF
+      fi
+      hardcode_libdir_flag_spec='-R$libdir'
+      hardcode_direct=yes
+      hardcode_shlibpath_var=no
+      ;;
+
+    newsos6)
+      archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+      hardcode_direct=yes
+      hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+      hardcode_libdir_separator=:
+      hardcode_shlibpath_var=no
+      ;;
+
+    *nto* | *qnx*)
+      ;;
+
+    openbsd* | bitrig*)
+      if test -f /usr/libexec/ld.so; then
+	hardcode_direct=yes
+	hardcode_shlibpath_var=no
+	hardcode_direct_absolute=yes
+	if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then
+	  archive_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags'
+	  archive_expsym_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags $wl-retain-symbols-file,$export_symbols'
+	  hardcode_libdir_flag_spec='$wl-rpath,$libdir'
+	  export_dynamic_flag_spec='$wl-E'
+	else
+	  archive_cmds='$CC -shared $pic_flag -o $lib $libobjs $deplibs $compiler_flags'
+	  hardcode_libdir_flag_spec='$wl-rpath,$libdir'
+	fi
+      else
+	ld_shlibs=no
+      fi
+      ;;
+
+    os2*)
+      hardcode_libdir_flag_spec='-L$libdir'
+      hardcode_minus_L=yes
+      allow_undefined_flag=unsupported
+      shrext_cmds=.dll
+      archive_cmds='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	$ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	$ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	$ECHO EXPORTS >> $output_objdir/$libname.def~
+	emxexp $libobjs | $SED /"_DLL_InitTerm"/d >> $output_objdir/$libname.def~
+	$CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	emximp -o $lib $output_objdir/$libname.def'
+      archive_expsym_cmds='$ECHO "LIBRARY ${soname%$shared_ext} INITINSTANCE TERMINSTANCE" > $output_objdir/$libname.def~
+	$ECHO "DESCRIPTION \"$libname\"" >> $output_objdir/$libname.def~
+	$ECHO "DATA MULTIPLE NONSHARED" >> $output_objdir/$libname.def~
+	$ECHO EXPORTS >> $output_objdir/$libname.def~
+	prefix_cmds="$SED"~
+	if test EXPORTS = "`$SED 1q $export_symbols`"; then
+	  prefix_cmds="$prefix_cmds -e 1d";
+	fi~
+	prefix_cmds="$prefix_cmds -e \"s/^\(.*\)$/_\1/g\""~
+	cat $export_symbols | $prefix_cmds >> $output_objdir/$libname.def~
+	$CC -Zdll -Zcrtdll -o $output_objdir/$soname $libobjs $deplibs $compiler_flags $output_objdir/$libname.def~
+	emximp -o $lib $output_objdir/$libname.def'
+      old_archive_From_new_cmds='emximp -o $output_objdir/${libname}_dll.a $output_objdir/$libname.def'
+      enable_shared_with_static_runtimes=yes
+      file_list_spec='@'
+      ;;
+
+    osf3*)
+      if test yes = "$GCC"; then
+	allow_undefined_flag=' $wl-expect_unresolved $wl\*'
+	archive_cmds='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+      else
+	allow_undefined_flag=' -expect_unresolved \*'
+	archive_cmds='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+      fi
+      archive_cmds_need_lc='no'
+      hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+      hardcode_libdir_separator=:
+      ;;
+
+    osf4* | osf5*)	# as osf3* with the addition of -msym flag
+      if test yes = "$GCC"; then
+	allow_undefined_flag=' $wl-expect_unresolved $wl\*'
+	archive_cmds='$CC -shared$allow_undefined_flag $pic_flag $libobjs $deplibs $compiler_flags $wl-msym $wl-soname $wl$soname `test -n "$verstring" && func_echo_all "$wl-set_version $wl$verstring"` $wl-update_registry $wl$output_objdir/so_locations -o $lib'
+	hardcode_libdir_flag_spec='$wl-rpath $wl$libdir'
+      else
+	allow_undefined_flag=' -expect_unresolved \*'
+	archive_cmds='$CC -shared$allow_undefined_flag $libobjs $deplibs $compiler_flags -msym -soname $soname `test -n "$verstring" && func_echo_all "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib'
+	archive_expsym_cmds='for i in `cat $export_symbols`; do printf "%s %s\\n" -exported_symbol "\$i" >> $lib.exp; done; printf "%s\\n" "-hidden">> $lib.exp~
+          $CC -shared$allow_undefined_flag $wl-input $wl$lib.exp $compiler_flags $libobjs $deplibs -soname $soname `test -n "$verstring" && $ECHO "-set_version $verstring"` -update_registry $output_objdir/so_locations -o $lib~$RM $lib.exp'
+
+	# Both c and cxx compiler support -rpath directly
+	hardcode_libdir_flag_spec='-rpath $libdir'
+      fi
+      archive_cmds_need_lc='no'
+      hardcode_libdir_separator=:
+      ;;
+
+    solaris*)
+      no_undefined_flag=' -z defs'
+      if test yes = "$GCC"; then
+	wlarc='$wl'
+	archive_cmds='$CC -shared $pic_flag $wl-z ${wl}text $wl-h $wl$soname -o $lib $libobjs $deplibs $compiler_flags'
+	archive_expsym_cmds='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+          $CC -shared $pic_flag $wl-z ${wl}text $wl-M $wl$lib.exp $wl-h $wl$soname -o $lib $libobjs $deplibs $compiler_flags~$RM $lib.exp'
+      else
+	case `$CC -V 2>&1` in
+	*"Compilers 5.0"*)
+	  wlarc=''
+	  archive_cmds='$LD -G$allow_undefined_flag -h $soname -o $lib $libobjs $deplibs $linker_flags'
+	  archive_expsym_cmds='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+            $LD -G$allow_undefined_flag -M $lib.exp -h $soname -o $lib $libobjs $deplibs $linker_flags~$RM $lib.exp'
+	  ;;
+	*)
+	  wlarc='$wl'
+	  archive_cmds='$CC -G$allow_undefined_flag -h $soname -o $lib $libobjs $deplibs $compiler_flags'
+	  archive_expsym_cmds='echo "{ global:" > $lib.exp~cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $lib.exp~echo "local: *; };" >> $lib.exp~
+            $CC -G$allow_undefined_flag -M $lib.exp -h $soname -o $lib $libobjs $deplibs $compiler_flags~$RM $lib.exp'
+	  ;;
+	esac
+      fi
+      hardcode_libdir_flag_spec='-R$libdir'
+      hardcode_shlibpath_var=no
+      case $host_os in
+      solaris2.[0-5] | solaris2.[0-5].*) ;;
+      *)
+	# The compiler driver will combine and reorder linker options,
+	# but understands '-z linker_flag'.  GCC discards it without '$wl',
+	# but is careful enough not to reorder.
+	# Supported since Solaris 2.6 (maybe 2.5.1?)
+	if test yes = "$GCC"; then
+	  whole_archive_flag_spec='$wl-z ${wl}allextract$convenience $wl-z ${wl}defaultextract'
+	else
+	  whole_archive_flag_spec='-z allextract$convenience -z defaultextract'
+	fi
+	;;
+      esac
+      link_all_deplibs=yes
+      ;;
+
+    sunos4*)
+      if test sequent = "$host_vendor"; then
+	# Use $CC to link under sequent, because it throws in some extra .o
+	# files that make .init and .fini sections work.
+	archive_cmds='$CC -G $wl-h $soname -o $lib $libobjs $deplibs $compiler_flags'
+      else
+	archive_cmds='$LD -assert pure-text -Bstatic -o $lib $libobjs $deplibs $linker_flags'
+      fi
+      hardcode_libdir_flag_spec='-L$libdir'
+      hardcode_direct=yes
+      hardcode_minus_L=yes
+      hardcode_shlibpath_var=no
+      ;;
+
+    sysv4)
+      case $host_vendor in
+	sni)
+	  archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+	  hardcode_direct=yes # is this really true???
+	;;
+	siemens)
+	  ## LD is ld it makes a PLAMLIB
+	  ## CC just makes a GrossModule.
+	  archive_cmds='$LD -G -o $lib $libobjs $deplibs $linker_flags'
+	  reload_cmds='$CC -r -o $output$reload_objs'
+	  hardcode_direct=no
+        ;;
+	motorola)
+	  archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+	  hardcode_direct=no #Motorola manual says yes, but my tests say they lie
+	;;
+      esac
+      runpath_var='LD_RUN_PATH'
+      hardcode_shlibpath_var=no
+      ;;
+
+    sysv4.3*)
+      archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+      hardcode_shlibpath_var=no
+      export_dynamic_flag_spec='-Bexport'
+      ;;
+
+    sysv4*MP*)
+      if test -d /usr/nec; then
+	archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+	hardcode_shlibpath_var=no
+	runpath_var=LD_RUN_PATH
+	hardcode_runpath_var=yes
+	ld_shlibs=yes
+      fi
+      ;;
+
+    sysv4*uw2* | sysv5OpenUNIX* | sysv5UnixWare7.[01].[10]* | unixware7* | sco3.2v5.0.[024]*)
+      no_undefined_flag='$wl-z,text'
+      archive_cmds_need_lc=no
+      hardcode_shlibpath_var=no
+      runpath_var='LD_RUN_PATH'
+
+      if test yes = "$GCC"; then
+	archive_cmds='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	archive_expsym_cmds='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+      else
+	archive_cmds='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	archive_expsym_cmds='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+      fi
+      ;;
+
+    sysv5* | sco3.2v5* | sco5v6*)
+      # Note: We CANNOT use -z defs as we might desire, because we do not
+      # link with -lc, and that would cause any symbols used from libc to
+      # always be unresolved, which means just about no library would
+      # ever link correctly.  If we're not using GNU ld we use -z text
+      # though, which does catch some bad symbols but isn't as heavy-handed
+      # as -z defs.
+      no_undefined_flag='$wl-z,text'
+      allow_undefined_flag='$wl-z,nodefs'
+      archive_cmds_need_lc=no
+      hardcode_shlibpath_var=no
+      hardcode_libdir_flag_spec='$wl-R,$libdir'
+      hardcode_libdir_separator=':'
+      link_all_deplibs=yes
+      export_dynamic_flag_spec='$wl-Bexport'
+      runpath_var='LD_RUN_PATH'
+
+      if test yes = "$GCC"; then
+	archive_cmds='$CC -shared $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	archive_expsym_cmds='$CC -shared $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+      else
+	archive_cmds='$CC -G $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+	archive_expsym_cmds='$CC -G $wl-Bexport:$export_symbols $wl-h,$soname -o $lib $libobjs $deplibs $compiler_flags'
+      fi
+      ;;
+
+    uts4*)
+      archive_cmds='$LD -G -h $soname -o $lib $libobjs $deplibs $linker_flags'
+      hardcode_libdir_flag_spec='-L$libdir'
+      hardcode_shlibpath_var=no
+      ;;
+
+    *)
+      ld_shlibs=no
+      ;;
+    esac
+
+    if test sni = "$host_vendor"; then
+      case $host in
+      sysv4 | sysv4.2uw2* | sysv4.3* | sysv5*)
+	export_dynamic_flag_spec='$wl-Blargedynsym'
+	;;
+      esac
+    fi
+  fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ld_shlibs" >&5
+printf "%s\n" "$ld_shlibs" >&6; }
+test no = "$ld_shlibs" && can_build_shared=no
+
+with_gnu_ld=$with_gnu_ld
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+# Do we need to explicitly link libc?
+#
+case "x$archive_cmds_need_lc" in
+x|xyes)
+  # Assume -lc should be added
+  archive_cmds_need_lc=yes
+
+  if test yes,yes = "$GCC,$enable_shared"; then
+    case $archive_cmds in
+    *'~'*)
+      # FIXME: we may have to deal with multi-command sequences.
+      ;;
+    '$CC '*)
+      # Test whether the compiler implicitly links with -lc since on some
+      # systems, -lgcc has to come before -lc. If gcc already passes -lc
+      # to ld, don't add -lc before -lgcc.
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether -lc should be explicitly linked in" >&5
+printf %s "checking whether -lc should be explicitly linked in... " >&6; }
+if test ${lt_cv_archive_cmds_need_lc+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  $RM conftest*
+	echo "$lt_simple_compile_test_code" > conftest.$ac_ext
+
+	if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
+  (eval $ac_compile) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; } 2>conftest.err; then
+	  soname=conftest
+	  lib=conftest
+	  libobjs=conftest.$ac_objext
+	  deplibs=
+	  wl=$lt_prog_compiler_wl
+	  pic_flag=$lt_prog_compiler_pic
+	  compiler_flags=-v
+	  linker_flags=-v
+	  verstring=
+	  output_objdir=.
+	  libname=conftest
+	  lt_save_allow_undefined_flag=$allow_undefined_flag
+	  allow_undefined_flag=
+	  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$archive_cmds 2\>\&1 \| $GREP \" -lc \" \>/dev/null 2\>\&1\""; } >&5
+  (eval $archive_cmds 2\>\&1 \| $GREP \" -lc \" \>/dev/null 2\>\&1) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }
+	  then
+	    lt_cv_archive_cmds_need_lc=no
+	  else
+	    lt_cv_archive_cmds_need_lc=yes
+	  fi
+	  allow_undefined_flag=$lt_save_allow_undefined_flag
+	else
+	  cat conftest.err 1>&5
+	fi
+	$RM conftest*
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_archive_cmds_need_lc" >&5
+printf "%s\n" "$lt_cv_archive_cmds_need_lc" >&6; }
+      archive_cmds_need_lc=$lt_cv_archive_cmds_need_lc
+      ;;
+    esac
+  fi
+  ;;
+esac
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking dynamic linker characteristics" >&5
+printf %s "checking dynamic linker characteristics... " >&6; }
+
+if test yes = "$GCC"; then
+  case $host_os in
+    darwin*) lt_awk_arg='/^libraries:/,/LR/' ;;
+    *) lt_awk_arg='/^libraries:/' ;;
+  esac
+  case $host_os in
+    mingw* | cegcc*) lt_sed_strip_eq='s|=\([A-Za-z]:\)|\1|g' ;;
+    *) lt_sed_strip_eq='s|=/|/|g' ;;
+  esac
+  lt_search_path_spec=`$CC -print-search-dirs | awk $lt_awk_arg | $SED -e "s/^libraries://" -e $lt_sed_strip_eq`
+  case $lt_search_path_spec in
+  *\;*)
+    # if the path contains ";" then we assume it to be the separator
+    # otherwise default to the standard path separator (i.e. ":") - it is
+    # assumed that no part of a normal pathname contains ";" but that should
+    # okay in the real world where ";" in dirpaths is itself problematic.
+    lt_search_path_spec=`$ECHO "$lt_search_path_spec" | $SED 's/;/ /g'`
+    ;;
+  *)
+    lt_search_path_spec=`$ECHO "$lt_search_path_spec" | $SED "s/$PATH_SEPARATOR/ /g"`
+    ;;
+  esac
+  # Ok, now we have the path, separated by spaces, we can step through it
+  # and add multilib dir if necessary...
+  lt_tmp_lt_search_path_spec=
+  lt_multi_os_dir=/`$CC $CPPFLAGS $CFLAGS $LDFLAGS -print-multi-os-directory 2>/dev/null`
+  # ...but if some path component already ends with the multilib dir we assume
+  # that all is fine and trust -print-search-dirs as is (GCC 4.2? or newer).
+  case "$lt_multi_os_dir; $lt_search_path_spec " in
+  "/; "* | "/.; "* | "/./; "* | *"$lt_multi_os_dir "* | *"$lt_multi_os_dir/ "*)
+    lt_multi_os_dir=
+    ;;
+  esac
+  for lt_sys_path in $lt_search_path_spec; do
+    if test -d "$lt_sys_path$lt_multi_os_dir"; then
+      lt_tmp_lt_search_path_spec="$lt_tmp_lt_search_path_spec $lt_sys_path$lt_multi_os_dir"
+    elif test -n "$lt_multi_os_dir"; then
+      test -d "$lt_sys_path" && \
+	lt_tmp_lt_search_path_spec="$lt_tmp_lt_search_path_spec $lt_sys_path"
+    fi
+  done
+  lt_search_path_spec=`$ECHO "$lt_tmp_lt_search_path_spec" | awk '
+BEGIN {RS = " "; FS = "/|\n";} {
+  lt_foo = "";
+  lt_count = 0;
+  for (lt_i = NF; lt_i > 0; lt_i--) {
+    if ($lt_i != "" && $lt_i != ".") {
+      if ($lt_i == "..") {
+        lt_count++;
+      } else {
+        if (lt_count == 0) {
+          lt_foo = "/" $lt_i lt_foo;
+        } else {
+          lt_count--;
+        }
+      }
+    }
+  }
+  if (lt_foo != "") { lt_freq[lt_foo]++; }
+  if (lt_freq[lt_foo] == 1) { print lt_foo; }
+}'`
+  # AWK program above erroneously prepends '/' to C:/dos/paths
+  # for these hosts.
+  case $host_os in
+    mingw* | cegcc*) lt_search_path_spec=`$ECHO "$lt_search_path_spec" |\
+      $SED 's|/\([A-Za-z]:\)|\1|g'` ;;
+  esac
+  sys_lib_search_path_spec=`$ECHO "$lt_search_path_spec" | $lt_NL2SP`
+else
+  sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib"
+fi
+library_names_spec=
+libname_spec='lib$name'
+soname_spec=
+shrext_cmds=.so
+postinstall_cmds=
+postuninstall_cmds=
+finish_cmds=
+finish_eval=
+shlibpath_var=
+shlibpath_overrides_runpath=unknown
+version_type=none
+dynamic_linker="$host_os ld.so"
+sys_lib_dlsearch_path_spec="/lib /usr/lib"
+need_lib_prefix=unknown
+hardcode_into_libs=no
+
+# when you set need_version to no, make sure it does not cause -set_version
+# flags to be left without arguments
+need_version=unknown
+
+
+
+case $host_os in
+aix3*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  library_names_spec='$libname$release$shared_ext$versuffix $libname.a'
+  shlibpath_var=LIBPATH
+
+  # AIX 3 has no versioning support, so we append a major version to the name.
+  soname_spec='$libname$release$shared_ext$major'
+  ;;
+
+aix[4-9]*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  hardcode_into_libs=yes
+  if test ia64 = "$host_cpu"; then
+    # AIX 5 supports IA64
+    library_names_spec='$libname$release$shared_ext$major $libname$release$shared_ext$versuffix $libname$shared_ext'
+    shlibpath_var=LD_LIBRARY_PATH
+  else
+    # With GCC up to 2.95.x, collect2 would create an import file
+    # for dependence libraries.  The import file would start with
+    # the line '#! .'.  This would cause the generated library to
+    # depend on '.', always an invalid library.  This was fixed in
+    # development snapshots of GCC prior to 3.0.
+    case $host_os in
+      aix4 | aix4.[01] | aix4.[01].*)
+      if { echo '#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 97)'
+	   echo ' yes '
+	   echo '#endif'; } | $CC -E - | $GREP yes > /dev/null; then
+	:
+      else
+	can_build_shared=no
+      fi
+      ;;
+    esac
+    # Using Import Files as archive members, it is possible to support
+    # filename-based versioning of shared library archives on AIX. While
+    # this would work for both with and without runtime linking, it will
+    # prevent static linking of such archives. So we do filename-based
+    # shared library versioning with .so extension only, which is used
+    # when both runtime linking and shared linking is enabled.
+    # Unfortunately, runtime linking may impact performance, so we do
+    # not want this to be the default eventually. Also, we use the
+    # versioned .so libs for executables only if there is the -brtl
+    # linker flag in LDFLAGS as well, or --with-aix-soname=svr4 only.
+    # To allow for filename-based versioning support, we need to create
+    # libNAME.so.V as an archive file, containing:
+    # *) an Import File, referring to the versioned filename of the
+    #    archive as well as the shared archive member, telling the
+    #    bitwidth (32 or 64) of that shared object, and providing the
+    #    list of exported symbols of that shared object, eventually
+    #    decorated with the 'weak' keyword
+    # *) the shared object with the F_LOADONLY flag set, to really avoid
+    #    it being seen by the linker.
+    # At run time we better use the real file rather than another symlink,
+    # but for link time we create the symlink libNAME.so -> libNAME.so.V
+
+    case $with_aix_soname,$aix_use_runtimelinking in
+    # AIX (on Power*) has no versioning support, so currently we cannot hardcode correct
+    # soname into executable. Probably we can add versioning support to
+    # collect2, so additional links can be useful in future.
+    aix,yes) # traditional libtool
+      dynamic_linker='AIX unversionable lib.so'
+      # If using run time linking (on AIX 4.2 or later) use lib<name>.so
+      # instead of lib<name>.a to let people know that these are not
+      # typical AIX shared libraries.
+      library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+      ;;
+    aix,no) # traditional AIX only
+      dynamic_linker='AIX lib.a(lib.so.V)'
+      # We preserve .a as extension for shared libraries through AIX4.2
+      # and later when we are not doing run time linking.
+      library_names_spec='$libname$release.a $libname.a'
+      soname_spec='$libname$release$shared_ext$major'
+      ;;
+    svr4,*) # full svr4 only
+      dynamic_linker="AIX lib.so.V($shared_archive_member_spec.o)"
+      library_names_spec='$libname$release$shared_ext$major $libname$shared_ext'
+      # We do not specify a path in Import Files, so LIBPATH fires.
+      shlibpath_overrides_runpath=yes
+      ;;
+    *,yes) # both, prefer svr4
+      dynamic_linker="AIX lib.so.V($shared_archive_member_spec.o), lib.a(lib.so.V)"
+      library_names_spec='$libname$release$shared_ext$major $libname$shared_ext'
+      # unpreferred sharedlib libNAME.a needs extra handling
+      postinstall_cmds='test -n "$linkname" || linkname="$realname"~func_stripname "" ".so" "$linkname"~$install_shared_prog "$dir/$func_stripname_result.$libext" "$destdir/$func_stripname_result.$libext"~test -z "$tstripme" || test -z "$striplib" || $striplib "$destdir/$func_stripname_result.$libext"'
+      postuninstall_cmds='for n in $library_names $old_library; do :; done~func_stripname "" ".so" "$n"~test "$func_stripname_result" = "$n" || func_append rmfiles " $odir/$func_stripname_result.$libext"'
+      # We do not specify a path in Import Files, so LIBPATH fires.
+      shlibpath_overrides_runpath=yes
+      ;;
+    *,no) # both, prefer aix
+      dynamic_linker="AIX lib.a(lib.so.V), lib.so.V($shared_archive_member_spec.o)"
+      library_names_spec='$libname$release.a $libname.a'
+      soname_spec='$libname$release$shared_ext$major'
+      # unpreferred sharedlib libNAME.so.V and symlink libNAME.so need extra handling
+      postinstall_cmds='test -z "$dlname" || $install_shared_prog $dir/$dlname $destdir/$dlname~test -z "$tstripme" || test -z "$striplib" || $striplib $destdir/$dlname~test -n "$linkname" || linkname=$realname~func_stripname "" ".a" "$linkname"~(cd "$destdir" && $LN_S -f $dlname $func_stripname_result.so)'
+      postuninstall_cmds='test -z "$dlname" || func_append rmfiles " $odir/$dlname"~for n in $old_library $library_names; do :; done~func_stripname "" ".a" "$n"~func_append rmfiles " $odir/$func_stripname_result.so"'
+      ;;
+    esac
+    shlibpath_var=LIBPATH
+  fi
+  ;;
+
+amigaos*)
+  case $host_cpu in
+  powerpc)
+    # Since July 2007 AmigaOS4 officially supports .so libraries.
+    # When compiling the executable, add -use-dynld -Lsobjs: to the compileline.
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    ;;
+  m68k)
+    library_names_spec='$libname.ixlibrary $libname.a'
+    # Create ${libname}_ixlibrary.a entries in /sys/libs.
+    finish_eval='for lib in `ls $libdir/*.ixlibrary 2>/dev/null`; do libname=`func_echo_all "$lib" | $SED '\''s%^.*/\([^/]*\)\.ixlibrary$%\1%'\''`; $RM /sys/libs/${libname}_ixlibrary.a; $show "cd /sys/libs && $LN_S $lib ${libname}_ixlibrary.a"; cd /sys/libs && $LN_S $lib ${libname}_ixlibrary.a || exit 1; done'
+    ;;
+  esac
+  ;;
+
+beos*)
+  library_names_spec='$libname$shared_ext'
+  dynamic_linker="$host_os ld.so"
+  shlibpath_var=LIBRARY_PATH
+  ;;
+
+bsdi[45]*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  finish_cmds='PATH="\$PATH:/sbin" ldconfig $libdir'
+  shlibpath_var=LD_LIBRARY_PATH
+  sys_lib_search_path_spec="/shlib /usr/lib /usr/X11/lib /usr/contrib/lib /lib /usr/local/lib"
+  sys_lib_dlsearch_path_spec="/shlib /usr/lib /usr/local/lib"
+  # the default ld.so.conf also contains /usr/contrib/lib and
+  # /usr/X11R6/lib (/usr/X11 is a link to /usr/X11R6), but let us allow
+  # libtool to hard-code these into programs
+  ;;
+
+cygwin* | mingw* | pw32* | cegcc*)
+  version_type=windows
+  shrext_cmds=.dll
+  need_version=no
+  need_lib_prefix=no
+
+  case $GCC,$cc_basename in
+  yes,*)
+    # gcc
+    library_names_spec='$libname.dll.a'
+    # DLL is installed to $(libdir)/../bin by postinstall_cmds
+    postinstall_cmds='base_file=`basename \$file`~
+      dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; echo \$dlname'\''`~
+      dldir=$destdir/`dirname \$dlpath`~
+      test -d \$dldir || mkdir -p \$dldir~
+      $install_prog $dir/$dlname \$dldir/$dlname~
+      chmod a+x \$dldir/$dlname~
+      if test -n '\''$stripme'\'' && test -n '\''$striplib'\''; then
+        eval '\''$striplib \$dldir/$dlname'\'' || exit \$?;
+      fi'
+    postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; echo \$dlname'\''`~
+      dlpath=$dir/\$dldll~
+       $RM \$dlpath'
+    shlibpath_overrides_runpath=yes
+
+    case $host_os in
+    cygwin*)
+      # Cygwin DLLs use 'cyg' prefix rather than 'lib'
+      soname_spec='`echo $libname | $SED -e 's/^lib/cyg/'``echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext'
+
+      sys_lib_search_path_spec="$sys_lib_search_path_spec /usr/lib/w32api"
+      ;;
+    mingw* | cegcc*)
+      # MinGW DLLs use traditional 'lib' prefix
+      soname_spec='$libname`echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext'
+      ;;
+    pw32*)
+      # pw32 DLLs use 'pw' prefix rather than 'lib'
+      library_names_spec='`echo $libname | $SED -e 's/^lib/pw/'``echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext'
+      ;;
+    esac
+    dynamic_linker='Win32 ld.exe'
+    ;;
+
+  *,cl* | *,icl*)
+    # Native MSVC or ICC
+    libname_spec='$name'
+    soname_spec='$libname`echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext'
+    library_names_spec='$libname.dll.lib'
+
+    case $build_os in
+    mingw*)
+      sys_lib_search_path_spec=
+      lt_save_ifs=$IFS
+      IFS=';'
+      for lt_path in $LIB
+      do
+        IFS=$lt_save_ifs
+        # Let DOS variable expansion print the short 8.3 style file name.
+        lt_path=`cd "$lt_path" 2>/dev/null && cmd //C "for %i in (".") do @echo %~si"`
+        sys_lib_search_path_spec="$sys_lib_search_path_spec $lt_path"
+      done
+      IFS=$lt_save_ifs
+      # Convert to MSYS style.
+      sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e 's|\\\\|/|g' -e 's| \\([a-zA-Z]\\):| /\\1|g' -e 's|^ ||'`
+      ;;
+    cygwin*)
+      # Convert to unix form, then to dos form, then back to unix form
+      # but this time dos style (no spaces!) so that the unix form looks
+      # like /cygdrive/c/PROGRA~1:/cygdr...
+      sys_lib_search_path_spec=`cygpath --path --unix "$LIB"`
+      sys_lib_search_path_spec=`cygpath --path --dos "$sys_lib_search_path_spec" 2>/dev/null`
+      sys_lib_search_path_spec=`cygpath --path --unix "$sys_lib_search_path_spec" | $SED -e "s/$PATH_SEPARATOR/ /g"`
+      ;;
+    *)
+      sys_lib_search_path_spec=$LIB
+      if $ECHO "$sys_lib_search_path_spec" | $GREP ';[c-zC-Z]:/' >/dev/null; then
+        # It is most probably a Windows format PATH.
+        sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e 's/;/ /g'`
+      else
+        sys_lib_search_path_spec=`$ECHO "$sys_lib_search_path_spec" | $SED -e "s/$PATH_SEPARATOR/ /g"`
+      fi
+      # FIXME: find the short name or the path components, as spaces are
+      # common. (e.g. "Program Files" -> "PROGRA~1")
+      ;;
+    esac
+
+    # DLL is installed to $(libdir)/../bin by postinstall_cmds
+    postinstall_cmds='base_file=`basename \$file`~
+      dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; echo \$dlname'\''`~
+      dldir=$destdir/`dirname \$dlpath`~
+      test -d \$dldir || mkdir -p \$dldir~
+      $install_prog $dir/$dlname \$dldir/$dlname'
+    postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; echo \$dlname'\''`~
+      dlpath=$dir/\$dldll~
+       $RM \$dlpath'
+    shlibpath_overrides_runpath=yes
+    dynamic_linker='Win32 link.exe'
+    ;;
+
+  *)
+    # Assume MSVC and ICC wrapper
+    library_names_spec='$libname`echo $release | $SED -e 's/[.]/-/g'`$versuffix$shared_ext $libname.lib'
+    dynamic_linker='Win32 ld.exe'
+    ;;
+  esac
+  # FIXME: first we should search . and the directory the executable is in
+  shlibpath_var=PATH
+  ;;
+
+darwin* | rhapsody*)
+  dynamic_linker="$host_os dyld"
+  version_type=darwin
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$major$shared_ext $libname$shared_ext'
+  soname_spec='$libname$release$major$shared_ext'
+  shlibpath_overrides_runpath=yes
+  shlibpath_var=DYLD_LIBRARY_PATH
+  shrext_cmds='`test .$module = .yes && echo .so || echo .dylib`'
+
+  sys_lib_search_path_spec="$sys_lib_search_path_spec /usr/local/lib"
+  sys_lib_dlsearch_path_spec='/usr/local/lib /lib /usr/lib'
+  ;;
+
+dgux*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  ;;
+
+freebsd* | dragonfly* | midnightbsd*)
+  # DragonFly does not have aout.  When/if they implement a new
+  # versioning mechanism, adjust this.
+  if test -x /usr/bin/objformat; then
+    objformat=`/usr/bin/objformat`
+  else
+    case $host_os in
+    freebsd[23].*) objformat=aout ;;
+    *) objformat=elf ;;
+    esac
+  fi
+  version_type=freebsd-$objformat
+  case $version_type in
+    freebsd-elf*)
+      library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+      soname_spec='$libname$release$shared_ext$major'
+      need_version=no
+      need_lib_prefix=no
+      ;;
+    freebsd-*)
+      library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix'
+      need_version=yes
+      ;;
+  esac
+  shlibpath_var=LD_LIBRARY_PATH
+  case $host_os in
+  freebsd2.*)
+    shlibpath_overrides_runpath=yes
+    ;;
+  freebsd3.[01]* | freebsdelf3.[01]*)
+    shlibpath_overrides_runpath=yes
+    hardcode_into_libs=yes
+    ;;
+  freebsd3.[2-9]* | freebsdelf3.[2-9]* | \
+  freebsd4.[0-5] | freebsdelf4.[0-5] | freebsd4.1.1 | freebsdelf4.1.1)
+    shlibpath_overrides_runpath=no
+    hardcode_into_libs=yes
+    ;;
+  *) # from 4.6 on, and DragonFly
+    shlibpath_overrides_runpath=yes
+    hardcode_into_libs=yes
+    ;;
+  esac
+  ;;
+
+haiku*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  dynamic_linker="$host_os runtime_loader"
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  sys_lib_dlsearch_path_spec='/boot/home/config/lib /boot/common/lib /boot/system/lib'
+  hardcode_into_libs=yes
+  ;;
+
+hpux9* | hpux10* | hpux11*)
+  # Give a soname corresponding to the major version so that dld.sl refuses to
+  # link against other versions.
+  version_type=sunos
+  need_lib_prefix=no
+  need_version=no
+  case $host_cpu in
+  ia64*)
+    shrext_cmds='.so'
+    hardcode_into_libs=yes
+    dynamic_linker="$host_os dld.so"
+    shlibpath_var=LD_LIBRARY_PATH
+    shlibpath_overrides_runpath=yes # Unless +noenvvar is specified.
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    soname_spec='$libname$release$shared_ext$major'
+    if test 32 = "$HPUX_IA64_MODE"; then
+      sys_lib_search_path_spec="/usr/lib/hpux32 /usr/local/lib/hpux32 /usr/local/lib"
+      sys_lib_dlsearch_path_spec=/usr/lib/hpux32
+    else
+      sys_lib_search_path_spec="/usr/lib/hpux64 /usr/local/lib/hpux64"
+      sys_lib_dlsearch_path_spec=/usr/lib/hpux64
+    fi
+    ;;
+  hppa*64*)
+    shrext_cmds='.sl'
+    hardcode_into_libs=yes
+    dynamic_linker="$host_os dld.sl"
+    shlibpath_var=LD_LIBRARY_PATH # How should we handle SHLIB_PATH
+    shlibpath_overrides_runpath=yes # Unless +noenvvar is specified.
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    soname_spec='$libname$release$shared_ext$major'
+    sys_lib_search_path_spec="/usr/lib/pa20_64 /usr/ccs/lib/pa20_64"
+    sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec
+    ;;
+  *)
+    shrext_cmds='.sl'
+    dynamic_linker="$host_os dld.sl"
+    shlibpath_var=SHLIB_PATH
+    shlibpath_overrides_runpath=no # +s is required to enable SHLIB_PATH
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    soname_spec='$libname$release$shared_ext$major'
+    ;;
+  esac
+  # HP-UX runs *really* slowly unless shared libraries are mode 555, ...
+  postinstall_cmds='chmod 555 $lib'
+  # or fails outright, so override atomically:
+  install_override_mode=555
+  ;;
+
+interix[3-9]*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  dynamic_linker='Interix 3.x ld.so.1 (PE, like ELF)'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  hardcode_into_libs=yes
+  ;;
+
+irix5* | irix6* | nonstopux*)
+  case $host_os in
+    nonstopux*) version_type=nonstopux ;;
+    *)
+	if test yes = "$lt_cv_prog_gnu_ld"; then
+		version_type=linux # correct to gnu/linux during the next big refactor
+	else
+		version_type=irix
+	fi ;;
+  esac
+  need_lib_prefix=no
+  need_version=no
+  soname_spec='$libname$release$shared_ext$major'
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$release$shared_ext $libname$shared_ext'
+  case $host_os in
+  irix5* | nonstopux*)
+    libsuff= shlibsuff=
+    ;;
+  *)
+    case $LD in # libtool.m4 will add one of these switches to LD
+    *-32|*"-32 "|*-melf32bsmip|*"-melf32bsmip ")
+      libsuff= shlibsuff= libmagic=32-bit;;
+    *-n32|*"-n32 "|*-melf32bmipn32|*"-melf32bmipn32 ")
+      libsuff=32 shlibsuff=N32 libmagic=N32;;
+    *-64|*"-64 "|*-melf64bmip|*"-melf64bmip ")
+      libsuff=64 shlibsuff=64 libmagic=64-bit;;
+    *) libsuff= shlibsuff= libmagic=never-match;;
+    esac
+    ;;
+  esac
+  shlibpath_var=LD_LIBRARY${shlibsuff}_PATH
+  shlibpath_overrides_runpath=no
+  sys_lib_search_path_spec="/usr/lib$libsuff /lib$libsuff /usr/local/lib$libsuff"
+  sys_lib_dlsearch_path_spec="/usr/lib$libsuff /lib$libsuff"
+  hardcode_into_libs=yes
+  ;;
+
+# No shared lib support for Linux oldld, aout, or coff.
+linux*oldld* | linux*aout* | linux*coff*)
+  dynamic_linker=no
+  ;;
+
+linux*android*)
+  version_type=none # Android doesn't support versioned libraries.
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext'
+  soname_spec='$libname$release$shared_ext'
+  finish_cmds=
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+
+  # This implies no fast_install, which is unacceptable.
+  # Some rework will be needed to allow for fast_install
+  # before this can be enabled.
+  hardcode_into_libs=yes
+
+  dynamic_linker='Android linker'
+  # Don't embed -rpath directories since the linker doesn't support them.
+  hardcode_libdir_flag_spec='-L$libdir'
+  ;;
+
+# This must be glibc/ELF.
+linux* | k*bsd*-gnu | kopensolaris*-gnu | gnu*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  finish_cmds='PATH="\$PATH:/sbin" ldconfig -n $libdir'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+
+  # Some binutils ld are patched to set DT_RUNPATH
+  if test ${lt_cv_shlibpath_overrides_runpath+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  lt_cv_shlibpath_overrides_runpath=no
+    save_LDFLAGS=$LDFLAGS
+    save_libdir=$libdir
+    eval "libdir=/foo; wl=\"$lt_prog_compiler_wl\"; \
+	 LDFLAGS=\"\$LDFLAGS $hardcode_libdir_flag_spec\""
+    cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main (void)
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  if  ($OBJDUMP -p conftest$ac_exeext) 2>/dev/null | grep "RUNPATH.*$libdir" >/dev/null
+then :
+  lt_cv_shlibpath_overrides_runpath=yes
+fi
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+    LDFLAGS=$save_LDFLAGS
+    libdir=$save_libdir
+
+fi
+
+  shlibpath_overrides_runpath=$lt_cv_shlibpath_overrides_runpath
+
+  # This implies no fast_install, which is unacceptable.
+  # Some rework will be needed to allow for fast_install
+  # before this can be enabled.
+  hardcode_into_libs=yes
+
+  # Ideally, we could use ldconfig to report *all* directores which are
+  # searched for libraries, however this is still not possible.  Aside from not
+  # being certain /sbin/ldconfig is available, command
+  # 'ldconfig -N -X -v | grep ^/' on 64bit Fedora does not report /usr/lib64,
+  # even though it is searched at run-time.  Try to do the best guess by
+  # appending ld.so.conf contents (and includes) to the search path.
+  if test -f /etc/ld.so.conf; then
+    lt_ld_extra=`awk '/^include / { system(sprintf("cd /etc; cat %s 2>/dev/null", \$2)); skip = 1; } { if (!skip) print \$0; skip = 0; }' < /etc/ld.so.conf | $SED -e 's/#.*//;/^[	 ]*hwcap[	 ]/d;s/[:,	]/ /g;s/=[^=]*$//;s/=[^= ]* / /g;s/"//g;/^$/d' | tr '\n' ' '`
+    sys_lib_dlsearch_path_spec="/lib /usr/lib $lt_ld_extra"
+  fi
+
+  # We used to test for /lib/ld.so.1 and disable shared libraries on
+  # powerpc, because MkLinux only supported shared libraries with the
+  # GNU dynamic linker.  Since this was broken with cross compilers,
+  # most powerpc-linux boxes support dynamic linking these days and
+  # people can always --disable-shared, the test was removed, and we
+  # assume the GNU/Linux dynamic linker is in use.
+  dynamic_linker='GNU/Linux ld.so'
+  ;;
+
+netbsdelf*-gnu)
+  version_type=linux
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='${libname}${release}${shared_ext}$versuffix ${libname}${release}${shared_ext}$major ${libname}${shared_ext}'
+  soname_spec='${libname}${release}${shared_ext}$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  hardcode_into_libs=yes
+  dynamic_linker='NetBSD ld.elf_so'
+  ;;
+
+netbsd*)
+  version_type=sunos
+  need_lib_prefix=no
+  need_version=no
+  if echo __ELF__ | $CC -E - | $GREP __ELF__ >/dev/null; then
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix'
+    finish_cmds='PATH="\$PATH:/sbin" ldconfig -m $libdir'
+    dynamic_linker='NetBSD (a.out) ld.so'
+  else
+    library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+    soname_spec='$libname$release$shared_ext$major'
+    dynamic_linker='NetBSD ld.elf_so'
+  fi
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  hardcode_into_libs=yes
+  ;;
+
+newsos6)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  ;;
+
+*nto* | *qnx*)
+  version_type=qnx
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  hardcode_into_libs=yes
+  dynamic_linker='ldqnx.so'
+  ;;
+
+openbsd* | bitrig*)
+  version_type=sunos
+  sys_lib_dlsearch_path_spec=/usr/lib
+  need_lib_prefix=no
+  if test -z "`echo __ELF__ | $CC -E - | $GREP __ELF__`"; then
+    need_version=no
+  else
+    need_version=yes
+  fi
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix'
+  finish_cmds='PATH="\$PATH:/sbin" ldconfig -m $libdir'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  ;;
+
+os2*)
+  libname_spec='$name'
+  version_type=windows
+  shrext_cmds=.dll
+  need_version=no
+  need_lib_prefix=no
+  # OS/2 can only load a DLL with a base name of 8 characters or less.
+  soname_spec='`test -n "$os2dllname" && libname="$os2dllname";
+    v=$($ECHO $release$versuffix | tr -d .-);
+    n=$($ECHO $libname | cut -b -$((8 - ${#v})) | tr . _);
+    $ECHO $n$v`$shared_ext'
+  library_names_spec='${libname}_dll.$libext'
+  dynamic_linker='OS/2 ld.exe'
+  shlibpath_var=BEGINLIBPATH
+  sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib"
+  sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec
+  postinstall_cmds='base_file=`basename \$file`~
+    dlpath=`$SHELL 2>&1 -c '\''. $dir/'\''\$base_file'\''i; $ECHO \$dlname'\''`~
+    dldir=$destdir/`dirname \$dlpath`~
+    test -d \$dldir || mkdir -p \$dldir~
+    $install_prog $dir/$dlname \$dldir/$dlname~
+    chmod a+x \$dldir/$dlname~
+    if test -n '\''$stripme'\'' && test -n '\''$striplib'\''; then
+      eval '\''$striplib \$dldir/$dlname'\'' || exit \$?;
+    fi'
+  postuninstall_cmds='dldll=`$SHELL 2>&1 -c '\''. $file; $ECHO \$dlname'\''`~
+    dlpath=$dir/\$dldll~
+    $RM \$dlpath'
+  ;;
+
+osf3* | osf4* | osf5*)
+  version_type=osf
+  need_lib_prefix=no
+  need_version=no
+  soname_spec='$libname$release$shared_ext$major'
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  shlibpath_var=LD_LIBRARY_PATH
+  sys_lib_search_path_spec="/usr/shlib /usr/ccs/lib /usr/lib/cmplrs/cc /usr/lib /usr/local/lib /var/shlib"
+  sys_lib_dlsearch_path_spec=$sys_lib_search_path_spec
+  ;;
+
+rdos*)
+  dynamic_linker=no
+  ;;
+
+solaris*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  hardcode_into_libs=yes
+  # ldd complains unless libraries are executable
+  postinstall_cmds='chmod +x $lib'
+  ;;
+
+sunos4*)
+  version_type=sunos
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$shared_ext$versuffix'
+  finish_cmds='PATH="\$PATH:/usr/etc" ldconfig $libdir'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  if test yes = "$with_gnu_ld"; then
+    need_lib_prefix=no
+  fi
+  need_version=yes
+  ;;
+
+sysv4 | sysv4.3*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  case $host_vendor in
+    sni)
+      shlibpath_overrides_runpath=no
+      need_lib_prefix=no
+      runpath_var=LD_RUN_PATH
+      ;;
+    siemens)
+      need_lib_prefix=no
+      ;;
+    motorola)
+      need_lib_prefix=no
+      need_version=no
+      shlibpath_overrides_runpath=no
+      sys_lib_search_path_spec='/lib /usr/lib /usr/ccs/lib'
+      ;;
+  esac
+  ;;
+
+sysv4*MP*)
+  if test -d /usr/nec; then
+    version_type=linux # correct to gnu/linux during the next big refactor
+    library_names_spec='$libname$shared_ext.$versuffix $libname$shared_ext.$major $libname$shared_ext'
+    soname_spec='$libname$shared_ext.$major'
+    shlibpath_var=LD_LIBRARY_PATH
+  fi
+  ;;
+
+sysv5* | sco3.2v5* | sco5v6* | unixware* | OpenUNIX* | sysv4*uw2*)
+  version_type=sco
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=yes
+  hardcode_into_libs=yes
+  if test yes = "$with_gnu_ld"; then
+    sys_lib_search_path_spec='/usr/local/lib /usr/gnu/lib /usr/ccs/lib /usr/lib /lib'
+  else
+    sys_lib_search_path_spec='/usr/ccs/lib /usr/lib'
+    case $host_os in
+      sco3.2v5*)
+        sys_lib_search_path_spec="$sys_lib_search_path_spec /lib"
+	;;
+    esac
+  fi
+  sys_lib_dlsearch_path_spec='/usr/lib'
+  ;;
+
+tpf*)
+  # TPF is a cross-target only.  Preferred cross-host = GNU/Linux.
+  version_type=linux # correct to gnu/linux during the next big refactor
+  need_lib_prefix=no
+  need_version=no
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  shlibpath_var=LD_LIBRARY_PATH
+  shlibpath_overrides_runpath=no
+  hardcode_into_libs=yes
+  ;;
+
+uts4*)
+  version_type=linux # correct to gnu/linux during the next big refactor
+  library_names_spec='$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext'
+  soname_spec='$libname$release$shared_ext$major'
+  shlibpath_var=LD_LIBRARY_PATH
+  ;;
+
+*)
+  dynamic_linker=no
+  ;;
+esac
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $dynamic_linker" >&5
+printf "%s\n" "$dynamic_linker" >&6; }
+test no = "$dynamic_linker" && can_build_shared=no
+
+variables_saved_for_relink="PATH $shlibpath_var $runpath_var"
+if test yes = "$GCC"; then
+  variables_saved_for_relink="$variables_saved_for_relink GCC_EXEC_PREFIX COMPILER_PATH LIBRARY_PATH"
+fi
+
+if test set = "${lt_cv_sys_lib_search_path_spec+set}"; then
+  sys_lib_search_path_spec=$lt_cv_sys_lib_search_path_spec
+fi
+
+if test set = "${lt_cv_sys_lib_dlsearch_path_spec+set}"; then
+  sys_lib_dlsearch_path_spec=$lt_cv_sys_lib_dlsearch_path_spec
+fi
+
+# remember unaugmented sys_lib_dlsearch_path content for libtool script decls...
+configure_time_dlsearch_path=$sys_lib_dlsearch_path_spec
+
+# ... but it needs LT_SYS_LIBRARY_PATH munging for other configure-time code
+func_munge_path_list sys_lib_dlsearch_path_spec "$LT_SYS_LIBRARY_PATH"
+
+# to be used as default LT_SYS_LIBRARY_PATH value in generated libtool
+configure_time_lt_sys_library_path=$LT_SYS_LIBRARY_PATH
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking how to hardcode library paths into programs" >&5
+printf %s "checking how to hardcode library paths into programs... " >&6; }
+hardcode_action=
+if test -n "$hardcode_libdir_flag_spec" ||
+   test -n "$runpath_var" ||
+   test yes = "$hardcode_automatic"; then
+
+  # We can hardcode non-existent directories.
+  if test no != "$hardcode_direct" &&
+     # If the only mechanism to avoid hardcoding is shlibpath_var, we
+     # have to relink, otherwise we might link with an installed library
+     # when we should be linking with a yet-to-be-installed one
+     ## test no != "$_LT_TAGVAR(hardcode_shlibpath_var, )" &&
+     test no != "$hardcode_minus_L"; then
+    # Linking always hardcodes the temporary library directory.
+    hardcode_action=relink
+  else
+    # We can link without hardcoding, and we can hardcode nonexisting dirs.
+    hardcode_action=immediate
+  fi
+else
+  # We cannot hardcode anything, or else we can only hardcode existing
+  # directories.
+  hardcode_action=unsupported
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $hardcode_action" >&5
+printf "%s\n" "$hardcode_action" >&6; }
+
+if test relink = "$hardcode_action" ||
+   test yes = "$inherit_rpath"; then
+  # Fast installation is not supported
+  enable_fast_install=no
+elif test yes = "$shlibpath_overrides_runpath" ||
+     test no = "$enable_shared"; then
+  # Fast installation is not necessary
+  enable_fast_install=needless
+fi
+
+
+
+
+
+
+  if test yes != "$enable_dlopen"; then
+  enable_dlopen=unknown
+  enable_dlopen_self=unknown
+  enable_dlopen_self_static=unknown
+else
+  lt_cv_dlopen=no
+  lt_cv_dlopen_libs=
+
+  case $host_os in
+  beos*)
+    lt_cv_dlopen=load_add_on
+    lt_cv_dlopen_libs=
+    lt_cv_dlopen_self=yes
+    ;;
+
+  mingw* | pw32* | cegcc*)
+    lt_cv_dlopen=LoadLibrary
+    lt_cv_dlopen_libs=
+    ;;
+
+  cygwin*)
+    lt_cv_dlopen=dlopen
+    lt_cv_dlopen_libs=
+    ;;
+
+  darwin*)
+    # if libdl is installed we need to link against it
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for dlopen in -ldl" >&5
+printf %s "checking for dlopen in -ldl... " >&6; }
+if test ${ac_cv_lib_dl_dlopen+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-ldl  $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+char dlopen ();
+int
+main (void)
+{
+return dlopen ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_lib_dl_dlopen=yes
+else $as_nop
+  ac_cv_lib_dl_dlopen=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_dl_dlopen" >&5
+printf "%s\n" "$ac_cv_lib_dl_dlopen" >&6; }
+if test "x$ac_cv_lib_dl_dlopen" = xyes
+then :
+  lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-ldl
+else $as_nop
+
+    lt_cv_dlopen=dyld
+    lt_cv_dlopen_libs=
+    lt_cv_dlopen_self=yes
+
+fi
+
+    ;;
+
+  tpf*)
+    # Don't try to run any link tests for TPF.  We know it's impossible
+    # because TPF is a cross-compiler, and we know how we open DSOs.
+    lt_cv_dlopen=dlopen
+    lt_cv_dlopen_libs=
+    lt_cv_dlopen_self=no
+    ;;
+
+  *)
+    ac_fn_c_check_func "$LINENO" "shl_load" "ac_cv_func_shl_load"
+if test "x$ac_cv_func_shl_load" = xyes
+then :
+  lt_cv_dlopen=shl_load
+else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for shl_load in -ldld" >&5
+printf %s "checking for shl_load in -ldld... " >&6; }
+if test ${ac_cv_lib_dld_shl_load+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-ldld  $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+char shl_load ();
+int
+main (void)
+{
+return shl_load ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_lib_dld_shl_load=yes
+else $as_nop
+  ac_cv_lib_dld_shl_load=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_dld_shl_load" >&5
+printf "%s\n" "$ac_cv_lib_dld_shl_load" >&6; }
+if test "x$ac_cv_lib_dld_shl_load" = xyes
+then :
+  lt_cv_dlopen=shl_load lt_cv_dlopen_libs=-ldld
+else $as_nop
+  ac_fn_c_check_func "$LINENO" "dlopen" "ac_cv_func_dlopen"
+if test "x$ac_cv_func_dlopen" = xyes
+then :
+  lt_cv_dlopen=dlopen
+else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for dlopen in -ldl" >&5
+printf %s "checking for dlopen in -ldl... " >&6; }
+if test ${ac_cv_lib_dl_dlopen+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-ldl  $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+char dlopen ();
+int
+main (void)
+{
+return dlopen ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_lib_dl_dlopen=yes
+else $as_nop
+  ac_cv_lib_dl_dlopen=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_dl_dlopen" >&5
+printf "%s\n" "$ac_cv_lib_dl_dlopen" >&6; }
+if test "x$ac_cv_lib_dl_dlopen" = xyes
+then :
+  lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-ldl
+else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for dlopen in -lsvld" >&5
+printf %s "checking for dlopen in -lsvld... " >&6; }
+if test ${ac_cv_lib_svld_dlopen+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-lsvld  $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+char dlopen ();
+int
+main (void)
+{
+return dlopen ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_lib_svld_dlopen=yes
+else $as_nop
+  ac_cv_lib_svld_dlopen=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_svld_dlopen" >&5
+printf "%s\n" "$ac_cv_lib_svld_dlopen" >&6; }
+if test "x$ac_cv_lib_svld_dlopen" = xyes
+then :
+  lt_cv_dlopen=dlopen lt_cv_dlopen_libs=-lsvld
+else $as_nop
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for dld_link in -ldld" >&5
+printf %s "checking for dld_link in -ldld... " >&6; }
+if test ${ac_cv_lib_dld_dld_link+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-ldld  $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+char dld_link ();
+int
+main (void)
+{
+return dld_link ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_lib_dld_dld_link=yes
+else $as_nop
+  ac_cv_lib_dld_dld_link=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_dld_dld_link" >&5
+printf "%s\n" "$ac_cv_lib_dld_dld_link" >&6; }
+if test "x$ac_cv_lib_dld_dld_link" = xyes
+then :
+  lt_cv_dlopen=dld_link lt_cv_dlopen_libs=-ldld
+fi
+
+
+fi
+
+
+fi
+
+
+fi
+
+
+fi
+
+
+fi
+
+    ;;
+  esac
+
+  if test no = "$lt_cv_dlopen"; then
+    enable_dlopen=no
+  else
+    enable_dlopen=yes
+  fi
+
+  case $lt_cv_dlopen in
+  dlopen)
+    save_CPPFLAGS=$CPPFLAGS
+    test yes = "$ac_cv_header_dlfcn_h" && CPPFLAGS="$CPPFLAGS -DHAVE_DLFCN_H"
+
+    save_LDFLAGS=$LDFLAGS
+    wl=$lt_prog_compiler_wl eval LDFLAGS=\"\$LDFLAGS $export_dynamic_flag_spec\"
+
+    save_LIBS=$LIBS
+    LIBS="$lt_cv_dlopen_libs $LIBS"
+
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether a program can dlopen itself" >&5
+printf %s "checking whether a program can dlopen itself... " >&6; }
+if test ${lt_cv_dlopen_self+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  	  if test yes = "$cross_compiling"; then :
+  lt_cv_dlopen_self=cross
+else
+  lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
+  lt_status=$lt_dlunknown
+  cat > conftest.$ac_ext <<_LT_EOF
+#line $LINENO "configure"
+#include "confdefs.h"
+
+#if HAVE_DLFCN_H
+#include <dlfcn.h>
+#endif
+
+#include <stdio.h>
+
+#ifdef RTLD_GLOBAL
+#  define LT_DLGLOBAL		RTLD_GLOBAL
+#else
+#  ifdef DL_GLOBAL
+#    define LT_DLGLOBAL		DL_GLOBAL
+#  else
+#    define LT_DLGLOBAL		0
+#  endif
+#endif
+
+/* We may have to define LT_DLLAZY_OR_NOW in the command line if we
+   find out it does not work in some platform. */
+#ifndef LT_DLLAZY_OR_NOW
+#  ifdef RTLD_LAZY
+#    define LT_DLLAZY_OR_NOW		RTLD_LAZY
+#  else
+#    ifdef DL_LAZY
+#      define LT_DLLAZY_OR_NOW		DL_LAZY
+#    else
+#      ifdef RTLD_NOW
+#        define LT_DLLAZY_OR_NOW	RTLD_NOW
+#      else
+#        ifdef DL_NOW
+#          define LT_DLLAZY_OR_NOW	DL_NOW
+#        else
+#          define LT_DLLAZY_OR_NOW	0
+#        endif
+#      endif
+#    endif
+#  endif
+#endif
+
+/* When -fvisibility=hidden is used, assume the code has been annotated
+   correspondingly for the symbols needed.  */
+#if defined __GNUC__ && (((__GNUC__ == 3) && (__GNUC_MINOR__ >= 3)) || (__GNUC__ > 3))
+int fnord () __attribute__((visibility("default")));
+#endif
+
+int fnord () { return 42; }
+int main ()
+{
+  void *self = dlopen (0, LT_DLGLOBAL|LT_DLLAZY_OR_NOW);
+  int status = $lt_dlunknown;
+
+  if (self)
+    {
+      if (dlsym (self,"fnord"))       status = $lt_dlno_uscore;
+      else
+        {
+	  if (dlsym( self,"_fnord"))  status = $lt_dlneed_uscore;
+          else puts (dlerror ());
+	}
+      /* dlclose (self); */
+    }
+  else
+    puts (dlerror ());
+
+  return status;
+}
+_LT_EOF
+  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_link\""; } >&5
+  (eval $ac_link) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; } && test -s "conftest$ac_exeext" 2>/dev/null; then
+    (./conftest; exit; ) >&5 2>/dev/null
+    lt_status=$?
+    case x$lt_status in
+      x$lt_dlno_uscore) lt_cv_dlopen_self=yes ;;
+      x$lt_dlneed_uscore) lt_cv_dlopen_self=yes ;;
+      x$lt_dlunknown|x*) lt_cv_dlopen_self=no ;;
+    esac
+  else :
+    # compilation failed
+    lt_cv_dlopen_self=no
+  fi
+fi
+rm -fr conftest*
+
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_dlopen_self" >&5
+printf "%s\n" "$lt_cv_dlopen_self" >&6; }
+
+    if test yes = "$lt_cv_dlopen_self"; then
+      wl=$lt_prog_compiler_wl eval LDFLAGS=\"\$LDFLAGS $lt_prog_compiler_static\"
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether a statically linked program can dlopen itself" >&5
+printf %s "checking whether a statically linked program can dlopen itself... " >&6; }
+if test ${lt_cv_dlopen_self_static+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  	  if test yes = "$cross_compiling"; then :
+  lt_cv_dlopen_self_static=cross
+else
+  lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
+  lt_status=$lt_dlunknown
+  cat > conftest.$ac_ext <<_LT_EOF
+#line $LINENO "configure"
+#include "confdefs.h"
+
+#if HAVE_DLFCN_H
+#include <dlfcn.h>
+#endif
+
+#include <stdio.h>
+
+#ifdef RTLD_GLOBAL
+#  define LT_DLGLOBAL		RTLD_GLOBAL
+#else
+#  ifdef DL_GLOBAL
+#    define LT_DLGLOBAL		DL_GLOBAL
+#  else
+#    define LT_DLGLOBAL		0
+#  endif
+#endif
+
+/* We may have to define LT_DLLAZY_OR_NOW in the command line if we
+   find out it does not work in some platform. */
+#ifndef LT_DLLAZY_OR_NOW
+#  ifdef RTLD_LAZY
+#    define LT_DLLAZY_OR_NOW		RTLD_LAZY
+#  else
+#    ifdef DL_LAZY
+#      define LT_DLLAZY_OR_NOW		DL_LAZY
+#    else
+#      ifdef RTLD_NOW
+#        define LT_DLLAZY_OR_NOW	RTLD_NOW
+#      else
+#        ifdef DL_NOW
+#          define LT_DLLAZY_OR_NOW	DL_NOW
+#        else
+#          define LT_DLLAZY_OR_NOW	0
+#        endif
+#      endif
+#    endif
+#  endif
+#endif
+
+/* When -fvisibility=hidden is used, assume the code has been annotated
+   correspondingly for the symbols needed.  */
+#if defined __GNUC__ && (((__GNUC__ == 3) && (__GNUC_MINOR__ >= 3)) || (__GNUC__ > 3))
+int fnord () __attribute__((visibility("default")));
+#endif
+
+int fnord () { return 42; }
+int main ()
+{
+  void *self = dlopen (0, LT_DLGLOBAL|LT_DLLAZY_OR_NOW);
+  int status = $lt_dlunknown;
+
+  if (self)
+    {
+      if (dlsym (self,"fnord"))       status = $lt_dlno_uscore;
+      else
+        {
+	  if (dlsym( self,"_fnord"))  status = $lt_dlneed_uscore;
+          else puts (dlerror ());
+	}
+      /* dlclose (self); */
+    }
+  else
+    puts (dlerror ());
+
+  return status;
+}
+_LT_EOF
+  if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_link\""; } >&5
+  (eval $ac_link) 2>&5
+  ac_status=$?
+  printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; } && test -s "conftest$ac_exeext" 2>/dev/null; then
+    (./conftest; exit; ) >&5 2>/dev/null
+    lt_status=$?
+    case x$lt_status in
+      x$lt_dlno_uscore) lt_cv_dlopen_self_static=yes ;;
+      x$lt_dlneed_uscore) lt_cv_dlopen_self_static=yes ;;
+      x$lt_dlunknown|x*) lt_cv_dlopen_self_static=no ;;
+    esac
+  else :
+    # compilation failed
+    lt_cv_dlopen_self_static=no
+  fi
+fi
+rm -fr conftest*
+
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $lt_cv_dlopen_self_static" >&5
+printf "%s\n" "$lt_cv_dlopen_self_static" >&6; }
+    fi
+
+    CPPFLAGS=$save_CPPFLAGS
+    LDFLAGS=$save_LDFLAGS
+    LIBS=$save_LIBS
+    ;;
+  esac
+
+  case $lt_cv_dlopen_self in
+  yes|no) enable_dlopen_self=$lt_cv_dlopen_self ;;
+  *) enable_dlopen_self=unknown ;;
+  esac
+
+  case $lt_cv_dlopen_self_static in
+  yes|no) enable_dlopen_self_static=$lt_cv_dlopen_self_static ;;
+  *) enable_dlopen_self_static=unknown ;;
+  esac
+fi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+striplib=
+old_striplib=
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether stripping libraries is possible" >&5
+printf %s "checking whether stripping libraries is possible... " >&6; }
+if test -z "$STRIP"; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+else
+  if $STRIP -V 2>&1 | $GREP "GNU strip" >/dev/null; then
+    old_striplib="$STRIP --strip-debug"
+    striplib="$STRIP --strip-unneeded"
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+  else
+    case $host_os in
+    darwin*)
+      # FIXME - insert some real tests, host_os isn't really good enough
+      striplib="$STRIP -x"
+      old_striplib="$STRIP -S"
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+      ;;
+    freebsd*)
+      if $STRIP -V 2>&1 | $GREP "elftoolchain" >/dev/null; then
+        old_striplib="$STRIP --strip-debug"
+        striplib="$STRIP --strip-unneeded"
+        { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+      else
+        { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+      fi
+      ;;
+    *)
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+      ;;
+    esac
+  fi
+fi
+
+
+
+
+
+
+
+
+
+
+
+
+  # Report what library types will actually be built
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if libtool supports shared libraries" >&5
+printf %s "checking if libtool supports shared libraries... " >&6; }
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $can_build_shared" >&5
+printf "%s\n" "$can_build_shared" >&6; }
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build shared libraries" >&5
+printf %s "checking whether to build shared libraries... " >&6; }
+  test no = "$can_build_shared" && enable_shared=no
+
+  # On AIX, shared libraries and static libraries use the same namespace, and
+  # are all built from PIC.
+  case $host_os in
+  aix3*)
+    test yes = "$enable_shared" && enable_static=no
+    if test -n "$RANLIB"; then
+      archive_cmds="$archive_cmds~\$RANLIB \$lib"
+      postinstall_cmds='$RANLIB $lib'
+    fi
+    ;;
+
+  aix[4-9]*)
+    if test ia64 != "$host_cpu"; then
+      case $enable_shared,$with_aix_soname,$aix_use_runtimelinking in
+      yes,aix,yes) ;;			# shared object as lib.so file only
+      yes,svr4,*) ;;			# shared object as lib.so archive member only
+      yes,*) enable_static=no ;;	# shared object in lib.a archive as well
+      esac
+    fi
+    ;;
+  esac
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_shared" >&5
+printf "%s\n" "$enable_shared" >&6; }
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build static libraries" >&5
+printf %s "checking whether to build static libraries... " >&6; }
+  # Make sure either enable_shared or enable_static is yes.
+  test yes = "$enable_shared" || enable_static=yes
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_static" >&5
+printf "%s\n" "$enable_static" >&6; }
+
+
+
+
+fi
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+CC=$lt_save_CC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+        ac_config_commands="$ac_config_commands libtool"
+
+
+
+
+# Only expand once:
+
+
+
+# check for headers
+# Autoupdate added the next two lines to ensure that your configure
+# script's behavior did not change.  They are probably safe to remove.
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for egrep" >&5
+printf %s "checking for egrep... " >&6; }
+if test ${ac_cv_path_EGREP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  if echo a | $GREP -E '(a|b)' >/dev/null 2>&1
+   then ac_cv_path_EGREP="$GREP -E"
+   else
+     if test -z "$EGREP"; then
+  ac_path_EGREP_found=false
+  # Loop through the user's path and test for each of PROGNAME-LIST
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    for ac_prog in egrep
+   do
+    for ac_exec_ext in '' $ac_executable_extensions; do
+      ac_path_EGREP="$as_dir$ac_prog$ac_exec_ext"
+      as_fn_executable_p "$ac_path_EGREP" || continue
+# Check for GNU ac_path_EGREP and select it if it is found.
+  # Check for GNU $ac_path_EGREP
+case `"$ac_path_EGREP" --version 2>&1` in
+*GNU*)
+  ac_cv_path_EGREP="$ac_path_EGREP" ac_path_EGREP_found=:;;
+*)
+  ac_count=0
+  printf %s 0123456789 >"conftest.in"
+  while :
+  do
+    cat "conftest.in" "conftest.in" >"conftest.tmp"
+    mv "conftest.tmp" "conftest.in"
+    cp "conftest.in" "conftest.nl"
+    printf "%s\n" 'EGREP' >> "conftest.nl"
+    "$ac_path_EGREP" 'EGREP$' < "conftest.nl" >"conftest.out" 2>/dev/null || break
+    diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break
+    as_fn_arith $ac_count + 1 && ac_count=$as_val
+    if test $ac_count -gt ${ac_path_EGREP_max-0}; then
+      # Best one so far, save it but keep looking for a better one
+      ac_cv_path_EGREP="$ac_path_EGREP"
+      ac_path_EGREP_max=$ac_count
+    fi
+    # 10*(2^10) chars as input seems more than enough
+    test $ac_count -gt 10 && break
+  done
+  rm -f conftest.in conftest.tmp conftest.nl conftest.out;;
+esac
+
+      $ac_path_EGREP_found && break 3
+    done
+  done
+  done
+IFS=$as_save_IFS
+  if test -z "$ac_cv_path_EGREP"; then
+    as_fn_error $? "no acceptable egrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin" "$LINENO" 5
+  fi
+else
+  ac_cv_path_EGREP=$EGREP
+fi
+
+   fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP" >&5
+printf "%s\n" "$ac_cv_path_EGREP" >&6; }
+ EGREP="$ac_cv_path_EGREP"
+
+
+
+ac_fn_c_check_header_compile "$LINENO" "byteswap.h" "ac_cv_header_byteswap_h" "$ac_includes_default"
+if test "x$ac_cv_header_byteswap_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_BYTESWAP_H 1" >>confdefs.h
+
+fi
+ac_fn_c_check_header_compile "$LINENO" "stdatomic.h" "ac_cv_header_stdatomic_h" "$ac_includes_default"
+if test "x$ac_cv_header_stdatomic_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_STDATOMIC_H 1" >>confdefs.h
+
+fi
+
+
+# check for functions
+
+  for ac_func in getopt_long
+do :
+  ac_fn_c_check_func "$LINENO" "getopt_long" "ac_cv_func_getopt_long"
+if test "x$ac_cv_func_getopt_long" = xyes
+then :
+  printf "%s\n" "#define HAVE_GETOPT_LONG 1" >>confdefs.h
+ GETOPT_O_FILES=''
+else $as_nop
+  GETOPT_O_FILES='getopt_long.o'
+fi
+
+done
+ac_fn_c_check_func "$LINENO" "posix_fadvise" "ac_cv_func_posix_fadvise"
+if test "x$ac_cv_func_posix_fadvise" = xyes
+then :
+  printf "%s\n" "#define HAVE_POSIX_FADVISE 1" >>confdefs.h
+
+fi
+
+ac_fn_c_check_func "$LINENO" "posix_memalign" "ac_cv_func_posix_memalign"
+if test "x$ac_cv_func_posix_memalign" = xyes
+then :
+  printf "%s\n" "#define HAVE_POSIX_MEMALIGN 1" >>confdefs.h
+
+fi
+
+ac_fn_c_check_func "$LINENO" "gettimeofday" "ac_cv_func_gettimeofday"
+if test "x$ac_cv_func_gettimeofday" = xyes
+then :
+  printf "%s\n" "#define HAVE_GETTIMEOFDAY 1" >>confdefs.h
+
+fi
+
+ac_fn_c_check_func "$LINENO" "sysconf" "ac_cv_func_sysconf"
+if test "x$ac_cv_func_sysconf" = xyes
+then :
+  printf "%s\n" "#define HAVE_SYSCONF 1" >>confdefs.h
+
+fi
+
+ac_fn_c_check_func "$LINENO" "lseek64" "ac_cv_func_lseek64"
+if test "x$ac_cv_func_lseek64" = xyes
+then :
+  printf "%s\n" "#define HAVE_LSEEK64 1" >>confdefs.h
+
+fi
+
+ac_fn_c_check_func "$LINENO" "srand48_r" "ac_cv_func_srand48_r"
+if test "x$ac_cv_func_srand48_r" = xyes
+then :
+  printf "%s\n" "#define HAVE_SRAND48_R 1" >>confdefs.h
+
+fi
+
+SAVED_LIBS=$LIBS
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing pthread_create" >&5
+printf %s "checking for library containing pthread_create... " >&6; }
+if test ${ac_cv_search_pthread_create+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_func_search_save_LIBS=$LIBS
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+char pthread_create ();
+int
+main (void)
+{
+return pthread_create ();
+  ;
+  return 0;
+}
+_ACEOF
+for ac_lib in '' pthread
+do
+  if test -z "$ac_lib"; then
+    ac_res="none required"
+  else
+    ac_res=-l$ac_lib
+    LIBS="-l$ac_lib  $ac_func_search_save_LIBS"
+  fi
+  if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_search_pthread_create=$ac_res
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext
+  if test ${ac_cv_search_pthread_create+y}
+then :
+  break
+fi
+done
+if test ${ac_cv_search_pthread_create+y}
+then :
+
+else $as_nop
+  ac_cv_search_pthread_create=no
+fi
+rm conftest.$ac_ext
+LIBS=$ac_func_search_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_pthread_create" >&5
+printf "%s\n" "$ac_cv_search_pthread_create" >&6; }
+ac_res=$ac_cv_search_pthread_create
+if test "$ac_res" != no
+then :
+  test "$ac_res" = "none required" || LIBS="$ac_res $LIBS"
+
+fi
+
+# AC_SEARCH_LIBS adds libraries at the start of $LIBS so remove $SAVED_LIBS
+# from the end of $LIBS.
+pthread_lib=${LIBS%${SAVED_LIBS}}
+ac_fn_c_check_func "$LINENO" "pthread_cancel" "ac_cv_func_pthread_cancel"
+if test "x$ac_cv_func_pthread_cancel" = xyes
+then :
+  printf "%s\n" "#define HAVE_PTHREAD_CANCEL 1" >>confdefs.h
+
+fi
+ac_fn_c_check_func "$LINENO" "pthread_kill" "ac_cv_func_pthread_kill"
+if test "x$ac_cv_func_pthread_kill" = xyes
+then :
+  printf "%s\n" "#define HAVE_PTHREAD_KILL 1" >>confdefs.h
+
+fi
+
+LIBS=$SAVED_LIBS
+PTHREAD_LIB=$pthread_lib
+
+
+SAVED_LIBS=$LIBS
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing clock_gettime" >&5
+printf %s "checking for library containing clock_gettime... " >&6; }
+if test ${ac_cv_search_clock_gettime+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+  ac_func_search_save_LIBS=$LIBS
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+char clock_gettime ();
+int
+main (void)
+{
+return clock_gettime ();
+  ;
+  return 0;
+}
+_ACEOF
+for ac_lib in '' rt
+do
+  if test -z "$ac_lib"; then
+    ac_res="none required"
+  else
+    ac_res=-l$ac_lib
+    LIBS="-l$ac_lib  $ac_func_search_save_LIBS"
+  fi
+  if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_search_clock_gettime=$ac_res
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext
+  if test ${ac_cv_search_clock_gettime+y}
+then :
+  break
+fi
+done
+if test ${ac_cv_search_clock_gettime+y}
+then :
+
+else $as_nop
+  ac_cv_search_clock_gettime=no
+fi
+rm conftest.$ac_ext
+LIBS=$ac_func_search_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_clock_gettime" >&5
+printf "%s\n" "$ac_cv_search_clock_gettime" >&6; }
+ac_res=$ac_cv_search_clock_gettime
+if test "$ac_res" != no
+then :
+  test "$ac_res" = "none required" || LIBS="$ac_res $LIBS"
+
+fi
+
+rt_lib=${LIBS%${SAVED_LIBS}}
+ac_fn_c_check_func "$LINENO" "clock_gettime" "ac_cv_func_clock_gettime"
+if test "x$ac_cv_func_clock_gettime" = xyes
+then :
+  printf "%s\n" "#define HAVE_CLOCK_GETTIME 1" >>confdefs.h
+
+fi
+
+LIBS=$SAVED_LIBS
+RT_LIB=$rt_lib
+
+
+
+
+
+
+
+
+printf "%s\n" "#define SG_LIB_BUILD_HOST \"${host}\"" >>confdefs.h
+
+
+check_for_getrandom() {
+	       for ac_header in sys/random.h
+do :
+  ac_fn_c_check_header_compile "$LINENO" "sys/random.h" "ac_cv_header_sys_random_h" "$ac_includes_default"
+if test "x$ac_cv_header_sys_random_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_SYS_RANDOM_H 1" >>confdefs.h
+
+printf "%s\n" "#define HAVE_GETRANDOM 1" >>confdefs.h
+
+fi
+
+done
+}
+
+check_for_linux_nvme_headers() {
+	       for ac_header in linux/nvme_ioctl.h
+do :
+  ac_fn_c_check_header_compile "$LINENO" "linux/nvme_ioctl.h" "ac_cv_header_linux_nvme_ioctl_h" "$ac_includes_default"
+if test "x$ac_cv_header_linux_nvme_ioctl_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_LINUX_NVME_IOCTL_H 1" >>confdefs.h
+
+printf "%s\n" "#define HAVE_NVME 1" >>confdefs.h
+
+fi
+
+done
+	ac_fn_c_check_header_compile "$LINENO" "linux/types.h" "ac_cv_header_linux_types_h" "#ifdef HAVE_LINUX_TYPES_H
+		     # include <linux/types.h>
+		     #endif
+
+"
+if test "x$ac_cv_header_linux_types_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_LINUX_TYPES_H 1" >>confdefs.h
+
+fi
+ac_fn_c_check_header_compile "$LINENO" "linux/bsg.h" "ac_cv_header_linux_bsg_h" "#ifdef HAVE_LINUX_TYPES_H
+		     # include <linux/types.h>
+		     #endif
+
+"
+if test "x$ac_cv_header_linux_bsg_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_LINUX_BSG_H 1" >>confdefs.h
+
+fi
+ac_fn_c_check_header_compile "$LINENO" "linux/kdev_t.h" "ac_cv_header_linux_kdev_t_h" "#ifdef HAVE_LINUX_TYPES_H
+		     # include <linux/types.h>
+		     #endif
+
+"
+if test "x$ac_cv_header_linux_kdev_t_h" = xyes
+then :
+  printf "%s\n" "#define HAVE_LINUX_KDEV_T_H 1" >>confdefs.h
+
+fi
+
+}
+
+check_for_linux_sg_v4_hdr() {
+	ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking how to run the C preprocessor" >&5
+printf %s "checking how to run the C preprocessor... " >&6; }
+# On Suns, sometimes $CPP names a directory.
+if test -n "$CPP" && test -d "$CPP"; then
+  CPP=
+fi
+if test -z "$CPP"; then
+  if test ${ac_cv_prog_CPP+y}
+then :
+  printf %s "(cached) " >&6
+else $as_nop
+      # Double quotes because $CC needs to be expanded
+    for CPP in "$CC -E" "$CC -E -traditional-cpp" cpp /lib/cpp
+    do
+      ac_preproc_ok=false
+for ac_c_preproc_warn_flag in '' yes
+do
+  # Use a header file that comes with gcc, so configuring glibc
+  # with a fresh cross-compiler works.
+  # On the NeXT, cc -E runs the code through the compiler's parser,
+  # not just through cpp. "Syntax error" is here to catch this case.
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <limits.h>
+		     Syntax error
+_ACEOF
+if ac_fn_c_try_cpp "$LINENO"
+then :
+
+else $as_nop
+  # Broken: fails on valid input.
+continue
+fi
+rm -f conftest.err conftest.i conftest.$ac_ext
+
+  # OK, works on sane cases.  Now check whether nonexistent headers
+  # can be detected and how.
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <ac_nonexistent.h>
+_ACEOF
+if ac_fn_c_try_cpp "$LINENO"
+then :
+  # Broken: success on invalid input.
+continue
+else $as_nop
+  # Passes both tests.
+ac_preproc_ok=:
+break
+fi
+rm -f conftest.err conftest.i conftest.$ac_ext
+
+done
+# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped.
+rm -f conftest.i conftest.err conftest.$ac_ext
+if $ac_preproc_ok
+then :
+  break
+fi
+
+    done
+    ac_cv_prog_CPP=$CPP
+
+fi
+  CPP=$ac_cv_prog_CPP
+else
+  ac_cv_prog_CPP=$CPP
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CPP" >&5
+printf "%s\n" "$CPP" >&6; }
+ac_preproc_ok=false
+for ac_c_preproc_warn_flag in '' yes
+do
+  # Use a header file that comes with gcc, so configuring glibc
+  # with a fresh cross-compiler works.
+  # On the NeXT, cc -E runs the code through the compiler's parser,
+  # not just through cpp. "Syntax error" is here to catch this case.
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <limits.h>
+		     Syntax error
+_ACEOF
+if ac_fn_c_try_cpp "$LINENO"
+then :
+
+else $as_nop
+  # Broken: fails on valid input.
+continue
+fi
+rm -f conftest.err conftest.i conftest.$ac_ext
+
+  # OK, works on sane cases.  Now check whether nonexistent headers
+  # can be detected and how.
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <ac_nonexistent.h>
+_ACEOF
+if ac_fn_c_try_cpp "$LINENO"
+then :
+  # Broken: success on invalid input.
+continue
+else $as_nop
+  # Passes both tests.
+ac_preproc_ok=:
+break
+fi
+rm -f conftest.err conftest.i conftest.$ac_ext
+
+done
+# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped.
+rm -f conftest.i conftest.err conftest.$ac_ext
+if $ac_preproc_ok
+then :
+
+else $as_nop
+  { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "C preprocessor \"$CPP\" fails sanity check
+See \`config.log' for more details" "$LINENO" 5; }
+fi
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+ # include <scsi/sg.h>
+		  #ifdef SG_IOSUBMIT
+		   found
+		  #endif
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP "found" >/dev/null 2>&1
+then :
+
+printf "%s\n" "#define HAVE_LINUX_SG_V4_HDR 1" >>confdefs.h
+
+fi
+rm -rf conftest*
+
+}
+
+case "${host}" in
+	*-*-android*)
+
+printf "%s\n" "#define SG_LIB_ANDROID 1" >>confdefs.h
+
+
+printf "%s\n" "#define SG_LIB_LINUX 1" >>confdefs.h
+
+		check_for_linux_sg_v4_hdr
+		check_for_getrandom
+		check_for_linux_nvme_headers;;
+        *-*-freebsd*|*-*-kfreebsd*-gnu*)
+
+printf "%s\n" "#define SG_LIB_FREEBSD 1" >>confdefs.h
+
+
+printf "%s\n" "#define HAVE_NVME 1" >>confdefs.h
+
+		check_for_getrandom
+                LIBS="$LIBS -lcam";;
+        *-*-solaris*)
+
+printf "%s\n" "#define SG_LIB_SOLARIS 1" >>confdefs.h
+;;
+        *-*-netbsd*)
+
+printf "%s\n" "#define SG_LIB_NETBSD 1" >>confdefs.h
+;;
+        *-*-openbsd*)
+
+printf "%s\n" "#define SG_LIB_OPENBSD 1" >>confdefs.h
+;;
+        *-*-osf*)
+
+printf "%s\n" "#define SG_LIB_OSF1 1" >>confdefs.h
+;;
+        *-*-cygwin*)
+
+printf "%s\n" "#define SG_LIB_WIN32 1" >>confdefs.h
+
+		# AC_CHECK_HEADERS([nvme.h], [AC_DEFINE_UNQUOTED(HAVE_NVME, 1, [Found NVMe])], [], [])
+
+printf "%s\n" "#define HAVE_NVME 1" >>confdefs.h
+
+		check_for_getrandom
+                CFLAGS="$CFLAGS -Wno-char-subscripts";;
+        *-*-mingw* | *-*-msys*)
+
+printf "%s\n" "#define SG_LIB_WIN32 1" >>confdefs.h
+
+
+printf "%s\n" "#define SG_LIB_MINGW 1" >>confdefs.h
+
+		# AC_CHECK_HEADERS([nvme.h], [AC_DEFINE_UNQUOTED(HAVE_NVME, 1, [Found NVMe])], [], [])
+
+printf "%s\n" "#define HAVE_NVME 1" >>confdefs.h
+
+		check_for_getrandom
+                CFLAGS="$CFLAGS -D__USE_MINGW_ANSI_STDIO";;
+        *-*-linux-gnu* | *-*-linux* | *-*-uclinux-gnu* | *-*-uclinux*)
+
+printf "%s\n" "#define SG_LIB_LINUX 1" >>confdefs.h
+
+		check_for_linux_sg_v4_hdr
+		check_for_getrandom
+                check_for_linux_nvme_headers;;
+        *-*-haiku*)
+
+printf "%s\n" "#define SG_LIB_HAIKU 1" >>confdefs.h
+
+                os_cflags=''
+
+                os_libs=''
+ ;;
+        *)
+
+printf "%s\n" "#define SG_LIB_OTHER 1" >>confdefs.h
+
+		isother=yes;;
+esac
+
+# Define platform-specific symbol.
+ if echo $host_os | grep 'freebsd' > /dev/null; then
+  OS_FREEBSD_TRUE=
+  OS_FREEBSD_FALSE='#'
+else
+  OS_FREEBSD_TRUE='#'
+  OS_FREEBSD_FALSE=
+fi
+
+ if echo $host_os | grep -E '^(uc)?linux' > /dev/null; then
+  OS_LINUX_TRUE=
+  OS_LINUX_FALSE='#'
+else
+  OS_LINUX_TRUE='#'
+  OS_LINUX_FALSE=
+fi
+
+ if echo $host_os | grep '^osf' > /dev/null; then
+  OS_OSF_TRUE=
+  OS_OSF_FALSE='#'
+else
+  OS_OSF_TRUE='#'
+  OS_OSF_FALSE=
+fi
+
+ if echo $host_os | grep '^solaris' > /dev/null; then
+  OS_SOLARIS_TRUE=
+  OS_SOLARIS_FALSE='#'
+else
+  OS_SOLARIS_TRUE='#'
+  OS_SOLARIS_FALSE=
+fi
+
+ if echo $host_os | grep '^mingw' > /dev/null; then
+  OS_WIN32_MINGW_TRUE=
+  OS_WIN32_MINGW_FALSE='#'
+else
+  OS_WIN32_MINGW_TRUE='#'
+  OS_WIN32_MINGW_FALSE=
+fi
+
+ if echo $host_os | grep '^cygwin' > /dev/null; then
+  OS_WIN32_CYGWIN_TRUE=
+  OS_WIN32_CYGWIN_FALSE='#'
+else
+  OS_WIN32_CYGWIN_TRUE='#'
+  OS_WIN32_CYGWIN_FALSE=
+fi
+
+ if echo $host_os | grep 'android' > /dev/null; then
+  OS_ANDROID_TRUE=
+  OS_ANDROID_FALSE='#'
+else
+  OS_ANDROID_TRUE='#'
+  OS_ANDROID_FALSE=
+fi
+
+ if echo $host_os | grep 'netbsd' > /dev/null; then
+  OS_NETBSD_TRUE=
+  OS_NETBSD_FALSE='#'
+else
+  OS_NETBSD_TRUE='#'
+  OS_NETBSD_FALSE=
+fi
+
+ if echo $host_os | grep 'openbsd' > /dev/null; then
+  OS_OPENBSD_TRUE=
+  OS_OPENBSD_FALSE='#'
+else
+  OS_OPENBSD_TRUE='#'
+  OS_OPENBSD_FALSE=
+fi
+
+ if echo $host_os | grep '^haiku' > /dev/null; then
+  OS_HAIKU_TRUE=
+  OS_HAIKU_FALSE='#'
+else
+  OS_HAIKU_TRUE='#'
+  OS_HAIKU_FALSE=
+fi
+
+ if test "x$isother" = "xyes"; then
+  OS_OTHER_TRUE=
+  OS_OTHER_FALSE='#'
+else
+  OS_OTHER_TRUE='#'
+  OS_OTHER_FALSE=
+fi
+
+
+# Check whether --enable-debug was given.
+if test ${enable_debug+y}
+then :
+  enableval=$enable_debug; case "${enableval}" in
+		  yes) debug=true ;;
+		  no)  debug=false ;;
+		  *) as_fn_error $? "bad value ${enableval} for --enable-debug" "$LINENO" 5 ;;
+	       esac
+else $as_nop
+  debug=false
+fi
+
+ if test x$debug = xtrue; then
+  DEBUG_TRUE=
+  DEBUG_FALSE='#'
+else
+  DEBUG_TRUE='#'
+  DEBUG_FALSE=
+fi
+
+
+# Check whether --enable-pt_dummy was given.
+if test ${enable_pt_dummy+y}
+then :
+  enableval=$enable_pt_dummy; case "${enableval}" in
+		  yes) pt_dummy=true ;;
+		  no)  pt_dummy=false ;;
+		  *) as_fn_error $? "bad value ${enableval} for --enable-dummy_pt" "$LINENO" 5 ;;
+	       esac
+else $as_nop
+  pt_dummy=false
+fi
+
+ if test x$pt_dummy = xtrue; then
+  PT_DUMMY_TRUE=
+  PT_DUMMY_FALSE='#'
+else
+  PT_DUMMY_TRUE='#'
+  PT_DUMMY_FALSE=
+fi
+
+
+# Check whether --enable-linuxbsg was given.
+if test ${enable_linuxbsg+y}
+then :
+  enableval=$enable_linuxbsg;
+printf "%s\n" "#define IGNORE_LINUX_BSG 1" >>confdefs.h
+
+fi
+
+
+# Check whether --enable-win32-spt-direct was given.
+if test ${enable_win32_spt_direct+y}
+then :
+  enableval=$enable_win32_spt_direct;
+printf "%s\n" "#define WIN32_SPT_DIRECT 1" >>confdefs.h
+
+
+fi
+
+
+# Check whether --enable-scsistrings was given.
+if test ${enable_scsistrings+y}
+then :
+  enableval=$enable_scsistrings;
+else $as_nop
+
+printf "%s\n" "#define SG_SCSI_STRINGS 1" >>confdefs.h
+
+fi
+
+
+# Check whether --enable-nvme-supp was given.
+if test ${enable_nvme_supp+y}
+then :
+  enableval=$enable_nvme_supp;
+printf "%s\n" "#define IGNORE_NVME 1" >>confdefs.h
+
+fi
+
+
+# Check whether --enable-fast-lebe was given.
+if test ${enable_fast_lebe+y}
+then :
+  enableval=$enable_fast_lebe;
+printf "%s\n" "#define IGNORE_FAST_LEBE 1" >>confdefs.h
+
+fi
+
+
+# Check whether --enable-linux-sgv4 was given.
+if test ${enable_linux_sgv4+y}
+then :
+  enableval=$enable_linux_sgv4;
+printf "%s\n" "#define IGNORE_LINUX_SGV4 1" >>confdefs.h
+
+fi
+
+
+
+ac_config_files="$ac_config_files Makefile include/Makefile lib/Makefile src/Makefile doc/Makefile scripts/Makefile"
+
+cat >confcache <<\_ACEOF
+# This file is a shell script that caches the results of configure
+# tests run on this system so they can be shared between configure
+# scripts and configure runs, see configure's option --config-cache.
+# It is not useful on other systems.  If it contains results you don't
+# want to keep, you may remove or edit it.
+#
+# config.status only pays attention to the cache file if you give it
+# the --recheck option to rerun configure.
+#
+# `ac_cv_env_foo' variables (set or unset) will be overridden when
+# loading this file, other *unset* `ac_cv_foo' will be assigned the
+# following values.
+
+_ACEOF
+
+# The following way of writing the cache mishandles newlines in values,
+# but we know of no workaround that is simple, portable, and efficient.
+# So, we kill variables containing newlines.
+# Ultrix sh set writes to stderr and can't be redirected directly,
+# and sets the high bit in the cache file unless we assign to the vars.
+(
+  for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do
+    eval ac_val=\$$ac_var
+    case $ac_val in #(
+    *${as_nl}*)
+      case $ac_var in #(
+      *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5
+printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;;
+      esac
+      case $ac_var in #(
+      _ | IFS | as_nl) ;; #(
+      BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #(
+      *) { eval $ac_var=; unset $ac_var;} ;;
+      esac ;;
+    esac
+  done
+
+  (set) 2>&1 |
+    case $as_nl`(ac_space=' '; set) 2>&1` in #(
+    *${as_nl}ac_space=\ *)
+      # `set' does not quote correctly, so add quotes: double-quote
+      # substitution turns \\\\ into \\, and sed turns \\ into \.
+      sed -n \
+	"s/'/'\\\\''/g;
+	  s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p"
+      ;; #(
+    *)
+      # `set' quotes correctly as required by POSIX, so do not add quotes.
+      sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p"
+      ;;
+    esac |
+    sort
+) |
+  sed '
+     /^ac_cv_env_/b end
+     t clear
+     :clear
+     s/^\([^=]*\)=\(.*[{}].*\)$/test ${\1+y} || &/
+     t end
+     s/^\([^=]*\)=\(.*\)$/\1=${\1=\2}/
+     :end' >>confcache
+if diff "$cache_file" confcache >/dev/null 2>&1; then :; else
+  if test -w "$cache_file"; then
+    if test "x$cache_file" != "x/dev/null"; then
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5
+printf "%s\n" "$as_me: updating cache $cache_file" >&6;}
+      if test ! -f "$cache_file" || test -h "$cache_file"; then
+	cat confcache >"$cache_file"
+      else
+        case $cache_file in #(
+        */* | ?:*)
+	  mv -f confcache "$cache_file"$$ &&
+	  mv -f "$cache_file"$$ "$cache_file" ;; #(
+        *)
+	  mv -f confcache "$cache_file" ;;
+	esac
+      fi
+    fi
+  else
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5
+printf "%s\n" "$as_me: not updating unwritable cache $cache_file" >&6;}
+  fi
+fi
+rm -f confcache
+
+test "x$prefix" = xNONE && prefix=$ac_default_prefix
+# Let make expand exec_prefix.
+test "x$exec_prefix" = xNONE && exec_prefix='${prefix}'
+
+DEFS=-DHAVE_CONFIG_H
+
+ac_libobjs=
+ac_ltlibobjs=
+for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue
+  # 1. Remove the extension, and $U if already installed.
+  ac_script='s/\$U\././;s/\.o$//;s/\.obj$//'
+  ac_i=`printf "%s\n" "$ac_i" | sed "$ac_script"`
+  # 2. Prepend LIBOBJDIR.  When used with automake>=1.10 LIBOBJDIR
+  #    will be set to the directory where LIBOBJS objects are built.
+  as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext"
+  as_fn_append ac_ltlibobjs " \${LIBOBJDIR}$ac_i"'$U.lo'
+done
+LIBOBJS=$ac_libobjs
+
+LTLIBOBJS=$ac_ltlibobjs
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking that generated files are newer than configure" >&5
+printf %s "checking that generated files are newer than configure... " >&6; }
+   if test -n "$am_sleep_pid"; then
+     # Hide warnings about reused PIDs.
+     wait $am_sleep_pid 2>/dev/null
+   fi
+   { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: done" >&5
+printf "%s\n" "done" >&6; }
+ if test -n "$EXEEXT"; then
+  am__EXEEXT_TRUE=
+  am__EXEEXT_FALSE='#'
+else
+  am__EXEEXT_TRUE='#'
+  am__EXEEXT_FALSE=
+fi
+
+if test -z "${MAINTAINER_MODE_TRUE}" && test -z "${MAINTAINER_MODE_FALSE}"; then
+  as_fn_error $? "conditional \"MAINTAINER_MODE\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${AMDEP_TRUE}" && test -z "${AMDEP_FALSE}"; then
+  as_fn_error $? "conditional \"AMDEP\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${am__fastdepCC_TRUE}" && test -z "${am__fastdepCC_FALSE}"; then
+  as_fn_error $? "conditional \"am__fastdepCC\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_FREEBSD_TRUE}" && test -z "${OS_FREEBSD_FALSE}"; then
+  as_fn_error $? "conditional \"OS_FREEBSD\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_LINUX_TRUE}" && test -z "${OS_LINUX_FALSE}"; then
+  as_fn_error $? "conditional \"OS_LINUX\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_OSF_TRUE}" && test -z "${OS_OSF_FALSE}"; then
+  as_fn_error $? "conditional \"OS_OSF\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_SOLARIS_TRUE}" && test -z "${OS_SOLARIS_FALSE}"; then
+  as_fn_error $? "conditional \"OS_SOLARIS\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_WIN32_MINGW_TRUE}" && test -z "${OS_WIN32_MINGW_FALSE}"; then
+  as_fn_error $? "conditional \"OS_WIN32_MINGW\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_WIN32_CYGWIN_TRUE}" && test -z "${OS_WIN32_CYGWIN_FALSE}"; then
+  as_fn_error $? "conditional \"OS_WIN32_CYGWIN\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_ANDROID_TRUE}" && test -z "${OS_ANDROID_FALSE}"; then
+  as_fn_error $? "conditional \"OS_ANDROID\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_NETBSD_TRUE}" && test -z "${OS_NETBSD_FALSE}"; then
+  as_fn_error $? "conditional \"OS_NETBSD\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_OPENBSD_TRUE}" && test -z "${OS_OPENBSD_FALSE}"; then
+  as_fn_error $? "conditional \"OS_OPENBSD\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_HAIKU_TRUE}" && test -z "${OS_HAIKU_FALSE}"; then
+  as_fn_error $? "conditional \"OS_HAIKU\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${OS_OTHER_TRUE}" && test -z "${OS_OTHER_FALSE}"; then
+  as_fn_error $? "conditional \"OS_OTHER\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${DEBUG_TRUE}" && test -z "${DEBUG_FALSE}"; then
+  as_fn_error $? "conditional \"DEBUG\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${PT_DUMMY_TRUE}" && test -z "${PT_DUMMY_FALSE}"; then
+  as_fn_error $? "conditional \"PT_DUMMY\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+
+: "${CONFIG_STATUS=./config.status}"
+ac_write_fail=0
+ac_clean_files_save=$ac_clean_files
+ac_clean_files="$ac_clean_files $CONFIG_STATUS"
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5
+printf "%s\n" "$as_me: creating $CONFIG_STATUS" >&6;}
+as_write_fail=0
+cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1
+#! $SHELL
+# Generated by $as_me.
+# Run this file to recreate the current configuration.
+# Compiler output produced by configure, useful for debugging
+# configure, is in config.log if it exists.
+
+debug=false
+ac_cs_recheck=false
+ac_cs_silent=false
+
+SHELL=\${CONFIG_SHELL-$SHELL}
+export SHELL
+_ASEOF
+cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1
+## -------------------- ##
+## M4sh Initialization. ##
+## -------------------- ##
+
+# Be more Bourne compatible
+DUALCASE=1; export DUALCASE # for MKS sh
+as_nop=:
+if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1
+then :
+  emulate sh
+  NULLCMD=:
+  # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which
+  # is contrary to our usage.  Disable this feature.
+  alias -g '${1+"$@"}'='"$@"'
+  setopt NO_GLOB_SUBST
+else $as_nop
+  case `(set -o) 2>/dev/null` in #(
+  *posix*) :
+    set -o posix ;; #(
+  *) :
+     ;;
+esac
+fi
+
+
+
+# Reset variables that may have inherited troublesome values from
+# the environment.
+
+# IFS needs to be set, to space, tab, and newline, in precisely that order.
+# (If _AS_PATH_WALK were called with IFS unset, it would have the
+# side effect of setting IFS to empty, thus disabling word splitting.)
+# Quoting is to prevent editors from complaining about space-tab.
+as_nl='
+'
+export as_nl
+IFS=" ""	$as_nl"
+
+PS1='$ '
+PS2='> '
+PS4='+ '
+
+# Ensure predictable behavior from utilities with locale-dependent output.
+LC_ALL=C
+export LC_ALL
+LANGUAGE=C
+export LANGUAGE
+
+# We cannot yet rely on "unset" to work, but we need these variables
+# to be unset--not just set to an empty or harmless value--now, to
+# avoid bugs in old shells (e.g. pre-3.0 UWIN ksh).  This construct
+# also avoids known problems related to "unset" and subshell syntax
+# in other old shells (e.g. bash 2.01 and pdksh 5.2.14).
+for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH
+do eval test \${$as_var+y} \
+  && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || :
+done
+
+# Ensure that fds 0, 1, and 2 are open.
+if (exec 3>&0) 2>/dev/null; then :; else exec 0</dev/null; fi
+if (exec 3>&1) 2>/dev/null; then :; else exec 1>/dev/null; fi
+if (exec 3>&2)            ; then :; else exec 2>/dev/null; fi
+
+# The user is always right.
+if ${PATH_SEPARATOR+false} :; then
+  PATH_SEPARATOR=:
+  (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {
+    (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||
+      PATH_SEPARATOR=';'
+  }
+fi
+
+
+# Find who we are.  Look in the path if we contain no directory separator.
+as_myself=
+case $0 in #((
+  *[\\/]* ) as_myself=$0 ;;
+  *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  case $as_dir in #(((
+    '') as_dir=./ ;;
+    */) ;;
+    *) as_dir=$as_dir/ ;;
+  esac
+    test -r "$as_dir$0" && as_myself=$as_dir$0 && break
+  done
+IFS=$as_save_IFS
+
+     ;;
+esac
+# We did not find ourselves, most probably we were run as `sh COMMAND'
+# in which case we are not to be found in the path.
+if test "x$as_myself" = x; then
+  as_myself=$0
+fi
+if test ! -f "$as_myself"; then
+  printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2
+  exit 1
+fi
+
+
+
+# as_fn_error STATUS ERROR [LINENO LOG_FD]
+# ----------------------------------------
+# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are
+# provided, also output the error to LOG_FD, referencing LINENO. Then exit the
+# script with STATUS, using 1 if that was 0.
+as_fn_error ()
+{
+  as_status=$1; test $as_status -eq 0 && as_status=1
+  if test "$4"; then
+    as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+    printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4
+  fi
+  printf "%s\n" "$as_me: error: $2" >&2
+  as_fn_exit $as_status
+} # as_fn_error
+
+
+
+# as_fn_set_status STATUS
+# -----------------------
+# Set $? to STATUS, without forking.
+as_fn_set_status ()
+{
+  return $1
+} # as_fn_set_status
+
+# as_fn_exit STATUS
+# -----------------
+# Exit the shell with STATUS, even in a "trap 0" or "set -e" context.
+as_fn_exit ()
+{
+  set +e
+  as_fn_set_status $1
+  exit $1
+} # as_fn_exit
+
+# as_fn_unset VAR
+# ---------------
+# Portably unset VAR.
+as_fn_unset ()
+{
+  { eval $1=; unset $1;}
+}
+as_unset=as_fn_unset
+
+# as_fn_append VAR VALUE
+# ----------------------
+# Append the text in VALUE to the end of the definition contained in VAR. Take
+# advantage of any shell optimizations that allow amortized linear growth over
+# repeated appends, instead of the typical quadratic growth present in naive
+# implementations.
+if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null
+then :
+  eval 'as_fn_append ()
+  {
+    eval $1+=\$2
+  }'
+else $as_nop
+  as_fn_append ()
+  {
+    eval $1=\$$1\$2
+  }
+fi # as_fn_append
+
+# as_fn_arith ARG...
+# ------------------
+# Perform arithmetic evaluation on the ARGs, and store the result in the
+# global $as_val. Take advantage of shells that can avoid forks. The arguments
+# must be portable across $(()) and expr.
+if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null
+then :
+  eval 'as_fn_arith ()
+  {
+    as_val=$(( $* ))
+  }'
+else $as_nop
+  as_fn_arith ()
+  {
+    as_val=`expr "$@" || test $? -eq 1`
+  }
+fi # as_fn_arith
+
+
+if expr a : '\(a\)' >/dev/null 2>&1 &&
+   test "X`expr 00001 : '.*\(...\)'`" = X001; then
+  as_expr=expr
+else
+  as_expr=false
+fi
+
+if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then
+  as_basename=basename
+else
+  as_basename=false
+fi
+
+if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then
+  as_dirname=dirname
+else
+  as_dirname=false
+fi
+
+as_me=`$as_basename -- "$0" ||
+$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \
+	 X"$0" : 'X\(//\)$' \| \
+	 X"$0" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X/"$0" |
+    sed '/^.*\/\([^/][^/]*\)\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\/\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\/\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`
+
+# Avoid depending upon Character Ranges.
+as_cr_letters='abcdefghijklmnopqrstuvwxyz'
+as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+as_cr_Letters=$as_cr_letters$as_cr_LETTERS
+as_cr_digits='0123456789'
+as_cr_alnum=$as_cr_Letters$as_cr_digits
+
+
+# Determine whether it's possible to make 'echo' print without a newline.
+# These variables are no longer used directly by Autoconf, but are AC_SUBSTed
+# for compatibility with existing Makefiles.
+ECHO_C= ECHO_N= ECHO_T=
+case `echo -n x` in #(((((
+-n*)
+  case `echo 'xy\c'` in
+  *c*) ECHO_T='	';;	# ECHO_T is single tab character.
+  xy)  ECHO_C='\c';;
+  *)   echo `echo ksh88 bug on AIX 6.1` > /dev/null
+       ECHO_T='	';;
+  esac;;
+*)
+  ECHO_N='-n';;
+esac
+
+# For backward compatibility with old third-party macros, we provide
+# the shell variables $as_echo and $as_echo_n.  New code should use
+# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively.
+as_echo='printf %s\n'
+as_echo_n='printf %s'
+
+rm -f conf$$ conf$$.exe conf$$.file
+if test -d conf$$.dir; then
+  rm -f conf$$.dir/conf$$.file
+else
+  rm -f conf$$.dir
+  mkdir conf$$.dir 2>/dev/null
+fi
+if (echo >conf$$.file) 2>/dev/null; then
+  if ln -s conf$$.file conf$$ 2>/dev/null; then
+    as_ln_s='ln -s'
+    # ... but there are two gotchas:
+    # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail.
+    # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable.
+    # In both cases, we have to default to `cp -pR'.
+    ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe ||
+      as_ln_s='cp -pR'
+  elif ln conf$$.file conf$$ 2>/dev/null; then
+    as_ln_s=ln
+  else
+    as_ln_s='cp -pR'
+  fi
+else
+  as_ln_s='cp -pR'
+fi
+rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file
+rmdir conf$$.dir 2>/dev/null
+
+
+# as_fn_mkdir_p
+# -------------
+# Create "$as_dir" as a directory, including parents if necessary.
+as_fn_mkdir_p ()
+{
+
+  case $as_dir in #(
+  -*) as_dir=./$as_dir;;
+  esac
+  test -d "$as_dir" || eval $as_mkdir_p || {
+    as_dirs=
+    while :; do
+      case $as_dir in #(
+      *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'(
+      *) as_qdir=$as_dir;;
+      esac
+      as_dirs="'$as_qdir' $as_dirs"
+      as_dir=`$as_dirname -- "$as_dir" ||
+$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+	 X"$as_dir" : 'X\(//\)[^/]' \| \
+	 X"$as_dir" : 'X\(//\)$' \| \
+	 X"$as_dir" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$as_dir" |
+    sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)[^/].*/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`
+      test -d "$as_dir" && break
+    done
+    test -z "$as_dirs" || eval "mkdir $as_dirs"
+  } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir"
+
+
+} # as_fn_mkdir_p
+if mkdir -p . 2>/dev/null; then
+  as_mkdir_p='mkdir -p "$as_dir"'
+else
+  test -d ./-p && rmdir ./-p
+  as_mkdir_p=false
+fi
+
+
+# as_fn_executable_p FILE
+# -----------------------
+# Test if FILE is an executable regular file.
+as_fn_executable_p ()
+{
+  test -f "$1" && test -x "$1"
+} # as_fn_executable_p
+as_test_x='test -x'
+as_executable_p=as_fn_executable_p
+
+# Sed expression to map a string onto a valid CPP name.
+as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'"
+
+# Sed expression to map a string onto a valid variable name.
+as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'"
+
+
+exec 6>&1
+## ----------------------------------- ##
+## Main body of $CONFIG_STATUS script. ##
+## ----------------------------------- ##
+_ASEOF
+test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+# Save the log message, to keep $0 and so on meaningful, and to
+# report actual input values of CONFIG_FILES etc. instead of their
+# values after options handling.
+ac_log="
+This file was extended by sg3_utils $as_me 1.48, which was
+generated by GNU Autoconf 2.71.  Invocation command line was
+
+  CONFIG_FILES    = $CONFIG_FILES
+  CONFIG_HEADERS  = $CONFIG_HEADERS
+  CONFIG_LINKS    = $CONFIG_LINKS
+  CONFIG_COMMANDS = $CONFIG_COMMANDS
+  $ $0 $@
+
+on `(hostname || uname -n) 2>/dev/null | sed 1q`
+"
+
+_ACEOF
+
+case $ac_config_files in *"
+"*) set x $ac_config_files; shift; ac_config_files=$*;;
+esac
+
+case $ac_config_headers in *"
+"*) set x $ac_config_headers; shift; ac_config_headers=$*;;
+esac
+
+
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+# Files that config.status was made for.
+config_files="$ac_config_files"
+config_headers="$ac_config_headers"
+config_commands="$ac_config_commands"
+
+_ACEOF
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+ac_cs_usage="\
+\`$as_me' instantiates files and other configuration actions
+from templates according to the current configuration.  Unless the files
+and actions are specified as TAGs, all are instantiated by default.
+
+Usage: $0 [OPTION]... [TAG]...
+
+  -h, --help       print this help, then exit
+  -V, --version    print version number and configuration settings, then exit
+      --config     print configuration, then exit
+  -q, --quiet, --silent
+                   do not print progress messages
+  -d, --debug      don't remove temporary files
+      --recheck    update $as_me by reconfiguring in the same conditions
+      --file=FILE[:TEMPLATE]
+                   instantiate the configuration file FILE
+      --header=FILE[:TEMPLATE]
+                   instantiate the configuration header FILE
+
+Configuration files:
+$config_files
+
+Configuration headers:
+$config_headers
+
+Configuration commands:
+$config_commands
+
+Report bugs to <dgilbert@interlog.com>."
+
+_ACEOF
+ac_cs_config=`printf "%s\n" "$ac_configure_args" | sed "$ac_safe_unquote"`
+ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"`
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+ac_cs_config='$ac_cs_config_escaped'
+ac_cs_version="\\
+sg3_utils config.status 1.48
+configured by $0, generated by GNU Autoconf 2.71,
+  with options \\"\$ac_cs_config\\"
+
+Copyright (C) 2021 Free Software Foundation, Inc.
+This config.status script is free software; the Free Software Foundation
+gives unlimited permission to copy, distribute and modify it."
+
+ac_pwd='$ac_pwd'
+srcdir='$srcdir'
+INSTALL='$INSTALL'
+MKDIR_P='$MKDIR_P'
+AWK='$AWK'
+test -n "\$AWK" || AWK=awk
+_ACEOF
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+# The default lists apply if the user does not specify any file.
+ac_need_defaults=:
+while test $# != 0
+do
+  case $1 in
+  --*=?*)
+    ac_option=`expr "X$1" : 'X\([^=]*\)='`
+    ac_optarg=`expr "X$1" : 'X[^=]*=\(.*\)'`
+    ac_shift=:
+    ;;
+  --*=)
+    ac_option=`expr "X$1" : 'X\([^=]*\)='`
+    ac_optarg=
+    ac_shift=:
+    ;;
+  *)
+    ac_option=$1
+    ac_optarg=$2
+    ac_shift=shift
+    ;;
+  esac
+
+  case $ac_option in
+  # Handling of the options.
+  -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r)
+    ac_cs_recheck=: ;;
+  --version | --versio | --versi | --vers | --ver | --ve | --v | -V )
+    printf "%s\n" "$ac_cs_version"; exit ;;
+  --config | --confi | --conf | --con | --co | --c )
+    printf "%s\n" "$ac_cs_config"; exit ;;
+  --debug | --debu | --deb | --de | --d | -d )
+    debug=: ;;
+  --file | --fil | --fi | --f )
+    $ac_shift
+    case $ac_optarg in
+    *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;;
+    '') as_fn_error $? "missing file argument" ;;
+    esac
+    as_fn_append CONFIG_FILES " '$ac_optarg'"
+    ac_need_defaults=false;;
+  --header | --heade | --head | --hea )
+    $ac_shift
+    case $ac_optarg in
+    *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;;
+    esac
+    as_fn_append CONFIG_HEADERS " '$ac_optarg'"
+    ac_need_defaults=false;;
+  --he | --h)
+    # Conflict between --help and --header
+    as_fn_error $? "ambiguous option: \`$1'
+Try \`$0 --help' for more information.";;
+  --help | --hel | -h )
+    printf "%s\n" "$ac_cs_usage"; exit ;;
+  -q | -quiet | --quiet | --quie | --qui | --qu | --q \
+  | -silent | --silent | --silen | --sile | --sil | --si | --s)
+    ac_cs_silent=: ;;
+
+  # This is an error.
+  -*) as_fn_error $? "unrecognized option: \`$1'
+Try \`$0 --help' for more information." ;;
+
+  *) as_fn_append ac_config_targets " $1"
+     ac_need_defaults=false ;;
+
+  esac
+  shift
+done
+
+ac_configure_extra_args=
+
+if $ac_cs_silent; then
+  exec 6>/dev/null
+  ac_configure_extra_args="$ac_configure_extra_args --silent"
+fi
+
+_ACEOF
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+if \$ac_cs_recheck; then
+  set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion
+  shift
+  \printf "%s\n" "running CONFIG_SHELL=$SHELL \$*" >&6
+  CONFIG_SHELL='$SHELL'
+  export CONFIG_SHELL
+  exec "\$@"
+fi
+
+_ACEOF
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+exec 5>>config.log
+{
+  echo
+  sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX
+## Running $as_me. ##
+_ASBOX
+  printf "%s\n" "$ac_log"
+} >&5
+
+_ACEOF
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+#
+# INIT-COMMANDS
+#
+AMDEP_TRUE="$AMDEP_TRUE" MAKE="${MAKE-make}"
+
+
+# The HP-UX ksh and POSIX shell print the target directory to stdout
+# if CDPATH is set.
+(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
+
+sed_quote_subst='$sed_quote_subst'
+double_quote_subst='$double_quote_subst'
+delay_variable_subst='$delay_variable_subst'
+macro_version='`$ECHO "$macro_version" | $SED "$delay_single_quote_subst"`'
+macro_revision='`$ECHO "$macro_revision" | $SED "$delay_single_quote_subst"`'
+enable_shared='`$ECHO "$enable_shared" | $SED "$delay_single_quote_subst"`'
+enable_static='`$ECHO "$enable_static" | $SED "$delay_single_quote_subst"`'
+pic_mode='`$ECHO "$pic_mode" | $SED "$delay_single_quote_subst"`'
+enable_fast_install='`$ECHO "$enable_fast_install" | $SED "$delay_single_quote_subst"`'
+shared_archive_member_spec='`$ECHO "$shared_archive_member_spec" | $SED "$delay_single_quote_subst"`'
+SHELL='`$ECHO "$SHELL" | $SED "$delay_single_quote_subst"`'
+ECHO='`$ECHO "$ECHO" | $SED "$delay_single_quote_subst"`'
+PATH_SEPARATOR='`$ECHO "$PATH_SEPARATOR" | $SED "$delay_single_quote_subst"`'
+host_alias='`$ECHO "$host_alias" | $SED "$delay_single_quote_subst"`'
+host='`$ECHO "$host" | $SED "$delay_single_quote_subst"`'
+host_os='`$ECHO "$host_os" | $SED "$delay_single_quote_subst"`'
+build_alias='`$ECHO "$build_alias" | $SED "$delay_single_quote_subst"`'
+build='`$ECHO "$build" | $SED "$delay_single_quote_subst"`'
+build_os='`$ECHO "$build_os" | $SED "$delay_single_quote_subst"`'
+SED='`$ECHO "$SED" | $SED "$delay_single_quote_subst"`'
+Xsed='`$ECHO "$Xsed" | $SED "$delay_single_quote_subst"`'
+GREP='`$ECHO "$GREP" | $SED "$delay_single_quote_subst"`'
+EGREP='`$ECHO "$EGREP" | $SED "$delay_single_quote_subst"`'
+FGREP='`$ECHO "$FGREP" | $SED "$delay_single_quote_subst"`'
+LD='`$ECHO "$LD" | $SED "$delay_single_quote_subst"`'
+NM='`$ECHO "$NM" | $SED "$delay_single_quote_subst"`'
+LN_S='`$ECHO "$LN_S" | $SED "$delay_single_quote_subst"`'
+max_cmd_len='`$ECHO "$max_cmd_len" | $SED "$delay_single_quote_subst"`'
+ac_objext='`$ECHO "$ac_objext" | $SED "$delay_single_quote_subst"`'
+exeext='`$ECHO "$exeext" | $SED "$delay_single_quote_subst"`'
+lt_unset='`$ECHO "$lt_unset" | $SED "$delay_single_quote_subst"`'
+lt_SP2NL='`$ECHO "$lt_SP2NL" | $SED "$delay_single_quote_subst"`'
+lt_NL2SP='`$ECHO "$lt_NL2SP" | $SED "$delay_single_quote_subst"`'
+lt_cv_to_host_file_cmd='`$ECHO "$lt_cv_to_host_file_cmd" | $SED "$delay_single_quote_subst"`'
+lt_cv_to_tool_file_cmd='`$ECHO "$lt_cv_to_tool_file_cmd" | $SED "$delay_single_quote_subst"`'
+reload_flag='`$ECHO "$reload_flag" | $SED "$delay_single_quote_subst"`'
+reload_cmds='`$ECHO "$reload_cmds" | $SED "$delay_single_quote_subst"`'
+FILECMD='`$ECHO "$FILECMD" | $SED "$delay_single_quote_subst"`'
+OBJDUMP='`$ECHO "$OBJDUMP" | $SED "$delay_single_quote_subst"`'
+deplibs_check_method='`$ECHO "$deplibs_check_method" | $SED "$delay_single_quote_subst"`'
+file_magic_cmd='`$ECHO "$file_magic_cmd" | $SED "$delay_single_quote_subst"`'
+file_magic_glob='`$ECHO "$file_magic_glob" | $SED "$delay_single_quote_subst"`'
+want_nocaseglob='`$ECHO "$want_nocaseglob" | $SED "$delay_single_quote_subst"`'
+DLLTOOL='`$ECHO "$DLLTOOL" | $SED "$delay_single_quote_subst"`'
+sharedlib_from_linklib_cmd='`$ECHO "$sharedlib_from_linklib_cmd" | $SED "$delay_single_quote_subst"`'
+AR='`$ECHO "$AR" | $SED "$delay_single_quote_subst"`'
+lt_ar_flags='`$ECHO "$lt_ar_flags" | $SED "$delay_single_quote_subst"`'
+AR_FLAGS='`$ECHO "$AR_FLAGS" | $SED "$delay_single_quote_subst"`'
+archiver_list_spec='`$ECHO "$archiver_list_spec" | $SED "$delay_single_quote_subst"`'
+STRIP='`$ECHO "$STRIP" | $SED "$delay_single_quote_subst"`'
+RANLIB='`$ECHO "$RANLIB" | $SED "$delay_single_quote_subst"`'
+old_postinstall_cmds='`$ECHO "$old_postinstall_cmds" | $SED "$delay_single_quote_subst"`'
+old_postuninstall_cmds='`$ECHO "$old_postuninstall_cmds" | $SED "$delay_single_quote_subst"`'
+old_archive_cmds='`$ECHO "$old_archive_cmds" | $SED "$delay_single_quote_subst"`'
+lock_old_archive_extraction='`$ECHO "$lock_old_archive_extraction" | $SED "$delay_single_quote_subst"`'
+CC='`$ECHO "$CC" | $SED "$delay_single_quote_subst"`'
+CFLAGS='`$ECHO "$CFLAGS" | $SED "$delay_single_quote_subst"`'
+compiler='`$ECHO "$compiler" | $SED "$delay_single_quote_subst"`'
+GCC='`$ECHO "$GCC" | $SED "$delay_single_quote_subst"`'
+lt_cv_sys_global_symbol_pipe='`$ECHO "$lt_cv_sys_global_symbol_pipe" | $SED "$delay_single_quote_subst"`'
+lt_cv_sys_global_symbol_to_cdecl='`$ECHO "$lt_cv_sys_global_symbol_to_cdecl" | $SED "$delay_single_quote_subst"`'
+lt_cv_sys_global_symbol_to_import='`$ECHO "$lt_cv_sys_global_symbol_to_import" | $SED "$delay_single_quote_subst"`'
+lt_cv_sys_global_symbol_to_c_name_address='`$ECHO "$lt_cv_sys_global_symbol_to_c_name_address" | $SED "$delay_single_quote_subst"`'
+lt_cv_sys_global_symbol_to_c_name_address_lib_prefix='`$ECHO "$lt_cv_sys_global_symbol_to_c_name_address_lib_prefix" | $SED "$delay_single_quote_subst"`'
+lt_cv_nm_interface='`$ECHO "$lt_cv_nm_interface" | $SED "$delay_single_quote_subst"`'
+nm_file_list_spec='`$ECHO "$nm_file_list_spec" | $SED "$delay_single_quote_subst"`'
+lt_sysroot='`$ECHO "$lt_sysroot" | $SED "$delay_single_quote_subst"`'
+lt_cv_truncate_bin='`$ECHO "$lt_cv_truncate_bin" | $SED "$delay_single_quote_subst"`'
+objdir='`$ECHO "$objdir" | $SED "$delay_single_quote_subst"`'
+MAGIC_CMD='`$ECHO "$MAGIC_CMD" | $SED "$delay_single_quote_subst"`'
+lt_prog_compiler_no_builtin_flag='`$ECHO "$lt_prog_compiler_no_builtin_flag" | $SED "$delay_single_quote_subst"`'
+lt_prog_compiler_pic='`$ECHO "$lt_prog_compiler_pic" | $SED "$delay_single_quote_subst"`'
+lt_prog_compiler_wl='`$ECHO "$lt_prog_compiler_wl" | $SED "$delay_single_quote_subst"`'
+lt_prog_compiler_static='`$ECHO "$lt_prog_compiler_static" | $SED "$delay_single_quote_subst"`'
+lt_cv_prog_compiler_c_o='`$ECHO "$lt_cv_prog_compiler_c_o" | $SED "$delay_single_quote_subst"`'
+need_locks='`$ECHO "$need_locks" | $SED "$delay_single_quote_subst"`'
+MANIFEST_TOOL='`$ECHO "$MANIFEST_TOOL" | $SED "$delay_single_quote_subst"`'
+DSYMUTIL='`$ECHO "$DSYMUTIL" | $SED "$delay_single_quote_subst"`'
+NMEDIT='`$ECHO "$NMEDIT" | $SED "$delay_single_quote_subst"`'
+LIPO='`$ECHO "$LIPO" | $SED "$delay_single_quote_subst"`'
+OTOOL='`$ECHO "$OTOOL" | $SED "$delay_single_quote_subst"`'
+OTOOL64='`$ECHO "$OTOOL64" | $SED "$delay_single_quote_subst"`'
+libext='`$ECHO "$libext" | $SED "$delay_single_quote_subst"`'
+shrext_cmds='`$ECHO "$shrext_cmds" | $SED "$delay_single_quote_subst"`'
+extract_expsyms_cmds='`$ECHO "$extract_expsyms_cmds" | $SED "$delay_single_quote_subst"`'
+archive_cmds_need_lc='`$ECHO "$archive_cmds_need_lc" | $SED "$delay_single_quote_subst"`'
+enable_shared_with_static_runtimes='`$ECHO "$enable_shared_with_static_runtimes" | $SED "$delay_single_quote_subst"`'
+export_dynamic_flag_spec='`$ECHO "$export_dynamic_flag_spec" | $SED "$delay_single_quote_subst"`'
+whole_archive_flag_spec='`$ECHO "$whole_archive_flag_spec" | $SED "$delay_single_quote_subst"`'
+compiler_needs_object='`$ECHO "$compiler_needs_object" | $SED "$delay_single_quote_subst"`'
+old_archive_from_new_cmds='`$ECHO "$old_archive_from_new_cmds" | $SED "$delay_single_quote_subst"`'
+old_archive_from_expsyms_cmds='`$ECHO "$old_archive_from_expsyms_cmds" | $SED "$delay_single_quote_subst"`'
+archive_cmds='`$ECHO "$archive_cmds" | $SED "$delay_single_quote_subst"`'
+archive_expsym_cmds='`$ECHO "$archive_expsym_cmds" | $SED "$delay_single_quote_subst"`'
+module_cmds='`$ECHO "$module_cmds" | $SED "$delay_single_quote_subst"`'
+module_expsym_cmds='`$ECHO "$module_expsym_cmds" | $SED "$delay_single_quote_subst"`'
+with_gnu_ld='`$ECHO "$with_gnu_ld" | $SED "$delay_single_quote_subst"`'
+allow_undefined_flag='`$ECHO "$allow_undefined_flag" | $SED "$delay_single_quote_subst"`'
+no_undefined_flag='`$ECHO "$no_undefined_flag" | $SED "$delay_single_quote_subst"`'
+hardcode_libdir_flag_spec='`$ECHO "$hardcode_libdir_flag_spec" | $SED "$delay_single_quote_subst"`'
+hardcode_libdir_separator='`$ECHO "$hardcode_libdir_separator" | $SED "$delay_single_quote_subst"`'
+hardcode_direct='`$ECHO "$hardcode_direct" | $SED "$delay_single_quote_subst"`'
+hardcode_direct_absolute='`$ECHO "$hardcode_direct_absolute" | $SED "$delay_single_quote_subst"`'
+hardcode_minus_L='`$ECHO "$hardcode_minus_L" | $SED "$delay_single_quote_subst"`'
+hardcode_shlibpath_var='`$ECHO "$hardcode_shlibpath_var" | $SED "$delay_single_quote_subst"`'
+hardcode_automatic='`$ECHO "$hardcode_automatic" | $SED "$delay_single_quote_subst"`'
+inherit_rpath='`$ECHO "$inherit_rpath" | $SED "$delay_single_quote_subst"`'
+link_all_deplibs='`$ECHO "$link_all_deplibs" | $SED "$delay_single_quote_subst"`'
+always_export_symbols='`$ECHO "$always_export_symbols" | $SED "$delay_single_quote_subst"`'
+export_symbols_cmds='`$ECHO "$export_symbols_cmds" | $SED "$delay_single_quote_subst"`'
+exclude_expsyms='`$ECHO "$exclude_expsyms" | $SED "$delay_single_quote_subst"`'
+include_expsyms='`$ECHO "$include_expsyms" | $SED "$delay_single_quote_subst"`'
+prelink_cmds='`$ECHO "$prelink_cmds" | $SED "$delay_single_quote_subst"`'
+postlink_cmds='`$ECHO "$postlink_cmds" | $SED "$delay_single_quote_subst"`'
+file_list_spec='`$ECHO "$file_list_spec" | $SED "$delay_single_quote_subst"`'
+variables_saved_for_relink='`$ECHO "$variables_saved_for_relink" | $SED "$delay_single_quote_subst"`'
+need_lib_prefix='`$ECHO "$need_lib_prefix" | $SED "$delay_single_quote_subst"`'
+need_version='`$ECHO "$need_version" | $SED "$delay_single_quote_subst"`'
+version_type='`$ECHO "$version_type" | $SED "$delay_single_quote_subst"`'
+runpath_var='`$ECHO "$runpath_var" | $SED "$delay_single_quote_subst"`'
+shlibpath_var='`$ECHO "$shlibpath_var" | $SED "$delay_single_quote_subst"`'
+shlibpath_overrides_runpath='`$ECHO "$shlibpath_overrides_runpath" | $SED "$delay_single_quote_subst"`'
+libname_spec='`$ECHO "$libname_spec" | $SED "$delay_single_quote_subst"`'
+library_names_spec='`$ECHO "$library_names_spec" | $SED "$delay_single_quote_subst"`'
+soname_spec='`$ECHO "$soname_spec" | $SED "$delay_single_quote_subst"`'
+install_override_mode='`$ECHO "$install_override_mode" | $SED "$delay_single_quote_subst"`'
+postinstall_cmds='`$ECHO "$postinstall_cmds" | $SED "$delay_single_quote_subst"`'
+postuninstall_cmds='`$ECHO "$postuninstall_cmds" | $SED "$delay_single_quote_subst"`'
+finish_cmds='`$ECHO "$finish_cmds" | $SED "$delay_single_quote_subst"`'
+finish_eval='`$ECHO "$finish_eval" | $SED "$delay_single_quote_subst"`'
+hardcode_into_libs='`$ECHO "$hardcode_into_libs" | $SED "$delay_single_quote_subst"`'
+sys_lib_search_path_spec='`$ECHO "$sys_lib_search_path_spec" | $SED "$delay_single_quote_subst"`'
+configure_time_dlsearch_path='`$ECHO "$configure_time_dlsearch_path" | $SED "$delay_single_quote_subst"`'
+configure_time_lt_sys_library_path='`$ECHO "$configure_time_lt_sys_library_path" | $SED "$delay_single_quote_subst"`'
+hardcode_action='`$ECHO "$hardcode_action" | $SED "$delay_single_quote_subst"`'
+enable_dlopen='`$ECHO "$enable_dlopen" | $SED "$delay_single_quote_subst"`'
+enable_dlopen_self='`$ECHO "$enable_dlopen_self" | $SED "$delay_single_quote_subst"`'
+enable_dlopen_self_static='`$ECHO "$enable_dlopen_self_static" | $SED "$delay_single_quote_subst"`'
+old_striplib='`$ECHO "$old_striplib" | $SED "$delay_single_quote_subst"`'
+striplib='`$ECHO "$striplib" | $SED "$delay_single_quote_subst"`'
+
+LTCC='$LTCC'
+LTCFLAGS='$LTCFLAGS'
+compiler='$compiler_DEFAULT'
+
+# A function that is used when there is no print builtin or printf.
+func_fallback_echo ()
+{
+  eval 'cat <<_LTECHO_EOF
+\$1
+_LTECHO_EOF'
+}
+
+# Quote evaled strings.
+for var in SHELL \
+ECHO \
+PATH_SEPARATOR \
+SED \
+GREP \
+EGREP \
+FGREP \
+LD \
+NM \
+LN_S \
+lt_SP2NL \
+lt_NL2SP \
+reload_flag \
+FILECMD \
+OBJDUMP \
+deplibs_check_method \
+file_magic_cmd \
+file_magic_glob \
+want_nocaseglob \
+DLLTOOL \
+sharedlib_from_linklib_cmd \
+AR \
+archiver_list_spec \
+STRIP \
+RANLIB \
+CC \
+CFLAGS \
+compiler \
+lt_cv_sys_global_symbol_pipe \
+lt_cv_sys_global_symbol_to_cdecl \
+lt_cv_sys_global_symbol_to_import \
+lt_cv_sys_global_symbol_to_c_name_address \
+lt_cv_sys_global_symbol_to_c_name_address_lib_prefix \
+lt_cv_nm_interface \
+nm_file_list_spec \
+lt_cv_truncate_bin \
+lt_prog_compiler_no_builtin_flag \
+lt_prog_compiler_pic \
+lt_prog_compiler_wl \
+lt_prog_compiler_static \
+lt_cv_prog_compiler_c_o \
+need_locks \
+MANIFEST_TOOL \
+DSYMUTIL \
+NMEDIT \
+LIPO \
+OTOOL \
+OTOOL64 \
+shrext_cmds \
+export_dynamic_flag_spec \
+whole_archive_flag_spec \
+compiler_needs_object \
+with_gnu_ld \
+allow_undefined_flag \
+no_undefined_flag \
+hardcode_libdir_flag_spec \
+hardcode_libdir_separator \
+exclude_expsyms \
+include_expsyms \
+file_list_spec \
+variables_saved_for_relink \
+libname_spec \
+library_names_spec \
+soname_spec \
+install_override_mode \
+finish_eval \
+old_striplib \
+striplib; do
+    case \`eval \\\\\$ECHO \\\\""\\\\\$\$var"\\\\"\` in
+    *[\\\\\\\`\\"\\\$]*)
+      eval "lt_\$var=\\\\\\"\\\`\\\$ECHO \\"\\\$\$var\\" | \\\$SED \\"\\\$sed_quote_subst\\"\\\`\\\\\\"" ## exclude from sc_prohibit_nested_quotes
+      ;;
+    *)
+      eval "lt_\$var=\\\\\\"\\\$\$var\\\\\\""
+      ;;
+    esac
+done
+
+# Double-quote double-evaled strings.
+for var in reload_cmds \
+old_postinstall_cmds \
+old_postuninstall_cmds \
+old_archive_cmds \
+extract_expsyms_cmds \
+old_archive_from_new_cmds \
+old_archive_from_expsyms_cmds \
+archive_cmds \
+archive_expsym_cmds \
+module_cmds \
+module_expsym_cmds \
+export_symbols_cmds \
+prelink_cmds \
+postlink_cmds \
+postinstall_cmds \
+postuninstall_cmds \
+finish_cmds \
+sys_lib_search_path_spec \
+configure_time_dlsearch_path \
+configure_time_lt_sys_library_path; do
+    case \`eval \\\\\$ECHO \\\\""\\\\\$\$var"\\\\"\` in
+    *[\\\\\\\`\\"\\\$]*)
+      eval "lt_\$var=\\\\\\"\\\`\\\$ECHO \\"\\\$\$var\\" | \\\$SED -e \\"\\\$double_quote_subst\\" -e \\"\\\$sed_quote_subst\\" -e \\"\\\$delay_variable_subst\\"\\\`\\\\\\"" ## exclude from sc_prohibit_nested_quotes
+      ;;
+    *)
+      eval "lt_\$var=\\\\\\"\\\$\$var\\\\\\""
+      ;;
+    esac
+done
+
+ac_aux_dir='$ac_aux_dir'
+
+# See if we are running on zsh, and set the options that allow our
+# commands through without removal of \ escapes INIT.
+if test -n "\${ZSH_VERSION+set}"; then
+   setopt NO_GLOB_SUBST
+fi
+
+
+    PACKAGE='$PACKAGE'
+    VERSION='$VERSION'
+    RM='$RM'
+    ofile='$ofile'
+
+
+
+
+_ACEOF
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+
+# Handling of arguments.
+for ac_config_target in $ac_config_targets
+do
+  case $ac_config_target in
+    "config.h") CONFIG_HEADERS="$CONFIG_HEADERS config.h" ;;
+    "depfiles") CONFIG_COMMANDS="$CONFIG_COMMANDS depfiles" ;;
+    "libtool") CONFIG_COMMANDS="$CONFIG_COMMANDS libtool" ;;
+    "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;;
+    "include/Makefile") CONFIG_FILES="$CONFIG_FILES include/Makefile" ;;
+    "lib/Makefile") CONFIG_FILES="$CONFIG_FILES lib/Makefile" ;;
+    "src/Makefile") CONFIG_FILES="$CONFIG_FILES src/Makefile" ;;
+    "doc/Makefile") CONFIG_FILES="$CONFIG_FILES doc/Makefile" ;;
+    "scripts/Makefile") CONFIG_FILES="$CONFIG_FILES scripts/Makefile" ;;
+
+  *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;;
+  esac
+done
+
+
+# If the user did not use the arguments to specify the items to instantiate,
+# then the envvar interface is used.  Set only those that are not.
+# We use the long form for the default assignment because of an extremely
+# bizarre bug on SunOS 4.1.3.
+if $ac_need_defaults; then
+  test ${CONFIG_FILES+y} || CONFIG_FILES=$config_files
+  test ${CONFIG_HEADERS+y} || CONFIG_HEADERS=$config_headers
+  test ${CONFIG_COMMANDS+y} || CONFIG_COMMANDS=$config_commands
+fi
+
+# Have a temporary directory for convenience.  Make it in the build tree
+# simply because there is no reason against having it here, and in addition,
+# creating and moving files from /tmp can sometimes cause problems.
+# Hook for its removal unless debugging.
+# Note that there is a small window in which the directory will not be cleaned:
+# after its creation but before its name has been assigned to `$tmp'.
+$debug ||
+{
+  tmp= ac_tmp=
+  trap 'exit_status=$?
+  : "${ac_tmp:=$tmp}"
+  { test ! -d "$ac_tmp" || rm -fr "$ac_tmp"; } && exit $exit_status
+' 0
+  trap 'as_fn_exit 1' 1 2 13 15
+}
+# Create a (secure) tmp directory for tmp files.
+
+{
+  tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null` &&
+  test -d "$tmp"
+}  ||
+{
+  tmp=./conf$$-$RANDOM
+  (umask 077 && mkdir "$tmp")
+} || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5
+ac_tmp=$tmp
+
+# Set up the scripts for CONFIG_FILES section.
+# No need to generate them if there are no CONFIG_FILES.
+# This happens for instance with `./config.status config.h'.
+if test -n "$CONFIG_FILES"; then
+
+
+ac_cr=`echo X | tr X '\015'`
+# On cygwin, bash can eat \r inside `` if the user requested igncr.
+# But we know of no other shell where ac_cr would be empty at this
+# point, so we can use a bashism as a fallback.
+if test "x$ac_cr" = x; then
+  eval ac_cr=\$\'\\r\'
+fi
+ac_cs_awk_cr=`$AWK 'BEGIN { print "a\rb" }' </dev/null 2>/dev/null`
+if test "$ac_cs_awk_cr" = "a${ac_cr}b"; then
+  ac_cs_awk_cr='\\r'
+else
+  ac_cs_awk_cr=$ac_cr
+fi
+
+echo 'BEGIN {' >"$ac_tmp/subs1.awk" &&
+_ACEOF
+
+
+{
+  echo "cat >conf$$subs.awk <<_ACEOF" &&
+  echo "$ac_subst_vars" | sed 's/.*/&!$&$ac_delim/' &&
+  echo "_ACEOF"
+} >conf$$subs.sh ||
+  as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5
+ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'`
+ac_delim='%!_!# '
+for ac_last_try in false false false false false :; do
+  . ./conf$$subs.sh ||
+    as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5
+
+  ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X`
+  if test $ac_delim_n = $ac_delim_num; then
+    break
+  elif $ac_last_try; then
+    as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5
+  else
+    ac_delim="$ac_delim!$ac_delim _$ac_delim!! "
+  fi
+done
+rm -f conf$$subs.sh
+
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK &&
+_ACEOF
+sed -n '
+h
+s/^/S["/; s/!.*/"]=/
+p
+g
+s/^[^!]*!//
+:repl
+t repl
+s/'"$ac_delim"'$//
+t delim
+:nl
+h
+s/\(.\{148\}\)..*/\1/
+t more1
+s/["\\]/\\&/g; s/^/"/; s/$/\\n"\\/
+p
+n
+b repl
+:more1
+s/["\\]/\\&/g; s/^/"/; s/$/"\\/
+p
+g
+s/.\{148\}//
+t nl
+:delim
+h
+s/\(.\{148\}\)..*/\1/
+t more2
+s/["\\]/\\&/g; s/^/"/; s/$/"/
+p
+b
+:more2
+s/["\\]/\\&/g; s/^/"/; s/$/"\\/
+p
+g
+s/.\{148\}//
+t delim
+' <conf$$subs.awk | sed '
+/^[^""]/{
+  N
+  s/\n//
+}
+' >>$CONFIG_STATUS || ac_write_fail=1
+rm -f conf$$subs.awk
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+_ACAWK
+cat >>"\$ac_tmp/subs1.awk" <<_ACAWK &&
+  for (key in S) S_is_set[key] = 1
+  FS = ""
+
+}
+{
+  line = $ 0
+  nfields = split(line, field, "@")
+  substed = 0
+  len = length(field[1])
+  for (i = 2; i < nfields; i++) {
+    key = field[i]
+    keylen = length(key)
+    if (S_is_set[key]) {
+      value = S[key]
+      line = substr(line, 1, len) "" value "" substr(line, len + keylen + 3)
+      len += length(value) + length(field[++i])
+      substed = 1
+    } else
+      len += 1 + keylen
+  }
+
+  print line
+}
+
+_ACAWK
+_ACEOF
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then
+  sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g"
+else
+  cat
+fi < "$ac_tmp/subs1.awk" > "$ac_tmp/subs.awk" \
+  || as_fn_error $? "could not setup config files machinery" "$LINENO" 5
+_ACEOF
+
+# VPATH may cause trouble with some makes, so we remove sole $(srcdir),
+# ${srcdir} and @srcdir@ entries from VPATH if srcdir is ".", strip leading and
+# trailing colons and then remove the whole line if VPATH becomes empty
+# (actually we leave an empty line to preserve line numbers).
+if test "x$srcdir" = x.; then
+  ac_vpsub='/^[	 ]*VPATH[	 ]*=[	 ]*/{
+h
+s///
+s/^/:/
+s/[	 ]*$/:/
+s/:\$(srcdir):/:/g
+s/:\${srcdir}:/:/g
+s/:@srcdir@:/:/g
+s/^:*//
+s/:*$//
+x
+s/\(=[	 ]*\).*/\1/
+G
+s/\n//
+s/^[^=]*=[	 ]*$//
+}'
+fi
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+fi # test -n "$CONFIG_FILES"
+
+# Set up the scripts for CONFIG_HEADERS section.
+# No need to generate them if there are no CONFIG_HEADERS.
+# This happens for instance with `./config.status Makefile'.
+if test -n "$CONFIG_HEADERS"; then
+cat >"$ac_tmp/defines.awk" <<\_ACAWK ||
+BEGIN {
+_ACEOF
+
+# Transform confdefs.h into an awk script `defines.awk', embedded as
+# here-document in config.status, that substitutes the proper values into
+# config.h.in to produce config.h.
+
+# Create a delimiter string that does not exist in confdefs.h, to ease
+# handling of long lines.
+ac_delim='%!_!# '
+for ac_last_try in false false :; do
+  ac_tt=`sed -n "/$ac_delim/p" confdefs.h`
+  if test -z "$ac_tt"; then
+    break
+  elif $ac_last_try; then
+    as_fn_error $? "could not make $CONFIG_HEADERS" "$LINENO" 5
+  else
+    ac_delim="$ac_delim!$ac_delim _$ac_delim!! "
+  fi
+done
+
+# For the awk script, D is an array of macro values keyed by name,
+# likewise P contains macro parameters if any.  Preserve backslash
+# newline sequences.
+
+ac_word_re=[_$as_cr_Letters][_$as_cr_alnum]*
+sed -n '
+s/.\{148\}/&'"$ac_delim"'/g
+t rset
+:rset
+s/^[	 ]*#[	 ]*define[	 ][	 ]*/ /
+t def
+d
+:def
+s/\\$//
+t bsnl
+s/["\\]/\\&/g
+s/^ \('"$ac_word_re"'\)\(([^()]*)\)[	 ]*\(.*\)/P["\1"]="\2"\
+D["\1"]=" \3"/p
+s/^ \('"$ac_word_re"'\)[	 ]*\(.*\)/D["\1"]=" \2"/p
+d
+:bsnl
+s/["\\]/\\&/g
+s/^ \('"$ac_word_re"'\)\(([^()]*)\)[	 ]*\(.*\)/P["\1"]="\2"\
+D["\1"]=" \3\\\\\\n"\\/p
+t cont
+s/^ \('"$ac_word_re"'\)[	 ]*\(.*\)/D["\1"]=" \2\\\\\\n"\\/p
+t cont
+d
+:cont
+n
+s/.\{148\}/&'"$ac_delim"'/g
+t clear
+:clear
+s/\\$//
+t bsnlc
+s/["\\]/\\&/g; s/^/"/; s/$/"/p
+d
+:bsnlc
+s/["\\]/\\&/g; s/^/"/; s/$/\\\\\\n"\\/p
+b cont
+' <confdefs.h | sed '
+s/'"$ac_delim"'/"\\\
+"/g' >>$CONFIG_STATUS || ac_write_fail=1
+
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+  for (key in D) D_is_set[key] = 1
+  FS = ""
+}
+/^[\t ]*#[\t ]*(define|undef)[\t ]+$ac_word_re([\t (]|\$)/ {
+  line = \$ 0
+  split(line, arg, " ")
+  if (arg[1] == "#") {
+    defundef = arg[2]
+    mac1 = arg[3]
+  } else {
+    defundef = substr(arg[1], 2)
+    mac1 = arg[2]
+  }
+  split(mac1, mac2, "(") #)
+  macro = mac2[1]
+  prefix = substr(line, 1, index(line, defundef) - 1)
+  if (D_is_set[macro]) {
+    # Preserve the white space surrounding the "#".
+    print prefix "define", macro P[macro] D[macro]
+    next
+  } else {
+    # Replace #undef with comments.  This is necessary, for example,
+    # in the case of _POSIX_SOURCE, which is predefined and required
+    # on some systems where configure will not decide to define it.
+    if (defundef == "undef") {
+      print "/*", prefix defundef, macro, "*/"
+      next
+    }
+  }
+}
+{ print }
+_ACAWK
+_ACEOF
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+  as_fn_error $? "could not setup config headers machinery" "$LINENO" 5
+fi # test -n "$CONFIG_HEADERS"
+
+
+eval set X "  :F $CONFIG_FILES  :H $CONFIG_HEADERS    :C $CONFIG_COMMANDS"
+shift
+for ac_tag
+do
+  case $ac_tag in
+  :[FHLC]) ac_mode=$ac_tag; continue;;
+  esac
+  case $ac_mode$ac_tag in
+  :[FHL]*:*);;
+  :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;;
+  :[FH]-) ac_tag=-:-;;
+  :[FH]*) ac_tag=$ac_tag:$ac_tag.in;;
+  esac
+  ac_save_IFS=$IFS
+  IFS=:
+  set x $ac_tag
+  IFS=$ac_save_IFS
+  shift
+  ac_file=$1
+  shift
+
+  case $ac_mode in
+  :L) ac_source=$1;;
+  :[FH])
+    ac_file_inputs=
+    for ac_f
+    do
+      case $ac_f in
+      -) ac_f="$ac_tmp/stdin";;
+      *) # Look for the file first in the build tree, then in the source tree
+	 # (if the path is not absolute).  The absolute path cannot be DOS-style,
+	 # because $ac_f cannot contain `:'.
+	 test -f "$ac_f" ||
+	   case $ac_f in
+	   [\\/$]*) false;;
+	   *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";;
+	   esac ||
+	   as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;;
+      esac
+      case $ac_f in *\'*) ac_f=`printf "%s\n" "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac
+      as_fn_append ac_file_inputs " '$ac_f'"
+    done
+
+    # Let's still pretend it is `configure' which instantiates (i.e., don't
+    # use $as_me), people would be surprised to read:
+    #    /* config.h.  Generated by config.status.  */
+    configure_input='Generated from '`
+	  printf "%s\n" "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g'
+	`' by configure.'
+    if test x"$ac_file" != x-; then
+      configure_input="$ac_file.  $configure_input"
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5
+printf "%s\n" "$as_me: creating $ac_file" >&6;}
+    fi
+    # Neutralize special characters interpreted by sed in replacement strings.
+    case $configure_input in #(
+    *\&* | *\|* | *\\* )
+       ac_sed_conf_input=`printf "%s\n" "$configure_input" |
+       sed 's/[\\\\&|]/\\\\&/g'`;; #(
+    *) ac_sed_conf_input=$configure_input;;
+    esac
+
+    case $ac_tag in
+    *:-:* | *:-) cat >"$ac_tmp/stdin" \
+      || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;;
+    esac
+    ;;
+  esac
+
+  ac_dir=`$as_dirname -- "$ac_file" ||
+$as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+	 X"$ac_file" : 'X\(//\)[^/]' \| \
+	 X"$ac_file" : 'X\(//\)$' \| \
+	 X"$ac_file" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$ac_file" |
+    sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)[^/].*/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`
+  as_dir="$ac_dir"; as_fn_mkdir_p
+  ac_builddir=.
+
+case "$ac_dir" in
+.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;;
+*)
+  ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'`
+  # A ".." for each directory in $ac_dir_suffix.
+  ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'`
+  case $ac_top_builddir_sub in
+  "") ac_top_builddir_sub=. ac_top_build_prefix= ;;
+  *)  ac_top_build_prefix=$ac_top_builddir_sub/ ;;
+  esac ;;
+esac
+ac_abs_top_builddir=$ac_pwd
+ac_abs_builddir=$ac_pwd$ac_dir_suffix
+# for backward compatibility:
+ac_top_builddir=$ac_top_build_prefix
+
+case $srcdir in
+  .)  # We are building in place.
+    ac_srcdir=.
+    ac_top_srcdir=$ac_top_builddir_sub
+    ac_abs_top_srcdir=$ac_pwd ;;
+  [\\/]* | ?:[\\/]* )  # Absolute name.
+    ac_srcdir=$srcdir$ac_dir_suffix;
+    ac_top_srcdir=$srcdir
+    ac_abs_top_srcdir=$srcdir ;;
+  *) # Relative name.
+    ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix
+    ac_top_srcdir=$ac_top_build_prefix$srcdir
+    ac_abs_top_srcdir=$ac_pwd/$srcdir ;;
+esac
+ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix
+
+
+  case $ac_mode in
+  :F)
+  #
+  # CONFIG_FILE
+  #
+
+  case $INSTALL in
+  [\\/$]* | ?:[\\/]* ) ac_INSTALL=$INSTALL ;;
+  *) ac_INSTALL=$ac_top_build_prefix$INSTALL ;;
+  esac
+  ac_MKDIR_P=$MKDIR_P
+  case $MKDIR_P in
+  [\\/$]* | ?:[\\/]* ) ;;
+  */*) ac_MKDIR_P=$ac_top_build_prefix$MKDIR_P ;;
+  esac
+_ACEOF
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+# If the template does not know about datarootdir, expand it.
+# FIXME: This hack should be removed a few years after 2.60.
+ac_datarootdir_hack=; ac_datarootdir_seen=
+ac_sed_dataroot='
+/datarootdir/ {
+  p
+  q
+}
+/@datadir@/p
+/@docdir@/p
+/@infodir@/p
+/@localedir@/p
+/@mandir@/p'
+case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in
+*datarootdir*) ac_datarootdir_seen=yes;;
+*@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*)
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5
+printf "%s\n" "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;}
+_ACEOF
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+  ac_datarootdir_hack='
+  s&@datadir@&$datadir&g
+  s&@docdir@&$docdir&g
+  s&@infodir@&$infodir&g
+  s&@localedir@&$localedir&g
+  s&@mandir@&$mandir&g
+  s&\\\${datarootdir}&$datarootdir&g' ;;
+esac
+_ACEOF
+
+# Neutralize VPATH when `$srcdir' = `.'.
+# Shell code in configure.ac might set extrasub.
+# FIXME: do we really want to maintain this feature?
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+ac_sed_extra="$ac_vpsub
+$extrasub
+_ACEOF
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+:t
+/@[a-zA-Z_][a-zA-Z_0-9]*@/!b
+s|@configure_input@|$ac_sed_conf_input|;t t
+s&@top_builddir@&$ac_top_builddir_sub&;t t
+s&@top_build_prefix@&$ac_top_build_prefix&;t t
+s&@srcdir@&$ac_srcdir&;t t
+s&@abs_srcdir@&$ac_abs_srcdir&;t t
+s&@top_srcdir@&$ac_top_srcdir&;t t
+s&@abs_top_srcdir@&$ac_abs_top_srcdir&;t t
+s&@builddir@&$ac_builddir&;t t
+s&@abs_builddir@&$ac_abs_builddir&;t t
+s&@abs_top_builddir@&$ac_abs_top_builddir&;t t
+s&@INSTALL@&$ac_INSTALL&;t t
+s&@MKDIR_P@&$ac_MKDIR_P&;t t
+$ac_datarootdir_hack
+"
+eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$ac_tmp/subs.awk" \
+  >$ac_tmp/out || as_fn_error $? "could not create $ac_file" "$LINENO" 5
+
+test -z "$ac_datarootdir_hack$ac_datarootdir_seen" &&
+  { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } &&
+  { ac_out=`sed -n '/^[	 ]*datarootdir[	 ]*:*=/p' \
+      "$ac_tmp/out"`; test -z "$ac_out"; } &&
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir'
+which seems to be undefined.  Please make sure it is defined" >&5
+printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir'
+which seems to be undefined.  Please make sure it is defined" >&2;}
+
+  rm -f "$ac_tmp/stdin"
+  case $ac_file in
+  -) cat "$ac_tmp/out" && rm -f "$ac_tmp/out";;
+  *) rm -f "$ac_file" && mv "$ac_tmp/out" "$ac_file";;
+  esac \
+  || as_fn_error $? "could not create $ac_file" "$LINENO" 5
+ ;;
+  :H)
+  #
+  # CONFIG_HEADER
+  #
+  if test x"$ac_file" != x-; then
+    {
+      printf "%s\n" "/* $configure_input  */" >&1 \
+      && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs"
+    } >"$ac_tmp/config.h" \
+      || as_fn_error $? "could not create $ac_file" "$LINENO" 5
+    if diff "$ac_file" "$ac_tmp/config.h" >/dev/null 2>&1; then
+      { printf "%s\n" "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5
+printf "%s\n" "$as_me: $ac_file is unchanged" >&6;}
+    else
+      rm -f "$ac_file"
+      mv "$ac_tmp/config.h" "$ac_file" \
+	|| as_fn_error $? "could not create $ac_file" "$LINENO" 5
+    fi
+  else
+    printf "%s\n" "/* $configure_input  */" >&1 \
+      && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" \
+      || as_fn_error $? "could not create -" "$LINENO" 5
+  fi
+# Compute "$ac_file"'s index in $config_headers.
+_am_arg="$ac_file"
+_am_stamp_count=1
+for _am_header in $config_headers :; do
+  case $_am_header in
+    $_am_arg | $_am_arg:* )
+      break ;;
+    * )
+      _am_stamp_count=`expr $_am_stamp_count + 1` ;;
+  esac
+done
+echo "timestamp for $_am_arg" >`$as_dirname -- "$_am_arg" ||
+$as_expr X"$_am_arg" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+	 X"$_am_arg" : 'X\(//\)[^/]' \| \
+	 X"$_am_arg" : 'X\(//\)$' \| \
+	 X"$_am_arg" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$_am_arg" |
+    sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)[^/].*/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`/stamp-h$_am_stamp_count
+ ;;
+
+  :C)  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: executing $ac_file commands" >&5
+printf "%s\n" "$as_me: executing $ac_file commands" >&6;}
+ ;;
+  esac
+
+
+  case $ac_file$ac_mode in
+    "depfiles":C) test x"$AMDEP_TRUE" != x"" || {
+  # Older Autoconf quotes --file arguments for eval, but not when files
+  # are listed without --file.  Let's play safe and only enable the eval
+  # if we detect the quoting.
+  # TODO: see whether this extra hack can be removed once we start
+  # requiring Autoconf 2.70 or later.
+  case $CONFIG_FILES in #(
+  *\'*) :
+    eval set x "$CONFIG_FILES" ;; #(
+  *) :
+    set x $CONFIG_FILES ;; #(
+  *) :
+     ;;
+esac
+  shift
+  # Used to flag and report bootstrapping failures.
+  am_rc=0
+  for am_mf
+  do
+    # Strip MF so we end up with the name of the file.
+    am_mf=`printf "%s\n" "$am_mf" | sed -e 's/:.*$//'`
+    # Check whether this is an Automake generated Makefile which includes
+    # dependency-tracking related rules and includes.
+    # Grep'ing the whole file directly is not great: AIX grep has a line
+    # limit of 2048, but all sed's we know have understand at least 4000.
+    sed -n 's,^am--depfiles:.*,X,p' "$am_mf" | grep X >/dev/null 2>&1 \
+      || continue
+    am_dirpart=`$as_dirname -- "$am_mf" ||
+$as_expr X"$am_mf" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+	 X"$am_mf" : 'X\(//\)[^/]' \| \
+	 X"$am_mf" : 'X\(//\)$' \| \
+	 X"$am_mf" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$am_mf" |
+    sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)[^/].*/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`
+    am_filepart=`$as_basename -- "$am_mf" ||
+$as_expr X/"$am_mf" : '.*/\([^/][^/]*\)/*$' \| \
+	 X"$am_mf" : 'X\(//\)$' \| \
+	 X"$am_mf" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X/"$am_mf" |
+    sed '/^.*\/\([^/][^/]*\)\/*$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\/\(\/\/\)$/{
+	    s//\1/
+	    q
+	  }
+	  /^X\/\(\/\).*/{
+	    s//\1/
+	    q
+	  }
+	  s/.*/./; q'`
+    { echo "$as_me:$LINENO: cd "$am_dirpart" \
+      && sed -e '/# am--include-marker/d' "$am_filepart" \
+        | $MAKE -f - am--depfiles" >&5
+   (cd "$am_dirpart" \
+      && sed -e '/# am--include-marker/d' "$am_filepart" \
+        | $MAKE -f - am--depfiles) >&5 2>&5
+   ac_status=$?
+   echo "$as_me:$LINENO: \$? = $ac_status" >&5
+   (exit $ac_status); } || am_rc=$?
+  done
+  if test $am_rc -ne 0; then
+    { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "Something went wrong bootstrapping makefile fragments
+    for automatic dependency tracking.  If GNU make was not used, consider
+    re-running the configure script with MAKE=\"gmake\" (or whatever is
+    necessary).  You can also try re-running configure with the
+    '--disable-dependency-tracking' option to at least be able to build
+    the package (albeit without support for automatic dependency tracking).
+See \`config.log' for more details" "$LINENO" 5; }
+  fi
+  { am_dirpart=; unset am_dirpart;}
+  { am_filepart=; unset am_filepart;}
+  { am_mf=; unset am_mf;}
+  { am_rc=; unset am_rc;}
+  rm -f conftest-deps.mk
+}
+ ;;
+    "libtool":C)
+
+    # See if we are running on zsh, and set the options that allow our
+    # commands through without removal of \ escapes.
+    if test -n "${ZSH_VERSION+set}"; then
+      setopt NO_GLOB_SUBST
+    fi
+
+    cfgfile=${ofile}T
+    trap "$RM \"$cfgfile\"; exit 1" 1 2 15
+    $RM "$cfgfile"
+
+    cat <<_LT_EOF >> "$cfgfile"
+#! $SHELL
+# Generated automatically by $as_me ($PACKAGE) $VERSION
+# NOTE: Changes made to this file will be lost: look at ltmain.sh.
+
+# Provide generalized library-building support services.
+# Written by Gordon Matzigkeit, 1996
+
+# Copyright (C) 2014 Free Software Foundation, Inc.
+# This is free software; see the source for copying conditions.  There is NO
+# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+# GNU Libtool is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of of the License, or
+# (at your option) any later version.
+#
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program or library that is built
+# using GNU Libtool, you may include this file under the  same
+# distribution terms that you use for the rest of that program.
+#
+# GNU Libtool is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+# The names of the tagged configurations supported by this script.
+available_tags=''
+
+# Configured defaults for sys_lib_dlsearch_path munging.
+: \${LT_SYS_LIBRARY_PATH="$configure_time_lt_sys_library_path"}
+
+# ### BEGIN LIBTOOL CONFIG
+
+# Which release of libtool.m4 was used?
+macro_version=$macro_version
+macro_revision=$macro_revision
+
+# Whether or not to build shared libraries.
+build_libtool_libs=$enable_shared
+
+# Whether or not to build static libraries.
+build_old_libs=$enable_static
+
+# What type of objects to build.
+pic_mode=$pic_mode
+
+# Whether or not to optimize for fast installation.
+fast_install=$enable_fast_install
+
+# Shared archive member basename,for filename based shared library versioning on AIX.
+shared_archive_member_spec=$shared_archive_member_spec
+
+# Shell to use when invoking shell scripts.
+SHELL=$lt_SHELL
+
+# An echo program that protects backslashes.
+ECHO=$lt_ECHO
+
+# The PATH separator for the build system.
+PATH_SEPARATOR=$lt_PATH_SEPARATOR
+
+# The host system.
+host_alias=$host_alias
+host=$host
+host_os=$host_os
+
+# The build system.
+build_alias=$build_alias
+build=$build
+build_os=$build_os
+
+# A sed program that does not truncate output.
+SED=$lt_SED
+
+# Sed that helps us avoid accidentally triggering echo(1) options like -n.
+Xsed="\$SED -e 1s/^X//"
+
+# A grep program that handles long lines.
+GREP=$lt_GREP
+
+# An ERE matcher.
+EGREP=$lt_EGREP
+
+# A literal string matcher.
+FGREP=$lt_FGREP
+
+# A BSD- or MS-compatible name lister.
+NM=$lt_NM
+
+# Whether we need soft or hard links.
+LN_S=$lt_LN_S
+
+# What is the maximum length of a command?
+max_cmd_len=$max_cmd_len
+
+# Object file suffix (normally "o").
+objext=$ac_objext
+
+# Executable file suffix (normally "").
+exeext=$exeext
+
+# whether the shell understands "unset".
+lt_unset=$lt_unset
+
+# turn spaces into newlines.
+SP2NL=$lt_lt_SP2NL
+
+# turn newlines into spaces.
+NL2SP=$lt_lt_NL2SP
+
+# convert \$build file names to \$host format.
+to_host_file_cmd=$lt_cv_to_host_file_cmd
+
+# convert \$build files to toolchain format.
+to_tool_file_cmd=$lt_cv_to_tool_file_cmd
+
+# A file(cmd) program that detects file types.
+FILECMD=$lt_FILECMD
+
+# An object symbol dumper.
+OBJDUMP=$lt_OBJDUMP
+
+# Method to check whether dependent libraries are shared objects.
+deplibs_check_method=$lt_deplibs_check_method
+
+# Command to use when deplibs_check_method = "file_magic".
+file_magic_cmd=$lt_file_magic_cmd
+
+# How to find potential files when deplibs_check_method = "file_magic".
+file_magic_glob=$lt_file_magic_glob
+
+# Find potential files using nocaseglob when deplibs_check_method = "file_magic".
+want_nocaseglob=$lt_want_nocaseglob
+
+# DLL creation program.
+DLLTOOL=$lt_DLLTOOL
+
+# Command to associate shared and link libraries.
+sharedlib_from_linklib_cmd=$lt_sharedlib_from_linklib_cmd
+
+# The archiver.
+AR=$lt_AR
+
+# Flags to create an archive (by configure).
+lt_ar_flags=$lt_ar_flags
+
+# Flags to create an archive.
+AR_FLAGS=\${ARFLAGS-"\$lt_ar_flags"}
+
+# How to feed a file listing to the archiver.
+archiver_list_spec=$lt_archiver_list_spec
+
+# A symbol stripping program.
+STRIP=$lt_STRIP
+
+# Commands used to install an old-style archive.
+RANLIB=$lt_RANLIB
+old_postinstall_cmds=$lt_old_postinstall_cmds
+old_postuninstall_cmds=$lt_old_postuninstall_cmds
+
+# Whether to use a lock for old archive extraction.
+lock_old_archive_extraction=$lock_old_archive_extraction
+
+# A C compiler.
+LTCC=$lt_CC
+
+# LTCC compiler flags.
+LTCFLAGS=$lt_CFLAGS
+
+# Take the output of nm and produce a listing of raw symbols and C names.
+global_symbol_pipe=$lt_lt_cv_sys_global_symbol_pipe
+
+# Transform the output of nm in a proper C declaration.
+global_symbol_to_cdecl=$lt_lt_cv_sys_global_symbol_to_cdecl
+
+# Transform the output of nm into a list of symbols to manually relocate.
+global_symbol_to_import=$lt_lt_cv_sys_global_symbol_to_import
+
+# Transform the output of nm in a C name address pair.
+global_symbol_to_c_name_address=$lt_lt_cv_sys_global_symbol_to_c_name_address
+
+# Transform the output of nm in a C name address pair when lib prefix is needed.
+global_symbol_to_c_name_address_lib_prefix=$lt_lt_cv_sys_global_symbol_to_c_name_address_lib_prefix
+
+# The name lister interface.
+nm_interface=$lt_lt_cv_nm_interface
+
+# Specify filename containing input files for \$NM.
+nm_file_list_spec=$lt_nm_file_list_spec
+
+# The root where to search for dependent libraries,and where our libraries should be installed.
+lt_sysroot=$lt_sysroot
+
+# Command to truncate a binary pipe.
+lt_truncate_bin=$lt_lt_cv_truncate_bin
+
+# The name of the directory that contains temporary libtool files.
+objdir=$objdir
+
+# Used to examine libraries when file_magic_cmd begins with "file".
+MAGIC_CMD=$MAGIC_CMD
+
+# Must we lock files when doing compilation?
+need_locks=$lt_need_locks
+
+# Manifest tool.
+MANIFEST_TOOL=$lt_MANIFEST_TOOL
+
+# Tool to manipulate archived DWARF debug symbol files on Mac OS X.
+DSYMUTIL=$lt_DSYMUTIL
+
+# Tool to change global to local symbols on Mac OS X.
+NMEDIT=$lt_NMEDIT
+
+# Tool to manipulate fat objects and archives on Mac OS X.
+LIPO=$lt_LIPO
+
+# ldd/readelf like tool for Mach-O binaries on Mac OS X.
+OTOOL=$lt_OTOOL
+
+# ldd/readelf like tool for 64 bit Mach-O binaries on Mac OS X 10.4.
+OTOOL64=$lt_OTOOL64
+
+# Old archive suffix (normally "a").
+libext=$libext
+
+# Shared library suffix (normally ".so").
+shrext_cmds=$lt_shrext_cmds
+
+# The commands to extract the exported symbol list from a shared archive.
+extract_expsyms_cmds=$lt_extract_expsyms_cmds
+
+# Variables whose values should be saved in libtool wrapper scripts and
+# restored at link time.
+variables_saved_for_relink=$lt_variables_saved_for_relink
+
+# Do we need the "lib" prefix for modules?
+need_lib_prefix=$need_lib_prefix
+
+# Do we need a version for libraries?
+need_version=$need_version
+
+# Library versioning type.
+version_type=$version_type
+
+# Shared library runtime path variable.
+runpath_var=$runpath_var
+
+# Shared library path variable.
+shlibpath_var=$shlibpath_var
+
+# Is shlibpath searched before the hard-coded library search path?
+shlibpath_overrides_runpath=$shlibpath_overrides_runpath
+
+# Format of library name prefix.
+libname_spec=$lt_libname_spec
+
+# List of archive names.  First name is the real one, the rest are links.
+# The last name is the one that the linker finds with -lNAME
+library_names_spec=$lt_library_names_spec
+
+# The coded name of the library, if different from the real name.
+soname_spec=$lt_soname_spec
+
+# Permission mode override for installation of shared libraries.
+install_override_mode=$lt_install_override_mode
+
+# Command to use after installation of a shared archive.
+postinstall_cmds=$lt_postinstall_cmds
+
+# Command to use after uninstallation of a shared archive.
+postuninstall_cmds=$lt_postuninstall_cmds
+
+# Commands used to finish a libtool library installation in a directory.
+finish_cmds=$lt_finish_cmds
+
+# As "finish_cmds", except a single script fragment to be evaled but
+# not shown.
+finish_eval=$lt_finish_eval
+
+# Whether we should hardcode library paths into libraries.
+hardcode_into_libs=$hardcode_into_libs
+
+# Compile-time system search path for libraries.
+sys_lib_search_path_spec=$lt_sys_lib_search_path_spec
+
+# Detected run-time system search path for libraries.
+sys_lib_dlsearch_path_spec=$lt_configure_time_dlsearch_path
+
+# Explicit LT_SYS_LIBRARY_PATH set during ./configure time.
+configure_time_lt_sys_library_path=$lt_configure_time_lt_sys_library_path
+
+# Whether dlopen is supported.
+dlopen_support=$enable_dlopen
+
+# Whether dlopen of programs is supported.
+dlopen_self=$enable_dlopen_self
+
+# Whether dlopen of statically linked programs is supported.
+dlopen_self_static=$enable_dlopen_self_static
+
+# Commands to strip libraries.
+old_striplib=$lt_old_striplib
+striplib=$lt_striplib
+
+
+# The linker used to build libraries.
+LD=$lt_LD
+
+# How to create reloadable object files.
+reload_flag=$lt_reload_flag
+reload_cmds=$lt_reload_cmds
+
+# Commands used to build an old-style archive.
+old_archive_cmds=$lt_old_archive_cmds
+
+# A language specific compiler.
+CC=$lt_compiler
+
+# Is the compiler the GNU compiler?
+with_gcc=$GCC
+
+# Compiler flag to turn off builtin functions.
+no_builtin_flag=$lt_lt_prog_compiler_no_builtin_flag
+
+# Additional compiler flags for building library objects.
+pic_flag=$lt_lt_prog_compiler_pic
+
+# How to pass a linker flag through the compiler.
+wl=$lt_lt_prog_compiler_wl
+
+# Compiler flag to prevent dynamic linking.
+link_static_flag=$lt_lt_prog_compiler_static
+
+# Does compiler simultaneously support -c and -o options?
+compiler_c_o=$lt_lt_cv_prog_compiler_c_o
+
+# Whether or not to add -lc for building shared libraries.
+build_libtool_need_lc=$archive_cmds_need_lc
+
+# Whether or not to disallow shared libs when runtime libs are static.
+allow_libtool_libs_with_static_runtimes=$enable_shared_with_static_runtimes
+
+# Compiler flag to allow reflexive dlopens.
+export_dynamic_flag_spec=$lt_export_dynamic_flag_spec
+
+# Compiler flag to generate shared objects directly from archives.
+whole_archive_flag_spec=$lt_whole_archive_flag_spec
+
+# Whether the compiler copes with passing no objects directly.
+compiler_needs_object=$lt_compiler_needs_object
+
+# Create an old-style archive from a shared archive.
+old_archive_from_new_cmds=$lt_old_archive_from_new_cmds
+
+# Create a temporary old-style archive to link instead of a shared archive.
+old_archive_from_expsyms_cmds=$lt_old_archive_from_expsyms_cmds
+
+# Commands used to build a shared archive.
+archive_cmds=$lt_archive_cmds
+archive_expsym_cmds=$lt_archive_expsym_cmds
+
+# Commands used to build a loadable module if different from building
+# a shared archive.
+module_cmds=$lt_module_cmds
+module_expsym_cmds=$lt_module_expsym_cmds
+
+# Whether we are building with GNU ld or not.
+with_gnu_ld=$lt_with_gnu_ld
+
+# Flag that allows shared libraries with undefined symbols to be built.
+allow_undefined_flag=$lt_allow_undefined_flag
+
+# Flag that enforces no undefined symbols.
+no_undefined_flag=$lt_no_undefined_flag
+
+# Flag to hardcode \$libdir into a binary during linking.
+# This must work even if \$libdir does not exist
+hardcode_libdir_flag_spec=$lt_hardcode_libdir_flag_spec
+
+# Whether we need a single "-rpath" flag with a separated argument.
+hardcode_libdir_separator=$lt_hardcode_libdir_separator
+
+# Set to "yes" if using DIR/libNAME\$shared_ext during linking hardcodes
+# DIR into the resulting binary.
+hardcode_direct=$hardcode_direct
+
+# Set to "yes" if using DIR/libNAME\$shared_ext during linking hardcodes
+# DIR into the resulting binary and the resulting library dependency is
+# "absolute",i.e impossible to change by setting \$shlibpath_var if the
+# library is relocated.
+hardcode_direct_absolute=$hardcode_direct_absolute
+
+# Set to "yes" if using the -LDIR flag during linking hardcodes DIR
+# into the resulting binary.
+hardcode_minus_L=$hardcode_minus_L
+
+# Set to "yes" if using SHLIBPATH_VAR=DIR during linking hardcodes DIR
+# into the resulting binary.
+hardcode_shlibpath_var=$hardcode_shlibpath_var
+
+# Set to "yes" if building a shared library automatically hardcodes DIR
+# into the library and all subsequent libraries and executables linked
+# against it.
+hardcode_automatic=$hardcode_automatic
+
+# Set to yes if linker adds runtime paths of dependent libraries
+# to runtime path list.
+inherit_rpath=$inherit_rpath
+
+# Whether libtool must link a program against all its dependency libraries.
+link_all_deplibs=$link_all_deplibs
+
+# Set to "yes" if exported symbols are required.
+always_export_symbols=$always_export_symbols
+
+# The commands to list exported symbols.
+export_symbols_cmds=$lt_export_symbols_cmds
+
+# Symbols that should not be listed in the preloaded symbols.
+exclude_expsyms=$lt_exclude_expsyms
+
+# Symbols that must always be exported.
+include_expsyms=$lt_include_expsyms
+
+# Commands necessary for linking programs (against libraries) with templates.
+prelink_cmds=$lt_prelink_cmds
+
+# Commands necessary for finishing linking programs.
+postlink_cmds=$lt_postlink_cmds
+
+# Specify filename containing input files.
+file_list_spec=$lt_file_list_spec
+
+# How to hardcode a shared library path into an executable.
+hardcode_action=$hardcode_action
+
+# ### END LIBTOOL CONFIG
+
+_LT_EOF
+
+    cat <<'_LT_EOF' >> "$cfgfile"
+
+# ### BEGIN FUNCTIONS SHARED WITH CONFIGURE
+
+# func_munge_path_list VARIABLE PATH
+# -----------------------------------
+# VARIABLE is name of variable containing _space_ separated list of
+# directories to be munged by the contents of PATH, which is string
+# having a format:
+# "DIR[:DIR]:"
+#       string "DIR[ DIR]" will be prepended to VARIABLE
+# ":DIR[:DIR]"
+#       string "DIR[ DIR]" will be appended to VARIABLE
+# "DIRP[:DIRP]::[DIRA:]DIRA"
+#       string "DIRP[ DIRP]" will be prepended to VARIABLE and string
+#       "DIRA[ DIRA]" will be appended to VARIABLE
+# "DIR[:DIR]"
+#       VARIABLE will be replaced by "DIR[ DIR]"
+func_munge_path_list ()
+{
+    case x$2 in
+    x)
+        ;;
+    *:)
+        eval $1=\"`$ECHO $2 | $SED 's/:/ /g'` \$$1\"
+        ;;
+    x:*)
+        eval $1=\"\$$1 `$ECHO $2 | $SED 's/:/ /g'`\"
+        ;;
+    *::*)
+        eval $1=\"\$$1\ `$ECHO $2 | $SED -e 's/.*:://' -e 's/:/ /g'`\"
+        eval $1=\"`$ECHO $2 | $SED -e 's/::.*//' -e 's/:/ /g'`\ \$$1\"
+        ;;
+    *)
+        eval $1=\"`$ECHO $2 | $SED 's/:/ /g'`\"
+        ;;
+    esac
+}
+
+
+# Calculate cc_basename.  Skip known compiler wrappers and cross-prefix.
+func_cc_basename ()
+{
+    for cc_temp in $*""; do
+      case $cc_temp in
+        compile | *[\\/]compile | ccache | *[\\/]ccache ) ;;
+        distcc | *[\\/]distcc | purify | *[\\/]purify ) ;;
+        \-*) ;;
+        *) break;;
+      esac
+    done
+    func_cc_basename_result=`$ECHO "$cc_temp" | $SED "s%.*/%%; s%^$host_alias-%%"`
+}
+
+
+# ### END FUNCTIONS SHARED WITH CONFIGURE
+
+_LT_EOF
+
+  case $host_os in
+  aix3*)
+    cat <<\_LT_EOF >> "$cfgfile"
+# AIX sometimes has problems with the GCC collect2 program.  For some
+# reason, if we set the COLLECT_NAMES environment variable, the problems
+# vanish in a puff of smoke.
+if test set != "${COLLECT_NAMES+set}"; then
+  COLLECT_NAMES=
+  export COLLECT_NAMES
+fi
+_LT_EOF
+    ;;
+  esac
+
+
+
+ltmain=$ac_aux_dir/ltmain.sh
+
+
+  # We use sed instead of cat because bash on DJGPP gets confused if
+  # if finds mixed CR/LF and LF-only lines.  Since sed operates in
+  # text mode, it properly converts lines to CR/LF.  This bash problem
+  # is reportedly fixed, but why not run on old versions too?
+  $SED '$q' "$ltmain" >> "$cfgfile" \
+     || (rm -f "$cfgfile"; exit 1)
+
+   mv -f "$cfgfile" "$ofile" ||
+    (rm -f "$ofile" && cp "$cfgfile" "$ofile" && rm -f "$cfgfile")
+  chmod +x "$ofile"
+
+ ;;
+
+  esac
+done # for ac_tag
+
+
+as_fn_exit 0
+_ACEOF
+ac_clean_files=$ac_clean_files_save
+
+test $ac_write_fail = 0 ||
+  as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5
+
+
+# configure is writing to config.log, and then calls config.status.
+# config.status does its own redirection, appending to config.log.
+# Unfortunately, on DOS this fails, as config.log is still kept open
+# by configure, so config.status won't be able to write to it; its
+# output is simply discarded.  So we exec the FD to /dev/null,
+# effectively closing config.log, so it can be properly (re)opened and
+# appended to by config.status.  When coming back to configure, we
+# need to make the FD available again.
+if test "$no_create" != yes; then
+  ac_cs_success=:
+  ac_config_status_args=
+  test "$silent" = yes &&
+    ac_config_status_args="$ac_config_status_args --quiet"
+  exec 5>/dev/null
+  $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false
+  exec 5>>config.log
+  # Use ||, not &&, to avoid exiting from the if with $? = 1, which
+  # would make configure fail if this is the last instruction.
+  $ac_cs_success || as_fn_exit 1
+fi
+if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5
+printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;}
+fi
+
+
+
+# Borrowed from smartmontools configure.ac
+# Note: Use `...` here as some shells do not properly parse '$(... case $x in X) ...)'
+info=`
+  echo "-----------------------------------------------------------------------------"
+  echo "${PACKAGE}-${VERSION} configuration:"
+  echo "host operating system:  $host"
+  echo "default C compiler:     $CC"
+
+  case "$host_os" in
+    mingw*)
+      echo "application manifest:   ${os_win32_manifest:-built-in}"
+      echo "resource compiler:      $WINDRES"
+      echo "message compiler:       $WINDMC"
+      echo "NSIS compiler:          $MAKENSIS"
+      ;;
+
+    *)
+      echo "binary install path:    \`eval eval eval echo $bindir\`"
+      echo "scripts install path:   \`eval eval eval echo $bindir\`"
+      echo "man page install path:  \`eval eval eval echo $mandir\`"
+      ;;
+  esac
+  echo "-----------------------------------------------------------------------------"
+`
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}:
+$info
+" >&5
+printf "%s\n" "$as_me:
+$info
+" >&6;}
+
+
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..c6deea8
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,220 @@
+AC_INIT(sg3_utils, 1.48, dgilbert@interlog.com)
+
+AM_INIT_AUTOMAKE([-Wall -Werror foreign])
+AM_MAINTAINER_MODE
+AM_CONFIG_HEADER(config.h)
+
+AC_PROG_CC
+# AC_PROG_CXX
+AC_PROG_INSTALL
+
+# AM_PROG_AR is supported and needed since automake v1.12+
+ifdef([AM_PROG_AR], [AM_PROG_AR], []) 
+		
+# Adding libtools to the build seems to bring in C++ environment
+AC_PROG_LIBTOOL
+
+# check for headers
+AC_HEADER_STDC
+AC_CHECK_HEADERS([byteswap.h stdatomic.h], [], [], [])
+
+# check for functions
+AC_CHECK_FUNCS(getopt_long,
+	       GETOPT_O_FILES='',
+	       GETOPT_O_FILES='getopt_long.o')
+AC_CHECK_FUNCS(posix_fadvise)
+AC_CHECK_FUNCS(posix_memalign)
+AC_CHECK_FUNCS(gettimeofday)
+AC_CHECK_FUNCS(sysconf)
+AC_CHECK_FUNCS(lseek64)
+AC_CHECK_FUNCS(srand48_r)
+SAVED_LIBS=$LIBS
+AC_SEARCH_LIBS([pthread_create], [pthread])
+# AC_SEARCH_LIBS adds libraries at the start of $LIBS so remove $SAVED_LIBS
+# from the end of $LIBS.
+pthread_lib=${LIBS%${SAVED_LIBS}}
+AC_CHECK_FUNCS([pthread_cancel pthread_kill])
+LIBS=$SAVED_LIBS
+AC_SUBST(PTHREAD_LIB, [$pthread_lib])
+
+SAVED_LIBS=$LIBS
+AC_SEARCH_LIBS([clock_gettime], [rt])
+rt_lib=${LIBS%${SAVED_LIBS}}
+AC_CHECK_FUNCS(clock_gettime)
+LIBS=$SAVED_LIBS
+AC_SUBST(RT_LIB, [$rt_lib])
+
+AC_SUBST(GETOPT_O_FILES)
+
+
+AC_CANONICAL_HOST
+
+AC_DEFINE_UNQUOTED(SG_LIB_BUILD_HOST, "${host}", [sg3_utils Build Host])
+
+check_for_getrandom() {
+	AC_CHECK_HEADERS([sys/random.h], [AC_DEFINE_UNQUOTED(HAVE_GETRANDOM, 1, [Found sys/random.h])], [], [])
+}
+
+check_for_linux_nvme_headers() {
+	AC_CHECK_HEADERS([linux/nvme_ioctl.h], [AC_DEFINE_UNQUOTED(HAVE_NVME, 1, [Found NVMe])], [], [])
+	AC_CHECK_HEADERS([linux/types.h linux/bsg.h linux/kdev_t.h], [], [],
+		     [[#ifdef HAVE_LINUX_TYPES_H
+		     # include <linux/types.h>
+		     #endif
+		     ]])
+}
+
+check_for_linux_sg_v4_hdr() {
+	AC_EGREP_CPP(found,
+		[ # include <scsi/sg.h>
+		  #ifdef SG_IOSUBMIT
+		   found
+		  #endif
+		],
+		[AC_DEFINE_UNQUOTED(HAVE_LINUX_SG_V4_HDR, 1, [Have Linux sg v4 header]) ])
+}
+
+case "${host}" in
+	*-*-android*)
+		AC_DEFINE_UNQUOTED(SG_LIB_ANDROID, 1, [sg3_utils on android])
+		AC_DEFINE_UNQUOTED(SG_LIB_LINUX, 1, [sg3_utils on linux])
+		check_for_linux_sg_v4_hdr
+		check_for_getrandom
+		check_for_linux_nvme_headers;;
+        *-*-freebsd*|*-*-kfreebsd*-gnu*)
+		AC_DEFINE_UNQUOTED(SG_LIB_FREEBSD, 1, [sg3_utils on FreeBSD])
+		AC_DEFINE_UNQUOTED(HAVE_NVME, 1, [Found NVMe])
+		check_for_getrandom
+                LIBS="$LIBS -lcam";;
+        *-*-solaris*)
+		AC_DEFINE_UNQUOTED(SG_LIB_SOLARIS, 1, [sg3_utils on Solaris]);;
+        *-*-netbsd*)
+		AC_DEFINE_UNQUOTED(SG_LIB_NETBSD, 1, [sg3_utils on NetBSD]);;
+        *-*-openbsd*)
+		AC_DEFINE_UNQUOTED(SG_LIB_OPENBSD, 1, [sg3_utils on OpenBSD]);;
+        *-*-osf*)
+		AC_DEFINE_UNQUOTED(SG_LIB_OSF1, 1, [sg3_utils on Tru64 UNIX]);;
+        *-*-cygwin*)
+		AC_DEFINE_UNQUOTED(SG_LIB_WIN32, 1, [sg3_utils on Win32])
+		# AC_CHECK_HEADERS([nvme.h], [AC_DEFINE_UNQUOTED(HAVE_NVME, 1, [Found NVMe])], [], [])
+		AC_DEFINE_UNQUOTED(HAVE_NVME, 1, [Found NVMe])
+		check_for_getrandom
+                CFLAGS="$CFLAGS -Wno-char-subscripts";;
+        *-*-mingw* | *-*-msys*)
+		AC_DEFINE_UNQUOTED(SG_LIB_WIN32, 1, [sg3_utils on Win32])
+		AC_DEFINE_UNQUOTED(SG_LIB_MINGW, 1, [also MinGW environment])
+		# AC_CHECK_HEADERS([nvme.h], [AC_DEFINE_UNQUOTED(HAVE_NVME, 1, [Found NVMe])], [], [])
+		AC_DEFINE_UNQUOTED(HAVE_NVME, 1, [Found NVMe])
+		check_for_getrandom
+                CFLAGS="$CFLAGS -D__USE_MINGW_ANSI_STDIO";;
+        *-*-linux-gnu* | *-*-linux* | *-*-uclinux-gnu* | *-*-uclinux*)
+                AC_DEFINE_UNQUOTED(SG_LIB_LINUX, 1, [sg3_utils on linux])
+		check_for_linux_sg_v4_hdr
+		check_for_getrandom
+                check_for_linux_nvme_headers;;
+        *-*-haiku*)
+		AC_DEFINE_UNQUOTED(SG_LIB_HAIKU, 1, [sg3_utils on Haiku])
+                AC_SUBST([os_cflags], [''])
+                AC_SUBST([os_libs], ['']) ;;
+        *)
+                AC_DEFINE_UNQUOTED(SG_LIB_OTHER, 1, [sg3_utils on other])
+		isother=yes;;
+esac
+
+# Define platform-specific symbol.
+AM_CONDITIONAL(OS_FREEBSD, [echo $host_os | grep 'freebsd' > /dev/null])
+AM_CONDITIONAL(OS_LINUX, [echo $host_os | grep -E '^(uc)?linux' > /dev/null])
+AM_CONDITIONAL(OS_OSF, [echo $host_os | grep '^osf' > /dev/null])
+AM_CONDITIONAL(OS_SOLARIS, [echo $host_os | grep '^solaris' > /dev/null])
+AM_CONDITIONAL(OS_WIN32_MINGW, [echo $host_os | grep '^mingw' > /dev/null])
+AM_CONDITIONAL(OS_WIN32_CYGWIN, [echo $host_os | grep '^cygwin' > /dev/null])
+AM_CONDITIONAL(OS_ANDROID, [echo $host_os | grep 'android' > /dev/null])
+AM_CONDITIONAL(OS_NETBSD, [echo $host_os | grep 'netbsd' > /dev/null])
+AM_CONDITIONAL(OS_OPENBSD, [echo $host_os | grep 'openbsd' > /dev/null])
+AM_CONDITIONAL(OS_HAIKU, [echo $host_os | grep '^haiku' > /dev/null])
+AM_CONDITIONAL(OS_OTHER, [test "x$isother" = "xyes"])
+
+AC_ARG_ENABLE([debug],
+	      [  --enable-debug          Turn on debugging],
+	      [case "${enableval}" in
+		  yes) debug=true ;;
+		  no)  debug=false ;;
+		  *) AC_MSG_ERROR([bad value ${enableval} for --enable-debug]) ;;
+	       esac],[debug=false])
+AM_CONDITIONAL([DEBUG], [test x$debug = xtrue])
+
+AC_ARG_ENABLE([pt_dummy],
+	      [  --enable-pt_dummy       pass-through codes compiles, does nothing],
+	      [case "${enableval}" in
+		  yes) pt_dummy=true ;;
+		  no)  pt_dummy=false ;;
+		  *) AC_MSG_ERROR([bad value ${enableval} for --enable-dummy_pt]) ;;
+	       esac],[pt_dummy=false])
+AM_CONDITIONAL([PT_DUMMY], [test x$pt_dummy = xtrue])
+
+AC_ARG_ENABLE([linuxbsg],
+  AC_HELP_STRING([--disable-linuxbsg], [option ignored, this is placeholder]),
+  [AC_DEFINE_UNQUOTED(IGNORE_LINUX_BSG, 1, [option ignored], )], [])
+
+AC_ARG_ENABLE([win32-spt-direct],
+  AC_HELP_STRING([--enable-win32-spt-direct], [enable Win32 SPT Direct]),
+  AC_DEFINE_UNQUOTED(WIN32_SPT_DIRECT, 1, [enable Win32 SPT Direct], )
+)
+
+AC_ARG_ENABLE([scsistrings],
+  [AS_HELP_STRING([--disable-scsistrings],
+		  [Disable full SCSI sense strings and NVMe status strings])],
+  [], [AC_DEFINE_UNQUOTED(SG_SCSI_STRINGS, 1, [full SCSI sense strings and NVMe status strings], )])
+
+AC_ARG_ENABLE([nvme-supp],
+  AC_HELP_STRING([--disable-nvme-supp], [remove all or most NVMe code]),
+  [AC_DEFINE_UNQUOTED(IGNORE_NVME, 1, [compile out NVMe support], )], [])
+
+AC_ARG_ENABLE([fast-lebe],
+  AC_HELP_STRING([--disable-fast-lebe], [use generic little-endian/big-endian code instead]),
+  [AC_DEFINE_UNQUOTED(IGNORE_FAST_LEBE, 1, [use generic little-endian/big-endian instead], )], [])
+
+AC_ARG_ENABLE([linux-sgv4],
+  AC_HELP_STRING([--disable-linux-sgv4], [for Linux sg driver avoid v4 interface even if available]),
+  [AC_DEFINE_UNQUOTED(IGNORE_LINUX_SGV4, 1, [even if Linux sg v4 available, use v3 instead], )], [])
+
+
+AC_OUTPUT(
+	Makefile
+	include/Makefile
+	lib/Makefile
+	src/Makefile
+	doc/Makefile
+	scripts/Makefile
+)
+
+
+# Borrowed from smartmontools configure.ac
+# Note: Use `...` here as some shells do not properly parse '$(... case $x in X) ...)'
+info=`
+  echo "-----------------------------------------------------------------------------"
+  echo "${PACKAGE}-${VERSION} configuration:"
+  echo "host operating system:  $host"
+  echo "default C compiler:     $CC"
+
+  case "$host_os" in
+    mingw*)
+      echo "application manifest:   ${os_win32_manifest:-built-in}"
+      echo "resource compiler:      $WINDRES"
+      echo "message compiler:       $WINDMC"
+      echo "NSIS compiler:          $MAKENSIS"
+      ;;
+
+    *)
+      echo "binary install path:    \`eval eval eval echo $bindir\`"
+      echo "scripts install path:   \`eval eval eval echo $bindir\`"
+      echo "man page install path:  \`eval eval eval echo $mandir\`"
+      ;;
+  esac
+  echo "-----------------------------------------------------------------------------"
+`
+
+AC_MSG_NOTICE([
+$info
+])
+
diff --git a/debian/README.debian4 b/debian/README.debian4
new file mode 100644
index 0000000..900b04e
--- /dev/null
+++ b/debian/README.debian4
@@ -0,0 +1,14 @@
+For whatever reason Debian build scripts (e.g. debhelper and dbclean)
+seem to have changed in such a way to be Debian 4.0 ("etch") unfriendly.
+
+So when the ./build_debian.sh script is called on a Debian 4.0 system,
+it fails saying the debhelper is too old. That can be fixed by editing
+the 'control' file, changing this line:
+    Build-Depends: debhelper (>> 7), libtool, libcam-dev [kfreebsd-i386 kfreebsd-amd64]
+to:
+    Build-Depends: debhelper, libtool, libcam-dev [kfreebsd-i386 kfreebsd-amd64]
+
+The script then dies in dbclean and the hack to get around that is to
+edit the 'compat' file. It contains "7" which needs to be changed to
+"4". Evidently "4" is deprecated and "5" is preferable and should work.
+
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..9ebae1e
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,278 @@
+sg3-utils (1.48-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Sat, 12 Nov 2022 21:00:00 -0500
+
+sg3-utils (1.47-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Tue, 09 Nov 2021 12:00:00 -0500
+
+sg3-utils (1.46-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Mon, 29 Mar 2021 01:00:00 -0400
+
+sg3-utils (1.45-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Sat, 29 Feb 2020 20:00:00 -0500
+
+sg3-utils (1.44-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Wed, 12 Sep 2018 14:00:00 -0400
+
+sg3-utils (1.43-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Tue, 11 Sep 2018 09:00:00 -0400
+
+sg3-utils (1.42-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Wed, 17 Feb 2016 15:00:00 -0500
+
+sg3-utils (1.41-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Tue, 28 Apr 2015 11:00:00 -0400
+
+sg3-utils (1.40-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Mon, 10 Nov 2014 23:00:00 -0500
+
+sg3-utils (1.39-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Thu, 12 Jun 2014 09:00:00 -0400
+
+sg3-utils (1.38-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Tue, 01 Apr 2014 15:00:00 -0400
+
+sg3-utils (1.37-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Mon, 14 Oct 2013 15:00:00 -0400
+
+sg3-utils (1.36-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Fri, 31 May 2013 10:00:00 -0400
+
+sg3-utils (1.35-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Thu, 17 Jan 2013 19:00:00 -0500
+
+sg3-utils (1.34-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Sat, 13 Oct 2012 19:00:00 -0400
+
+sg3-utils (1.33-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Wed, 18 Jan 2012 14:00:00 -0500
+
+sg3-utils (1.32-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Wed, 22 Jun 2011 16:00:00 -0400
+
+sg3-utils (1.31-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Wed, 16 Feb 2011 16:00:00 -0500
+
+sg3-utils (1.30-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Fri, 05 Nov 2010 10:30:00 -0400
+
+sg3-utils (1.29-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Wed, 31 Mar 2010 23:00:00 -0400
+
+sg3-utils (1.28-0.1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Douglas Gilbert <dgilbert@interlog.com>  Fri, 02 Oct 2009 00:20:00 -0400
+
+sg3-utils (1.27-0.1) unstable; urgency=low
+
+  [ Martin Pitt ]
+  * Non-maintainer upload; this package blocks DeviceKit, and maintainer is
+    apparently not active any more. Also clean up and modernize the package
+    somewhat while we are at it.
+  * New upstream release (required for current devicekit-disks).
+    (Closes: #532546). Upstream original tarball repacked to not contain
+    debian/ directory.
+  * Rename libsgutils1{,-dev} to libsgutils2-2{,-dev}, upstream bumped SONAME.
+    Also call the library libgsutils2-2 to match SONAME. Add
+    Conflicts/Replaces for "libsgutils2" to provide a clean upgrade from the
+    packages as provided by Upstream and Ubuntu.
+  * debian/rules: Update build rules for upstream Makefile → autotools switch.
+  * debian/rules: Fix cleaning a clean source package.
+  * Demote Recommends to Suggests; the library doesn't actually call the
+    binaries in sg3-utils. (Closes: #532547)
+  * Drop debian/*.dirs, unnecessary with dh_install.
+  * Drop sg3-utils.preinst, not necessary to deal with kernel 2.4 any more.
+  * Drop libsgutils2-0.install, libsgutils2-0-dev.install, these packages
+    don't exist.
+  * Drop libsgutils2.post{inst,rm}: Basically empty, debhelper will create its
+    own.
+  * libsgutils2-dev.install: Drop *.lo.
+  * debian/compat: 4 -> 7. Bump debhelper build-depends accordingly.
+  * debian/control: Bump Standards-Version to 3.8.2.
+  * debian/control: Modernize package description for Linux 2.6.
+    (Closes: #506578)
+  * debian/rules: Drop -k argument from dh_clean (thanks lintian).
+
+  [ Frank Lichtenheld ]
+  * debian/control, debian/rules: Add dependency of libsgutils-dev on
+    libcam-dev on kfreebsd-*. (Closes: #519460)
+
+ -- Martin Pitt <mpitt@debian.org>  Mon, 22 Jun 2009 12:04:20 +0200
+
+sg3-utils (1.24-2) unstable; urgency=low
+
+  * Cleaned up package description (Closes: #445920).
+  * Don't make libtool think rpath is necessary (Closes: #451153)
+  * Capitalized Linux in extended description (Closes: #457526).
+  * Completed sentence in libsgutils1 long description (Closes: #421391).
+  * Added patch from Aurelian Jarno to build on kfreebsd-* (Closes: #455430).
+  * Symlinks in examples directory cleaned up (Closes: #372610).
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Sun, 30 Dec 2007 11:52:58 -0700
+
+sg3-utils (1.24-1) unstable; urgency=low
+
+  * New upstream release
+  * Conflicts with upstream libsgutils package libsgutils-1-0 (closes: #391077)
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Tue,  5 Jun 2007 17:04:21 -0600
+
+sg3-utils (1.21-2.1) unstable; urgency=medium
+
+  * Non-maintainer upload.
+  * Fix FTBFS due to old syscall usage (Closes: #395512).
+
+ -- Luk Claes <luk@debian.org>  Sun,  5 Nov 2006 17:23:29 +0100
+
+sg3-utils (1.21-2) unstable; urgency=low
+
+  * Added Depends on libsgutils1 to libsgutils1-dev (closes: #387798)
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Fri, 22 Sep 2006 00:20:28 -0600
+
+sg3-utils (1.21-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Wed, 13 Sep 2006 21:54:30 -0600
+
+sg3-utils (1.20-1) unstable; urgency=low
+
+  * New upstream release
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Wed, 26 Apr 2006 22:31:15 -0600
+
+sg3-utils (1.17-3) unstable; urgency=low
+
+  * Cleaned up sg_read(8) manpage (Closes: #294521)
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Mon, 13 Feb 2006 17:59:46 -0700
+
+sg3-utils (1.17-2) unstable; urgency=low
+
+  * Add libtool to build-depends
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Tue,  4 Oct 2005 19:40:00 -0600
+
+sg3-utils (1.17-1) unstable; urgency=low
+
+  * New upstream version
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Sat,  1 Oct 2005 13:26:16 -0600
+
+sg3-utils (1.08-2) unstable; urgency=low
+
+  * Fix packaging bug that accidentally left off binaries.  Sigh.
+    (closes: #271906)
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Wed, 15 Sep 2004 22:40:06 -0600
+
+sg3-utils (1.08-1) unstable; urgency=low
+
+  * New upstream version
+  * Unified package description with list of tools actually installed
+    (closes: #271093)
+
+ -- Eric Schwartz (Skif) <emschwar@debian.org>  Sun, 12 Sep 2004 21:22:42 -0600
+
+sg3-utils (1.05-1) unstable; urgency=low
+
+  * New upstream release
+  * updated description to match tools in package (closes: #221143)
+
+ -- Eric Schwartz <emschwar@debian.org>  Tue, 18 Nov 2003 22:22:29 -0700
+
+sg3-utils (1.03-1) unstable; urgency=low
+
+  * New upstream release (closes: #181999)
+
+ -- Eric Schwartz <emschwar@debian.org>  Tue, 29 Apr 2003 20:18:30 -0600
+
+sg3-utils (0.95-4) unstable; urgency=low
+
+  * Only warns if installed on a kernel version < 2.4 (closes: #136434)
+      
+ -- Eric Schwartz <emschwar@debian.org>  Tue, 28 May 2002 22:55:29 -0600
+
+sg3-utils (0.95-3) unstable; urgency=low
+
+  * Extended description to include descriptions of all tools included in
+    the package. (closes: #121968)
+
+ -- Eric Schwartz <emschwar@debian.org>  Sun, 13 Jan 2002 17:09:27 -0700
+
+sg3-utils (0.95-2) unstable; urgency=low
+
+  * Packaging manpages (closes: #122692)
+  * Conflicts with cdwrite (closes: #123779)
+
+ -- Eric Schwartz <emschwar@debian.org>  Wed,  2 Jan 2002 01:05:08 -0700
+
+sg3-utils (0.95-1) unstable; urgency=low
+
+  * Initial Release.
+  * Adjusted Makefile to include $DESTDIR
+
+ -- Eric Schwartz <emschwar@debian.org>  Wed, 14 Nov 2001 17:05:56 -0700
+
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..f599e28
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+10
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..8364643
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,43 @@
+Source: sg3-utils
+Section: admin
+Priority: optional
+Maintainer: Eric Schwartz (Skif) <emschwar@debian.org>
+Build-Depends: debhelper (>> 7), libtool, libcam-dev [kfreebsd-i386 kfreebsd-amd64]
+Standards-Version: 3.8.2
+
+Package: sg3-utils
+Architecture: any
+Depends: ${shlibs:Depends}
+Conflicts: sg-utils, cdwrite
+Replaces: sg-utils
+Description: utilities for devices using the SCSI command set.
+ Most OSes have SCSI pass-through interfaces that enable user space programs
+ to send SCSI commands to a device and fetch the response. With SCSI to ATA
+ Translation (SAT) many ATA disks now can process SCSI commands. Typically
+ each utility in this package implements one SCSI command. See the draft
+ standards at www.t10.org for SCSI command definitions plus SAT. ATA
+ commands are defined in the draft standards at www.t13.org . For a mapping
+ between supported SCSI and ATA commands and utility names in this package
+ see the COVERAGE file. Also some support for NVMe devices, especially via
+ sg_ses to NVMe enclsoures.
+
+Package: libsgutils2-2
+Section: libs
+Depends: ${shlibs:Depends}
+Architecture: any
+Conflicts: libsgutils2
+Replaces: libsgutils2
+Suggests: sg3-utils
+Description: utilities for devices using the SCSI command set (shared libraries)
+ Shared library used by the utilities in the sg3-utils package.
+
+Package: libsgutils2-dev
+Section: libdevel
+Architecture: any
+Depends: libsgutils2-2 (= ${binary:Version}), ${shlibs:Depends}, ${kfreebsd:Depends}
+Conflicts: libsgutils1-dev
+Suggests: sg3-utils
+Description: utilities for devices using the SCSI command set (developer files)
+ Developer files (i.e. headers and a static library) which are associated with
+ the utilities in the sg3-utils package.
+
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..7f1906b
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,24 @@
+
+Copyright (c) 1999-2018, Douglas Gilbert
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/debian/docs b/debian/docs
new file mode 100644
index 0000000..67fe85a
--- /dev/null
+++ b/debian/docs
@@ -0,0 +1,7 @@
+README
+README.sg_start
+AUTHORS
+COVERAGE
+CREDITS
+INSTALL
+ChangeLog
diff --git a/debian/libsgutils2-2.install b/debian/libsgutils2-2.install
new file mode 100644
index 0000000..093956b
--- /dev/null
+++ b/debian/libsgutils2-2.install
@@ -0,0 +1 @@
+usr/lib/*.so.*
diff --git a/debian/libsgutils2-dev.install b/debian/libsgutils2-dev.install
new file mode 100644
index 0000000..5d09f30
--- /dev/null
+++ b/debian/libsgutils2-dev.install
@@ -0,0 +1,4 @@
+usr/include/scsi
+usr/lib/*.so
+usr/lib/*.la
+usr/lib/*.a
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..d3a2b0c
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,83 @@
+#!/usr/bin/make -f
+# Sample debian/rules that uses debhelper. 
+# GNU copyright 1997 by Joey Hess.
+#
+# This version is for a hypothetical package that builds an
+# architecture-dependant package, as well as an architecture-independent
+# package.
+
+# Uncomment this to turn on verbose mode. 
+# export DH_VERBOSE=1
+
+DEB_HOST_ARCH_OS := $(shell dpkg-architecture -qDEB_HOST_ARCH_OS 2>/dev/null)
+
+configure: configure-stamp
+configure-stamp:
+	dh_testdir
+	# Add here commands to configure the package.
+	CFLAGS="$(CFLAGS)" ./configure --host=$(DEB_HOST_GNU_TYPE) --build=$(DEB_BUILD_GNU_TYPE) --bindir=/usr/bin --prefix=/usr --mandir=\$${prefix}/share/man
+	touch configure-stamp
+
+build: configure-stamp build-stamp
+build-stamp:
+	dh_testdir
+
+	# Add here commands to compile the package.
+	PREFIX=/usr MANDIR=/usr/share/man $(MAKE) -e
+
+	touch build-stamp
+
+clean:
+	dh_testdir
+	dh_testroot
+
+	# Add here commands to clean up after the build process.
+	-$(MAKE) distclean
+
+	rm -f build-stamp configure-stamp debian/substvars
+
+	dh_clean
+
+install: DH_OPTIONS=
+install: build
+	dh_testdir
+	dh_testroot
+	dh_clean
+	dh_installdirs
+
+	# Add here commands to install the package into debian/tmp
+	$(MAKE) -e install DESTDIR=$(CURDIR)/debian/tmp PREFIX=/usr
+
+	dh_install --autodest --sourcedir=debian/tmp
+
+	dh_installman
+
+# Build architecture-independent files here.
+# Pass -i to all debhelper commands in this target to reduce clutter.
+binary-indep: build install
+# nothing to do here
+
+# Build architecture-dependent files here.
+binary-arch: build install
+	dh_testdir -a
+	dh_testroot -a
+	dh_installdocs -a
+	dh_installexamples -a 
+	dh_installmenu -a
+	dh_installchangelogs ChangeLog -a
+	dh_strip -a
+	dh_link -a
+	dh_compress -a -X archive -X .c -X .h
+	dh_fixperms -a
+	dh_makeshlibs -V -v
+	dh_installdeb -a
+ifeq ($(DEB_HOST_ARCH_OS),kfreebsd)
+	echo kfreebsd:Depends=libcam-dev >>debian/libsgutils2-dev.substvars
+endif
+	dh_shlibdeps -ldebian/tmp/usr/lib -L libsgutils2
+	dh_gencontrol -a
+	dh_md5sums -a
+	dh_builddeb -a
+
+binary: binary-indep binary-arch
+.PHONY: build clean binary-indep binary-arch binary install configure
diff --git a/debian/sg3-utils.examples b/debian/sg3-utils.examples
new file mode 100644
index 0000000..e39721e
--- /dev/null
+++ b/debian/sg3-utils.examples
@@ -0,0 +1 @@
+examples/*
diff --git a/debian/sg3-utils.install b/debian/sg3-utils.install
new file mode 100644
index 0000000..6161376
--- /dev/null
+++ b/debian/sg3-utils.install
@@ -0,0 +1,2 @@
+usr/bin/*
+usr/share/man/man8/*
diff --git a/depcomp b/depcomp
new file mode 100755
index 0000000..715e343
--- /dev/null
+++ b/depcomp
@@ -0,0 +1,791 @@
+#! /bin/sh
+# depcomp - compile a program generating dependencies as side-effects
+
+scriptversion=2018-03-07.03; # UTC
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# Originally written by Alexandre Oliva <oliva@dcc.unicamp.br>.
+
+case $1 in
+  '')
+    echo "$0: No command.  Try '$0 --help' for more information." 1>&2
+    exit 1;
+    ;;
+  -h | --h*)
+    cat <<\EOF
+Usage: depcomp [--help] [--version] PROGRAM [ARGS]
+
+Run PROGRAMS ARGS to compile a file, generating dependencies
+as side-effects.
+
+Environment variables:
+  depmode     Dependency tracking mode.
+  source      Source file read by 'PROGRAMS ARGS'.
+  object      Object file output by 'PROGRAMS ARGS'.
+  DEPDIR      directory where to store dependencies.
+  depfile     Dependency file to output.
+  tmpdepfile  Temporary file to use when outputting dependencies.
+  libtool     Whether libtool is used (yes/no).
+
+Report bugs to <bug-automake@gnu.org>.
+EOF
+    exit $?
+    ;;
+  -v | --v*)
+    echo "depcomp $scriptversion"
+    exit $?
+    ;;
+esac
+
+# Get the directory component of the given path, and save it in the
+# global variables '$dir'.  Note that this directory component will
+# be either empty or ending with a '/' character.  This is deliberate.
+set_dir_from ()
+{
+  case $1 in
+    */*) dir=`echo "$1" | sed -e 's|/[^/]*$|/|'`;;
+      *) dir=;;
+  esac
+}
+
+# Get the suffix-stripped basename of the given path, and save it the
+# global variable '$base'.
+set_base_from ()
+{
+  base=`echo "$1" | sed -e 's|^.*/||' -e 's/\.[^.]*$//'`
+}
+
+# If no dependency file was actually created by the compiler invocation,
+# we still have to create a dummy depfile, to avoid errors with the
+# Makefile "include basename.Plo" scheme.
+make_dummy_depfile ()
+{
+  echo "#dummy" > "$depfile"
+}
+
+# Factor out some common post-processing of the generated depfile.
+# Requires the auxiliary global variable '$tmpdepfile' to be set.
+aix_post_process_depfile ()
+{
+  # If the compiler actually managed to produce a dependency file,
+  # post-process it.
+  if test -f "$tmpdepfile"; then
+    # Each line is of the form 'foo.o: dependency.h'.
+    # Do two passes, one to just change these to
+    #   $object: dependency.h
+    # and one to simply output
+    #   dependency.h:
+    # which is needed to avoid the deleted-header problem.
+    { sed -e "s,^.*\.[$lower]*:,$object:," < "$tmpdepfile"
+      sed -e "s,^.*\.[$lower]*:[$tab ]*,," -e 's,$,:,' < "$tmpdepfile"
+    } > "$depfile"
+    rm -f "$tmpdepfile"
+  else
+    make_dummy_depfile
+  fi
+}
+
+# A tabulation character.
+tab='	'
+# A newline character.
+nl='
+'
+# Character ranges might be problematic outside the C locale.
+# These definitions help.
+upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ
+lower=abcdefghijklmnopqrstuvwxyz
+digits=0123456789
+alpha=${upper}${lower}
+
+if test -z "$depmode" || test -z "$source" || test -z "$object"; then
+  echo "depcomp: Variables source, object and depmode must be set" 1>&2
+  exit 1
+fi
+
+# Dependencies for sub/bar.o or sub/bar.obj go into sub/.deps/bar.Po.
+depfile=${depfile-`echo "$object" |
+  sed 's|[^\\/]*$|'${DEPDIR-.deps}'/&|;s|\.\([^.]*\)$|.P\1|;s|Pobj$|Po|'`}
+tmpdepfile=${tmpdepfile-`echo "$depfile" | sed 's/\.\([^.]*\)$/.T\1/'`}
+
+rm -f "$tmpdepfile"
+
+# Avoid interferences from the environment.
+gccflag= dashmflag=
+
+# Some modes work just like other modes, but use different flags.  We
+# parameterize here, but still list the modes in the big case below,
+# to make depend.m4 easier to write.  Note that we *cannot* use a case
+# here, because this file can only contain one case statement.
+if test "$depmode" = hp; then
+  # HP compiler uses -M and no extra arg.
+  gccflag=-M
+  depmode=gcc
+fi
+
+if test "$depmode" = dashXmstdout; then
+  # This is just like dashmstdout with a different argument.
+  dashmflag=-xM
+  depmode=dashmstdout
+fi
+
+cygpath_u="cygpath -u -f -"
+if test "$depmode" = msvcmsys; then
+  # This is just like msvisualcpp but w/o cygpath translation.
+  # Just convert the backslash-escaped backslashes to single forward
+  # slashes to satisfy depend.m4
+  cygpath_u='sed s,\\\\,/,g'
+  depmode=msvisualcpp
+fi
+
+if test "$depmode" = msvc7msys; then
+  # This is just like msvc7 but w/o cygpath translation.
+  # Just convert the backslash-escaped backslashes to single forward
+  # slashes to satisfy depend.m4
+  cygpath_u='sed s,\\\\,/,g'
+  depmode=msvc7
+fi
+
+if test "$depmode" = xlc; then
+  # IBM C/C++ Compilers xlc/xlC can output gcc-like dependency information.
+  gccflag=-qmakedep=gcc,-MF
+  depmode=gcc
+fi
+
+case "$depmode" in
+gcc3)
+## gcc 3 implements dependency tracking that does exactly what
+## we want.  Yay!  Note: for some reason libtool 1.4 doesn't like
+## it if -MD -MP comes after the -MF stuff.  Hmm.
+## Unfortunately, FreeBSD c89 acceptance of flags depends upon
+## the command line argument order; so add the flags where they
+## appear in depend2.am.  Note that the slowdown incurred here
+## affects only configure: in makefiles, %FASTDEP% shortcuts this.
+  for arg
+  do
+    case $arg in
+    -c) set fnord "$@" -MT "$object" -MD -MP -MF "$tmpdepfile" "$arg" ;;
+    *)  set fnord "$@" "$arg" ;;
+    esac
+    shift # fnord
+    shift # $arg
+  done
+  "$@"
+  stat=$?
+  if test $stat -ne 0; then
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  mv "$tmpdepfile" "$depfile"
+  ;;
+
+gcc)
+## Note that this doesn't just cater to obsosete pre-3.x GCC compilers.
+## but also to in-use compilers like IMB xlc/xlC and the HP C compiler.
+## (see the conditional assignment to $gccflag above).
+## There are various ways to get dependency output from gcc.  Here's
+## why we pick this rather obscure method:
+## - Don't want to use -MD because we'd like the dependencies to end
+##   up in a subdir.  Having to rename by hand is ugly.
+##   (We might end up doing this anyway to support other compilers.)
+## - The DEPENDENCIES_OUTPUT environment variable makes gcc act like
+##   -MM, not -M (despite what the docs say).  Also, it might not be
+##   supported by the other compilers which use the 'gcc' depmode.
+## - Using -M directly means running the compiler twice (even worse
+##   than renaming).
+  if test -z "$gccflag"; then
+    gccflag=-MD,
+  fi
+  "$@" -Wp,"$gccflag$tmpdepfile"
+  stat=$?
+  if test $stat -ne 0; then
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  # The second -e expression handles DOS-style file names with drive
+  # letters.
+  sed -e 's/^[^:]*: / /' \
+      -e 's/^['$alpha']:\/[^:]*: / /' < "$tmpdepfile" >> "$depfile"
+## This next piece of magic avoids the "deleted header file" problem.
+## The problem is that when a header file which appears in a .P file
+## is deleted, the dependency causes make to die (because there is
+## typically no way to rebuild the header).  We avoid this by adding
+## dummy dependencies for each header file.  Too bad gcc doesn't do
+## this for us directly.
+## Some versions of gcc put a space before the ':'.  On the theory
+## that the space means something, we add a space to the output as
+## well.  hp depmode also adds that space, but also prefixes the VPATH
+## to the object.  Take care to not repeat it in the output.
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly.  Breaking it into two sed invocations is a workaround.
+  tr ' ' "$nl" < "$tmpdepfile" \
+    | sed -e 's/^\\$//' -e '/^$/d' -e "s|.*$object$||" -e '/:$/d' \
+    | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+hp)
+  # This case exists only to let depend.m4 do its work.  It works by
+  # looking at the text of this script.  This case will never be run,
+  # since it is checked for above.
+  exit 1
+  ;;
+
+sgi)
+  if test "$libtool" = yes; then
+    "$@" "-Wp,-MDupdate,$tmpdepfile"
+  else
+    "$@" -MDupdate "$tmpdepfile"
+  fi
+  stat=$?
+  if test $stat -ne 0; then
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+
+  if test -f "$tmpdepfile"; then  # yes, the sourcefile depend on other files
+    echo "$object : \\" > "$depfile"
+    # Clip off the initial element (the dependent).  Don't try to be
+    # clever and replace this with sed code, as IRIX sed won't handle
+    # lines with more than a fixed number of characters (4096 in
+    # IRIX 6.2 sed, 8192 in IRIX 6.5).  We also remove comment lines;
+    # the IRIX cc adds comments like '#:fec' to the end of the
+    # dependency line.
+    tr ' ' "$nl" < "$tmpdepfile" \
+      | sed -e 's/^.*\.o://' -e 's/#.*$//' -e '/^$/ d' \
+      | tr "$nl" ' ' >> "$depfile"
+    echo >> "$depfile"
+    # The second pass generates a dummy entry for each header file.
+    tr ' ' "$nl" < "$tmpdepfile" \
+      | sed -e 's/^.*\.o://' -e 's/#.*$//' -e '/^$/ d' -e 's/$/:/' \
+      >> "$depfile"
+  else
+    make_dummy_depfile
+  fi
+  rm -f "$tmpdepfile"
+  ;;
+
+xlc)
+  # This case exists only to let depend.m4 do its work.  It works by
+  # looking at the text of this script.  This case will never be run,
+  # since it is checked for above.
+  exit 1
+  ;;
+
+aix)
+  # The C for AIX Compiler uses -M and outputs the dependencies
+  # in a .u file.  In older versions, this file always lives in the
+  # current directory.  Also, the AIX compiler puts '$object:' at the
+  # start of each line; $object doesn't have directory information.
+  # Version 6 uses the directory in both cases.
+  set_dir_from "$object"
+  set_base_from "$object"
+  if test "$libtool" = yes; then
+    tmpdepfile1=$dir$base.u
+    tmpdepfile2=$base.u
+    tmpdepfile3=$dir.libs/$base.u
+    "$@" -Wc,-M
+  else
+    tmpdepfile1=$dir$base.u
+    tmpdepfile2=$dir$base.u
+    tmpdepfile3=$dir$base.u
+    "$@" -M
+  fi
+  stat=$?
+  if test $stat -ne 0; then
+    rm -f "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3"
+    exit $stat
+  fi
+
+  for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3"
+  do
+    test -f "$tmpdepfile" && break
+  done
+  aix_post_process_depfile
+  ;;
+
+tcc)
+  # tcc (Tiny C Compiler) understand '-MD -MF file' since version 0.9.26
+  # FIXME: That version still under development at the moment of writing.
+  #        Make that this statement remains true also for stable, released
+  #        versions.
+  # It will wrap lines (doesn't matter whether long or short) with a
+  # trailing '\', as in:
+  #
+  #   foo.o : \
+  #    foo.c \
+  #    foo.h \
+  #
+  # It will put a trailing '\' even on the last line, and will use leading
+  # spaces rather than leading tabs (at least since its commit 0394caf7
+  # "Emit spaces for -MD").
+  "$@" -MD -MF "$tmpdepfile"
+  stat=$?
+  if test $stat -ne 0; then
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+  # Each non-empty line is of the form 'foo.o : \' or ' dep.h \'.
+  # We have to change lines of the first kind to '$object: \'.
+  sed -e "s|.*:|$object :|" < "$tmpdepfile" > "$depfile"
+  # And for each line of the second kind, we have to emit a 'dep.h:'
+  # dummy dependency, to avoid the deleted-header problem.
+  sed -n -e 's|^  *\(.*\) *\\$|\1:|p' < "$tmpdepfile" >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+## The order of this option in the case statement is important, since the
+## shell code in configure will try each of these formats in the order
+## listed in this file.  A plain '-MD' option would be understood by many
+## compilers, so we must ensure this comes after the gcc and icc options.
+pgcc)
+  # Portland's C compiler understands '-MD'.
+  # Will always output deps to 'file.d' where file is the root name of the
+  # source file under compilation, even if file resides in a subdirectory.
+  # The object file name does not affect the name of the '.d' file.
+  # pgcc 10.2 will output
+  #    foo.o: sub/foo.c sub/foo.h
+  # and will wrap long lines using '\' :
+  #    foo.o: sub/foo.c ... \
+  #     sub/foo.h ... \
+  #     ...
+  set_dir_from "$object"
+  # Use the source, not the object, to determine the base name, since
+  # that's sadly what pgcc will do too.
+  set_base_from "$source"
+  tmpdepfile=$base.d
+
+  # For projects that build the same source file twice into different object
+  # files, the pgcc approach of using the *source* file root name can cause
+  # problems in parallel builds.  Use a locking strategy to avoid stomping on
+  # the same $tmpdepfile.
+  lockdir=$base.d-lock
+  trap "
+    echo '$0: caught signal, cleaning up...' >&2
+    rmdir '$lockdir'
+    exit 1
+  " 1 2 13 15
+  numtries=100
+  i=$numtries
+  while test $i -gt 0; do
+    # mkdir is a portable test-and-set.
+    if mkdir "$lockdir" 2>/dev/null; then
+      # This process acquired the lock.
+      "$@" -MD
+      stat=$?
+      # Release the lock.
+      rmdir "$lockdir"
+      break
+    else
+      # If the lock is being held by a different process, wait
+      # until the winning process is done or we timeout.
+      while test -d "$lockdir" && test $i -gt 0; do
+        sleep 1
+        i=`expr $i - 1`
+      done
+    fi
+    i=`expr $i - 1`
+  done
+  trap - 1 2 13 15
+  if test $i -le 0; then
+    echo "$0: failed to acquire lock after $numtries attempts" >&2
+    echo "$0: check lockdir '$lockdir'" >&2
+    exit 1
+  fi
+
+  if test $stat -ne 0; then
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+  # Each line is of the form `foo.o: dependent.h',
+  # or `foo.o: dep1.h dep2.h \', or ` dep3.h dep4.h \'.
+  # Do two passes, one to just change these to
+  # `$object: dependent.h' and one to simply `dependent.h:'.
+  sed "s,^[^:]*:,$object :," < "$tmpdepfile" > "$depfile"
+  # Some versions of the HPUX 10.20 sed can't process this invocation
+  # correctly.  Breaking it into two sed invocations is a workaround.
+  sed 's,^[^:]*: \(.*\)$,\1,;s/^\\$//;/^$/d;/:$/d' < "$tmpdepfile" \
+    | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+hp2)
+  # The "hp" stanza above does not work with aCC (C++) and HP's ia64
+  # compilers, which have integrated preprocessors.  The correct option
+  # to use with these is +Maked; it writes dependencies to a file named
+  # 'foo.d', which lands next to the object file, wherever that
+  # happens to be.
+  # Much of this is similar to the tru64 case; see comments there.
+  set_dir_from  "$object"
+  set_base_from "$object"
+  if test "$libtool" = yes; then
+    tmpdepfile1=$dir$base.d
+    tmpdepfile2=$dir.libs/$base.d
+    "$@" -Wc,+Maked
+  else
+    tmpdepfile1=$dir$base.d
+    tmpdepfile2=$dir$base.d
+    "$@" +Maked
+  fi
+  stat=$?
+  if test $stat -ne 0; then
+     rm -f "$tmpdepfile1" "$tmpdepfile2"
+     exit $stat
+  fi
+
+  for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2"
+  do
+    test -f "$tmpdepfile" && break
+  done
+  if test -f "$tmpdepfile"; then
+    sed -e "s,^.*\.[$lower]*:,$object:," "$tmpdepfile" > "$depfile"
+    # Add 'dependent.h:' lines.
+    sed -ne '2,${
+               s/^ *//
+               s/ \\*$//
+               s/$/:/
+               p
+             }' "$tmpdepfile" >> "$depfile"
+  else
+    make_dummy_depfile
+  fi
+  rm -f "$tmpdepfile" "$tmpdepfile2"
+  ;;
+
+tru64)
+  # The Tru64 compiler uses -MD to generate dependencies as a side
+  # effect.  'cc -MD -o foo.o ...' puts the dependencies into 'foo.o.d'.
+  # At least on Alpha/Redhat 6.1, Compaq CCC V6.2-504 seems to put
+  # dependencies in 'foo.d' instead, so we check for that too.
+  # Subdirectories are respected.
+  set_dir_from  "$object"
+  set_base_from "$object"
+
+  if test "$libtool" = yes; then
+    # Libtool generates 2 separate objects for the 2 libraries.  These
+    # two compilations output dependencies in $dir.libs/$base.o.d and
+    # in $dir$base.o.d.  We have to check for both files, because
+    # one of the two compilations can be disabled.  We should prefer
+    # $dir$base.o.d over $dir.libs/$base.o.d because the latter is
+    # automatically cleaned when .libs/ is deleted, while ignoring
+    # the former would cause a distcleancheck panic.
+    tmpdepfile1=$dir$base.o.d          # libtool 1.5
+    tmpdepfile2=$dir.libs/$base.o.d    # Likewise.
+    tmpdepfile3=$dir.libs/$base.d      # Compaq CCC V6.2-504
+    "$@" -Wc,-MD
+  else
+    tmpdepfile1=$dir$base.d
+    tmpdepfile2=$dir$base.d
+    tmpdepfile3=$dir$base.d
+    "$@" -MD
+  fi
+
+  stat=$?
+  if test $stat -ne 0; then
+    rm -f "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3"
+    exit $stat
+  fi
+
+  for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3"
+  do
+    test -f "$tmpdepfile" && break
+  done
+  # Same post-processing that is required for AIX mode.
+  aix_post_process_depfile
+  ;;
+
+msvc7)
+  if test "$libtool" = yes; then
+    showIncludes=-Wc,-showIncludes
+  else
+    showIncludes=-showIncludes
+  fi
+  "$@" $showIncludes > "$tmpdepfile"
+  stat=$?
+  grep -v '^Note: including file: ' "$tmpdepfile"
+  if test $stat -ne 0; then
+    rm -f "$tmpdepfile"
+    exit $stat
+  fi
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  # The first sed program below extracts the file names and escapes
+  # backslashes for cygpath.  The second sed program outputs the file
+  # name when reading, but also accumulates all include files in the
+  # hold buffer in order to output them again at the end.  This only
+  # works with sed implementations that can handle large buffers.
+  sed < "$tmpdepfile" -n '
+/^Note: including file:  *\(.*\)/ {
+  s//\1/
+  s/\\/\\\\/g
+  p
+}' | $cygpath_u | sort -u | sed -n '
+s/ /\\ /g
+s/\(.*\)/'"$tab"'\1 \\/p
+s/.\(.*\) \\/\1:/
+H
+$ {
+  s/.*/'"$tab"'/
+  G
+  p
+}' >> "$depfile"
+  echo >> "$depfile" # make sure the fragment doesn't end with a backslash
+  rm -f "$tmpdepfile"
+  ;;
+
+msvc7msys)
+  # This case exists only to let depend.m4 do its work.  It works by
+  # looking at the text of this script.  This case will never be run,
+  # since it is checked for above.
+  exit 1
+  ;;
+
+#nosideeffect)
+  # This comment above is used by automake to tell side-effect
+  # dependency tracking mechanisms from slower ones.
+
+dashmstdout)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the preprocessed file to stdout, regardless of -o.
+  "$@" || exit $?
+
+  # Remove the call to Libtool.
+  if test "$libtool" = yes; then
+    while test "X$1" != 'X--mode=compile'; do
+      shift
+    done
+    shift
+  fi
+
+  # Remove '-o $object'.
+  IFS=" "
+  for arg
+  do
+    case $arg in
+    -o)
+      shift
+      ;;
+    $object)
+      shift
+      ;;
+    *)
+      set fnord "$@" "$arg"
+      shift # fnord
+      shift # $arg
+      ;;
+    esac
+  done
+
+  test -z "$dashmflag" && dashmflag=-M
+  # Require at least two characters before searching for ':'
+  # in the target name.  This is to cope with DOS-style filenames:
+  # a dependency such as 'c:/foo/bar' could be seen as target 'c' otherwise.
+  "$@" $dashmflag |
+    sed "s|^[$tab ]*[^:$tab ][^:][^:]*:[$tab ]*|$object: |" > "$tmpdepfile"
+  rm -f "$depfile"
+  cat < "$tmpdepfile" > "$depfile"
+  # Some versions of the HPUX 10.20 sed can't process this sed invocation
+  # correctly.  Breaking it into two sed invocations is a workaround.
+  tr ' ' "$nl" < "$tmpdepfile" \
+    | sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' \
+    | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+dashXmstdout)
+  # This case only exists to satisfy depend.m4.  It is never actually
+  # run, as this mode is specially recognized in the preamble.
+  exit 1
+  ;;
+
+makedepend)
+  "$@" || exit $?
+  # Remove any Libtool call
+  if test "$libtool" = yes; then
+    while test "X$1" != 'X--mode=compile'; do
+      shift
+    done
+    shift
+  fi
+  # X makedepend
+  shift
+  cleared=no eat=no
+  for arg
+  do
+    case $cleared in
+    no)
+      set ""; shift
+      cleared=yes ;;
+    esac
+    if test $eat = yes; then
+      eat=no
+      continue
+    fi
+    case "$arg" in
+    -D*|-I*)
+      set fnord "$@" "$arg"; shift ;;
+    # Strip any option that makedepend may not understand.  Remove
+    # the object too, otherwise makedepend will parse it as a source file.
+    -arch)
+      eat=yes ;;
+    -*|$object)
+      ;;
+    *)
+      set fnord "$@" "$arg"; shift ;;
+    esac
+  done
+  obj_suffix=`echo "$object" | sed 's/^.*\././'`
+  touch "$tmpdepfile"
+  ${MAKEDEPEND-makedepend} -o"$obj_suffix" -f"$tmpdepfile" "$@"
+  rm -f "$depfile"
+  # makedepend may prepend the VPATH from the source file name to the object.
+  # No need to regex-escape $object, excess matching of '.' is harmless.
+  sed "s|^.*\($object *:\)|\1|" "$tmpdepfile" > "$depfile"
+  # Some versions of the HPUX 10.20 sed can't process the last invocation
+  # correctly.  Breaking it into two sed invocations is a workaround.
+  sed '1,2d' "$tmpdepfile" \
+    | tr ' ' "$nl" \
+    | sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' \
+    | sed -e 's/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile" "$tmpdepfile".bak
+  ;;
+
+cpp)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the preprocessed file to stdout.
+  "$@" || exit $?
+
+  # Remove the call to Libtool.
+  if test "$libtool" = yes; then
+    while test "X$1" != 'X--mode=compile'; do
+      shift
+    done
+    shift
+  fi
+
+  # Remove '-o $object'.
+  IFS=" "
+  for arg
+  do
+    case $arg in
+    -o)
+      shift
+      ;;
+    $object)
+      shift
+      ;;
+    *)
+      set fnord "$@" "$arg"
+      shift # fnord
+      shift # $arg
+      ;;
+    esac
+  done
+
+  "$@" -E \
+    | sed -n -e '/^# [0-9][0-9]* "\([^"]*\)".*/ s:: \1 \\:p' \
+             -e '/^#line [0-9][0-9]* "\([^"]*\)".*/ s:: \1 \\:p' \
+    | sed '$ s: \\$::' > "$tmpdepfile"
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  cat < "$tmpdepfile" >> "$depfile"
+  sed < "$tmpdepfile" '/^$/d;s/^ //;s/ \\$//;s/$/ :/' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+msvisualcpp)
+  # Important note: in order to support this mode, a compiler *must*
+  # always write the preprocessed file to stdout.
+  "$@" || exit $?
+
+  # Remove the call to Libtool.
+  if test "$libtool" = yes; then
+    while test "X$1" != 'X--mode=compile'; do
+      shift
+    done
+    shift
+  fi
+
+  IFS=" "
+  for arg
+  do
+    case "$arg" in
+    -o)
+      shift
+      ;;
+    $object)
+      shift
+      ;;
+    "-Gm"|"/Gm"|"-Gi"|"/Gi"|"-ZI"|"/ZI")
+        set fnord "$@"
+        shift
+        shift
+        ;;
+    *)
+        set fnord "$@" "$arg"
+        shift
+        shift
+        ;;
+    esac
+  done
+  "$@" -E 2>/dev/null |
+  sed -n '/^#line [0-9][0-9]* "\([^"]*\)"/ s::\1:p' | $cygpath_u | sort -u > "$tmpdepfile"
+  rm -f "$depfile"
+  echo "$object : \\" > "$depfile"
+  sed < "$tmpdepfile" -n -e 's% %\\ %g' -e '/^\(.*\)$/ s::'"$tab"'\1 \\:p' >> "$depfile"
+  echo "$tab" >> "$depfile"
+  sed < "$tmpdepfile" -n -e 's% %\\ %g' -e '/^\(.*\)$/ s::\1\::p' >> "$depfile"
+  rm -f "$tmpdepfile"
+  ;;
+
+msvcmsys)
+  # This case exists only to let depend.m4 do its work.  It works by
+  # looking at the text of this script.  This case will never be run,
+  # since it is checked for above.
+  exit 1
+  ;;
+
+none)
+  exec "$@"
+  ;;
+
+*)
+  echo "Unknown depmode $depmode" 1>&2
+  exit 1
+  ;;
+esac
+
+exit 0
+
+# Local Variables:
+# mode: shell-script
+# sh-indentation: 2
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/doc/Makefile.am b/doc/Makefile.am
new file mode 100644
index 0000000..2d5c52e
--- /dev/null
+++ b/doc/Makefile.am
@@ -0,0 +1,47 @@
+
+dist_man_MANS = \
+	scsi_mandat.8 scsi_readcap.8 scsi_ready.8 scsi_satl.8 scsi_start.8 \
+	scsi_stop.8 scsi_temperature.8 sg3_utils.8 sg3_utils_json.8 \
+	sg_bg_ctl.8 sg_compare_and_write.8 sg_decode_sense.8 sg_format.8 \
+	sg_get_config.8 sg_get_elem_status.8 sg_get_lba_status.8 sg_ident.8 \
+	sg_inq.8 sg_logs.8 sg_luns.8 sg_modes.8 sg_opcodes.8 sg_persist.8 \
+	sg_prevent.8 sg_raw.8 sg_rdac.8 sg_read_attr.8 \
+	sg_read_block_limits.8 sg_read_buffer.8 sg_read_long.8 sg_readcap.8 \
+	sg_reassign.8 sg_referrals.8 sg_rem_rest_elem.8 sg_rep_density.8 \
+	sg_rep_pip.8 sg_rep_zones.8 sg_requests.8 sg_reset_wp.8 sg_rmsn.8 \
+	sg_rtpg.8 sg_safte.8 sg_sanitize.8 sg_sat_identify.8 \
+	sg_sat_phy_event.8 sg_sat_read_gplog.8 sg_sat_set_features.8 \
+	sg_seek.8 sg_senddiag.8 sg_ses.8 sg_ses_microcode.8 sg_start.8 \
+	sg_stpg.8 sg_stream_ctl.8 sg_sync.8 sg_timestamp.8 sg_turs.8 \
+	sg_unmap.8 sg_verify.8 sg_vpd.8 sg_wr_mode.8 sg_write_buffer.8 \
+	sg_write_long.8 sg_write_same.8 sg_write_verify.8 sg_write_x.8 \
+	sg_zone.8 sg_z_act_query.8
+CLEANFILES =
+
+if OS_LINUX
+dist_man_MANS += \
+	rescan-scsi-bus.sh.8 scsi_logging_level.8 sg_copy_results.8 sg_dd.8 \
+	sg_emc_trespass.8 sg_map.8 sg_map26.8 sg_rbuf.8 sg_read.8 sg_reset.8 \
+	sg_scan.8 sg_test_rwbuf.8 sg_xcopy.8 sginfo.8 sgm_dd.8 sgp_dd.8
+CLEANFILES += sg_scan.8
+sg_scan.8: sg_scan.8.linux
+	cp -p $< $@
+endif
+
+if OS_WIN32_MINGW
+dist_man_MANS += sg_scan.8
+CLEANFILES += sg_scan.8
+sg_scan.8: sg_scan.8.win32
+	cp -p $< $@
+endif
+
+if OS_WIN32_CYGWIN
+dist_man_MANS += sg_scan.8
+CLEANFILES += sg_scan.8
+sg_scan.8: sg_scan.8.win32
+	cp -p $< $@
+endif
+
+EXTRA_DIST = \
+	sg_scan.8.linux \
+	sg_scan.8.win32
diff --git a/doc/Makefile.in b/doc/Makefile.in
new file mode 100644
index 0000000..a75d0fd
--- /dev/null
+++ b/doc/Makefile.in
@@ -0,0 +1,565 @@
+# Makefile.in generated by automake 1.16.5 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2021 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+VPATH = @srcdir@
+am__is_gnu_make = { \
+  if test -z '$(MAKELEVEL)'; then \
+    false; \
+  elif test -n '$(MAKE_HOST)'; then \
+    true; \
+  elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+    true; \
+  else \
+    false; \
+  fi; \
+}
+am__make_running_with_option = \
+  case $${target_option-} in \
+      ?) ;; \
+      *) echo "am__make_running_with_option: internal error: invalid" \
+              "target option '$${target_option-}' specified" >&2; \
+         exit 1;; \
+  esac; \
+  has_opt=no; \
+  sane_makeflags=$$MAKEFLAGS; \
+  if $(am__is_gnu_make); then \
+    sane_makeflags=$$MFLAGS; \
+  else \
+    case $$MAKEFLAGS in \
+      *\\[\ \	]*) \
+        bs=\\; \
+        sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+          | sed "s/$$bs$$bs[$$bs $$bs	]*//g"`;; \
+    esac; \
+  fi; \
+  skip_next=no; \
+  strip_trailopt () \
+  { \
+    flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+  }; \
+  for flg in $$sane_makeflags; do \
+    test $$skip_next = yes && { skip_next=no; continue; }; \
+    case $$flg in \
+      *=*|--*) continue;; \
+        -*I) strip_trailopt 'I'; skip_next=yes;; \
+      -*I?*) strip_trailopt 'I';; \
+        -*O) strip_trailopt 'O'; skip_next=yes;; \
+      -*O?*) strip_trailopt 'O';; \
+        -*l) strip_trailopt 'l'; skip_next=yes;; \
+      -*l?*) strip_trailopt 'l';; \
+      -[dEDm]) skip_next=yes;; \
+      -[JT]) skip_next=yes;; \
+    esac; \
+    case $$flg in \
+      *$$target_option*) has_opt=yes; break;; \
+    esac; \
+  done; \
+  test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@OS_LINUX_TRUE@am__append_1 = \
+@OS_LINUX_TRUE@	rescan-scsi-bus.sh.8 scsi_logging_level.8 sg_copy_results.8 sg_dd.8 \
+@OS_LINUX_TRUE@	sg_emc_trespass.8 sg_map.8 sg_map26.8 sg_rbuf.8 sg_read.8 sg_reset.8 \
+@OS_LINUX_TRUE@	sg_scan.8 sg_test_rwbuf.8 sg_xcopy.8 sginfo.8 sgm_dd.8 sgp_dd.8
+
+@OS_LINUX_TRUE@am__append_2 = sg_scan.8
+@OS_WIN32_MINGW_TRUE@am__append_3 = sg_scan.8
+@OS_WIN32_MINGW_TRUE@am__append_4 = sg_scan.8
+@OS_WIN32_CYGWIN_TRUE@am__append_5 = sg_scan.8
+@OS_WIN32_CYGWIN_TRUE@am__append_6 = sg_scan.8
+subdir = doc
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+	$(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo "  GEN     " $@;
+am__v_GEN_1 = 
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 = 
+SOURCES =
+DIST_SOURCES =
+am__can_run_installinfo = \
+  case $$AM_UPDATE_INFO_DIR in \
+    n|no|NO) false;; \
+    *) (install-info --version) >/dev/null 2>&1;; \
+  esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+    $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+    *) f=$$p;; \
+  esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+  srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+  for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+  for p in $$list; do echo "$$p $$p"; done | \
+  sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+  $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+    if (++n[$$2] == $(am__install_max)) \
+      { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+    END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+  sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+  sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+  test -z "$$files" \
+    || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+    || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+         $(am__cd) "$$dir" && rm -f $$files; }; \
+  }
+man8dir = $(mandir)/man8
+am__installdirs = "$(DESTDIR)$(man8dir)"
+NROFF = nroff
+MANS = $(dist_man_MANS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+am__DIST_COMMON = $(dist_man_MANS) $(srcdir)/Makefile.in README
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CSCOPE = @CSCOPE@
+CTAGS = @CTAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+ETAGS = @ETAGS@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FILECMD = @FILECMD@
+GETOPT_O_FILES = @GETOPT_O_FILES@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PTHREAD_LIB = @PTHREAD_LIB@
+RANLIB = @RANLIB@
+RT_LIB = @RT_LIB@
+SED = @SED@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+STRIP = @STRIP@
+VERSION = @VERSION@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+os_cflags = @os_cflags@
+os_libs = @os_libs@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+dist_man_MANS = scsi_mandat.8 scsi_readcap.8 scsi_ready.8 scsi_satl.8 \
+	scsi_start.8 scsi_stop.8 scsi_temperature.8 sg3_utils.8 \
+	sg3_utils_json.8 sg_bg_ctl.8 sg_compare_and_write.8 \
+	sg_decode_sense.8 sg_format.8 sg_get_config.8 \
+	sg_get_elem_status.8 sg_get_lba_status.8 sg_ident.8 sg_inq.8 \
+	sg_logs.8 sg_luns.8 sg_modes.8 sg_opcodes.8 sg_persist.8 \
+	sg_prevent.8 sg_raw.8 sg_rdac.8 sg_read_attr.8 \
+	sg_read_block_limits.8 sg_read_buffer.8 sg_read_long.8 \
+	sg_readcap.8 sg_reassign.8 sg_referrals.8 sg_rem_rest_elem.8 \
+	sg_rep_density.8 sg_rep_pip.8 sg_rep_zones.8 sg_requests.8 \
+	sg_reset_wp.8 sg_rmsn.8 sg_rtpg.8 sg_safte.8 sg_sanitize.8 \
+	sg_sat_identify.8 sg_sat_phy_event.8 sg_sat_read_gplog.8 \
+	sg_sat_set_features.8 sg_seek.8 sg_senddiag.8 sg_ses.8 \
+	sg_ses_microcode.8 sg_start.8 sg_stpg.8 sg_stream_ctl.8 \
+	sg_sync.8 sg_timestamp.8 sg_turs.8 sg_unmap.8 sg_verify.8 \
+	sg_vpd.8 sg_wr_mode.8 sg_write_buffer.8 sg_write_long.8 \
+	sg_write_same.8 sg_write_verify.8 sg_write_x.8 sg_zone.8 \
+	sg_z_act_query.8 $(am__append_1) $(am__append_3) \
+	$(am__append_5)
+CLEANFILES = $(am__append_2) $(am__append_4) $(am__append_6)
+EXTRA_DIST = \
+	sg_scan.8.linux \
+	sg_scan.8.win32
+
+all: all-am
+
+.SUFFIXES:
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am  $(am__configure_deps)
+	@for dep in $?; do \
+	  case '$(am__configure_deps)' in \
+	    *$$dep*) \
+	      ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+	        && { if test -f $@; then exit 0; else break; fi; }; \
+	      exit 1;; \
+	  esac; \
+	done; \
+	echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign doc/Makefile'; \
+	$(am__cd) $(top_srcdir) && \
+	  $(AUTOMAKE) --foreign doc/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+	@case '$?' in \
+	  *config.status*) \
+	    cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+	  *) \
+	    echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+	    cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+	esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+mostlyclean-libtool:
+	-rm -f *.lo
+
+clean-libtool:
+	-rm -rf .libs _libs
+install-man8: $(dist_man_MANS)
+	@$(NORMAL_INSTALL)
+	@list1=''; \
+	list2='$(dist_man_MANS)'; \
+	test -n "$(man8dir)" \
+	  && test -n "`echo $$list1$$list2`" \
+	  || exit 0; \
+	echo " $(MKDIR_P) '$(DESTDIR)$(man8dir)'"; \
+	$(MKDIR_P) "$(DESTDIR)$(man8dir)" || exit 1; \
+	{ for i in $$list1; do echo "$$i"; done;  \
+	if test -n "$$list2"; then \
+	  for i in $$list2; do echo "$$i"; done \
+	    | sed -n '/\.8[a-z]*$$/p'; \
+	fi; \
+	} | while read p; do \
+	  if test -f $$p; then d=; else d="$(srcdir)/"; fi; \
+	  echo "$$d$$p"; echo "$$p"; \
+	done | \
+	sed -e 'n;s,.*/,,;p;h;s,.*\.,,;s,^[^8][0-9a-z]*$$,8,;x' \
+	      -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,' | \
+	sed 'N;N;s,\n, ,g' | { \
+	list=; while read file base inst; do \
+	  if test "$$base" = "$$inst"; then list="$$list $$file"; else \
+	    echo " $(INSTALL_DATA) '$$file' '$(DESTDIR)$(man8dir)/$$inst'"; \
+	    $(INSTALL_DATA) "$$file" "$(DESTDIR)$(man8dir)/$$inst" || exit $$?; \
+	  fi; \
+	done; \
+	for i in $$list; do echo "$$i"; done | $(am__base_list) | \
+	while read files; do \
+	  test -z "$$files" || { \
+	    echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(man8dir)'"; \
+	    $(INSTALL_DATA) $$files "$(DESTDIR)$(man8dir)" || exit $$?; }; \
+	done; }
+
+uninstall-man8:
+	@$(NORMAL_UNINSTALL)
+	@list=''; test -n "$(man8dir)" || exit 0; \
+	files=`{ for i in $$list; do echo "$$i"; done; \
+	l2='$(dist_man_MANS)'; for i in $$l2; do echo "$$i"; done | \
+	  sed -n '/\.8[a-z]*$$/p'; \
+	} | sed -e 's,.*/,,;h;s,.*\.,,;s,^[^8][0-9a-z]*$$,8,;x' \
+	      -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,'`; \
+	dir='$(DESTDIR)$(man8dir)'; $(am__uninstall_files_from_dir)
+tags TAGS:
+
+ctags CTAGS:
+
+cscope cscopelist:
+
+distdir: $(BUILT_SOURCES)
+	$(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+	@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	list='$(DISTFILES)'; \
+	  dist_files=`for file in $$list; do echo $$file; done | \
+	  sed -e "s|^$$srcdirstrip/||;t" \
+	      -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+	case $$dist_files in \
+	  */*) $(MKDIR_P) `echo "$$dist_files" | \
+			   sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+			   sort -u` ;; \
+	esac; \
+	for file in $$dist_files; do \
+	  if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+	  if test -d $$d/$$file; then \
+	    dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+	    if test -d "$(distdir)/$$file"; then \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+	      cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+	  else \
+	    test -f "$(distdir)/$$file" \
+	    || cp -p $$d/$$file "$(distdir)/$$file" \
+	    || exit 1; \
+	  fi; \
+	done
+check-am: all-am
+check: check-am
+all-am: Makefile $(MANS)
+installdirs:
+	for dir in "$(DESTDIR)$(man8dir)"; do \
+	  test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+	done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+	@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+	if test -z '$(STRIP)'; then \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	      install; \
+	else \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	    "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+	fi
+mostlyclean-generic:
+
+clean-generic:
+	-test -z "$(CLEANFILES)" || rm -f $(CLEANFILES)
+
+distclean-generic:
+	-test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+	-test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+	@echo "This command is intended for maintainers to use"
+	@echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool mostlyclean-am
+
+distclean: distclean-am
+	-rm -f Makefile
+distclean-am: clean-am distclean-generic
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-man
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man: install-man8
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+	-rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-generic mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-man
+
+uninstall-man: uninstall-man8
+
+.MAKE: install-am install-strip
+
+.PHONY: all all-am check check-am clean clean-generic clean-libtool \
+	cscopelist-am ctags-am distclean distclean-generic \
+	distclean-libtool distdir dvi dvi-am html html-am info info-am \
+	install install-am install-data install-data-am install-dvi \
+	install-dvi-am install-exec install-exec-am install-html \
+	install-html-am install-info install-info-am install-man \
+	install-man8 install-pdf install-pdf-am install-ps \
+	install-ps-am install-strip installcheck installcheck-am \
+	installdirs maintainer-clean maintainer-clean-generic \
+	mostlyclean mostlyclean-generic mostlyclean-libtool pdf pdf-am \
+	ps ps-am tags-am uninstall uninstall-am uninstall-man \
+	uninstall-man8
+
+.PRECIOUS: Makefile
+
+@OS_LINUX_TRUE@sg_scan.8: sg_scan.8.linux
+@OS_LINUX_TRUE@	cp -p $< $@
+@OS_WIN32_MINGW_TRUE@sg_scan.8: sg_scan.8.win32
+@OS_WIN32_MINGW_TRUE@	cp -p $< $@
+@OS_WIN32_CYGWIN_TRUE@sg_scan.8: sg_scan.8.win32
+@OS_WIN32_CYGWIN_TRUE@	cp -p $< $@
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/doc/README b/doc/README
new file mode 100644
index 0000000..6664a32
--- /dev/null
+++ b/doc/README
@@ -0,0 +1,36 @@
+Various html files used to be included in this directory but they were
+beginning to bloat the size of the source tarball so they have been
+removed in version 1.24 .
+
+Here are the urls of the files that were previously here plus a brief
+summary:
+
+https://sg.danny.cz/sg/sg3_utils.html
+    - overview and examples of this package
+
+https://sg.danny.cz/sg/sg_dd.html
+    - discussion and examples of the sg_dd utility which is a dd variant
+      (also discusses the sgm_dd, sgp_dd and sg_read utilities)
+
+https://sg.danny.cz/sg/sg_ses.html
+    - discussion and examples of the sg_ses utility. SCSI Enclosure
+      Services (SES) devices may contain a lot of information,
+      structured in a non-trivial way.
+
+https://sg.danny.cz/sg/sg_io.html
+    - discussion of Linux SG_IO ioctl (SCSI pass-through)
+
+https://sg.danny.cz/sg/tools.html
+    - overview of around 25 storage and SCSI tools
+
+Many of those pages are also found at: https://doug-gilbert.github.io/
+
+
+There are two versions of sg_scan: one for Linux and the other for Windows.
+They have different man pages: sg_scan.8.linux and sg_scan.8.win32 with
+the Makefile logic (rule in Makefile.am) copying the appropriate one to
+sg_scan.8 . sg_scan is not supported for other ports.
+
+
+Douglas Gilbert
+10th June 2022
diff --git a/doc/rescan-scsi-bus.sh.8 b/doc/rescan-scsi-bus.sh.8
new file mode 100644
index 0000000..51aa00a
--- /dev/null
+++ b/doc/rescan-scsi-bus.sh.8
@@ -0,0 +1,135 @@
+.TH RESCAN\-SCSI\-BUS.SH "1" "September 2022" "rescan\-scsi\-bus.sh" "User Commands"
+.SH NAME
+rescan-scsi-bus.sh \- script to add and remove SCSI devices without rebooting
+.SH SYNOPSIS
+.B rescan\-scsi\-bus.sh
+[\fI\-\-alltargets\fR] [\fI\-\-attachpq3\fR] [\fI\-c\fR]
+[\fI\-\--channels=CLIST\fR] [\fI\-\-color\fR] [\fI\-d\fR] [\fI\-\-flush\fR]
+[\fI\-f\fR] [\fI\-\-forceremove\fR] [\fI\-\-forcerescan\fR] [\fI\-\-help\fR]
+[\fI\-\-hosts=HLIST\fR] [\fI\-\-ids=TLIST\fR] [\fI\-\-ignore\-rev\fR]
+[\fI\-\-issue\-lip\fR] [\fI\-i\fR] [\fI\-\-issue\-lip\-wait=SECS\fR]
+[\fI\-I SECS\fR] [\fI\-l\fR] [\fI\-L NUM\fR] [\fI\-\-largelun\fR]
+[\fI\-\-luns=LLIST\fR] [\fI\-m\fR] [\fI\-\-multipath\fR]
+[\fI\-\-no\-lip\-scan\fR] [\fI\-\-nooptscan\fR] [\fI\-\-nosync\fR]
+[\fI\-\-remove\fR] [\fI\-\-removelun2\fR] [\fI\-\-resize\fR]
+[\fI\-\-sparselun\fR] [\fI\-\-sync\fR] [\fI\-\-timeout=SECS\fR]
+[\fI\-\-update\fR] [\fI\-\-version\fR] [\fI\-\-wide\fR]
+[\fIHOST1 \fR[\fIHOST2 \fR...]]
+.SH OPTIONS
+Option are ordered by their long name. Those without a long name are ordered
+as if their single letter was a long name.
+.TP
+\fB\-a\fR, \fB\-\-alltargets\fR
+scan all targets, not just currently existing [default: disabled]
+.TP
+\fB\-\-attachpq3\fR
+tell kernel to attach sg to LUN 0 that reports PQ=3
+.TP
+\fB\-c\fR
+enables scanning of channels 0 1   [default: 0 / all detected ones]
+.TP
+\fB\-\-channels\fR=\fICLIST\fR
+scan only channel(s) in \fICLIST\fR
+.TP
+\fB\-\-color\fR
+use coloured prefixes OLD/NEW/DEL
+.TP
+\fB\-d\fR
+enable debug                       [default: 0]
+.TP
+\fB\-f\fR, \fB\-\-flush\fR
+flush failed multipath devices     [default: disabled]
+.TP
+\fB\-\-forceremove\fR
+remove stale devices (DANGEROUS)
+.TP
+\fB\-\-forcerescan\fR
+remove and re\-add existing devices (DANGEROUS)
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print usage message then exit
+.TP
+\fB\-\-hosts\fR=\fIHLIST\fR
+scan only host(s) in \fIHLIST\fR
+.TP
+\fB\-\-ids\fR=\fITLIST\fR
+scan only target ID(s) in \fITLIST\fR
+.TP
+\fB\-\-ignore\-rev\fR
+ignore (firmware) revision change. This is the third text field (4 bytes
+long) in a standard INQUIRY response.
+.TP
+\fB\-i\fR, \fB\-\-issue\-lip\fR
+issue a FibreChannel LIP reset     [default: disabled]
+.TP
+\fB\-I SECS\fR, \fB\-\-issue\-lip\-wait=SECS\fR
+issue a FibreChannel LIP reset and then wait SECS seconds.
+.TP
+\fB\-L\fR NUM
+activates scanning for LUNs 0\-\-NUM [default: 0]
+.TP
+\fB\-l\fR
+activates scanning for LUNs 0\-\-7   [default: 0]
+.TP
+\fB\-\-largelun\fR
+tell kernel to support LUNs > 7 even on SCSI2 devs
+.TP
+\fB\-\-luns\fR=\fINLIST\fR
+scan only lun(s) in \fINLIST\fR
+.TP
+\fB\-m\fR, \fB\-\-multipath\fR
+update multipath devices           [default: disabled]
+.TP
+\fB\-\-no\-lip\-scan\fR
+don't scan FC Host when the \fI\-\-issue\-lip\fR option is also given.
+.TP
+\fB\-\-nooptscan\fR
+don't stop looking for LUNs is 0 is not found
+.TP
+\fB\-\-nosync\fR
+do not issue a sync [default: sync if remove]
+.TP
+\fB\-r\fR, \fB\-\-remove\fR
+enables removing of devices        [default: disabled]
+.TP
+\fB\-\-reportlun2\fR
+tell kernel to try REPORT_LUN even on SCSI2 devices
+.TP
+\fB\-s\fR, \fB\-\-resize\fR
+look for resized disks and reload associated multipath devices, if applicable
+.TP
+\fB\-\-sparselun\fR
+tell kernel to support sparse LUN numbering
+.TP
+\fB\-\-sync\fR
+issue a sync [default: sync if remove]
+.TP
+\fB\-t\fR, \fB\-\-timeout=SECS\fR
+timeout for testing if device is online. Test is skipped if 0 [default:
+30 (seconds)]
+.TP
+\fB\-u\fR, \fB\-\-update\fR
+look for existing disks that have been remapped
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+shows version string then exits. The version string is a numeric datestamp
+of the form YYYYMMDD.
+.TP
+\fB\-w\fR, \fB\-\-wide\fR
+scan for target device IDs 0\-\-15   [default: 0\-\-7]
+.IP
+Host numbers may thus be specified either directly on cmd line (deprecated)
+or with the \fB\-\-hosts\fR=\fILIST\fR parameter (recommended).
+.PP
+Arguments to options that end in \fILIST\fR (e.g. \fITLIST\fR) can have this
+form:
+.br
+    A[\-B][,C[\-D]]...
+.br
+which is a comma separated list of single values and/or ranges (no spaces
+allowed).
+.SH SEE ALSO
+There is a brief descripion here:
+https://fibrevillage.com/storage/585-rescan-scsi-bus-sh-script-for-adding-and-removing-scsi-devices-without-rebooting
+.PP
+\fBsg3_utils\fR Homepage: \fBhttps://sg.danny.cz/sg\fR
diff --git a/doc/scsi_logging_level.8 b/doc/scsi_logging_level.8
new file mode 100644
index 0000000..57a5cf4
--- /dev/null
+++ b/doc/scsi_logging_level.8
@@ -0,0 +1,128 @@
+.TH SCSI_LOGGING_LEVEL "8" "April 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+scsi_logging_level \- access Linux SCSI logging level information
+.SH SYNOPSIS
+.B scsi_logging_level
+[\fI\-\-all=LEV\fR] [\fI\-\-create\fR] [\fI\-\-error=LEV\fR] [\fI\-\-get\fR]
+[\fI\-\-help\fR] [\fI\-\-highlevel=LEV\fR] [\fI\-\-hlcomplete=LEV\fR]
+[\fI\-\-hlqueue=LEV\fR] [\fI\-\-ioctl=LEV\fR] [\fI\-\-llcomplete=LEV\fR]
+[\fI\-\-llqueue=LEV\fR] [\fI\-\-lowlevel=LEV\fR] [\fI\-\-midlevel=LEV\fR]
+[\fI\-\-mlcomplete=LEV\fR] [\fI\-\-mlqueue=LEV\fR] [\fI\-\-scan=LEV\fR]
+[\fI\-\-set\fR] [\fI\-\-timeout=LEV\fR] [\fI\-\-version\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This bash shell script accesses the Linux SCSI subsystem logging
+level. The current values can be shown (e.g. with \fI\-\-get\fR)
+or changed (e.g. with \fI\-\-set\fR). Superuser permissions will
+typically be required to set the logging level.
+.PP
+One of these options: \fI\-\-create\fR, \fI\-\-get\fR or \fI\-\-set\fR
+is required. Only one of them can be given.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-a\fR, \fB\-\-all\fR=\fILEV\fR
+\fILEV\fR is used for all SCSI_LOG fields.
+.TP
+\fB\-c\fR, \fB\-\-create\fR
+Options are parsed and placed in internal fields that are displayed but
+no logging levels are changed within the Linux kernel.
+.TP
+\fB\-E\fR, \fB\-\-error\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_ERROR field.
+.TP
+\fB\-g\fR, \fB\-\-get\fR
+Fetches the current SCSI logging levels from the Linux kernel and
+displays them.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-highlevel\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_HLQUEUE and SCSI_LOG_HLCOMPLETE fields.
+.TP
+\fB\-\-hlcomplete\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_HLCOMPLETE field.
+.TP
+\fB\-\-hlqueue\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_HLQUEUE field.
+.TP
+\fB\-I\fR, \fB\-\-ioctl\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_IOCTL field.
+.TP
+\fB\-\-llcomplete\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_LLCOMPLETE field.
+.TP
+\fB\-\-llqueue\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_LLQUEUE field.
+.TP
+\fB\-L\fR, \fB\-\-lowlevel\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_LLQUEUE and SCSI_LOG_LLCOMPLETE fields.
+.TP
+\fB\-M\fR, \fB\-\-midlevel\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_MLQUEUE and SCSI_LOG_MLCOMPLETE fields.
+.TP
+\fB\-\-mlcomplete\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_MLCOMPLETE field.
+.TP
+\fB\-\-mlqueue\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_MLQUEUE field.
+.TP
+\fB\-S\fR, \fB\-\-scan\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_SCAN field.
+.TP
+\fB\-s\fR, \fB\-\-set\fR
+Uses the fields specified in this command's options and attempts to
+apply them to the Linux SCSI subsystem logging levels. Typically superuser
+permissions will be required to do this.
+.TP
+\fB\-T\fR, \fB\-\-timeout\fR=\fILEV\fR
+\fILEV\fR is placed in the SCSI_LOG_TIMEOUT field.
+.TP
+\fB\-v\fR, \fB\-\-version\fR
+Outputs the version information and then exits.
+.SH NOTES
+The \fI\-\-get\fR and \fI\-\-set\fR options access the
+/proc/sys/dev/scsi/logging_level pseudo file.
+.SH EXIT STATUS
+The exit status of this script is 0 when it is successful. Any other
+exit status indicates that an error has occurred.
+.SH EXAMPLES
+The following will set SCSI_LOG_ERROR to level 5 in the Linux kernel. It
+requires root permissions:
+.PP
+  scsi_logging_level \-s \-E 5
+.PP
+So as to not interfere with other SCSI subsystem upper level drivers (ULDs)
+which most likely will be active at the same time, the Linux sg driver uses
+SCSI_LOG_TIMEOUT for logging purposes. To see full debugging and trace from
+the sg driver use:
+.PP
+  scsi_logging_level \-s \-T 7
+.PP
+The output from the sg driver caused by this will go to the system
+logs (e.g. /var/log/syslog). To reduce the amount of output use a number
+lower than 7. Using 0 will turn off the tracing and debug.
+.PP
+To turn on maximum SCSI subsystem logging use:
+.PP
+  scsi_logging_level \-s \-a 7
+.PP
+That is probably best done on a system that does not use a SCSI command
+device to hold the root file system, or the file system that holds the
+system log. Note that SATA disks and USB attached storage nearly always
+use the SCSI subsystem.
+.SH AUTHORS
+Written by IBM. Small alterations by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co IBM Corp. 2006
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.PP
+The software was obtained from an IBM package called s390\-tools\-1.6.2
+found on that company's "developerworks" site. The most recent version of
+that package at this time is 1.8.3 .
diff --git a/doc/scsi_mandat.8 b/doc/scsi_mandat.8
new file mode 100644
index 0000000..95a708c
--- /dev/null
+++ b/doc/scsi_mandat.8
@@ -0,0 +1,44 @@
+.TH SCSI_MANDAT "8" "May 2013" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+scsi_mandat \- check SCSI device support for mandatory commands
+.SH SYNOPSIS
+.B scsi_mandat
+[\fI\-\-help\fR] [\fI\-\-log\fR] [\fI\-\-quiet\fR] [\fI\-\-verbose\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This bash shell script calls several SCSI commands on the given
+\fIDEVICE\fR. These SCSI commands are considered mandatory (although
+that varies a little depending on which standard/draft the \fIDEVICE\fR
+complies with). The results of each test and a pass/fail count are
+output.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-L\fR, \fB\-\-log\fR
+the output to stderr (from each SCSI command executed) is appended to
+a file called 'scsi_mandat.err' in the current working directory.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+the amount of output is reduced and typically only the pass/fail
+count is output.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level or verbosity.
+.SH EXIT STATUS
+The exit status of this script is the number of "bad" errors found.
+So an exit status of 0 means all mandatory SCSI commands worked as
+expected.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2011\-2013 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq,sg_luns,sg_turs,sg_requests,sg_vpd,sg_senddiag (sg3_utils)
diff --git a/doc/scsi_readcap.8 b/doc/scsi_readcap.8
new file mode 100644
index 0000000..d898ce9
--- /dev/null
+++ b/doc/scsi_readcap.8
@@ -0,0 +1,51 @@
+.TH SCSI_READCAP "8" "May 2013" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+scsi_readcap \- do SCSI READ CAPACITY command on disks
+.SH SYNOPSIS
+.B scsi_readcap
+[\fI\-\-brief\fR] [\fI\-\-help\fR] [\fI\-\-long\fR] [\fI\-\-verbose\fR]
+\fIDEVICE\fR [\fIDEVICE\fR]*
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This bash shell script calls the sg_readcap utility on each given
+\fIDEVICE\fR. This will send a SCSI READ CAPACITY command to each
+\fIDEVICE\fR.
+.PP
+The default action of this script is to send the 10 byte cdb READ
+CAPACITY(10) command to each \fIDEVICE\fR. If a response indicates
+the number of blocks is greater than or equal to '2**32 \- 1' then
+the READ CAPACITY(16) is sent and its response is output.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-brief\fR
+shortens the output to two hexadecimal numbers, both prefixed by '0x'.
+The first number is the number of blocks available and the second is
+the size of each blocks in bytes (e.g. '0x12a19eb0 0x200'). If an error
+is detected '0x0 0x0' is output and the script continues if there are
+more \fIDEVICE\fRs.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-l\fR, \fB\-\-long\fR
+the default is to send the READ CAPACITY(10) command (i.e. the 10 byte
+cdb variant). When this option is given the READ CAPACITY(16) command
+is sent. The latter command yields more information in its response.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level or verbosity.
+.SH EXIT STATUS
+The exit status of this script is 0 when it is successful. Otherwise the
+exit status is that of the last sg_readcap utility called. See
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2009\-2013 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_readcap (sg3_utils)
diff --git a/doc/scsi_ready.8 b/doc/scsi_ready.8
new file mode 100644
index 0000000..e0548e0
--- /dev/null
+++ b/doc/scsi_ready.8
@@ -0,0 +1,40 @@
+.TH SCSI_READY "8" "May 2013" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+scsi_ready \- do SCSI TEST UNIT READY on devices
+.SH SYNOPSIS
+.B scsi_ready
+[\fI\-\-brief\fR] [\fI\-\-help\fR] [\fI\-\-verbose\fR]
+\fIDEVICE\fR [\fIDEVICE\fR]*
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This bash shell script calls the sg_turs utility on each given
+\fIDEVICE\fR. This will send a SCSI TEST UNIT READY command to each
+\fIDEVICE\fR. Disks, tape drives and DVD/BD players amongst others
+may respond to this SCSI command.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-brief\fR
+for each \fIDEVICE\fR given output a line containing either '    ready'
+or '    device not ready'. If \fIDEVICE\fR is not found or there is
+another serious error then an error message will appear instead.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level or verbosity.
+.SH EXIT STATUS
+The exit status of this script is 0 when it is successful. Otherwise the
+exit status is that of the last sg_turs utility called. See
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2009\-2013 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_turs (sg3_utils)
diff --git a/doc/scsi_satl.8 b/doc/scsi_satl.8
new file mode 100644
index 0000000..bddee9f
--- /dev/null
+++ b/doc/scsi_satl.8
@@ -0,0 +1,44 @@
+.TH SCSI_SATL "8" "May 2013" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+scsi_satl \- check SCSI to ATA Translation (SAT) device support
+.SH SYNOPSIS
+.B scsi_satl
+[\fI\-\-help\fR] [\fI\-\-log\fR] [\fI\-\-quiet\fR] [\fI\-\-verbose\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This bash shell script calls several SCSI commands on the given
+\fIDEVICE\fR that is assumed to be an ATA device behind a SCSI
+to ATA Translation (SAT) layer (SATL). The results of each test
+and a pass/fail count are output.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-L\fR, \fB\-\-log\fR
+the output to stderr (from each SCSI command executed) is appended to
+a file called 'scsi_satl.err' in the current working directory.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+the amount of output is reduced and typically only the pass/fail
+count is output.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level or verbosity.
+.SH EXIT STATUS
+The exit status of this script is the number of "bad" errors found.
+So an exit status of 0 means all mandatory SCSI commands worked as
+expected.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2011\-2013 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq, sg_luns, sg_turs, sg_requests, sg_vpd, sg_senddiag, sg_modes,
+.B sg_sat_identify (sg3_utils)
diff --git a/doc/scsi_start.8 b/doc/scsi_start.8
new file mode 100644
index 0000000..7268232
--- /dev/null
+++ b/doc/scsi_start.8
@@ -0,0 +1,40 @@
+.TH SCSI_START "8" "May 2013" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+scsi_start \- start one or more SCSI disks
+.SH SYNOPSIS
+.B scsi_start
+[\fI\-\-help\fR] [\fI\-\-verbose\fR] [\fI\-\-wait\fR]
+\fIDEVICE\fR [\fIDEVICE\fR]*
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This bash shell script calls the sg_start utility on each given
+\fIDEVICE\fR. The purpose is to spin up (start) each given \fIDEVICE\fR.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level or verbosity.
+.TP
+\fB\-w\fR, \fB\-\-wait\fR
+wait for the spin up (start) on each given \fIDEVICE\fR to complete.
+The default action is to do each start in immediate mode.
+.SH NOTES
+If a large number of disks are spun up at the same time (i.e. without
+the \fI\-\-wait\fR option) then the power supply may be overloaded.
+.SH EXIT STATUS
+The exit status of this script is 0 when it is successful. Otherwise the
+exit status is that of the last sg_start utility called. See
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2009\-2013 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_start (sg3_utils)
diff --git a/doc/scsi_stop.8 b/doc/scsi_stop.8
new file mode 100644
index 0000000..79b7b2f
--- /dev/null
+++ b/doc/scsi_stop.8
@@ -0,0 +1,41 @@
+.TH SCSI_STOP "8" "May 2013" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+scsi_stop \- stop (spin down) one or more SCSI disks
+.SH SYNOPSIS
+.B scsi_stop
+[\fI\-\-help\fR] [\fI\-\-verbose\fR] [\fI\-\-wait\fR]
+\fIDEVICE\fR [\fIDEVICE\fR]*
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This bash shell script calls the sg_start utility on each given
+\fIDEVICE\fR. The purpose is to spin down (stop) each given \fIDEVICE\fR.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level or verbosity.
+.TP
+\fB\-w\fR, \fB\-\-wait\fR
+wait for the spin down (stop) on each given \fIDEVICE\fR to complete.
+The default action is to do each stop in immediate mode.
+.SH NOTES
+The sg_start utility calls the SCSI START STOP UNIT command and can
+either start (spin up) or stop (spin down) a SCSI disk depending
+on the given command line options.
+.SH EXIT STATUS
+The exit status of this script is 0 when it is successful. Otherwise the
+exit status is that of the last sg_start utility called. See
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2009\-2013 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_start (sg3_utils)
diff --git a/doc/scsi_temperature.8 b/doc/scsi_temperature.8
new file mode 100644
index 0000000..e3ce56b
--- /dev/null
+++ b/doc/scsi_temperature.8
@@ -0,0 +1,35 @@
+.TH SCSI_TEMPERATURE "8" "May 2011" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+scsi_temperature \- fetch the temperature of a SCSI device
+.SH SYNOPSIS
+.B scsi_temperature
+[\fI\-\-help\fR] [\fI\-\-verbose\fR]
+\fIDEVICE\fR [\fIDEVICE\fR]*
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This bash shell script calls the sg_logs utility on each given
+\fIDEVICE\fR in order to find the device's temperature. The Temperature
+log page is checked first and if it is not available then the Informational
+Exceptions log page is checked.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level or verbosity.
+.SH EXIT STATUS
+The exit status of this script is 0 when it is successful. Otherwise the
+exit status is that of the last sg_logs utility called. See
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2011\-2013 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_logs (sg3_utils)
diff --git a/doc/sg3_utils.8 b/doc/sg3_utils.8
new file mode 100644
index 0000000..d038cb5
--- /dev/null
+++ b/doc/sg3_utils.8
@@ -0,0 +1,883 @@
+.TH SG3_UTILS "8" "November 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg3_utils \- a package of utilities for sending SCSI commands
+.SH SYNOPSIS
+.B sg_*
+[\fI\-\-dry\-run\fR] [\fI\-\-enumerate\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-in=FN\fR] [\fI\-\-inhex=FN\fR] [\fI\-\-json[=JO]\fR]
+[\fI\-\-maxlen=LEN\fR] [\fI\-\-raw\fR] [\fI\-\-timeout=SECS\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR]
+[\fIOTHER_OPTIONS\fR] [\fIDEVICE\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+sg3_utils is a package of utilities that send SCSI commands to the given
+\fIDEVICE\fR via a SCSI pass through interface provided by the host
+operating system.
+.PP
+The names of all utilities start with "sg" and most start with "sg_" often
+followed by the name, or a shortening of the name, of the SCSI command that
+they send. For example the "sg_verify" utility sends the SCSI VERIFY
+command. A mapping between SCSI commands and the sg3_utils utilities that
+issue them is shown in the COVERAGE file. The sg_raw utility can be used to
+send an arbitrary SCSI command (supplied on the command line) to the
+given \fIDEVICE\fR.
+.PP
+sg_decode_sense can be used to decode SCSI sense data given on the command
+line or in a file. sg_raw \-vvv will output the T10 name of a given SCSI
+CDB which is most often 16 bytes or less in length.
+.PP
+SCSI draft standards can be found at https://www.t10.org . The standards
+themselves can be purchased from ANSI and other standards organizations.
+A good overview of various SCSI standards can be seen in
+https://www.t10.org/scsi\-3.htm with the SCSI command sets in the upper part
+of the diagram. The highest level (i.e. most abstract) document is the SCSI
+Architecture Model (SAM) with SAM\-5 being the most recent standard (ANSI
+INCITS 515\-2016) with the most recent draft being SAM\-6 revision 4 . SCSI
+commands in common with all device types can be found in SCSI Primary
+Commands (SPC) of which SPC\-5 is the most recent standard (ANSI INCITS
+502-2020). The most recent SPC draft is SPC\-6 revision 6. Block device
+specific commands (e.g. as used by disks) are in SBC, those for tape drives
+in SSC, those for SCSI enclosures in SES and those for CD/DVD/BD drives in
+MMC.
+.PP
+It is becoming more common to control ATA disks with the SCSI command set.
+This involves the translation of SCSI commands to their corresponding ATA
+equivalents (and that is an imperfect mapping in some cases). The relevant
+standard is called SCSI to ATA Translation (SAT, SAT\-2 and SAT\-3) are
+now standards at INCITS(ANSI) and ISO while SAT\-4 is at the draft stage.
+The logic to perform the command translation is often called a SAT Layer or
+SATL and may be within an operating system, in host bus adapter firmware or
+in an external device (e.g. associated with a SAS expander). See
+https://www.t10.org for more information.
+.PP
+There is some support for SCSI tape devices but not for their basic
+operation. The reader is referred to the "mt" utility.
+.PP
+There are two generations of command line option usage. The newer
+utilities (written since July 2004) use the getopt_long() function to parse
+command line options. With that function, each option has two representations:
+a short form (e.g. '\-v') and a longer form (e.g. '\-\-verbose'). If an
+argument is required then it follows a space (optionally) in the short form
+and a "=" in the longer form (e.g. in the sg_verify utility '\-l 2a6h'
+and '\-\-lba=2a6h' are equivalent). Note that with getopt_long(), short form
+options can be elided, for example: '\-all' is equivalent to '\-a \-l \-l'.
+The \fIDEVICE\fR argument may appear after, between or prior to any options.
+.PP
+The older utilities, including as sg_inq, sg_logs, sg_modes, sg_opcode,
+sg_rbuff, sg_readcap, sg_senddiag, sg_start and sg_turs had individual
+command line processing code typically based on a single "\-" followed by one
+or more characters. If an argument is needed then it follows a "=" (
+e.g. '\-p=1f' in sg_modes with its older interface). Various options can be
+elided as long as it is not ambiguous (e.g. '\-vv' to increase the verbosity).
+.PP
+Over time the command line interface of these older utilities became messy
+and overloaded with options. So in sg3_utils version 1.23 the command line
+interface of these older utilities was altered to have both a cleaner
+getopt_long() interface and their older interface for backward compatibility.
+By default these older utilities use their getopt_long() based interface.
+The getopt_long() is a GNU extension (i.e. not yet POSIX certified) but
+more recent command line utilities tend to use it. That can be overridden
+by defining the SG3_UTILS_OLD_OPTS environment variable or using '\-O'
+or '\-\-old' as the first command line option. The man pages of the older
+utilities documents the details.
+.PP
+Several sg3_utils utilities are based on the Unix dd command (e.g. sg_dd)
+and permit copying data at the level of SCSI READ and WRITE commands. sg_dd
+is tightly bound to Linux and hence is not ported to other OSes. A more
+generic utility (than sg_dd) called ddpt in a package of the same name has
+been ported to other OSes.
+.SH ENVIRONMENT VARIABLES
+The SG3_UTILS_OLD_OPTS environment variable is explained in the previous
+section. It is only for backward compatibility of the command line options
+for older utilities.
+.PP
+The SG3_UTILS_DSENSE environment variable may be set to a number. It is
+only used by the embedded SNTL within the library used by the utilities in
+this library. SNTL is a SCSI to NVMe Translation Layer. This environment
+variable defaults to 0 which will lead to any utility that issues a SCSI
+command that is translated to a NVMe command (by the embedded SNTL) that
+fails at the NVMe device, to return SCSI sense in 'fixed' format. If this
+variable is non\-zero then then the returned SCSI sense will be in 'descriptor'
+format.
+.PP
+Several utilities have their own environment variable setting (e.g.
+sg_persist has SG_PERSIST_IN_RDONLY). See individual utility man pages
+for more information.
+.PP
+There is a Linux specific environment variable called SG3_UTILS_LINUX_NANO
+that if defined and the sg driver in the system is 4.0.30 or later, will
+show command durations in nanoseconds rather than the default milliseconds.
+Command durations are typically only shown if \-\-verbose is used 3 or more
+times. Due to an interface problem (a 32 bit integer that should be 64 bits
+with the benefit of hindsight) the maximum duration that can be represented
+in nanoseconds is about 4.2 seconds. If longer durations may occur then
+don't define this environment variable (or undefine it).
+.SH LINUX DEVICE NAMING
+Most disk block devices have names like /dev/sda, /dev/sdb, /dev/sdc, etc.
+SCSI disks in Linux have always had names like that but in recent Linux
+kernels it has become more common for many other disks (including SATA
+disks and USB storage devices) to be named like that. Partitions within a
+disk are specified by a number appended to the device name, starting at
+1 (e.g. /dev/sda1 ).
+.PP
+Tape drives are named /dev/st<num> or /dev/nst<num> where <num> starts
+at zero. Additionally one letter from this list: "lma" may be appended to
+the name. CD, DVD and BD readers (and writers) are named /dev/sr<num>
+where <num> start at zero. There are less used SCSI device type names,
+the dmesg and the lsscsi commands may help to find if any are attached to
+a running system.
+.PP
+There is also a SCSI device driver which offers alternate generic access
+to SCSI devices. It uses names of the form /dev/sg<num> where <num> starts
+at zero. The "lsscsi \-g" command may be useful in finding these and which
+generic name corresponds to a device type name (e.g. /dev/sg2 may
+correspond to /dev/sda). In the lk 2.6 series a block SCSI generic
+driver was introduced and its names are of the form
+/dev/bsg/<h:c:t:l> where h, c, t and l are numbers. Again see the lsscsi
+command to find the correspondence between that SCSI tuple (i.e. <h:c:t:l>)
+and alternate device names.
+.PP
+Prior to the Linux kernel 2.6 series these utilities could only use
+generic device names (e.g. /dev/sg1 ). In almost all cases in the Linux
+kernel 2.6 series, any device name can be used by these utilities.
+.PP
+Very little has changed in Linux device naming in the Linux kernel 3
+and 4 series.
+.SH WINDOWS DEVICE NAMING
+Storage and related devices can have several device names in Windows.
+Probably the most common in the volume name (e.g. "D:"). There are also
+a "class" device names such as "PhysicalDrive<n>", "CDROM<n>"
+and "TAPE<n>". <n> is an integer starting at 0 allocated in ascending
+order as devices are discovered (and sometimes rediscovered).
+.PP
+Some storage devices have a SCSI lower level device name which starts
+with a SCSI (pseudo) adapter name of the form "SCSI<n>:". To this is added
+sub\-addressing in the form of a "bus" number, a "target" identifier and
+a LUN (Logical Unit Number). The "bus" number is also known as a "PathId".
+These are assembled to form a device name of the
+form: "SCSI<n>:<bus>,<target>,<lun>". The trailing ",<lun>" may be omitted
+in which case a LUN of zero is assumed. This lower level device name cannot
+often be used directly since Windows blocks attempts to use it if a class
+driver has "claimed" the device. There are SCSI device types (e.g.
+Automation/Drive interface type) for which there is no class driver. At
+least two transports ("bus types" in Windows jargon): USB and IEEE 1394 do
+not have a "scsi" device names of this form.
+.PP
+In keeping with DOS file system conventions, the various device names
+can be given in upper, lower or mixed case. Since "PhysicalDrive<n>" is
+tedious to write, a shortened form of "PD<n>" is permitted by all
+utilities in this package.
+.PP
+A single device (e.g. a disk) can have many device names. For
+example: "PD0" can also be "C:", "D:" and "SCSI0:0,1,0". The two volume names
+reflect that the disk has two partitions on it. Disk partitions that are
+not recognized by Windows are not usually given a volume name. However
+Vista does show a volume name for a disk which has no partitions recognized
+by it and when selected invites the user to format it (which may be rather
+unfriendly to other OSes).
+.PP
+These utilities assume a given device name is in the Win32 device namespace.
+To make that explicit "\\\\.\\" can be prepended to the device names mentioned
+in this section. Beware that backslash is an escape character in Unix like
+shells and the C programming language. In a shell like Msys (from MinGW)
+each backslash may need to be typed twice.
+.PP
+The sg_scan utility within this package lists out Windows device names in
+a form that is suitable for other utilities in this package to use.
+.SH FREEBSD DEVICE NAMING
+SCSI disks have block names of the form /dev/da<num> where <num> is an
+integer starting at zero. The "da" is replaced by "sa" for SCSI tape
+drives and "cd" for SCSI CD/DVD/BD drives. Each SCSI device has a
+corresponding pass\-through device name of the form /dev/pass<num>
+where <num> is an integer starting at zero. The "camcontrol devlist"
+command may be useful for finding out which SCSI device names are
+available and the correspondence between class and pass\-through names.
+.PP
+FreeBSD allows device names to be given without the leading "/dev/" (e.g.
+da0 instead of /dev/da0). That worked in this package up until version
+1.43 when the unadorned device name (e.g. "da0") gave an error. The
+original action (i.e. allowing unadorned device names) has been restored
+in version 1.46 . Also note that symlinks (to device names) are followed
+before prepending "/dev/" if the resultant name doesn't start with a "/".
+.PP
+FreeBSD's NVMe naming has been evolving. The controller naming is the
+same as Linux: "/dev/nvme<n>" but the namespaces have an
+extra "s" (e.g. "/dev/nvme0ns1"). The latter is not a block (GEOM)
+device (strictly speaking FreeBSD does not have block devices). Initially
+FreeBSD had "/dev/nvd<m>" GEOM devices that were not based on the CAM
+subsystem. Then in FreeBSD release 12 a new nda driver was added that is
+CAM (and GEOM) based for NVMe namespaces; it has names like "/dev/nda0".
+The preferred device nodes for this package are "/dev/nvme0" for NVMe
+controllers and "/dev/nda0" for NVMe namespaces.
+.SH SOLARIS DEVICE NAMING
+SCSI device names below the /dev directory have a form like: c5t4d3s2
+where the number following "c" is the controller (HBA) number, the number
+following "t" is the target number (from the SCSI parallel interface days)
+and the number following "d" is the LUN. Following the "s" is the slice
+number which is related to a partition and by convention "s2" is the whole
+disk.
+.PP
+OpenSolaris also has a c5t4d3p2 form where the number following the "p" is
+the partition number apart from "p0" which is the whole disk. So a whole
+disk may be referred to as either c5t4d3, c5t4d3s2 or c5t4d3p0 .
+.PP
+And these device names are duplicated in the /dev/dsk and /dev/rdsk
+directories. The former is the block device name and the latter is
+for "raw" (or char device) access which is what sg3_utils needs. So in
+OpenSolaris something of the form 'sg_inq /dev/rdsk/c5t4d3p0' should work.
+If it doesn't work then add a '\-vvv' option for more debug information.
+Trying this form 'sg_inq /dev/dsk/c5t4d3p0' (note "rdsk" changed to "dsk")
+will result in an "inappropriate ioctl for device" error.
+.PP
+The device names within the /dev directory are typically symbolic links to
+much longer topological names in the /device directory. In Solaris cd/dvd/bd
+drives seem to be treated as disks and so are found in the /dev/rdsk
+directory. Tape drives appear in the /dev/rmt directory.
+.PP
+There is also a sgen (SCSI generic) driver which by default does not attach
+to any device. See the /kernel/drv/sgen.conf file to control what is
+attached. Any attached device will have a device name of the
+form /dev/scsi/c5t4d3 .
+.PP
+Listing available SCSI devices in Solaris seems to be a challenge. "Use
+the 'format' command" advice works but seems a very dangerous way to list
+devices. [It does prompt again before doing any damage.] 'devfsadm \-Cv'
+cleans out the clutter in the /dev/rdsk directory, only leaving what
+is "live". The "cfgadm \-v" command looks promising.
+.SH NVME SUPPORT
+NVMe (or NVM Express) is a relatively new storage transport and command
+set. The level of abstraction of the NVMe command set is somewhat lower
+the SCSI command sets, closer to the level of abstraction of ATA (and SATA)
+command sets. NVMe claims to be designed with flash and modern "solid
+state" storage in mind, something unheard of when SCSI was originally
+developed in the 1980s.
+.PP
+The SCSI command sets' advantage is the length of time they have been in
+place and the existing tools (like these) to support it. Plus SCSI command
+sets level of abstraction is both and advantage and disadvantage. Recently
+the NVME\-MI (Management Interface) designers decide to use the SCSI
+Enclosure Services (SES\-3) standard "as is" with the addition of two
+tunnelling NVME\-MI commands: SES Send and SES Receive. This means after the
+OS interface differences are taken into account, the sg_ses, sg_ses_microcode
+and sg_senddiag utilities can be used on a NVMe device that supports a newer
+version of NVME\-MI.
+.PP
+The NVME\-MI SES Send and SES Receive commands correspond to the SCSI
+SEND DIAGNOSTIC and RECEIVE DIAGNOSTIC RESULTS commands respectively.
+There are however a few other commands that need to be translated, the
+most important of which is the SCSI INQUIRY command to the NVMe Identify
+controller/namespace. Starting in version 1.43 these utilities contain a
+small SNTL (SCSI to NVMe Translation Layer) to take care of these details.
+.PP
+As a side effect of this "juggling" if the sg_inq utility is used (without
+the \-\-page= option) on a NVMe \fIDEVICE\fR then the actual NVMe
+Identifier (controller and possibly namespace) responses are decoded and
+output. However if 'sg_inq \-\-page=sinq <device>' is given for the
+same \fIDEVICE\fR then parts of the NVMe Identify controller and namespace
+response are translated to a SCSI standard INQUIRY response which is then
+decoded and output.
+.PP
+Apart from the special case with the sg_inq, all other utilities in the
+package assume they are talking to a SCSI device and decode any response
+accordingly. One easy way for users to see the underlying device is a
+NVMe device is the standard INQUIRY response Vendor Identification field
+of "NVMe    " (an 8 character long string with 4 spaces to the right).
+.PP
+The following SCSI commands are currently supported by the SNTL library:
+INQUIRY, MODE SELECT(10), MODE SENSE(10), READ(10,16), READ CAPACITY(10,16),
+RECEIVE DIAGNOSTIC RESULTS, REQUEST SENSE, REPORT LUNS, REPORT SUPPORTED
+OPERATION CODES, REPORT SUPPORTED TASK MANAGEMENT FUNCTIONS, SEND
+DIAGNOSTICS, START STOP UNIT, SYNCHRONIZE CACHE(10,16), TEST UNIT READY,
+VERIFY(10,16), WRITE(10,16) and WRITE SAME(10,16).
+.SH EXIT STATUS
+To aid scripts that call these utilities, the exit status is set to indicate
+success (0) or failure (1 or more). Note that some of the lower values
+correspond to the SCSI sense key values.
+.PP
+The exit status values listed below can be given to the sg_decode_sense
+utility (which is found in this package) as follows:
+.br
+  sg_decode_sense \-\-err=<exit_status>
+.br
+and a short explanatory string will be output to stdout.
+.PP
+The exit status values are:
+.TP
+.B 0
+success. Also used for some utilities that wish to return a boolean value
+for the "true" case (and that no error has occurred). The false case is
+conveyed by exit status 36.
+.TP
+.B 1
+syntax error. Either illegal command line options, options with bad
+arguments or a combination of options that is not permitted.
+.TP
+.B 2
+the \fIDEVICE\fR reports that it is not ready for the operation requested.
+The \fIDEVICE\fR may be in the process of becoming ready (e.g.  spinning up
+but not at speed) so the utility may work after a wait. In Linux the
+\fIDEVICE\fR may be temporarily blocked while error recovery is taking place.
+See exit status values 12 and 13 below which refine this exit value.
+.TP
+.B 3
+the \fIDEVICE\fR reports a medium or hardware error (or a blank check). For
+example an attempt to read a corrupted block on a disk will yield this value.
+.TP
+.B 5
+the \fIDEVICE\fR reports an "illegal request" with an additional sense code
+other than "invalid command operation code". This is often a supported
+command with a field set requesting an unsupported capability. For commands
+that require a "service action" field this value can indicate that the
+command with that service action value is not supported.
+.TP
+.B 6
+the \fIDEVICE\fR reports a "unit attention" condition. This usually indicates
+that something unrelated to the requested command has occurred (e.g. a device
+reset) potentially before the current SCSI command was sent. The requested
+command has not been executed by the device. Note that unit attention
+conditions are usually only reported once by a device.
+.TP
+.B 7
+the \fIDEVICE\fR reports a "data protect" sense key. This implies some
+mechanism has blocked writes (or possibly all access to the media).
+.TP
+.B 9
+the \fIDEVICE\fR reports an illegal request with an additional sense code
+of "invalid command operation code" which means that it doesn't support the
+requested command.
+.TP
+.B 10
+the \fIDEVICE\fR reports a "copy aborted". This implies another command or
+device problem has stopped a copy operation. The EXTENDED COPY family of
+commands (including WRITE USING TOKEN) may return this sense key.
+.TP
+.B 11
+the \fIDEVICE\fR reports an aborted command. In some cases aborted
+commands can be retried immediately (e.g. if the transport aborted
+the command due to congestion).
+.TP
+.B 12
+the \fIDEVICE\fR reports a sense key of not ready together with an
+additional sense code of "target port in standby state".
+.TP
+.B 13
+the \fIDEVICE\fR reports a sense key of not ready together with an
+additional sense code of "target port in unavailable state".
+.TP
+.B 14
+the \fIDEVICE\fR reports a miscompare sense key. VERIFY and COMPARE AND
+WRITE commands may report this.
+.TP
+.B 15
+the utility is unable to open, close or use the given \fIDEVICE\fR or some
+other file. The given file name could be incorrect or there may be
+permission problems. Adding the '\-v' option may give more information.
+.TP
+.B 17
+a SCSI "Illegal request" sense code received with a flag indicating the
+Info field is valid. This is often a LBA but its meaning is command specific.
+.TP
+.B 18
+the \fIDEVICE\fR reports a medium or hardware error (or a blank check)
+with a flag indicating the Info field is valid. This is often a LBA (of
+the first encountered error) but its meaning is command specific.
+.TP
+.B 20
+the \fIDEVICE\fR reports it has a check condition but "no sense"
+and non\-zero information in its additional sense codes. Some polling
+commands (e.g. REQUEST SENSE) can receive this response. There may
+be useful information in the sense data such as a progress indication.
+.TP
+.B 21
+the \fIDEVICE\fR reports a "recovered error". The requested command
+was successful. Most likely a utility will report a recovered error
+to stderr and continue, probably leaving the utility with an exit
+status of 0 .
+.TP
+.B 22
+the \fIDEVICE\fR reports that the current command or its parameters imply
+a logical block address (LBA) that is out of range. This happens surprisingly
+often when trying to access the last block on a storage device; either a
+classic "off by one" logic error or a misreading of the response from READ
+CAPACITY(10 or 16) in which the address of the last block rather than the
+number of blocks on the \fIDEVICE\fR is returned. Since LBAs are origin zero
+they range from 0 to n\-1 where n is the number of blocks on the \fIDEVICE\fR,
+so the LBA of the last block is one less than the total number of blocks.
+.TP
+.B 24
+the \fIDEVICE\fR reports a SCSI status of "reservation conflict". This
+means access to the \fIDEVICE\fR with the current command has been blocked
+because another machine (HBA or SCSI "initiator") holds a reservation on
+this \fIDEVICE\fR. On modern SCSI systems this is related to the use of
+the PERSISTENT RESERVATION family of commands.
+.TP
+.B 25
+the \fIDEVICE\fR reports a SCSI status of "condition met". Currently only
+the PRE\-FETCH command (see SBC\-4) yields this status.
+.TP
+.B 26
+the \fIDEVICE\fR reports a SCSI status of "busy". SAM\-6 defines this status
+as the logical unit is temporarily unable to process a command. It is
+recommended to re\-issue the command.
+.TP
+.B 27
+the \fIDEVICE\fR reports a SCSI status of "task set full".
+.TP
+.B 28
+the \fIDEVICE\fR reports a SCSI status of "ACA active". ACA is "auto
+contingent allegiance" and is seldom used.
+.TP
+.B 29
+the \fIDEVICE\fR reports a SCSI status of "task aborted". SAM\-5 says:
+"This status shall be returned if a command is aborted by a command or task
+management function on another I_T nexus and the Control mode page TAS bit
+is set to one".
+.TP
+.B 31
+error involving two or more command line options. They may be contradicting,
+select an unsupported mode, or a required option (given the context) is
+missing.
+.TP
+.B 32
+there is a logic error in the utility. It corresponds to code comments
+like "shouldn't/can't get here". Perhaps the author should be informed.
+.TP
+.B 33
+the command sent to \fIDEVICE\fR has timed out.
+.TP
+.B 34
+this is a Windows only exit status and indicates that the Windows error
+number (32 bits) cannot meaningfully be mapped to an equivalent Unix error
+number returned as the exit status (7 bits).
+.TP
+.B 35
+a transport error has occurred. This will either be in the driver (e.g. HBA
+driver) or in the interconnect between the host (initiator) and the
+device (target).  For example in SAS an expander can run out of paths and
+thus be unable to return the user data from a READ command.
+.TP
+.B 36
+no error has occurred plus the utility wants to convey a boolean value
+of false. The corresponding true value is conveyed by a 0 exit status.
+.TP
+.B 40
+the command sent to \fIDEVICE\fR has received an "aborted command" sense
+key with an additional sense code of 0x10. This value is related to
+problems with protection information (PI or DIF). For example this error
+may occur when reading a block on a drive that has never been written (or
+is unmapped) if that drive was formatted with type 1, 2 or 3 protection.
+.TP
+.B 41
+the command sent to \fIDEVICE\fR has received an "aborted command" sense
+key with an additional sense code of 0x10 (as with error code) plus a flag
+indicating the Info field is valid.
+.TP
+.B 48
+this is an internal message indicating a NVMe status field (SF) is other
+than zero after a command has been executed (i.e. something went wrong).
+Work in this area is currently experimental.
+.TP
+.B 49
+low level driver reports a response's residual count (i.e. number of bytes
+actually received by HBA is 'requested_bytes \- residual_count') that is
+nonsensical.
+.TP
+.B 50
+OS system calls that fail often return a small integer number to help. In
+Unix these are called "errno" values where 0 implies no error. These error
+codes set aside 51 to 96 for mapping these errno values but that may not be
+sufficient. Higher errno values that cannot be mapped are all mapped to
+this value (i.e. 50).
+.br
+Note that an errno value of 0 is mapped to error code 0.
+.TP
+.B 50 + <os_error_number>
+OS system calls that fail often return a small integer number to help
+indicate what the error is. For example in Unix the inability of a system
+call to allocate memory returns (in 'errno') ENOMEM which often is
+associated with the integer 12. So 62 (i.e. '50 + 12') may be returned
+by a utility in this case. It is also possible that a utility in this
+package reports 50+ENOMEM when it can't allocate memory, not necessarily
+from an OS system call. In recent versions of Linux the file showing the
+mapping between symbolic constants (e.g. ENOMEM) and the corresponding
+integer is in the kernel source code file:
+include/uapi/asm\-generic/errno\-base.h
+.br
+Note that errno values that are greater than or equal to 47 cannot fit in
+range provided. Instead they are all mapped to 50 as discussed in the
+previous entry.
+.TP
+.B 97
+a SCSI command response failed sanity checks.
+.TP
+.B 98
+the \fIDEVICE\fR reports it has a check condition but the error
+doesn't fit into any of the above categories.
+.TP
+.B 99
+any errors that can't be categorized into values 1 to 98 may yield
+this value. This includes transport and operating system errors
+after the command has been sent to the device.
+.TP
+.B 100\-125
+these error codes are used by the ddpt utility which uses the sg3_utils
+library. They are mainly specialized error codes associated with offloaded
+copies.
+.TP
+.B 126
+the utility was found but could not be executed. That might occur if the
+executable does not have execute permissions.
+.TP
+.B 127
+This is the exit status for utility not found. That might occur when a
+script calls a utility in this package but the PATH environment variable
+has not been properly set up, so the script cannot find the executable.
+.TP
+.B 128 + <signum>
+If a signal kills a utility then the exit status is 128 plus the signal
+number. For example if a segmentation fault occurs then a utility is
+typically killed by SIGSEGV which according to 'man 7 signal' has an
+associated signal number of 11; so the exit status will be 139 .
+.TP
+.B 255
+the utility tried to yield an exit status of 255 or larger. That should
+not happen; given here for completeness.
+.PP
+Most of the error conditions reported above will be repeatable (an example
+of one that is not is "unit attention") so the utility can be run again with
+the '\-v' option (or several) to obtain more information.
+.SH COMMON OPTIONS
+Arguments to long options are mandatory for short options as well. In the
+short form an argument to an option uses zero or more spaces as a
+separator (i.e. the short form does not use "=" as a separator).
+.PP
+If an option takes a numeric argument then that argument is assumed to
+be decimal unless otherwise indicated (e.g. with a leading "0x", a
+trailing "h" or as noted in the usage message).
+.PP
+Some options are used uniformly in most of the utilities in this
+package. Those options are listed below. Note that there are some
+exceptions.
+.TP
+\fB\-d\fR, \fB\-\-dry\-run\fR
+utilities that can cause lots of user data to be lost or overwritten
+sometimes have a \fI\-\-dry\-run\fR option. Device modifying actions are
+typically bypassed (or skipped) to implement a policy of "do no harm".
+This allows complex command line invocations to be tested before the
+action required (e.g. format a disk) is performed. The \fI\-\-dry\-run\fR
+option has become a common feature of many command line utilities (e.g.
+the Unix 'patch' command), not just those from this package.
+.br
+Note that most hyphenated option names in this package also can be given
+with an underscore rather than a hyphen (e.g.  \fI\-\-dry_run\fR).
+.TP
+\fB\-e\fR, \fB\-\-enumerate\fR
+some utilities (e.g. sg_ses and sg_vpd) store a lot of information in
+internal tables. This option will output that information in some readable
+form (e.g. sorted by an acronym or by page number) then exit. Note that
+with this option \fIDEVICE\fR is ignored (as are most other options) and no
+SCSI IO takes place, so the invoker does not need any elevated permissions.
+.TP
+\fB\-h\fR, \fB\-?\fR, \fB\-\-help\fR
+output the usage message then exit. In a few older utilities the '\-h'
+option requests hexadecimal output. In these cases the '\-?' option will
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+for SCSI commands that yield a non\-trivial response, print out that response
+in ASCII hexadecimal. When used once, 16 bytes are printed on each line,
+prefixed by an relative address, starting at 0 (hex). When used twice, an
+ASCII rendering of the 16 bytes is appended to each line, with non printable
+characters replaced by a '.' . When used three times only the 16 hex bytes
+are printed on each line (hence no address prefix nor ASCII appended). To
+produce hexadecimal that can be parsed by other utilities use this option
+three or four times.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIFN\fR
+many SCSI commands fetch a significant amount of data (returned in the
+data\-in buffer) which several of these utilities decode (e.g. sg_vpd and
+sg_logs). To separate the two steps of fetching the data from a SCSI device
+and then decoding it, this option has been added. The first step (fetching
+the data) can be done using the \fI\-\-hex\fR or \fI\-\-raw\fR option and
+redirecting the command line output to a file (often done with ">" in Unix
+based operating systems). The difference between \fI\-\-hex\fR and
+\fI\-\-raw\fR is that the former produces output in ASCII hexadecimal
+while \fI\-\-raw\fR produces its output in "raw" binary.
+.br
+The second step (i.e. decoding the SCSI response data now held in a file)
+can be done using this \fI\-\-in=FN\fR option where the file name is
+\fIFN\fR. If "\-" is used for \fIFN\fR then stdin is assumed, again this
+allows for command line redirection (or piping). That file (or stdin)
+is assumed to contain ASCII hexadecimal unless the \fI\-\-raw\fR option is
+also given in which case it is assumed to be binary. Notice that the meaning
+of the \fI\-\-raw\fR option is "flipped" when used with \fI\-\-in=FN\fR to
+act on the input, typically it acts on the output data.
+.br
+Since the structure of the data returned by SCSI commands varies
+considerably then the usage information or the manpage of the utility being
+used should be checked. In some cases \fI\-\-hex\fR may need to be used
+multiple times (and is more conveniently given as '\-HH' or '\-HHH).
+.TP
+\fB\-i\fR, \fB\-\-inhex\fR=\fIFN\fR
+This option has the same or similar functionality as \fI\-\-in=FN\fR. And
+perhaps 'inhex' is more descriptive since by default, ASCII hexadecimal is
+expected in the contents of file: \fIFN\fR. Alternatively the short form
+option may be \fI\-I\fR or \fI\-X\fR. See the "FORMAT OF FILES CONTAINING
+ASCII HEX" section below for more information.
+.TP
+\fB\-\-json\fR[=\fIJO\fR]
+The default output of most utilities that decode information returned from
+SCSI devices is designed for human readability. Sometimes a more parseable
+form of output is required and JSON is a popular way to do this. Only
+utilities that decode a significant amount of SCSI data support this option.
+.br
+The corresponding short option is usually \fI\-j[JO]\fR but maybe
+\fI\-J[JO]\fR if \fI\-j\fR is already in use. Note that in all cases \fIJO\fR
+argument is itself optional. See the sg3_utils_json manpage for more
+information.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+several important SCSI commands (e.g. INQUIRY and MODE SENSE) have response
+lengths that vary depending on many factors, only some of which these
+utilities take into account. The maximum response length is typically
+specified in the 'allocation length' field of the cdb. In the absence of
+this option, several utilities use a default allocation length (sometimes
+recommended in the SCSI draft standards) or a "double fetch" strategy.
+See sg_logs(8) for its description of a "double fetch" strategy. These
+techniques are imperfect and in the presence of faulty SCSI targets can
+cause problems (e.g. some USB mass storage devices freeze if they receive
+an INQUIRY allocation length other than 36). Also use of this option
+disables any "double fetch" strategy that may have otherwise been used.
+.br
+To head off a class of degenerate bugs, if \fILEN\fR is less than 16 then
+it is ignored (usually with a warning message) and the default value is
+used instead. Some utilities use 4 (bytes), rather than 16, as the cutoff
+value.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+for SCSI commands that yield a non\-trivial response, output that response
+in binary to stdout. If any error messages or warning are produced they are
+usually sent to stderr so as to not interfere with the output from this
+option.
+.br
+Some utilities that consume data to send to the \fIDEVICE\fR along with the
+SCSI command, use this option. Alternatively the \fI\-\-in=FN\fR option causes
+\fIDEVICE\fR to be ignored and the response data (to be decoded) fetched
+from a file named \fIFN\fR. In these cases this option may indicate that
+binary data can be read from stdin or from a nominated file (e.g. \fIFN\fR).
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fISECS\fR
+utilities that issue potentially long\-running SCSI commands often have a
+\fI\-\-timeout=SECS\fR option. This typically instructs the operating system
+to abort the SCSI command in question once the timeout expires. Aborting
+SCSI commands is typically a messy business and in the case of format like
+commands may leave the device in a "format corrupt" state requiring another
+long\-running re\-initialization command to be sent. The argument, \fISECS\fR,
+is usually in seconds and the short form of the option may be something
+other than \fI\-t\fR since the timeout option was typically added later as
+storage devices grew in size and initialization commands took longer. Since
+many utilities had relatively long internal command timeouts before this
+option was introduced, the actual command timeout given to the operating
+systems is the higher of the internal timeout and \fISECS\fR.
+.br
+Many long running SCSI commands have an IMMED bit which causes the command
+to finish relatively quickly but the initialization process to continue. In
+such cases the REQUEST SENSE command can be used to monitor progress with
+its progress indication field (see the sg_requests and sg_turs utilities).
+Utilities that send such SCSI command either have an \fI\-\-immed\fR option
+or a \fI\-\-wait\fR option which is the logical inverse of the "immediate"
+action.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output). Can be used multiple
+times to further increase verbosity. The additional output caused by this
+option is almost always sent to stderr.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit. Each utility has its own version
+number and date of last code change.
+.SH NUMERIC ARGUMENTS
+Many utilities have command line options that take numeric arguments. These
+numeric arguments can be large values (e.g. a logical block address (LBA) on
+a disk) and can be inconvenient to enter in the default decimal
+representation. So various other representations are permitted.
+.PP
+Multiplicative suffixes are accepted. They are one, two or three letter
+strings appended directly after the number to which they apply:
+.PP
+   c C         *1
+.br
+   w W         *2
+.br
+   b B         *512
+.br
+   k K KiB     *1024
+.br
+   KB kB       *1000
+.br
+   m M MiB     *1048576
+.br
+   MB mB       *1000000
+.br
+   g G GiB     *(2^30)
+.br
+   GB gB       *(10^9)
+.br
+   t T TiB     *(2^40)
+.br
+   TB          *(10^12)
+.br
+   p P PiB     *(2^50)
+.br
+   PB          *(10^15)
+.PP
+An example is "2k" for 2048. The large tera and peta suffixes are only
+available for numeric arguments that might require 64 bits to represent
+internally.
+.PP
+These multiplicative suffixes are compatible with GNU's dd command (since
+2002) which claims compliance with SI and with IEC 60027\-2.
+.PP
+A suffix of the form "x<n>" multiplies the preceding number by <n>. An
+example is "2x33" for "66". The left argument cannot be '0' as '0x' will
+be interpreted as hexadecimal number prefix (see below). The left
+argument to the multiplication must end in a hexadecimal digit (i.e.
+0 to f) and the whole expression cannot have any embedded whitespace (e.g.
+spaces). An ugly example: "0xfx0x2" for 30.
+.PP
+A suffix of the form "+<n>" adds the preceding number to <n>. An example
+is "3+1k" for "1027". The left argument to the addition must end in a
+hexadecimal digit (i.e. 0 to f) and the whole expression cannot have any
+embedded whitespace (e.g. spaces). Another example: "0xf+0x2" for 17.
+.PP
+Alternatively numerical arguments can be given in hexadecimal. There are
+two syntaxes. The number can be preceded by either "0x" or "0X" as found
+in the C programming language. The second hexadecimal representation is a
+trailing "h" or "H" as found in (storage) standards. When hex numbers are
+given, multipliers cannot be used. For example the decimal value "256" can
+be given as "0x100" or "100h".
+.SH FORMAT OF FILES CONTAINING ASCII HEX
+Such a file is assumed to contain a sequence of one or two digit ASCII
+hexadecimal values separated by whitespace. "Whitespace consists of either
+spaces, tabs, blank lines, or any combination thereof". Hyphens (e.g. '\-')
+are also allowed as separators. Each one or two digit ASCII hex pair is
+decoded into a byte (i.e. 8 bits). The following will be decoded to
+valid (ascending valued) bytes: '0', '01', '3', 'c', 'F', '4a', 'cC'
+and 'ff'. Lines containing only whitespace are ignored. The contents of any
+line containing a hash mark ('#') are ignored from that point until the end
+of that line. Users are encouraged to use hash marks to introduce comments
+in hex files. The author uses the extension '.hex' on such files. Examples
+can be found in the 'inhex' directory. Note that this format does _not_
+have an index (counter) value at the beginning of each line (like, for
+example, the hexdump utility outputs).
+.PP
+The hexadecimal format described in the previous paragraph can be converted
+to binary using the sg_decode_sense utility with these
+options: "\fI\-\-inhex=HFN \-\-nodecode \-\-write=WFN\fR". The input (in
+hex) is in the \fIHFN\fR file while the output is placed in the \fIWFN\fR
+file.
+.PP
+To convert a binary file into a hexadecimal form that can be given as input
+to various sg3_utils utilities, the sg_decode_sense utility can also be
+used with these options: "\fI\-\-binary=BFN \-\-nodecode \-HHH\fR" and the
+hex output will be sent to the console (stdout).
+.SH MICROCODE AND FIRMWARE
+There are two standardized methods for downloading microcode (i.e. device
+firmware) to a SCSI device. The more general way is with the SCSI WRITE
+BUFFER command, see the sg_write_buffer utility. SCSI enclosures have
+their own method based on the Download microcode control/status diagnostic
+page, see the sg_ses_microcode utility.
+.SH SCRIPTS, EXAMPLES and UTILS
+There are several bash shell scripts in the 'scripts' subdirectory that
+invoke compiled utilities (e.g. sg_readcap). Several of the scripts start
+with 'scsi_' rather than 'sg_'. One purpose of these scripts is to call the
+same utility (e.g. sg_readcap) on multiple devices. Most of the basic
+compiled utilities only allow one device as an argument. Some distributions
+install these scripts in a more visible directory (e.g. /usr/bin). Some of
+these scripts have man page entries. See the README file in the 'scripts'
+subdirectory.
+.PP
+There is some example C code plus examples of complex invocations in
+the 'examples' subdirectory. There is also a README file. The example C
+may be a simpler example of how to use a SCSI pass\-through in Linux
+than the main utilities (found in the 'src' subdirectory). This is due
+to the fewer abstraction layers (e.g. they don't worry the MinGW in
+Windows may open a file in text rather than binary mode).
+.PP
+Some utilities that the author has found useful have been placed in
+the 'utils' subdirectory.
+.SH DEBUGGING
+Each utility and most scripts have a \fI\-\-verbose\fR option (short
+form: \fI\-v\fR) that can be used multiple times to increase the verbosity
+of the output to aid debugging. Normal output (if any) is sent to stdout
+while verbose output (and error output) is sent to stderr. This may be
+important when the (normal output) of a utility is being piped to another
+command (e.g. the grep command to find a particular field in the output).
+.PP
+The Linux SCSI subsystem has a pseudo file for getting and changing the SCSI
+logging level: /proc/sys/dev/scsi/logging_level . The scsi_logging_level
+script in this package can be used to manipulate the logging level in a
+command line friendly way. See its manpage.
+.PP
+The logging level runs from 0 (no logging and the default) to 7 (lots of
+logging) and applies to all storage devices that use the SCSI subsystem.
+The logging output goes to "the log" which is often the /var/log/syslog
+file.
+.PP
+The Linux SCSI generic (sg) driver is often used under the utilities in
+this package. It uses a seldom (otherwise) used logging type of
+SCSI_LOG_TIMEOUT. An example of its use to turn on full debugging is:
+.PP
+  scsi_logging_level \-s \-T 7
+.PP
+To reduce the amount of output to only error paths, the following is
+suggested:
+.PP
+  scsi_logging_level \-s \-T 3
+.PP
+And to turn off logging in the sg driver:
+.PP
+  scsi_logging_level \-s \-T 0
+.PP
+For analyzing machine crashes associated with a SCSI command, nothing beats
+a real serial port. By "real" means that it is _not_ a USB serial port.
+The reason is that like SCSI, USB needs a functioning software stack within
+the OS kernel, the very thing that may be crippled during a machine crash.
+.PP
+Modern laptops do not have real serial ports and many server machines
+don't either (or it is an optional extra). In Linux the netconsole module
+does a pretty good job by sending log entries to another machine (on the
+same sub\-net)) using the UDP ("fire and forget") network protocol .
+.SH WEB SITE
+There is a web page discussing this package at
+https://sg.danny.cz/sg/sg3_utils.html . The device naming used by this
+package on various operating systems is discussed at:
+https://sg.danny.cz/sg/device_name.html . There is a git code mirror at
+https://github.com/hreinecke/sg3_utils . The principle code repository
+uses subversion and is on the author's equipment. The author keeps track
+of this via the subversion revision number which is an ascending integer
+(currently at 922 for this package). The github mirror gets updated
+periodically from the author's repository. Depending on the time of
+update, the above Downloads section at sg.danny.cz may be more up to
+date than the github mirror.
+.SH AUTHORS
+Written by Douglas Gilbert. Some utilities have been contributed, see the
+CREDITS file and individual source files (in the 'src' directory).
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 1999\-2022 Douglas Gilbert
+.br
+Some utilities are distributed under a GPL version 2 license while others,
+usually more recent ones, are under a BSD\-2\-Clause license. The files
+that are common to almost all utilities and thus contain the most reusable
+code, namely sg_lib.[hc], sg_cmds_basic.[hc] and sg_cmds_extra.[hc] are
+under a BSD\-2\-Clause license. There is NO warranty; not even for
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg3_utils_json,sg_decode_sense(sg3_utils), sdparm(sdparm), ddpt(ddpt),
+.B lsscsi(lsscsi), dmesg(1), mt(1)
+.br
+The format of this section is: <utility_name>(<package_containing_utility>)
+or <utility_name>(<manpage_section_number_containing_utility>) .
diff --git a/doc/sg3_utils_json.8 b/doc/sg3_utils_json.8
new file mode 100644
index 0000000..e734b4a
--- /dev/null
+++ b/doc/sg3_utils_json.8
@@ -0,0 +1,296 @@
+.TH SG3_UTILS_JSON "8" "November 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg3_utils_json \- JSON output for some sg3_utils utilities
+.SH SYNOPSIS
+.B sg_*
+\fI\-\-json[=JO]\fR [\fIOTHER_OPTIONS\fR] [\fIDEVICE\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+sg3_utils is a package of utilities that send SCSI commands to the given
+\fIDEVICE\fR via a SCSI pass through interface provided by the host
+operating system. Some utilities, mainly those decoding structured data
+returned by SCSI commands (e.g. sg_vpd) can optionally provide JSON
+output, rather than simple, human-readable output. The default remains
+human-readable output.
+.PP
+JavaScript Object Notation (JSON) is an open standard file format that can be
+used for data exchange between programs including across a network. See
+https://en.wikipedia.org/wiki/JSON . JSON comes in many flavours and this one
+uses the json-builder C implementation found at
+https://github.com/json-parser/json-builder which implements four simple JSON
+data types: string, integer, boolean and null. Its other data types are JSON
+object and JSON array.
+.PP
+This project uses the "snake_case" convention for JSON object names: all in
+lower case letters or numerals with individual words joined with a single
+underscore (e.g. "starting_lba"). There should be no leading or trailing
+underscore characters. The json-builder library uses the
+SPDX\-License\-Identifier: BSD\-2\-Clause which is the same license as the
+bulk of the utilities in the sg3_utils package.
+.PP
+The json-builder library is relatively lightweight (700 lines of C code) and
+is "hidden" fully within the sg3_utils library so that its function interface
+and data types are not available (directly) to the utilities in the sg3_utils
+package. That is why the json-builder interface (a file named
+sg_json_builder.h) is in the lib directory and not in the include directory.
+As presented on github, json-builder shares some header files with its
+companion json-parser. The author has modified the json-builder header to
+include what is needed from the json-parser header so that only the builder
+and not the parser are built. The parser could be added later, but currently
+there seems to be no need for it.
+.PP
+The user interface to JSON functionality in the sg3_utils package is heavily
+based on what has been done by Christian Franke and others in smartctl, a
+utility in the smartmontools package for getting S.M.A.R.T. information
+from disks (and other storage devices).
+.PP
+This manpage discusses the \fI\-\-json\fR option which may or may not itself
+have an optional argument. In its shorter form it may either be \fI\-j\fR or
+\fI\-J\fR (lower case preferred if not already in use). The shorter form may
+also take an argument but there must not be a space (or whitespace) between
+\fI\-j\fR and that argument.
+.SH ENVIRONMENT VARIABLES
+The SG3_UTILS_JSON_OPTS environment variable allows the user to override the
+default values of the \fIJO\fR settings. Those settings can again be overridden
+by the command line \fI\-\-json[=JO]\fR option. If the string associated with
+SG3_UTILS_JSON_OPTS cannot be parsed this error message is sent to
+stderr: "error parsing SG3_UTILS_JSON_OPTS environment variable, ignore".
+.SH OPTIONS
+Since the argument to \fI\-\-json[=JO]\fR is optional, in the shorter form
+there can be no space(s) between the option and its argument.
+.TP
+\fB\-j[JO]\fR, \fB\-\-json\fR\fI[=JO]\fR
+\fIJO\fR is a string of 0 or more characters whose order is not significant
+apart from the negation characters ('\-' is preferred). The negation character
+must appear immediately before the (boolean) feature it is toggling.
+.br
+In the short form the option letter may be other than \fI\-j\fR if that letter
+has already been used (\fI\-J\fR is preferred next). For example the sg_ses
+utility uses \fI\-j\fR for its "join" operation. Also since the argument to
+the short form option is itself optional, there can be no space between the
+short form option and \fIJO\fR, if it is given. To make this read a little
+better on the command line, "=" may be first character of \fIJO\fR, so for
+example, this is valid '\-j=av'.
+.SH JSON CONTROL CHARACTERS
+Each \fIJO\fR string is made up of zero or more of the following JSON control
+characters.
+.TP
+\fB0\fR
+If pretty printing JSON output, tab to 2 spaces.
+.TP
+\fB2\fR
+If pretty printing JSON output, tab to 2 spaces.
+.TP
+\fB4\fR
+If pretty printing JSON output, tab to 4 spaces.
+.br
+This is the default tab setting for pretty printing JSON.
+.TP
+\fB8\fR
+If pretty printing JSON output, tab to 8 spaces.
+.TP
+\fB=\fR
+does nothing. May appear as first character of \fIJO\fR. This character is
+defined to make the short option form look better (e.g. '\-j=av').
+.TP
+\fB\-\fR
+negation character. Toggles the (boolean) sense of the following control
+character.
+.TP
+\fBe\fR
+this is a boolean control character for "exit status". If active an "exit
+status" field is placed at the end of the JSON output. The integer value
+of this field is the Unix exit status value that is return to the operating
+system when this utility exits. The value of 0 is a good exit (i.e. no
+errors detected).
+.br
+This boolean control character is default on (true).
+.TP
+\fBh\fR
+this is a boolean control character for "hexadecimal". Many values associated
+with SCSI are best (or at least historically) viewed in hexadecimal while
+JSON output prefers decimal integers (assumed to have a maximum size of 64
+bits, signed). The maximum size of most SCSI fields is 64 bit _unsigned_ .
+Also some SCSI fields are masks which are best viewed in hex. When this
+control character is active most (non\-trivial) fields that have an integer
+value instead receive a a sub\-object containing at least a "i" field with
+the integer value and a "hex" field with the corresponding hex value in a
+JSON string. That hex string has no hex decorations (i.e. no leading '0x'
+nor trailing 'h').
+.br
+This boolean control character is default off (false).
+.TP
+\fBk\fR
+this is a boolean control character for finer control of non\-pretty printed
+JSON output. If the 'p' control character is set on (true) then this option
+has no effect.
+.br
+If the 'p' control character is set off (false) and this control character is
+set off (false) then the single line JSON output contains some spaces for
+readability. If the 'p' control character is set off (false) and this control
+character is set on (true) then the JSON single line JSON output is "packed"
+removing all unnecessary spaces.
+.br
+This boolean control character is default off (false).
+.TP
+\fBl\fR
+this is a boolean control character to control whether lead\-in fields are
+output. Lead\-in fields are at the start of the JSON output and include
+json_format_version and utility_invoked sub\-objects. The utility_invoked
+sub\-object includes name, version_date string fields and an JSON array
+named argv with an entry for each command line argument. If the \fIo\fR
+control character is also active, then if available, the non_JSON output
+is placed in and array called output with one element per line
+of 'normal' output.
+.br
+This boolean control character is default on (true).
+.TP
+\fBn\fR
+this is a boolean control character for "name_extra". It is used to provide
+additional information about the name it is a sub\-object of. The most
+common usage is to spell out an abbreviated name (e.g. a T10 name like "SKSV"
+to "Sense Key Specific Valid"). Another use is to note that a T10 field is
+obsolete and in which T10 standard it first became obsolete. Also if the
+named field's value is a physical quantity where the unit is unclear (e.g. a
+timeout) then "name_extra" can state that (e.g. "unit: millisecond").
+Only some fields have associated "name_extra" data.
+.br
+This boolean control character is default off (false).
+.TP
+\fBo\fR
+this is a boolean control character to control whether normal (i.e.
+non\-JSON) lines of output are placed in a JSON array (one element per
+line of normal output) within the utility_invoked subject (see control
+character \fIl\fR). This control character is active even if the
+\fIl\fR control character is negated).
+.br
+This boolean control character is default off (false).
+.TP
+\fBp\fR
+this boolean control character controls whether the JSON output
+is "pretty printed" or sent in a relatively compact stream suitable
+for more efficient transmission over a communications channel.
+.br
+The pretty printed form of output has one JSON name with its associated
+integer, string or boolean value per line; and one array element per line.
+JSON objects and arrays that have an associated JSON object as their value,
+have their name on a separate line. These lines are indented with the
+current tab setting to indicate the level of nesting. Basically the pretty
+printed form is for human consumption.
+.br
+There are two forms of non\-pretty printed output, see the "packed" control
+character ['k'].
+.br
+This boolean control character is default on (true).
+.TP
+\fBs\fR
+this boolean control character controls whether T10 field values that have
+a defined meaning are broken out with an added JSON sub\-object usually
+named "meaning". When active the field name has a sub\-object that contains
+at least an "i" field with the integer value of the field and a JSON string
+object, usually named "meaning", with a string that corresponds to the T10
+defined meaning of the value in the "i" field.
+.br
+This boolean control character is default on (true).
+.TP
+\fBv\fR
+this is an integer control character that controls the amount of debug output.
+It can be given multiple times to increase the level of JSON debug
+verbosity in the output.
+.br
+Note that this verbose control character is JSON specific while the
+\fI\-\-verbose\fR option (short form: fI\-v\fR often repeated: fI\-vvv\fR) that
+most utilities support is more general.
+.br
+This integer control character is set to 0 by default.
+.SH OUTPUT PROCESSING
+The default remains the same for all utilities that support the
+\fI\-\-json\fR option, namely the decoded information is sent to stdout in
+human readable form. Errors are reported to stderr and may cause the early
+termination of a utility (e.g. command line option syntax error).
+.PP
+When the \fI\-\-json\fR option is given and no errors are detected, then
+only JSON is normally sent to stdout. As the SCSI response is parsed, a JSON
+representation is built as a tree in memory. After all other actions (perhaps
+apart from the final exit status report) that JSON tree is "dumped" to
+stdout. This means if there is any non-JSON output sent to stdout that
+it will appear _before_ the JSON output.
+.PP
+If the 'o' control character is in the \fIJO\fR argument to the
+\fI\-\-json\fR option, then the former "human readable" output is placed in
+a JSON array named "output" within a JSON object named "utility_invoked".
+Each line of the former "human readable" output is placed in its own
+element of the JSON array named "output".
+.PP
+A JSON tree is built in memory as the utility parses the data returned
+from the SCSI device (e.g. sg_vpd parsing a VPD page returned from a
+SCSI INQUIRY command). SCSI "list"s become JSON named arrays (e.g. in
+the Device Identification VPD page there is a "Designation descriptor
+list" that becomes a JSON array named "designation_descriptor_list").
+.PP
+At the completion of the utility that JSON tree is "measured" taking into
+account the form of output (i.e. pretty-printed, single line or packed single
+line). For the pretty-printed JSON output, the size of each indentation in
+spaces is also given (i.e. the tab width). The JSON is then output to a
+single C string, then sent to stdout. If a NULL character (ASCII zero and C
+string terminator) somehow finds its way into a field that should (according
+to the spec) be space padded, then the JSON output may appear truncated.
+.PP
+Note that this JSON processing means that if a utility is aborted for whatever
+reason then no JSON output will appear. With the normal, human readable output
+processing, some output may appear before the utility aborts in such bad
+situations.
+.SH INTERACTION WITH OTHER OPTIONS
+As stated above, the default output is in human readable form using 7 bit
+ASCII. The \fI\-\-json[=JO]\fR option is designed to be an alternative to that
+human readable form. There are other alternative output formats such as the
+response output as a hexadecimal sequence of bytes or in "raw" binary output;
+both of those take precedence over the \fI\-\-json[=JO]\fR option. Other
+specialized output format (e.g. 'sg_inq \-\-export') will usually take
+precedence over JSON output.
+.PP
+When the \fI\-\-raw\fR option is used together with the \fI\-\-inhex=FN\fR
+option only the data input to the utility is interpreted as binary. So the
+output format defaults to human readable form and thus can be changed to
+JSON if the \fI\-\-json[=JO]\fR option is also used.
+.PP
+There is typically only one form of JSON output so options like
+\fI\-\-brief\fR and \fI\-\-quiet\fR are ignored in the JSON output. In some
+cases (i.e 'sg_inq \-\-descriptors') the JSON output is expanded.
+.SH ERRORS
+No attempts have been made to translate errors into JSON form, apart from the
+final "exit_status" JSON object where a value of 0 means "no errors". Exit
+status values indicating a problem range from 1 to 255.
+.PP
+The sg_decode_sense utility will parse SCSI sense data into JSON form if
+requested. So if another utility is failing with a sense data report (most
+often seen when the \fI\-\-verbose\fR option is used). That sense data (in
+hex bytes) could be cut\-and\-paste onto the command line
+following 'sg_decode_sense \-j ' which should then render that sense data
+in JSON.
+.PP
+Otherwise, when a error is detected while JSON output is selected, the error
+message is sent to stderr in human readable form. Typically once an error is
+detected the utility will exit, first dumping the JSON in\-memory tree as
+discussed above and a non\-zero exit status will be set. The JSON output will
+be well formed but missing any fields or list elements following the point
+that the error was detected.
+.PP
+The summary is that when JSON output is selected and an error occurs each
+utility will process the error the same way as it would if JSON output had
+not been selected. In all cases error messages, in human readable form,
+are sent to stderr.
+.SH AUTHORS
+Written by Douglas Gilbert. Some utilities have been contributed, see the
+CREDITS file and individual source files (in the 'src' directory).
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2 or the BSD\-2\-Clause
+license. There is NO warranty; not even for MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg3_utils(sg3_utils), smartctl(smartmontools)
diff --git a/doc/sg_bg_ctl.8 b/doc/sg_bg_ctl.8
new file mode 100644
index 0000000..98d56b4
--- /dev/null
+++ b/doc/sg_bg_ctl.8
@@ -0,0 +1,72 @@
+.TH SG_BG_CTL "8" "May 2016" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_bg_ctl \- send SCSI BACKGROUND CONTROL command
+.SH SYNOPSIS
+.B sg_bg_ctl
+[\fI\-\-ctl=CTL\fR] [\fI\-\-help\fR] [\fI\-\-time=TN\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI BACKGROUND CONTROL command to the \fIDEVICE\fR. This command
+was first found in the SBC\-4 draft standard revision 8 (sbc4r08.pdf). It can
+be used to start and stop 'advanced background operations' on the
+\fIDEVICE\fR. Only resource or thin provisioned devices (logical units which
+are typically (solid state) disks) support this command. Those advanced
+background operations often include garbage collection type operations which
+may degrade the disk's performance while they are being performed.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-c\fR, \fB\-\-ctl\fR=\fICTL\fR
+\fICTL\fR is the value placed in the BO_CTL field of the BACKGROUND CONTROL
+command (cdb). It is a two bit field so has 4 variants: 0 does not change
+the host initiated advanced background operations; 1 starts these operations;
+2 stops these operations and 3 is reserved. The default value is 0.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-t\fR, \fB\-\-time\fR=\fITN\fR
+\fITN\fR is a maximum time (with a unit of 100 ms or 1/10 second) that
+advanced background operations can occur. This value is ignored if the
+\fICTL\fR argument is other than 1. The default value is 0 which means there
+is no maximum time limit. Only values 0 to 255 (which is 25.5 seconds) can
+be given. This value is place in the BO_TIME field of the BACKGROUND CONTROL
+command.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+According to T10, support for 'background control operations' is indicated by
+the BOCS bit being set in the Block device characteristics VPD page [0xb1].
+The setting of the BOCS bit can be checked with the sg_vpd and sdparm
+utilities (and it is read only). There is a Background operations control
+mode page [0xa, 0x6] with a BO_MODE field for modifying the action of this
+operation. The BO_MODE field can be accessed and possibly modified with the
+sdparm utility. The BO_STATUS field can be found in the Background operation
+log page [0x15, 0x2] and that can be viewed with the sg_logs utility.
+.PP
+The current draft describing this area is SBC\-4 revision 10 (sbc4r10.pdf)
+in clause 4.33 . That contains the following example of a background
+operation: "Advanced background operation may include NAND block erase
+operations, media read operations, and media write operations (e.g.,
+garbage collection), which may impact response time for normal read requests
+or write requests from the application client."
+.SH EXIT STATUS
+The exit status of sg_bg_ctl is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2016 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_vpd,sg_logs(sg3_utils); sdparm(sdparm)
diff --git a/doc/sg_compare_and_write.8 b/doc/sg_compare_and_write.8
new file mode 100644
index 0000000..83ea2ba
--- /dev/null
+++ b/doc/sg_compare_and_write.8
@@ -0,0 +1,244 @@
+.TH "COMPARE AND WRITE" "8" "April 2021" "sg3_utils\-1.47" SG3_UTILS
+.SH NAME
+sg_compare_and_write \- send the SCSI COMPARE AND WRITE command
+.SH SYNOPSIS
+.B sg_compare_and_write
+[\fI\-\-dpo\fR] [\fI\-\-fua\fR] [\fI\-\-fua_nv\fR] [\fI\-\-grpnum=GN\fR]
+[\fI\-\-help\fR] \fI\-\-in=IF\fR [\fI\-\-inw=WF\fR] \fI\-\-lba=LBA\fR
+[\fI\-\-num=NUM\fR] [\fI\-\-quiet\fR] [\fI\-\-timeout=TO\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] [\fI\-\-wrprotect=WP\fR]
+[\fI\-\-xferlen=LEN\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+Send the SCSI COMPARE AND WRITE command to \fIDEVICE\fR. This utility fetches
+a compare buffer and a write buffer from either one or two files. If the
+\fI\-\-inw=WF\fR option is given then the compare buffer is fetched from the
+file indicated by the \fI\-\-in=IF\fR while the write buffer is fetched from
+the file indicated by the \fI\-\-inw=WF\fR. If the \fI\-\-inw=WF\fR option is
+not given then the concatenated compare and write buffers are fetched from the
+file indicated by the \fI\-\-in=IF\fR option.
+.PP
+Those buffers are expected to each contain \fINUM\fR blocks of data. The
+compare starts at logical block address \fILBA\fR on the \fIDEVICE\fR and if
+the comparison fails (i.e. the provided compare buffer does not equal the data
+at \fILBA\fR on the \fIDEVICE\fR) then the COMPARE AND WRITE command finishes
+with a sense key of MISCOMPARE. In this case this utility will complete and
+set an exit status of 14 (which happens to be the sense key value of
+MISCOMPARE).
+.PP
+If the comparison succeeds then the provided write buffer is stored starting
+at \fILBA\fR for \fINUM\fR blocks on the \fIDEVICE\fR.
+.PP
+The actual number of bytes transferred in the data\-out buffer of the COMPARE
+AND WRITE command may need to be given by the user with the
+\fI\-\-xferlen=LEN\fR option. \fILEN\fR defaults to (2 * \fINUM\fR * 512)
+which is 1024 for the default \fINUM\fR of 1. If the block size is other than
+512 then the user will need to use \fI\-\-xferlen=LEN\fR option.  If
+protection information is given (indicated by a value of \fIWP\fR other than
+0 (the default)) then for a \fINUM\fR of 1 \fILEN\fR should be 1040 . Note
+that the SCSI READ CAPACITY command is not performed by this utility (e.g.
+to find the block size).
+.PP
+The T10 definition of the SCSI COMPARE AND WRITE command requires that the
+\fIDEVICE\fR implement the compare and optional write as an uninterrupted
+series of actions. Depending on some other \fIDEVICE\fR settings a
+verify operation may occur prior to the compare.
+.PP
+When a mismatch occurs between the compare buffer and the blocks starting
+at \fILBA\fR read from the \fIDEVICE\fR the sense buffer containing the
+MISCOMPARE sense key causes several messages to be sent to stderr (including
+the offset of the first byte mismatch). To suppress these messages use the
+\fI\-\-quiet\fR option. With or without the \fI\-\-quiet\fR option the exit
+status will be set to 14.
+.PP
+This command is defined in SBC\-3 whose most recent revision is 36. SBC\-3
+and other SCSI documents can be found at https://www.t10.org .
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long option name.
+.TP
+\fB\-d\fR, \fB\-\-dpo\fR
+Set the DPO bit in the COMPARE AND WRITE CDB
+.TP
+\fB\-f\fR, \fB\-\-fua\fR
+Set the FUA bit in the COMPARE AND WRITE CDB
+.TP
+\fB\-F\fR, \fB\-\-fua_nv\fR
+Set the FUA_NV bit in the COMPARE AND WRITE CDB. This bit was removed in
+SBC\-3 revision 35d and its position marked as "reserved".
+.TP
+\fB\-g\fR, \fB\-\-grpnum\fR=\fIGN\fR
+where \fIGN\fR is the value to be placed in the group number field in the
+COMPARE AND WRITE CDB.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIIF\fR
+read data (binary) from file named \fIIF\fR. This will either be the combined
+compare and write buffers (when the \fI\-\-inw=WF\fR option is not given) or
+just the compare buffer (when the \fI\-\-inw=WF\fR option is given). If
+\fIIF\fR is '\-' then stdin (e.g. a pipe) is read.
+.TP
+\fB\-C\fR, \fB\-\-inc\fR=\fIIF\fR
+The same as the \fB\-\-in\fR option.
+.TP
+\fB\-D\fR, \fB\-\-inw\fR=\fIWF\fR
+read data (binary) from file named \fIWF\fR. This will the write buffer
+that will become the second half of the data\-out buffer sent to the
+\fIDEVICE\fR associated with the COMPARE AND WRITE command. Note that
+when this option is given then the \fI\-\-in=IF\fR is expected to hold
+the associated compare buffer.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR is the logical block address to start the COMPARE AND WRITE
+command. Assumed to be in decimal unless prefixed with '0x' or has a
+trailing 'h'.
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fINUM\fR
+where \fINUM\fR is the number of blocks, starting at \fILBA\fR, to read
+and compare with the verify instance. And given a match, the \fINUM\fR of
+blocks to write starting \fILBA\fR. The default value for \fINUM\fR is 1.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+suppress the sense buffer messages associated with a MISCOMPARE sense key
+that would otherwise be sent to stderr. Still set the exit status to 14
+which is the sense key value indicating a MISCOMPARE.
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fITO\fR
+where \fITO\fR is the command timeout value in seconds. The default value is
+60 seconds. If \fINUM\fR is large (or zero) a WRITE SAME command may require
+considerably more time than 60 seconds to complete.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the degree of verbosity (debug messages).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+output version string then exit.
+.TP
+\fB\-w\fR, \fB\-\-wrprotect\fR=\fIWP\fR
+set the WRPROTECT field in the cdb to \fIWP\fR. The default value is 0 which
+implies no protection information is sent (along with the user data) by this
+utility.
+.TP
+\fB\-x\fR, \fB\-\-xferlen\fR=\fILEN\fR
+where \fILEN\fR is the data out buffer length in byte. It defaults to (2 *
+\fINUM\fR * 512) bytes. If the \fIDEVICE\fR block size is other than 512
+bytes or \fIWP\fR is non\-zero (implying additional protection information)
+then this default will be incorrect; the use must supply the correct value
+for \fILEN\fR
+.SH NOTES
+Various numeric arguments (e.g. \fILBA\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.SH EXAMPLES
+Before overwriting the first two blocks of whatever (SCSI) storage device
+that is chosen, take a small backup. The logical block size is assumed to
+be 512 bytes. Take a copy (in backup01.bin) of the first two blocks::
+.PP
+  # sg_dd if=/dev/sg1 bs=512 of=backup01.bin count=2
+  2+0 records in
+  2+0 records out
+.PP
+.B WARNING:
+if /dev/sg1 corresponds to a disk on your system that contains currently
+mounted file systems, do _not_ continue. If you can, unmount all file
+systems on that disk. If that is not possible, use another disk with no
+mounted file systems on it. In Linux the scsi_debug driver is a good
+candidate for experimentation.
+.PP
+Now fill the first block with 0xff bytes:
+.PP
+  # sg_dd iflag=ff bs=512 of=/dev/sg1 count=1
+  1+0 records in
+  1+0 records out
+.PP
+and the second block with 0x0 bytes:
+.PP
+  # sg_dd iflag=00 bs=512 seek=1 of=/dev/sg1 count=1
+  1+0 records in
+  1+0 records out
+.PP
+Now copy those two blocks into a file:
+.PP
+  # sg_dd if=/dev/sg1 bs=512 of=ff00.bin count=2
+  2+0 records in
+  2+0 records out
+.PP
+Now we can do a compare and write command. It is told to compare the first
+block (i.e. LBA 0) with the first block in the given file (i.e. ff00.bin).
+If they are equal (they should be both full of 0xff bytes). Since the
+compare succeeds, it will write the second block in ff00.bin over LBA 0:
+.PP
+  # sg_compare_and_write \-\-in=ff00.bin \-\-lba=0 \-\-num=1 /dev/sg1
+
+.PP
+No news is good news. Now if we do that command again:
+.PP
+  # sg_compare_and_write \-\-in=ff00.bin \-\-lba=0 \-\-num=1 /dev/sg1
+  Miscompare at byte offset: 0 [0x0]
+  sg_compare_and_write failed: Miscompare
+.PP
+This is expected. The first sg_compare_and_write ended up writing 0x0 bytes
+over LBA 0x0. The second sg_compare_and_write command compares LBA 0x0 with
+0xff bytes and fails immediately (i.e. at byte offset: 0). Now we will
+overwrite the first 3 bytes of ff00.bin with 0x0:
+.PP
+  # sg_dd bs=1 iflag=00 of=ff00.bin count=3
+  3+0 records in
+  3+0 records out
+.PP
+Notice the 'bs=1' operand. The dd utility (and thus sg_dd) is very useful for
+doing small binary edits on a file. Now if we do that sg_compare_and_write
+again, it still fails but with a small difference:
+.PP
+  # sg_compare_and_write \-\-in=ff00.bin \-\-lba=0 \-\-num=1 /dev/sg1
+  Miscompare at byte offset: 3 [0x3]
+  sg_compare_and_write failed: Miscompare
+.PP
+So the bytes at offset 0, 1, and 2 compared equal but not the byte at
+offset 3. The SCSI COMPARE AND WRITE will stop on the first micompared
+byte.
+.SH EXIT STATUS
+The exit status of sg_compare_and_write is 0 when it is successful. If the
+compare step fails then the exit status is 14. For other exit status values
+see the EXIT STATUS section in the sg3_utils(8) man page.
+.PP
+Earlier versions of this utility set an exit status of 98 when there was a
+MISCOMPARE.
+.SH AUTHORS
+Written by Shahar Salzman. Maintained by Douglas Gilbert. Additions by
+Eric Seppanen.
+.SH "REPORTING BUGS"
+Report bugs to shahar.salzman@kaminario.com or dgilbert@interlog.com
+.SH COPYRIGHT
+Copyright \(co 2012\-2020 Kaminario Technologies LTD
+.br
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+.br
+* Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+.br
+* Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+.br
+* Neither the name of the <organization> nor the names of its contributors may
+be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+.br
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL Kaminario Technologies LTD BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+.SH "SEE ALSO"
+.B sg_xcopy, sg_receive_copy_results(sg3_utils)
diff --git a/doc/sg_copy_results.8 b/doc/sg_copy_results.8
new file mode 100644
index 0000000..2e2203b
--- /dev/null
+++ b/doc/sg_copy_results.8
@@ -0,0 +1,126 @@
+.TH SG_COPY_RESULTS "8" "September 2014" "sg3_utils\-1.40" SG3_UTILS
+.SH NAME
+sg_copy_results \- send SCSI RECEIVE COPY RESULTS command (XCOPY related)
+.SH SYNOPSIS
+.B sg_copy_results
+[\fI\-\-failed\fR|\fI\-\-params\fR|\fI\-\-receive\fR|\fI\-\-status\fR]
+[\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-list_id=ID\fR] [\fI\-\-readonly\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] [\fI\-\-xfer_len=BTL\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility is designed to query the status of the SCSI Extended
+Copy (XCOPY) facility (see SPC\-3 revision 23 sections 6.3 and 6.17), present
+in some modern storage arrays. This utility sends a SCSI RECEIVE COPY
+RESULTS command to the given \fIDEVICE\fR and displays the response.
+.PP
+During the draft stages of SPC\-4 the T10 committee has expanded the XCOPY
+command so that it now has two variants: "LID1" (for a List Identifier
+length of 1 byte) and "LID4" (for a List Identifier length of 4 bytes).
+This utility supports the older, LID1 variant which is also found in SPC\-3
+and earlier. While the LID1 variant in SPC\-4 is command level (binary)
+compatible with XCOPY as defined in SPC\-3, some of the command naming has
+changed. This utility uses the older, SPC\-3 XCOPY names.
+.PP
+The command has four distinct modes of operation, distinguished by
+the service action field:
+.TP
+\fBCOPY STATUS  [SPC\-4: RECEIVE COPY STATUS(LID1)]\fR
+Displays the current status of the EXTENDED COPY command identified by
+the list id field.
+.TP
+\fBRECEIVE DATA  [SPC\-4: RECEIVE COPY DATA(LID1)]\fR
+Return the held data read by the EXTENDED COPY command identified by
+the list id field. This option is only meaningful if the respective
+segment descriptor are supported.
+.TP
+\fBOPERATING PARAMETERS  [SPC\-4: RECEIVE COPY OPERATING PARAMETERS]\fR
+Return copy manager operating parameters. This option is also useful
+to determine if the SCSI Extended Copy facility is supported.
+.TP
+\fBFAILED SEGMENT DETAILS  [SPC\-4: RECEIVE COPY FAILURE DETAILS(LID1)]\fR
+Return copy target device sense data and other information about any
+failed segments.
+
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-f\fR, \fB\-\-failed\fR
+sets the service action field to FAILED SEGMENT DETAILS [4].
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+prints out the response buffer in hex.
+.TP
+\fB\-l\fR, \fB\-\-list_id\fR=\fIID\fR
+sets the list identifier field to \fIID\fR (default: 0).
+.TP
+\fB\-p\fR, \fB\-\-params\fR
+sets the service action field to OPERATING PARAMETERS [3].
+This is the default.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-r\fR, \fB\-\-receive\fR
+sets the service action field to RECEIVE DATA [1].
+.TP
+\fB\-s\fR, \fB\-\-status\fR
+sets the service action field to COPY STATUS [0].
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-x\fR, \fB\-\-xfer_len\fR=\fIBTL\fR
+sets the allocation length field to \fIBTL\fR. It is the byte transfer
+length and is the maximum (byte) size of the response. \fIBTL\fR must be
+less than 10000 and defaults to 520.
+.SH NOTES
+Decoding of \fIRECEIVE DATA\fR service action is not implemented.
+.PP
+In a similar way the functionality of sg_xcopy has been ported to the
+more general ddpt utility (and package), the functionality of this utility
+has been ported to the ddptctl utility.
+.SH EXAMPLES
+Query the operating parameters for a device:
+.PP
+# sg_copy_results \-p /dev/sdo
+.br
+Receive copy results (report operating parameters):
+    Supports no list identifier: no
+    Maximum target descriptor count: 2
+    Maximum segment descriptor count: 1
+    Maximum descriptor list length: 92 bytes
+    Maximum segment length: 33553920 bytes
+    Inline data not supported
+    Held data limit: 0 bytes
+    Maximum stream device transfer size: 0 bytes
+    Total concurrent copies: 0
+    Maximum concurrent copies: 255
+    Data segment granularity: 512 bytes
+    Inline data granularity: 1 bytes
+    Held data granularity: 1 bytes
+    Implemented descriptor list:
+        Segment descriptor 0x02: Copy from block device to block device
+        Target descriptor 0xe4: Identification descriptor
+
+.SH EXIT STATUS
+The exit status of sg_copy_results is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2012\-2014 Hannes Reinecke and Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_xcopy(sg3_utils), ddpt,ddptctl(ddpt)
diff --git a/doc/sg_dd.8 b/doc/sg_dd.8
new file mode 100644
index 0000000..df4e94c
--- /dev/null
+++ b/doc/sg_dd.8
@@ -0,0 +1,614 @@
+.TH SG_DD "8" "August 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_dd \- copy data to and from files and devices, especially SCSI
+devices
+.SH SYNOPSIS
+.B sg_dd
+[\fIbs=BS\fR] [\fIconv=CONV\fR] [\fIcount=COUNT\fR] [\fIibs=BS\fR]
+[\fIif=IFILE\fR] [\fIiflag=FLAGS\fR] [\fIobs=BS\fR] [\fIof=OFILE\fR]
+[\fIoflag=FLAGS\fR] [\fIseek=SEEK\fR] [\fIskip=SKIP\fR] [\fI\-\-help\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR]
+.PP
+[\fIblk_sgio=\fR{0|1}] [\fIbpt=BPT\fR] [\fIcdbsz=\fR{6|10|12|16}]
+[\fIcdl=CDL\fR] [\fIcoe=\fR{0|1|2|3}] [\fIcoe_limit=CL\fR]
+[\fIdio=\fR{0|1}] [\fIodir=\fR{0|1}] [\fIof2=OFILE2\fR]
+[\fIretries=RETR\fR] [\fIsync=\fR{0|1}] [\fItime=\fR{0|1}[,TO]]
+[\fIverbose=VERB\fR] [\fI\-\-dry\-run\fR] [\fI\-\-progress\fR]
+[\fI\-\-verify\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Copy data to and from any files. Specialized for "files" that are Linux SCSI
+generic (sg) devices, raw devices or other devices that support the SG_IO
+ioctl (which are only found in the lk 2.6 series). Similar syntax and
+semantics to
+.B dd(1)
+command.
+.PP
+The first group in the synopsis above are "standard" Unix
+.B dd(1)
+operands. The second group are extra options added by this utility.
+Both groups are defined below.
+.PP
+When the \fI\-\-verify\fR option is given, then the read side is the
+same but the on the write side, the WRITE SCSI command is replaced by
+the VERIFY SCSI command. If any VERIFY commands yields a sense key of
+MISCOMPARE then the verify operation will stop. The \fI\-\-verify\fR
+option can only be used when \fIOFILE\fR is either a sg device or
+a block device with oflag=sgio also given. When the \fI\-\-verify\fR
+option is used, this utility works in a similar fashion to the Unix
+cmp(1) command.
+.PP
+This utility is only supported on Linux whereas most other utilities in the
+sg3_utils package have been ported to other operating systems. A utility
+called "ddpt" has similar syntax and functionality to sg_dd. ddpt drops some
+Linux specific features while adding some other generic features. This allows
+ddpt to be ported to other operating systems.
+.SH OPTIONS
+.TP
+\fBblk_sgio\fR={0|1}
+when set to 0, block devices (e.g. /dev/sda) are treated like normal
+files (i.e.
+.B read(2)
+and
+.B write(2)
+are used for IO). When set to 1, block devices are assumed to accept the
+SG_IO ioctl and SCSI commands are issued for IO. This is only supported
+for 2.6 series kernels. Note that ATAPI devices (e.g. cd/dvd players) use
+the SCSI command set but ATA disks do not (unless there is a protocol
+conversion as often occurs in the USB mass storage class). If the input
+or output device is a block device partition (e.g. /dev/sda3) then setting
+this option causes the partition information to be ignored (since access
+is directly to the underlying device). Default is 0. See the 'sgio' flag.
+.TP
+\fBbpt\fR=\fIBPT\fR
+each IO transaction will be made using \fIBPT\fR blocks (or less if near
+the end of the copy). Default is 128 for logical block sizes less that 2048
+bytes, otherwise the default is 32. So for bs=512 the reads and writes
+will each convey 64 KiB of data by default (less if near the end of the
+transfer or memory restrictions). When cd/dvd drives are accessed, the
+logical block size is typically 2048 bytes and bpt defaults to 32 which
+again implies 64 KiB transfers. The block layer when the blk_sgio=1 option
+is used has relatively low upper limits for transfer sizes (compared
+to sg device nodes, see /sys/block/<dev_name>/queue/max_sectors_kb ).
+.TP
+\fBbs\fR=\fIBS\fR
+where \fIBS\fR
+.B must
+be the logical block size of the physical device (if either the input or
+output files are accessed via SCSI commands). Note that this differs from
+.B dd(1)
+which permits \fIBS\fR to be an integral multiple. Default is 512 which
+is usually correct for disks but incorrect for cdroms (which normally
+have 2048 byte blocks). For this utility the maximum size of each individual
+IO operation is \fIBS\fR * \fIBPT\fR bytes.
+.TP
+\fBcdbsz\fR={6|10|12|16}
+size of SCSI READ and/or WRITE commands issued on sg device
+names (or block devices when 'iflag=sgio' and/or 'oflag=sgio' is given).
+Default is 10 byte SCSI command blocks (unless calculations indicate
+that a 4 byte block number may be exceeded or \fIBPT\fR is greater than
+16 bits (65535), in which case it defaults to 16 byte SCSI commands).
+.TP
+\fBcdl\fR=\fICDL\fR
+allows setting of command duration limits. \fICDL\fR is either a single value
+or two values separated by a comma. If one value is given, it applies to both
+\fIIFILE\fR and \fIOFILE\fR (if they are pass\-through devices). If two
+values are given, the first applies to \fIIFILE\fR while the second applies
+to \fIOFILE\fR. The value may be from 0 to 7 where 0 is the default and means
+there are no command duration limits. Command duration limits are only
+supported by 16 byte READ and WRITE commands (plus READ(32), WRITE(32) and
+the WRITE SCATTERED command, bit they are used by this utility). If the
+cdbsz operand is not given and would have a value less than 16, then if
+\fICDL\fR is greater than 0, the cdbsz is increased to 16.
+.br
+Command duration limits can be accesses and changed in the Command duration
+limit A and B mode pages, plus the Command duration limit T2A and T2B mode
+pages. The sdparm utility may be used to access and change these mode pages.
+.TP
+\fBcoe\fR={0|1|2|3}
+set to 1 or more for continue on error ('coe'). Only applies to errors on sg
+devices or block devices with the 'sgio' flag set. Thus errors on other
+files will stop sg_dd. Default is 0 which implies stop on any error. See
+the 'coe' flag for more information.
+.TP
+\fBcoe_limit\fR=\fICL\fR
+where \fICL\fR is the maximum number of consecutive bad blocks stepped
+over (due to "coe>0") on reads before the copy terminates. This only
+applies when \fIIFILE\fR is accessed via the SG_IO ioctl. The default
+is 0 which is interpreted as no limit. This option is meant to stop
+the copy soon after unrecorded media is detected while still
+offering "continue on error" capability.
+.TP
+\fBconv\fR=\fBsparse\fR
+see the CONVERSIONS section below.
+.TP
+\fBcount\fR=\fICOUNT\fR
+copy \fICOUNT\fR blocks from \fIIFILE\fR to \fIOFILE\fR. Default is the
+minimum (of \fIIFILE\fR and \fIOFILE\fR) number of blocks that sg devices
+report from SCSI READ CAPACITY commands or that block devices (or their
+partitions) report. Normal files are not probed for their size. If
+\fIskip=SKIP\fR or \fIseek=SEEK\fR are given and the count is derived (i.e.
+not explicitly given) then the derived count is scaled back so that the
+copy will not overrun the device. If the file name is a block device
+partition and \fICOUNT\fR is not given then the size of the partition
+rather than the size of the whole device is used. If \fICOUNT\fR is not
+given (or \fIcount=\-1\fR) and cannot be derived then an error message is
+issued and no copy takes place.
+.TP
+\fBdio\fR={0|1}
+default is 0 which selects indirect (buffered) IO on sg devices. Value of 1
+attempts direct IO which, if not available, falls back to indirect IO and
+notes this at completion. If direct IO is selected and
+/sys/module/sg/parameters/allow_dio has the value of 0 then a warning is
+issued (and indirect IO is performed). For finer grain control
+use 'iflag=dio' or 'oflag=dio'.
+.TP
+\fBibs\fR=\fIBS\fR
+if given must be the same as \fIBS\fR given to 'bs=' option.
+.TP
+\fBif\fR=\fIIFILE\fR
+read from \fIIFILE\fR instead of stdin. If \fIIFILE\fR is '\-' then stdin
+is read. Starts reading at the beginning of \fIIFILE\fR unless \fISKIP\fR
+is given.
+.TP
+\fBiflag\fR=\fIFLAGS\fR
+where \fIFLAGS\fR is a comma separated list of one or more flags outlined
+below.  These flags are associated with \fIIFILE\fR and are ignored when
+\fIIFILE\fR is stdin.
+.TP
+\fBobs\fR=\fIBS\fR
+if given must be the same as \fIBS\fR given to 'bs=' option.
+.TP
+\fBodir\fR={0|1}
+when set to one opens block devices (e.g. /dev/sda) with the O_DIRECT
+flag. User memory buffers are aligned to the page size when set. The
+default is 0 (i.e. the O_DIRECT flag is not used). Has no effect on sg,
+normal or raw files. If blk_sgio is also set then both are honoured:
+block devices are opened with the O_DIRECT flag and SCSI commands are
+issued via the SG_IO ioctl.
+.TP
+\fBof\fR=\fIOFILE\fR
+write to \fIOFILE\fR instead of stdout. If \fIOFILE\fR is '\-' then writes
+to stdout.  If \fIOFILE\fR is /dev/null then no actual writes are performed.
+If \fIOFILE\fR is '.' (period) then it is treated the same way as
+/dev/null (this is a shorthand notation). If \fIOFILE\fR exists then it
+is _not_ truncated; it is overwritten from the start of \fIOFILE\fR
+unless 'oflag=append' or \fISEEK\fR is given.
+.TP
+\fBof2\fR=\fIOFILE2\fR
+write output to \fIOFILE2\fR. The default action is not to do this additional
+write (i.e. when this option is not given). \fIOFILE2\fR is assumed to be
+a normal file or a fifo (i.e. a named pipe). \fIOFILE2\fR is opened for
+writing, created if necessary, and closed at the end of the transfer. If
+\fIOFILE2\fR is a fifo (named pipe) then some other command should be
+consuming that data (e.g. 'md5sum OFILE2'), otherwise this utility will block.
+.TP
+\fBoflag\fR=\fIFLAGS\fR
+where \fIFLAGS\fR is a comma separated list of one or more flags outlined
+below.  These flags are associated with \fIOFILE\fR and are ignored when
+\fIOFILE\fR is /dev/null, '.' (period), or stdout.
+.TP
+\fBretries\fR=\fIRETR\fR
+sometimes retries at the host are useful, for example when there is a
+transport error. When \fIRETR\fR is greater than zero then SCSI READs and
+WRITEs are retried on error, \fIRETR\fR times. Default value is zero.
+.TP
+\fBseek\fR=\fISEEK\fR
+start writing \fISEEK\fR bs\-sized blocks from the start of \fIOFILE\fR.
+Default is block 0 (i.e. start of file).
+.TP
+\fBskip\fR=\fISKIP\fR
+start reading \fISKIP\fR bs\-sized blocks from the start of \fIIFILE\fR.
+Default is block 0 (i.e. start of file).
+.TP
+\fBsync\fR={0|1}
+when 1, does SYNCHRONIZE CACHE command on \fIOFILE\fR at the end of the
+transfer. Only active when \fIOFILE\fR is a sg device file name or a block
+device and 'blk_sgio=1' is given.
+.TP
+\fBtime\fR={0|1}[,\fITO\fR]
+when 1, times transfer and does throughput calculation, outputting the
+results (to stderr) at completion. When 0 (default) doesn't perform timing.
+.br
+If that value is followed by a comma, then \fITO\fR is the command timeout
+in seconds for SCSI READ, WRITE or VERIFY commands issued by this utility.
+The default is 60 seconds.
+.TP
+\fBverbose\fR=\fIVERB\fR
+as \fIVERB\fR increases so does the amount of debug output sent to stderr.
+Default value is zero which yields the minimum amount of debug output.
+A value of 1 reports extra information that is not repetitive. A value
+2 reports cdbs and responses for SCSI commands that are not repetitive
+(i.e. other that READ and WRITE). Error processing is not considered
+repetitive. Values of 3 and 4 yield output for all SCSI commands (and
+Unix read() and write() calls) so there can be a lot of output.
+This only occurs for scsi generic (sg) devices and block devices when
+the 'blk_sgio=1' option is set.
+.TP
+\fB\-d\fR, \fB\-\-dry\-run\fR
+does all the command line parsing and preparation but bypasses the actual
+copy or read. That preparation may include opening \fIIFILE\fR or
+\fIOFILE\fR to determine their lengths. This option may be useful for
+testing the syntax of complex command line invocations in advance of
+executing them.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs usage message and exits.
+.TP
+\fB\-p\fR, \fB\-\-progress\fR
+this option causes a progress report to be output every two minutes until
+the copy is complete. After the copy is complete a line with "completed"
+is printed to distinguish the final report from the prior progress reports.
+When used twice the progress report is every minute, when used three times
+the progress report is every 30 seconds.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+when used once, this is equivalent to \fIverbose=1\fR. When used
+twice (e.g. "\-vv") this is equivalent to \fIverbose=2\fR, etc.
+.TP
+\fB\-x\fR, \fB\-\-verify\fR
+do a verify operation (like Unix command cmp(1)) rather than a copy. Cannot
+be used with "oflag=sparse". \fIof=OFILE\fR must be given and \fIOFILE\fR
+must be an sg device or a block device with "oflag=sgio" also given. Uses the
+SCSI VERIFY command with the BYTCHK field set to 1. The VERIFY command is
+used instead of WRITE when this option is given. There is no VERIFY(6)
+command. Stops on the first miscompare unless \fIoflag=coe\fR is given.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+outputs version number information and exits.
+.SH CONVERSIONS
+One or more conversions can be given to the "conv=" option. If more than one
+is given, they should be comma separated. sg_dd does not perform the
+traditional dd conversions (e.g. ASCII to EBCDIC). Recently added
+conversions overlap somewhat with the flags so some conversions are
+now supported by sg_dd.
+.TP
+nocreat
+this conversion has the same effect as "oflag=nocreat", namely: \fIOFILE\fR
+must exist, it will not be created.
+.TP
+noerror
+this conversion is very close to "iflag=coe" and is treated as such. See
+the "coe" flag. Note that an error on \fIOFILE\fR will stop the copy.
+.TP
+notrunc
+this conversion is accepted for compatibility with dd and ignored since
+the default action of this utility is not to truncate \fIOFILE\fR.
+.TP
+null
+has no affect, just a placeholder.
+.TP
+sparse
+FreeBSD supports "conv=sparse" so the same syntax is supported in sg_dd.
+See "sparse" in the FLAGS sections for more information.
+.TP
+sync
+is ignored by sg_dd. With dd it means supply zero fill (rather than skip)
+and is typically used like this "conv=noerror,sync" to have the same
+functionality as sg_dd's "iflag=coe".
+.SH FLAGS
+Here is a list of flags and their meanings:
+.TP
+00
+this flag is only active with \fIiflag=\fR and when given replaces
+\fIif=IFILE\fR. If both are given an error is generated. The input will
+be a stream of zeros, similar to using "if=/dev/zero" alone (but a little
+quicker), apart from the following case.
+.br
+If 'iflag=00,ff' is given then the block address (lower 32 bits, in 4
+bytes, big endian) is placed, multiple times, in each block. The block
+address takes into account the \fIskip=SKIP\fR setting. The
+.B sgp_dd
+utility has a \fI\-\-chkaddr\fR option that complements this option.
+.TP
+append
+causes the O_APPEND flag to be added to the open of \fIOFILE\fR. For regular
+files this will lead to data appended to the end of any existing data. Cannot
+be used together with the \fIseek=SEEK\fR option as they conflict. The default
+action of this utility is to overwrite any existing data from the beginning
+of the file or, if \fISEEK\fR is given, starting at block \fISEEK\fR. Note
+that attempting to 'append' to a device file (e.g. a disk) will usually be
+ignored or may cause an error to be reported.
+.TP
+coe
+continue on error. Only active for sg devices and block devices that have
+the 'sgio' flag set. 'iflag=coe oflag=coe' and 'coe=1' are equivalent. Use
+this flag twice (e.g. 'iflag=coe,coe') to have the same action as the 'coe=2'.
+A medium, hardware or blank check error while reading will re\-read blocks
+prior to the bad block, then try to recover the bad block, supplying zeros
+if that fails, and finally re\-read the blocks after the bad block. A medium,
+hardware or blank check error while writing is noted and ignored. A miscompare
+sense key during a VERIFY command (i.e. \fI\-\-verify\fR given) is noted and
+ignored when 'oflag=coe'. The recovery of the bad block when reading uses the
+SCSI READ LONG command if 'coe' given twice or more (also with the command
+line option 'coe=2'). Further, the READ LONG will set its CORRCT bit if 'coe'
+given thrice. SCSI disks may automatically try and remap faulty sectors (see
+the AWRE and ARRE in the read write error recovery mode page (the sdparm
+utility can access and possibly change these attributes)). Errors occurring on
+other files types will stop sg_dd. Error messages are sent to stderr. This
+flag is similar to 'conv=noerror,sync' in the
+.B dd(1)
+utility. See note about READ LONG below.
+.TP
+dio
+request the sg device node associated with this flag does direct IO. If direct
+IO is not available, falls back to indirect IO and notes this at completion.
+If direct IO is selected and /sys/module/sg/parameters/allow_dio has the
+value of 0 then a warning is issued (and indirect IO is performed).
+.TP
+direct
+causes the O_DIRECT flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR. This flag requires some memory alignment on IO. Hence user
+memory buffers are aligned to the page size. Has no effect on sg, normal
+or raw files. If 'iflag=sgio' and/or 'oflag=sgio' is also set then both
+are honoured: block devices are opened with the O_DIRECT flag and SCSI
+commands are issued via the SG_IO ioctl.
+.TP
+dpo
+set the DPO bit (disable page out) in SCSI READ and WRITE commands. Not
+supported for 6 byte cdb variants of READ and WRITE. Indicates that data is
+unlikely to be required to stay in device (e.g. disk) cache. May speed media
+copy and/or cause a media copy to have less impact on other device users.
+.TP
+dsync
+causes the O_SYNC flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR. The 'd' is prepended to lower confusion with the 'sync=0|1'
+option which has another action (i.e. a synchronisation to media at the
+end of the transfer).
+.TP
+excl
+causes the O_EXCL flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR.
+.TP
+ff
+this flag is only active with \fIiflag=\fR and when given replaces
+\fIif=IFILE\fR. If both are given an error is generated. The input will be
+a stream of 0xff bytes (or all bits set), apart from the following case.
+.br
+If 'iflag=00,ff' is given then the block address (lower 32 bits, in 4
+bytes, big endian) is placed, multiple times, in each block. The block
+address takes into account the \fIskip=SKIP\fR setting.
+.TP
+flock
+after opening the associated file (i.e. \fIIFILE\fR and/or \fIOFILE\fR)
+an attempt is made to get an advisory exclusive lock with the flock()
+system call. The flock arguments are "FLOCK_EX | FLOCK_NB" which will
+cause the lock to be taken if available else a "temporarily unavailable"
+error is generated. An exit status of 90 is produced in the latter case
+and no copy is done.
+.TP
+fua
+causes the FUA (force unit access) bit to be set in SCSI READ and/or WRITE
+commands. This only has an effect with sg devices or block devices
+that have the 'sgio' flag set. The 6 byte variants of the SCSI READ and
+WRITE commands do not support the FUA bit.
+.TP
+nocache
+use posix_fadvise() to advise corresponding file there is no need to fill
+the file buffer with recently read or written blocks.
+.TP
+nocreat
+this flag is only active in \fIoflag=FLAGS\fR. If present then \fIOFILE\fR
+will be opened if it exists. If \fIOFILE\fR doesn't exist then an error
+is generated. Without this flag a regular (empty) file named \fIOFILE\fR
+will be created (and then filled). For production quality scripts where
+\fIOFILE\fR is a device node (e.g. '/dev/sdc') this flag is recommended.
+It guards against the remote possibility of 'dev/sdc' disappearing
+temporarily (e.g. a USB memory key removed) resulting in a large regular
+file called '/dev/sdc' being created.
+.TP
+null
+has no affect, just a placeholder.
+.TP
+random
+this flag is only active with \fIiflag=\fR and when given replaces
+\fIif=IFILE\fR. If both are given an error is generated. The input will
+be a stream of pseudo random bytes. The Linux getrandom(2) system call is
+used to create a seed and there after mrand48(3) is used to generate a
+pseudo random sequence, 4 bytes at a time. The quality of the randomness
+can be viewed with the ent(1) utility. This is not a high quality random
+number generator, it is built for speed, not quality. One application is
+checking the correctness of the copy and verify operations of this utility.
+.TP
+sgio
+causes block devices to be accessed via the SG_IO ioctl rather than
+standard UNIX read() and write() commands. When the SG_IO ioctl is
+used the SCSI READ and WRITE commands are used directly to move
+data. sg devices always use the SG_IO ioctl. This flag offers finer
+grain control compared to the otherwise identical 'blk_sgio=1' option.
+.TP
+sparse
+after each \fIBS\fR * \fIBPT\fR byte segment is read from the input,
+it is checked for being all zeros. If so, nothing is written to the output
+file unless this is the last segment of the transfer. This flag is only
+active with the oflag option. It cannot be used when the output is not
+seekable (e.g. stdout). It is ignored if the output file is /dev/null .
+Note that this utility does not remove the \fIOFILE\fR prior to starting
+to write to it. Hence it may be advantageous to manually remove the
+\fIOFILE\fR if it is large prior to using oflag=sparse. The last segment
+is always written so regular files will show the same length and so
+programs like md5sum and sha1sum will generate the same value regardless
+of whether oflag=sparse is given or not. This option may be used when the
+\fIOFILE\fR is a raw device but is probably only useful if the device is
+known to contain zeros (e.g. a SCSI disk after a FORMAT command).
+.SH RETIRED OPTIONS
+Here are some retired options that are still present:
+.TP
+append=0 | 1
+when set, equivalent to 'oflag=append'. When clear the action is
+to overwrite the existing file (if it exists); this is the default.
+See the 'append' flag.
+.TP
+fua=0 | 1 | 2 | 3
+force unit access bit. When 3, fua is set on both \fIIFILE\fR and
+\fIOFILE\fR; when 2, fua is set on \fIIFILE\fR;, when 1, fua is set on
+\fIOFILE\fR; when 0 (default), fua is cleared on both. See the 'fua' flag.
+.SH NOTES
+Block devices (e.g. /dev/sda and /dev/hda) can be given for \fIIFILE\fR.
+If neither '\-iflag=direct', 'iflag=sgio' nor 'blk_sgio=1' is given then
+normal block IO involving buffering and caching is performed. If
+only '\-iflag=direct' is given then the buffering and caching is
+bypassed (this is applicable to both SCSI devices and ATA disks).
+If 'iflag=sgio' or 'blk_sgio=1' is given then the SG_IO ioctl is used on
+the given file causing SCSI commands to be sent to the device and that also
+bypasses most of the actions performed by the block layer (this is only
+applicable to SCSI devices, not ATA disks). The same applies for block
+devices given for \fIOFILE\fR.
+.PP
+Various numeric arguments (e.g. \fISKIP\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+The \fICOUNT\fR, \fISKIP\fR and \fISEEK\fR arguments can take 64 bit
+values (i.e. very big numbers). Other values are limited to what can fit in
+a signed 32 bit number.
+.PP
+Data usually gets to the user space in a 2 stage process: first the
+SCSI adapter DMAs into kernel buffers and then the sg driver copies
+this data into user memory (write operations reverse this sequence).
+This is called "indirect IO" and there is a 'dio' option to
+select "direct IO" which will DMA directly into user memory. Due to some
+issues "direct IO" is disabled in the sg driver and needs a
+configuration change to activate it. This is typically done
+with 'echo 1 > /sys/module/sg/parameters/allow_dio'.
+.PP
+All informative, warning and error output is sent to stderr so that
+dd's output file can be stdout and remain unpolluted. If no options
+are given, then the usage message is output and nothing else happens.
+.PP
+Even if READ LONG succeeds on a "bad" block when 'coe=2' (or 'coe=3')
+is given, the recovered data may not be useful. There are no guarantees
+that the user data will appear "as is" in the first 512 bytes.
+.PP
+A raw device must be bound to a block device prior to using sg_dd.
+See
+.B raw(8)
+for more information about binding raw devices. To be safe, the sg device
+mapping to SCSI block devices should be checked with sg_map before use.
+.PP
+Disk partition information can often be found with
+.B fdisk(8)
+[the "\-ul" argument is useful in this respect].
+.PP
+For sg devices (and block devices when blk_sgio=1 is given) this utility
+issues SCSI READ and WRITE (SBC) commands which are appropriate for disks and
+reading from CD/DVD/HD\-DVD/BD drives. Those commands
+are not formatted correctly for tape devices so sg_dd should not be used on
+tape devices. If the largest block address of the requested transfer
+exceeds a 32 bit block number (i.e 0xffff) then a warning is issued and
+the sg device is accessed via SCSI READ(16) and WRITE(16) commands.
+.PP
+The attributes of a block device (partition) are ignored when 'blk_sgio=1'
+is used. Hence the whole device is read (rather than just the second
+partition) by this invocation:
+.PP
+   sg_dd if=/dev/sdb2 blk_sgio=1 of=t bs=512
+.SH EXAMPLES
+.PP
+Looks quite similar in usage to dd:
+.PP
+   sg_dd if=/dev/sg0 of=t bs=512 count=1MB
+.PP
+This will copy 1 million 512 byte blocks from the device associated with
+/dev/sg0 (which should have 512 byte blocks) to a file called t.
+Assuming /dev/sda and /dev/sg0 are the same device then the above is
+equivalent to:
+.PP
+   dd if=/dev/sda iflag=direct of=t bs=512 count=1000000
+.PP
+although dd's speed may improve if bs was larger and count was suitably
+reduced. The use of the 'iflag=direct' option bypasses the buffering and
+caching that is usually done on a block device.
+.PP
+Using a raw device to do something similar on a ATA disk:
+.PP
+   raw /dev/raw/raw1 /dev/hda
+.br
+   sg_dd if=/dev/raw/raw1 of=t bs=512 count=1MB
+.PP
+To copy a SCSI disk partition to an ATA disk partition:
+.PP
+   raw /dev/raw/raw2 /dev/hda3
+.br
+   sg_dd if=/dev/sg0 skip=10123456 of=/dev/raw/raw2 bs=512
+.PP
+This assumes a valid partition is found on the SCSI disk at the given
+skip block address (past the 5 GB point of that disk) and that
+the partition goes to the end of the SCSI disk. An explicit count
+is probably a safer option. The partition is copied to /dev/hda3 which
+is an offset into the ATA disk /dev/hda . The exact number of blocks
+read from /dev/sg0 are written to /dev/hda (i.e. no padding).
+.PP
+To time a streaming read of the first 1 GB (2 ** 30 bytes) on a disk
+this utility could be used:
+.PP
+   sg_dd if=/dev/sg0 of=/dev/null bs=512 count=2m time=1
+.PP
+On completion this will output a line like:
+"time to transfer data was 18.779506 secs, 57.18 MB/sec". The "MB/sec"
+in this case is 1,000,000 bytes per second.
+.PP
+The 'of2=' option can be used to copy data and take a md5sum of it
+without needing to re\-read the data:
+.PP
+  mkfifo fif
+.br
+  md5sum fif &
+.br
+  sg_dd if=/dev/sg3 iflag=coe of=sg3.img oflag=sparse of2=fif bs=512
+.PP
+This will image /dev/sg3 (e.g. an unmounted disk) and place the contents
+in the (sparse) file sg3.img . Without re\-reading the data it will also
+perform a md5sum calculation on the image.
+.SH SIGNALS
+The signal handling has been borrowed from dd: SIGINT, SIGQUIT and
+SIGPIPE output the number of remaining blocks to be transferred and
+the records in + out counts; then they have their default action.
+SIGUSR1 causes the same information to be output yet the copy continues.
+All output caused by signals is sent to stderr.
+.SH EXIT STATUS
+The exit status of sg_dd is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page. Since this utility works at a higher level
+than individual commands, and there are 'coe' and 'retries' flags,
+individual SCSI command failures do not necessary cause the process
+to exit.
+.PP
+An additional exit status of 90 is generated if the flock flag is given
+and some other process holds the advisory exclusive lock.
+.SH AUTHORS
+Written by Douglas Gilbert and Peter Allworth.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2000\-2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+cmp(1)
+.PP
+There is a web page discussing sg_dd at https://sg.danny.cz/sg/sg_dd.html
+.PP
+A POSIX threads version of this utility called
+.B sgp_dd
+is in the sg3_utils package. Another version from that package is called
+.B sgm_dd
+and it uses memory mapped IO to speed transfers from sg devices.
+.PP
+The lmbench package contains
+.B lmdd
+which is also interesting. For moving data to and from tapes see
+.B dt
+which is found at https://www.scsifaq.org/RMiller_Tools/index.html
+.PP
+To change mode parameters that effect a SCSI device's caching and error
+recovery see
+.B sdparm(sdparm)
+.PP
+To verify the data on the media or to verify it against some other
+copy of the data see
+.B sg_verify(sg3_utils)
+.PP
+See also
+.B raw(8), dd(1), ddrescue(GNU), ddpt
diff --git a/doc/sg_decode_sense.8 b/doc/sg_decode_sense.8
new file mode 100644
index 0000000..e91d689
--- /dev/null
+++ b/doc/sg_decode_sense.8
@@ -0,0 +1,219 @@
+.TH SG_DECODE_SENSE "8" "August 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_decode_sense \- decode SCSI sense and related data
+.SH SYNOPSIS
+.B sg_decode_sense
+[\fI\-\-binary=BFN\fR] [\fI\-\-cdb\fR] [\fI\-\-err=ES\fR] [\fI\-\-file=HFN\fR]
+[\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-inhex=HFN\fR]
+[\fI\-\-ignore\-first\fR] [\fI\-\-json[=JO]\fR] [\fI\-\-nodecode\fR]
+[\fI\-\-nospace\fR] [\fI\-\-status=SS\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] [\fI\-\-write=WFN\fR] [H1 H2 H3 ...]
+.SH DESCRIPTION
+.\" Add any additional description here
+This utility takes SCSI sense data in binary or as a sequence of ASCII
+hexadecimal bytes and decodes it. The primary reference for the
+decoding is SPC\-5 ANSI INCITS 502\-2020 and the most recent draft
+SPC\-6 revision 6 which can be found at https://www.t10.org and other
+locations on the internet.
+.PP
+SCSI sense data is often found in kernel log files as a result of
+something going wrong or may be an informative warning. It is often shown
+as a sequence of hexadecimal bytes, starting with 70, 71, 72, 73, f0 or f1.
+Sense data could be up to 252 bytes long but typically is much shorter
+than that, 18 bytes long is often seen and is usually associated with
+the older "fixed" format sense data.
+.PP
+The sense data can be provided on the command line or in a file. If given
+on the command line the sense data should be a sequence of hexadecimal bytes
+separated by space. Alternatively a file can be given with the contents in
+binary or ASCII hexadecimal bytes. The latter form can contain several lines
+each with none, one or more ASCII hexadecimal bytes separated by
+space (comma or tab). The hash symbol may appear and it and the rest of the
+line is ignored making it useful for comments.
+.PP
+If the \fI\-\-cdb\fR option is given then rather than viewing the given hex
+arguments as sense data, it is viewed as a SCSI command descriptor
+block (CDB). In this case the command name is printed out. That name is
+based on the first hex byte given (know as the opcode) and optionally on
+another field called the "service action".
+.PP
+Another alternate action is when the \fI\-\-err=ES\fR is given. \fIES\fR
+is assumed to be an "exit status" value between 0 and 255 from one of the
+utilities in this package. A descriptive string is printed. Other options
+are ignored apart from \fI\-\-verbose\fR.
+.PP
+When the \fI\-\-nodecode\fR option is given, this utility may be used to
+convert a binary file to hexadecimal or vice versa. The data converted does
+not need to be sense data.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-binary\fR=\fIBFN\fR
+the data is read in binary from a file called \fIBFN\fR. The option
+cannot be given with \fI\-\-file=HFN\fR or \fI\-\-inhex=HFN\fR as they
+contradict. The data is assumed to be sense data unless the
+fI\-\-nodecode\fR is given.
+.TP
+\fB\-c\fR, \fB\-\-cdb\fR
+treat the given string of hex arguments as bytes in a SCSI CDB and
+decode the command name.
+.TP
+\fB\-e\fR, \fB\-\-err\fR=\fIES\fR
+\fIES\fR should be an "exit status" value between 0 and 255 that is
+available from the shell (i.e. the utility's execution context) after the
+utility is finished. By default an indicative error message is printed to
+stdout; and if the \fI\-\-verbose\fR option is given once (or an odd number
+of times) then the message is instead printed to stderr. If \fI\-\-verbose\fR
+is given two or more times a longer form of the message is output. In all
+cases the message is less than 128 characters long with one trailing line
+feed. All other command line options and arguments are ignored.
+.TP
+\fB\-f\fR, \fB\-\-file\fR=\fIHFN\fR
+the sense data is read in ASCII hexadecimal from a file called \fIHFN\fR.
+The sense data should appear as a sequence of bytes separated by space,
+comma, tab, hyphen or newline. Everything from and including a hash symbol
+to the end of that line is ignored. If \fI\-\-nospace\fR is set then no
+separator is required between the ASCII hexadecimal digits in \fIHFN\fR
+with bytes decoded from pairs of ASCII hexadecimal digits.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+this option is used once in conjunction with \fI\-\-write=WFN\fR in order to
+change the output written to \fIWFN\fR to lines of ASCII hex bytes suitable
+for a C language compiler. Each line contains up to 16 bytes (e.g. a line
+starting with "0x3b,0x07,0x00,0xff").
+.br
+In other cases (i.e. when \fI\-\-write=WFN\fR is not given, or this option
+is given more than once) then the output is as described in the sg3_utils(8)
+manpage.
+.br
+The combination of \fI\-\-inhex=HFN\fR and this option used three times
+can be useful to converting hexadecimal bytes (e.g. hyphen separated) into
+a more regular form. The short option form is more convenient for invoking
+this option three times (e.g. '\-HHH').
+.TP
+\fB\-i\fR, \fB\-\-inhex\fR=\fIHFN\fR
+same action as \fI\-\-file=HFN\fR. This option was added for compatibility
+with other utilities in this package that have a \fI\-\-inhex=\fR option.
+.TP
+\fB\-I\fR, \fB\-\-ignore\-first\fR
+many programs that output hex bytes (e.g. 'hexdump \-C') have a running
+count (or index) in the first column of each line. This option ignores the
+first hexadecimal value on each line. This option has no effect if
+\fI\-\-binary=BFN\fR or \fI\-\-nospace\fR are given. Blank lines and any
+character from and after "#" on a line are ignored. Useful with the
+\fI\-\-file=HFN\fR and \fI\-\-nodecode\fR options.
+.TP
+\fB\-j\fR, \fB\-\-json[\fR=\fIJO\fR]
+output is in JSON format instead of human readable form. See sg3_utils_json
+manpage or use '?' for \fIJO\fR for a summary.
+.br
+This option is designed to parse sense data into JSON output.
+.TP
+\fB\-N\fR, \fB\-\-nodecode\fR
+Do not decode the given data as sense or a cdb. Useful when arbitrary data
+is given (e.g. when converting hex to binary or vice versa).
+.TP
+\fB\-n\fR, \fB\-\-nospace\fR
+expect ASCII hexadecimal to be a string of hexadecimal digits with no
+spaces between them. Bytes are decoded by taking two hexadecimal digits
+at a time, so an even number of digits is expected. The string of
+hexadecimal digits may be on the command line (replacing "H1 H2 H3")
+or spread across multiple lines the \fIHFN\fR given to \fI\-\-file=\fR.
+On the command line, spaces (or other whitespace characters) between
+sequences of hexadecimal digits are ignored; the maximum command line
+hex string is 1023 characters long.
+.TP
+\fB\-s\fR, \fB\-\-status\fR=\fISS\fR
+where \fISS\fR is a SCSI status byte value, given in hexadecimal. The
+SCSI status byte is related to, but distinct from, sense data.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the degree of verbosity (debug messages).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+output version string then exit.
+.TP
+\fB\-w\fR, \fB\-\-write\fR=\fIWFN\fR
+writes the sense data out to a file called \fIWFN\fR. If necessary \fIWFN\fR
+is created. If \fIWFN\fR exists then it is truncated prior to writing the
+sense data to it. If the \fI\-\-hex\fR option is also given then ASCII hex
+is written to \fIWFN\fR (see the \fI\-\-hex\fR option description);
+otherwise binary is written to \fIWFN\fR. This option is a convenience and
+may be helpful in converting the ASCII hexadecimal representation of sense
+data (or anything else) into the equivalent binary or a compilable ASCII
+hex form.
+.SH NOTES
+Unlike most utilities in this package, this utility does not access a
+SCSI device (logical unit). This utility accesses a library associated
+with this package. Amongst other things the library decodes SCSI sense
+data.
+.PP
+The sg_raw utility takes a ASCII hexadecimal sequence representing a SCSI
+CDB. When sg_raw is given the '\-vvv' option, it will attempt to decode the
+CDB name.
+.PP
+Using the option combination: "\fI\-\-inhex=HFN \-\-nodecode \-\-write=WFN\fR"
+may be used to convert hexadecimal (as produced by this and other utilities
+in this package) to binary where the output file is \fIWFN\fR.
+.PP
+Unlike many other utilities there is no \fI\-\-raw\fR option. However binary
+data can be input using the \fI\-\-binary=BFN\fR option while binary data
+can be output using the \fI\-\-write=WFN\fR option (in the absence of the
+\fI\-\-hex\fR option).
+.SH EXAMPLES
+Sense data is often printed out in kernel logs and sometimes on the
+command line when verbose or debug flags are given. It will be at least
+8 bytes long, often 18 bytes long but may be longer. A sense data string
+might look like this:
+.PP
+f0 00 03 00 00 12 34 0a  00 00 00 00 11 00 00 00
+.br
+00 00
+.PP
+Cut and paste it after the sg_decode_sense command:
+.PP
+  sg_decode_sense f0 00 03 00 00 12 34 0a 00 00 00 00 11 00 00 00 00 00
+.PP
+and for this sense data the output should look like this:
+.PP
+ Fixed format, current;  Sense key: Medium Error
+.br
+ Additional sense: Unrecovered read error
+.br
+  Info fld=0x1234 [4660]
+.PP
+For a medium error the Info field is the logical block address (LBA)
+of the lowest numbered block that the associated SCSI command was not
+able to read (verify or write).
+.PP
+To convert arbitrary binary data to hex, suitable to be parsed by other
+sg3_utils utilities. The \fI\-\-nodecode\fR option is used in this case:
+.PP
+  sg_decode_sense \-N \-i vpd_zbdc.hex \-w vpd_zbdc.bin
+.PP
+The '\-HHH' will output hex to the console (stdout) in a form suitable for
+other utilities in this package to parse as input. And sg_decode_sense can
+also be used to convert from arbitrary hex to binary with:
+.PP
+  sg_decode_sense \-N \-b vpd_zbdc.raw \-HHH
+.PP
+Note that tools like hexdump and od place a counter (i.e. an index starting
+at 0) at the beginning of each line which is a pain when parsing hex.
+The '/-HHH' option(s) does not output that leading counter on each line.
+.SH EXIT STATUS
+The exit status of sg_decode_sense is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2010\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_requests,sg_raw(sg3_utils)
diff --git a/doc/sg_emc_trespass.8 b/doc/sg_emc_trespass.8
new file mode 100644
index 0000000..94b592b
--- /dev/null
+++ b/doc/sg_emc_trespass.8
@@ -0,0 +1,52 @@
+.TH SG_EMC_TRESPASS "8" "December 2012" "sg3_utils\-1.35" SG3_UTILS
+.SH NAME
+sg_emc_trespass \- change ownership of SCSI LUN from another
+Service\-Processor to this one
+.SH SYNOPSIS
+.B sg_emc_trespass
+[\fI\-d\fR] [\fI\-hr\fR] [\fI\-s\fR]
+[\fI\-V\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+sg_emc_trespass sends an EMC\-specific Trespass Command to the \fIDEVICE\fR
+with the selected options. This Mode Select changes the ownership of the LUN
+of the device from another Service\-Processor to the one the command was
+received on.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-d\fR
+outputs some extra debug information associated with executing this command
+.TP
+\fB\-hr\fR
+Sets the 'Honor Reservation' bit in the command. If set, the trespass
+will only succeed to change the ownership from the Peer SP if the Peer
+SP does not have an outstanding SCSI reservation for the LUN. By
+default, the reservation state will be ignored.
+.TP
+\fB\-s\fR
+Send the short version of the trespass command instead of the long
+version. The short version is supported on the EMC FC5300, FC4500 and
+FC4700. The long version (default) is supported on the CLARiiON CX and
+AX family arrays.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.PP
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be a SCSI
+generic (sg) device. In the 2.6 series block devices (e.g. SCSI disks
+and DVD drives) can also be specified. For example "sg_start 0 /dev/sda"
+will work in the 2.6 series kernels.
+.SH EXIT STATUS
+The exit status of sg_emc_trespass is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHOR
+Written by Lars Marowsky\-Bree, based on sg_start.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2012 Lars Marowsky\-Bree, Douglas Gilbert.
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/sg_format.8 b/doc/sg_format.8
new file mode 100644
index 0000000..7c00cc5
--- /dev/null
+++ b/doc/sg_format.8
@@ -0,0 +1,725 @@
+.TH SG_FORMAT "8" "February 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_format \- format, format with preset, resize SCSI disk; format tape
+.SH SYNOPSIS
+.B sg_format
+[\fI\-\-cmplst=\fR{0|1}] [\fI\-\-count=COUNT\fR] [\fI\-\-dcrt\fR]
+[\fI\-\-dry\-run\fR] [\fI\-\-early\fR] [\fI\-\-ffmt=FFMT\fR]
+[\fI\-\-fmtmaxlba\R] [\fI\-\-fmtpinfo=FPI\fR] [\fI\-\-format\fR]
+[\fI\-\-help\fR] [\fI\-\-ip\-def\fR] [\fI\-\-long\fR] [\fI\-\-mode=MP\fR]
+[\fI\-\-pfu=PFU\fR] [\fI\-\-pie=PIE\fR] [\fI\-\-pinfo\fR] [\fI\-\-poll=PT\fR]
+[\fI\-\-preset=ID\fR] [\fI\-\-quick\fR] [\fI\-\-resize\fR] [\fI\-\-rto_req\fR]
+[\fI\-\-security\fR] [\fI\-\-six\fR] [\fI\-\-size=LB_SZ\fR]
+[\fI\-\-tape=FM\fR] [\fI\-\-timeout=SECS\fR] [\fI\-\-verbose\fR]
+[\fI\-\-verify\fR] [\fI\-\-version\fR] [\fI\-\-wait\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Not all SCSI direct access devices need to be formatted and some have vendor
+specific formatting procedures. SCSI disks with rotating media are probably
+the largest group that do support a 'standard' format operation. They are
+typically factory formatted to a block size of 512 bytes with the largest
+number of blocks that the manufacturer recommends. The manufacturer's
+recommendation typically leaves aside a certain number of tracks, spread
+across the media, for reassignment of blocks to logical block addresses
+during the life of the disk.
+.PP
+This utility issues one of three SCSI format commands: FORMAT UNIT, FORMAT
+MEDIUM or FORMAT WITH PRESET. In the following description, unqualified
+sections will usually be referring to the SCSI FORMAT UNIT command. Both
+FORMAT UNIT and FORMAT WITH PRESET apply to disks (or disk\-like devices).
+The FORMAT MEDIUM command is for tapes. A SCSI INQUIRY response categorizes
+the 'Peripheral Device Type' (PDT) of each SCSI device. This utility uses
+the PDT to check if there is a conflict between the \fIDEVICE\fR and the
+given option (e.g. giving the \fI\-\-tape=FM\fR option when \fIDEVICE\fR is
+a normal disk). If there is a conflict, this utility will not continue.
+.PP
+This utility can format modern SCSI disks and potentially change their block
+size (if permitted) and the block count (i.e. number of accessible blocks on
+the media also known as "resizing"). Resizing a disk to less than the
+manufacturer's recommended block count is sometimes called "short
+stroking" (see NOTES section). Resizing the block count while not changing
+the block size may not require a format operation. The SBC\-2 standard (see
+www.t10.org) has obsoleted the "format device" mode page. Many of the low
+level details found in that mode page are now left up to the discretion of
+the manufacturer. There is a Format Status log page which reports on the
+previous successful format operation(s).
+.PP
+When this utility is used without options (i.e. it is only given a
+\fIDEVICE\fR argument) it prints out the existing block size and block count
+derived from two sources. These two sources are a block descriptor in the
+response to a MODE SENSE command and the response to a READ CAPACITY command.
+The reason for this double check is to detect a "format corrupt" state (see
+the NOTES section). This usage will not modify the disk.
+.PP
+When this utility is used with either \fI\-\-format\fR, \fI\-\-preset=ID\fR
+or \fI\-\-tape=FM\fR, it will attempt to format the given DEVICE. In the
+absence of the \fI\-\-quick\fR option there is a 15 second pause during which
+time the user is invited thrice (5 seconds apart) to abort sg_format. This
+occurs just prior the SCSI FORMAT UNIT, FORMAT WITH PRESET or FORMAT MEDIUM
+command being issued. See the NOTES section for more information.
+.PP
+Protection information (PI) is optional and is made up of one or more
+protection intervals, each made up of 8 bytes associated with a logical
+block. When PI is active each logical block will have 1, 2, 4, 8, etc
+protection intervals (i.e. a power of two), interleaved with (and following)
+the user data to which they refer. Four protection types are defined with
+protection type 0 being no protection intervals. See the PROTECTION
+INFORMATION section below for more information.
+.PP
+When the \fI\-\-tape=FM\fR option is given then the SCSI FORMAT MEDIUM
+command is sent to the \fIDEVICE\fR. FORMAT MEDIUM is defined in the SSC
+documents at T10 and prepares a volume for use. That may include partitioning
+the medium. See the section below on TAPE for more information.
+.PP
+The FORMAT WITH PRESET was added in draft SBC\-4 revision 18. A preset
+pattern, selected by the PRESET IDENTIFIER field (\fI\-\-id=FWPID\fR),
+is written to the disk. See the FORMAT PRESETS VPD page (0xb8) for a list
+of available Format preset identifiers and their associated data.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-C\fR, \fB\-\-cmplst\fR={0|1}
+sets the CMPLST ("complete list") bit in the FORMAT UNIT cdb to 0 or 1.
+If the value is 0 then the existing GLIST (grown list) is taken into account.
+If the value is 1 then the existing GLIST is ignored. CMPLST defaults to 1
+apart from when the \fI\-\-ffmt=FFMT\fR option's value is non\-zero in which
+case CMPLST defaults to 0. See the LISTS section below. In most cases this
+bit should be left at its default value.
+.TP
+\fB\-c\fR, \fB\-\-count\fR=\fICOUNT\fR
+where \fICOUNT\fR is the number of blocks to be formatted or media to be
+resized to. Can be used with either \fI\-\-format\fR or \fI\-\-resize\fR.
+With \fI\-\-format\fR this option need not be given in which case it is
+assumed to be zero.
+.br
+With \fI\-\-format\fR the interpretation of \fICOUNT\fR is:
+.br
+  (\fICOUNT\fR > 0) : only format the first \fICOUNT\fR blocks and READ
+CAPACITY will report \fICOUNT\fR blocks after format
+.br
+  (\fICOUNT\fR = 0) and block size unchanged : use existing block count
+.br
+  (\fICOUNT\fR = 0) and block size changed : recommended maximum block
+count for new block size
+.br
+  (\fICOUNT\fR = \-1) : use recommended maximum block count
+.br
+  (\fICOUNT\fR < \-1) : illegal
+.br
+With \fI\-\-resize\fR this option must be given and \fICOUNT\fR has this
+interpretation:
+.br
+  (\fICOUNT\fR > 0) : after resize READ CAPACITY will report \fICOUNT\fR
+blocks
+.br
+  (\fICOUNT\fR = 0) : after resize READ CAPACITY will report 0 blocks
+.br
+  (\fICOUNT\fR = \-1) : after resize READ CAPACITY will report its
+maximum number of blocks
+.br
+  (\fICOUNT\fR < \-1) : illegal
+.br
+In both cases if the given \fICOUNT\fR exceeds the maximum number of
+blocks (for the block size) then the disk reports an error.
+See NOTES section below.
+.TP
+\fB\-D\fR, \fB\-\-dcrt\fR
+this option sets the DCRT bit in the FORMAT UNIT command's parameter list
+header. It will "disable certification". Certification verifies that blocks
+are usable during the format process. Using this option may speed the format
+but \fI\-\-ffmt=FFMT\fR, if available, would probably be better. The default
+action of this utility (i.e. when this option is not given) is to clear the
+DCRT bit thereby requesting "media certification" (also unless another
+option needs it, the FOV bit will be cleared). When the DCRT bit is set, the
+FOV bit must also be set hence sg_format does that.
+.br
+If this option is given twice then certification is enabled by clearing the
+DCRT bit and setting the FOV bit. Both these bits are found in the parameter
+list associated with the FORMAT UNIT cdb.
+.TP
+\fB\-d\fR, \fB\-\-dry\-run\fR
+this option will parse the command line, do all the preparation but bypass
+the actual FORMAT UNIT, FORMAT WITH PRESET or FORMAT MEDIUM command. Also if
+the options would otherwise cause the logical block size to change, then the
+MODE SELECT command that would do that is also bypassed when the dry
+run option is given.
+.TP
+\fB\-e\fR, \fB\-\-early\fR
+during a format operation, The default action of this utility is to poll the
+disk every 60 seconds (or every 10 seconds if \fIFFMT\fR is non\-zero) to
+determine the progress of the format operation until it is finished. When this
+option is given this utility will exit "early", that is as soon as the format
+operation has commenced. Then the user can monitor the progress of the ongoing
+format operation with other utilities (e.g. sg_turs(8) or sg_requests(8)).
+This option and \fI\-\-wait\fR are mutually exclusive.
+.TP
+\fB\-t\fR, \fB\-\-ffmt\fR=\fIFFMT\fR
+\fIFFMT\fR (fast format) is placed in a field of the same name in the FORMAT
+UNIT cdb. The field was introduced in SBC\-4 revision 10. The default value
+is 0 which implies the former action which is typically to overwrite all
+blocks on the \fIDEVICE\fR. That can take a long time (e.g. with hard disks
+over 10 TB in size that can be days). With \fIFFMT\fR set that time may be
+reduced to minutes or less. So it is worth trying if it is available.
+.br
+\fIFFMT\fR has values 1 and 2 for fast format with 3 being reserved
+currently. These two values include this description: "The device server
+initializes the medium ... without overwriting the medium (i.e. resources
+for managing medium access are initialized and the medium is not written)".
+The difference between 1 and 2 concerns read operations on LBAs to which no
+data has been written to, after the fast format. When \fIFFMT\fR is 1 the
+read operation should return "unspecified logical block data" and complete
+without error. When \fIFFMT\fR is 2 the read operation may yield check
+condition status with a sense key set to hardware error, medium error or
+command aborted. See draft SBC\-4 revision 16 section 4.34 for more details.
+.TP
+\fB\-b\fR, \fB\-\-fmtmaxlba\fR
+This option is only active if it is given together with the
+\fI\-\-preset=ID\fR option. If so it sets the FMTMAXLBA field in the FORMAT
+WITH PRESET command.
+.TP
+\fB\-f\fR, \fB\-\-fmtpinfo\fR=\fIFPI\fR
+sets the FMTPINFO field in the FORMAT UNIT cdb to a value between 0 and 3.
+The default value is 0. The FMTPINFO field from SBC\-3 revision 16 is a 2
+bit field (bits 7 and 6 of byte 1 in the cdb). Prior to that revision it was
+a single bit field (bit 7 of byte 1 in the cdb) and there was an accompanying
+bit called RTO_REQ (bit 6 of byte 1 in the cdb). The deprecated
+options "\-\-pinfo" and "\-\-rto\-req" represent the older usage. This
+option should be used in their place. See the PROTECTION INFORMATION section
+below for more information.
+.TP
+\fB\-F\fR, \fB\-\-format\fR
+issue one of the three SCSI "format" commands. In the absence of the
+\fI\-\-preset=ID\fR and \fI\-\-tape=FM\fR options, the SCSI FORMAT UNIT
+command is issued.
+.B These commands will destroy all the data held on the media.
+This option is required to change the block size of a disk. In the absence
+of the \fI\-\-quick\fR option, the user is given a 15 second count down to
+ponder the wisdom of doing this, during which time control\-C (amongst other
+Unix commands) can be used to kill this process before it does any damage.
+.br
+When used three times (or more) the preliminary MODE SENSE and SELECT
+commands are bypassed, leaving only the initial INQUIRY and FORMAT UNIT
+commands. This is for emergency use (e.g. when the MODE SENSE/SELECT
+commands are not working) and cannot change the logical block size.
+.br
+Host managed zoned devices (e.g. many zoned disks) have a different PDT
+compared to other disks but can still be formatted as if they were 'normal'
+disks.
+.br
+See NOTES section for implementation details and EXAMPLES section for typical
+use.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage information then exit.
+.TP
+\fB\-I\fR, \fB\-\-ip\-def\fR
+sets the default Initialization Pattern. Some disks (SSDs) use this to flag
+that a format should fully provision (i.e. associate a physical block with
+every logical block). The same disks (SSDs) might thin provision if this
+option is not given. If this option is given then the \fI\-\-security\fR
+option cannot be given. Also accepts \fI\-\-ip_def\fR for this option.
+.TP
+\fB\-l\fR, \fB\-\-long\fR
+the default action of this utility is to assume 32 bit logical block
+addresses. With 512 byte block size this permits more than 2
+terabytes (almost 2 ** 41 bytes) on a single disk. This option selects
+commands and parameters that allow for 64 bit logical block addresses.
+Specifically this option sets the "longlba" flag in the MODE SENSE (10)
+command and uses READ CAPACITY (16) rather than READ CAPACITY (10). If this
+option is not given and READ CAPACITY (10) or MODE SELECT detects a disk
+the needs more than 32 bits to represent its logical blocks then it is
+set internally. This option does not set the LONGLIST bit in the FORMAT UNIT
+command. The LONGLIST bit is set as required depending other
+parameters (e.g. when '\-\-pie=PIE' is greater than zero).
+.TP
+\fB\-M\fR, \fB\-\-mode\fR=\fIMP\fR
+\fIMP\fR is a mode page number (0 to 62 inclusive) that will be used for
+reading and perhaps changing the device logical block size. The default
+is 1 which is the Read\-Write Error Recovery mode page.
+.br
+Preferably the chosen (or default) mode page should be saveable (i.e.
+accept the SP bit set in the MODE SELECT command used when the logical
+block size is being changed). Recent version of this utility will retry a
+MODE SELECT if the SP=1 variant fails with a sense key of ILLEGAL REQUEST.
+That retry will use the same MODE SELECT command but with SP=0 .
+.TP
+\fB\-P\fR, \fB\-\-pfu\fR=\fIPFU\fR
+sets the "Protection Field Usage" field in the parameter block associated
+with a FORMAT UNIT command to \fIPFU\fR. The default value is 0, the only
+other defined value currently is 1. See the PROTECTION INFORMATION section
+below for more information.
+.TP
+\fB\-q\fR, \fB\-\-pie\fR=\fIPIE\fR
+sets the "Protection Interval Exponent" field in the parameter block
+associated with a FORMAT UNIT command to \fIPIE\fR. The default value is 0.
+\fIPIE\fR can only be non\-zero with protection types 2 and 3.
+The value of 0 is typical for 512 byte blocks; with 4096 byte blocks a value
+of 3 may be appropriate (i.e. 8 protection intervals interleaved with 4096
+bytes of user data). A device may not support any non\-zero values. This
+field first appeared in SBC\-3 revision 18.
+.TP
+\fB\-p\fR, \fB\-\-pinfo\fR
+this option is deprecated, use the \fI\-\-fmtpinfo=FPI\fR option instead.
+If used, then it sets bit 7 of byte 1 in the FORMAT UNIT cdb and that
+is equivalent to setting \fI\-\-fmtpinfo=2\fR. [So if \fI\-\-pinfo\fR is
+used (plus \fI\-\-fmtpinfo=FPI\fR and \fI\-\-pfu=PFU\fR are not given or
+their arguments are 0) then protection type 1 is selected.]
+.TP
+\fB\-x\fR, \fB\-\-poll\fR=\fIPT\fR
+where \fIPT\fR is the type of poll used. If \fIPT\fR is 0 then a TEST UNIT
+READY command is used, otherwise a REQUEST SENSE command is used. The
+default is currently 0 but this will change to 1 in the near future. See
+the NOTES sections below.
+.TP
+\fB\-E\fR, \fB\-\-preset\fR=\fIID\fR
+this option instructs this utility to issue a SCSI FORMAT WITH PRESET
+command. The PRESET IDENTIFIER field in that cdb is set to \fIID\fR. The
+IMMED field in that cdb is also set unless the \fI\-\-wait\fR option is
+also given, in which case it is cleared.
+.TP
+\fB\-Q\fR, \fB\-\-quick\fR
+the default action (i.e. when the option is not given) is to give the user
+15 seconds to reconsider doing a format operation on the \fIDEVICE\fR.
+When this option is given that step (i.e. the 15 second warning period)
+is skipped.
+.TP
+\fB\-r\fR, \fB\-\-resize\fR
+rather than format the disk, it can be resized. This means changing the
+number of blocks on the device reported by the READ CAPACITY command.
+This option should be used with the \fI\-\-count=COUNT\fR option.
+The contents of all logical blocks on the media remain unchanged when
+this option is used. This means that any resize operation can be
+reversed. This option cannot be used together with either \fI\-\-format\fR
+or a \fI\-\-size=LB_SZ\fR whose argument is different to the existing block
+size.
+.TP
+\fB\-R\fR, \fB\-\-rto_req\fR
+The option is deprecated, use the \fI\-\-fmtpinfo=FPI\fR option instead.
+If used, then it sets bit 6 of byte 1 in the FORMAT UNIT cdb.
+.TP
+\fB\-S\fR, \fB\-\-security\fR
+sets the "Security Initialization" (SI) bit in the FORMAT UNIT command's
+initialization pattern descriptor within the parameter list. According
+to SBC\-3 the default initialization pattern "shall be written using a
+security erasure write technique". See the NOTES section on the SCSI
+SANITIZE command. If this option is given then the \fI\-\-ip_def\fR option
+cannot be given.
+.TP
+\fB\-6\fR, \fB\-\-six\fR
+Use 6 byte variants of MODE SENSE and MODE SELECT. The default action
+is to use the 10 byte variants. Some MO drives need this option set
+when doing a format.
+.TP
+\fB\-s\fR, \fB\-\-size\fR=\fILB_SZ\fR
+where \fILB_SZ\fR is the logical block size (i.e. number of user bytes in each
+block) to format the device to. The default value is whatever is currently
+reported by the block descriptor in a MODE SENSE command. If the block size
+given by this option is different from the current value then a MODE SELECT
+command is used to change it prior to the FORMAT UNIT command being
+started (as recommended in the SBC standards). Some SCSI disks have 512 byte
+logical blocks by default and allow an alternate logical block size of 4096
+bytes. If the given size in unacceptable to the disk, most likely an "Invalid
+field in parameter list" message will appear in sense data (requires the
+use of '\-v' to decode sense data).
+.br
+Note that formatting a disk to add or remove protection information is not
+regarded as a change to its logical block size so this option should not
+be used.
+.TP
+\fB\-T\fR, \fB\-\-tape\fR=\fIFM\fR
+will send a FORMAT MEDIUM command to the \fIDEVICE\fR with its FORMAT field
+set to \fIFM\fR. This option is used to prepare a tape (i.e. the "medium")
+in a tape drive for use. Values for \fIFM\fR include 0 to do the "default"
+format; 1 to partition a volume and 2 to do a default format then partition.
+.TP
+\fB\-m\fR, \fB\-\-timeout\fR=\fISECS\fR
+where \fISECS\fR is the FORMAT UNIT, FORMAT WITH PRESET or FORMAT MEDIUM
+command timeout in seconds. \fISECS\fR will only be used if it exceeds the
+internal timeout which is 20 seconds if the IMMED bit is set and 72000
+seconds (20 hours) or higher if the IMMED bit is not set. If the disk size
+exceeds 4 TB then the timeout value is increased to 144000 seconds (40 hours).
+And if it is greater than 8 TB then the timeout value is increased to
+288000 seconds (80 hours). If the timeout is exceeded then the operating
+system will typically abort the command. Aborting a command may escalate to
+a LUN reset (or worse). A timeout may also leave the disk or tape format
+operation incomplete. And that may result in the disk or tape being in
+a "format corrupt" state requiring another format to remedy the situation.
+So for various reasons command timeouts are best avoided.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output). "\-vvv" gives
+a lot more debug output.
+.TP
+\fB\-y\fR, \fB\-\-verify\fR
+set the VERIFY bit in the FORMAT MEDIUM cdb. The default is that the VERIFY
+bit is clear. This option is only appropriate for tapes.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-w\fR, \fB\-\-wait\fR
+the default format action is to set the "IMMED" bit in the FORMAT UNIT
+command's (short) parameter header. If this option (i.e. \fI\-\-wait\fR) is
+given then the "IMMED" bit is not set. If \fI\-\-wait\fR is given then the
+FORMAT UNIT, FORMAT WITH PRESET or FORMAT MEDIUM command waits until the
+format operation completes before returning its response. This can be many
+hours on large disks. See the \fI\-\-timeout=SECS\fR option.
+.br
+Alternatively this option may be useful when used together with
+\fI\-\-ffmt=FFMT\fR (and \fIFFMT\fR greater than 0) since the fast format
+may only be a matter of seconds.
+.SH LISTS
+The SBC\-3 draft (revision 20) defines PLIST, CLIST, DLIST and GLIST in
+section 4.10 on "Medium defects". Briefly, the PLIST is the "primary"
+list of manufacturer detected defects, the CLIST ("certification" list)
+contains those detected during the format operation, the DLIST is a list of
+defects that can be given to the format operation. The GLIST is the grown
+list which starts in the format process as CLIST+DLIST and can "grow" later
+due to automatic reallocation (see the ARRE and AWRE bits in the
+Read\-Write Error Recovery mode page (see sdparm(8))) and use of the
+SCSI REASSIGN BLOCKS command (see sg_reassign(8)).
+.PP
+By the SBC\-3 standard (following draft revision 36) the CLIST and DLIST
+had been removed, leaving PLIST and GLIST. Only PLIST and GLIST are found
+in the SBC\-4 drafts.
+.PP
+The CMPLST bit (controlled by the \fI\-\-cmplst=\fR0|1 option) determines
+whether the existing GLIST, when the format operation is invoked,
+is taken into account. The sg_format utility sets the FOV bit to zero
+which causes DPRY=0, so the PLIST is taken into account, and DCRT=0, so
+the CLIST is generated and used during the format process.
+.PP
+The sg_format utility does not permit a user to provide a defect
+list (i.e. DLIST).
+.SH PROTECTION INFORMATION
+Protection Information (PI) is additional information held with logical
+blocks so that an application and/or host bus adapter can check the
+correctness of those logical blocks. PI is placed in one or more
+protection intervals interleaved in each logical block. Each protection
+interval follows the user data to which it refers. A protection interval
+contains 8 bytes made up of a 2 byte "logical block guard" (CRC), a 2
+byte "logical block application guard", and a 4 byte "logical block
+reference tag". Devices with 512 byte logical block size typically have
+one protection interval appended, making its logical block data 520 bytes
+long. Devices with 4096 byte logical block size often have 8 protection
+intervals spread across its logical block data for a total size of 4160
+bytes. Note that for all other purposes the logical block size is considered
+to be 512 and 4096 bytes respectively.
+.PP
+The SBC\-3 standard have added several "protection types" to the PI
+introduced in the SBC\-2 standard. SBC\-3 defines 4 protection types (types
+0 to 3) with protection type 0 meaning no PI is maintained. While a device
+may support one or more protection types, it can only be formatted with 1
+of the 4. To change a device's protection type, it must be re\-formatted.
+For more information see the Protection Information in section 4.21 of
+draft SBC\-4 revision 16.
+.PP
+A device that supports PI information (i.e. supports one or more protection
+types 1, 2 and 3) sets the "PROTECT" bit in its standard INQUIRY response. It
+also sets the SPT field in the EXTENDED INQUIRY VPD page response to indicate
+which protection types it supports. Given PROTECT=1 then SPT=0 implies the
+device supports PI type 1 only, SPT=1 implies the device supports PI types 1
+and 2, and various other non\-obvious mappings up to SPT=7 which implies
+protection types 1, 2 and 3 are supported. The
+.B current
+protection type of a disk can be found in the "P_TYPE" and "PROT_EN"
+fields in the response of a READ CAPACITY (16) command (e.g. with
+the 'sg_readcap \-\-long' utility).
+.PP
+Given that a device supports a particular protection type, a user can
+then choose to format that disk with that protection type by setting
+the "FMTPINFO" and "Protection Field Usage" fields in the FORMAT UNIT
+command. Those fields correspond to the \fI\-\-fmtpinfo=FPI\fR and the
+\fI\-\-pfu=PFU\fR options in this utility. The list below shows the four
+protection types followed by the options of this utility needed to select
+them:
+.br
+  \fB0\fR : \-\-fmtpinfo=0 \-\-pfu=0
+.br
+  \fB1\fR : \-\-fmtpinfo=2 \-\-pfu=0
+.br
+  \fB2\fR : \-\-fmtpinfo=3 \-\-pfu=0
+.br
+  \fB3\fR : \-\-fmtpinfo=3 \-\-pfu=1
+.br
+The default value of \fIFPI\fR (in \fI\-\-fmtpinfo=FPI\fR) is 0 and the
+default value of \fIPFU\fR (in \fI\-\-pfu=PFU\fR) is 0. So if neither
+\fI\-\-fmtpinfo=FPI\fR nor \fI\-\-pfu=PFU\fR are given then protection
+type 0 (i.e. no protection information) is chosen.
+.SH NOTES
+After a format that changes the logical block size or the number of logical
+blocks on a disk, the operating system may need to be told to re\-initialize
+its setting for that disk. In Linux that can be done with:
+.br
+    echo 1 > /sys/block/sd{letter(s)}/device/rescan
+.br
+where "letter(s)" will be between 'a' and 'zzz'. The lsscsi utility in Linux
+can be used to check the various namings of a disk.
+.PP
+The SBC\-2 standard states that the REQUEST SENSE command should be used
+for obtaining progress indication when the format command is underway.
+However, tests on a selection of disks shows that TEST UNIT READY
+commands yield progress indications (but not REQUEST SENSE commands). So
+the current version of this utility defaults to using TEST UNIT READY
+commands to poll the disk to find out the progress of the format. The
+\fI\-\-poll=PT\fR option has been added to control this.
+.PP
+When the \fI\-\-format\fR, \fI\-\-preset=ID\fR or \fI\-\-tape=FM\fR option
+is given without the \fI\-\-wait\fR option then the corresponding SCSI
+command is issued with the IMMED bit set which causes the SCSI command to
+return after it has started the format operation. The \fI\-\-early\fR option
+will cause sg_format to exit at that point. Otherwise the \fIDEVICE\fR is
+polled every 60 seconds or every 10 seconds if \fIFFMT\fR is non\-zero. The
+poll is with TEST UNIT READY or REQUEST SENSE commands until one reports
+an "all clear" (i.e. the format operation has completed). Normally these
+polling commands will result in a progress indicator (expressed as a
+percentage) being output to the screen. If the user gets bored watching the
+progress report then sg_format process can be terminated (e.g. with
+control\-C) without affecting the format operation which continues. However
+a target or device reset (or a power cycle) will probably cause the format
+to cease and the \fIDEVICE\fR to become "format corrupt".
+.PP
+When the \fI\-\-format\fR (\fI\-\-preset=ID\fR or \fI\-\-tape\fR) and
+\fI\-\-wait\fR options are both given then this utility may take a long time
+to return. In this case care should be taken not to send any other SCSI
+commands to the disk as it may not respond leaving those commands queued
+behind the active format command. This may cause a timeout in the OS
+driver (in a lot shorter period than 20 hours applicable to some format
+operations). This may result in the OS resetting the disk leaving the format
+operation incomplete. This may leave the disk in a "format corrupt" state
+requiring another format to remedy the situation. Modern SCSI devices should
+yield a "not ready" sense key with an additional sense indicating a format
+is in progress. With older devices the user should take precautions that
+nothing attempts to access a device while it is being formatted. Unmounting
+in mounted file systems on a \fIDEVICE\fR prior to calling this utility
+is strongly advised.
+.PP
+When the block size (i.e. the number of bytes in each block) is changed
+on a disk two SCSI commands must be sent: a MODE SELECT to change the block
+size followed by a FORMAT command. If the MODE SELECT command succeeds and
+the FORMAT fails then the disk may be in a state that the standard
+calls "format corrupt". A block descriptor in a subsequent MODE SENSE
+will report the requested new block size while a READ CAPACITY command
+will report the existing (i.e. previous) block size. Alternatively
+the READ CAPACITY command may fail, reporting the device is not ready,
+potentially requiring a format. The solution to this situation is to
+do a format again (and this time the new block size does not have to
+be given) or change the block size back to the original size.
+.PP
+The SBC\-2 standard states that the block count can be set back to the
+manufacturer's maximum recommended value in a format or resize operation.
+This can be done by placing an address of 0xffffffff (or the 64 bit
+equivalent) in the appropriate block descriptor field to a MODE SELECT
+command. In signed (two's complement) arithmetic that value corresponds
+to '\-1'. So a \-\-count=\-1 causes the block count to be set back to
+the manufacturer's maximum recommended value. To see exactly which SCSI
+commands are being executed and parameters passed add the "\-vvv" option to
+the sg_format command line.
+.PP
+The FMTDATA field shown in the FORMAT UNIT cdb does not have a corresponding
+option in this utility. When set in the cdb it indicates an additional
+parameter list will be sent to the \fIDEVICE\fR along with the cdb. It is set
+as required, basically when any field in the parameter list header is set.
+.PP
+Short stroking is a technique to trade off capacity for performance on
+hard disks. "Hard" disk is often used to mean a storage device with
+spinning platters which contain the user data. Solid State Disk (SSD) is
+the newer form of storage device that contains no moving parts. Hard disk
+performance is usually highest on the outer tracks (usually the lower logical
+block addresses) so by resizing or reformatting a disk to a smaller capacity,
+average performance will usually be increased.
+.PP
+Other utilities may be useful in finding information associated with
+formatting. These include sg_inq(8) to fetch standard INQUIRY
+information (e.g. the PROTECT bit) and to fetch the EXTENDED INQUIRY
+VPD page (e.g. RTO and GRD_CHK bits). The sdparm(8) utility can be
+used to access and potentially change the now obsolete format mode page.
+.PP
+scsiformat is another utility available for formatting SCSI disks
+with Linux. It dates from 1997 (most recent update) and may be useful for
+disks whose firmware is of that vintage.
+.PP
+The \fICOUNT\fR numeric argument may include a multiplicative suffix or be
+given in hexadecimal. See the "NUMERIC ARGUMENTS" section in the
+sg3_utils(8) man page.
+.PP
+The SCSI SANITIZE command was introduced in SBC\-3 revision 27. It is closely
+related to the ATA sanitize disk feature set and can be used to remove all
+existing data from a disk. Sanitize is more likely to be implemented on
+modern disks (including SSDs) than FORMAT UNIT's security initialization
+feature (see the \fI\-\-security\fR option) and in some cases much faster.
+.PP
+SSDs that support thin provisioning will typically unmap all logical blocks
+during a format. The reason is to improve the SSD's endurance. Also thin
+provisioned formats typically complete faster than fully provisioned ones
+on the same disk (see the \fI\-\-ip_def\fR option). In either case format
+operations on SSDs tend to be a lot faster than they are on hard disks with
+spinning media.
+.PP
+Host managed zoned devices (aka zoned disks) have a different Peripheral
+Device Type [PDT=20 or 0x14] from normal disks. They can be considered
+as a superset of normal disks (e.g. SSDs and hard disks) at least from
+the perspective of the number of SCSI commands they support. Typically
+they can be formatted just like other SCSI disks. They have their own
+T10 standards: ZBC standard (INCITS 536\-2016) and draft ZBC\-2.
+.br
+Two other zoned disk variants ("host aware" and "Domains and Realms") use
+the same PDT as other disks (i.e. PDT=0) and can be formatted by this
+utility as if they were normal disks.
+.SH TAPE
+Tape system use a variant of the FORMAT UNIT command used on disks. Tape
+systems use the FORMAT MEDIUM command which is simpler with only three
+fields in the cdb typically used. Apart from sharing the same opcode the
+cdbs of FORMAT UNIT and FORMAT MEDIUM are quite different. FORMAT MEDIUM's
+fields are VERIFY, IMMED and FORMAT (with TRANSFER LENGTH always set to 0).
+The VERIFY bit field is set with the \fI\-\-verify\fR option. The IMMED bit
+is manipulated by the \fI\-\-wait\fR option in the same way it is for disks;
+one difference is that if the \fI\-\-poll=PT\fR option is not given then it
+defaults to \fIPT\fR of 1 which means the poll is done with REQUEST SENSE
+commands.
+.PP
+The argument given to the \fI\-\-tape=FM\fR option is used to set the FORMAT
+field. \fIFM\fR can take values from "\-1" to "15" where "\-1" (the default)
+means don't do a tape format; value "8" to "15" are for vendor specific
+formats. The \fI\-\-early\fR option may also be used to set the IMMED
+bit and then exit this utility (rather than poll periodically until it is
+finished). In this case the tape drive will still be busy doing the format
+for some time but, according to T10, should still respond in full to the
+INQUIRY and REPORT LUNS commands. Other commands (including REQUEST SENSE)
+should yield a "not ready" sense key with an additional sense code
+of "Logical unit not ready, format in progress". Additionally REQUEST SENSE
+should contain a progress indication in its sense data.
+.PP
+When \fIFM\fR is 1 or 2 then the settings in the Medium partition mode page
+control the partitioning. That mode page can be viewed and modified with the
+sdparm utility.
+.PP
+Prior to invoking this utility the tape may need to be positioned to the
+beginning of partition 0. In Linux that can typically be done with the mt
+utility (e.g. 'mt \-f /dev/st0 rewind').
+.SH EXAMPLES
+These examples use Linux device names. For suitable device names in
+other supported Operating Systems see the sg3_utils(8) man page.
+.PP
+In the first example below simply find out the existing block count and
+size derived from two sources: a block descriptor in a MODE SELECT command
+response and from the response of a READ CAPACITY commands. No changes
+are made:
+.PP
+   # sg_format /dev/sdm
+.PP
+Now a simple format, leaving the block count and size as they were previously.
+The FORMAT UNIT command is executed in IMMED mode and the device is polled
+every 60 seconds to print out a progress indication:
+.PP
+   # sg_format \-\-format /dev/sdm
+.PP
+Now the same format, but waiting (passively) until the format operation is
+complete:
+.PP
+   # sg_format \-\-format \-\-wait /dev/sdm
+.PP
+Next is a format in which the block size is changed to 520 bytes and the block
+count is set to the manufacturer's maximum value (for that block size). Note,
+not all disks support changing the block size:
+.PP
+   # sg_format \-\-format \-\-size=520 /dev/sdm
+.PP
+Now a resize operation so that only the first 0x10000 (65536) blocks on a disk
+are accessible. The remaining blocks remain unaltered.
+.PP
+   # sg_format \-\-resize \-\-count=0x10000 /dev/sdm
+.PP
+Now resize the disk back to its normal (maximum) block count:
+.PP
+   # sg_format \-\-resize \-\-count=\-1 /dev/sdm
+.PP
+One reason to format a SCSI disk is to add protection information. First
+check which protection types are supported by a disk (by checking the SPT
+field in the Extended inquiry VPD page together with the Protect bit in the
+standard inquiry response):
+.PP
+   # sg_vpd \-p ei \-l /dev/sdb
+.br
+   extended INQUIRY data VPD page:
+.br
+     ACTIVATE_MICROCODE=0
+.br
+     SPT=1 [protection types 1 and 2 supported]
+.br
+     ....
+.PP
+Format with type 1 protection:
+.PP
+   # sg_format \-\-format \-\-fmtpinfo=2 /dev/sdm
+.PP
+After a successful format with type 1 protection, READ CAPACITY(16)
+should show something like this:
+.PP
+   # sg_readcap \-l /dev/sdm
+.br
+   Read Capacity results:
+.br
+      Protection: prot_en=1, p_type=0, p_i_exponent=0 [type 1 protection]
+.br
+      Logical block provisioning: lbpme=0, lbprz=0
+.br
+      ....
+.PP
+To format with type 3 protection:
+.PP
+   # sg_format \-\-format \-\-fmtpinfo=3 \-\-pfu=1 /dev/sdm
+.PP
+For the disk shown above this will probably fail because the Extended inquiry
+VPD page showed only types 1 and 2 protection are supported.
+.PP
+Here are examples of using fast format (FFMT field in FORMAT UNIT cdb) to
+quickly switch between 512 and 4096 byte logical block size. Assume disk
+starts with 4096 byte logical block size and all important data has been
+backed up.
+.PP
+   # sg_format \-\-format \-\-ffmt=1 \-\-size=512 /dev/sdd
+.PP
+Now /dev/sdd should have 512 byte logical block size. And to switch it back:
+.PP
+   # sg_format \-\-format \-\-ffmt=1 \-\-size=4096 /dev/sdd
+.PP
+Since fast formats can be very quick (a matter of seconds) using the
+\-\-wait option may be appropriate.
+.PP
+And to use the Format with preset command this invocation could be used:
+.PP
+   # sg_format \-\-preset=1 \-\-fmtmaxlba /dev/sdd
+.PP
+The FORMAT PRESETS VPD page (0xb8) should be consulted to check that Preset
+identifier 0x1 is there and has the expected format (i.e. "default host aware
+zoned block device model with 512 bytes of user data in each logical block").
+That VPD page can be viewed with the sg_vpd utility.
+.SH EXIT STATUS
+The exit status of sg_format is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page. Unless the \fI\-\-wait\fR option is given, the
+exit status may not reflect the success of otherwise of the format.
+Using sg_turs(8) and sg_readcap(8) after the format operation may be wise.
+.PP
+The Unix convention is that "no news is good news" but that can be a bit
+unnerving after an operation like format, especially if it finishes
+quickly (i.e. before the first progress poll is sent). Giving the
+\fI\-\-verbose\fR option once should supply enough additional output to
+settle those nerves.
+.SH AUTHORS
+Written by Grant Grundler, James Bottomley and Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2005\-2022 Grant Grundler, James Bottomley and Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_turs(8), sg_requests(8), sg_inq(8), sg_modes(8), sg_vpd(8),
+.B sg_reassign(8), sg_readcap(8), sg3_utils(8),
+.B sg_sanitize(8) [all in sg3_utils],
+.B lsscsi(8), mt(mt\-st), sdparm(8), scsiformat (old), hdparm(8)
diff --git a/doc/sg_get_config.8 b/doc/sg_get_config.8
new file mode 100644
index 0000000..3716ed6
--- /dev/null
+++ b/doc/sg_get_config.8
@@ -0,0 +1,143 @@
+.TH SG_GET_CONFIG "8" "December 2012" "sg3_utils\-1.35" SG3_UTILS
+.SH NAME
+sg_get_config \- send SCSI GET CONFIGURATION command (MMC\-4 +)
+.SH SYNOPSIS
+.B sg_get_config
+[\fI\-\-brief\fR] [\fI\-\-current\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-inner\-hex\fR] [\fI\-\-list\fR] [\fI\-\-raw\fR] [\fI\-\-readonly\fR]
+[\fI\-\-rt=RT\fR] [\fI\-\-starting=FC\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI GET CONFIGURATION command to \fIDEVICE\fR and decodes the
+response. The response includes the features and profiles of the device.
+Typically these devices are CD, DVD, HD\-DVD and BD players that may (but
+not necessarily) have media in them. These devices may well be connected via
+ATAPI, USB or IEEE 1394 transports. In such cases they are "SCSI" devices
+only in the sense that they use the "Multi\-Media command" set (MMC).
+MMC is a specialized SCSI command set whose definition can be found
+at https://www.t10.org .
+.PP
+This utility is based on the MMC\-4 and later draft standards. See section
+5 on "Features and Profile for Multi_Media devices" for more information on
+specific feature parameters and profiles. The manufacturer's product manual
+may also be useful.
+.PP
+Since modern DVD and BD writers support many features and profiles, the
+decoded output from this utility can be large. There are various ways to cut
+down the output. If the \fI\-\-brief\fR option is used only the feature names
+are shown and the feature parameters are not decoded. Alternatively if only
+one feature is of interest then this combination of options is
+appropriate: "\-\-rt=2 \-\-starting=\fIFC\fR". Another possibility is to show
+only the features that are relevant to the media in the drive (i.e. "current")
+with the "\-\-rt=1" option.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-brief\fR
+show the feature names but don't decode the parameters of those features.
+When used with \fI\-\-list\fR outputs known feature names but not known
+profile names.
+.TP
+\fB\-c\fR, \fB\-\-current\fR
+output features marked as current. This option is equivalent to '\-\-rt=1'.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response in hex (don't decode response).
+.TP
+\fB\-i\fR, \fB\-\-inner\-hex\fR
+decode to the feature name level then output each feature's data in hex.
+.TP
+\fB\-l\fR, \fB\-\-list\fR
+list all known feature and profile names. Ignore the device name (if given).
+Simply lists the feature names and profiles (followed by their hex values)
+that this utility knows about. If \fI\-\-brief\fR is also given then only
+feature names are listed.
+.TP
+\fB\-q\fR, \fB\-\-readonly\fR
+opens the DEVICE read\-only rather than read\-write which is the
+default. The Linux sg driver needs read\-write access for the SCSI
+GET CONFIGURATION command but other access methods may require
+read\-only access.
+.TP
+\fB\-r\fR, \fB\-\-rt\fR=\fIRT\fR
+where \fIRT\fR is the field of that name in the GET CONFIGURATION cdb.
+Allowable values are 0, 1, 2, or 3 . The command's action also depends on
+the value given to the \fI\-\-starting=FC\fR option. The default value is 0.
+When \fIRT\fR is 0 then all features, regardless of currency, are
+returned (whose feature code is greater than or equal to \fIFC\fR given
+to \fI\-\-starting=\fR). When \fIRT\fR is 1 then all current features are
+returned (whose feature code is greater than or equal to \fIFC\fR). When
+\fIRT\fR is 2 then the feature whose feature code is equal to \fIFC\fR,
+if any, is returned.  When \fIRT\fR is 3 the response is reserved (probably
+yields an "illegal field in cdb" error). To simplify the meanings of the
+\fIRT\fR values are:
+.br
+  \fB0\fR : all features, current on not
+.br
+  \fB1\fR : only current features
+.br
+  \fB2\fR : only feature whose code is \fIFC\fR
+.br
+  \fB3\fR : reserved
+.br
+.TP
+\fB\-R\fR, \fB\-\-raw\fR
+output response in binary (to stdout). Note that the short form is \fI\-R\fR
+unlike most other utilities in this package that use \fI\-r\fR for this
+action.
+.TP
+\fB\-s\fR, \fB\-\-starting\fR=\fIFC\fR
+where \fIFC\fR is the feature code value. This option works closely with
+the \fI\-\-rt=RT\fR option. The \fIFC\fR value is in the range 0 to
+65535 (0xffff) inclusive. Its default value is 0. A value prefixed
+with "0x" (or a trailing 'h') is interpreted as hexadecimal.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+There are multiple versions of the MMC (draft) standards: MMC [1997],
+MMC\-2 [2000],  MMC\-3 [2002], MMC\-4 and MMC\-5. The first three are now
+ANSI INCITS standards with the year they became standards shown in
+brackets. The draft immediately prior to standardization can
+be found at https://www.t10.org . In the initial MMC standard there
+was no GET CONFIGURATION command and the relevant information was
+obtained from the "CD capabilities and mechanical status mode
+page" (mode page 0x2a). It was later renamed the "MM capabilities and
+mechanical status mode page" and has been made obsolete in MMC\-4 and
+MMC\-5. The GET CONFIGURATION command was introduced in MMC\-2 and has
+become a replacement for that mode page. New features such as support
+for "BD" (blue ray) media type can only be found by using the
+GET CONFIGURATION command. Hence older CD players may not support
+the GET CONFIGURATION command in which case the "MM capabilities ..."
+mode page can be checked with sdparm(8), sginfo(8) or sg_modes(8).
+.PP
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be
+a SCSI generic (sg) device. In the 2.6 series block devices
+can also be specified. For example "sg_get_config /dev/hdc"
+will work in the 2.6 series kernels as long as /dev/hdc is
+an ATAPI device. In the 2.6 series external DVD writers attached
+via USB could be queried with "sg_get_config /dev/scd1" for example.
+.SH EXIT STATUS
+The exit status of sg_get_config is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2012 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sginfo(8), sg_modes(8), sg_inq(8), sg_prevent(8),
+.B sg_start(8) [all in sg3_utils],
+.B sdparm(8)
diff --git a/doc/sg_get_elem_status.8 b/doc/sg_get_elem_status.8
new file mode 100644
index 0000000..b45ae13
--- /dev/null
+++ b/doc/sg_get_elem_status.8
@@ -0,0 +1,137 @@
+.TH SG_GET_ELEM_STATUS "8" "November 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_get_elem_status \- send SCSI GET PHYSICAL ELEMENT STATUS command
+.SH SYNOPSIS
+.B sg_get_elem_status
+[\fI\-\-brief\fR] [\fI\-\-filter=FLT\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-inhex=FN\fR] [\fI\-\-json[=JO\fR]] [\fI\-\-maxlen=LEN\fR]
+[\fI\-\-raw\fR] [\fI\-\-readonly\fR] [\fI\-\-report\-type=RT\fR]
+[\fI\-\-starting=ELEM\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send the SCSI GET PHYSICAL ELEMENT STATUS command to the \fIDEVICE\fR and
+output the response. That command was introduced in (draft) SBC\-4 revision
+16.
+.PP
+T10 drafts now speak of both 'physical' and 'storage' elements. The latter
+term is more specific (i.e. storage elements are a sub\-set of physical
+elements) and refers to disk resources that control user data storage. An
+example of a storage element is the user data associated with a head on a
+spinning hard disk. When a storage element has been "depopulated" its former
+storage accessed via LBAs is no longer available. Physical elements are more
+general and includes storage elements and might include disk resources used
+for "saved" mode page settings amongst other things.
+.PP
+The default action of this utility is to decode the response into a header
+and up to 32 physical element status descriptors. The status descriptors are
+output one per line. The amount of output can be reduced by the
+\fI\-\-brief\fR option.
+.PP
+Rather than send this SCSI command to \fIDEVICE\fR, if the \fI\-\-inhex=FN\fR
+option is given, then the contents of the file named \fIFN\fR are decoded
+as ASCII hex (or binary if \fI\-\-raw\fR is also given) and then processed
+as if it was the response of the GET PHYSICAL ELEMENT STATUS command.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-brief\fR
+when used once, the output of each physical element status descriptor is
+reduced to: <element_id>: <element_type>,<element_health> . All three are
+output as decimal integers. When used twice the "Element descriptors:"
+line introducing the status descriptors is not output. When used three
+or more times only the response header is output.
+.TP
+\fB\-f\fR, \fB\-\-filter\fR=\fIFLT\fR
+where \fIFLT\fR is placed in a two bit field called FILTER in the GET
+PHYSICAL ELEMENT STATUS command. Only two values are defined for that
+field: 0 for all element descriptors; 1 for those element descriptors that
+are outside 'spec' or have depopulation information to report. In both cases
+the REPORT TYPE and STARTING ELEMENT fields may further restrict (reduce)
+the number of element descriptors returned. The default value is zero.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output response to this command in ASCII hex. Each line of 16 bytes is
+preceded by an address or index, starting at 0 and the address is also in
+hex. If given twice then an ASCII rendering of each byte is appended to the
+line output. If given three or more times then only the ASCII hex of each
+byte is output, 16 bytes per line (i.e. so no leading address nor trailing
+ASCII rendering). This latter form is suitable for placing in a file and
+being used with the \fI\-\-inhex=FN\fR option in a later invocation.
+.TP
+\fB\-i\fR, \fB\-\-inhex\fR=\fIFN\fR
+where \fIFN\fR is a file name whose contents are assumed to be ASCII
+hexadecimal. If \fIDEVICE\fR is also given then \fIDEVICE\fR is ignored,
+a warning is issued and the utility continues, decoding the file named
+\fIFN\fR. See the "FORMAT OF FILES CONTAINING ASCII HEX" section in the
+sg3_utils manpage for more information. If the \fI\-\-raw\fR option is
+also given then the contents of \fIFN\fR are treated as binary.
+.TP
+\fB\-j\fR, \fB\-\-json[\fR=\fIJO\fR]
+output is in JSON format instead of human readable form. See sg3_utils_json
+manpage or use '?' for \fIJO\fR for a summary.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given then 1056 is used. 1056 is
+enough space for the response header plus 32 physical element status
+descriptors. \fILEN\fR should be a multiple of 32 (e.g. 32, 64, and 96 are
+suitable).
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary (to stdout) unless the \fI\-\-inhex=FN\fR option
+is also given. In that case the input file name (\fIFN\fR) is decoded as
+binary (and the output is _not_ in binary).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-t\fR, \fB\-\-report\-type\fR=\fIRT\fR
+where \fIRT\fR will be placed in the REPORT TYPE field of the GET PHYSICAL
+ELEMENT STATUS command. Currently only two values are defined: 0
+for 'physical element' and 1: for 'storage element'. The default value
+is 0 .
+.TP
+\fB\-s\fR, \fB\-\-starting\fR=\fIELEM\fR
+where \fIELEM\fR is placed in the STARTING ELEMENT field of the GET PHYSICAL
+ELEMENT STATUS command. Only physical elements with identifiers greater
+than, or equal to \fIELEM\fR are returned. The default value is zero
+which, while it isn't a valid element identifier (since they must be
+non\-zero), is given in an example in Annex L of SBC\-4 revision 17. So
+an \fIELEM\fR of zero is assumed to be valid in this context.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output). Additional output
+caused by this option is sent to stderr.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+The "Warning - physical element status change" additional sense code [0xb,
+0x14] is special and should prompt an application client to call the GET
+PHYSICAL ELEMENT STATUS command. How this warning is triggered depends on
+the settings in the Informational Exceptions Control mode page [0xc, 0x0].
+.PP
+After detecting one or more out\-of\-spec storage elements the disk in
+question should either be decommissioned or have the REMOVE ELEMENT AND
+TRUNCATE (or ... AND MODIFY ZONES) command invoked to repair (and reduce
+the storage capacity) of the disk.
+.SH EXIT STATUS
+The exit status of sg_get_elem_status is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2019\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_get_lba_status,sg3_utils,sg3_utils_json(sg3_utils)
diff --git a/doc/sg_get_lba_status.8 b/doc/sg_get_lba_status.8
new file mode 100644
index 0000000..0ccc70e
--- /dev/null
+++ b/doc/sg_get_lba_status.8
@@ -0,0 +1,161 @@
+.TH SG_GET_LBA_STATUS "8" "August 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_get_lba_status \- send SCSI GET LBA STATUS(16 or 32) command
+.SH SYNOPSIS
+.B sg_get_lba_status
+[\fI\-\-16\fR] [\fI\-\-32\fR] [\fI\-\-blockhex\fR] [\fI\-\-brief\fR]
+[\fI\-\-element-id=EI\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-inhex=FN\fR] [\fI\-\-json[=JO\fR]] [\fI\-\-lba=LBA\fR]
+[\fI\-\-maxlen=LEN\fR] [\fI\-\-raw\fR] [\fI\-\-readonly\fR]
+[\fI\-\-report\-type=RT\fR] [\fI\-\-scan-len=SL\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send the SCSI GET LBA STATUS(16) or GET LBA STATUS(32) command to the
+\fIDEVICE\fR and output the response. The 16 byte command variant was
+introduced in (draft) SBC\-3 revision 20 and devices that support logical
+block provisioning should support this command. The GET LBA STATUS(32)
+command was added in (draft) SBC\-4 revision 14.
+.PP
+The default action is to decode the response into one LBA status descriptor
+per line then output a header and the status descriptors to stdout. The
+descriptor LBA is output in hex (prefixed by '0x') and the number of blocks
+is output in decimal followed by the provisioning status and additional status
+in decimal. The provisioning status can be in the range 0 to 15 of which only
+0 (mapped or unknown), 1 (unmapped), 2 (anchored), 3 (mapped) and 4 (unknown)
+are used currently. The amount of output can be reduced by the
+\fI\-\-brief\fR option.
+.PP
+Rather than send this SCSI command to \fIDEVICE\fR, if the \fI\-\-inhex=FN\fR
+option is given, then the contents of the file named \fIFN\fR are decoded
+as ASCII hex and then processed if it was the response of this command.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-S\fR, \fB\-\-16\fR
+send SCSI GET LBA STATUS(16) command which is the 16 byte variant. In the
+absence of the \fI\-\-16\fR or the \fI\-\-32\fR options the SCSI GET LBA
+STATUS(16) command is sent. If both \fI\-\-16\fR and the \fI\-\-32\fR options
+are given then the GET LBA STATUS(16) command is sent.
+.TP
+\fB\-T\fR, \fB\-\-32\fR
+send SCSI GET LBA STATUS(32) command which is the 32 byte variant. When
+given together with the \fI\-\-16\fR option then this option is ignored (so
+the GET LBA STATUS(16) command is sent).
+.TP
+\fB\-b\fR, \fB\-\-brief\fR
+when use once then one LBA status descriptor per line is output to stdout.
+Each line has this
+format: "0x<descriptor_LBA>  0x<blocks> <provisioning_status>
+<additional_status>". So the descriptor's starting LBA and number of blocks
+are output in hex while the provisioning status and additional status are
+in decimal. When used twice (e.g. '\-bb' or '\-\-brief \-\-brief') then the
+provisioning status of the given \fILBA\fR (or LBA 0 if the \fI\-\-lba\fR
+option is not given) is output to stdout. A check is made that the given
+\fILBA\fR lies in the range of the first returned LBA status descriptor (as
+it should according to SBC\-3 revision 20) and warnings are sent to stderr
+if it doesn't.
+.TP
+\fB\-B\fR, \fB\-\-blockhex\fR
+the number of blocks in each LBA status descriptor is usually displayed in
+decimal. An exception is when the \fI\-\-brief\fR option is given in which
+case it is shown in hexadecimal. When the option is given once, both cases
+are output in hexadecimal. When the option is given twice, both cases are
+output in decimal.
+.TP
+\fB\-e\fR, \fB\-\-element\-id\fR=\fIEI\fR
+where \fIEI\fR is the element identifier of the physical element for which
+the LBAs shall be reported based on the value in the report type field (i.e.
+\fIRT\fR). This option is only active with the SCSI GET LBA STATUS(32)
+command (i.e. it is ignored if the GET LBA STATUS(16) command is sent).
+.br
+Valid element identifiers are non\-zero. The default value of \fIEI\fR is 0
+which means in the context that no element identifier is specified.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output response to this command in ASCII hex.
+.TP
+\fB\-i\fR, \fB\-\-inhex\fR=\fIFN\fR
+where \fIFN\fR is a filename whose contents are assumed to be ASCII
+hexadecimal bytes. See the "FORMAT OF FILES CONTAINING ASCII HEX" section
+in the sg3_utils manpage for more information. If \fIDEVICE\fR is also
+given then it is ignored. If the \fI\-\-raw\fR option is also given then
+the contents of \fIFN\fR are treated as binary.
+.TP
+\fB\-j\fR, \fB\-\-json[\fR=\fIJO\fR]
+output is in JSON format instead of human readable form. See sg3_utils_json
+manpage or use '?' for \fIJO\fR for a summary.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR is the starting Logical Block Address (LBA) to check the
+provisioning status for. Note that the \fIDEVICE\fR chooses how many
+following blocks that it will return provisioning status for.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given then 24 is used. 24 is
+enough space for the response header and one LBA status descriptor.
+\fILEN\fR should be 8 plus a multiple of 16 (e.g. 24, 40, and 56 are suitable).
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary (to stdout) unless the \fI\-\-inhex=FN\fR option
+is also given. In that case the input file name (\fIFN\fR) is decoded as
+binary (and the output is _not_ in binary).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-t\fR, \fB\-\-report\-type\fR=\fIRT\fR
+where \fIRT\fR is 0 for report all LBAs; 1 for report LBAs using non\-zero
+provisioning status; 2 for report LBAs that are mapped; 3 for report LBAs
+that are de\-allocated; 4 for report LBAs that are anchored; 16 for report
+LBAs that may return an unrecovered error. The REPORT TYPE field was added
+to the GET LBA STATUS cdb in sbc4r12.
+.br
+Since the REPORT TYPE field is newer than the command, the response contains
+the RTP bit to indicate whether or not the \fIDEVICE\fR acts on the REPORT
+TYE field (set when it does act on it, clear otherwise).
+.TP
+\fB\-s\fR, \fB\-\-scan\-len\fR=\fISL\fR
+where \fISL\fR is the scan length which is the maximum number of contiguous
+logical blocks to be scanned for logical blocks that meet the given report
+type (i.e. \fIRT\fR). This option is only active with the SCSI GET LBA
+STATUS(32) command (i.e. it is ignored if the GET LBA STATUS(16) command is
+sent).
+.br
+The default value of \fISL\fR is 0 which should be interpreted by the
+\fIDEVICE\fR as there is no limits to the number of LBAs that shall be
+scanned.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output). Additional output
+caused by this option is sent to stderr.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+In SBC\-3 revision 25 the calculation associated with the Parameter Data
+Length field in the response was modified. Prior to that the byte offset
+was 8 and in revision 25 it was changed to 4.
+.PP
+For a discussion of logical block provisioning see section 4.7 of sbc4r14.pdf
+at https://www.t10.org (or the corresponding section of a later draft).
+.SH EXIT STATUS
+The exit status of sg_get_lba_status is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2009\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_write_same,sg_unmap,sg3_utils,sg3_utils_json(sg3_utils)
diff --git a/doc/sg_ident.8 b/doc/sg_ident.8
new file mode 100644
index 0000000..d36a176
--- /dev/null
+++ b/doc/sg_ident.8
@@ -0,0 +1,119 @@
+.TH SG_IDENT "8" "August 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_ident \- send SCSI REPORT/SET IDENTIFYING INFORMATION command
+.SH SYNOPSIS
+.B sg_ident
+[\fI\-\-ascii\fR] [\fI\-\-clear\fR] [\fI\-\-help\fR] [\fI\-\-itype=IT\fR]
+[\fI\-\-raw\fR] [\fI\-\-set\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send a SCSI REPORT IDENTIFYING INFORMATION or SET IDENTIFYING INFORMATION
+command to \fIDEVICE\fR. Prior to SPC\-4 (revision 7) these
+commands were called REPORT DEVICE IDENTIFIER and SET DEVICE IDENTIFIER
+respectively. SCSI devices that support these two commands allow users
+to write (set) identifying information and report it back at some
+later time. The information is persistent (i.e. stored on some
+non\-volatile medium within the SCSI device that will survive a power
+outage).
+.PP
+Typically the space allocated for the information is limited:
+SPC\-4 (revision 7) states that for information type 0, the minimum
+length is 64 bytes and the maximum is 512 bytes. For other information
+types (1 to 126 inclusive) the maximum length is 256 bytes. Also
+information types 1 to 126 (inclusive) should contain a null
+terminated UTF\-8 string. The author has seen older disks that only
+support 16 bytes.
+.PP
+The default action when no options are given is to invoke the
+Report Identifying Information command with the information type defaulting
+to zero. Error reports are sent to stderr. By default the information is
+shown in ASCII\-HEX (up to 16 bytes per line) with an ASCII representation
+to the right with dots replacing non printable characters.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-A\fR, \fB\-\-ascii\fR
+invokes the Report Identifying Information command and if anything is
+found interprets it as ASCII (or UTF\-8 which is locale dependent) and
+prints the information to stdout.
+.TP
+\fB\-C\fR, \fB\-\-clear\fR
+invokes the Set Identifying Information command with an information length
+of zero. This has the effect of clearing the existing information.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-i\fR, \fB\-\-itype\fR=\fIIT\fR
+where \fIIT\fR is the information type. Defaults to zero. The maximum value
+is 127 which is special and cannot be used with \fI\-\-set\fR or
+\fI\-\-clear\fR. The information type of 127 (if supported) causes the REPORT
+IDENTIFYING INFORMATION command to respond with a list of available
+information types and their maximum lengths in bytes. The odd numbered
+information types between 3 and 125 (inclusive) are not to be used (as they
+clash with the SCC\-2 standard).
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+invokes the Report Identifying information command and if anything
+is found sends the information (which may be binary) to stdout. Nothing else
+is sent to stdout however error reports, if any, are sent to stderr.
+.TP
+\fB\-S\fR, \fB\-\-set\fR
+first reads stdin until an EOF is detected then invokes the Set Identifying
+Information command to set what has been fetched from stdin as the
+information. The amount of data read must be between 1 and 512 bytes
+length (inclusive).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.PP
+This utility permits users to write their own identifying information to
+their SCSI devices. There are several other types of descriptors (or
+designators) that the user cannot change. These include the SCSI INQUIRY
+command with its standard vendor and product identification strings and the
+product revision level; plus the large amount of information provided by
+the "Device Identification" VPD page (see sg_vpd). There is also the READ
+MEDIA SERIAL NUMBER command (see sg_rmsn). The MMC\-4 command set for CD
+and DVDs has a "media serial number" feature (0x109) [and a "logical unit
+serial number" feature]. These can be viewed with the sg_get_config utility.
+.SH EXAMPLES
+First, to see if there is an existing information whose format
+is unknown (for information type 0), use no options:
+.PP
+  # sg_ident /dev/sdb
+.br
+   00     31 32 33 34 35 36 37 38  39 30          1234567890
+.PP
+If it is ASCII then it can printed as such:
+.PP
+  # sg_ident \-\-ascii /dev/sdb
+.br
+  1234567890
+.PP
+The information can be copied to a file, cleared and then
+re\-asserted with this sequence:
+.PP
+  # sg_ident \-\-raw /dev/sdb > t
+.br
+  # sg_ident \-\-clear /dev/sdb
+.br
+  # cat t | sg_ident \-\-set /dev/sdb
+.SH EXIT STATUS
+The exit status of sg_ident is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2005\-2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_vpd(sg3_utils), sg_rmsn(sg3_utils), sg_get_config(sg3_utils)
diff --git a/doc/sg_inq.8 b/doc/sg_inq.8
new file mode 100644
index 0000000..d01d35c
--- /dev/null
+++ b/doc/sg_inq.8
@@ -0,0 +1,562 @@
+.TH SG_INQ "8" "August 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_inq \- issue SCSI INQUIRY command and/or decode its response
+.SH SYNOPSIS
+.B sg_inq
+[\fI\-\-ata\fR] [\fI\-\-block=0|1\fR] [\fI\-\-cmddt\fR]
+[\fI\-\-descriptors\fR] [\fI\-\-export\fR] [\fI\-\-extended\fR]
+[\fI\-\-force\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-id\fR]
+[\fI\-\-inhex=FN\fR] [\fI\-\-json[=JO]\fR] [\fI\-\-len=LEN\fR]
+[\fI\-\-long\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-only\fR] [\fI\-\-page=PG\fR]
+[\fI\-\-raw\fR] [\fI\-\-vendor\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+[\fI\-\-vpd\fR] \fIDEVICE\fR
+.PP
+.B sg_inq
+[\fI\-36\fR] [\fI\-a\fR] [\fI\-A\fR] [\fI\-b\fR] [\fI\-\-B=0|1\fR]
+[\fI\-c\fR] [\fI\-cl\fR] [\fI\-d\fR] [\fI\-e\fR] [\fI\-f\fR] [\fI\-h\fR]
+[\fI\-H\fR] [\fI\-i\fR] [\fI\-I=FN\fR] [\fI\-j[=LEN]\fR] [\fI\-l=LEN\fR]
+[\fI\-L\fR] [\fI\-m\fR] [\fI\-M\fR] [\fI\-o\fR] [\fI\-p=VPD_PG\fR]
+[\fI\-P\fR] [\fI\-r\fR] [\fI\-s\fR] [\fI\-u\fR] [\fI\-v\fR]
+[\fI\-V\fR] [\fI\-x\fR] [\fI\-36\fR] [\fI\-?\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility, when \fIDEVICE\fR is given, sends a SCSI INQUIRY command to it
+then outputs the response. All SCSI devices are meant to respond to
+a "standard" INQUIRY command with at least a 36 byte response (in SCSI 2 and
+higher). An INQUIRY is termed as "standard" when both the EVPD and CmdDt (now
+obsolete) bits are clear. Formally (i.e. as per SPC stardards) the name of
+a standard INQUIRY response is the "standard INQUIRY data format" but here
+the "standard INQUIRY response" is used as it is shorter and more descriptive.
+.PP
+Alternatively the \fI\-\-inhex=FN\fR option can be given. In this case
+\fIFN\fR is assumed to be a file name ('\-' for stdin) containing ASCII
+hexadecimal representing an INQUIRY response.
+.PP
+This utility supports two command line syntaxes. The preferred one is shown
+first in the synopsis and is described in the main OPTIONS section. A later
+section titled OLDER COMMAND LINE OPTIONS describes the second group of
+options.
+.PP
+An important "non\-standard" INQUIRY page is the Device Identification
+Vital Product Data (VPD) page [0x83]. Since SPC\-3, support for this page
+is mandatory. The \fI\-\-id\fR option decodes this page. New VPD page
+information is no longer being added to this utility. The sg_vpd(8) utility
+is specialized for decoding VPD pages and shares code with this utility for
+that purpose. The sdparm(8) utility which is in a package of that name also
+decodes VPD pages although its major purpose is to access and modify SCSI
+mode pages.
+.PP
+In Linux, if the \fIDEVICE\fR exists and the SCSI INQUIRY fails (e.g. because
+the SG_IO ioctl is not supported) then an ATA IDENTIFY (PACKET) DEVICE is
+tried. If it succeeds then device identification strings are output. The
+\fI\-\-raw\fR and \fI\-\-hex\fR options can be used to manipulate the output.
+If the \fI\-\-ata\fR option is given then the SCSI INQUIRY is not performed
+and the \fIDEVICE\fR is assumed to be ATA (or ATAPI). For more information
+see the ATA DEVICES section below.
+.PP
+In some operating systems a NVMe device (e.g. SSD) may be given as the
+\fIDEVICE\fR. For more information see the NVME DEVICES section below.
+.PP
+The reference document used for interpreting an INQUIRY is T10/BSR INCITS
+566 Revision 6 which is draft SPC\-6 dated 22 October 2021. It can be found
+at https://www.t10.org .  Obsolete and reserved items in the standard
+INQUIRY response output are displayed in square brackets.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-a\fR, \fB\-\-ata\fR
+Assume given \fIDEVICE\fR is an ATA or ATAPI device which can receive ATA
+commands from the host operating system. Skip the SCSI INQUIRY command and
+use either the ATA IDENTIFY DEVICE command (for non\-packet devices) or the
+ATA IDENTIFY PACKET DEVICE command. To show the response in hex, add
+a '\-\-verbose' option. This option is only available in Linux.
+.TP
+\fB\-B\fR, \fB\-\-block\fR=\fI0|1\fR
+this option controls how the file handle to the \fIDEVICE\fR is opened. If
+this argument is 0 then the open is non\-blocking. If the argument is 1 then
+the open is blocking. In Unix a non\-blocking open is indicated by a
+O_NONBLOCK flag while a blocking open is indicated by the absence of that
+flag. The default value depends on the operating system and the type of
+\fIDEVICE\fR node. For Linux pass\-throughs (i.e. the sg and bsg drivers)
+the default is 0.
+.TP
+\fB\-c\fR, \fB\-\-cmddt\fR
+set the Command Support Data (CmdDt) bit (defaults to clear(0)). Used in
+conjunction with the \fI\-\-page=PG\fR option where \fIPG\fR specifies the
+SCSI command opcode to query. When used twice (e.g. '\-cc') this utility
+forms a list by looping over all 256 opcodes (0 to 255 inclusive) only
+outputting a line for commands that are found. The CmdDt bit is now
+obsolete; it has been replaced by the REPORT SUPPORTED OPERATION CODES
+command, see the sg_opcodes(8) utility.
+.TP
+\fB\-d\fR, \fB\-\-descriptors\fR
+decodes and prints the version descriptors found in a standard INQUIRY
+response. There are up to 8 of them. Version descriptors indicate which
+versions of standards and/or drafts the \fIDEVICE\fR complies with. The
+normal components of a standard INQUIRY are output (typically from
+the first 36 bytes of the response) followed by the version descriptors
+if any.
+.TP
+\fB\-e\fR
+see entry below for \fI\-\-vpd\fR.
+.TP
+\fB\-f\fR, \fB\-\-force\fR
+As a sanity check, the normal action when fetching VPD pages other than
+page 0x0 (the "Supported VPD pages" VPD page), is to first fetch page 0x0
+and only if the requested page is one of the supported pages, to go ahead
+and fetch the requested page.
+.br
+When this option is given, skip checking of VPD page 0x0 before accessing
+the requested VPD page. The prior check of VPD page 0x0 is known to
+crash certain USB devices, so use with care.
+.TP
+\fB\-u\fR, \fB\-\-export\fR
+prints out information obtained from the device. The output can be
+modified by selecting a VPD page with \fIPG\fR (from
+\fI\-\-page=PG\fR). If the device identification VPD page 0x83 is
+given it prints out information in the form:
+"SCSI_IDENT_<assoc>_<type>=<ident>" to stdout. If the device serial
+number VPD page 0x80 is given it prints out information in the form:
+"SCSI_SERIAL=<ident>". Other VPD pages are not supported. If no VPD
+page is given it prints out information in the form:
+"SCSI_VENDOR=<vendor>", "SCSI_MODEL=<model>", and
+"SCSI_REVISION=<rev>", taken from the standard inquiry. This may be
+useful for tools like udev(7) in Linux.
+.TP
+\fB\-E\fR, \fB\-x\fR, \fB\-\-extended\fR
+prints the extended INQUIRY VPD page [0x86]. It has the same effect
+as giving the \fI\-\-page=ei\fR option.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit. When used twice, after the
+usage message, there is a list of available abbreviations than can be
+given to the \fI\-\-page=PG\fR option.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+rather than decode a standard INQUIRY response, a VPD page or command
+support data; print out the response in hex and send the output to stdout.
+Error messages and warnings are typically output to stderr. When used twice
+with the ATA Information VPD page [0x89] decodes the start of the response
+then outputs the ATA IDENTIFY (PACKET) DEVICE response in hexadecimal
+bytes (not 16 bit words). When used three times with the ATA Information VPD
+page [0x89] or the \fI\-\-ata\fR option, this utility outputs the ATA
+IDENTIFY (PACKET) DEVICE response in hexadecimal words suitable for input
+to 'hdparm \-\-Istdin'.  See note below.
+.br
+To generate output suitable for placing in a file that can be used by a
+later invocation with the \fI\-\-inhex=FN\fR option, use the '\-HHHH'
+option (e.g. 'sg_inq \-p di \-HHHH /dev/sg3 > dev_id.hex').
+.TP
+\fB\-i\fR, \fB\-\-id\fR
+prints the device identification VPD page [0x83]. It has the same effect
+as giving the \fI\-\-page=di\fR option.
+.TP
+\fB\-I\fR, \fB\-\-inhex\fR=\fIFN\fR
+\fIFN\fR is expected to be a file name (or '\-' for stdin) which contains
+ASCII hexadecimal or binary representing an INQUIRY (including VPD page)
+response. This utility will then decode that response. It is preferable to
+also supply the \fI\-\-page=PG\fR option, if not this utility will attempt
+to guess which VPD page (or standard INQUIRY) that the response is associated
+with. The hexadecimal should be arranged as 1 or 2 digits representing a
+byte each of which is whitespace or comma separated. Anything from and
+including a hash mark to the end of a line is ignored. If the \fI\-\-raw\fR
+option is also given then \fIFN\fR is treated as binary.
+.TP
+\fB\-j\fR, \fB\-\-json[\fR=\fIJO\fR]
+output is in JSON format instead of human readable form. See sg3_utils_json
+manpage or use '?' for \fIJO\fR for a summary.
+.TP
+\fB\-l\fR, \fB\-\-len\fR=\fILEN\fR
+the number \fILEN\fR is the "allocation length" field in the INQUIRY cdb.
+This is the (maximum) length of the response returned by the device. The
+default value of \fILEN\fR is 0 which is interpreted as: first request is
+for 36 bytes and if necessary execute another INQUIRY if the "additional
+length" field in the response indicates that more than 36 bytes is available.
+.br
+If \fILEN\fR is greater than 0 then only one INQUIRY command is performed.
+This means that the Serial Number (obtained from the Serial Number VPD
+pgae (0x80)) is not fetched and therefore not printed.
+See the NOTES section below about "36 byte INQUIRYs".
+.TP
+\fB\-L\fR, \fB\-\-long\fR
+this option causes more information to be decoded from the Identify command
+sent to a NVMe \fIDEVICE\fR.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+this option has the same action as the \fI\-\-len=LEN\fR option above. It has
+been added for compatibility with the sg_vpd, sg_modes and sg_logs utilities.
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option on the command line.
+.TP
+\fB\-o\fR, \fB\-\-only\fR
+Do not attempt to additionally retrieve the serial number VPD page (0x80) to
+enhance the output of a standard INQUIRY. So with this option given and no
+others, this utility will send a standard INQUIRY SCSI command and decode
+its response. No other SCSI commands will be sent to the \fIDEVICE\fR.
+Without this option an additional SCSI command is sent: a (non\-standard)
+SCSI INQUIRY to fetch the Serial Number VPD page. However the Serial
+Number VPD page is not mandatory (while the Device Identification page is
+mandatory but a billion USB keys ignore that) and may cause nuisance error
+reports.
+.br
+For NVMe devices only the Identify controller is performed, even if the
+\fIDEVICE\fR includes a namespace identifier. For example in FreeBSD
+given a \fIDEVICE\fR named /dev/nvme0ns1 then an Identify controller is
+sent to /dev/nvme0 and nothing is sent to its "ns1" (first namespace).
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPG\fR
+the \fIPG\fR argument can be either a number of an abbreviation for a VPD
+page. To enumerate the available abbreviations for VPD pages use '\-hh' or
+a bad abbreviation (e.g, '\-\-page=xxx'). When the \fI\-\-cmddt\fR option is
+given (once) then \fIPG\fR is interpreted as an opcode number (so VPD page
+abbreviations make little sense).
+.br
+If \fIPG\fR is a negative number, then a standard INQUIRY is performed. This
+can be used to override some guessing logic associated with the
+\fI\-\-inhex=FN\fR option.
+.br
+If \fIPG\fR is not found in the 'Supported VPD pages' VPD page (0x0) then
+EDOM is returned. To bypass this check use the \fI\-\-force\fR option.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+in the absence of \fI\-\-inhex=FN\fR then the output response is in binary.
+The output should be piped to a file or another utility when this option is
+used. The binary is sent to stdout, and errors are sent to stderr.
+.br
+If used with \fI\-\-inhex=FN\fR then the contents of \fIFN\fR is treated as
+binary.
+.TP
+\fB\-s\fR, \fB\-\-vendor\fR
+output a standard INQUIRY response's vendor specific fields from offset 36
+to 55 in ASCII. When used twice (i.e. '\-ss') also output the vendor
+specific field from offset 96 in ASCII. This is only done if the data
+passes some simple sanity checks.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string then exit.
+.TP
+\fB\-e\fR, \fB\-\-vpd\fR
+set the Enable Vital Product Data (EVPD) bit (defaults to clear(0)). Used in
+conjunction with the \fI\-\-page=PG\fR option where \fIPG\fR specifies the
+VPD page number to query. If the \fI\-\-page=PG\fR is not given then \fIPG\fR
+defaults to zero which is the "Supported VPD pages" VPD page.
+.SH NOTES
+Some devices with weak SCSI command set implementations lock up when they
+receive commands they don't understand (and some lock up if they receive
+response lengths that they don't expect). Such devices need to be treated
+carefully, use the '\-\-len=36' option. Without this option this utility will
+issue an initial standard INQUIRY requesting 36 bytes of response data. If
+the device indicates it could have supplied more data then a second INQUIRY
+is issued to fetch the longer response. That second command may lock up
+faulty devices.
+.PP
+ATA or ATAPI devices that use a SCSI to ATA Translation layer (see
+SAT at www.t10.org) may support the SCSI ATA INFORMATION VPD page. This
+returns the IDENTIFY (PACKET) DEVICE response amongst other things.
+The ATA Information VPD page can be fetched with '\-\-page=ai'.
+.PP
+In the INQUIRY standard response there is a 'MultiP' flag which is set
+when the device has 2 or more ports. Some vendors use the preceding
+vendor specific ('VS') bit to indicate which port is being accessed by
+the INQUIRY command (0 \-> relative port 1 (port "a"), 1 \-> relative
+port 2 (port "b")). When the 'MultiP' flag is set, the preceding vendor
+specific bit is shown in parentheses. SPC\-3 compliant devices should
+use the device identification VPD page (0x83) to show which port is
+being used for access and the SCSI ports VPD page (0x88) to show all
+available ports on the device.
+.PP
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be
+a SCSI generic (sg) device. In the 2.6 series and later block devices (e.g.
+disks and ATAPI DVDs) can also be specified. For example "sg_inq /dev/sda"
+will work in the 2.6 series kernels. From lk 2.6.6 other SCSI "char"
+device names may be used as well (e.g. "/dev/st0m").
+.PP
+The number of bytes output by \fI\-\-hex\fR and \fI\-\-raw\fR is 36 bytes
+or the number given to \fI\-\-len=LEN\fR (or \fI\-\-maxlen=LEN\fR). That
+number is reduced if the "resid" returned by the HBA indicates less bytes
+were sent back from \fIDEVICE\fR.
+.PP
+The \fIDEVICE\fR is opened with a read\-only flag (e.g. in Unix with the
+O_RDONLY flag).
+.SH ATA DEVICES
+There are two major types of ATA devices: non\-packet devices (e.g. ATA
+disks) and packet devices (ATAPI). The majority of ATAPI devices are
+CD/DVD/BD drives in which the ATAPI transport carries the MMC set (i.e.
+a SCSI command set). Further, both types of ATA devices can be connected
+to a host computer via a "SCSI" (or some other) transport. When an
+ATA disk is controlled via a SCSI (or non\-ATA) transport then two
+approaches are commonly used: tunnelling (e.g. STP in Serial Attached
+SCSI (SAS)) or by emulating a SCSI device (e.g. with a SCSI to
+ATA translation layer, see SAT at www.t10.org ). Even when the
+physical transport to the host computer is ATA (especially in the
+case of SATA) the operating system may choose to put a SAT
+layer in the driver "stack" (e.g. libata in Linux).
+.PP
+The main identifying command for any SCSI device is an INQUIRY. The
+corresponding command for an ATA non\-packet device is IDENTIFY DEVICE
+while for an ATA packet device it is IDENTIFY PACKET DEVICE.
+.PP
+When this utility is invoked for an ATAPI device (e.g. a CD/DVD/BD
+drive with "sg_inq /dev/hdc") then a SCSI INQUIRY is sent to the
+device and if it responds then the response to decoded and output and
+this utility exits. To see the response for an ATA IDENTIFY PACKET
+DEVICE command add the \fI\-\-ata\fR option (e.g. "sg_inq \-\-ata /dev/hdc).
+.PP
+This utility doesn't decode the response to an ATA IDENTIFY (PACKET)
+DEVICE command, hdparm does a good job at that. The '\-HHH' option has
+been added for use with either the '\-\-ata' or '\-\-page=ai'
+option to produce a format acceptable to "hdparm \-\-Istdin".
+An example: 'sg_inq \-\-ata \-HHH /dev/hdc | hdparm \-\-Istdin'. See hdparm.
+.SH NVME DEVICES
+Currently these device are typically SSDs (Solid State Disks) directly
+connected to a PCIe connector or via a specialized connector such as a M2
+connector. Linux and FreeBSD treat NVMe storage devices as separate from
+SCSI storage with device names like /dev/nvme0n1 (in Linux) and
+/dev/nvme0ns1 (in FreeBSD). The NVM Express group has a document titled "NVM
+Express: SCSI Translation Reference" which defines a partial "SCSI to NVMe
+Translation Layer" often known by its acronym: SNTL.
+.PP
+On operating systems where it is supported by this package, this utility
+will detect NVMe storage devices directly connected and send an Identify
+controller NVMe Admin command and decode its response. A NVMe controller
+is architecturally similar to a SCSI target device. If the NVMe \fIDEVICE\fR
+indicates a namespace then an Identify namespace NVMe Admin command is sent
+to that namespace and its response is decoded. Namespaces are numbered
+sequentially starting from 1. Namespaces are similar to SCSI Logical Units
+and their identifiers (nsid_s) can be thought of as SCSI LUNs. In the
+Linux and FreeBSD example device names above the "n1" and the "ns1" parts
+indicate nsid 1 . If no namespace is given in the \fIDEVICE\fR then all
+namespaces found in the controller are sent Identify namespace commands and
+the responses are decoded.
+.PP
+To get more details in the response use the \fI\-\-long\fR option. To only
+get the controller's Identify decoded use the \fI\-\-only\fR option.
+.PP
+It is possible that even though the \fIDEVICE\fR presents as a NVMe device,
+it has a SNTL and accepts SCSI commands. In this case to send a SCSI INQUIRY
+command (and fetch its VPD pages) use the sg_vpd(8) utility.
+.SH SG_INQ and SG_VPD
+Both these utilities have much in common since VPD pages are fetched with the
+SCSI INQUIRY command. As more VPD pages have been added (and existing pages
+expanded) the newer ones were only decoded with sg_vpd. Recently with the
+optional JSON output work, it was decided to place all VPD page decoding in
+a source file called sg_vpd_common.c that is linked in by both sg_inq and
+sg_vpd.
+.PP
+This means that the VPD page decoding capabilities of both sg_inq and sg_vpd
+will be the same. Their default actions remain as they were, namely when
+sg_inq is used without command line options, it decodes the "standard INQUIRY
+data format" (usually called the "standard INQUIRY response") response while
+when sg_vpd is used without options, it decodes the "Supported VPD pages" VPD
+page.
+.SH EXIT STATUS
+The exit status of sg_inq is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.TP
+\fB\-36\fR
+only requests 36 bytes of response data for an INQUIRY. Furthermore even
+if the device indicates in its response it can supply more data, a
+second (longer) INQUIRY is not performed. This is a paranoid setting.
+Equivalent to '\-\-len=36' in the OPTIONS section.
+.TP
+\fB\-a\fR
+fetch the ATA Information VPD page [0x89]. Equivalent to '\-\-page=ai' in
+the OPTIONS section. This page is defined in SAT (see at www.t10.org).
+.TP
+\fB\-A\fR
+Assume given \fIDEVICE\fR is an ATA or ATAPI device.
+Equivalent to \fI\-\-ata\fR in the OPTIONS section.
+.TP
+\fB\-b\fR
+decodes the Block Limits VPD page [0xb0].  Equivalent to '\-\-page=bl' in
+the OPTIONS section. This page is defined in SBC\-2 (see www.t10.org) and
+later.
+.TP
+\fB\-B\fR=\fI0|1\fR
+equivalent to \fI\-\-block=0|1\fR in OPTIONS section.
+.TP
+\fB\-c\fR
+set the Command Support Data (CmdDt) bit (defaults to clear(0)). Used in
+conjunction with the \fI\-p=VPD_PG\fR option to specify the SCSI command
+opcode to query. Equivalent to \fI\-\-cmddt\fR in the OPTIONS section.
+.TP
+\fB\-cl\fR
+lists the command data for all supported commands (followed by the command
+name) by looping through all 256 opcodes. This option uses the CmdDt bit
+which is now obsolete. See the sg_opcodes(8) utility.
+Equivalent to '\-\-cmddt \-\-cmddt' in the OPTIONS section.
+.TP
+\fB\-d\fR
+decodes depending on context. If \fI\-e\fR option is given, or any option
+that implies \fI\-e\fR (e.g. '\-i' or '\-p=80'), then this utility attempts
+to decode the indicated VPD page.  Otherwise the version descriptors (if any)
+are listed following a standard INQUIRY response. In the version descriptors
+sense, equivalent to \fI\-\-descriptors\fR in the OPTIONS section.
+.TP
+\fB\-e\fR
+enable (i.e. sets) the Vital Product Data (EVPD) bit (defaults to clear(0)).
+Used in conjunction with the \fI\-p=VPD_PG\fR option to specify the VPD page
+to fetch. If \fI\-p=VPD_PG\fR is not given then VPD page 0 (list supported
+VPD pages) is assumed.
+.TP
+\fB\-f\fR
+Equivalent to \fI\-\-force\fR in the OPTIONS section.
+.TP
+\fB\-h\fR
+outputs INQUIRY response in hex rather than trying to decode it.
+Equivalent to \fI\-\-hex\fR in the OPTIONS section.
+.TP
+\fB\-H\fR
+same action as \fI\-h\fR.
+Equivalent to \fI\-\-hex\fR in the OPTIONS section.
+.TP
+\fB\-i\fR
+decodes the Device Identification VPD page [0x83]. Equivalent to \fI\-\-id\fR
+in the OPTIONS section. This page is made up of several "designation
+descriptors". If \fI\-h\fR is given then each descriptor header is decoded
+and the identifier itself is output in hex. To see the whole VPD 0x83 page
+response in hex use '\-p=83 \-h'.
+.TP
+\fB\-I\fR=\fIFN\fR
+equivalent to \fI\-\-inhex=FN\fR in the OPTIONS section.
+.TP
+\fB\-j[\fR=\fIJO]\fR
+equivalent to \fI\-\-json[=JO]\fR in the OPTIONS section.
+.TP
+\fB\-l\fR=\fILEN\fR
+equivalent to \fI\-\-len=LEN\fR in the OPTIONS section.
+.TP
+\fB\-L\fR
+equivalent to \fI\-\-long\fR in the OPTIONS section.
+.TP
+\fB\-m\fR
+decodes the Management network addresses VPD page [0x85]. Equivalent
+to '\-\-page=mna' in the OPTIONS section.
+.TP
+\fB\-M\fR
+decodes the Mode page policy VPD page [0x87].  Equivalent to '\-\-page=mpp'
+in the OPTIONS section.
+.TP
+\fB-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-o\fR
+equivalent to \fI\-\-only\fR in the OPTIONS section.
+.TP
+\fB\-p\fR=\fIVPD_PG\fR
+used in conjunction with the \fI\-e\fR or \fI\-c\fR option. If neither given
+then the \fI\-e\fR option assumed. When the \fI\-e\fR option is also
+given (or assumed) then the argument to this option is the VPD page number.
+The argument is interpreted as hexadecimal and is expected to be in the range
+0 to ff inclusive. Only VPD page 0 is decoded and it lists supported VPD pages
+and their names (if known). To decode the mandatory device identification
+page (0x83) use the \fI\-i\fR option. A now obsolete usage is when the
+\fI\-c\fR option is given in which case the argument to this option is assumed
+to be a command opcode number. Recent SCSI draft standards have moved this
+facility to a separate command (see sg_opcodes(8)). Defaults to 0 so if
+\fI\-e\fR is given without this option then VPD page 0 is output.
+.TP
+\fB\-P\fR
+decodes the Unit Path Report VPD page [0xc0] which is EMC specific.
+Equivalent to '\-\-page=upr' in the OPTIONS section.
+.TP
+\fB\-r\fR
+outputs the response in binary to stdout.  Equivalent to \fI\-\-raw\fR in
+the OPTIONS section.  Can be used twice (i.e. '\-rr' (and '\-HHH' has
+same effect)) and if used with the \fI\-A\fR or \fI\-a\fR option yields
+output with the same format as "cat /proc/ide/hd<x>/identify" so that it
+can then be piped to "hdparm \-\-Istdin".
+.TP
+\fB\-s\fR
+decodes the SCSI Ports VPD page [0x88].
+Equivalent to '\-\-page=sp' in the OPTIONS section.
+.TP
+\fB\-u\fR
+equivalent to '\-\-export' in the OPTIONS section.
+.TP
+\fB\-v\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.TP
+\fB\-x\fR
+decodes the Extended INQUIRY data VPD [0x86] page.
+Equivalent to '\-\-page=ei' in the OPTIONS section.
+.TP
+\fB\-?\fR
+output usage message and exit. Ignore all other parameters.
+.SH EXAMPLES
+The examples in this page use Linux device names. For suitable device
+names in other supported Operating Systems see the sg3_utils(8) man page.
+.PP
+To view the standard inquiry response use without options:
+.PP
+   sg_inq /dev/sda
+.PP
+Some SCSI devices include version descriptors indicating the various
+SCSI standards and drafts they support. They can be viewed with:
+.PP
+   sg_inq \-d /dev/sda
+.PP
+Modern SCSI devices include Vital Product Data (VPD)pages which can be
+viewed with the SCSI INQUIRY command. To list the supported VPD
+pages (but not their contents) try:
+.PP
+   sg_inq \-e /dev/sda
+.PP
+In Linux, binary images of some important VPD page responses (e.g. 0, 80h
+and 83h) are cached in files within the sysfs pseudo file system. Since
+VPD pages hardly ever change their contents, decoding those files will
+give the same output as probing the device with the added benefit that
+decoding those files doesn't need root permissions. If /dev/sg3 is a disk
+at 2:0:0:0 , then these three invocations should result in the same output:
+.PP
+   sg_inq \-\-raw \-\-inhex=/sys/class/scsi_generic/sg3/device/vpd_pg83
+.PP
+   sg_inq \-rI /sys/class/scsi_generic/sg3/device/vpd_pg83
+.PP
+   sg_inq \-r \-I /sys/class/scsi_disk/2:0:0:0/device/vpd_pg83
+.PP
+Without the \fI\-\-raw\fR option, the \fI\-\-inhex=FN\fR option would
+expect the contents of those files to be hexadecimal. vpd_pg83 contains
+the response (in binary) to the Device Identification VPD page whose page
+number is 83h (i.e. hexadecimal).
+.PP
+Some VPD pages can be read with the sg_inq utility but a newer utility
+called sg_vpd specializes in showing their contents. The sdparm utility
+can also be used to show the contents of VPD pages.
+.PP
+Further examples of sg_inq together with some typical output can be found
+on https://sg.danny.cz/sg/sg3_utils.html web page.
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2001\-2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2 or the BSD\-2\-Clause
+license. There is NO warranty; not even for MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_opcodes(8), sg_vpd(8), sg_logs(8), sg_modes(8), sdparm(8), hdparm(8),
+.B sgdiag(scsirastools)
diff --git a/doc/sg_logs.8 b/doc/sg_logs.8
new file mode 100644
index 0000000..2de64cd
--- /dev/null
+++ b/doc/sg_logs.8
@@ -0,0 +1,571 @@
+.TH SG_LOGS "8" "November 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_logs \- access log pages with SCSI LOG SENSE command
+.SH SYNOPSIS
+.B sg_logs
+[\fI\-\-ALL\fR] [\fI\-\-all\fR] [\fI\-\-brief\fR] [\fI\-\-exclude\fR]
+[\fI\-\-filter=FL\fR] [\fI\-\-full\fR] [\fI\-\-hex\fR] [\fI\-\-json[=JO]\fR]
+[\fI\-\-list\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-name\fR] [\fI\-\-no_inq\fR]
+[\fI\-\-page=PG\fR] [\fI\-\-paramp=PP\fR] [\fI\-\-pcb\fR] [\fI\-\-ppc\fR]
+[\fI\-\-pdt=DT\fR] [\fI\-\-raw\fR] [\fI\-\-readonly\fR] [\fI\-\-sp\fR]
+[\fI\-\-temperature\fR] [\fI\-\-transport\fR] [\fI\-\-undefined\fR]
+[\fI\-\-vendor=VP\fR] [\fI\-\-verbose\fR] \fIDEVICE\fR
+.PP
+.B sg_logs
+\fI\-\-in=FN\fR  [\fI\-\-brief\fR] [\fI\-\-exclude\fR] [\fI\-\-filter=FL\fR]
+[\fI\-\-full\fR] [\fI\-\-hex\fR] [\fI\-\-json[=JO]\fR] [\fI\-\-name\fR]
+[\fI\-\-page=PG\fR] [\fI\-\-pdt=DT\fR] [\fI\-\-raw\fR] [\fI\-\-undefined\fR]
+[\fI\-\-vendor=VP\fR]
+.PP
+.B sg_logs
+\fI\-\-select\fR  [\fI\-\-control=PC\fR] [\fI\-\-page=PG\fR] [\fI\-\-raw\fR]
+[\fI\-\-reset\fR] [\fI\-\-sp\fR] [\fI\-\-verbose\fR] \fIDEVICE\fR
+.PP
+.B sg_logs
+\fI\-\-enumerate\fR  [\fI\-\-filter=FL\fR] [\fI\-\-help\fR]
+[\fI\-\-vendor=VP\fR] [\fI\-\-version\fR]
+.PP
+.B sg_logs
+[\fI\-a\fR] [\fI\-A\fR] [\fI\-b\fR] [\fI\-c=PC\fR] [\fI\-D=DT\fR] [\fI\-e\fR]
+[\fI\-E\fR] [\fI\-f=FL\fR] [\fI\-F\fR] [\fI\-h\fR] [\fI\-H\fR] [\fI\-i=FN\fR]
+[\fI\-l\fR] [\fI\-L\fR] [\fI\-m=LEN\fR] [\fI\-M=VP\fR] [\fI\-n\fR]
+[\fI\-p=PG\fR] [\fI\-paramp=PP\fR] [\fI\-pcb\fR] [\fI\-ppc\fR] [\fI\-r\fR]
+[\fI\-R\fR] [\fI\-select\fR] [\fI\-sp\fR] [\fI\-t\fR] [\fI\-T\fR] [\fI\-u\fR]
+[\fI\-v\fR] [\fI\-V\fR] [\fI\-?\fR] [\fI\-x\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends a SCSI LOG SENSE command to the \fIDEVICE\fR and then
+outputs the response. The LOG SENSE command is used to fetch log pages which,
+if known, are decoded by default. When the \fI\-\-reset\fR and/or
+\fI\-\-select\fR option is given then a SCSI LOG SELECT command is issued
+to the \fIDEVICE\fR. Alternatively one or more log page responses can be in
+a file read using the \fI\-\-in=FN\fR option; in this case those responses
+are decoded and the \fIDEVICE\fR argument, if given, is ignored.
+.PP
+In SPC\-4 revision 5 the subpage code was introduced to both the LOG SENSE and
+LOG SELECT command. At the same time a page code field was introduced to the
+to the LOG SELECT command. The log subpage code can range from 0 to 255 (0xff)
+inclusive. The subpage code value 255 can be thought of as a wildcard.
+.PP
+The SYNOPSIS section above is divided into five forms. The first form
+shows the options that can be used to send a LOG SENSE command to the
+\fIDEVICE\fR and decode its response. The second form fetches data from a
+file (named \fIFN\fR) and decodes it as if it were a response from a LOG
+SENSE command. The third form shows the options that can be used to send a
+LOG SELECT command. The fourth form groups various management options.
+The last form shows the older, deprecated command line interface which is
+maintained for backward compatibility.
+.PP
+When no options are given, just a \fIDEVICE\fR, that is equivalent to calling
+this utility with the \fI\-\-list\fR option. In that case the names of the
+supported log pages (but not subpages) are listed out.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well. The options
+are arranged in alphabetical order based on the long option name.
+.TP
+\fB\-A\fR, \fB\-\-ALL\fR
+fetch and decode all the log pages and subpages supported by the \fIDEVICE\fR.
+This requires a two stage process: first the "supported log pages and
+subpages" log page is fetched, then for each entry in its response, the
+corresponding log page (or subpage) is fetched and displayed. Note that there
+are many SCSI devices that do not support LOG SENSE subpages and respond
+to this option with an illegal request sense key.
+.br
+Since some vendors don't list all log pages in the "supported log pages and
+subpages" log page, the '\-lll' option can be given in addition. This will
+merge both "supported ..." log pages then, from that resultant merged list,
+fetch page contents.
+.br
+The long option may also appear as \fB\-\-All\fR.
+.br
+This option overrides the \fI\-\-page=PG\fR if the latter is also given.
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+outputs all the log pages supported by the \fIDEVICE\fR. This requires a two
+stage process: first the "supported log pages" log page is fetched, then for
+each entry in its response, the corresponding log page is fetched and
+displayed. When used twice (e.g. '\-aa') all log pages and subpages are
+fetched.
+.br
+This option overrides the \fI\-\-page=PG\fR if the latter is also given.
+.TP
+\fB\-b\fR, \fB\-\-brief\fR
+shorten the amount of output for some log pages. For example the Tape
+Alert log page only outputs parameters whose flags are set when
+\fI\-\-brief\fR is given.
+.TP
+\fB\-c\fR, \fB\-\-control\fR=\fIPC\fR
+accepts 0, 1, 2 or 3 for the \fIPC\fR argument:
+.br
+  \fB0\fR : current threshold values
+.br
+  \fB1\fR : current cumulative values
+.br
+  \fB2\fR : default threshold values
+.br
+  \fB3\fR : default cumulative values
+.br
+The default value is 1 (i.e. current cumulative values).
+.TP
+\fB\-e\fR, \fB\-\-enumerate\fR
+this option is used to output information held in this utility's internal
+tables about known log pages including their name, acronym and fields. If
+given, the \fIDEVICE\fR argument is ignored. When given once (e.g. '\-e')
+all known pages are listed, sorted in ascending alphabetical acronym order.
+.br
+When given twice, vendor pages are excluded.  When given three times, all
+known pages are listed, sorted in ascending numeric order listed; when given
+four times, vendor pages are excluded from the numeric order.
+.br
+The \fI\-\-filter=FL\fR and \fI\-\-verbose\fR options reduce the output
+of the enumeration.
+.TP
+\fB\-E\fR, \fB\-\-exclude\fR
+this option excludes vendor specific pages and parameters from the output.
+Trying to decode vendor specific pages and parameters does not necessarily
+work well for many reasons. This option limits the output to pages and
+parameters defined by T10.
+.br
+Only parameter fields identified in the drafts as 'vendor specific' are
+excluded. So parameters codes identified as 'reserved' are shown.
+.TP
+\fB\-f\fR, \fB\-\-filter\fR=\fIFL\fR
+\fIFL\fR is either a parameter code when \fIDEVICE\fR is given, or a
+peripheral device type (pdt) (or other) if \fI\-\-enumerate\fR is given.
+.br
+In the parameter code case \fIFL\fR is a value between 0 and 65535 (0xffff)
+and only the parameter section matching that code is output. If the
+\fB\-\-hex\fR option is given the log parameter is output in hexadecimal
+rather than decoding it. If the \fB\-\-hex\fR option is used twice then the
+leading address on each line of hex is removed. If the \fB\-\-raw\fR option
+is given then the log parameter is output in binary. Most log pages contain
+one or more log parameters. Examples of those that don't follow that
+convention are those pages that list supported log pages (and subpages).
+.br
+In the \fI\-\-enumerate\fR case, when \fIFL\fR >= zero it is taken as a
+pdt value and only log pages associated with that pdt plus generic pages
+listed in SPC are enumerated. If \fIFL\fR is \-1 then the filter does
+nothing which is the same as not giving this option; when \fIFL\fR is \-2
+then only generic pages listed in SPC are enumerated. If \fIFL\fR is \-10
+then only generic direct access like (e.g. disk) pages are enumerated. If
+\fIFL\fR is \-11 then only generic tape like pages (e.g. includes ADC)
+are enumerated.
+.TP
+\fB\-F\fR, \fB\-\-full\fR
+this option only applies to the Application client log page. Typically that
+log page has more than 16,000 bytes of user supplied data. Rather than
+print it all out, the default is to print out the first 64 bytes of data.
+When this option is given, the application client log pages is fully
+decoded.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+The default action is to decode known log page numbers (and subpage numbers)
+into text. When this option is used once, the response is output in
+hexadecimal. When used twice, each line of hex has the ASCII equivalent shown
+to the right. When used three times, the hex has no leading address nor
+trailing ASCII making it suitable to be placed in a file (or piped). That
+file might later be used by another invocation using the \fI\-\-in=FN\fR
+option.
+.br
+A weaker form of this option, called \fI\-\-undefined\fR, handles the
+formatting of hexadecimal output for fields that this utility is unable to
+decode.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIFN\fR
+This option may be used in two different contexts. One is with the
+\fI\-\-select\fR to send a LOG SELECT command to the given \fIDEVICE\fR;
+see the LOG SELECT section below.
+.br
+The other context is with no \fIDEVICE\fR argument given in which case
+the contents of \fIFN\fR are decoded as if it were the response of a LOG
+SENSE command (i.e. a log page). For decoding the page and subpage numbers
+are taken from \fIFN\fR while the peripheral device type is either
+generic (i.e. from SPC) or the value given by \fI\-\-pdt=DT\fR.
+.br
+\fIFN\fR is treated as a file name (or '\-' for stdin) which contains ASCII
+hexadecimal or binary representing a log page. The hexadecimal should be
+arranged as 1 or 2 digits representing a byte each of which is whitespace or
+comma separated. Anything from and including a hash mark to the end of line
+is ignored. If the \fI\-\-raw\fR option is also given then \fIFN\fR is
+treated as binary.
+.br
+For compatibility with other utilities in this package "inhex" may also be
+used as this (long) option name.
+.TP
+\fB\-j\fR, \fB\-\-json[\fR=\fIJO\fR]
+output is in JSON format instead of human readable form. See sg3_utils_json
+manpage or use '?' for \fIJO\fR in order to have a summary printed to stderr.
+.TP
+\fB\-l\fR, \fB\-\-list\fR
+lists the names of the logs sense pages supported by this device. This is
+done by reading the "supported log pages" log page. When used once only
+log pages, but not subpages, are listed. When used twice the "supported
+log pages and subpages" log page is output. Some vendors do not list some
+log pages (e.g. those without any subpages) in the "supported log pages
+and subpages" log page. To get a full inventory, this option can be used
+three times (e.g. '\-lll') and the output of the two log pages is merged.
+Even if the "supported log pages and subpages" log page is not supported
+using this option three times will yield a list from the "supported log
+pages" log page. In the absence of other options, the page/subpage names,
+but not their contents, are shown when this option is given.
+.br
+The '\-lll' form may be useful with the \fI\-\-ALL\fR option to show the
+contents of all pages referred to in either the "supported log page" or
+the "supported log page and subpage" log pages.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+sets the "allocation length" field in the LOG SENSE cdb. The is the maximum
+length in bytes that the response will be. Without this option (or \fILEN\fR
+equal to 0) this utility first fetches the 4 byte response then does a second
+access with the length indicated in the first (4 byte) response. Negative
+values and 1 for \fILEN\fR are not accepted. Responses can be quite
+large (e.g. the background scan results log page) and this option can be used
+to limit the amount of information returned.
+.br
+The default \fILEN\fR is 65532 unless the \fI\-\-in=FN\fR option is given;
+in that case the default is 262144 .
+.TP
+\fB\-n\fR, \fB\-\-name\fR
+decode some log pages into 'name=value' entries, one per line. The name
+contains no space and may be abbreviated and the value is decimal unless
+prefixed by '0x'. Nesting is indicated by leading spaces. This form
+is meant to be relatively easy to parse.
+.br
+This option is superseded by the \fI\-\-json[=JO]\fR option. If both are
+given then this option is ignored.
+.TP
+\fB\-x\fR, \fB\-\-no_inq\fR
+suppresses the output of information obtained from an initial call to the
+INQUIRY command for the standard response. The default (assuming some other
+options that suppress this output are also not given) is to output several
+device identification strings.
+.br
+If this option is given twice (or more) then no INQUIRY command is sent
+hence there will be no device identification string output either. Also the
+peripheral device type (PDT) field will not be obtained so this utility will
+not be able to differentiate between some log pages that are device
+dependent. It will assume a PDT of 0 (i.e. a disk).
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option.
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPG\fR
+log page name/number to access. \fIPG\fR is either an acronym, a page number,
+or a page, subpage number pair. Available acronyms can be listed with the
+\fI\-\-enumerate\fR option. Page (0 to 63) and subpage (0 to 255) numbers
+are comma separated. They are decimal unless a hexadecimal indication is
+given. A hexadecimal number can be specified by a leading "0x" or a
+trailing "h".
+.br
+A few acronyms specify a range of subpage values in which case the acronym
+may be followed by a comma then a subpage number. This method can also be
+used to fetch the Supported subpages log page (e.g. \-\-page=temp,0xff).
+.TP
+\fB\-P\fR, \fB\-\-paramp\fR=\fIPP\fR
+\fIPP\fR is the parameter pointer value to place in a field of that name in
+the LOG SENSE cdb. A number in the range 0 to 65535 (0x0 to 0xffff) is
+expected. When a value greater than 0 is given the \fI\-\-ppc\fR option
+should be selected. The default value is 0.
+.br
+For log pages that have parameter codes, the \fIDEVICE\fR should return
+only parameters code equal to \fIPP\fR or higher in its response.
+.TP
+\fB\-q\fR, \fB\-\-pcb\fR
+show Parameter Control Byte settings (only relevant when log parameters
+being output in ASCII). This byte includes the DU and TSD bits plus
+the 'Format and linking' field (2 bits wide).
+.TP
+\fB\-D\fR, \fB\-\-pdt\fR=\fIDT\fR
+\fIDT\fR is the peripheral device type (PDT) that is used when it is not
+available from the \fIDEVICE\fR. There are two main cases of this: with
+the \fI\-\-pdt=DT\fR without a \fIDEVICE\fR (e.g. when \fI\-\-in=FN\fR
+is used) and when \fI\-\-no_inq\fR is used with a \fIDEVICE\fR.
+.br
+\fIDT\fR may be -1 which is the default value. This value may select any
+device type but favours the lower numbers (e.g. the PDT of disks is 0).
+.TP
+\fB\-Q\fR, \fB\-\-ppc\fR
+sets the Parameter Pointer Control (PPC) bit in the LOG SENSE cdb. Default
+is 0 (i.e. cleared). This bit was made obsolete in SPC\-4 revision 18.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the response in binary to stdout. Error messages and warnings are
+output to stderr.
+.br
+This option may also be given together with \fI\-\-in=FN\fR in which case
+the contents of \fIFN\fR are interpreted as binary data (and the response is
+decoded as normal, not dumped as binary).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag). The
+default action is to try and open \fIDEVICE\fR read\-write then if that
+fails try to open again with read\-only. However when a read\-write open
+succeeds there may still be unwanted actions on the close (e.g. some OSes
+try to do a SYNCHRONIZE CACHE command). So this option forces a read\-only
+open on \fIDEVICE\fR and if it fails, this utility will exit. Note that
+options like \fI\-\-select\fR most likely need a read\-write open.
+.TP
+\fB\-R\fR, \fB\-\-reset\fR
+use SCSI LOG SELECT command (with the PCR bit set) to reset the all log
+pages (or the given page). Exactly what is reset depends on the accompanying
+SP bit (i.e. \fI\-\-sp\fR option which defaults to 0) and the
+\fIPC\fR ("page control") value (which defaults to 1). Supplying this option
+implies the \fI\-\-select\fR option as well. This option seems to clear error
+counter log pages but leaves pages like self\-test results, start\-stop cycle
+counter and temperature log pages unaffected. This option may be required to
+clear log pages if a counter reaches its maximum value since the log page in
+which the counter is found will remain "stuck" at its maximum value until
+some user interaction (e.g. calling sg_logs with this option).
+.TP
+\fB\-S\fR, \fB\-\-select\fR
+use a LOG SELECT command. The default action (i.e. when neither this option
+nor \fI\-\-reset\fR is given) is to do a LOG SENSE command. See the LOG
+SELECT section.
+.TP
+\fB\-s\fR, \fB\-\-sp\fR
+sets the Saving Parameters (SP) bit. Default is 0 (i.e. cleared). When set
+this instructs the device to store the current log page parameters (as
+indicated by the DS and TSD parameter codes) in some non\-volatile location.
+Hence the log parameters will be preserved across power cycles. This option
+is typically not needed, especially if the GLTSD flag is clear in the
+control mode page which causes the \fIDEVICE\fR to periodically save all
+saveable log parameters to non\-volatile storage.
+.TP
+\fB\-t\fR, \fB\-\-temperature\fR
+outputs the temperature. First looks in the temperature log page and if
+that is not available tries the Informational Exceptions log page which
+may also have the current temperature (especially on older disks).
+.TP
+\fB\-T\fR, \fB\-\-transport\fR
+outputs the transport ('Protocol specific port') log page. Equivalent to
+setting '\-\-page=18h'.
+.TP
+\fB\-u\fR, \fB\-\-undefined\fR
+to see fields decoded, the \fI\-\-hex\fR option cannot be used. However some
+fields are not defined in the T10 documents and in that case they are output
+in hex. This option controls the format of 'undefined' fields when they
+output in hex. Multiple uses of this option has the same sense as the
+\fI\-\-hex\fR option. For example '\-uu' will output undefined fields in
+hexadecimal with an ASCII rendering to the right of each line.
+.TP
+\fB\-M\fR, \fB\-\-vendor\fR=\fIVP\fR
+where \fIVP\fR is a vendor/manufacturer (e.g. "sea" for Seagate) or
+product (group) acronym (e.g. "lto5" for the 5th generation LTO (tape)
+consortium). Either the whole log page is vendor specific (e.g. page
+numbers 0x30 to 0x3f) or part of a T10 defined log page is vendor specific.
+For example SPC\-5 defines parameter code 0x0 of page 0x2f (the Informational
+Exceptions log page) and states that the remaining parameter codes (i.e. 0x1
+to 0xffff) are vendor specific. Using a \fIVP\fR of "xxx" will list the
+available acronyms.
+.br
+If this option is used with \fI\-\-page=PG\fR and \fIPG\fR is an acronym
+then this option is ignored. If \fIPG\fR is a number (e.g. 0xc0) then
+\fIVP\fR is used to choose the which vendor specific page (e.g. sharing
+page number 0xc0) to decode.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level of verbosity. When used with \fI\-\-enumerate\fR, in the
+list of known log page names, those that have no associated decode logic
+are followed by "[hex only]".
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string then exit.
+.SH LOG SELECT
+The SCSI LOG SELECT command can be used to reset certain parameters to vendor
+specific defaults, save them to non\-volatile storage (i.e. the media), or
+supply new page contents. This command has changed between SPC\-3 and SPC\-4
+with the addition of the Page and Subpage Code fields which can only be
+non zero when the Parameter list length is zero.
+.PP
+The \fI\-\-select\fR (or \fI\-\-reset\fR) option is required to issue a LOG
+SELECT command. If the \fI\-\-in=FN\fR option is not given (or \fIFN\fR is
+effectively empty) then the Parameter list length field is set to zero. If
+the \fI\-\-in=FN\fR option is is given then its decoded data is placed in
+the data\-out buffer and its length in bytes is placed in the Parameter list
+length field.
+.PP
+Other options that are active with the LOG SELECT command are
+\fI\-\-control=PC\fR, \fI\-\-reset\fR (which sets the PCR bit) and
+\fI\-\-sp\fR.
+.SH
+APPLICATION CLIENT
+This is the name of a log page that acts as a container for data provided
+by the user. An application client is a SCSI term for the program that issues
+commands to a SCSI initiator (often known as a Host Bus Adapter (HBA)). So,
+for example, this utility is a SCSI application client.
+.PP
+The Application Client log page has 64 log parameters with parameters codes
+0 to 63. Each can hold 252 bytes of user binary data. That 252 bytes (or
+less) of user data, with a 4 byte prefix (for a total of 256 bytes) can be
+provided with the \fI\-\-in=FN\fR option. A typical prefix would
+be '0,n,83,fc'. The "n" is the parameter code in hex so the last log
+parameter would be '0,3f,83,fc'. That log parameter could be read back at
+some later time with '\-\-page=0xf \-\-filter=0x<n>'.
+.SH NOTES
+This utility will usually do a double fetch of log pages with the SCSI LOG
+SENSE command. The first fetch requests a 4 byte response (i.e. place 4 in
+the "allocation length" field in the cdb). From that response it can
+calculate the actual length of the response which is what it asks for
+on the second fetch. This is typical practice in SCSI and guaranteed to
+work in the standards. However some older devices don't comply. For
+those devices using the \fI\-\-maxlen=LEN\fR option will do a single fetch.
+A value of 252 should be a safe starting point.
+.PP
+Various log pages hold information error rates, device temperature, start
+stop cycles since the device was produced and the results of the last
+20 self tests. Self tests can be initiated by the sg_senddiag(8) utility.
+The smartmontools package provides much of the information found with
+sg_logs in a form suitable for monitoring the health of SCSI disks and
+tape drives.
+.PP
+The simplest way to find which log pages can be decoded by this utility is
+to use the \fI\-\-enumerate\fR option. Some page names are known but there
+is no decode logic; such cases have "[hex only]" after the log page name
+when the \fI\-\-verbose\fR option is given with \fI\-\-enumerate\fR.
+.PP
+Vendors are specifically permitted by the SPC\-6 to _not_ report all pages
+and subpages supported by a device. That weakens the usefulness of the pages
+that report a list of supported pages and subpages. One guarantee which is
+given is that the pages reported shall be in ascending order.
+.SH EXIT STATUS
+The exit status of sg_logs is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.PP
+Options with arguments or with two or more letters can have an extra '\-'
+prepended. For example: both '\-pcb' and '\-\-pcb' are acceptable.
+.TP
+\fB\-a\fR
+outputs all the log pages supported by the \fIDEVICE\fR.
+Equivalent to \fI\-\-all\fR in the main description.
+.TP
+\fB\-A\fR
+outputs all the log pages and subpages supported by the \fIDEVICE\fR.
+Equivalent to \fI\-\-ALL\fR in the main description.
+.TP
+\fB\-c\fR=\fIPC\fR
+Equivalent to \fI\-\-control=PC\fR in the main description.
+.TP
+\fB\-D\fR=\fIDT\fR
+Equivalent to \fI\-\-pdt=DT\fR in the main description.
+.TP
+\fB\-e\fR
+enumerate internal tables to show information about known log pages.
+Equivalent to \fI\-\-enumerate\fR in the main description.
+.TP
+\fB\-E\fR
+Equivalent to \fI\-\-exclude\fR in the main description.
+.TP
+\fB\-h\fR
+suppresses decoding of known log sense pages and prints out the
+response in hex instead.
+.TP
+\fB\-i\fR=\fIFN\fR
+\fIFN\fR is treated as a file name (or '\-' for stdin) which contains ASCII
+hexadecimal representing a log page that will be sent as parameter data of a
+LOG SELECT command. See the LOG SELECT section.
+.TP
+\fB\-H\fR
+same action as '\-h' in this section and equivalent to \fI\-\-hex\fR in
+the main description.
+.TP
+\fB\-l\fR
+lists the names of all logs sense pages supported by this \fIDEVICE\fR.
+Equivalent to \fI\-\-list\fR in the main description.
+.TP
+\fB\-L\fR
+lists the names of all logs sense pages and subpages supported by this
+\fIDEVICE\fR. Equivalent to '\-\-list \-\-list' in the main description.
+.TP
+\fB\-m\fR=\fILEN\fR
+request only \fILEN\fR bytes of response data. Default is 0 which is
+interpreted as all that is available. \fILEN\fR is decimal unless it has
+a leading '0x' or trailing 'h'.  Equivalent to \fI\-\-maxlen=LEN\fR in
+the main description.
+.TP
+\fB\-M\fR=\fIVP\fR
+Equivalent to \fI\-\-vendor=VP\fR in the main description.
+.TP
+\fB\-n\fR
+Equivalent to \fI\-\-name\fR in the main description.
+.TP
+\fB\-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-p\fR=\fIPG\fR
+log page code to access. \fIPG\fR is either an acronym, a page number, or
+a page, subpage pair. Available acronyms can be listed with the
+\fI\-\-enumerate\fR option. Page (0 to 3f) and subpage (0 to ff) numbers
+are comma separated. The numbers are assumed to be hexadecimal.
+.TP
+\fB\-paramp\fR=\fIPP\fR
+\fIPP\fR is the parameter pointer value (in hex) to place in command.
+Should be a number between 0 and ffff inclusive.
+.TP
+\fB\-pcb\fR
+show Parameter Control Byte settings (only relevant when log parameters
+being output in ASCII).
+.TP
+\fB\-ppc\fR
+sets the Parameter Pointer Control (PPC) bit. Default is 0 (i.e. cleared).
+.TP
+\fB\-r\fR
+use SCSI LOG SELECT command (PCR bit set) to reset the all log pages (or
+the given page). Equivalent to \fI\-\-reset\fR in the main description.
+.TP
+\fB\-R\fR
+Equivalent to \fI\-\-readonly\fR in the main description.
+.TP
+\fB\-select\fR
+use a LOG SELECT command. Equivalent to \fI\-\-select\fR in the main
+description.
+.TP
+\fB\-sp\fR
+sets the Saving Parameters (SP) bit. Default is 0 (i.e. cleared).
+Equivalent to \fI\-\-sp\fR in the main description.
+.TP
+\fB\-t\fR
+outputs the temperature. Equivalent to \fI\-\-temperature\fR in the main
+description.
+.TP
+\fB\-T\fR
+outputs the transport ('Protocol specific port') log page. Equivalent
+to \fI\-\-transport\fR in the main description.
+.TP
+\fB\-v\fR
+increase level of verbosity.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.TP
+\fB\-x\fR
+suppress the INQUIRY command. Equivalent to \fI\-\-no_inq\fR in the main
+description.
+.TP
+\fB\-?\fR
+output usage message then exit.
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2002\-2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B smartctl(smartmontools), sg_senddiag(8)
diff --git a/doc/sg_luns.8 b/doc/sg_luns.8
new file mode 100644
index 0000000..3ededa9
--- /dev/null
+++ b/doc/sg_luns.8
@@ -0,0 +1,319 @@
+.TH SG_LUNS "8" "January 2020" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_luns \- send SCSI REPORT LUNS command or decode given LUN
+.SH SYNOPSIS
+.B sg_luns
+[\fI\-\-decode\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-linux\fR]
+[\fI\-\-lu_cong\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-quiet\fR] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-select=SR\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.PP
+.B sg_luns
+\fI\-\-test=ALUN\fR [\fI\-\-decode\fR] [\fI\-\-hex\fR] [\fI\-\-lu_cong\fR]
+[\fI\-\-verbose\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+In the first form shown in the SYNOPSIS this utility sends the SCSI REPORT
+LUNS command to the \fIDEVICE\fR and outputs the response. The response
+should be a list of LUNs ("a LUN inventory") for the I_T nexus associated
+with the \fIDEVICE\fR. Roughly speaking that is all LUNs that share the
+target device that the REPORT LUNS command is sent through. This command
+is defined in the SPC\-3 and SPC\-4 SCSI standards and its support is
+mandatory. The most recent draft if SPC\-6 revision 1.
+.PP
+When the \fI\-\-test=ALUN\fR option is given (the second form in the
+SYNOPSIS), then the \fIALUN\fR value is decoded as outlined in various
+SCSI Architecture Model (SAM) standards and recent drafts (e.g. SAM\-6
+revision 2, section 4.7) .
+.PP
+Where required below the first form shown in the SYNOPSIS is called "device
+mode" and the second form is called "test mode".
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-d\fR, \fB\-\-decode\fR
+decode LUNs into their component parts, as described in the LUN section
+of SAM\-3, SAM\-4 and SAM\-5.
+.br
+[test mode] \fIALUN\fR is decoded irrespective of whether this option is
+given or not. If this option is given once then the given \fIALUN\fR is
+output in T10 preferred format (which is 8 pairs of hex digits, each
+separated by a space). If given twice then the given \fIALUN\fR is output
+in an alternate T10 format made up of four quads of hex digits with each
+quad separated by a "-" (e.g. C101\-0000\-0000\-0000).
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+[device mode] when given once this utility will output the SCSI
+response (i.e. the data\-out buffer) to the REPORT LUNS command in ASCII
+hex then exit. When given twice it causes \fI\-\-decode\fR to output
+component fields in hex rather than decimal.
+.br
+[test mode] when this option is given, then decoded component fields of
+\fIALUN\fR are output in hex.
+.TP
+\fB\-l\fR, \fB\-\-linux\fR
+this option is only available in Linux. After the T10 representation of
+each 64 bit LUN (in 16 hexadecimal digits), if this option is given then
+to the right, in square brackets, is the Linux LUN integer in decimal.
+If the \fI\-\-hex\fR option is given twice (e.g. \-HH) as well then the
+Linux LUN integer is output in hexadecimal.
+.TP
+\fB\-L\fR, \fB\-\-lu_cong\fR
+this option is only considered with \fI\-\-decode\fR. When given once
+then the list of LUNs is decoded as if the LU_CONG bit was set in
+each LU's corresponding INQUIRY response. When given twice the list of
+LUNs is decoded as if the LU_CONG bit was clear in each LU's corresponding
+INQUIRY response. When this option is not given and \fI\-\-decode\fR is
+given then an INQUIRY is sent to the \fIDEVICE\fR and the setting of
+its LU_CONG bit is used to decode the list of LUNs.
+.br
+[test mode] decode \fIALUN\fR as if the LU_CONG bit is set in its
+corresponding standard INQUIRY response. In other words treat \fIALUN\fR
+as if it is a conglomerate LUN. If not given (or given twice) then decode
+\fIALUN\fR as if the LU_CONG bit is clear.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given (or \fILEN\fR is zero)
+then 8192 is used. The maximum allowed value of \fILEN\fR is 1048576.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+output only the ASCII hex rendering of each report LUN, one per line.
+Without the \fI\-\-quiet\fR option, there is header information printed
+before the LUN listing.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the SCSI response (i.e. the data\-out buffer) in binary (to stdout).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-s\fR, \fB\-\-select\fR=\fISR\fR
+\fISR\fR is placed in the SELECT REPORT field of the SCSI REPORT LUNS
+command. The default value is 0. Hexadecimal values may be given with
+a leading "0x" or a trailing "h". For detailed information see the
+REPORT LUNS command in SPC (most recent is SPC\-4 revision 37 in section
+6.33). To simplify, for the I_T nexus associated with the \fIDEVICE\fR, the
+meanings of the \fISR\fR values defined to date for SPC\-4 are:
+.br
+  \fB0\fR : most luns excluding well known logical unit numbers
+.br
+  \fB1\fR : well known logical unit numbers
+.br
+  \fB2\fR : all luns accessible to this I_T nexus
+.br
+  \fB0x10\fR : only accessible administrative luns
+.br
+  \fB0x11\fR : administrative luns plus non-conglomerate luns (see SPC\-4)
+.br
+  \fB0x12\fR : if \fIDEVICE\fR is an administrative LU, then report its
+.br
+         lun plus its subsidiary luns
+.PP
+For \fISR\fR values 0x10 and 0x11, the \fIDEVICE\fR must be either LUN 0 or
+the REPORT LUNS well known logical unit. Values between 0xf8 and
+0xff (inclusive) are vendor specific, other values are reserved. This
+utility will accept any value between 0 and 255 (0xff) for \fISR\fR .
+.TP
+\fB\-t\fR, \fB\-\-test\fR=\fIALUN\fR
+\fIALUN\fR is assumed to be a hexadecimal number in ASCII hex or the
+letter 'L' followed by a decimal number (see below). The hexadecimal number
+can be up to 64 bits in size (i.e. 16 hexadecimal digits) and is padded to
+the right if less than 16 hexadecimal digits are given (e.g.
+\fI\-\-test=0122003a\fR represents T10 LUN: 01 22 00 3a 00 00 00 00).
+\fIALUN\fR may be prefixed by '0x' or '0X' (e.g. the previous example could
+have been \fI\-\-test=0x0122003a\fR). \fIALUN\fR may also be given with
+spaces, tabs, or a '\-' between each byte (or other grouping (e.g.
+c101\-0000\-0000\-0000)). However in the case of space or tab separators
+the \fIALUN\fR would need to be surrounded by single or double quotes.
+.br
+In the leading 'L' case the, following decimal number (hex if preceded
+by '0x') is assumed to be a Linux "word flipped" LUN which is converted
+into a T10 LUN representation and printed. In both cases the number is
+interpreted as a LUN and decoded as if the \fI\-\-decode\fR option had been
+given. Also when \fIALUN\fR is a hexadecimal number it can have a
+trailing 'L' in which case the corresponding Linux "word flipped" LUN value
+is output. The LUN is decoded in all cases.
+.br
+The action when used with \fI\-\-decode\fR is explained under that option.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+The SCSI REPORT LUNS command is important for Logical Unit (LU) discovery.
+After a target device is discovered (usually via some transport specific
+mechanism) and after sending an INQUIRY command (to determine the LU_CONG
+setting), a REPORT LUNS command should either be sent to LUN 0 (which
+is Peripheral device addressing method with bus_id=0 and target/lun=0)
+or to the REPORT LUNS well known LUN (i.e. 0xc101000000000000). SAM\-5
+requires that one of these responds with an inventory of LUNS that are
+contained in this target device.
+.PP
+In test mode, if the \fI\-\-hex\fR option is given once then in the decoded
+output, some of the component fields are printed in hex with leading zeros.
+The leading zeros are to indicate the size of the component field. For
+example: in the Peripheral device addressing method (16 bits overall), the
+bus ID is 6 bits wide and the target/LUN field is 8 bits wide; so both are
+shown with two hex digits (e.g. bus_id=0x02, target=0x3a).
+.SH EXAMPLES
+Typically by the time user space programs get to run, SCSI LUs have been
+discovered. In Linux the lsscsi utility lists the LUs that are currently
+present. The LUN of a device (LU) is the fourth element in the tuple at the
+beginning of each line. Below we see a target (or "I_T Nexus": "6:0:0") has
+two LUNS: 1 and 49409. If 49409 is converted into T10 LUN format it is
+0xc101000000000000 which is the REPORT LUNS well known LUN.
+.PP
+  # lsscsi \-g
+.br
+  [6:0:0:1]    disk    Linux    scsi_debug       0004  /dev/sdb   /dev/sg1
+.br
+  [6:0:0:2]    disk    Linux    scsi_debug       0004  /dev/sdc   /dev/sg2
+.br
+  [6:0:0:49409]wlun    Linux    scsi_debug       0004  \-          /dev/sg3
+.PP
+We could send a REPORT LUNS command (with \fISR\fR 0x0, 0x1 or 0x2) to any
+of those file device nodes and get the same result. Below we use /dev/sg1 :
+.PP
+  # sg_luns /dev/sg1
+.br
+  Lun list length = 16 which imples 2 lun entry
+.br
+  Report luns [select_report=0x0]:
+.br
+      0001000000000000
+.br
+      0002000000000000
+.PP
+That is a bit noisy so cut down the clutter with \fI\-\-quiet\fR:
+.PP
+  # sg_luns \-q /dev/sg1
+.br
+  0001000000000000
+.br
+  0002000000000000
+.PP
+Now decode that LUN into its component parts:
+.PP
+  # sg_luns \-d \-q /dev/sg1
+.br
+  0001000000000000
+.br
+        Peripheral device addressing: lun=1
+.br
+  0002000000000000
+.br
+        Peripheral device addressing: lun=2
+.PP
+Now use \fI\-\-select=1\fR to find out if there are any well known
+LUNs:
+.PP
+  # sg_luns \-q \-s 1 /dev/sg1
+.br
+  c101000000000000
+.PP
+So how many LUNs do we have all together (associated with the current
+I_T Nexus):
+.PP
+  # sg_luns \-q \-s 2 /dev/sg1
+.br
+  0001000000000000
+.br
+  0002000000000000
+.br
+  c101000000000000
+.PP
+  # sg_luns \-q \-s 2 \-d /dev/sg1
+.br
+  0001000000000000
+.br
+        Peripheral device addressing: lun=1
+.br
+  0002000000000000
+.br
+        Peripheral device addressing: lun=1
+.br
+  c101000000000000
+.br
+        REPORT LUNS well known logical unit
+.PP
+The following example uses the \fI\-\-linux\fR option and is not available
+in other operating systems. The extra number in square brackets is the
+Linux version of T10 LUN shown at the start of the line.
+.PP
+  # sg_luns \-q \-s 2 \-l /dev/sg1
+.br
+  0001000000000000    [1]
+.br
+  0002000000000000    [2]
+.br
+  c101000000000000    [49409]
+.PP
+Now we use the \fI\-\-test=\fR option to decode LUNS input on the command
+line (rather than send a REPORT LUNS command and act on the response):
+.PP
+  # sg_luns \-\-test=0002000000000000
+.br
+  Decoded LUN:
+.br
+    Peripheral device addressing: lun=2
+.PP
+  # sg_luns \-\-test="c1 01"
+.br
+  Decoded LUN:
+.br
+    REPORT LUNS well known logical unit
+.PP
+  # sg_luns \-t 0x023a004b \-H
+.br
+  Decoded LUN:
+.br
+    Peripheral device addressing: bus_id=0x02, target=0x3a
+.br
+    >>Second level addressing:
+.br
+      Peripheral device addressing: lun=0x4b
+.PP
+The next example is Linux specific as we try to find out what the
+Linux LUN 49409 translates to in the T10 world:
+.PP
+  # sg_luns \-\-test=L49409
+.br
+  64 bit LUN in T10 preferred (hex) format:  c1 01 00 00 00 00 00 00
+.br
+  Decoded LUN:
+.br
+    REPORT LUNS well known logical unit
+.PP
+And the mapping between T10 and Linux LUN representations can be done the
+other way:
+.PP
+  # sg_luns \-t c101L
+.br
+  Linux 'word flipped' integer LUN representation: 49409
+.br
+  Decoded LUN:
+.br
+    REPORT LUNS well known logical unit
+.br
+.SH EXIT STATUS
+The exit status of sg_luns is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2020 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq(8)
diff --git a/doc/sg_map.8 b/doc/sg_map.8
new file mode 100644
index 0000000..abe0935
--- /dev/null
+++ b/doc/sg_map.8
@@ -0,0 +1,182 @@
+.TH SG_MAP "8" "May 2013" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+sg_map \- displays mapping between Linux sg and other SCSI devices
+.SH SYNOPSIS
+.B sg_map
+[\fI\-a\fR] [\fI-h\fR] [\fI\-i\fR] [\fI\-n\fR] [\fI\-scd\fR] [\fI\-sd\fR]
+[\fI\-sr\fR] [\fI\-st\fR] [\fI\-V\fR] [\fI\-x\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sometimes it is difficult to determine which SCSI device a sg device
+name (e.g. /dev/sg0) refers to. This command loops through the
+sg devices and finds the corresponding SCSI disk, cdrom or tape
+device name (if any). Scanners are an example of SCSI devices
+that have no alternate SCSI device name apart from their sg device
+name.
+.PP
+This utility is deprecated and has not been updated for years, only very
+obvious bugs will be fixed. Unless a very old version of Linux is being
+used (e.g.  2.4 series or earlier), then please use a utility like lsscsi(8)
+or the facilities offered by udev(8).
+.SH OPTIONS
+.TP
+\fB\-a\fR
+assume the sg devices have alphabetical device names and loop
+through /dev/sga, /dev/sgb, etc. Default is numeric scan.
+Note that sg device nodes with an alphabetical index have been
+deprecated since the Linux kernel 2.2 series.
+.TP
+\fB\-h\fR
+print usage message then exit.
+.TP
+\fB\-i\fR
+in addition do a standard INQUIRY and output vendor, product and revision
+strings for devices that are found.
+.TP
+\fB\-n\fR
+assume the sg devices have numeric device names and loop
+through /dev/sg0, /dev/sg1, etc. Default is numeric scan
+.TP
+\fB\-scd\fR
+display mappings to SCSI cdrom device names of the form
+/dev/scd0, /dev/scd1 etc
+.TP
+\fB\-sd\fR
+display mappings to SCSI disk device names
+.TP
+\fB\-sr\fR
+display mappings to SCSI cdrom device names of the form
+/dev/sr0, /dev/sr1 etc
+.TP
+\fB\-st\fR
+display mappings to SCSI tape device names
+.TP
+\fB\-V\fR
+print out version string then exit (without further ado).
+.TP
+\fB\-x\fR
+after each active sg device name is displayed there are
+five digits: <host_number> <bus> <scsi_id> <lun> <scsi_type>
+.SH NOTES
+If no options starting with "\-s" are given then the mapping to
+all SCSI disk, cdrom and tape device names is shown.
+.PP
+If the device file system (devfs) is present a line noting
+this is output. The "native" devfs scsi hierarchy makes the
+relationship between a sg device name and any corresponding
+disk, cdrom or tape device name easy to establish. This
+replaces the need for this command. However many applications
+will continue to look for Linux SCSI device names in their
+traditional places. [Devfs supplies a compatibility daemon
+called devfsd whose default configuration adds back the
+Linux device names in their traditional positions.
+.PP
+Quite often the mapping information can be derived by
+observing the output of the command: "sg_map".
+However if devices have been added since boot this can
+be deceptive.
+.PP
+In the Linux kernel 2.6 series something close to the mapping
+shown by this utility can be found by analysing sysfs. The
+main difference is that sysfs analysis will show the mapping
+between sg nodes and other SCSI device nodes in terms of
+major and minor numbers. While major 8, minor 16 will usually
+be /dev/sdb this is not necessarily so. Facilities associated
+with udev may assign major 8, minor 16 some other device node
+name. This version of sg_map has been extended to cope with
+sparse disk device node names of the form "/dev/sd<str>"
+where <str> can be one of [a\-z,aa\-zz,aaa\-zzz]. See the sg_map26
+utility for a more precise way (i.e. less directory scanning)
+for mapping between sg device names and higher level names;
+including finding user defined names.
+.PP
+This utility was written at a time when hotplugging of SCSI devices
+was not supported in Linux. It used a simple algorithm to scan sg
+device nodes in ascending numeric or alphabetical order, stopping
+after there were 5 consecutive errors.
+.PP
+In the Linux kernel 2.6 series, this utility uses sysfs to find which
+sg device nodes are active and only checks those. Hence there can be
+large "holes" in the numbering of sg device nodes (e.g. after an
+adapter has been removed) and still all active sg device nodes will
+be listed. This utility assumes that sg device nodes are named using
+the normal conventions and searches from /dev/sg0 to /dev/sg4095
+inclusive.
+.SH EXAMPLES
+.PP
+My system has a SCSI disk, a cd writer and a dvd player:
+.br
+   $ sg_map
+.br
+   # Note: the devfs pseudo file system is present
+.br
+   /dev/sg0  /dev/sda
+.br
+   /dev/sg1  /dev/sr0
+.br
+   /dev/sg2  /dev/sr1
+.PP
+In order to find which sg device name corresponds to the disk:
+.br
+   $ sg_map \-sd
+.br
+   # Note: the devfs pseudo file system is present
+.br
+   /dev/sg0  /dev/sda
+.br
+   /dev/sg1
+.br
+   /dev/sg2
+.PP
+The "\-x" option gives the following output:
+.br
+   sg_map \-x
+.br
+   # Note: the devfs pseudo file system is present
+.br
+   /dev/sg0  1 0 1 0  0  /dev/sda
+.br
+   /dev/sg1  2 0 4 0  5  /dev/sr0
+.br
+   /dev/sg2  2 0 6 0  5  /dev/sr1
+.PP
+When a SCSI scanner is added the output becomes:
+.br
+   $ sg_map
+.br
+   # Note: the devfs pseudo file system is present
+.br
+   /dev/sg0  /dev/sda
+.br
+   /dev/sg1  /dev/sr0
+.br
+   /dev/sg2  /dev/sr1
+.br
+   /dev/sg3
+.PP
+By process of elimination /dev/sg3 must be the scanner.
+.SH EXIT STATUS
+The exit status of sg_map is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2000\-2013 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_map26(8)
+,
+.B scsi_info(8)
+,
+.B scsidev(8)
+,
+.B devfsd(8)
+,
+.B lsscsi(8)
+,
+.B udev(7)
diff --git a/doc/sg_map26.8 b/doc/sg_map26.8
new file mode 100644
index 0000000..7ad013b
--- /dev/null
+++ b/doc/sg_map26.8
@@ -0,0 +1,161 @@
+.TH SG_MAP26 "8" "November 2012" "sg3_utils\-1.35" SG3_UTILS
+.SH NAME
+sg_map26 \- map SCSI generic (sg) device to corresponding device names
+.SH SYNOPSIS
+.B sg_map26
+[\fI\-\-dev_dir=DIR\fR] [\fI\-\-given_is=\fR0|1] [\fI\-\-help\fR]
+[\fI\-\-result=\fR0|1|2|3] [\fI\-\-symlink\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Maps a special file (block or char) associated with a SCSI device
+to the corresponding SCSI generic (sg) device, or vice versa.
+Can also be given a sysfs file, for example '/sys/block/sda'
+or '/sys/block/sda/dev'.
+.PP
+Rather than map to or from a sg device, the sysfs file name
+matching a given device special file (or vice versa) can be
+requested. This is done with '\-\-result=2' and '\-\-result=3'.
+This feature works on ATA devices (e.g. 'dev/hdc') as well
+as SCSI devices.
+.PP
+In this utility, "mapped" refers to finding the relationship between
+a SCSI generic (sg) node and the higher level SCSI device name; or
+vice versa. For example '/dev/sg0' may "map" to '/dev/sda'.
+Mappings may not exist, if a relevant module is not loaded, for
+example. Also there are SCSI devices that can only be accessed via a sg
+node (e.g. SAF\-TE and some SES devices).
+.PP
+In this utility, "matching" refers to different representations of
+the same device accessed via the same driver. For example, '/dev/hdc'
+and '/sys/block/hdc' usually refer to the same device and thus would
+be considered matching. A related example is that '/dev/cdrom'
+and '/dev/hdc' are also considered matching if '/dev/cdrom' is a
+symlink to '/dev/hdc'.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-d\fR, \fB\-\-dev_dir\fR=\fIDIR\fR
+where \fIDIR\fR is the directory to search for resultant device special
+files in (or symlinks to same). Only active when '\-\-result=0' (the
+default) or '\-\-result=2'. If this option is not given and \fIDEVICE\fR is
+a device special file then the directory part of \fIDEVICE\fR is assumed.
+If this option is not given and \fIDEVICE\fR is a sysfs name, then if
+necessary '/dev' is assumed as the directory.
+.TP
+\fB\-g\fR, \fB\-\-given_is\fR=0 | 1
+specifies the \fIDEVICE\fR is either a device special file (when the
+argument is 0), or a sysfs 'dev' file (when the argument is 1). The parent
+directory of a sysfs 'dev' file is also accepted (e.g.
+either '/sys/block/sda/dev' or '/sys/block/sda' are accepted). Usually
+there is no need to give this option since this utility first checks for
+special files (or symlinks to special files) and if not, assumes it
+has been given a sysfs 'dev' file (or its parent). Generates an error
+if given and disagrees with variety of \fIDEVICE\fR.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-r\fR, \fB\-\-result\fR=0 | 1 | 2 | 3
+specifies what variety of file (or files) that this utility tries to find.
+The default is a "mapped" device special file, when the argument is 0.
+When the argument is 1, this utility tries to find the "mapped" sysfs node
+name. When the argument is 2, this utility tries to find the "matching"
+device special file. When the argument is 3, this utility tries to find
+the "matching" sysfs node name.
+.TP
+\fB\-s\fR, \fB\-\-symlink\fR
+when a device special file is being sought (i.e. when '\-\-result=0' (the
+default) or '\-\-result=2') then also look for symlinks to that device
+special file in the same directory.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+This utility is designed for the Linux 2.6 (and later) kernel series.
+It uses special file major and minor numbers (and whether the special
+is block or character) together with sysfs to do its mapping or
+matching. In the absence of any other information, device special
+files are assumed to be in the '/dev' directory while sysfs is
+assumed to be mounted at '/sys'. Device names in sysfs are
+predictable, given the corresponding major and minor number of
+the device. However, due to udev rules, the name of device
+special files can be anything the user desires (e.g. '/dev/sda'
+could be named '/dev/my_boot_disk'). When trying to
+find a resultant device special file, this utility uses the major
+and minor numbers (and whether a block or char device is sought)
+to search the device directory.
+.PP
+This utility only shows one relationship at a time. To get an
+overview of all SCSI devices, with special file names and optionally
+the "mapped" sg device name, see the lsscsi utility.
+.SH EXAMPLES
+Assume sg2 maps to sdb while dvd, cdrom and hdc are all matching.
+.PP
+  # sg_map26 /dev/sg2
+.br
+  /dev/sdb
+.PP
+  # sg_map26 /dev/sdb
+.br
+  /dev/sg2
+.PP
+  # sg_map26 \-\-result=0 /dev/sdb
+.br
+  /dev/sg2
+.PP
+  # sg_map26 \-\-result=3 /dev/sdb
+.br
+  /sys/block/sda
+.PP
+  # sg_map26 \-\-result=1 /dev/sdb
+.br
+  /sys/class/scsi_generic/sg0
+.PP
+Now look at '/dev/hdc' and friends
+.PP
+  # sg_map26 /dev/hdc
+.br
+  <error: a hd device does not map to a sg device>
+.PP
+  # sg_map26 \-\-result=3 /dev/hdc
+.br
+  /sys/block/hdc
+.PP
+  # sg_map26 \-\-result=2 /dev/hdc
+.br
+  /dev/hdc
+.PP
+  # sg_map26 \-\-result=2 \-\-symlink /dev/hdc
+.br
+  /dev/cdrom
+.br
+  /dev/dvd
+.br
+  /dev/hdc
+.PP
+  # sg_map26 \-\-result=2 \-\-symlink /sys/block/hdc
+.br
+  /dev/cdrom
+.br
+  /dev/dvd
+.br
+  /dev/hdc
+.SH EXIT STATUS
+The exit status of sg_map26 is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2005\-2012 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B udev(7), lsscsi(lsscsi)
diff --git a/doc/sg_modes.8 b/doc/sg_modes.8
new file mode 100644
index 0000000..4fef24a
--- /dev/null
+++ b/doc/sg_modes.8
@@ -0,0 +1,314 @@
+.TH SG_MODES "8" "July 2022" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_modes \- reads mode pages with SCSI MODE SENSE command
+.SH SYNOPSIS
+.B sg_modes
+[\fI\-\-all\fR] [\fI\-\-control=PC\fR] [\fI\-\-dbd\fR] [\fI\-\-dbout\fR]
+[\fI\-\-examine\fR] [\fI\-\-flexible\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-list\fR] [\fI\-\-llbaa\fR] [\fI\-\-maxlen=LEN\fR]
+[\fI\-\-page=PG[,SPG]\fR] [\fI\-\-raw\fR] [\fI\-R\fR] [\fI\-\-readwrite\fR]
+[\fI\-\-six\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR] [\fIDEVICE\fR]
+.PP
+.B sg_modes
+[\fI\-6\fR] [\fI\-a\fR] [\fI\-A\fR] [\fI\-c=PC\fR] [\fI\-d\fR] [\fI\-D\fR]
+[\fI\-e\fR] [\fI\-f\fR] [\fI\-h\fR] [\fI\-H\fR] [\fI\-l\fR] [\fI\-L\fR]
+[\fI\-m=LEN\fR] [\fI\-p=PG[,SPG]\fR] [\fI\-r\fR] [\fI\-subp=SPG\fR]
+[\fI\-v\fR] [\fI\-V\fR] [\fI\-w\fR] [\fI\-?\fR] [\fIDEVICE\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends a MODE SENSE SCSI command to the \fIDEVICE\fR and
+outputs the response. There is a 6 byte and 10 byte (cdb) variant of the
+MODE SENSE command, this utility defaults to the 10 byte variant. The SPC\-4
+standard (and the SPC\-5 standard) include a note stating that implementers
+should migrate away from the SCSI MODE SELECT(6) and MODE SENSE(6) commands
+in favour of the 10 byte variants (e.g. MODE SENSE(10)).
+.PP
+This utility decodes mode page headers and block descriptors but outputs
+the contents of each mode page in hex. It also has no facility to change
+the mode page contents or block descriptor data. Mode page contents are
+decoded and can be changed by the
+.B sdparm
+utility.
+.PP
+This utility supports two command line syntaxes, the preferred one is
+shown first in the synopsis and explained in this section. A later
+section on the old command line syntax outlines the second group of
+options.
+.PP
+If no page is given (and \fI\-\-list\fR is not selected) then \fI\-\-all\fR
+is assumed. The \fI\-\-all\fR option requests all mode pages (but not
+subpages) in a single response.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+output all the mode pages reported by the \fIDEVICE\fR. This is what the
+page code 63 (0x3f) is defined to do. When used once, mode subpages are
+not fetched. When used twice (e.g. '\-aa'), all mode pages and subpages
+are requested which is equivalent to '\-\-page=63,255'.
+.TP
+\fB\-c\fR, \fB\-\-control\fR=\fIPC\fR
+\fIPC\fR is the page control value. Up to four different versions of each
+page are held by the device:
+.br
+  \fB0\fR : current values (i.e. those active at present)
+.br
+  \fB1\fR : changeable values
+.br
+  \fB2\fR : default values (i.e. the manufacturer's settings)
+.br
+  \fB3\fR : saved values
+.br
+The changeable values are bit masks showing which fields could be changed
+with a MODE SELECT. The saved values will be re\-instated the next time
+the device is power cycled or reset. If this option is not given then
+current values [0] are assumed.
+.TP
+\fB\-d\fR, \fB\-\-dbd\fR
+disable block descriptors. By default, block descriptors (usually
+one (for disks) or none) are returned in a MODE SENSE response. This option
+sets the "disable block descriptors" (DBD) bit in the cdb which instructs
+the device not to return any block descriptors in its response. Older
+devices may not support this setting and may return an "illegal request"
+sense key; alternatively they may ignore it. Oddly the Reduced Block Command
+set (RBC) requires this bit set.
+.TP
+\fB\-D\fR, \fB\-\-dbout\fR
+disable outputting block descriptors. Irrespective of whether block
+descriptors are present in the response or not, they are not output.
+.TP
+\fB\-e\fR, \fB\-\-examine\fR
+examine each mode page in the range 0 through to 62 (inclusive).
+If some response is given then print out the mode page name or
+number (in hex) if the name is not known.
+.br
+The sdparm utility which lists mode and VPD pages also has a \fB\-\-examine\fR
+option will similar functionility.
+.TP
+\fB\-f\fR, \fB\-\-flexible\fR
+Some devices, bridges and/or drivers attempt crude translations between
+MODE SENSE 6 and 10 byte commands without correcting the response. This
+will cause the response to be mis\-interpreted (usually with an error saying
+the response is malformed). With this option, the length of the response
+is checked, and if it looks wrong, the response is then decoded as if the
+other mode sense (cdb length) was sent.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+The default action is to decode known mode page numbers (and subpage
+numbers) into text. When this option is used once, the response is output
+in hexadecimal to stdout. When this option is used twice, mode page numbers
+and page control values are output in hex.
+.br
+When this option is used three times, the full response to the MODE SENSE
+command is output in hex to stdout without any decoding. This form can
+be redirected to a file (or piped) and then used 'sdparm \-\-inhex=' to
+decode.
+.TP
+\fB\-l\fR, \fB\-\-list\fR
+lists all common page and subpage codes and their names that are found in
+the command set that matches the peripheral type of the given \fIDEVICE\fR.
+If no \fIDEVICE\fR and no \fI\-\-page=PG\fR is given then the common page and
+subpage codes and their names are listed for SBC (e.g. a disk). If no
+\fIDEVICE\fR is given and a \fI\-\-page=PG\fR is given then the
+common page and subpage codes and their names are listed for the command set
+whose peripheral device type matches the value given to \fIPG\fR. For
+example 'sg_mode \-\-list \-\-page=1' lists the command mode pages and
+subpages for tape devices. Additionally if a sub_page_code is given then it
+is interpreted as a transport identifier and command transport specific mode
+page codes and their names are listed following the main mode page list.
+Other options are ignored.
+.TP
+\fB\-L\fR, \fB\-\-llbaa\fR
+set the Long LBA Accepted (LLBAA) bit in the MODE SENSE (10) cdb. This
+bit is not defined in the MODE SENSE (6) cdb so setting the '\-L'
+and '\-\-six' options is reported as an error. When set the \fIDEVICE\fR
+may respond with 16 byte block descriptors as indicated by
+the 'LongLBA' field in the response. In most cases setting this option
+is not needed.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+The \fILEN\fR argument is the maximum response length in bytes. It is
+the 'allocation length' field in the cdb. When not given (or \fILEN\fR is
+zero) then the allocation length field is set to 4096 for MODE SENSE (10)
+or 252 for MODE SENSE (6). The \fILEN\fR argument must be non\-negative
+and no greater than 65535 for MODE SENSE (10) and not greater than 255
+for MODE SENSE (6).
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option.
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPG\fR
+page code to fetch. The \fIPG\fR is assumed to be a decimal value unless
+prefixed by '0x' or has a trailing 'h'. It should be a value between 0
+and 63 (inclusive). When not given and a default is required then
+a value of 63 (0x3f), which fetches all mode pages, is used.
+.br
+Alternatively an acronym for the mode page can be given. The available
+acronyms can be listed out with the \fI\-\-page=xxx\fR option. They are
+almost the same as the acronyms used for mode pages in the sdparm utility.
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPG,SPG\fR
+page code and subpage code values to fetch. Both arguments are assumed
+to be decimal unless flagged as hexadecimal. The page code should be
+between 0 and 63 inclusive. The subpage code should be between 0 and 255
+inclusive. The default value for the subpage code is 0.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the response in binary to stdout. Error messages and warnings, if
+any, are sent to stderr. When this option is used twice (e.g. '\-rr')
+then has the same action as '\-R'
+.TP
+\fB\-R\fR
+output the selected mode page to stdout a byte per line. Each line contains
+two hexadecimal digits (e.g. "3e"). Useful as input (after editing) to
+the sg_wr_mode(8) utility.
+.TP
+\fB\-w\fR, \fB\-\-readwrite\fR
+open \fIDEVICE\fR in "read\-write" mode. Default is to open it in read\-only
+mode.
+.TP
+\fB\-6\fR, \fB\-s\fR, \fB\-\-six\fR
+by default this utility sends a 10 byte MODE SENSE command to the
+\fIDEVICE\fR. However some SCSI devices only support 6 byte MODE SENSE
+commands (e.g. SCSI\-2 tape drives). This parameter forces the use of 6
+byte MODE SENSE commands.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string then exit.
+.SH NOTES
+If the normal sg_modes utility fails with "illegal command
+operation code" then try the '\-\-six' (or '\-6') option.
+.PP
+This utility performs a SCSI INQUIRY command to determine the peripheral
+type of the device (e.g. 0 \-> Direct Access Device (disk)) prior to
+sending a MODE SENSE command. This helps in decoding the block
+descriptor and mode pages.
+.PP
+This utility opens \fIDEVICE\fR in read\-only mode (e.g. in Unix, with
+the O_RDONLY flag) by default. It will open \fIDEVICE\fR in read\-write
+mode if the \fI\-\-readwrite\fR option is given.
+.PP
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be a SCSI
+generic (sg) device. In the 2.6 series block devices (e.g. SCSI disks
+and DVD drives) can also be specified. For example "sg_modes \-a /dev/sda"
+will work in the 2.6 series kernels.
+.SH EXIT STATUS
+The exit status of sg_modes is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.TP
+\fB\-6\fR
+by default this utility sends a 10 byte MODE SENSE command to
+the \fIDEVICE\fR. This parameter forces the use of 6 byte MODE SENSE commands.
+See \fI\-\-six\fR in the main description.
+.TP
+\fB\-a\fR
+see \fI\-\-all\fR in the main description.
+.TP
+\fB\-A\fR
+output all the mode pages and subpages supported by the \fIDEVICE\fR. Same
+as '\-\-all \-\-all' in the new syntax.
+.TP
+\fB\-c\fR=\fIPC\fR
+\fIPC\fR is the page control value. See \fB\-\-control\fR=\fIPC\fR in
+the main description.
+.TP
+\fB\-d\fR
+see \fB\-\-dbd\fR in the main description.
+.TP
+\fB\-D\fR
+see \fB\-\-dbout\fR in the main description.
+.TP
+\fB\-e\fR
+see \fB\-\-examine\fR in the main description.
+.TP
+\fB\-f\fR
+see \fB\-\-flexible\fR in the main description.
+.TP
+\fB\-h\fR
+The default action is to decode known mode page numbers (and subpage
+numbers) into text. With this option mode page numbers (and subpage
+numbers) are output in hexadecimal.
+.TP
+\fB\-H\fR
+same action as the '\-h' option.
+.TP
+\fB\-l\fR
+see \fB\-\-list\fR in the main description.
+.TP
+\fB\-L\fR
+see \fB\-\-llbaa\fR in the main description.
+.TP
+\fB-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-m\fR=\fILEN\fR
+see \fB\-\-maxlen\fR=\fILEN\fR in the main description.
+.TP
+\fB\-p\fR=\fIPG\fR
+\fIPG\fR is page code to fetch. Should be a hexadecimal number between 0
+and 3f inclusive (0 to 63 decimal). The default value when required is
+3f (fetch all mode pages). Note that an acronym for the page and/or
+subpage values is not accepted in this older format (because any acronym
+starting with the letters 'a' to 'f' is ambiguous; it could either be a hex
+number or an acronym).
+.TP
+\fB\-p\fR=\fIPG,SPG\fR
+page code and subpage code values to fetch. The page code should be a
+hexadecimal number between 0 and 3f inclusive. The subpage code should
+be a hexadecimal number between 0 and ff inclusive. The default value
+for the subpage code is 0.
+.TP
+\fB\-r\fR
+output the selected mode page to stdout a byte per line. Each line contains
+two hexadecimal digits (e.g. "3e"). Useful as input (after editing) to
+the sg_wr_mode(8) utility.
+.TP
+\fB\-subp\fR=\fISPG\fR
+sub page code to fetch. Should be a hexadecimal number between 0 and
+0xff inclusive. The default value is 0.
+.TP
+\fB\-v\fR
+increase verbosity of output.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.TP
+\fB\-w\fR
+see \fB\-\-readwrite\fR in the main description.
+.TP
+\fB\-?\fR
+output usage message then exit. Ignore all other parameters.
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2000\-2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sdparm(8), sg_wr_mode(8), sginfo(8),
+.B sgmode(scsirastools), scsiinfo(net), scu(net),
+.B seatools(seagate)
+.PP
+All these utilities offer some facility to change mode page (or block
+descriptor) parameters.
diff --git a/doc/sg_opcodes.8 b/doc/sg_opcodes.8
new file mode 100644
index 0000000..21d148b
--- /dev/null
+++ b/doc/sg_opcodes.8
@@ -0,0 +1,339 @@
+.TH SG_OPCODES "8" "September 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_opcodes \- report supported SCSI commands or task management functions
+.SH SYNOPSIS
+.B sg_opcodes
+[\fI\-\-alpha\fR] [\fI\-\-compact\fR] [\fI\-\-enumerate\fR] [\fI\-\-help\fR]
+[\fI\-\-hex\fR] [\fI\-\-inhex=FN\fR] [\fI\-\-json[=JO]\fR] [\fI\-\-mask\fR]
+[\fI\-\-mlu\fR] [\fI\-\-no-inquiry\fR] [\fI\-\-opcode=OP[,SA]\fR]
+[\fI\-\-pdt=DT\fR] [\fI\-\-raw\fR] [\fI\-\-rctd\fR] [\fI\-\-repd\fR]
+[\fI\-\-sa=SA\fR] [\fI\-\-tmf\fR] [\fI\-\-unsorted\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.PP
+.B sg_opcodes
+[\fI\-a\fR] [\fI\-c\fR] [\fI\-e\fR] [\fI\-H\fR] [\fI\-i=FN\fR] [\fI\-j\fR]
+[\fI\-m\fR] [\fI\-M\fR] [\fI\-n\fR] [\fI\-o=OP\fR] [\fI\-p=DT\fR] [\fI\-q\fR]
+[\fI\-R\fR] [\fI\-s=SA\fR] [\fI\-t\fR] [\fI\-u\fR] [\fI\-v\fR] [\fI\-V\fR]
+[\fI\-?\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends a SCSI REPORT SUPPORTED OPERATION CODES or a REPORT
+SUPPORTED TASK MANAGEMENT FUNCTIONS command to the \fIDEVICE\fR and then
+outputs the response. The default action is to report supported operation
+codes. In this mode it will either list all supported commands or give
+detailed information on a specific command identified by the
+\fI\-\-opcode=OP\fR option (perhaps with additional information from the
+\fI\-\-sa=SA\fR option).
+.PP
+The name of a SCSI command depends on its peripheral device type (e.g. a
+disk). The REPORT SUPPORTED OPERATION CODES and REPORT SUPPORTED TASK
+MANAGEMENT FUNCTIONS commands are not supported in the MMC command set for
+CD and DVD devices. This utility does an INQUIRY to obtain the peripheral
+device type and prints out the vendor, product and revision strings.
+.PP
+A similar facility to query supported operation codes previously was available
+via the CmdDt bit in the SCSI INQUIRY command (see sg_inq(8)). However that
+facility was made obsolete and replaced by the REPORT SUPPORTED OPERATION
+CODES command in SPC\-3 (revision 4) during February 2002.
+.PP
+This utility supports two command line syntaxes, the preferred one is
+shown first in the synopsis and explained in this section. A later section
+on the old command line syntax outlines the second group of options.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-a\fR, \fB\-\-alpha\fR
+when all supported commands are being listed there is no requirement for
+the device server (i.e. the \fIDEVICE\fR) to sort the list of commands. When
+this option is given the list of supported commands is sorted by
+name (alphabetically). When this option and the \fB\-\-unsorted\fR option are
+both _not_ given then the list of supported commands is sorted
+numerically (first by operation code and then by service action).
+.TP
+\fB\-c\fR, \fB\-\-compact\fR
+some command names, especially those associated with some service actions,
+are getting longer. This may cause line wrap in the one line per command
+mode on some terminals. When this option is given the opcode and service
+action fields are combined into a single field with the service action,
+prefixed by a comma shown directly after the opcode. If there is no service
+action associated with the command, then the comma and the service action
+are not shown after the opcode. The CDB size field is not shown when this
+option is given.
+.TP
+\fB\-e\fR, \fB\-\-enumerate\fR
+this option prints the name of the SCSI command based on the given opcode,
+peripheral device type and optionally the service action. If given,
+\fIDEVICE\fR is ignored. The opcode, peripheral device type and service
+action default to zero if not given. Thus if this option is the only option
+given then "Test Unit ready" is output since its opcode is 0, it has no
+service action and it is common to all peripheral device types since it is
+defined in the SCSI Primary Commands (SPC) standard(s).
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs the usage message summarizing command line options
+then exits. Ignores \fIDEVICE\fR if given.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+outputs the response in ASCII hexadecimal to stdout. When used once or
+twice, each line starts with a relative (hex) address starting at 0
+on the first hex line output. The difference is when used twice the
+hexadecimal bytes are rendered in ASCII at the right of each line;
+non\-printable characters are replaced by "." .
+.br
+When used three or more times, there is no leading relative address
+on each line. This output is suitable for being redirected to a file
+which can later by given to the \fI\-\-inhex=FN\fR option.
+.TP
+\fB\-i\fR, \fB\-\-inhex\fR=\fIFN\fR
+where \fIFN\fR is a file name whose contents are assumed to be ASCII
+hexadecimal. If \fIDEVICE\fR is also given then \fIDEVICE\fR is ignored,
+a warning is issued and the utility continues, decoding the file named
+\fIFN\fR. See the "FORMAT OF FILES CONTAINING ASCII HEX" section in the
+sg3_utils manpage for more information. If the \fI\-\-raw\fR option is
+also given then the contents of \fIFN\fR are treated as binary.
+.TP
+\fB\-j\fR, \fB\-\-json[\fR=\fIJO\fR]
+output is in JSON format instead of human readable form. See sg3_utils_json
+manpage or use '?' for \fIJO\fR for a summary.
+.TP
+\fB\-m\fR, \fB\-\-mask\fR
+additionally prints out the cdb mask in hex. So a 12 byte cdb will have
+a 12 byte hexadecimal mask. If the hexadecimal is expanded (mentally)
+to binary then a "1" means the corresponding position in the cdb may
+be set. And "0" means the corresponding position in the cdb must not
+be set. For "0" mask positions that a user tries to set in a cdb, the
+device may either ignore it or report an error, typically with a
+sense key of "illegal request".
+.TP
+\fB\-M\fR, \fB\-\-mlu\fR
+additionally prints out an indication (0 or 1) whether the command
+effects all logical units in the containing target. MLU (Multiple Logical
+Units) is a bit in the REPORT SUPPORTED OPERATION CODES response
+introduced by proposal 18\-045r1 (and possibly in spc5r20). Without
+the option, the default output format which lists all opcodes, does
+not include a MLU indication.
+.TP
+\fB\-n\fR, \fB\-\-no-inquiry\fR
+Prior to calling a SCSI REPORT SUPPORTED OPERATION CODES or a REPORT
+SUPPORTED TASK MANAGEMENT FUNCTIONS command, a SCSI INQUIRY command
+is performed. The reason is to determine the peripheral device type (pdt)
+of the \fIDEVICE\fR as this is helpful in translating operation codes
+to the command names. By default this utility prints a summary of INQUIRY
+command response on stdout. If this option (or the \fI\-\-raw\fR option)
+is given then that summary is not printed on stdout.
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option.
+.TP
+\fB\-o\fR, \fB\-\-opcode\fR=\fIOP[,SA]\fR
+the \fIDEVICE\fR will be queried for the given operation code (i.e. the
+\fIOP\fR value) which is the first byte of a SCSI command. Optionally, if
+a \fISA\fR value is given, it will be used as that SCSI command's service
+action. Note that \fIOP\fR and \fIOP,0\fR are not the same thing, as SCSI
+does allow the service action to be 0 (but not in this command). \fIOP\fR
+and \fISA\fR are decimal unless prefixed by "0x" or they have a
+trailing "h". \fIOP\fR should be in the range 0 to 255 (0xff) inclusive.
+\fISA\fR should be in the range 0 to 65535 (0xffff) inclusive. When this
+option is not given then all available SCSI commands supported by the
+\fIDEVICE\fR are listed.
+.TP
+\fB\-p\fR, \fB\-\-pdt\fR=\fIDT\fR
+where \fIDT\fR is the peripheral device type. This is used together with
+the \fI\-\-enumerate\fR to differentiate when a command opcode (and perhaps
+service action) is shared by multiple device types.
+.br
+This option may also be used with the \fI\-\-no-inquiry\fR option to
+suppress this utility doing an INQUIRY command since the main reason
+for doing that is to find the peripheral device type of the \fIDEVICE\fR.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary (to stdout) unless the \fI\-\-inhex=FN\fR option
+is also given. In that case the input file name (\fIFN\fR) is decoded as
+binary (and the output is _not_ in binary (but may be hex)).
+.TP
+\fB\-R\fR, \fB\-\-rctd\fR
+set report command timeout descriptor (RCTD) bit in the cdb. The response
+may or may not contain command timeout descriptors. If available they are
+output. If supported there are two values: a nominal command timeout
+and a recommended command timeout. Both have units of seconds. A value
+of zero means that no timeout is indicated and this is shown in
+the corresponding decoded output as "\-".
+.TP
+\fB\-q\fR, \fB\-\-repd\fR
+set read extended parameter data (REPD) bit in the report task management
+functions cdb. 16 bytes rather than the default 4 bytes expected in the
+response. This was added in SPC\-4 (revision 26).
+.TP
+\fB\-s\fR, \fB\-\-sa\fR=\fISA\fR
+the \fIDEVICE\fR will be queried for a command with the given service
+action (i.e. the \fISA\fR value). Used in conjunction with the
+\fI\-\-opcode=OP\fR option. If this option is not given, \fI\-\-opcode=OP\fR
+is given and the command in question does have a service action then a value
+of 0 will be assumed. \fISA\fR is decimal and expected to be in the range 0
+to 65535 (0xffff) inclusive.
+.TP
+\fB\-t\fR, \fB\-\-tmf\fR
+list supported task management functions. This is done with the SCSI REPORT
+SUPPORTED TASK MANAGEMENT FUNCTIONS command.  When this option is chosen
+the \fI\-\-alpha\fR, \fI\-\-opcode=OP\fR, \fI\-\-rctd\fR, \fI\-\-sa=SA\fR
+and \fI\-\-unsorted\fR options are ignored.
+.TP
+\fB\-u\fR, \fB\-\-unsorted\fR
+when all supported commands are being listed there is no requirement for
+the device server (i.e. the \fIDEVICE\fR) to sort the list of commands. When
+this option is given the list of supported commands is in the order given by
+the \fIDEVICE\fR. When this option is not given the supported commands
+are sorted numerically (first by operation code and then by service action).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string then exit.
+.SH NOTES
+As of SPC\-5 revision 8 the recognized task management functions are:
+abort set, abort task set, clear ACA, clear task set, logical unit reset,
+query task, query asynchronous event, query task set, and I_T nexus reset.
+In SPC\-4 revision 26 target reset and wakeup task management functions
+were made obsolete.
+.PP
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be a SCSI
+generic (sg) device. In the 2.6 series block devices (e.g. SCSI disks
+and DVD drives) can also be specified. For example "sg_opcodes /dev/sda"
+will work in the 2.6 series kernels.
+.SH EXIT STATUS
+The exit status of sg_opcodes is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.TP
+\fB\-a\fR
+sort command alphabetically. Equivalent to \fI\-\-alpha\fR in main
+description.
+.TP
+\fB\-c\fR
+see the \fI\-\-compact\fR option above.
+.TP
+\fB\-e\fR
+see the \fI\-\-enumerate\fR option above.
+.TP
+\fB\-H\fR
+see the \fI\-\-hex\fR option above.
+.TP
+\fB\-m\fR
+see the \fI\-\-mask\fR option above.
+.TP
+\fB\-n\fR
+don't print a summary of the SCSI INQUIRY response on stdout.
+.TP
+\fB-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-o\fR=\fIOP\fR
+the \fIDEVICE\fR will be queried for the given operation code (i.e.
+\fIOP\fR) which is the first byte of a SCSI command. \fIOP\fR is
+hexadecimal and expected to be in the range 0 to ff inclusive.
+When this option is not given then all available SCSI commands supported
+by the \fIDEVICE\fR are listed.
+.TP
+\fB\-p\fR=\fIDT\fR
+see the \fI\-\-pdt=DT\fR option above.
+.TP
+\fB\-q\fR
+set the read extended parameter data (REPD) bit in report TMF cdb.
+Equivalent to \fI\-\-repd\fR in main description.
+.TP
+\fB\-R\fR
+set the report command timeout descriptor (RCTD) bit in cdb. Equivalent
+to \fI\-\-rctd\fR in main description.
+.TP
+\fB\-s\fR=\fISA\fR
+the \fIDEVICE\fR will be queried for a command with the given service
+action (i.e. \fISA\fR). Used in conjunction with the \fI\-o=OP\fR
+option. If this option is not given, \fI\-o=OP\fR is given and the command
+in question does have a service action then a value of 0 will be assumed.
+\fISA\fR is hexadecimal and expected to be in the range 0 to ffff inclusive.
+.TP
+\fB\-t\fR
+list supported task management functions. Equivalent to \fI\-\-tmf\fR in
+the main description.
+.TP
+\fB\-u\fR
+output all supported commands in the order given by \fIDEVICE\fR.
+Equivalent to \fI\-\-unsorted\fR in main description.
+.TP
+\fB\-v\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.TP
+\fB\-?\fR
+output usage message. Ignore all other parameters.
+.SH EXAMPLES
+The examples in this page use Linux device names. For suitable device
+names in other supported Operating Systems see the sg3_utils(8) man page.
+.PP
+To see the information about a specific command give its operation
+code to the '\-\-op=' option. A command line invocation is shown first
+followed by a typical response:
+.PP
+   # sg_opcodes \-\-op=93h /dev/sdb
+.PP
+  Opcode=0x93
+.br
+  Command_name: Write same(16)
+.br
+  Command supported [conforming to SCSI standard]
+.br
+  Usage data: 93 e2 00 00 00 00 ff ff ff ff 00 00 ff ff 00 00
+.PP
+The next example shows the supported task management functions:
+.PP
+   # sg_opcodes \-\-tmf \-n /dev/sdb
+.PP
+Task Management Functions supported by device:
+.br
+    Abort task
+.br
+    Abort task set
+.br
+    Clear ACA
+.br
+    Clear task set
+.br
+    Logical unit reset
+.br
+    Query task
+.PP
+Enumerate can be used to look up a SCSI command name in the absence of a
+device that supports that command. The opcode and service action (if
+required) should be supplied:
+.PP
+   # sg_opcodes \-\-enumerate \-\-op=0x9b,0xa
+.PP
+  SCSI command:
+.br
+    Read buffer(16), read data from echo buffer
+.br
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq,sg3_utils_json(sg3_utils)
diff --git a/doc/sg_persist.8 b/doc/sg_persist.8
new file mode 100644
index 0000000..23a0c97
--- /dev/null
+++ b/doc/sg_persist.8
@@ -0,0 +1,435 @@
+.TH SG_PERSIST "8" "June 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_persist \- use SCSI PERSISTENT RESERVE command to access registrations
+and reservations
+.SH SYNOPSIS
+.B sg_persist
+[\fIOPTIONS\fR] \fIDEVICE\fR
+.PP
+.B sg_persist
+[\fIOPTIONS\fR] \fI\-\-device=DEVICE\fR
+.PP
+.B sg_persist
+\fI\-\-help\fR | \fI\-\-version\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility allows Persistent reservations and registrations to be
+queried and changed. Persistent reservations and registrations are
+queried by sub\-commands (called "service actions" in SPC\-4) of the
+SCSI PERSISTENT RESERVE IN (PRIN) command. Persistent reservations and
+registrations are changed by sub\-commands of the SCSI PERSISTENT RESERVE
+OUT (PROUT) command.
+.PP
+There is a two stage process to obtain a persistent reservation. First an
+application (an I_T nexus in standard's jargon) must register a reservation
+key. If that is accepted (and it should be unless some other I_T nexus has
+registered that key) then the application can try and reserve the device.
+The reserve operation must specify the reservation key and a "type" (see
+the \fI\-\-prout\-type=TYPE\fR option).
+.PP
+It is relatively safe to query the state of Persistent reservations and
+registrations. With no options this utility defaults to the READ KEYS
+sub\-command of the PRIN command. Other PRIN sub\-commands are
+READ RESERVATION, REPORT CAPABILITIES and READ FULL STATUS.
+.PP
+Before trying to change Persistent reservations and registrations users
+should be aware of what they are doing. The relevant sections of the SCSI
+Primary Commands document (i.e. SPC\-5 ANSI INCITS 502\-2020) are sections
+8.14 (titled "Reservations"), 9.16 (for the PRIN command) and 9.17 (for the
+PROUT command). To safeguard against accidental use, the \fI\-\-out\fR option
+must be given when a PROUT sub\-command (e.g.  \fI\-\-register\fR) is used.
+.PP
+The older SCSI RESERVE and RELEASE commands (both 6 and 10 byte variants)
+are not supported by this utility. In SPC\-3, RESERVE and RELEASE are
+deprecated, replaced by Persistent Reservations. RESERVE and RELEASE
+have been removed from SPC\-4 and Annex B is provided showing how to
+convert to persistent reservation commands. See a utility
+called 'scsires' for support of the SCSI RESERVE and RELEASE commands.
+.PP
+The \fIDEVICE\fR is required by all variants of this utility apart
+from \fI\-\-help\fR. The \fIDEVICE\fR can be given either as an
+argument (typically but not necessarily the last one) or via
+the \fI\-\-device=DEVICE\fR option.
+.PP
+SPC\-4 does not use the term "sub\-command". It uses the term "service action"
+for this and for part of a field's name in the parameter block associated
+with the PROUT command (i.e. "service action reservation key"). To lessen
+the potential confusion the term "sub\-command" has been introduced.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The following options are sorted in alphabetical order, based on their
+long option name.
+.TP
+\fB\-l\fR, \fB\-\-alloc-length\fR=\fILEN\fR
+specify the allocation length of the PRIN command. \fILEN\fR is a hex value.
+By default this value is set to the size of the data\-in buffer (8192).
+This parameter is of use for verification that response to PRIN commands
+with various allocation lengths is per section 4.3.5.6 of SPC\-4 revision 18.
+Valid \fILEN\fR values are 0\-8192.
+.TP
+\fB\-C\fR, \fB\-\-clear\fR
+Clear is a sub\-command of the PROUT command. It releases the
+persistent reservation (if any) and clears all registrations from the
+device. It is required to supply a reservation key that is registered
+for this I_T_L nexus (identified by \fI\-\-param\-rk=RK\fR).
+.TP
+\fB\-d\fR, \fB\-\-device\fR=\fIDEVICE\fR
+\fIDEVICE\fR to send SCSI commands to. The \fIDEVICE\fR can either be
+provided via this option or via a freestanding argument. For example,
+these two: 'sg_persist \-\-device=/dev/sg2' and 'sg_persist /dev/sg2'
+are equivalent.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output a usage message showing main options. Use twice (e.g. '\-hh') for
+the other option and more help.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+the response to a valid PRIN sub\-command will be output in hexadecimal.
+By default (i.e. without this option) if the PRIN sub\-command is recognised
+then the response will be decoded as per SPC\-4. May be used more than
+once for more hex and less text.
+.TP
+\fB\-i\fR, \fB\-\-in\fR
+specify that a SCSI PERSISTENT RESERVE IN command is required. This
+is the default.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+\fILEN\fR is used as the ALLOCATION LENGTH field of the PRIN command.
+\fILEN\fR is by default a decimal value. To give a hex value use a '0x'
+or '0X' prefix, or use a 'h' (or 'H') suffix. Can also take multipliers,
+see \fI\-\-maxlen=LEN\fR option in the sg3_utils manual page.
+.br
+This option is the same as \fI\-\-alloc\-length=LEN\fR option apart from
+the representation of \fILEN\fR. The option defaults to decimal while
+\fI\-\-alloc\-length=LEN\fR only takes hex.
+.TP
+\fB\-n\fR, \fB\-\-no\-inquiry\fR
+the default action is to do a standard SCSI INQUIRY command and output
+make, product and revision strings plus the peripheral device type
+prior to executing a PRIN or PROUT command. With this option the
+INQUIRY command is skipped.
+.TP
+\fB\-o\fR, \fB\-\-out\fR
+specify that a SCSI PERSISTENT RESERVE OUT command is required.
+.TP
+\fB\-Y\fR, \fB\-\-param\-alltgpt\fR
+set the 'all target ports' (ALL_TG_PT) flag in the parameter block of the
+PROUT command. Only relevant for 'register' and 'register and ignore existing
+key' sub\-commands.
+.TP
+\fB\-Z\fR, \fB\-\-param\-aptpl\fR
+set the 'activate persist through power loss' (APTPL) flag in the parameter
+block of the PROUT command. Relevant for 'register', 'register and ignore
+existing key' and 'register and move' sub\-commands.
+.TP
+\fB\-K\fR, \fB\-\-param\-rk\fR=\fIRK\fR
+specify the reservation key found in the parameter block of the PROUT
+command. \fIRK\fR is assumed to be hex (up to 8 bytes long). Default value
+is 0. This option is needed by most PROUT sub\-commands.
+.TP
+\fB\-S\fR, \fB\-\-param\-sark\fR=\fISARK\fR
+specify the service action reservation key found in the parameter block
+of the PROUT command. \fISARK\fR is assumed to be hex (up to 8 bytes long).
+Default value is 0. This option is needed by some PROUT sub\-commands.
+.TP
+\fB\-P\fR, \fB\-\-preempt\fR
+Preempt is a sub\-command of the PROUT command. Preempts the existing
+persistent reservation (identified by \fI\-\-param\-sark=SARK\fR) with
+the registration key that is registered for this I_T_L nexus (identified
+by \fI\-\-param\-rk=RK\fR). If a new reservation is established as
+a result of the preemption then the supplied \fI\-\-prout\-type=TYPE\fR
+is used as the type for this new reservation.
+.TP
+\fB\-A\fR, \fB\-\-preempt\-abort\fR
+Preempt and Abort is a sub\-command of the PROUT command. Preempts
+the existing persistent reservation (identified by \fI\-\-param\-sark=SARK\fR)
+with the registration key that is registered for this I_T_L nexus (identified
+by \fI\-\-param\-rk=RK\fR). If a new reservation is established as
+a result of the preemption then the supplied \fI\-\-prout\-type=TYPE\fR
+is used as the type for this new reservation. ACA and other pending
+tasks are aborted.
+.TP
+\fB\-T\fR, \fB\-\-prout\-type\fR=\fITYPE\fR
+specify the PROUT command's 'type' argument. Required by
+the 'register\-move', 'reserve', 'release' and 'preempt (and abort)'
+sub\-commands. Valid \fITYPE\fR values: 1\-> write exclusive, 3\->
+exclusive access, 5\-> write exclusive \- registrants only, 6\->
+exclusive access \- registrants only, 7\-> write exclusive \- all registrants,
+8\-> exclusive access \- all registrants. Default value is 0 (which is
+an invalid type). Each "persistent reservation type" is explained in more
+detail in a subsection of that name in the read reservation section of
+the PRIN command (section 6.15.3.3 of SPC\-4 revision 37).
+.TP
+\fB\-s\fR, \fB\-\-read\-full\-status\fR
+Read Full Status is a sub\-command of the PRIN command. For each registration
+with the given SCSI device, it lists the reservation key and associated
+information. TransportIDs, if supplied in the response, are decoded.
+.TP
+\fB\-k\fR, \fB\-\-read\-keys\fR
+Read Keys is a sub\-command of the PRIN command. Lists all the reservation
+keys registered (i.e. registrations) with the given SCSI device. This is
+the default sub\-command for the SCSI PRIN command.
+.TP
+\fB\-y\fR, \fB\-\-readonly\fR
+Open \fIDEVICE\fR read\-only. May be useful with PRIN commands if there are
+unwanted side effects with the default read\-write open. When given twice
+is interpreted as forcing a read\-write open thus overriding the
+SG_PERSIST_IN_RDONLY environment variable if present. See the ENVIRONMENT
+VARIABLES section for more.
+.TP
+\fB\-r\fR, \fB\-\-read\-reservation\fR
+Read Reservation is a sub\-command of the PRIN command. List information
+about the current holder of the reservation on the \fIDEVICE\fR. If there
+is no current reservation this will be noted. Information about the current
+holder of the reservation includes its reservation key, scope and type.
+.TP
+\fB\-s\fR, \fB\-\-read\-status\fR
+same as \fI\-\-read\-full\-status\fR.
+.TP
+\fB\-G\fR, \fB\-\-register\fR
+Register is a sub\-command of the PROUT command. It has 3 different
+actions depending on associated parameters. a) add a new registration
+with '\-\-param\-rk=0' and '\-\-param\-sark=<new_rk>'; b) Change an existing
+registration with '\-\-param\-rk=<old_rk>'
+and '\-\-param\-sark=<new_rk>'; or  c) Delete an existing registration
+with '\-\-param\-rk=<old_rk>' and '\-\-param\-sark=0'.
+.TP
+\fB\-I\fR, \fB\-\-register\-ignore\fR
+Register and Ignore Existing Key is a sub\-command of the PROUT command.
+Similar to \fI\-\-register\fR except that when changing a reservation key
+the old key is not specified. The '\-\-param\-sark=<new_rk>' option should
+also be given.
+.TP
+\fB\-M\fR, \fB\-\-register\-move\fR
+register (another initiator) and move (the reservation held by the current
+initiator to that other initiator) is a sub\-command of the PROUT command.
+It requires the transportID of the other initiator. [The standard uses the
+term I_T nexus but the point to stress is that there are two initiators
+(the one sending this command and another one) but only one logical unit.]
+The \fI\-\-prout\-type=TYPE\fR and \fI\-\-param\-rk=RK\fR options need to
+match that of the existing reservation while \fI\-\-param\-sark=SARK\fR
+option specifies the reservation key of the new (i.e. destination)
+registration.
+.TP
+\fB\-Q\fR, \fB\-\-relative\-target\-port\fR=\fIRTPI\fR
+relative target port identifier that reservation is to be moved to by
+PROUT 'register and move' sub\-command. \fIRTPI\fR is assumed to be hex
+in the range 0 to ffff inclusive. Defaults to 0 .
+.TP
+\fB\-L\fR, \fB\-\-release\fR
+Release is a sub\-command of the PROUT command. It releases the
+current persistent reservation. The \fI\-\-prout\-type=TYPE\fR
+and \fI\-\-param\-rk=RK\fR options, matching the reservation, must also be
+specified.
+.TP
+\fB\-z\fR, \fB\-\-replace\-lost\fR
+Replace Lost Reservation is a sub\-command of the PROUT command.  It "begins
+a recovery process for the lost persistent reservation that is managed by
+application clients". It also stops the device server terminating commands
+due to a lost persistent reservation. Options should be
+be '\-\-param\-rk=0' (or not given), '\-\-param\-sark=<new_rk>'
+and \fI\-\-prout\-type=TYPE\fR.
+.TP
+\fB\-c\fR, \fB\-\-report\-capabilities\fR
+Report Capabilities is a sub\-command of the PRIN command. It lists
+information about the aspects of persistent reservations that the
+\fIDEVICE\fR supports.
+.TP
+\fB\-R\fR, \fB\-\-reserve\fR
+Reserve is a sub\-command of the PROUT command. It creates a new
+persistent reservation (if permitted). The \fI\-\-prout\-type=TYPE\fR
+and \fI\-\-param\-rk=RK\fR options must also be specified.
+.TP
+\fB\-X\fR, \fB\-\-transport\-id\fR=\fITIDS\fR
+The \fITIDS\fR argument can take one of several forms. It can be a
+comma (or single space) separated list of ASCII hex bytes representing
+a single TransportID as defined in SPC\-5. They are usually 24 bytes
+long apart from in iSCSI. The \fITIDS\fR argument may be a transport
+specific form (e.g. "sas,5000c50005b32001" is clearer than an equivalent
+to the hex byte form: "6,0,0,0,5,0,c5,0,5,b3,20,1"). The \fITIDS\fR argument
+may be "\-" in which case one or more TransportIDs can be read from stdin.
+The \fITIDS\fR argument may be of the form "file=<name>" in which case
+one or more TransportIDs can be read from a file called <name>. See
+the "TRANSPORT IDs" section below for more information.
+.TP
+\fB\-U\fR, \fB\-\-unreg\fR
+optional when the PROUT register and move sub\-command is invoked. If given
+it will unregister the current initiator (I_T nexus) after the other initiator
+has been registered and the reservation moved to it. When not given the
+initiator (I_T nexus) that sent the PROUT command remains registered.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+print out cdb of issued commands prior to execution. If used twice
+prints out the parameter block associated with the PROUT command prior
+to its execution as well. If used thrice decodes given transportID(s)
+as well. To see the response to a PRIN command in low level form use
+the \fI\-\-hex\fR option.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string. Ignore all other parameters.
+.TP
+\fB\-?\fR
+output usage message. Ignore all other parameters.
+.SH TRANSPORT IDs
+TransportIDs are used in persistent reservations to identify initiators.
+The format of a TransportID differs depending on the type of transport
+being used. Their format is described in SPC\-4 (in draft revision 37 see
+section 7.6.4).
+.PP
+A TransportID is required for the PROUT 'register and move' sub\-command and
+the PROUT 'register' sub\-command can have zero, one or more TransportIDs.
+.PP
+When the \fI\-\-transport\-id=TIDS\fR option is given then the \fITIDS\fR
+argument may be a comma (or single space) separated list of ASCII hex bytes
+that represent a single TransportID as defined in SPC\-4. Alternatively the
+\fITIDS\fR argument may be a transport specific string starting with
+either "fcp,", "spi,", "sbp,", "srp,", "iqn", "sas," or "sop,". The "iqn"
+form is an iSCSI qualified name. Apart from "iqn" the other transport
+specific leadin string may be given in upper case (e.g. "FCP,").
+.PP
+The "fcp," form should be followed by 16 ASCII hex digits that represent an
+initiator's N_PORT_NAME (e.g. "fcp,10000000C9F3A571"). The "spi," form should
+be followed by "<scsi_address>,<relative_target_port_identifier>" (both
+decimal numbers). The "sbp," form should be followed by 16 ASCII hex digits
+that represent an initiator's EUI\-64 name. The "srp," form should be
+followed by 32 ASCII hex digits that represent an initiator port identifier.
+The "sas," form should be followed by 16 ASCII hex digits that represent an
+initiator's port SAS address (e.g. "sas,5000c50005b32001"). The "sop," form
+takes a hex number that represents a routing id.
+.PP
+There are two iSCSI qualified name forms. The shorter form contains the
+iSCSI name of the initiator
+port (e.g. "iqn.5886.com.acme.diskarrays\-sn\-a8675309"). The longer form
+adds the initiator session id (ISID in hex) separated by ",i,0x".
+For example "iqn.5886.com.acme.diskarrays\-sn\-a8675309,i,0x1234567890ab".
+On the command line to stop punctuation in an iSCSI name
+being (mis)\-interpreted by the shell, putting the option argument
+containing the iSCSI name in double quotes is advised. iSCSI names are
+encoded in UTF\-8 so if non (7 bit) ASCII characters appear in the
+iSCSI name on the command line, there will be difficulties if they are not
+encoded in UTF\-8. The locale can be changed temporarily by prefixing the
+command line invocation of sg_persist with "LANG=en_US.utf\-8" for example.
+.PP
+Alternatively the \fITIDS\fR argument may specify a file (or pipe) from which
+one or more TransportIDs may be read. If the \fITIDS\fR argument is "\-"
+then stdin (standard input) is read. If the \fITIDS\fR argument is of the
+form "file=<name>" then a file called <name> is read.
+A valid SPC\-4 TransportID is built from the transport specific string
+outlined in the previous paragraphs. The parsing of the data read is
+relatively simple. Empty lines are ignored. Everything from and including
+a "#" on a line is ignored. Leading spaces and tabs are ignored. There
+can be one transportID per line. The transportID can either be a comma,
+space or tab separated list of ASCII hex bytes that represent a
+TransportID as defined in SPC\-4. Padding with zero bytes to a minimum
+length of 24 bytes is performed if necessary. The transportID may also
+be transport specific string type discussed above.
+.PP
+In SPC\-3 the SPEC_I_PT bit set to one and TransportIDs were allowed for
+the PROUT register and ignore existing key sub\-command. In SPC\-4 that
+is disallowed yielding a CHECK CONDITION status with and ILLEGAL REQUEST
+sense key and an additional sense code set to INVALID FIELD IN PARAMETER
+LIST.
+.SH NOTES
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be
+a SCSI generic (sg) device. In the 2.6 series any SCSI device
+name (e.g. /dev/sdc, /dev/st1m or /dev/sg3) can be specified.
+For example "sg_persist \-\-read\-keys /dev/sdb"
+will work in the 2.6 series kernels.
+.PP
+The only scope for PROUT commands supported in the current draft of
+SPC\-4 is "LU_SCOPE". Hence there seems to be no point in offering an
+option to set scope to another value.
+.PP
+Most errors with the PROUT sub\-commands (e.g. missing or
+mismatched \fI\-\-prout\-type=TYPE\fR) will result in a RESERVATION
+CONFLICT status. This can be a bit confusing when you know there is
+only one (active) initiator: the "conflict" is with the SPC standard, not
+another initiator.
+.PP
+Some recent disks accept some PRIN and PROUT sub\-commands when the
+media is stopped. One exception was setting the APTPL flag (with
+the \fI\-\-param\-aptpl\fR option) during a key register operation,
+it complained if the disk one stopped. The error indicated it wanted
+the disk spun up and when that happened, the registration was
+successful.
+.SH EXAMPLES
+These examples use Linux device names. For suitable device names in
+other supported Operating Systems see the sg3_utils(8) man page.
+.PP
+Due to the various option defaults the simplest example executes
+the 'read keys' sub\-command of the PRIN command:
+.PP
+   sg_persist /dev/sdb
+.PP
+This is the same as the following (long\-winded) command:
+.PP
+   sg_persist \-\-in \-\-read\-keys \-\-device=/dev/sdb
+.PP
+To read the current reservation either the '\-\-read\-reservation' form or
+the shorter '\-r' can be used:
+.PP
+   sg_persist \-r /dev/sdb
+.PP
+To
+.B register
+the new reservation key 0x123abc the following could be used:
+.PP
+   sg_persist \-\-out \-\-register \-\-param\-sark=123abc /dev/sdb
+.PP
+Given the above registration succeeds, to
+.B reserve
+the \fIDEVICE\fR (with type 'write exclusive') the following
+could be used:
+.PP
+   sg_persist \-\-out \-\-reserve \-\-param\-rk=123abc
+.br
+              \-\-prout\-type=1 /dev/sdb
+.PP
+To
+.B release
+the reservation the following can be given (note that
+the \-\-param\-rk and \-\-prout\-type arguments must match those of the
+reservation):
+.PP
+   sg_persist \-\-out \-\-release \-\-param\-rk=123abc
+.br
+              \-\-prout\-type=1 /dev/sdb
+.PP
+Finally to
+.B unregister
+a reservation key (and not effect other
+registrations which is what '\-\-clear' would do) the command
+is a little surprising:
+.PP
+   sg_persist \-\-out \-\-register \-\-param\-rk=123abc /dev/sdb
+.PP
+Now have a close look at the difference between the register and
+unregister examples above.
+.PP
+An example file that is suitably formatted to pass transportIDs via
+a '\-\-transport\-id=file=transport_ids.txt' option can be found in the
+examples sub\-directory of the sg3_utils package. There is also a
+simple test script called sg_persist_tst.sh in the same directory.
+.PP
+The above sequence of commands was tested successfully on a Seagate Savvio
+10K.3 disk and a 1200 SSD both of which have SAS interfaces.
+.SH EXIT STATUS
+The exit status of sg_persist is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH ENVIRONMENT VARIABLES
+Currently there is one recognised environment variable: SG_PERSIST_IN_RDONLY.
+If present and only if a PRIN command has been selected then the
+given \fIDEVICE\fR is opened read\-only (e.g. in Unix that is with the
+O_RDONLY flag). See the \fI\-\-readonly\fR option.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2018 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg3_utils(sg3_utils), scsires(internet)
diff --git a/doc/sg_prevent.8 b/doc/sg_prevent.8
new file mode 100644
index 0000000..d0cfb12
--- /dev/null
+++ b/doc/sg_prevent.8
@@ -0,0 +1,59 @@
+.TH SG_PREVENT "8" "November 2012" "sg3_utils\-1.35" SG3_UTILS
+.SH NAME
+sg_prevent \- send SCSI PREVENT ALLOW MEDIUM REMOVAL command
+.SH SYNOPSIS
+.B sg_prevent
+[\fI\-\-allow\fR] [\fI\-\-help\fR] [\fI\-\-prevent=PC\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI PREVENT ALLOW MEDIUM REMOVAL command to \fIDEVICE\fR.
+The default action of this utility is to prevent the removing or
+ejecting of the medium from a drive. This is done by ignoring the
+SCSI START STOP UNIT command (see sg_start) and ignoring the eject
+button on the drive when the user presses it. Drives that hold removable
+disks, tape cartridges or cd/dvd media typically implement this command.
+The definition of the "prevent" codes for this command differ between
+disks and tapes (covered by SBC\-3 and SSC\-3) and cd/dvd drives (covered
+by MMC\-5). The "prevent codes" described here are from MMC\-5.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-a\fR, \fB\-\-allow\fR
+allow medium removal. This is equivalent to setting to '\-\-prevent=2'.
+Cannot be used with \fI\-\-prevent=PC\fR option (i.e. either use
+no options (hence prevent removal), this option or \fI\-\-prevent=PC\fR).
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-p\fR, \fB\-\-prevent\fR=\fIPC\fR
+where \fIPC\fR is a prevent code value. Defined values are: 0 allows removal,
+1 prevents removal (default), 2 allows persistent removal while 3 prevents
+persistent removal. "Persistent" in this context means that the
+initiator (port) that successfully uses code 3 blocks other initiators (ports)
+from allowing removal. A "persistent prevent" state can be cleared by the
+owner allowing persistent removal (code 2) or a power cycle (or anything that
+resets the device (LU)) or some special commands (e.g. various service
+actions of Persistent Reserve Out, see SPC\-3).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH EXIT STATUS
+The exit status of sg_prevent is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2012 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_start(sg3_utils), sg_persist(sg3_utils)
diff --git a/doc/sg_raw.8 b/doc/sg_raw.8
new file mode 100644
index 0000000..6c2bf84
--- /dev/null
+++ b/doc/sg_raw.8
@@ -0,0 +1,270 @@
+.TH SG_RAW "8" "May 2021" "sg3_utils\-1.47" SG3_UTILS
+.SH NAME
+sg_raw \- send arbitrary SCSI or NVMe command to a device
+.SH SYNOPSIS
+.B sg_raw
+[\fI\-\-binary\fR] [\fI\-\-cmdfile=CF\fR] [\fI\-\-cmdset=CS\fR]
+[\fI\-\-enumerate\fR] [\fI\-\-help\fR] [\fI\-\-infile=IFILE\fR]
+[\fI\-\-nosense\fR] [\fI\-\-nvm\fR] [\fI\-\-outfile=OFILE\fR] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-request=RLEN\fR] [\fI\-\-scan=FO,LO\fR]
+[\fI\-\-send=SLEN\fR] [\fI\-\-skip=KLEN\fR] [\fI\-\-timeout=SECS\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR]
+\fIDEVICE\fR [CDB0 CDB1 ...]
+.SH DESCRIPTION
+This utility sends an arbitrary SCSI command (between 6 and 256 bytes) to
+the \fIDEVICE\fR. There may be no associated data transfer; or data may be
+read from a file and sent to the \fIDEVICE\fR; or data may be received from
+the \fIDEVICE\fR and then displayed or written to a file. If supported
+by the pass through, bidirectional commands may be sent (i.e. containing
+both data to be sent to the \fIDEVICE\fR and received from the
+\fIDEVICE\fR).
+.PP
+The SCSI command may be between 6 and 256 bytes long. Each command byte is
+specified in plain hex format (00..FF) without a prefix or suffix. The
+command can be given either on the command line or via the
+\fI\-\-cmdfile=CF\fR option. See EXAMPLES section below.
+.PP
+The commands pass through a generic SCSI interface which is implemented
+for several operating systems including Linux, FreeBSD and Windows.
+.PP
+Experimental support has been added to send NVMe Admin and NVM commands to
+the \fIDEVICE\fR. Since all NVMe commands are 64 bytes long it is more
+convenient to use the \fI\-\-cmdfile=CF\fR option rather than type the 64
+bytes of the NVMe command on the command line. See the section on NVME
+below. A heuristic based on command length is used to decide if the given
+command is SCSI or NVMe, to override this heuristic use the
+\fI\-\-cmdset=CS\fR option.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-b\fR, \fB\-\-binary\fR
+Dump data in binary form, even when writing to stdout.
+.TP
+\fB\-c\fR, \fB\-\-cmdfile\fR=\fICF\fR
+\fICF\fR is the name of a file which contains the command to be executed.
+Without this option the command must be given on the command line, after
+the options and the \fIDEVICE\fR.
+.TP
+\fB\-C\fR, \fB\-\-cmdset\fR=\fICS\fR
+\fICS\fR is a number to indicate which command set (i.e. SCSI or NVMe)
+to use. 0, the default, causes a heuristic based on command length to be
+used. Use a \fICS\fR of 1 to override that heuristic and choose the SCSI
+command set. Use a \fICS\fR of 2 to override that heuristic and choose
+the NVMe command set.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+Display usage information and exit.
+.TP
+\fB\-i\fR, \fB\-\-infile\fR=\fIIFILE\fR
+Read binary data from \fIIFILE\fR instead of stdin. This option is ignored
+if \fB\-\-send\fR is not specified. That data, if used, will become the
+command's "data\-out" buffer.
+.TP
+\fB\-n\fR, \fB\-\-nosense\fR
+Don't display SCSI Sense information.
+.TP
+\fB\-N\fR, \fB\-\-nvm\fR
+When sending NVMe commands, the Admin command set is assumed. To send the
+NVM command set (e.g. the Read and Write (user data) commands) this option
+needs to be given.
+.TP
+\fB\-o\fR, \fB\-\-outfile\fR=\fIOFILE\fR
+Write data received from the \fIDEVICE\fR to \fIOFILE\fR. That data is
+the command's "data\-in" buffer. The data is written in binary. By default,
+data is dumped in hex format to stdout.
+If \fIOFILE\fR is '\-' then data is dumped in binary to stdout.
+This option is ignored if \fI\-\-request\fR is not specified.
+.TP
+\fB\-w\fR, \fB\-\-raw\fR
+interpret \fICF\fR (i.e. the command file) as containing binary. The default
+is to assume that it contains ASCII hexadecimal.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+Open \fIDEVICE\fR read\-only. The default (without this option) is to open
+it read\-write.
+.TP
+\fB\-r\fR, \fB\-\-request\fR=\fIRLEN\fR
+Expect to receive up to \fIRLEN\fR bytes of data from the \fIDEVICE\fR.
+\fIRLEN\fR may be suffixed with 'k' to use kilobytes (1024 bytes) instead
+of bytes. \fIRLEN\fR is decimal unless it has a leading '0x' or a
+trailing 'h'.
+.br
+If \fIRLEN\fR is too small (i.e. either smaller than indicated by the
+cdb (typically the "allocation length" field) and/or smaller than the
+\fIDEVICE\fR tries to send back) then the HBA driver may complain. Making
+\fIRLEN\fR larger than required should cause no problems. Most
+SCSI "data\-in" commands return a data block that contains (in its early
+bytes) a length that the \fIDEVICE\fR would "like" to send back if
+the "allocation length" field in the cdb is large enough. In practice, the
+\fIDEVICE\fR will return no more bytes than indicated in the "allocation
+length" field of the cdb.
+.TP
+\fB\-Q\fR, \fB\-\-scan\fR=\fIFO\fR,\fILO\fR
+Scan a range of opcodes (i.e. first byte of each command). The first opcode
+in the scan is \fIFO\fR (which is decimal unless it has a '0x' prefix or 'h'
+suffix). The last opcode in the scan is \fILO\fR. The maximum value of
+\fILO\fR is 255. The remaining bytes of the SCSI/NVMe command are as
+supplied at invocation.
+.br
+Warning: this option can be
+.B dangerous.
+Sending somewhat arbitrary commands to a device can have unexpected results.
+It is recommended that this option is used with the \fI\-\-cmdset=CS\fR
+option where \fICS\fR is 1 or 2 in order to stop the command set possibly
+changing during the scan.
+.TP
+\fB\-s\fR, \fB\-\-send\fR=\fISLEN\fR
+Read \fISLEN\fR bytes of data, either from stdin or from a file, and send
+them to the \fIDEVICE\fR. In the SCSI transport, \fISLEN\fR becomes the
+length (in bytes) of the "data\-out" buffer. \fISLEN\fR is decimal unless
+it has a leading '0x' or a trailing 'h'.
+.br
+It is the responsibility of the user to make sure that the "data\-out"
+length implied or stated in the cdb matches \fISLEN\fR. Note that some
+common SCSI commands such as WRITE(10) have a "transfer length" field whose
+units are logical blocks (which are usually 512 or 4096 bytes long).
+.TP
+\fB\-k\fR, \fB\-\-skip\fR=\fIKLEN\fR
+Skip the first \fIKLEN\fR bytes of the input file or stream. This option
+is ignored if \fI\-\-send\fR is not specified. If \fI\-\-send\fR is given
+and this option is not given, then zero bytes are skipped.
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fISECS\fR
+Wait up to \fISECS\fR seconds for command completion (default: 20).
+Note that if a command times out the operating system may start by
+aborting the command and if that is unsuccessful it may attempt
+to reset the device.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+Display version and license information and exit.
+.SH NOTES
+The sg_inq utility can be used to send an INQUIRY command to a device
+to determine its peripheral device type (e.g. '1' for a streaming
+device (tape drive)) which determines which SCSI command sets a device
+should support (e.g. SPC and SSC). The sg_vpd utility reads and decodes
+a device's Vital Product Pages which may contain useful information.
+.PP
+The ability to send more than a 16 byte CDB (in some cases 12 byte CDB)
+may be restricted by the pass\-through interface, the low level driver
+or the transport. In the Linux series 3 kernels, the bsg driver can
+handle longer CDBs, block devices (e.g. /dev/sdc) accessed via the
+SG_IO ioctl cannot handle CDBs longer than 16 bytes, and the sg driver
+can handle longer CDBs from lk 3.17 .
+.PP
+The CDB command name defined by T10 for the given CDB is shown if
+the '\-vv' option is given. The command line syntax still needs to be
+correct, so /dev/null may be used for the \fIDEVICE\fR since the CDB
+command name decoding is done before the \fIDEVICE\fR is checked.
+.PP
+The intention of the \fI\-\-scan=FO,LO\fR option is to slightly simplify
+the process of finding hidden or undocumented commands. It should be used
+with care; for example checking for vendor specific SCSI
+commands: 'sg_raw \-\-cmdset=1 \-\-scan=0xc0,0xff /dev/sg1 0 0 0 0 0 0'.
+.SH NVME SUPPORT
+Support for NVMe (a.k.a. NVM Express) is currently experimental. NVMe concepts
+map reasonably well to the SCSI architecture. A SCSI logical unit (LU) is
+similar to a NVMe namespace (although LUN 0 is very common in SCSI while
+namespace IDs start at 1). A SCSI target device is similar to a NVMe
+controller. SCSI commands vary from 6 to 260 bytes long (although SCSI command
+descriptor blocks (cdb_s) longer than 32 bytes are uncommon) while all NVMe
+commands are currently 64 bytes long. The SCSI architecture makes a clear
+distinction between an initiator (often called a HBA) and a target (device)
+while (at least on the PCIe transport) the NVMe controller plays both roles.
+This utility defaults to assuming the user provided 64 byte command belongs
+to NVMe's Admin command set. To issue commands from the "NVM" command set,
+the \fI\-\-nvm\fR option must be given. Admin and NVM commands are sent to
+submission queue 0.
+.PP
+One significant difference is that SCSI uses a big endian representation
+for integers that are longer than 8 bits (i.e. longer than 1 byte) while
+NVMe uses a little endian representation (like most things that have
+originated from the Intel organisation). NVMe specifications talk about
+Words (16 bits), Double Words (32 bits) and sometimes Quad Words (64
+bits) and has tighter alignment requirements than SCSI.
+.PP
+One difference that impacts this utility is that NVMe places pointers to
+host memory in its commands while SCSI leaves this detail to whichever
+transport it is using (e.g. SAS, iSCSI, SRP). Since this utility takes
+the command from the user (either on the command line or in a file named
+\fICF\fR) but this utility allocates a data\-in or data\-out buffer as
+required, the user does not know in advance what the address of that
+buffer will be. Some special addresses have been introduced to help with
+this problem: the address 0xfffffffffffffffe is interpreted as "use the
+data\-in buffer's address" while 0xfffffffffffffffd is interpreted as "use
+the data\-out buffer's address". Since NVMe uses little endian notation
+then that first address appears in the NVMe command byte stream as "fe"
+followed by seven "ff"s. A similar arrangement is made for the length
+of that buffer (in bytes), but since that is a 32 byte quantity, the
+first 4 bytes (all "ff"s) are removed.
+.PP
+Several command file examples can be found in the inhex directory of this
+package's source tarball: nvme_identify_ctl.hex, nvme_dev_self_test.hex,
+nvme_read_ctl.hex and nvme_write_ctl.hex .
+.PP
+Beware: the NVMe standard often refers to some of its fields as "0's based".
+They are typically counts of something like the number of blocks to be read.
+For example in NVMe Read command, a "0's based" number of blocks field
+containing the value 3 means to read 4 blocks! No, this is not a joke.
+.SH EXAMPLES
+These examples, apart from the last one, use Linux device names. For
+suitable device names in other supported Operating Systems see the
+sg3_utils(8) man page.
+.TP
+sg_raw /dev/scd0 1b 00 00 00 02 00
+Eject the medium in CD drive /dev/scd0.
+.TP
+sg_raw \-r 1k /dev/sg0 12 00 00 00 60 00
+Perform an INQUIRY on /dev/sg0 and dump the response data (up to
+1024 bytes) to stdout.
+.TP
+sg_raw \-s 512 \-i i512.bin /dev/sda 3b 02 00 00 00 00 00 02 00 00
+Showing an example of writing 512 bytes to a sector on a disk
+is a little dangerous. Instead this example will read i512.bin (assumed
+to be 512 bytes long) and use the SCSI WRITE BUFFER command to send
+it to the "data" buffer (that is mode 2). This is a safe operation.
+.TP
+sg_raw \-r 512 \-o o512.bin /dev/sda 3c 02 00 00 00 00 00 02 00 00
+This will use the SCSI READ BUFFER command to read 512 bytes from
+the "data" buffer (i.e. mode 2) then write it to the o512.bin file.
+When used in conjunction with the previous example, if both commands
+work then 'cmp i512.bin o512.bin' should show a match.
+.TP
+sg_raw \-\-infile=urandom.bin \-\-send=512 \-\-request=512 \-\-outfile=out.bin "/dev/bsg/7:0:0:0" 53 00 00 00 00 00 00 00 01 00
+This is a bidirectional XDWRITEREAD(10) command being sent via a Linux
+bsg device. Note that data is being read from "urandom.bin" and sent
+to the device (data\-out) while resulting data (data\-in) is placed
+in the "out.bin" file. Also note the length of both is 512 bytes
+which corresponds to the transfer length of 1 (block) in the cdb (i.e.
+the second last byte). urandom.bin can be produced like this:
+.br
+dd if=/dev/urandom bs=512 count=1 of=urandom.bin
+.TP
+sg_raw.exe PhysicalDrive1 a1 0c 0e 00 00 00 00 00 00 e0 00 00
+This example is from Windows and shows a ATA STANDBY IMMEDIATE command
+being sent to PhysicalDrive1. That ATA command is contained within
+the SCSI ATA PASS\-THROUGH(12) command (see the SAT or SAT\-2 standard at
+https://www.t10.org). Notice that the STANDBY IMMEDIATE command does not
+send or receive any additional data, however if it fails sense data
+should be returned and displayed.
+.TP
+For NVME examples see the files in this package's inhex directory that
+start with 'nvme_' such as inhex/nvme_identify_ctl.hex .
+.SH EXIT STATUS
+The exit status of sg_raw is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHOR
+Written by Ingo van Lil
+.SH "REPORTING BUGS"
+Report bugs to <inguin at gmx dot de> or to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2001\-2021 Ingo van Lil
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq, sg_vpd, sg3_utils (sg3_utils), plscsi
diff --git a/doc/sg_rbuf.8 b/doc/sg_rbuf.8
new file mode 100644
index 0000000..078d8b7
--- /dev/null
+++ b/doc/sg_rbuf.8
@@ -0,0 +1,189 @@
+.TH SG_RBUF "8" "October 2017" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_rbuf \- reads data using SCSI READ BUFFER command
+.SH SYNOPSIS
+.B sg_rbuf
+[\fI\-\-buffer=EACH\fR] [\fI\-\-dio\fR] [\fI\-\-help\fR] [\fI\-\-mmap\fR]
+[\fI\-\-quick\fR] [\fI\-\-size=OVERALL\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.PP
+.B sg_rbuf
+[\fI\-b=EACH_KIB\fR] [\fI\-d\fR] [\fI\-m\fR] [\fI\-q\fR]
+[\fI\-s=OVERALL_MIB\fR] [\fI\-t\fR] [\fI\-v\fR] [\fI\-V\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This command reads data with the SCSI READ BUFFER command and then discards
+it. Typically the data being read is from a disk's memory cache. It is
+assumed that the data is sourced quickly (although this is not guaranteed
+by the SCSI standards) so that it is faster than reading data from the media.
+This command is designed for timing transfer speeds across a SCSI transport.
+.PP
+To fetch the data with a SCSI READ BUFFER command and optionally decode it
+see the sg_read_buffer utility. There is also a sg_write_buffer utility
+useful for downloading firmware amongst other things.
+.PP
+This utility supports two command line syntaxes, the preferred one is
+shown first in the synopsis and explained in this section. A later section
+on the old command line syntax outlines the second group of options.
+.PP
+This is a Linux only utility and only works when \fIDEVICE\fR is an sg
+device (e.g. "/dev/sg1"). The sg_read_buffer utility has similar
+functionality and is ported to other OSes and within Linux can use
+bsg and normal block device names (e.g. "/dev/sdc").
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-buffer\fR=\fIEACH\fR
+where \fIEACH\fR is the number of bytes to be transferred by each READ
+BUFFER command. The default is the actual available buffer size returned
+by the READ BUFFER (descriptor) command. The maximum is
+the same as the default, hence this argument can only be used to reduce the
+size of each transfer to less than the device's actual available buffer size.
+.TP
+\fB\-d\fR, \fB\-\-dio\fR
+use direct IO if available. This option is only available if the \fIDEVICE\fR
+is a sg driver device node (e.g. /dev/sg1). In this case the sg driver will
+attempt to configure the DMA from the SCSI adapter to transfer directly into
+user memory. This will eliminate the copy via kernel buffers. If not
+available then this will be reported and indirect IO will be done instead.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print usage message then exit.
+.TP
+\fB\-m\fR, \fB\-\-mmap\fR
+use memory mapped IO if available. This option is only available if the
+\fIDEVICE\fR is a sg driver device node (e.g. /dev/sg1). In this case the
+sg driver will attempt to configure the DMA from the SCSI adapter to transfer
+directly into user memory. This will eliminate the copy via kernel buffers.
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option.
+.TP
+\fB\-q\fR, \fB\-\-quick\fR
+only transfer the data into kernel buffers (typically by DMA from the SCSI
+adapter card) and do not move it into the user space. This option is only
+available if the \fIDEVICE\fR is a sg driver device node (e.g. /dev/sg1).
+.TP
+\fB\-s\fR, \fB\-\-size\fR=\fIOVERALL\fR
+where \fIOVERALL\fR is the size of total transfer in bytes. The default is
+200 MiB (200*1024*1024 bytes). The actual number of bytes transferred may
+be slightly less than requested since all transfers are the same size (and
+an integer division is involved rounding towards zero).
+.TP
+\fB\-t\fR, \fB\-\-time\fR
+times the bulk data transfer component of this command. The elapsed time
+is printed out plus a MB/sec calculation. In this case "MB" is 1,000,000
+bytes. The gettimeofday() system call is used internally for the time
+calculation.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string then exit.
+.SH NOTES
+This command is typically used on modern SCSI disks which have a RAM cache
+in their drive electronics. If no IO to the magnetic media, or slower devices
+like flash RAM, is involved then the disk may be able to source data fast
+enough to saturate the bandwidth of the SCSI transport. The bottleneck may
+then be the DMA element in the HBA, the Linux drivers or the host machine's
+hardware (e.g. speed of RAM).
+.PP
+Various numeric arguments (e.g. \fIOVERALL\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.SH EXAMPLES
+.PP
+On the test system /dev/sg0 corresponds to a fast disk on a U2W SCSI
+bus (max 80 MB/sec). The disk specifications state that its cache is 4 MB.
+.br
+   $ time ./sg_rbuf /dev/sg0
+.br
+READ BUFFER reports: buffer capacity=3434944,
+.br
+    offset boundary=6
+.br
+Read 200 MiB (actual 199 MiB, 209531584 bytes),
+.br
+    buffer size=3354 KiB
+.br
+real 0m5.072s, user 0m0.000s, sys 0m2.280s
+.PP
+So that is approximately 40 MB/sec at 40 % utilization. Now with
+the addition of the "\-q" option this throughput improves and the
+utilization drops to 0%.
+.br
+   $ time ./sg_rbuf \-q /dev/sg0
+.br
+READ BUFFER reports: buffer capacity=3434944,
+.br
+    offset boundary=6
+.br
+Read 200 MiB (actual 199 MiB, 209531584 bytes),
+.br
+    buffer size=3354 KiB
+.br
+real 0m2.784s, user 0m0.000s, sys 0m0.000s
+.SH EXIT STATUS
+The exit status of sg_rbuf is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.TP
+\fB\-b\fR=\fIEACH_KIB\fR
+where \fIEACH_KIB\fR is the number of Kilobytes (i.e. 1024 byte units) to be
+transferred by each READ BUFFER command. Similar to the
+\fI\-\-buffer=EACH\fR option in the main description but the units are
+different.
+.TP
+\fB\-d\fR
+use direct IO if available. Equivalent to the \fI\-\-dio\fR option in the
+main description.
+.TP
+\fB\-m\fR
+use memory mapped IO if available. Equivalent to the \fI\-\-mmap\fR option
+in the main description.
+.TP
+\fB-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-q\fR
+only transfer the data into kernel buffers (typically by DMA from
+the SCSI adapter card) and do not move it into the user space.
+Equivalent to the \fI\-\-quick\fR option in the main description.
+.TP
+\fB\-s\fR=\fIOVERALL_MIB\fR
+where \fIOVERALL_MIB\fR is the size of total transfer in Megabytes (1048576
+bytes). Similar to the \fI\-\-size=OVERALL\fR option in the main description
+but the units are different.
+.TP
+\fB\-t\fR
+times the bulk data transfer component of this command. Equivalent to
+the \fI\-\-time\fR option in the main description.
+.TP
+\fB\-v\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2000\-2017 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_read_buffer, sg_write_buffer, sg_test_rwbuf(all in sg3_utils)
diff --git a/doc/sg_rdac.8 b/doc/sg_rdac.8
new file mode 100644
index 0000000..ddbda94
--- /dev/null
+++ b/doc/sg_rdac.8
@@ -0,0 +1,46 @@
+.TH SG_RDAC "8" "November 2017" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_rdac \- display or modify SCSI RDAC Redundant Controller mode page
+.SH SYNOPSIS
+.B sg_rdac
+[\fI\-6\fR] [\fI\-a\fR] [\fI\-f=LUN\fR] [\fI\-v\fR] [\fI\-V\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+sg_rdac displays or modifies the RDAC controller settings via the
+Redundant Controller mode page (0x2C). When modifying the settings it
+allows one to transfer the ownership of individual drives to the
+controller the command was received on.
+.SH OPTIONS
+.TP
+\fB\-6\fR
+Use the 6 byte cdb variants of the SCSI MODE SENSE and MODE SELECT commands.
+The default action (in the absence of this option) is to use the 10 byte
+cdb variants.
+.TP
+\fB\-a\fR
+Transfer all (visible) devices
+.TP
+\fB\-f\fR=\fILUN\fR
+Transfer the device identified by \fILUN\fR. This command will only work
+if the controller supports 'Dual Active Mode' (aka active/active mode).
+\fILUN\fR is a decimal number which cannot exceed 31 when the \fI\-6\fR
+option is given, otherwise is cannot exceed 255.
+.TP
+\fB\-v\fR
+be verbose
+.TP
+\fB\-V\fR
+print version string then exit
+.SH EXIT STATUS
+The exit status of sg_rdac is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHOR
+Written by Hannes Reinecke <hare at suse dot com>, based on sg_emc_trespass.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2006\-2017 Hannes Reinecke, Douglas Gilbert.
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/sg_read.8 b/doc/sg_read.8
new file mode 100644
index 0000000..3f370a6
--- /dev/null
+++ b/doc/sg_read.8
@@ -0,0 +1,192 @@
+.TH SG_READ "8" "September 2019" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_read \- read multiple blocks of data, optionally with SCSI READ commands
+.SH SYNOPSIS
+.B sg_read
+[\fIblk_sgio=\fR0|1] [\fIbpt=BPT\fR] [\fIbs=BS\fR] [\fIcdbsz=\fR6|10|12|16]
+\fIcount=COUNT\fR [\fIdio=\fR0|1] [\fIdpo=\fR0|1] [\fIfua=\fR0|1]
+\fIif=IFILE\fR [\fImmap=\fR0|1] [\fIno_dxfer=\fR0|1] [\fIodir=\fR0|1]
+[\fIskip=SKIP\fR] [\fItime=TI\fR] [\fIverbose=VERB\fR] [\fI\-\-help\fR]
+[\fI\-\-version\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Read data from a Linux SCSI generic (sg) device, a block device or
+a normal file with each read command issued to the same offset or
+logical block address (lba). This can be used to test (or time) disk
+caching, SCSI (or some other) transport throughput, and/or SCSI
+command overhead.
+.PP
+When the \fICOUNT\fR value is positive, then up to \fIBPT\fR blocks are
+read at a time, until the \fICOUNT\fR is exhausted. Each read operation
+starts at the same lba which, if \fISKIP\fR is not given, is the
+beginning of the file or device.
+.PP
+The \fICOUNT\fR value may be negative when \fIIFILE\fR is a sg device
+or is a block device with 'blk_sgio=1' set. Alternatively 'bpt=0' may
+be given. In these cases |\fICOUNT\fR| "zero block" SCSI READ commands
+are issued. "Zero block" means "do nothing" for SCSI READ 10, 12 and
+16 byte commands (but not for the 6 byte variant). In practice "zero
+block" SCSI READ commands have low latency and so are one way to measure
+SCSI command overhead.
+.PP
+Please note: this is a very old utility that uses 32 bit integers for
+disk LBAs and the count. Hence it will not be able to address beyond
+2 Terabytes on a disk with logical blocks that are 512 bytes long.
+Alternatives are the sg_dd and ddpt utilities.
+.SH OPTIONS
+.TP
+\fBblk_sgio\fR=0 | 1
+The default action of this utility is to use the Unix read() command when
+the \fIIFILE\fR is a block device. In lk 2.6 many block devices can handle
+SCSI commands issued via the SG_IO ioctl. So when this option is set
+the SG_IO ioctl sends SCSI READ commands to \fIIFILE\fR if it is a block
+device.
+.TP
+\fBbpt\fR=\fIBPT\fR
+where \fIBPT\fR is the maximum number of blocks each read operation fetches.
+Fewer blocks will be fetched when the remaining \fICOUNT\fR is less than
+\fIBPT\fR. The default value for \fIBPT\fR is 128. Note that each read
+operation starts at the same lba (as given by \fIskip=SKIP\fR or 0).
+If 'bpt=0' then the \fICOUNT\fR is interpreted as the number of zero
+block SCSI READ commands to issue.
+.TP
+\fBbs\fR=\fIBS\fR
+where \fIBS\fR is the size (in bytes) of each block read. This
+.B must
+be the block size of the physical device (defaults to 512) if SCSI commands
+are being issued to \fIIFILE\fR.
+.TP
+\fBcdbsz\fR=6 | 10 | 12 | 16
+size of SCSI READ commands issued on sg device names, or block devices
+if 'blk_sgio=1' is given. Default is 10 byte SCSI READ cdbs.
+.TP
+\fBcount\fR=\fICOUNT\fR
+when \fICOUNT\fR is a positive number, read that number of blocks,
+typically with multiple read operations. When \fICOUNT\fR is negative then
+|\fICOUNT\fR| SCSI READ commands are performed requesting zero blocks
+to be transferred. This option is mandatory.
+.TP
+\fBdio\fR=0 | 1
+default is 0 which selects indirect IO. Value of 1 attempts direct
+IO which, if not available, falls back to indirect IO and notes this
+at completion. This option is only active if \fIIFILE\fR is an sg device.
+If direct IO is selected and /sys/module/sg/parameters/allow_dio
+has the value of 0 then a warning is issued (and indirect IO is performed)
+.TP
+\fBdpo\fR=0 | 1
+when set the disable page out (DPO) bit in SCSI READ commands is set.
+Otherwise the DPO bit is cleared (default).
+.TP
+\fBfua\fR=0 | 1
+when set the force unit access (FUA) bit in SCSI READ commands is set.
+Otherwise the FUA bit is cleared (default).
+.TP
+\fBif\fR=\fIIFILE\fR
+read from this \fIIFILE\fR. This argument must be given. If the \fIIFILE\fR
+is a normal file then it must be seekable (if (\fICOUNT\fR > \fIBPT\fR) or
+\fIskip=SKIP\fR is given). Hence stdin is not acceptable (and giving "\-"
+as the \fIIFILE\fR argument is reported as an error).
+.TP
+\fBmmap\fR=0 | 1
+default is 0 which selects indirect IO. Value of 1 causes memory mapped
+IO to be performed. Selecting both dio and mmap is an error. This option
+is only active if \fIIFILE\fR is an sg device.
+.TP
+\fBno_dxfer\fR=0 | 1
+when set then DMA transfers from the device are made into kernel buffers
+but no further (i.e. there is no second copy into the user space). The
+default value is 0 in which case transfers are made into the user space.
+When neither mmap nor dio is set then data transfer are copied via
+kernel buffers (i.e. a double copy). Mainly for testing.
+.TP
+\fBodir\fR=0 | 1
+when set opens an \fIIFILE\fR which is a block device with an additional
+O_DIRECT flag. The default value is 0 (i.e. don't open block devices
+O_DIRECT).
+.TP
+\fBskip\fR=\fISKIP\fR
+all read operations will start offset by \fISKIP\fR bs\-sized blocks
+from the start of the input file (or device).
+.TP
+\fBtime\fR=\fITI\fR
+When \fITI\fR is 0 (default) doesn't perform timing.
+When 1, times transfer and does throughput calculation, starting at the
+first issued command until completion. When 2, times transfer and does
+throughput calculation, starting at the second issued command until
+completion. When 3 times from third command, etc. An average number of
+commands (SCSI READs or Unix read()s) executed per second is also
+output.
+.TP
+\fBverbose\fR=\fIVERB\fR
+as \fIVERB\fR increases so does the amount of debug output sent to stderr.
+Default value is zero which yields the minimum amount of debug output.
+A value of 1 reports extra information that is not repetitive.
+.TP
+\fB\-\-help\fR
+Output the usage message then exit.
+.TP
+\fB\-\-version\fR
+Output the version string then exit.
+.SH NOTES
+Various numeric arguments (e.g. \fISKIP\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+Data usually gets to the user space in a 2 stage process: first the
+SCSI adapter DMAs into kernel buffers and then the sg driver copies
+this data into user memory.
+This is called "indirect IO" and there is a "dio" option to select
+"direct IO" which will DMA directly into user memory. Due to some
+issues "direct IO" is disabled in the sg driver and needs a
+configuration change to activate it. This is typically done with
+"echo 1 > /sys/module/sg/parameters/allow_dio". An alternate way to avoid the
+2 stage copy is to select memory mapped IO with 'mmap=1'.
+.SH SIGNALS
+The signal handling has been borrowed from dd: SIGINT, SIGQUIT and
+SIGPIPE output the number of remaining blocks to be transferred;
+then they have their default action.
+SIGUSR1 causes the same information to be output yet the copy continues.
+All output caused by signals is sent to stderr.
+.SH EXAMPLES
+.PP
+Let us assume that /dev/sg0 is a disk and we wish to time the disk's
+cache performance.
+.PP
+   sg_read if=/dev/sg0 bs=512 count=1MB mmap=1 time=2
+.PP
+This command will continually read 128  512 byte blocks from block 0.
+The "128" is the default value for 'bpt' while "block 0" is chosen
+because the 'skip' argument was not given. This will continue until
+1,000,000 blocks are read. The idea behind using 'time=2' is that the
+first 64 KiB read operation will involve reading the magnetic media
+while the remaining read operations will "hit" the disk's cache. The
+output of third command will look like this:
+.PP
+  time from second command to end was 4.50 secs, 113.70 MB/sec
+.br
+  Average number of READ commands per second was 1735.27
+.br
+  1000000+0 records in, SCSI commands issued: 7813
+.SH EXIT STATUS
+The exit status of sg_read is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2000\-2019 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+To time streaming media read or write time see
+.B sg_dd
+is in the sg3_utils package and
+.B ddpt
+in a package of the same name.
+The lmbench package contains
+.B lmdd
+which is also interesting.
+.B raw(8), dd(1)
diff --git a/doc/sg_read_attr.8 b/doc/sg_read_attr.8
new file mode 100644
index 0000000..8ab59b9
--- /dev/null
+++ b/doc/sg_read_attr.8
@@ -0,0 +1,214 @@
+.TH SG_READ_ATTR "8" "December 2020" "sg3_utils\-1.46" SG3_UTILS
+.SH NAME
+sg_read_attr \- send SCSI READ ATTRIBUTE command
+.SH SYNOPSIS
+.B sg_read_attr
+[\fI\-\-cache\fR] [\fI\-\-enumerate\fR] [\fI\-\-ea=EA\fR]
+[\fI\-\-filter=FL\fR] [\fI\-\-first=FAI\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-in=FN\fR] [\fI\-\-lvn=LVN\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-pn=PN\fR]
+[\fI\-\-quiet\fR] [\fI\-\-raw\fR] [\fI\-\-readonly\fR] [\fI\-\-sa=SA\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI READ ATTRIBUTE command to \fIDEVICE\fR and outputs the data
+returned. This command was introduced in SPC\-3 revision 1 and thus is
+applicable to all SCSI devices. In practice it is used mainly for tape
+systems. This utility is based on the SPC\-5 draft standard, revision
+17 (spc5r17.pdf).
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-c\fR, \fB\-\-cache\fR
+sets the CACHE bit in the READ ATTRIBUTE cdb. This instructs the device
+server to return cached attributes. By default that bit is cleared
+which instructs the device server not to return cached attributes.
+.TP
+\fB\-e\fR, \fB\-\-enumerate\fR
+enumerates all known attributes and service actions. Attributes include
+an identifier, length, format and a name as defined by T10. If \fIDEVICE\fR
+is given then it is ignored.
+.TP
+\fB\-E\fR, \fB\-\-ea\fR=\fIEA\fR
+where \fIEA\fR is an element address which is placed in the READ ATTRIBUTE
+cdb. This field is only found in SMC\-2 and SMC\-3 drafts for medium
+changers usually associated with tape libraries. By default this field
+is set to zero.
+.TP
+\fB\-f\fR, \fB\-\-filter\fR=\fIFL\fR
+where \fIFL\fR is an attribute identifier in the range 0 to 65535 or \-1.
+Attribute identifiers are typically given in hexadecimal in which case the
+hex number should be prefixed by "0x" or has a trailing "h". "\-1" is
+the default value and means 'match all'; for all other values of \fIFL\fR
+on the matching attribute is output.
+.TP
+\fB\-F\fR, \fB\-\-first\fR=\fIFAI\fR
+where \fIFAI\fR is the "first attribute identifier" field in the cdb. It
+seems as though the intent of this field is that only attributes whose
+identifiers are equal to or greater than \fIFAI\fR are returned. The default
+value of \fIFAI\fR is zero. Attributes are returned in ascending identifier
+order.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response in hexadecimal to stdout. When used once the whole
+response is output in ASCII hexadecimal with a leading address (starting at
+0) on each line. When used twice each attribute descriptor in the response
+is output separately in hexadecimal. When used thrice the whole response is
+output in hexadecimal with no leading address (on each line).
+.br
+Output generated by '\-HHH' (or \fI\-\-hex\fR used three times) can be
+redirected to a file. That file will be in suitable format for \fI\-\-in=FN\fR
+to use in a later invocation.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIFN\fR
+\fIFN\fR is treated as a file name (or '\-' for stdin) which contains ASCII
+hexadecimal or binary representing the response to a READ ATTRIBUTE command
+with service action 0x0 (i.e (fetch) attribute values). When this option is
+given then \fIDEVICE\fR (if also given) is ignored.
+.br
+By default \fIFN\fR is assumed to contain ASCII hexadecimal arranged as
+bytes which a space, tab or comma delimited. All characters from (and
+including) "#" to the end of line are ignored. If the \fI\-\-raw\fR option
+is also given then \fIFN\fR is assumed to contain binary data. When the
+\fI\-\-raw\fR option is given then after processing the input the
+internal raw variable is reset to 0 so it has no effect on the output.
+.br
+Since the READ ATTRIBUTE response does not contain the service action number
+that it is a response to, then the \fI\-\-sa=SA\fR should be given (if not
+service action 0 (attribute values) is assumed.
+.TP
+\fB\-l\fR, \fB\-\-lvn\fR=\fILVN\fR
+where \fILVN\fR is placed in the "logical volume number" field of the cdb.
+The default value is zero which is required to be the logical volume number
+if the device only has one volume.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given (or \fILEN\fR is zero)
+then 8192 is used. The maximum allowed value of \fILEN\fR is 1048576.
+.TP
+\fB\-p\fR, \fB\-\-pn\fR=\fIPN\fR
+where \fIPN\fR is placed in the "partition number" field of the cdb. If
+the \fIDEVICE\fR only has one partition then its partition number must be
+zero. The default value of \fIPN\fR is zero.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+this option reduces the amount of information output. For example when
+used once (\fISA\fR=0), it suppresses the header line announcing the
+output of attributes; when used twice it suppresses the name of each
+attribute, leaving only the associated attribute values (or strings).
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the SCSI response (i.e. the data\-out buffer) in binary (to stdout).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-s\fR, \fB\-\-sa\fR=\fISA\fR
+where \fISA\fR is placed on the "service action" field of the cdb. Values
+of 0 to 63 are accepted with a default of 0. spc5r08.pdf defines five
+service actions: 0 for attributes values ; 1 for an attribute list (names,
+not values), 2 for the logical volume list; 3 for the partition list; 4
+is restricted for SMC\-3; and 5 for the supported attribute list.
+.br
+Alternatively an acronym can be given for \fISA\fR. The acronym should be
+one of "av", "al", "lvl", "pn", "smc" or "sa" for service actions 0 to 5
+respectively. The acronyms can also be given in upper case.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+Only tape systems seem to implement the SCSI READ ATTRIBUTE command. The vast
+majority of its definition is in the SPC standard so other device types could
+use it.
+.PP
+Much of the information provided by READ ATTRIBUTE can also be found in
+pages returned by LOG SENSE (see the sg_logs utility) and in the VPD
+pages returned by the INQUIRY command.
+.SH EXAMPLES
+To list the attributes of a tape drive whose \fIDEVICE\fR is /dev/sg1 ,
+the following could be used:
+.PP
+# sg_read_attr \-s al /dev/sg1
+.br
+Attribute list:
+.br
+  Remaining capacity in partition [MiB]
+.br
+  Maximum capacity in partition [MiB]
+.br
+  TapeAlert flags
+.br
+  Load count
+.br
+  MAM space remaining [B]
+.br
+  Assigning organization
+.br
+  Format density code
+.br
+  ...
+.PP
+To check the number of partitions:
+.PP
+# sg_read_attr \-s pl /dev/sg1
+.br
+Partition number list:
+.br
+  First partition number: 0
+.br
+  Number of partitions available: 2
+.PP
+And to see the attribute values (which is the default service action):
+.PP
+# sg_read_attr /dev/sg1
+.br
+Attribute values:
+.br
+  Remaining capacity in partition [MiB]: 1386103
+.br
+  Maximum capacity in partition [MiB]: 1386103
+.br
+  TapeAlert flags: 0
+.br
+  ....
+.PP
+To redirect the attribute values response to a file for later decoding:
+.PP
+# sg_read_attr \-HHH /dev/sg1 > av.hex
+.PP
+And later the response held in the av.hex file could be decoded with:
+.PP
+# sg_read_attr \-s av \-\-in=av.hex
+.br
+Attribute values:
+.br
+  Remaining capacity in partition [MiB]: 1386103
+.br
+  Maximum capacity in partition [MiB]: 1386103
+.br
+  TapeAlert flags: 0
+.br
+  ....
+.PP
+.SH EXIT STATUS
+The exit status of sg_read_attr is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2016\-2020 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_vpd,sg_logs(sg3_utils)
diff --git a/doc/sg_read_block_limits.8 b/doc/sg_read_block_limits.8
new file mode 100644
index 0000000..75bbd8e
--- /dev/null
+++ b/doc/sg_read_block_limits.8
@@ -0,0 +1,68 @@
+.TH SG_READ_BLOCK_LIMITS "8" "November 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_read_block_limits \- send SCSI READ BLOCK LIMITS command
+.SH SYNOPSIS
+.B sg_read_block_limits
+[\fI\-\-help\fR] [\fI\-\-hex\fR] [--mloi] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send a SCSI READ BLOCK LIMITS command to \fIDEVICE\fR and outputs the
+response. This command is defined for tape (drives) and its description
+is found in the SSC documents at https://www.t10.org .
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output response in hex (rather than decode it).
+.TP
+\fB\-m\fR, \fB\-\-mloi\fR
+sets the MLOI bit in the READ BLOCK LIMITS command and if that
+succeeds, prints out the Maximum Logical Object Identifier (MLOI)
+value. The MLOI bit was introduced in the ssc4r02.pdf draft.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary to stdout.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open \fIDEVICE\fR in read\-only mode. The default is to open it in
+read\-write mode.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH EXIT STATUS
+The exit status of sg_read_block_limits is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH EXAMPLES
+It is usually okay to use no options. Here is an invocation (on the first
+line following the "#" command prompt) followed by some typical output:
+.PP
+   # sg_read_block_limits /dev/st0
+.br
+Read Block Limits results:
+.br
+    Minimum block size: 1 byte(s)
+.br
+    Maximum block size: 16777215 byte(s), 16383 KB, 15 MB
+.br
+    Granularity: 0
+.br
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2009\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg3_utils(sg3_utils)
diff --git a/doc/sg_read_buffer.8 b/doc/sg_read_buffer.8
new file mode 100644
index 0000000..0acc916
--- /dev/null
+++ b/doc/sg_read_buffer.8
@@ -0,0 +1,175 @@
+.TH SG_READ_BUFFER "8" "February 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_read_buffer \- send SCSI READ BUFFER command
+.SH SYNOPSIS
+.B sg_read_buffer
+[\fI\-\-eh_code=EHC\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-id=ID\fR]
+[\fI\-\-inhex=FN\fR] [\fI\-\-length=LEN\fR] [\fI\-\-mode=MO\fR]
+[\fI\-\-no_output\fR] [\fI\-\-offset=OFF\fR] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-specific=MS\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI READ BUFFER command to the \fIDEVICE\fR, and if there is a
+response either decodes it, prints it in hexadecimal or sends it in binary to
+stdout. If a response is received for a "descriptor" mode then, in the absence
+of \fI\-\-hex\fR and \fI\-\-raw\fR, it is decoded. Response for
+non\-descriptor modes are output in hexadecimal unless the \fI\-\-raw\fR
+option is given.
+.PP
+The responses to the Read microcode status ('rd_microc_st' [0xf]) and Error
+history ('err_hist' [0x1c]) modes are decoded as described in spc6r06.pdf and
+earlier T10 documents.
+.PP
+This utility may be called without a \fIDEVICE\fR but with a
+\fI\-\-inhex=FN\fR option instead. \fIFN\fR is expected to be a file name (or
+ '\-' for stdin). The contents of the file (or stdin stream) is assumed to be
+hexadecimal (or binary) data that represents a SCSI READ BUFFER command
+response and is decoded as such.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-e\fR, \fB\-\-eh_code\fR=\fIEHC\fR
+\fIEHC\fR is the error history code placed in the Buffer ID field of the cdb.
+The Mode field is set to err_hist [0x1c]. The option is equivalent to using
+the '\fI\-\-mode=eh\fR \fI\-\-id=EHC\fR' options. If this option and one of
+the other options is given, then an error will be generated if they
+contradict. The default (maximum) response length is increased to 64 bytes
+when may need to be increased (if so that is noted if the output is
+truncated).
+.br
+An example is setting \fIEHC\fR to 0 in which case the error history
+directory will be decoded (unless \fI\-\-hex\fR or \fI\-\-raw\fR options
+is given).
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit. If used multiple times also prints
+the mode names and their acronyms.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response in hexadecimal. When given twice the response is
+output in hex with the corresponding representation in ASCII to the
+right of each line. When given three time the hex is printed without
+addresses (indexes) at the start of each line; this type of format is
+suitable for the \fI\-\-inhex=FN\fR option on a subsequent invocation.
+.TP
+\fB\-i\fR, \fB\-\-id\fR=\fIID\fR
+this option sets the Buffer ID field in the cdb. \fIID\fR is a value between
+0 (default) and 255 inclusive. The meaning of the Buffer ID field varies
+with the value in the Mode field of the cdb.
+.TP
+\fB\-I\fR, \fB\-\-inhex\fR=\fIFN\fR
+\fIFN\fR is expected to be a file name (or '\-' for stdin) which contains
+ASCII hexadecimal or binary representing a READ BUFFER response. If known
+this utility will then decode that response. It is preferable to also supply
+the \fI\-\-mode=MO\fR, \fI\-\-id=ID\fR and possible \fI\-\-specific=MS\fR
+options, since these are not present in the response. See the "FORMAT OF
+FILES CONTAINING ASCII HEX" section in the sg3_utils manpage for more
+information. If the \fI\-\-raw\fR option is also given then the contents
+of \fIFN\fR is treated as binary.
+.TP
+\fB\-l\fR, \fB\-\-length\fR=\fILEN\fR
+where \fILEN\fR is the length, in bytes, that is placed in the "allocation
+length" field in the cdb. The default value is 4 (bytes) which is increased
+to 64 if the 'err_hist' mode [0x1c] is given or implied. The device may
+respond with less bytes.
+.br
+If the \fI\-\-inhex=FN\fR option is given, then the default value of the
+length is increased to 8192 bytes. This length may then be reduced to match
+the number of bytes decoded from the contents of \fIFN\fR.
+.TP
+\fB\-m\fR, \fB\-\-mode\fR=\fIMO\fR
+this option sets the mode field in the cdb. \fIMO\fR is a value between
+0 (default) and 31 inclusive. Alternatively an abbreviation can be given.
+See the MODES section below. To list the available mode abbreviations use
+an invalid one (e.g. '\-\-mode=xxx'). As an example, to fetch the read
+buffer descriptor give '\-\-mode=desc' .
+.TP
+\fB\-N\fR, \fB\-\-no_output\fR
+when this option is given after sending the SCSI command to the \fIDEVICE\fR
+the response is processed, looking for any errors, and then this utility
+exits. Any data read by the READ BUFFER command is ignored.
+.br
+May be useful for timing larger reads from the \fIDEVICE\fR buffer in 'data'
+mode [0x2].
+.TP
+\fB\-o\fR, \fB\-\-offset\fR=\fIOFF\fR
+this option sets the buffer offset field in the cdb. \fIOFF\fR is a value
+between 0 (default) and 2**24\-1 . It is a byte offset.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+if a response is received then it is sent in binary to stdout. When this
+option is given together with \fI\-\-inhex=FN\fR then the contents of
+\fIFN\fR is assumed to be binary and the output of this utility is
+normal ASCII (i.e. _not_ in binary).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-S\fR, \fB\-\-specific\fR=\fIMS\fR
+this option sets the mode specific field in the cdb. \fIMS\fR is a value
+between 0 and 7 as this is a 3 bit field.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH MODES
+Following is a list of READ BUFFER command settings for the MODE field.
+First is an acronym accepted by the \fIMO\fR argument of this utility.
+Following the acronym in square brackets are the corresponding decimal and
+hex values that may also be given for \fIMO\fR. The following are listed
+in numerical order.
+.TP
+hd  [0, 0x0]
+Combined header and data (obsolete in SPC\-4).
+.TP
+vendor  [1, 0x1]
+Vendor specific.
+.TP
+data  [2, 0x2]
+Data.
+.TP
+desc  [3, 0x3]
+Descriptor: yields 4 bytes that contain an offset boundary field (1 byte)
+and buffer capacity (3 bytes).
+.TP
+echo  [10, 0xa]
+Read data from echo buffer (was called "Echo buffer" in SPC\-3).
+.TP
+echo_desc  [11, 0xb]
+Echo buffer descriptor: yields 4 bytes of which the last (lowest) 13 bits
+represent the echo buffer capacity. The maximum echo buffer size is 4096
+bytes.
+.TP
+rd_microc_st  [15, 0xf]
+Read microcode status. Added in spc5r20 .
+.TP
+en_ex  [26, 0x1a]
+Enable expander communications protocol and Echo buffer. Made obsolete in
+SPC\-4.
+.TP
+err_hist|eh  [28, 0x1c]
+Error history. Either 'err_hist' or the short 'eh' abbreviation can be used
+for this mode. Introduced in SPC\-4.
+.SH NOTES
+All numbers given with options are assumed to be decimal.
+Alternatively numerical values can be given in hexadecimal preceded by
+either "0x" or "0X" (or has a trailing "h" or "H").
+.SH EXIT STATUS
+The exit status of sg_read_buffer is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Luben Tuikov and Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2006\-2019 Luben Tuikov and Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_write_buffer(sg3_utils)
diff --git a/doc/sg_read_long.8 b/doc/sg_read_long.8
new file mode 100644
index 0000000..e1b1591
--- /dev/null
+++ b/doc/sg_read_long.8
@@ -0,0 +1,102 @@
+.TH SG_READ_LONG "8" "November 2015" "sg3_utils\-1.42" SG3_UTILS
+.SH NAME
+sg_read_long \- send a SCSI READ LONG command
+.SH SYNOPSIS
+.B sg_read_long
+[\fI\-\-16\fR] [\fI\-\-correct\fR] [\fI\-\-help\fR] [\fI\-\-lba=LBA\fR]
+[\fI\-\-out=OF\fR] [\fI\-\-pblock\fR] [\fI\-\-readonly\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] [\fI\-\-xfer_len=BTL\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send SCSI READ LONG command to \fIDEVICE\fR. The read buffer is output in hex
+and ASCII to stdout or placed in a file. Note that the data returned includes
+the logical block data (typically 512 bytes for a disk) plus ECC
+information (whose format is proprietary) plus optionally other proprietary
+data. Note that the logical block data may be encoded or encrypted.
+.PP
+In SBC\-4 revision 7 the SCSI READ LONG (10 and 16 byte) commands were made
+obsolete. In the same revision all uses of SCSI WRITE LONG (10 and 16 byte)
+commands were made obsolete apart from the case in which the WR_UNCOR bit is
+set.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-S\fR, \fB\-\-16\fR
+uses a SCSI READ LONG(16) command. The default action is to use a SCSI
+READ LONG(10) command. The READ LONG(10) command has a 32 bit field for
+the lba while READ LONG(16) has a 64 bit field.
+.TP
+\fB\-c\fR, \fB\-\-correct\fR
+sets the 'CORRCT' bit in the SCSI READ LONG command. When set the data is
+corrected by the ECC before being transferred back to this utility. The
+default is to leave the 'CORRCT' bit clear in which case the data is
+not corrected.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR is the logical block address of the sector to read. Assumed
+to be in decimal unless prefixed with '0x' (or has a trailing 'h'). Defaults
+to lba 0. If the lba is larger than can fit in 32 bits then the \fI\-\-16\fR
+option should be used.
+.TP
+\fB\-o\fR, \fB\-\-out\fR=\fIOF\fR
+instead of outputting ASCII hex to stdout, send it in binary to the
+file called \fIOF\fR. If '\-' is given for \fIOF\fR then the (binary)
+output is sent to stdout. Note that all informative and error output is
+sent to stderr.
+.TP
+\fB\-p\fR, \fB\-\-pblock\fR
+sets the 'PBLOCK' bit in the SCSI READ LONG command. When set the
+physical block (plus ECC data) containing the requested logical block
+address is read. The default is to leave the 'PBLOCK' bit clear in
+which case the logical block (plus any ECC data) is read.
+.TP
+\fB\-r\fR, \fB\-\-readonly\fR
+opens the DEVICE read\-only rather than read\-write which is the
+default. The Linux sg driver needs read\-write access for the SCSI
+READ LONG command but other access methods may require read\-only
+access.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-x\fR, \fB\-\-xfer_len\fR=\fIBTL\fR
+where \fIBTL\fR is the byte transfer length (default to 520). If the
+given value (or the default) does not match the "long" block size of the
+device, the appropriate \fIBTL\fR is deduced from the error response and
+printed (to stderr). The idea is that the user will retry this utility
+with the correct transfer length.
+.SH NOTES
+If a defective block is found and its contents, if any, has been
+retrieved then "sg_reassign" could be used to map out the defective
+block. Associated with such an action the number of elements in
+the "grown" defect list could be monitored (with "sg_reassign \-\-grown")
+as the disk could be nearing the end of its useful lifetime.
+.PP
+Various numeric arguments (e.g. \fILBA\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+As a data point, Fujitsu uses a 54 byte ECC (per block) which is capable
+of correcting up to a single burst error or 216 bits "on the
+fly". [Information obtained from MAV20xxrc product manual.]
+.SH EXIT STATUS
+The exit status of sg_read_long is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2016 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_reassign, sg_write_long, sg_dd
diff --git a/doc/sg_readcap.8 b/doc/sg_readcap.8
new file mode 100644
index 0000000..d936ef9
--- /dev/null
+++ b/doc/sg_readcap.8
@@ -0,0 +1,196 @@
+.TH SG_READCAP "8" "January 2020" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_readcap \- send SCSI READ CAPACITY command
+.SH SYNOPSIS
+.B sg_readcap
+[\fI\-\-16\fR] [\fI\-\-brief\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-lba=LBA\fR] [\fI\-\-long\fR] [\fI\-\-pmi\fR] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR] [\fI\-\-zbc\fR]
+\fIDEVICE\fR
+.PP
+.B sg_readcap
+[\fI\-16\fR] [\fI\-b\fR] [\fI\-h\fR] [\fI\-H\fR] [\fI\-lba=LBA\fR]
+[\fI\-pmi\fR] [\fI\-r\fR] [\fI\-R\fR] [\fI\-v\fR] [\fI\-V\fR] [\fI\-z\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+The normal action of the SCSI READ CAPACITY command is to fetch the number
+of blocks (and block size) from the \fIDEVICE\fR.
+.PP
+The SCSI READ CAPACITY command (both 10 and 16 byte cdbs) actually yield
+the block address of the last block and the block size. The number of
+blocks is thus one plus the block address of the last block (as blocks
+are counted origin zero (i.e. starting at block zero)). This is the source
+of many "off by one" errors.
+.PP
+The READ CAPACITY(16) response provides additional information not found in
+the READ CAPACITY(10) response. This includes protection and logical block
+provisioning information, plus the number of logical blocks per physical
+block. So even though the media size may not exceed what READ CAPACITY(10)
+can show, it may still be useful to examine the response to READ
+CAPACITY(16). Sadly there are horrible SCSI command set implementations in
+the wild that crash when the READ CAPACITY(16) command is sent to them.
+.PP
+Device capacity is the product of the number of blocks by the block size.
+This utility outputs this figure in bytes, MiB (1048576 bytes per MiB),
+GB (1000000000 bytes per GB) and, if large enough, TB (1000 GB).
+.PP
+If sg_readcap is called without the \fI\-\-long\fR option then the 10 byte
+cdb version (i.e. READ CAPACITY (10)) is sent to the \fIDEVICE\fR. If the
+number of blocks in the response is reported as
+0xffffffff (i.e. (2**32 \- 1) ) and the \fI\-\-hex\fR option has not been
+given, then READ CAPACITY (16) is called and its response is output.
+.PP
+This utility supports two command line syntaxes, the preferred one is
+shown first in the synopsis and explained in this section. A later section
+on the old command line syntax outlines the second group of options.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-\-16\fR
+Use the 16 byte cdb variant of the READ CAPACITY command. See the '\-\-long'
+option.
+\fB\-b\fR, \fB\-\-brief\fR
+outputs two hex numbers (prefixed with '0x' and space separated)
+to stdout. The first number is the maximum number of blocks on the
+device (which is one plus the lba of the last accessible block). The
+second number is the size in bytes of each block. If the operation fails
+then "0x0 0x0" is written to stdout.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response to the READ CAPACITY command (either the 10 or 16
+byte cdb variant) in ASCII hexadecimal on stdout.
+.TP
+\fB\-L\fR, \fB\-\-lba\fR=\fILBA\fR
+used in conjunction with \fI\-\-pmi\fR option. This variant of READ CAPACITY
+will yield the last block address after \fILBA\fR prior to a delay. For a
+disk, given a \fILBA\fR it yields the highest numbered block on the same
+cylinder (i.e. before the heads need to move). \fILBA\fR is assumed to be
+decimal unless prefixed by "0x" or it has a trailing "h". Defaults to 0.
+This option was made obsolete in SBC\-3 revision 26.
+.TP
+\fB\-l\fR, \fB\-\-long\fR
+Use the 16 byte cdb variant of the READ CAPACITY command. The default
+action is to use the 10 byte cdb variant which limits the maximum
+block address to (2**32 \- 2). When a 10 byte cdb READ CAPACITY command
+is used on a device whose size is too large then a last block address
+of 0xffffffff is returned (if the device complies with SBC\-2 or later).
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option.
+.TP
+\fB\-p\fR, \fB\-\-pmi\fR
+partial medium indicator: for finding the next block address prior to
+some delay (e.g. head movement). In the absence of this option, the
+total number of blocks and the block size of the device are output.
+Used in conjunction with the \fI\-\-lba=LBA\fR option. This option was
+made obsolete in SBC\-3 revision 26.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary to stdout.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default for READ CAPACITY(16) is to open it read\-write. The default
+for READ CAPACITY(10) is to open it read\-only so this option does not
+change anything for this case.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+outputs version string then exits.
+.TP
+\fB\-z\fR, \fB\-\-zbc\fR
+additionally prints out the extra ZBC field (RC_BASIS) in the READ CAPACITY
+response. Using the option implicitly sets the \fI\-\-16\fR option.
+.SH NOTES
+The response to READ CAPACITY(16) contains a LBPRZ bit in the SBC\-3
+standard (ANSI INCITS 514\-2014). There was also a LBPRZ bit with the same
+meaning in the Logical block provisioning VPD page (0xb2). Then somewhat
+confusingly T10 expanded the LBPRZ bit to a 3 bit field in SBC\-4 draft
+revision 7, but only in the LB provisioning VPD page. The reason for the
+expansion was to report a new "provisioning initialization pattern"
+state (when an unmapped logical block is read). The new state has been
+assigned LBPRZ=2 in the VPD page and it re\-uses LBPRZ=0 in the READ
+CAPACITY(16) response. LBPRZ=1 retains the same meaning for both variants,
+namely that a block of zeroes will be returned when an unmapped logical block
+is read.
+.SH EXIT STATUS
+The exit status of sg_readcap is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.TP
+\fB\-16\fR
+Use the 16 byte cdb variant of the READ CAPACITY command.
+Equivalent to \fI\-\-long\fR in the main description.
+.TP
+\fB\-b\fR
+utility outputs two hex numbers (prefixed with '0x' and space separated) to
+stdout. The first number is the maximum number of blocks on the device (which
+is one plus the lba of the last accessible block). The second number is the
+size of each block. If the operation fails then "0x0 0x0" is written to
+stdout.  Equivalent to \fI\-\-brief\fR in the main description.
+.TP
+\fB\-h\fR
+output the usage message then exit. Giving the \fI\-?\fR option also outputs
+the usage message then exits.
+.TP
+\fB\-H\fR
+output the response to the READ CAPACITY command (either the 10 or 16
+byte cdb variant) in ASCII hexadecimal on stdout.
+.TP
+\fB\-lba\fR=\fILBA\fR
+used in conjunction with \fI\-pmi\fR option. This variant of READ CAPACITY
+will yield the last block address after \fILBA\fR prior to a delay.
+Equivalent to \fI\-\-lba=LBA\fR in the main description.
+.TP
+\fB-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-pmi\fR
+partial medium indicator: for finding the next block address prior to
+some delay (e.g. head movement). In the absence of this switch, the
+total number of blocks and the block size of the device are output.
+Equivalent to \fI\-\-pmi\fR in the main description.
+.TP
+\fB\-r\fR
+output response in binary (to stdout).
+.TP
+\fB\-R\fR
+Equivalent to \fI\-\-readonly\fR in the main description.
+.TP
+\fB\-v\fR
+verbose: print out cdb of issued commands prior to execution. '\-vv'
+and '\-vvv' are also accepted yielding greater verbosity.
+.TP
+\fB\-V\fR
+outputs version string then exits.
+.TP
+\fB\-z\fR
+Equivalent to \fI\-\-zbc\fR in the main description.
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHORS
+Written by Douglas Gilbert
+.SH COPYRIGHT
+Copyright \(co 1999\-2020 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq(sg3_utils)
diff --git a/doc/sg_reassign.8 b/doc/sg_reassign.8
new file mode 100644
index 0000000..bbbea8c
--- /dev/null
+++ b/doc/sg_reassign.8
@@ -0,0 +1,151 @@
+.TH SG_REASSIGN "8" "October 2019" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_reassign \- send SCSI REASSIGN BLOCKS command
+.SH SYNOPSIS
+.B sg_reassign
+[\fI\-\-address=A,A...\fR] [\fI\-\-dummy\fR] [\fI\-\-eight=0|1\fR]
+[\fI\-\-grown\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-longlist=0|1\fR]
+[\fI\-\-primary\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send a SCSI REASSIGN BLOCKS command to \fIDEVICE\fR. Alternatively
+this utility can find the number of element in a "grown" or "primary"
+defect list with a SCSI READ DEFECT DATA (10) command. These SCSI commands
+are defined in SBC\-2 for direct access devices (e.g. a disk). Reassign
+blocks is designed to change the physical location of a logical block
+that is known or suspected to be defective to another area on the
+media. Disks are typically formatted with blocks held in reserve
+for this situation.
+.PP
+If neither the \fI\-\-grown\fR nor \fI\-\-primary\fR option is supplied
+then one or more addresses need to be given. If the address (or all of
+the addresses) fit into 4 bytes and '\-\-eight=1' is not given then the
+parameter block passed to \fIDEVICE\fR is made up of 4 byte logical block
+addresses. If any of the addresses need more than 4 bytes to
+represent (i.e. >= 2**32) or '\-\-eight=1' is given then the parameter block
+passed to \fIDEVICE\fR is made up of 8 byte logical block addresses.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-a\fR, \fB\-\-address\fR=\fIA,A...\fR
+where \fIA,A...\fR is a string of comma separated numbers. Each number
+is interpreted as decimal unless prefixed by '0x' or '0X' (or it has a
+trailing 'h' or 'H'). If multiple logical block addresses are given they
+must be separated by a comma or a (single) space. A string that contains
+any space separators needs to be quoted. At least one address must be given.
+.TP
+\fB\-a\fR, \fB\-\-address\fR=\-
+reads one or more logical block addresses from stdin. These may be comma,
+space, tab or linefeed (newline) separated. If a line contains "#" then
+the remaining characters on that line are ignored. Otherwise each non
+separator sequence of characters should resolve to a decimal number
+unless prefixed by '0x' or '0X' (or has a trailing 'h'). At least one
+address must be given. Lines should not be longer than 1023 bytes.
+.TP
+\fB\-d\fR, \fB\-\-dummy\fR
+prepare for but do not execute the SCSI REASSIGN BLOCKS command. Since
+the REASSIGN BLOCKS command is essentially irreversible, paranoid
+users may wish to check the invocation of this utility before reassigning
+defective blocks on a disk. Useful with '\-vv' for those who wish to
+view the parameter block that will accompany the command.
+.TP
+\fB\-e\fR, \fB\-\-eight\fR=0 | 1
+when value is 1 then it sets the 'LONGLBA' flag in the command indicating
+that the addresses in the associated parameter block are 8 byte quantities.
+When value is 0 then it clears the 'LONGLBA' flag in the command indicating
+that the addresses in the associated parameter block are 4 byte quantities.
+If this option is not given then 4 byte quantities are assumed unless one
+of the address is too large.
+.TP
+\fB\-g\fR, \fB\-\-grown\fR
+use the SCSI READ DEFECT DATA (10) command to determine the number of
+elements in the "grown defect list". When this option is given there
+is no reassignment of blocks (i.e. this utility is passive). When this
+option is given then the \fI\-\-address=\fR option is not permitted. See
+the discussion below concerning the relationship between reassigned blocks
+and the grown defect list. This list is sometimes referred to as the GLIST.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+print response in hex (for \fB\-g\fR, \fB\-\-grown\fR, \fB\-p\fR
+or \fB\-\-primary\fR).
+.TP
+\fB\-l\fR, \fB\-\-longlist\fR=0 | 1
+sets the REASSIGN BLOCKS cdb field of the same name to the given value.
+Only 1000 addresses are permitted so there should be no need to specify
+a value of 1. The short list variant restricts the parameter block
+length to 2 ** 16 bytes (i.e. about 16000 4 byte addresses or 8000
+8 byte addresses). Added for completeness.
+.TP
+\fB\-p\fR, \fB\-\-primary\fR
+use the SCSI READ DEFECT DATA (10) command to determine the number of
+elements in the "primary defect list" which is established during the
+manufacturing process. When this option is given there is no reassignment
+of blocks (i.e. this utility is passive). When this option is given then
+the \fI\-\-address=\fR option is not permitted. This list is sometimes
+referred to as the PLIST.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+Note that if the ARRE field (for reads) and/or the AWRE field (for writes)
+are set in the "Read Write Error Recovery" mode page then recoverable read
+and/or write errors cause automatic reassignment of the defective block. The
+PER bit in the same mode page controls whether a RECOVERED ERROR sense key
+is reported on not (PER=1 implies do report). Irrespective of the ARRE, AWRE
+or PER field settings, the error counter log pages reflect any
+errors (recovered or otherwise). Whenever a block is reassigned, a new entry
+is added in the "grown" defect list. Apart from doing selftests (see
+sg_senddiag or smartmontools) regularly, monitoring the grown defect list of
+a disk is a reasonable metric of its health. If the grown list starts growing
+quickly that is an ominous sign. The best grown defect lists are empty
+ones. The number of elements in the grown defect list can be viewed with
+the \fI\-\-grown\fR option. The contents of the grown defect list can be
+viewed with the 'sginfo \-G' utility.
+.PP
+If an unrecoverable error is detected at a logical block address then
+REASSIGN BLOCKS is needed to reassign the block. Also if the ARRE and/or
+AWRE fields are clear and a recoverable error is detected then the
+logical block in question may be reassigned with this utility (otherwise
+the error counter log pages will continually be incremented for each
+recovered access).
+.PP
+The number of blocks held in reserve for the purposes of REASSIGN
+BLOCKS is vendor specific and may well be limited to the zone within
+the media where the original (defective) block lay. When this number
+is exhausted subsequent invocations of this utility may result in
+a sense key of hardware error and an additional sense of 'No defect
+spare location available'. The next step would be to reformat the
+disk (or get a replacement).
+.PP
+The SBC\-2 draft standard (revision 16) notes that when multiple addresses
+are given to the SCSI REASSIGN BLOCKS command and there is some failure
+at one of the later addresses then all addresses prior to that have already
+be reassigned. Care should be taken in such a case. Re\-executing the command
+with the same addresses will cause the earlier addresses to be reassigned
+again. At some stage the disk will run out of reserved locations.
+So unless a large number of addresses are involved it may be safer to
+reassign them one address at a time.
+.SH EXIT STATUS
+The exit status of sg_reassign is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2005\-2019 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_format,sginfo,sg_senddiag(all in sg3_utils), sdparm(sdparm),
+.B smartmontools(internet, sourceforge)
diff --git a/doc/sg_referrals.8 b/doc/sg_referrals.8
new file mode 100644
index 0000000..6011106
--- /dev/null
+++ b/doc/sg_referrals.8
@@ -0,0 +1,71 @@
+.TH SG_REFERRALS "8" "May 2014" "sg3_utils\-1.39" SG3_UTILS
+.SH NAME
+sg_referrals \- send SCSI REPORT REFERRALS command
+.SH SYNOPSIS
+.B sg_referrals
+[\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-lba=LBA\fR] [\fI\-\-maxlen=LEN\fR]
+[\fI\-\-one-segment\fR] [\fI\-\-raw\fR] [\fI\-\-readonly\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send the SCSI REPORT REFERRALS command to the \fIDEVICE\fR and outputs the
+response. This command was introduced in (draft) SBC\-3 revision 24 and
+devices that support referrals should support this command.
+.PP
+The default action is to decode the response for all user data segment
+referral descriptors. The amount of output can be reduced by the
+\fI\-\-lba\fR and \fI\-\-one-segment\fR options.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output response to this command in ASCII hex.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR is the Logical Block Address (LBA) in the first user
+data segment the \fIDEVICE\fR should report the referrals parameter
+data for.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given then 256 is used. 256 is
+enough space for the response header and user data segment descriptors.
+.TP
+\fB\-s\fR, \fB\-\-one-segment\fR
+report the user data segment of the segment specified by the \fILBA\fR
+parameter only.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary (to stdout).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output). Additional output
+caused by this option is sent to stderr.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+For a discussion of referrals see section 4.25 of sbc3r25.pdf
+at https://www.t10.org (or the corresponding section of a later draft).
+.SH EXIT STATUS
+The exit status of sg_referrals is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert and Hannes Reinecke.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2009\-2014 Douglas Gilbert and Hannes Reinecke
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_vpd(8)
diff --git a/doc/sg_rem_rest_elem.8 b/doc/sg_rem_rest_elem.8
new file mode 100644
index 0000000..13ef9c1
--- /dev/null
+++ b/doc/sg_rem_rest_elem.8
@@ -0,0 +1,95 @@
+.TH SG_REM_REST_ELEM "8" "June 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_rem_rest_elem \- send SCSI remove or restore element command
+.SH SYNOPSIS
+.B sg_rem_rest_elem
+[\fI\-\-capacity=RC\fR] [\fI\-\-element=EID\fR] [\fI\-\-help\fR]
+[\fI\-\-quick\fR] [\fI\-\-remove\fR] [\fI\-\-restore\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI REMOVE ELEMENT AND TRUNCATE [RMEAT] or RESTORE ELEMENTS AND
+REBUILD [RSEAR] command to the \fIDEVICE\fR. Since both these commands have
+a potentially huge impact on the \fIDEVICE\fR (similar to the FORMAT UNIT
+command: destroying data and taking a long time to complete fully),
+they first give the user the chance to reconsider (3 times within 15
+seconds) before taking action.
+.PP
+Unlike the FORMAT UNIT command, these commands seem designed to work in
+the background. So they will return quickly (although sbc5r01.pdf does not
+state that) and the disk will be placed in a reduced functionality state
+where only a specified number of commands will be executed (e.g. INQUIRY and
+REPORT LUNS) until the operation is complete. Other commands will receive
+sense data with a sense key of NOT READY and an additional sense code
+of 'Depopulation in progress' (for RMEAT) or 'Depopulation restoration in
+progress' (for RSEAR).
+.PP
+The REMOVE ELEMENT AND TRUNCATE has a close relative in ZBC\-2 called the
+REMOVE ELEMENT AND MODIFY ZONES [RMEMZ] command. See the sg_zone utility
+for an implementation of the latter command.
+.br
+The difference between RMEAT and RMEMZ is that the former "changes the
+association between LBAs and physical blocks" and the latter does not
+change that association. Zones affected by the RMEMZ command are placed
+into the zone condition: "Offline".
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-c\fR, \fB\-\-capacity\fR=\fIRC\fR
+RC stands for Requested Capacity and is the number of logical blocks the
+\fIDEVICE\fR should have after the element is removed with the RMEAT
+command. The default value is 0 which allows the \fIDEVICE\fR to decide
+what the reduced capacity will be after the element removal. The RSEAR
+command ignores this value.
+.TP
+\fB\-e\fR, \fB\-\-element\fR=\fIEID\fR
+where \fIEID\fR is an element identifier which is a 32 bit unsigned integer
+starting at one. This field is used by the RMEAT command and ignored
+otherwise. The default value is zero (which is invalid). So the user needs
+to supply a valid element identifier when \fI\-\-remove\fR is used.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-q\fR, \fB\-\-quick\fR
+the default action (i.e. when this option is not given) is to give the user
+15 seconds to reconsider doing a remove or restore element operation on the
+\fIDEVICE\fR.  When this option is given that step (i.e. the 15 second
+warning period) is bypassed.
+.TP
+\fB\-r\fR, \fB\-\-remove\fR
+causes the REMOVE ELEMENT AND TRUNCATE command to be sent to the
+\fIDEVICE\fR. In practice, \fI\-\-element=EID\fR needs to be also given.
+.TP
+\fB\-R\fR, \fB\-\-restore\fR
+causes the RESTORE ELEMENTS AND REBUILD command to be sent to the
+\fIDEVICE\fR.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+Once an element is removed successfully it is termed as "depopulated".
+Depopulated elements that have the 'Restoration Allowed' (RALWD) bit
+set (see sg_get_elem_status) are candidates for future restoration.
+.PP
+A (storage) element of a rotating hard disk is one side of a platter
+typically associated with one head. Such hard disks typically have multiple
+platters with two heads per platter (i.e. one head each side of the platter).
+.SH EXIT STATUS
+The exit status of sg_rem_rest_elem is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_get_elem_status,sg_zone(sg3_utils)
diff --git a/doc/sg_rep_density.8 b/doc/sg_rep_density.8
new file mode 100644
index 0000000..f0633c4
--- /dev/null
+++ b/doc/sg_rep_density.8
@@ -0,0 +1,97 @@
+.TH SG_REP_DENSITY "8" "January 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_rep_density \- send SCSI REPORT DENSITY SUPPORT command
+.SH SYNOPSIS
+.B sg_rep_density
+[\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-inhex=FN\fR] [\fI\-\-maxlen=LEN\fR]
+[\fI\-\-media\fR] [\fI\-\-raw\fR] [\fI\-\-readonly\fR] [\fI\-\-typem\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI REPORT DENSITY command to \fIDEVICE\fR and outputs the data
+returned. This command is tape drive specific. This command is found in
+the SSC\-5 draft standard, revision 6 (ssc5r06.pdf). This command was
+present in the SSC\-2 standard (ANSI INCITS 380\-2003).
+.PP
+By default this utility requests the density code descriptors supported by
+the \fIDEVICE\fR (e.g. a tape drive) and decodes the response. If the
+\fI\-\-typem\fR option is given it fetches the medium type descriptors
+supported by the \fIDEVICE\fR and decodes the response. When the
+\fI\-\-media\fR option is given the density code or medium type descriptors
+supported by the media inside the \fIDEVICE\fR (e.g. a tape cartridge) are
+fetched.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response in hexadecimal to stdout. When used once the whole
+response is output in ASCII hexadecimal, prefixed by an address (starting at
+0) on each line. When used twice the whole response is output in hexadecimal
+with no leading address (on each line).
+.br
+Using this option three times will produce output that can be redirected to
+a file and later given to another invocation using the \fI\-\-inhex=FN\fR
+option.
+.TP
+\fB\-i\fR, \fB\-\-inhex\fR=\fIFN\fR
+where \fIFN\fR is a file name whose contents are assumed to be ASCII
+hexadecimal. If \fIDEVICE\fR is also given then \fIDEVICE\fR is ignored,
+a warning is issued and the utility continues, decoding the file named
+\fIFN\fR. See the "FORMAT OF FILES CONTAINING ASCII HEX" section in the
+sg3_utils manpage for more information. If the \fI\-\-raw\fR option is
+also given then the contents of \fIFN\fR are treated as binary.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given (or \fILEN\fR is zero)
+then 8192 is used. The maximum allowed value of \fILEN\fR is 65535.
+.TP
+\fB\-M\fR, \fB\-\-media\fR
+sets the MEDIA bit in the cdb which causes the density codes (or medium
+types) supported by the tape cartridge in the drive to be placed in the
+response. The default is to request the density codes (or medium types)
+supported by the tape drive itself.
+.br
+If there is no "medium" (e.g. tape cartridge) present in the drive the SCSI
+command will fail with a "not ready" sense key.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the SCSI response (i.e. the data\-out buffer) in binary (to stdout)
+unless the \fI\-\-inhex=FN\fR option is given.
+.br
+When used together with the \fI\-\-inhex=FN\fR option then the contents of
+\fIFN\fR are treated as binary (rather than hexadecimal).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-t\fR, \fB\-\-typem\fR
+sets the MEDIUM TYPE bit in the cdb which causes the medium types supported
+by the tape drive (or tape cartridge) to be placed in the response. The
+default is to request the density codes.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH EXIT STATUS
+The exit status of sg_rep_density is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg3_utils(sg3_utils)
diff --git a/doc/sg_rep_pip.8 b/doc/sg_rep_pip.8
new file mode 100644
index 0000000..c614fd3
--- /dev/null
+++ b/doc/sg_rep_pip.8
@@ -0,0 +1,58 @@
+.TH SG_REP_PIP "8" "January 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_rep_pip \- send SCSI REPORT PROVISIONING INITIALIZATION PATTERN command
+.SH SYNOPSIS
+.B sg_rep_pip
+[\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI REPORT PROVISIONING INITIALIZATION PATTERN command to
+\fIDEVICE\fR and outputs the data returned. This command is found in the
+SBC\-4 draft standard, revision 21 (sbc4r21.pdf).
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response in hexadecimal to stdout. When used once the whole
+response is output in ASCII hexadecimal, prefixed by an address (starting at
+0) on each line. When used twice the whole response is output in hexadecimal
+with no leading address (on each line). The default action is the same as
+giving the \fI\-\-hex\fR option once.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given (or \fILEN\fR is zero)
+then 8192 is used. The maximum allowed value of \fILEN\fR is 1048576.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the SCSI response (i.e. the data\-out buffer) in binary (to stdout).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH EXIT STATUS
+The exit status of sg_rep_pip is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2020\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg3_utils(sg3_utils)
diff --git a/doc/sg_rep_zones.8 b/doc/sg_rep_zones.8
new file mode 100644
index 0000000..49ee070
--- /dev/null
+++ b/doc/sg_rep_zones.8
@@ -0,0 +1,214 @@
+.TH SG_REP_ZONES "8" "AUGUST 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_rep_zones \- send SCSI REPORT ZONES, REALMS or ZONE DOMAINS command
+.SH SYNOPSIS
+.B sg_rep_zones
+[\fI\-\-brief\fR] [\fI\-\-domain\fR] [\fI\-\-find=ZT\fR] [\fI\-\-force\fR]
+[\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-inhex=FN\fR] [\fI\-\-json[=JO\fR]]
+[\fI\-\-locator=LBA\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-num=NUM\fR]
+[\fI\-\-partial\fR] [\fI\-\-raw\fR] [\fI\-\-readonly\fR] [\fI\-\-realm\fR]
+[\fI\-\-report=OPT\fR] [\fI\-\-start=LBA\fR] [\fI\-\-statistics\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] [\fI\-\-wp\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI REPORT ZONES, REPORT REALMS or REPORT ZONE DOMAINS command to
+\fIDEVICE\fR and decodes (or simply outputs) the data returned. These
+commands are found in the ZBC\-2 draft standard, revision 12 (zbc2r12.pdf).
+Only the REPORT ZONES command is defined in the original ZBC
+standard (INCITS 536\-2017) and it is the default for this utility.
+.PP
+The REPORT ZONE DOMAINS command will be sent (and decoded) when the
+\fI\-\-domain\fR option is given. The REPORT REALMS command will be
+sent (and decoded) when the \fI\-\-realm\fR option is given.
+.PP
+Rather than send a SCSI command to \fIDEVICE\fR, if the \fI\-\-inhex=FN\fR
+option is given, then the contents of the file named \fIFN\fR are decoded
+as ASCII hex (or binary if \fI\-\-raw\fR is also given) and then processed
+as if it was the response of the command. By default the REPORT ZONES
+command response is assumed; if the \fI\-\-domain\fR or \fI\-\-realm\fR
+option is given then the corresponding command response is assumed.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-brief\fR
+even though a ZBC disk will typically limit the size of the response to the
+REPORT ZONES command (e.g. due to the "allocation length" field), this may
+still be potentially a lot of output. This option will only decode and
+output fields found in the response header plus fields from the last
+descriptor in the current response.
+.TP
+\fB\-d\fR, \fB\-\-domain\fR
+send or decode the SCSI REPORT ZONE DOMAINS command.
+.TP
+\fB\-F\fR, \fB\-\-find\fR=\fIZT\fR
+where \fIZT\fR is a zone type number or an abbreviation for a zone
+type. If \fIZT\fR is prefixed by either '\-' or '!' then the check for
+equality is inverted to be a check for inequality. IOWs it does a: find
+the first occurrence that is
+.B not
+the given zone type.
+.br
+The algorithm used by this option takes into account the \fI\-\-hex\fR,
+\fI\-\-maxlen=LEN\fR, \fI\-\-num=NUM\fR, \fI\-\-report=OPT\fR and
+\fI\-\-start=LBA\fR options, if given, and ignores other options. It is only
+implemented for the Report zones command currently. The algorithm may call
+the Report zones command repeatedly, with the PARTIAL bit set and the Zone
+start LBA field being increased as it goes. This continues until either
+there is a match on the \fIZT\fR condition, \fI\-\-num=NUM\fR is exhausted
+or the number of zones is exhausted.
+.br
+The \fIZT\fR numbers and abbreviations are listed when the \fI\-\-help\fR
+option is given twice. Warning: using '!' for inverting the condition may
+not be so practical as the shell (e.g. bash) may interpret '!' as having
+special meaning. Placing single quotes around \fIZT\fR fixes the problem
+for the bash shell (e.g. \-\-find='!c' meaning find the first zone whose
+type is not conventional).
+.TP
+\fB\-f\fR, \fB\-\-force\fR
+when decoding the response to this command, certain sanity checks are
+done and if they fail a message is sent to stderr and a non\-zero
+exit status is set. If this option is given those sanity checks are
+bypassed.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit. When given twice, additional usage
+information is output.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response in hexadecimal to stdout. When used once the whole
+response is output in ASCII hexadecimal with a leading address (starting at
+0) on each line. When used twice each zone descriptor in the response is
+output separately in hexadecimal. When used thrice the whole response is
+output in hexadecimal with no leading address (on each line).
+.br
+When this option is used twice, it can be useful with either the
+\fI\-\-brief\fR or \fI\-\-find=ZT\fR option to only output the header
+and zone descriptor in hex that those two options would otherwise print
+in ASCII in the absence of the \fI\-\-hex\fR option.
+.br
+The output format when this option is given thrice is suitable for a later
+invocation with the \fI\-\-inhex=FN\fR option.
+.TP
+\fB\-i\fR, \fB\-\-inhex\fR=\fIFN\fR
+where \fIFN\fR is a file name whose contents are assumed to be ASCII
+hexadecimal. If \fIDEVICE\fR is also given then \fIDEVICE\fR is ignored,
+a warning is issued and the utility continues, decoding the file named
+\fIFN\fR. See the "FORMAT OF FILES CONTAINING ASCII HEX" section in the
+sg3_utils manpage for more information. If the \fI\-\-raw\fR option is
+also given then the contents of \fIFN\fR are treated as binary.
+.br
+Note that by default this utility assumes then contents are the response
+from a REPORT ZONES command. Use the \fI\-\-domain\fR or \fI\-\-realm\fR
+option for decoding the other two commands.
+.TP
+\fB\-j\fR, \fB\-\-json[\fR=\fIJO\fR]
+output is in JSON format instead of human readable form. See sg3_utils_json
+manpage or use '?' for \fIJO\fR for a summary.
+.TP
+\fB\-l\fR, \fB\-\-locator\fR=\fILBA\fR
+where \fILBA\fR plays a similar role as it does in \fI\-\-start=LBA\fR.
+It is the field name used in the REPORT REALMS and REPORT ZONE DOMAINS
+commands.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given (or \fILEN\fR is zero)
+then 16384 is used. The maximum allowed value of \fILEN\fR is 2097152.
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fINUM\fR
+where \fINUM\fR is the (maximum) number of zone descriptors to print out.
+The default value is zero which is taken to mean print out all zone
+descriptors returned by the REPORT ZONES command.
+.TP
+\fB\-p\fR, \fB\-\-partial\fR
+set the PARTIAL bit in the cdb. Without the PARTIAL bit set a ZBC disk
+will attempt to form a response with all zones from \fILBA\fR to the end
+of the disk. If there are a large number of zones (e.g. > 10,000) this
+large response will be truncated so that it doesn't exceed the "allocation
+length" field in the cdb (see \fI\-\-maxlen=LEN\fR). The advantage of doing
+this is that the number of (remaining) zones on the disk can be calculated.
+The disadvantage is the amount of time that may take.
+.br
+With the PARTIAL bit set in the cdb, only the number of zones implied by
+the "allocation length" field are fetched. This may be considerably faster
+than the same command without the PARTIAL bit set.
+.br
+When iterating through the zones on a ZBC disk, the process will be faster
+when the PARTIAL bit is set. Typically \fI\-\-start=LBA\fR is set to zero
+or the [LBA + zone_length] of the last zone reported in the previous
+iteration.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary (to stdout) unless the \fI\-\-inhex=FN\fR option
+is also given. In that case the input file name (\fIFN\fR) is decoded as
+binary (and the output is _not_ in binary (but may be hex)).
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-e\fR, \fB\-\-realm\fR
+send or decode the SCSI REPORT REALMS command.
+.TP
+\fB\-o\fR, \fB\-\-report\fR=\fIOPT\fR
+where \fIOPT\fR will become the contents of the REPORTING OPTION field
+in the cdb. The reporting options differ between REPORT ZONES, REPORT ZONE
+DOMAINS and REPORT REALMS. If the \fI\-\-help\fR option is given twice (
+or the equivalent '\-hh') a list of available reporting options (as of
+writing) for each command is output.
+.br
+The default value for REPORT ZONES is 0 which means report a list of all
+zones. Some other values are 1 for list zones with a zone condition of empty;
+2 for list zones with a zone condition of implicitly opened; 3 for list zones
+with a zone condition of explicitly opened; 4 for list zones with a zone
+condition of closed; 5 for list zones with a zone condition of full; 6 for
+list zones with a zone condition of read only; 7 for list zones with a zone
+condition of offline. Other values are 0x10 for list zones with 'RWP
+recommended' set to true; 0x11 for list zones with non\-sequential write
+resource active set to true, 0x3e for list zones apart from GAP zones, and
+0x3f for list zones with a zone condition of 'not write pointer'.
+.TP
+\fB\-s\fR, \fB\-\-start\fR=\fILBA\fR
+where \fILBA\fR is at the start or within the first zone to be reported. The
+default value is 0. If \fILBA\fR is not a zone start LBA then the preceding
+zone start LBA is used for reporting. Assumed to be in decimal unless
+prefixed with '0x' or has a trailing 'h' which indicate hexadecimal.
+.br
+The zone start LBA field used in the REPORT ZONES command was changed to
+the zone domain/realm locator field for the two newer ZBC\-2 commands. For
+this utility \fI\-\-locator=LBA\fR and \fI\-\-start=LBA\fR are
+interchangeable.
+.TP
+\fB\-S\fR, \fB\-\-statistics\fR
+reviews all or a limited number of report zones, collects statistics and
+prints them (on stdout). The number of zones reviewed may be limited by
+any combination of \fI\-\-num=NUM\fR, \fI\-\-report=OPT\fR and
+\fI\-\-start=LBA\fR options. The long option name may be abbreviated to
+\fI\-\-stats\fR.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-w\fR, \fB\-\-wp\fR
+print the write pointer (in hex) only. In the absence of errors, then a hex
+LBA will be printed on each line, one line for each zone. Can be usefully
+combined with the \fI\-\-num=NUM\fR and \fI\-\-start=LBA\fR options.
+.SH EXIT STATUS
+The exit status of sg_rep_zones is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2014\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_reset_wp,sg_zone,sg3_utils_json(sg3_utils),
+.B zbd(libzbd), blkzone(util-linux)
diff --git a/doc/sg_requests.8 b/doc/sg_requests.8
new file mode 100644
index 0000000..e1c1605
--- /dev/null
+++ b/doc/sg_requests.8
@@ -0,0 +1,138 @@
+.TH SG_REQUESTS "8" "October 2021" "sg3_utils\-1.47" SG3_UTILS
+.SH NAME
+sg_requests \- send one or more SCSI REQUEST SENSE commands
+.SH SYNOPSIS
+.B sg_requests
+[\fI\-\-desc\fR] [\fI\-\-error\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-maxlen=LEN\fR] [\fI\-\-num=NUM\fR] [\fI\-\-number=NUM\fR]
+[\fI\-\-progress\fR] [\fI\-\-raw\fR] [\fI\-\-status\fR] [\fI\-\-time\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send SCSI REQUEST SENSE command to \fIDEVICE\fR and output the parameter
+data response which is expected to be in sense data format. Both fixed
+and descriptor sense data formats are supported.
+.PP
+Multiple REQUEST SENSE commands can be sent with the \fI\-\-num=NUM\fR
+option. This can be used for timing purposes or monitoring the progress
+indication.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-d\fR, \fB\-\-desc\fR
+sets the DESC bit in the REQUEST SENSE SCSI cdb. The \fIDEVICE\fR
+should return sense data in descriptor (rather than fixed) format. This
+will only occur if the \fIDEVICE\fR recognizes descriptor format (SPC\-3
+and later). If the device is pre SPC\-3 then setting a bit in a reserved
+field may cause a check condition status with an illegal request sense key,
+but will most likely be ignored.
+.TP
+\fB\-e\fR, \fB\-\-error\fR
+when used once it changes the REQUEST SENSE opcode from 0x3 to 0xff which
+should be rejected by the \fIDEVICE\fR. There is a small chance that the
+device vendor has implemented a vendor specific command at that opcode (0xff).
+When used twice the pass\-through call to send the SCSI command is bypassed.
+The idea here is to measure the user space overhead of this package's
+library to set up and process the response of a SCSI command. This option
+will be typically used with the \fI\-\-num=NUM\fR and \fI\-\-time\fR
+options where \fINUM\fR is a large number (e.g. 1000000).
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output response in ASCII hexadecimal.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in the
+cdb's "allocation length" field. If not given (or \fILEN\fR is zero) then
+252 is used. The maximum value of \fILEN\fR is 255 (but SPC\-4 recommends 252).
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fINUM\fR
+perform \fINUM\fR SCSI REQUEST SENSE commands, stopping when either \fINUM\fR
+is reached or an error occurs. The default value for \fINUM\fR is 1 .
+.TP
+\fB\-\-number\fR=\fINUM\fR
+same action as \fI\-\-num=NUM\fR. Added for compatibility with sg_turs.
+.TP
+\fB\-p\fR, \fB\-\-progress\fR
+show progress indication (a percentage) if available. If \fI\-\-num=NUM\fR
+is given, \fINUM\fR is greater than 1 and an initial progress indication
+was detected then this utility waits 30 seconds before subsequent checks.
+Exits when \fINUM\fR is reached or there are no more progress indications.
+Ignores \fI\-\-hex\fR, \fI\-\-raw\fR and \fI\-\-time\fR options. See
+NOTES section below.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary (to stdout).
+.TP
+\fB\-s\fR, \fB\-\-status\fR
+if the REQUEST SENSE command finished without error (as indicated by its
+SCSI status) then the contents of the parameter data are analysed as
+sense data and the exit status is set accordingly. The default
+action (i.e. when this option is not given) is to ignore the contents
+of the parameter data for the purposes of setting the exit status.
+Some types of error set a sense key of "NO SENSE" with non\-zero
+information in the additional sense code (e.g. the FAILURE PREDICTION
+THRESHOLD EXCEEDED group of codes); this results in an exit status
+value of 10. If the sense key is "NO SENSE" and both asc and ascq are
+zero then the exit status is set to 0 . See the sg3_utils(8) man page
+for exit status values.
+.TP
+\fB\-t\fR, \fB\-\-time\fR
+time the SCSI REQUEST SENSE command(s) and calculate the average number
+of operations per second.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+Additionally the response (if received) is output in ASCII\-HEX. Use
+this option multiple times for greater verbosity.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+In SCSI 1 and 2 the REQUEST SENSE command was very important for error
+and warning processing in SCSI. The autosense capability rendered this
+command almost superfluous.
+.PP
+However recent SCSI drafts (e.g. SPC\-4 rev 14 and SBC\-3 rev 14) increase
+the utility of the REQUEST SENSE command. Idle and standby (low) power
+conditions can be detected with this command.
+.PP
+The REQUEST SENSE command is not marked as mandatory in SPC\-3 (i.e. for
+all SCSI devices) but is marked as mandatory in SBC\-2 (i.e. for disks),
+SSC\-3 (i.e. for tapes) and MMC\-4 (i.e. for CD/DVD/HD\-DVD/BD drives).
+.PP
+The progress indication is optionally part of the sense data. When a prior
+command that takes a long time to complete (and typically precludes other
+media access commands) is still underway, the progress indication can be used
+to determine how long before the device returns to its normal state.
+.PP
+The SCSI FORMAT command for disks used with the IMMED bit set is an example
+of an operation that takes a significant amount of time and precludes other
+media access during that time. The IMMED bit set instructs the FORMAT command
+to return control to the application client once the format has commenced (see
+SBC\-3). Several long duration SCSI commands associated with tape drives also
+use the progress indication (see SSC\-3).
+.PP
+Early standards suggested that the SCSI TEST UNIT READY command be used for
+polling the progress indication (see the sg_turs utility). Since SPC\-3 the
+standards suggest that the SCSI REQUEST SENSE command should be used instead.
+.PP
+The \fIDEVICE\fR is opened with a read\-only flag (e.g. in Unix with the
+O_RDONLY flag).
+.SH EXIT STATUS
+The exit status of sg_requests is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2021 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_turs (sg3_utils)
diff --git a/doc/sg_reset.8 b/doc/sg_reset.8
new file mode 100644
index 0000000..7406fa6
--- /dev/null
+++ b/doc/sg_reset.8
@@ -0,0 +1,135 @@
+.TH SG_RESET "8" "March 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_reset \- sends SCSI device, target, bus or host reset; or checks reset
+state
+.SH SYNOPSIS
+.B sg_reset
+[\fI\-\-bus\fR] [\fI\-\-device\fR] [\fI\-\-help\fR] [\fI\-\-host\fR]
+[\fI\-\-no-esc\fR] [\fI\-\-target\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+The sg_reset utility with no options (just a \fIDEVICE\fR) reports on the
+reset state (e.g. if a reset is underway) of the \fIDEVICE\fR. When given
+a \fI\-\-device\fR, \fI\-\-target\fR, \fI\-\-bus\fR or \fI\-\-host\fR
+option it requests a device, target, bus or host reset respectively.
+.PP
+A device reset is applied to the Logical Unit (LU) corresponding to
+\fIDEVICE\fR. It is most likely implemented by a Low level Driver (LLD)
+in Linux as a LOGICAL UNIT RESET task management function.
+.PP
+The ability to reset a SCSI target was added in Linux kernel 2.6.27 . A LLD
+may send Low level Drivers (LLDs) the I_T NEXUS RESET task management
+function. Alternatively it may use a transport mechanism to do the same
+thing (e.g. a hard reset on the link containing a SAS target).
+.PP
+In the Linux kernel 2.6 and 3 series this utility can be called on sd,
+sr (cd/dvd), st or sg device nodes; if the user has appropriate permissions.
+.PP
+Users of this utility can check whether a reset recovery is already underway
+before trying to send a new reset with this utility. Calling this utility
+with no options, just the \fIDEVICE\fR, will do such a check.
+.SH OPTIONS
+.TP
+\fB\-b\fR, \fB\-\-bus\fR
+attempt a SCSI bus reset. A bus reset is a SCSI Parallel Interface (SPI)
+concept not found in modern transports. A recent LLD may implement it as
+a series of resets on targets that might be considered as siblings to the
+target on the \fIDEVICE\fR path.
+.TP
+\fB\-d\fR, \fB\-\-device\fR
+attempt a SCSI device reset. This would typically involve sending a LOGICAL
+UNIT RESET task management function to \fIDEVICE\fR.
+.TP
+\fB\-z\fR, \fB\-\-help\fR
+print the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-host\fR
+attempt a host reset. The "host" in this context is often called
+a Host Bus Adapter (HBA) and contains one or more SCSI initiators.
+.TP
+\fB\-N\fR, \fB\-\-no\-esc\fR
+without this option, if a device reset (\fI\-\-device\fR) fails then it
+will escalate to a target reset. And if a target reset (\fI\-\-target\fR)
+fails then it will escalate to a bus reset. And if a bus
+reset (\fI\-\-bus\fR) fails then it will escalate to a host reset. With this
+option only the requested reset is attempted. An alternate option name of
+\fI\-\-no-escalate\fR is also accepted.
+.TP
+\fB\-\-no\-escalate\fR
+The same as \fB\-N\fR, \fB\-\-no\-esc\fR.
+.TP
+\fB\-t\fR, \fB\-\-target\fR
+attempt a SCSI target reset. A SCSI target contains one or more LUs. This
+would typically involve sending a I_T NEXUS RESET task management function
+to \fIDEVICE\fR There may be a transport action that is equivalent (e.g.
+in SAS a hard reset on the link that contains the target).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the degree of verbosity (debug messages).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+prints the version string then exits.
+.SH NOTES
+The error recovery code within the Linux kernel (SCSI mid\-level) when faced
+with a SCSI command timing out and no response from the device (LU) does the
+following. First it tries a device reset and if that is not successful tries
+a target reset. If that is not successful it tries a bus reset. If that is
+not successful it tries a host reset. The "device,target,bus,host" order is
+the reset escalation that the \fI\-\-no-esc\fR option attempts to stop. In
+large storage configurations the escalation may be (very) undesirable.
+.PP
+This utility calls the SG_SCSI_RESET ioctl and as of lk 3.10.7 the
+\fI\-\-no-esc\fR option is not supported. Patches to implement this
+functionality may be accepted in lk 3.18 or 3.19 .
+.PP
+SAM\-4 and 5 define a hard reset, a LOGICAL UNIT RESET and a I_T NEXUS
+RESET. A hard reset is defined to be a power on condition, a microcode
+change or a transport reset event. LOGICAL UNIT RESET and I_T NEXUS
+RESET can be requested via task management functions (and support for
+LOGICAL UNIT RESET is mandatory). In Linux the SCSI subsystem leaves it up
+to the LLDs as to exactly what type (if any) of reset is performed.
+The "bus reset" is SCSI Parallel Interface (SPI) concept that may not map
+well to recent SCSI transports so it may be a dummy operation. A "host reset"
+attempts to re\-initialize the HBA that the request passes through en route
+to the \fIDEVICE\fR. Note that a "host reset" and a "bus reset" may cause
+collateral damage.
+.PP
+This utility does not allow individual SCSI commands to be aborted. SAM\-4
+defines ABORT TASK and ABORT TASK SET task management functions for that.
+.PP
+Prior to SAM\-3 there was a TARGET RESET task management function. And in
+SAM\-4 I_T NEXUS RESET appeared which seems closely related: the "I_T"
+stands for Initiator\-Target.
+.PP
+Transports may have their own types of resets not supported by this utility.
+For example SAS has a link reset in which both ends of a physical link (e.g.
+between a SAS expander and a SAS tape drive) renegotiate their connection.
+.PP
+Prior to version 0.57 of this utility the command line had short options
+only (e.g. \fI\-d\fR but not \fI\-\-device\fR). Also \fI\-h\fR invoked a host
+reset while in the current version \fI\-h\fR is equivalent to \fI\-\-help\fR
+and both \fI\-H\fR and \fI\-\-host\fR invoke a host reset. For backward
+compatibility define the environment variable SG3_UTILS_OLD_OPTS or
+SG_RESET_OLD_OPTS . In this case \fI\-h\fR will invoke a host reset and the
+output will be verbose as it was previously (equivalent to using the
+\fI\-\-verbose\fR option now).
+For example:
+.PP
+    SG_RESET_OLD_OPTS=1 sg_reset \-h /dev/sg1
+.br
+sg_reset: starting host reset
+.br
+sg_reset: completed host reset
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variables SG3_UTILS_OLD_OPTS
+or SG_RESET_OLD_OPTS can be given. When either is present this utility will
+expect the older command line options as outlined in the NOTES section.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH COPYRIGHT
+Copyright \(co 1999\-2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/sg_reset_wp.8 b/doc/sg_reset_wp.8
new file mode 100644
index 0000000..47d4453
--- /dev/null
+++ b/doc/sg_reset_wp.8
@@ -0,0 +1,65 @@
+.TH SG_RESET_WP "8" "February 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_reset_wp \- send SCSI RESET WRITE POINTER command
+.SH SYNOPSIS
+.B sg_reset_wp
+[\fI\-\-all\fR] [\fI\-\-count=ZC\fR] [\fI\-\-help\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] [\fI\-\-zone=ID\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI RESET WRITE POINTER command to the \fIDEVICE\fR. This command
+is described in ZBC standard (INCITS 536\-2016) and the draft ZBC\-2
+documents at T10 (e.g. zbc2r12.pdf).
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+sets the ALL field in the cdb. This causes a reset write pointer operation of
+all open zones and full zones. When this option is given then the
+\fI\-\-zone=ID\fR option is ignored. Either this option or the
+\fI\-\-zone=ID\fR option is required.
+.TP
+\fB\-C\fR, \fB\-\-count\fR=\fIZC\fR
+ZC is placed in the Zone Count field in the cdb of the RESET WRITE POINTER
+command supported by this utility. ZC should be a value from 0 to
+65535 (0xffff) inclusive.
+.br
+The action that the \fIDEVICE\fR takes with this option depends on whether
+the \fI\-\-all\fR option is set. See the RESET WRITE POINTER command
+description (e.g. section 5.9, table 46 in zbc2r12.pdf).
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-z\fR, \fB\-\-zone\fR=\fIID\fR
+where \fIID\fR is placed in the cdb's ZONE ID field. A zone id is a zone
+start logical block address (LBA). This causes a reset write pointer
+operation on the zone identified by the ZONE ID field. The default value is
+0. Either this option or the \fI\-\-all\fR option is required.
+\fIID\fR is assumed to be in decimal unless prefixed with '0x' or has a
+trailing 'h' which indicate hexadecimal.
+.SH NOTES
+The Zones Emptied log parameter in the Zoned Block Device Statistics log
+page counts the number of times the RESET WRITE POINTER command has
+been (successfully) invoked.
+.SH EXIT STATUS
+The exit status of sg_reset_wp is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2014\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_rep_zones,sg_zone(sg3_utils)
diff --git a/doc/sg_rmsn.8 b/doc/sg_rmsn.8
new file mode 100644
index 0000000..7fc279e
--- /dev/null
+++ b/doc/sg_rmsn.8
@@ -0,0 +1,64 @@
+.TH SG_RMSN "8" "November 2012" "sg3_utils\-1.31" SG3_UTILS
+.SH NAME
+sg_rmsn \- send SCSI READ MEDIA SERIAL NUMBER command
+.SH SYNOPSIS
+.B sg_rmsn
+[\fI\-\-help\fR] [\fI\-\-raw\fR] [\fI\-\-readonly\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send a SCSI READ MEDIA SERIAL NUMBER command to \fIDEVICE\fR and outputs
+the response.
+.PP
+This command is described in SPC\-3 found at www.t10.org . It was originally
+added to SPC\-3 in revision 11 (2003/2/12). It is not an mandatory command
+and the author has not seen any SCSI devices that support it.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+sends the serial number (if found) to stdout. This output may contain
+non\-printable characters (e.g. the serial number is padded with NULLs
+at the end so its length is a multiple of 4). The default action is
+to print the serial number out in ASCII\-HEX with ASCII characters to
+the right. All error messages are sent to stderr.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+opens the DEVICE read\-only rather than read\-write which is the
+default. The Linux sg driver needs read\-write access for the SCSI
+READ MEDIA SERIAL NUMBER command but other access methods may require
+read\-only access.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+Device identification information is also found in a standard INQUIRY
+response and its VPD pages (see sg_vpd). The relevant VPD pages are
+the "device identification page" (VPD page 0x83) and the "unit serial
+number" page (VPD page 0x80).
+.PP
+The MMC\-4 command set for CD/DVD/HD-DVD/BD drives has a "media serial number"
+feature (0x109) [and a "logical unit serial number" feature]. These
+can be viewed with sg_get_config.
+.SH EXIT STATUS
+The exit status of sg_rmsn is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2005\-2012 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_vpd(sg3_utils), sg_get_config(sg3_utils)
diff --git a/doc/sg_rtpg.8 b/doc/sg_rtpg.8
new file mode 100644
index 0000000..d2ce33c
--- /dev/null
+++ b/doc/sg_rtpg.8
@@ -0,0 +1,64 @@
+.TH SG_RTPG "8" "May 2014" "sg3_utils\-1.39" SG3_UTILS
+.SH NAME
+sg_rtpg \- send SCSI REPORT TARGET PORT GROUPS command
+.SH SYNOPSIS
+.B sg_rtpg
+[\fI\-\-decode\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send a SCSI REPORT TARGET PORT GROUPS command to \fIDEVICE\fR and
+outputs the response.
+.PP
+Target port group access is described in SPC\-3 and SPC\-4 found at
+www.t10.org . The most recent draft of SPC\-4 is revision 37 in which
+target port groups are described in section 5.15 .
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-d\fR, \fB\-\-decode\fR
+decodes the status code and asymmetric access state from each
+target port group descriptor returned. The default action is not
+to decode these values.
+.TP
+\fB\-e\fR, \fB\-\-extended\fR
+use extended header format for parameter data. This sets the PARAMETER DATA
+FORMAT field in the cdb to 1.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output response in hex (rather than partially or fully decode it).
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary to stdout.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+The Report Target Port Groups command should be supported whenever the TPGS
+bits in a standard INQUIRY response are greater than zero. [View with
+sg_inq utility.]
+.SH EXIT STATUS
+The exit status of sg_rtpg is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2014 Christophe Varoqui and Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq(sg3_utils)
diff --git a/doc/sg_safte.8 b/doc/sg_safte.8
new file mode 100644
index 0000000..c46a5b1
--- /dev/null
+++ b/doc/sg_safte.8
@@ -0,0 +1,132 @@
+.TH SG_SAFTE "8" "April 2016" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_safte \- access SCSI Accessed Fault\-Tolerant Enclosure (SAF\-TE) device
+.SH SYNOPSIS
+.B sg_safte
+[\fI\-\-config\fR] [\fI\-\-devstatus\fR] [\fI\-\-encstatus\fR]
+[\fI\-\-flags\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-insertions\fR]
+[\fI\-\-raw\fR] [\fI\-\-usage\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Fetches enclosure status (via a SCSI READ BUFFER command).
+The \fIDEVICE\fR should be a SAF\-TE device which may be a storage
+array controller (INQUIRY peripheral device type 0xc) or a generic
+processor device (INQUIRY peripheral device type 0x3).
+.PP
+If no options are given (only the \fIDEVICE\fR argument) then the
+overall enclosure status as reported by the option
+.I
+\-\-config
+.R
+is reported.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-c\fR, \fB\-\-config\fR
+will issues a
+.I
+Read Enclosure Configuration
+.R
+(READ BUFFER ID 0) cdb to the device, which returns a list of the
+enclosure hardware resources.
+.TP
+\fB\-d\fR, \fB\-\-devstatus\fR
+will issue a
+.I
+Read Device Slot Status
+.R
+(READ BUFFER ID 4) cdb to the device, which returns information about
+the current state of each drive or slot.
+.TP
+\fB\-s\fR, \fB\-\-encstatus\fR
+will issue a
+.I
+Read Enclosure Status
+.R
+(READ BUFFER ID 1) cdb to the device, which returns the operational
+state of the components.
+.TP
+\fB\-f\fR, \fB\-\-flags\fR
+will issue a
+.I
+Read Global Flags
+.R
+(READ BUFFER ID 5) cdb to the device, which read the most recent state
+of the global flags of the RAID processor device.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response to a READ BUFFER command in ASCII hex to stdout. If used
+once, output the response to the first READ BUFFER command (i.e. with
+buffer_id=0). This should be the enclosure configuration. If used twice (or
+more often), the response to subsequent READ BUFFER commands is output.
+.TP
+\fB\-i\fR, \fB\-\-insertions\fR
+will issue a
+.I
+Read Device Insertions
+.R
+(READ BUFFER ID 3) cdb to the device, which returns information about
+the number of times devices have been inserted whilst the RAID system
+was powered on.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the response to a READ BUFFER command in binary to stdout. If used
+once, output the response to the first READ BUFFER command (i.e. with
+buffer_id=0). This should be the enclosure configuration. If used twice (or
+more often), the response to subsequent READ BUFFER commands is output.
+.TP
+\fB\-u\fR, \fB\-\-usage\fR
+will issue a
+.I
+Read Usage Statistics
+.R
+(READ BUFFER ID 2) cdb to the device, which returns the information on
+total usage time and number of power\-on cycles of the RAID device.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+This implementation is based on the intermediate review document dated
+19970414 and named "SR041497.pdf". So it is quite old. Intel and nStor
+are the authors. Intel have a zip archive containing this and related
+documents in the "SAF\-TE: SCSI Accessed Fault Tolerant Enclosures
+Interface Specification" section of this page:
+.PP
+https://www.intel.com/content/www/us/en/servers/ipmi/ipmi\-technical\-resources.html
+.PP
+Similar functionality is provided by SPC\-4 SCSI Enclosure Services (SES)
+devices (Peripheral device type 0xd), which can be queried with the
+sg_ses utility.
+.SH EXAMPLES
+To view the configuration:
+.PP
+   sg_safte /dev/sg1
+.PP
+To view the device slot status:
+.PP
+   sg_safte \-\-devstatus /dev/sg1
+.PP
+.SH EXIT STATUS
+The exit status of sg_safte is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Hannes Reinecke and Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2016 Hannes Reinecke and Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq, sg_ses (in sg3_utils package); safte\-monitor (internet)
diff --git a/doc/sg_sanitize.8 b/doc/sg_sanitize.8
new file mode 100644
index 0000000..bb7bdc4
--- /dev/null
+++ b/doc/sg_sanitize.8
@@ -0,0 +1,267 @@
+.TH SG_SANITIZE "8" "December 2020" "sg3_utils\-1.46" SG3_UTILS
+.SH NAME
+sg_sanitize \- remove all user data from disk with SCSI SANITIZE command
+.SH SYNOPSIS
+.B sg_sanitize
+[\fI\-\-ause\fR] [\fI\-\-block\fR] [\fI\-\-count=OC\fR] [\fI\-\-crypto\fR]
+[\fI\-\-dry\-run\fR] [\fI\-\-desc\fR] [\fI\-\-early\fR] [\fI\-\-fail\fR]
+[\fI\-\-help\fR] [\fI\-\-invert\fR] [\fI\-\-ipl=LEN\fR] [\fI\-\-overwrite\fR]
+[\fI\-\-pattern=PF\fR] [\fI\-\-quick\fR] [\fI\-\-test=TE\fR]
+[\fI\-\-timeout=SECS\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+[\fI\-\-wait\fR] [\fI\-\-zero\fR] [\fI\-\-znr\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility invokes the SCSI SANITIZE command. This command was first
+introduced in the SBC\-3 revision 27 draft. The purpose of the sanitize
+operation is to alter the information in the cache and on the medium of a
+logical unit (e.g. a disk) so that the recovery of user data is not
+possible. If that user data cannot be erased, or is in the process of
+being erased, then the sanitize operation prevents access to that user
+data.
+.PP
+Once a SCSI SANITIZE command has successfully started, then user data from
+that disk is no longer available. Even if the disk is power cycled, the
+sanitize operation will continue after power is re\-instated until it is
+complete.
+.PP
+This utility requires either the \fI\-\-block\fR, \fI\-\-crypto\fR,
+\fI\-\-fail\fR or \fI\-\-overwrite\fR option. With the \fI\-\-block\fR,
+\fI\-\-crypto\fR or \fI\-\-overwrite\fR option the user is given 15 seconds
+to reconsider whether they wish to erase all the data on a disk, unless
+the \fI\-\-quick\fR option is given in which case the sanitize operation
+starts immediately. The disk's INQUIRY response strings are printed out just
+in case the wrong \fIDEVICE\fR has been given.
+.PP
+If the \fI\-\-early\fR option is given then this utility will exit soon
+after starting the SANITIZE command with the IMMED bit set. The user can
+monitor the progress of the sanitize operation with
+the "sg_requests \-\-num=9999 \-\-progress" which sends a REQUEST SENSE
+command every 30 seconds. Otherwise if the \fI\-\-wait\fR option is given
+then this utility will wait until the SANITIZE command completes (or fails)
+and that can be many hours.
+.PP
+If the \fI\-\-wait\fR option is not given then the SANITIZE command is
+started with the IMMED bit set. If neither the \fI\-\-early\fR nor the
+\fI\-\-wait\fR options are given then this utility sends a REQUEST SENSE
+command after every 60 seconds until there are no more progress indications
+in which case this utility exits silently. If additionally the
+\fI\-\-verbose\fR option is given the exit will be marked by a short
+message that the sanitize seems to have succeeded.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-A\fR, \fB\-\-ause\fR
+sets the AUSE bit in the cdb. AUSE is an acronym for "allow unrestricted
+sanitize exit". The default action is to leave the AUSE bit cleared.
+.TP
+\fB\-B\fR, \fB\-\-block\fR
+perform a "block erase" sanitize operation.
+.TP
+\fB\-c\fR, \fB\-\-count\fR=\fIOC\fR
+where \fIOC\fR is the "overwrite count" associated with the "overwrite"
+sanitize operation. \fIOC\fR can be a value between 1 and 31 and 1 is
+the default.
+.TP
+\fB\-C\fR, \fB\-\-crypto\fR
+perform a "cryptographic erase" sanitize operation. Note that this erase is
+often very quick as it simply overwrites an internal cryptographic key with
+a new value. Those keys are not accessible to users and encrypt all data
+written then decrypt all data read from the media. The primary reason for
+doing that is to make this operation fast. This operation can not be
+reversed.
+.TP
+\fB\-d\fR, \fB\-\-desc\fR
+sets the DESC field in the REQUEST SENSE command used for polling. By
+default this field is set to zero. A REQUEST SENSE polling loop is
+used after the SANITIZE command is issued (assuming that neither the
+\fI\-\-early\fR nor the \fI\-\-wait\fR option have been given) to check
+on the progress of this command as it can take some time.
+.TP
+\fB\-D\fR, \fB\-\-dry\-run\fR
+this option will parse the command line, do all the preparation but bypass
+the actual SANITIZE command.
+.TP
+\fB\-e\fR, \fB\-\-early\fR
+the default action of this utility is to poll the disk every 60 seconds to
+fetch the progress indication until the sanitize is finished. When this
+option is given this utility will exit "early" as soon as the SANITIZE
+command with the IMMED bit set to 1 has been acknowledged. This option and
+\fI\-\-wait\fR cannot both be given.
+.TP
+\fB\-F\fR, \fB\-\-fail\fR
+perform an "exit failure mode" sanitize operation. Typically requires the
+preceding SANITIZE command to have set the AUSE bit.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage information then exit.
+.TP
+\fB\-i\fR, \fB\-\-ipl\fR=\fILEN\fR
+set the initialization pattern length to \fILEN\fR bytes. By default it is
+set to the length of the pattern file (\fIPF\fR) or 4 if the \fI\-\-zero\fR
+option is given. Only active when the \fI\-\-overwrite\fR option is also
+given. It is the number of bytes from the \fIPF\fR file that will be used
+as the initialization pattern (if the \fI\-\-zero\fR option is not given).
+The minimum size is 1 byte and the maximum is the logical block size of the
+\fIDEVICE\fR (and not to exceed 65535). If \fILEN\fR exceeds the \fIPF\fR
+file size then the initialization pattern is padded with zeros.
+.TP
+\fB\-I\fR, \fB\-\-invert\fR
+set the INVERT bit in the overwrite service action parameter list. This
+only affects the "overwrite" sanitize operation. The default is a clear
+INVERT bit. When the INVERT bit is set then the initialization pattern
+is inverted between consecutive overwrite passes.
+.TP
+\fB\-O\fR, \fB\-\-overwrite\fR
+perform an "overwrite" sanitize operation. When this option is given then
+the \fI\-\-pattern=PF\fR or the \fI\-\-zero\fR option is required.
+.TP
+\fB\-p\fR, \fB\-\-pattern\fR=\fIPF\fR
+where \fIPF\fR is the filename of a file containing the initialization
+pattern required by an "overwrite" sanitize operation. The length of
+this file will be used as the length of the initialization pattern unless
+the \fI\-\-ipl=LEN\fR option is given. The length of the initialization
+pattern must be from 1 to the logical block size of the \fIDEVICE\fR.
+.TP
+\fB\-Q\fR, \fB\-\-quick\fR
+the default action (i.e. when the option is not given) is to give the user
+15 seconds to reconsider doing a sanitize operation on the \fIDEVICE\fR.
+When this option is given that step (i.e. the 15 second warning period)
+is skipped.
+.TP
+\fB\-T\fR, \fB\-\-test\fR=\fITE\fR
+set the TEST field in the overwrite service action parameter list. This
+only affects the "overwrite" sanitize operation. The default is to place
+0 in that field.
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fISECS\fR
+where \fISECS\fR is the number of seconds used for the timeout on the
+SANITIZE command.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-w\fR, \fB\-\-wait\fR
+the default action (i.e. without this option and the \fI\-\-early\fR option)
+is to start the SANITIZE command with the IMMED bit set then poll for the
+progress indication with the REQUEST SENSE command until the sanitize
+operation is complete (or fails). When this option is given (and the
+\fI\-\-early\fR option is not given) then the SANITIZE command is started
+with the IMMED bit clear. For a large disk this might take hours. [A
+cryptographic erase operation could potentially be very quick.]
+.TP
+\fB\-z\fR, \fB\-\-zero\fR
+with an "overwrite" sanitize operation this option causes the initialization
+pattern to be zero (4 zeros are used as the initialization pattern). Cannot
+be used with the \fI\-\-pattern=PF\fR option. If this option is given
+twice (e.g. '\-zz') then 0xff is used as the initialization byte.
+.TP
+\fB\-Z\fR, \fB\-\-znr\fR
+sets ZNR bit (zoned no reset) in cdb. Introduced in the SBC\-4 revision 7
+draft.
+.SH NOTES
+The SCSI SANITIZE command is closely related to the ATA SANITIZE command,
+both are relatively new with the ATA command being the first one defined.
+The SCSI to ATA Translation (SAT) definition for the SCSI SANITIZE command
+appeared in the SAT\-3 revision 4 draft.
+.PP
+When a SAT layer is used to a (S)ATA disk then for OVERWRITE the
+initialization pattern must be 4 bytes long. So this means either the
+\fI\-\-zero\fR option may be given, or a pattern file (with the
+\fI\-\-pattern=PF\fR option) that is 4 bytes long or set to that
+length with the \fI\-\-ipl=LEN\fR option.
+.PP
+The SCSI SANITIZE command is related to the SCSI FORMAT UNIT command. It
+is likely that a block erase sanitize operation would take a similar
+amount of time as a format on the same disk (e.g. 9 hours for a 2 Terabyte
+disk). The primary goal of a format is the configuration of the disk at
+the end of a format (e.g. different logical block size or protection
+information added). Removal of user data is only a side effect of a format.
+With the SCSI SANITIZE command, removal of user data is the primary goal.
+If a sanitize operation is interrupted (e.g. the disk is power cycled)
+then after power up any remaining user data will not be available and the
+sanitize operation will continue. When a format is interrupted (e.g. the
+disk is power cycled) the drafts say very little about the state of the
+disk. In practice some of the original user data may remain and the format
+may need to be restarted.
+.PP
+Finding out whether a disk (SCSI or ATA) supports SANITIZE can be a
+challenge. If the user really needs to find out and no other information
+is available then try 'sg_sanitize \-\-fail \-vvv <device>' and observe
+the sense data returned may be the safest approach. Using the \fI\-\-fail\fR
+variant of this utility should have no effect unless it follows an already
+failed sanitize operation. If the SCSI REPORT SUPPORTED OPERATION CODES
+command (see sg_opcodes) is supported then using it would be a better
+approach for finding if sanitize is supported.
+.PP
+If using the dd command to check the before and after data of a particular
+block (i.e. check the erase actually worked) it is a good idea to use
+the 'iflag=direct' operand. Otherwise the first read might be cached and
+returned when the same LBA is read a little later. Obviously this utility
+should only be used to sanitize data on a disk whose mounted file
+systems (if any) have been unmounted prior to the erase!
+.SH EXAMPLES
+These examples use Linux device names. For suitable device names in
+other supported Operating Systems see the sg3_utils(8) man page.
+.PP
+As a precaution if this utility is called with no options then apart from
+printing a usage message, nothing happens:
+.PP
+   sg_sanitize /dev/sdm
+.PP
+To do a "block erase" sanitize the \fI\-\-block\fR option is required.
+The user will be given a 15 second period to reconsider, the SCSI SANITIZE
+command will be started with the IMMED bit set, then this utility will
+poll for a progress indication with a REQUEST SENSE command until the
+sanitize operation is finished:
+.PP
+   sg_sanitize \-\-block /dev/sdm
+.PP
+To start a "block erase" sanitize and return from this utility once it is
+started (but not yet completed) use the \fI\-\-early\fR option:
+.PP
+   sg_sanitize \-\-block \-\-early /dev/sdm
+.PP
+If the 15 second reconsideration time is not required add the
+\fI\-\-quick\fR option:
+.PP
+   sg_sanitize \-\-block \-\-quick \-\-early /dev/sdm
+.PP
+To do an "overwrite" sanitize a pattern file may be given:
+.PP
+   sg_sanitize \-\-overwrite \-\-pattern=rand.img /dev/sdm
+.PP
+If the length of that "rand.img" is 512 bytes (a typically logical block
+size) then to use only the first 17 bytes (repeatedly) in the "overwrite"
+sanitize operation:
+.PP
+   sg_sanitize \-\-overwrite \-\-pattern=rand.img \-\-ipl=17 /dev/sdm
+.PP
+To overwrite with zeros use:
+   sg_sanitize \-\-overwrite \-\-zero /dev/sdm
+.SH EXIT STATUS
+The exit status of sg_sanitize is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page. Unless the \fI\-\-wait\fR option is given, the
+exit status may not reflect the success of otherwise of the format.
+.PP
+The Unix convention is that "no news is good news" but that can be a bit
+unnerving after an operation like sanitize, especially if it finishes
+quickly (i.e. before the first progress poll is sent). Giving the
+\fI\-\-verbose\fR option once should supply enough additional output to
+settle those nerves.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2011\-2020 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_requests(8), sg_format(8)
diff --git a/doc/sg_sat_identify.8 b/doc/sg_sat_identify.8
new file mode 100644
index 0000000..595321c
--- /dev/null
+++ b/doc/sg_sat_identify.8
@@ -0,0 +1,167 @@
+.TH SG_SAT_IDENTIFY "8" "January 2020" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_sat_identify \- send ATA IDENTIFY DEVICE command via SCSI to ATA
+Translation (SAT) layer
+.SH SYNOPSIS
+.B sg_sat_identify
+[\fI\-\-ck_cond\fR] [\fI\-\-extend\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-ident\fR] [\fI\-\-len=CLEN\fR] [\fI\-\-packet\fR] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends either an ATA IDENTIFY DEVICE command or an ATA IDENTIFY
+PACKET DEVICE command to \fIDEVICE\fR and outputs the response. The devices
+that respond to these commands are ATA disks and ATAPI devices respectively.
+Rather than send these commands directly to the device they are sent via a
+SCSI transport which is assumed to contain a SCSI to ATA Translation (SAT)
+Layer (SATL). The SATL may be in an operating system driver, in host bus
+adapter firmware or in some external enclosure.
+.PP
+The SAT standard (SAT ANSI INCITS 431\-2007, prior draft: sat\-r09.pdf at
+www.t10.org) defines two SCSI "ATA PASS\-THROUGH" commands: one using a 16
+byte "cdb" and the other with a 12 byte cdb. This utility defaults to using
+the 16 byte cdb variant. SAT\-4 revision 5 added a SCSI "ATA
+PASS\-THROUGH(32)" command. SAT\-2 and SAT\-3 are now also standards: SAT\-2
+ANSI INCITS 465\-2010 and SAT\-3 ANSI INCITS 517-2015 . The SAT\-4 project
+is near standardization and the most recent draft is sat4r06.pdf .
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-c\fR, \fB\-\-ck_cond\fR
+sets the CK_COND bit in the ATA PASS\-THROUGH SCSI cdb. The
+default setting is clear (i.e. 0). When set the SATL should yield a
+sense buffer containing a ATA Result descriptor irrespective of whether
+the command succeeded or failed. When clear the SATL should only yield
+a sense buffer containing a ATA Result descriptor if the command failed.
+.TP
+\fB\-e\fR, \fB\-\-extend\fR
+sets the EXTEND bit in the ATA PASS\-THROUGH SCSI cdb. The
+default setting is clear (i.e. 0). When set a 48 bit LBA command is sent
+to the device. This option has no effect when \fI\-\-len=12\fR.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs the usage message summarizing command line options
+then exits. Ignores \fIDEVICE\fR if given.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+outputs the ATA IDENTIFY (PACKET) DEVICE response in hex. The default
+action (i.e. without any '\-H' options) is to output the response in
+hex, grouped in 16 bit words (i.e. the ATA standard's preference).
+When given once, the response is output in ASCII hex bytes (i.e. the
+SCSI standard's preference). When given twice (i.e. '\-HH') the output
+is in hex, grouped in 16 bit words, the same as the default but without
+a header. When given thrice (i.e. '\-HHH') the output is in hex, grouped in
+16 bit words, in a format that is acceptable for 'hdparm \-\-Istdin' to
+process. '\-HHHH' simply outputs hex data bytes, space separated, 16 per
+line.
+.TP
+\fB\-i\fR, \fB\-\-ident\fR
+outputs the World Wide Name (WWN) of the device. This should be a NAA\-5
+64 bit number. It is output in hex prefixed with "0x". If not available
+then "0x0000000000000000" is output. The equivalent for a SCSI disk (i.e. its
+logical unit name) can be found with "sg_vpd \-ii".
+.TP
+\fB\-l\fR, \fB\-\-len\fR=CLEN
+CLEN this is the length of the SCSI cdb used for the ATA PASS\-THROUGH
+command.  CLEN can either be 12, 16 or 32. The default is 16. The larger
+cdb sizes are needed for 48 bit LBA addressing of ATA devices. The ATA
+Auxiliary and ICC registers are only conveyed with the 32 byte cdb variant.
+.TP
+\fB\-p\fR, \fB\-\-packet\fR
+send an ATA IDENTIFY PACKET DEVICE command (via the SATL). The default
+action is to send an ATA IDENTIFY DEVICE command. Note that the ATAPI
+specification by T13 (i.e. the PACKET interface) is now obsolete.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the ATA IDENTIFY (PACKET) DEVICE response in binary. The output
+should be piped to a file or another utility when this option is used.
+The binary is sent to stdout, and errors are sent to stderr.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increases the level or verbosity.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string
+.SH NOTES
+Since the response to the IDENTIFY (PACKET) DEVICE command is very
+important for the correct use of an ATA(PI) device (and is typically the
+first command sent), a SATL should provide an ATA Information VPD page
+which contains the similar information.
+.PP
+The SCSI ATA PASS\-THROUGH (12) command's opcode is 0xa1 and it clashes with
+the MMC set's BLANK command used by cd/dvd writers. So a SATL in front
+of an ATAPI device that uses MMC (i.e. has peripheral device type 5)
+probably should treat opcode 0xa1 as a BLANK command and send it through
+to the cd/dvd drive. The ATA PASS\-THROUGH (16) command's opcode (0x85)
+does not clash with anything so it is a better choice.
+.PP
+Prior to Linux kernel 2.6.29 USB mass storage limited sense data to 18 bytes
+which made the \fB\-\-ck_cond\fR option yield strange (truncated) results.
+.SH EXAMPLES
+These examples use Linux device names and a Linux utility called hdparm. For
+suitable device names in other supported Operating Systems see the
+sg3_utils(8) man page.
+.PP
+In this example /dev/sdb is a SATA 2.5" disk connected via a USB (type C
+connector) dongle that implements the UAS (USB attached SCSI) protocol (also
+known as UASP). UAS is a vast improvement over the USB mass storage class.
+.PP
+    # sg_sat_identify /dev/sdb
+.br
+Response for IDENTIFY DEVICE ATA command:
+.br
+ 00   0c5a 3fff c837 0010 0000 0000 003f 0000  .Z ?. .7 .. .. .. .? ..
+.br
+ ....
+.PP
+The hexadecimal ASCII (with plain ASCII to the right) output is abridged
+to a single line (i.e. the first 16 bytes (or 8 words)). Now to decode
+some of that ATA Identify response. First sg_inq can decode a few strings:
+.PP
+    # sg_sat_identify \-HHHH /dev/sdb | sg_inq \-\-ata \-I \-
+.br
+ATA device: model, serial number and firmware revision:
+.br
+  ST9500420AS     5VJCE6R7 0002SDM1
+.PP
+For a lot more details, the hdparm utility is a good choice:
+.PP
+    # sg_sat_identify \-HHH /dev/sdb | hdparm \-\-Istdin
+.br
+ATA device, with non\-removable media
+.br
+        Model Number:       ST9500420AS
+.br
+        Serial Number:      5VJCE6R7
+.br
+        Firmware Revision:  0002SDM1
+.br
+        Transport:          Serial
+.br
+Standards:
+.br
+ ....
+.PP
+There are about 80 more lines of details decoded by hdparm in this case.
+Notice the difference in the number of "H" options: three give an unadorned
+hex output arranged in (little endian) words (i.e. 16 bits each) while
+four "H" options give an unadorned hex output in bytes (i.e. 8 bits each).
+.SH EXIT STATUS
+The exit status of sg_sat_identify is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2006\-2020 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_vpd(sg3_utils), sg_inq(sg3_utils), sdparm(sdparm), hdparm(hdparm)
diff --git a/doc/sg_sat_phy_event.8 b/doc/sg_sat_phy_event.8
new file mode 100644
index 0000000..8b842a3
--- /dev/null
+++ b/doc/sg_sat_phy_event.8
@@ -0,0 +1,109 @@
+.TH SG_SAT_PHY_EVENT "8" "July 2020" "sg3_utils\-1.46" SG3_UTILS
+.SH NAME
+sg_sat_phy_event \- use ATA READ LOG EXT via a SAT pass\-through to fetch
+SATA phy event counters
+.SH SYNOPSIS
+.B sg_sat_phy_event
+[\fI\-\-ck_cond\fR] [\fI\-\-extend\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-ignore\fR] [\fI\-\-len=\fR{16|12}] [\fI\-\-raw\fR] [\fI\-\-reset\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends an ATA READ LOG EXT with the log page ("address") set to
+11h to \fIDEVICE\fR and outputs the response. Log page 11h is defined in
+the SATA 2.5 standard and contains phy event counters. Rather than send this
+command directly to the \fIDEVICE\fR, are sent via a SCSI transport which is
+assumed to contain a SCSI to ATA Translation (SAT) Layer (SATL). The SATL may
+be in an operating system driver, in host bus adapter firmware or in some
+external enclosure.
+.PP
+The SAT standard (SAT ANSI INCITS 431\-2007, prior draft: sat\-r09.pdf at
+www.t10.org) defines two SCSI "ATA PASS\-THROUGH" commands: one using a 16
+byte "cdb" and the other with a 12 byte cdb. This utility defaults to using
+the 16 byte cdb variant. SAT\-2 is also a standard: SAT\-2 ANSI INCITS
+465\-2010 and the draft prior to that is sat2r09.pdf . The SAT-3 project has
+started and the most recent draft is sat3r01.pdf .
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-c\fR, \fB\-\-ck_cond\fR
+sets the CK_COND bit in the ATA PASS\-THROUGH SCSI cdb. The
+default setting is clear (i.e. 0). When set the SATL should yield a
+sense buffer containing a ATA Result descriptor irrespective of whether
+the command succeeded or failed. When clear the SATL should only yield
+a sense buffer containing a ATA Result descriptor if the command failed.
+.TP
+\fB\-e\fR, \fB\-\-extend\fR
+sets the EXTEND bit in the ATA PASS\-THROUGH SCSI cdb. The
+default setting is clear (i.e. 0). When set a 48 bit LBA command is sent
+to the device. This option has no effect when \fI\-\-len=12\fR.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs the usage message summarizing command line options
+then exits. Ignores \fIDEVICE\fR if given.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+outputs the ATA READ LOG EXT response in hex. The default
+action (i.e. without any '\-H' options) is to output the response in
+hex, grouped in 16 bit words (i.e. the ATA standard's preference).
+When given once, the response is output in ASCII hex bytes (i.e. the
+SCSI standard's preference). When given twice (i.e. '\-HH') the output
+is in hex, grouped in 16 bit words, the same as the default but without
+a header.
+.TP
+\fB\-i\fR, \fB\-\-ignore\fR
+usually the phy counter identifier names are decoded. When this option is
+given, the numeric value of the identifier is output, the vendor flag, the
+data length (in bytes) and the corresponding value.
+.TP
+\fB\-l\fR, \fB\-\-len\fR={16|12}
+this is the length of the SCSI cdb used for the ATA PASS\-THROUGH commands.
+The argument can either be 16 or 12. The default is 16. The larger cdb
+size is needed for 48 bit LBA addressing of ATA devices. On the other
+hand some SCSI transports cannot convey SCSI commands longer than 12 bytes.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the ATA READ LOG EXT response in binary. The output
+should be piped to a file or another utility when this option is used.
+The binary is sent to stdout, and errors are sent to stderr.
+.TP
+\fB\-R\fR, \fB\-\-reset\fR
+reset the counters after the current values are returned, decoded and
+displayed.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increases the level or verbosity.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string
+.SH NOTES
+The SCSI ATA PASS\-THROUGH (12) command's opcode is 0xa1 and it clashes with
+the MMC set's BLANK command used by cd/dvd writers. So a SATL in front
+of an ATAPI device that uses MMC (i.e. has peripheral device type 5)
+probably should treat opcode 0xa1 as a BLANK command and send it through
+to the cd/dvd drive. The ATA PASS\-THROUGH (16) command's opcode (0x85)
+does not clash with anything so it is a better choice.
+.PP
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be
+a SCSI generic (sg) device. In the 2.6 series block devices (e.g. disks
+and ATAPI DVDs) can also be specified. For example "sg_inq /dev/sda"
+will work in the 2.6 series kernels. From lk 2.6.6 other SCSI "char"
+device names may be used as well (e.g. "/dev/st0m"). Prior to lk 2.6.29
+USB mass storage limited sense data to 18 bytes which made the
+\fB\-\-ck_cond\fR option yield strange (truncated) results.
+.SH EXIT STATUS
+The exit status of sg_sat_identify is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2006\-2020 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_sat_identify,sg_sat_read_gplog(sg3_utils),
+.B smp_rep_phy_err_log(smp_utils),sdparm(sdparm),hdparm(hdparm)
diff --git a/doc/sg_sat_read_gplog.8 b/doc/sg_sat_read_gplog.8
new file mode 100644
index 0000000..552ad14
--- /dev/null
+++ b/doc/sg_sat_read_gplog.8
@@ -0,0 +1,114 @@
+.TH SG_SAT_READ_GPLOG "8" "April 2015" "sg3_utils\-1.41" SG3_UTILS
+.SH NAME
+sg_sat_read_gplog \- use ATA READ LOG EXT command via a SCSI to ATA
+Translation (SAT) layer
+.SH SYNOPSIS
+.B sg_sat_read_gplog
+[\fI\-\-ck_cond\fR] [\fI\-\-count=CO\fR] [\fI\-\-dma\fR] [\fI\-\-help\fR]
+[\fI\-\-hex\fR] [\fI\-\-len=\fR{16|12}] [\fI\-\-log=\fRLA]
+[\fI\-\-page=\fRPN] [\fI\-\-readonly\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends an ATA READ LOG EXT or an ATA READ LOG DMA EXT command to
+the \fIDEVICE\fR. This command is used to read the general purpose log
+of (S)ATA disks (not ATAPI devices such as DVD driver). Rather than send the
+READ LOG (DMA) EXT command directly to the device it is sent via a SCSI
+transport which is assumed to contain a SCSI to ATA Translation (SAT)
+Layer (SATL). The SATL may be in an operating system driver, in host bus
+adapter (HBA) firmware or in some external enclosure.
+.PP
+This utility does not currently attempt to decode the response from the
+ATA disk, rather it outputs the response in ASCII hexadecimal grouped in
+16 bit words. Following ATA conventions those words are decoded little
+endian (note that SCSI commands use a big endian representation). In the
+future this utility may attempt to decode some log pages, perhaps using
+the \fI\-\-decode\fR option.
+.PP
+The SAT\-2 standard (SAT ANSI INCITS 465-2010, prior draft: sat2r09.pdf at
+www.t10.org) defines two SCSI "ATA PASS\-THROUGH" commands: one using a 16
+byte "cdb" and the other with a 12 byte cdb. This utility defaults to using
+the 16 byte cdb variant.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-C\fR, \fB\-\-ck_cond\fR
+sets the CK_COND bit in the ATA PASS\-THROUGH SCSI cdb. The
+default setting is clear (i.e. 0). When set the SATL should yield a
+sense buffer containing a ATA Result descriptor irrespective of whether
+the ATA command succeeded or failed. When clear the SATL should only yield
+a sense buffer containing a ATA Result descriptor if the ATA command failed.
+.TP
+\fB\-c\fR, \fB\-\-count\fR=\fICO\fR
+the number \fICO\fR is placed in the "count" field in the ATA READ
+LOG EXT command. This specified the number of 512\-byte blocks of
+data to be read from the specified log.
+.TP
+\fB\-d\fR, \fB\-\-dma\fR
+use the ATA READ LOG DMA EXT command instead of ATA READ LOG EXT command.
+Some devices require this to return valid log data.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs the usage message summarizing command line options then exits.
+Ignores \fIDEVICE\fR if given.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+when given once, the response is output in ASCII hexadecimal bytes. When
+given twice, then the response is grouped into 16 bit words using ATA
+conventions (i.e. little endian); this is the default output (i.e. when
+this option is not given). When given thrice (i.e. '\-HHH') the output
+is in hex, grouped in 16 bit words (without a leading offset and trailing
+ASCII on each line), in a format that is acceptable for 'hdparm \-\-Istdin'
+to process.
+.TP
+\fB\-L\fR, \fB\-\-log\fR=\fILA\fR
+the number \fILA\fR is known as the "log address" in the ATA standards and
+is placed in bits 7:0 of the "lba" field of the ATA READ LOG (DMA) EXT
+command. This specifies the log to be returned (See ATA\-ACS for a detailed
+list of available log addresses). The default value placed in the "lba
+field is 0, returning the directory of available logs. The maximum value
+allowed for \fILOG\fR is 0xff.
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPN\fR
+the number \fIPN\fR is the page number (within the log address) and is
+placed in bits 32:16 of the "lba" field of the ATA READ LOG (DMA) EXT
+command. The default value placed in the "lba" field is 0. The maximum value
+allowed for \fILOG\fR is 0xffff.
+.TP
+\fB\-l\fR, \fB\-\-len\fR={16|12}
+this is the length of the SCSI cdb used for the ATA PASS\-THROUGH commands.
+The argument can either be 16 or 12. The default is 16. Some SCSI
+transports cannot convey SCSI commands longer than 12 bytes.
+.TP
+\fB\-r\fR, \fB\-\-readonly\fR
+causes the \fIDEVICE\fR to be opened with the read\-only flag (O_RDONLY in
+Unix). The default action is to open \fIDEVICE\fR with the read\-write
+flag (O_RDWR in Unix). In some cases sending power management commands to
+ATA disks are defeated by OS actions on the close() if the \fIDEVICE\fR was
+opened with the read\-write flag (e.g. the OS might think it needs to
+flush something to disk).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increases the level or verbosity.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string
+.SH NOTES
+Prior to Linux kernel 2.6.29 USB mass storage limited sense data to 18 bytes
+which made the \fB\-\-ck_cond\fR option yield strange (truncated) results.
+.SH EXIT STATUS
+The exit status of sg_sat_read_gplog is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHOR
+Written by Hannes Reinecke and Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2014\-2015 Hannes Reinecke, SUSE Linux GmbH
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_sat_identify(sg3_utils), sg_inq(sg3_utils), sdparm(sdparm),
+.B hdparm(hdparm)
diff --git a/doc/sg_sat_set_features.8 b/doc/sg_sat_set_features.8
new file mode 100644
index 0000000..304968f
--- /dev/null
+++ b/doc/sg_sat_set_features.8
@@ -0,0 +1,112 @@
+.TH SG_SAT_SET_FEATURES "8" "November 2014" "sg3_utils\-1.40" SG3_UTILS
+.SH NAME
+sg_sat_set_features \- use ATA SET FEATURES command via a SCSI to ATA
+Translation (SAT) layer
+.SH SYNOPSIS
+.B sg_sat_set_features
+[\fI\-\-count=CO\fR] [\fI\-\-ck_cond\fR] [\fI--extended\fR]
+[\fI\-\-feature=FEA\fR] [\fI\-\-help\fR] [\fI\-\-lba=LBA\fR]
+[\fI\-\-len=\fR{16|12}] [\fI\-\-readonly\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends an ATA SET FEATURES command to the \fIDEVICE\fR.
+This command is used to change settings of ATA non\-packet (i.e. disks) and
+packet devices (e.g. cd/dvd drives). Rather than send the SET FEATURES
+command directly to the device it is sent via a SCSI transport which is
+assumed to contain a SCSI to ATA Translation (SAT) Layer (SATL). The SATL
+may be in an operating system driver, in host bus adapter firmware or in
+some external enclosure.
+.PP
+The SAT standard (SAT ANSI INCITS 431\-2007, prior draft: sat\-r09.pdf at
+www.t10.org) defines two SCSI "ATA PASS\-THROUGH" commands: one using a 16
+byte "cdb" and the other with a 12 byte cdb. This utility defaults to using
+the 16 byte cdb variant. SAT\-2 is also a standard: SAT\-2 ANSI INCITS
+465\-2010 and the draft prior to that is sat2r09.pdf . The SAT-3 project has
+started and the most recent draft is sat3r05b.pdf .
+.PP
+The features can be read using the sg_sat_identify utility which uses either
+the ATA IDENTIFY DEVICE (for non\-packet devices) or the IDENTIFY PACKET
+DEVICE (for packet devices) command.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-c\fR, \fB\-\-count\fR=\fICO\fR
+the number \fICO\fR is placed in the "count" field in the ATA SET
+FEATURES command. Only some subcommands (a term used for the value
+placed in the "feature" field) require the count field to be set.
+The default value placed in the "count" field is 0.
+.TP
+\fB\-C\fR, \fB\-\-ck_cond\fR
+sets the CK_COND bit in the ATA PASS\-THROUGH SCSI cdb. The
+default setting is clear (i.e. 0). When set the SATL should yield a
+sense buffer containing a ATA Result descriptor irrespective of whether
+the ATA command succeeded or failed. When clear the SATL should only yield
+a sense buffer containing a ATA Result descriptor if the ATA command failed.
+.TP
+\fB\-e\fR, \fB\-\-extended\fR
+allow for extended LBA numbers (i.e. larger than 32 bits).
+This value is enabled automatically for large LBA numbers, but can be
+enabled explicitly even for low LBA numbers with this option.
+.TP
+\fB\-f\fR, \fB\-\-feature\fR=\fIFEA\fR
+the value \fIFEA\fR is placed in the "feature" field in the ATA SET
+FEATURES command. The term "subcommand" is sometimes used for this
+value. The default value placed in the "feature" field is 0 which
+is reserved and hence should not change anything. Two common examples
+are 2h to enable the write cache and 82h to disable it.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs the usage message summarizing command line options
+then exits. Ignores \fIDEVICE\fR if given.
+.TP
+\fB\-L\fR, \fB\-\-lba\fR=\fILBA\fR
+the number \fILBA\fR is placed in the "lba" field of the ATA SET
+FEATURES command. Only some sub\-commands (a term used for the value
+placed in the "feature" field) require the lba field to be set. This
+value is typically not a "logical block address" as the acronym might
+imply.  The default value placed in the "lba" field is 0. The maximum value
+allowed for \fILBA\fR is 0xfffffffe (or 0xffffff if \fI\-\-len=\fR12).
+.TP
+\fB\-l\fR, \fB\-\-len\fR={16|12}
+this is the length of the SCSI cdb used for the ATA PASS\-THROUGH commands.
+The argument can either be 16 or 12. The default is 16. Some SCSI
+transports cannot convey SCSI commands longer than 12 bytes.
+.TP
+\fB\-r\fR, \fB\-\-readonly\fR
+causes the \fIDEVICE\fR to be opened with the read\-only flag (O_RDONLY in
+Unix). The default action is to open \fIDEVICE\fR with the read\-write
+flag (O_RDWR in Unix). In some cases sending power management commands to
+ATA disks are defeated by OS actions on the close() if the \fIDEVICE\fR was
+opened with the read\-write flag (e.g. the OS might think it needs to
+flush something to disk).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increases the level or verbosity.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string
+.SH NOTES
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be
+a SCSI generic (sg) device. In the 2.6 and 3 series block devices (e.g. disks
+and ATAPI DVDs) can also be specified. For example "sg_inq /dev/sda"
+will work in the 2.6 series kernels. From lk 2.6.6 other SCSI "char"
+device names may be used as well (e.g. "/dev/st0m"). Prior to lk 2.6.29
+USB mass storage limited sense data to 18 bytes which made the
+\fB\-\-ck_cond\fR option yield strange (truncated) results.
+.SH EXIT STATUS
+The exit status of sg_sat_set_features is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2007\-2014 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_sat_identify(sg3_utils), sg_inq(sg3_utils), sdparm(sdparm),
+.B hdparm(hdparm)
diff --git a/doc/sg_scan.8.linux b/doc/sg_scan.8.linux
new file mode 100644
index 0000000..0698000
--- /dev/null
+++ b/doc/sg_scan.8.linux
@@ -0,0 +1,78 @@
+.TH SG_SCAN "8" "May 2013" "sg3_utils\-1.36" SG3_UTILS
+.SH NAME
+sg_scan \- scans sg devices (or SCSI/ATAPI/ATA devices) and prints
+results
+.SH SYNOPSIS
+.B sg_scan
+[\fI\-a\fR]
+[\fI\-i\fR]
+[\fI\-n\fR]
+[\fI\-w\fR]
+[\fI\-x\fR]
+[\fIDEVICE\fR]*
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+If no \fIDEVICE\fR names are given, sg_scan does a scan of the sg
+devices and outputs a line of information for each sg device that is
+currently bound to a SCSI device. If one or more \fIDEVICE\fRs are given
+only those devices are scanned.
+Each device is opened with the O_NONBLOCK flag so that the scan will
+not "hang" on any device that another process holds an O_EXCL lock on.
+.PP
+Any given \fIDEVICE\fR name is expected to comply
+with (to some extent) the Storage Architecture Model (SAM see www.t10.org).
+Any device names associated with the Linux SCSI subsystem (e.g. /dev/sda
+and /dev/st0m) are suitable. Devices names associated with ATAPI
+devices (e.g. most CD/DVD drives and ATAPI tape drives) are also suitable.
+If the device does not fall into the above categories then an ATA
+IDENTIFY command is tried.
+.PP
+In Linux 2.6 and 3 series kernels, the lsscsi utility may be helpful. Apart
+from providing more information (by data\-mining in the sysfs pseudo file
+system), it does not need root permissions to execute, as this utility
+would typically need.
+.SH OPTIONS
+.TP
+\fB\-a\fR
+do alphabetical scan (i.e. sga, sgb, sgc). Note that sg device nodes with
+an alphabetical index have been deprecated since the Linux kernel 2.2
+series.
+.TP
+\fB\-i\fR
+do a SCSI INQUIRY, output results in a second (indented) line. If the device
+is an ATA disk then output information from an ATA IDENTIFY command
+.TP
+\fB\-n\fR
+do numeric scan (i.e. sg0, sg1...) [default]
+.TP
+\fB\-w\fR
+use a read/write flag when opening sg device (default is read\-only)
+.TP
+\fB\-x\fR
+extra information output about queueing
+.SH NOTES
+This utility was written at a time when hotplugging of SCSI devices
+was not supported in Linux. It used a simple algorithm to scan sg
+device nodes in ascending numeric or alphabetical order, stopping
+after there were 4 consecutive errors.
+.PP
+In the Linux kernel 2.6 series, this utility uses sysfs to find which
+sg device nodes are active and only checks those. Hence there can be
+large "holes" in the numbering of sg device nodes (e.g. after an
+adapter has been removed) and still all active sg device nodes will
+be listed. This utility assumes that sg device nodes are named using
+the normal conventions and searches from /dev/sg0 to /dev/sg4095
+inclusive.
+.SH EXIT STATUS
+The exit status of sg_scan is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by D. Gilbert and F. Jansen
+.SH COPYRIGHT
+Copyright \(co 1999\-2013 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B lsscsi(8)
diff --git a/doc/sg_scan.8.win32 b/doc/sg_scan.8.win32
new file mode 100644
index 0000000..32b99b2
--- /dev/null
+++ b/doc/sg_scan.8.win32
@@ -0,0 +1,170 @@
+.TH SG_SCAN "8" "November 2018" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_scan \- scan storage devices and map to volume names
+.SH SYNOPSIS
+.B sg_scan
+[\fI\-\-bus\fR]  [\fI\-\-help\fR] [\fI\-\-letter=VL\fR] [\fI\-\-scsi\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility scans for physical drives (a.k.a. "hard drives"), cd/dvd drives
+and tape drives and maps them to the corresponding volumes. There may be
+many, one or no corresponding volumes. There is one line output per device
+with identification strings to the right. Its purpose is to list the
+storage device names that can be used by other utilities in this package.
+.PP
+In later versions of Windows this utility may need to be "run as
+Administrator" for disks and other devices to be seen. If not those devices
+will simply not appear as calls to query them fail with access permission
+problems.
+.PP
+There is an optional SCSI adapter scan which may find additional storage
+devices other than the ones listed above. An example is a SCSI Enclosure
+Services (SES) device typically found in disk arrays.
+.PP
+Storage and related devices can have several device names in Windows.
+Probably the most common in the volume name (e.g. "D:"). There is also
+a "class" device name, and this utility scans for three of
+them: "PhysicalDrive<n>", "CDROM<n>" and "TAPE<n>". <n> is an integer
+starting at 0 allocated in ascending order as devices are discovered (and
+sometimes rediscovered).
+.PP
+Some storage devices have a SCSI lower level device name which starts
+with a SCSI (pseudo) adapter name of the form "SCSI<n>:". To this is added
+sub\-addressing in the form of a "bus" number, a "target" identifier and
+a LUN (Logical Unit Number). The "bus" number is also known as a "PathId".
+These components are combined by the utility to make a device name of the
+form: "SCSI<n>:<bus>,<target>,<lun>". This utility allows the
+trailing ",<lun>" to be omitted in which case a LUN of zero is assumed. This
+lower level device name cannot often be used directly since Windows blocks
+attempts to use it if a class driver has "claimed" the device. There are
+SCSI device types (e.g. Automation/Drive interface type) for which there is
+no class driver. At least two transports ("bus types" in Windows jargin):
+USB and IEEE 1394 do not have a "scsi" device names of this form.
+.PP
+In keeping with DOS file system conventions, the various device names
+can be given in upper, lower or mixed case. Since "PhysicalDrive<n>" is
+tedious to write, a shortened form of "PD<n>" is permitted by all
+utilities in this package.
+.PP
+A single device (e.g. a disk) can have many device names! For
+example: "PDO" can also be "C:", "D:" and "SCSI0:0,1,0". The two volume names
+reflect that the disk has two partitions on it. Disk partitions that are
+not recognised by Windows are not usually given a volume name. However
+Vista does show a volume name for a disk which has no partitions recognised
+by it and when selected invites the user to format it (which is rather
+unfriendly to other OSes).
+.PP
+The scanning logic and output of this command changed significantly in
+sg3_utils version 1.27 . The SCSI adapter based scanned is now an
+optional extra.
+.PP
+For more information see the NOTES section below.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-bus\fR
+show the bus type (or transport) by which the device is attached to the
+operating systems. Two or more transports may be involved. For example,
+a SATA disk may be in the external enclosure connected to the computer via
+USB in which case the bus type is USB.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs the usage message summarizing command line options
+then exits.
+.TP
+\fB\-l\fR, \fB\-\-letter\fR=\fIVL\fR
+normally a device that has multiple volume names has up to four listed. If
+there are more than that a "+" is added after the fourth. When this option
+is given the \fIVL\fR argument is assumed to be a volume name (i.e. 'C'
+to 'Z') and if found in the scan, only that volume name appears in the
+output. If there are novolume names in the output then \fIVL\fR was not
+found.
+.TP
+\fB\-s\fR, \fB\-\-scsi\fR
+do a SCSI adapter based scan after the normal storage device based scan.
+There is a blank line between the normal scan and the SCSI adapter based
+scan. If this option is given twice then only the SCSI adapter based scan
+is done.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increases the level or verbosity. Can be used multiple times to display
+more of the internal data, both in normal and error processing.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string
+.SH NOTES
+This utility does not support Windows 95, 98 and ME (and earlier Windows
+operating systems). The target Windows operating systems are currently
+Windows 2000, 2003, XP and Vista (and their variants).
+.PP
+When the \fI\-\-scsi\fR option is given the SCSI adapter tuple is followed
+by a list of two or three fields. First is "claimed=0|1" indicating whether
+a class driver has claimed the device. The next field is "pdt=<num>"
+where <num> is the "peripheral device type" as defined in the SCSI INQUIRY
+command (see SPC\-4 at https://www.t10.org). The <num> has a trailing "h" to
+indicate that it is hexadecimal. Sometimes a third field with the
+word "dubious" appears. This flags that what is supposed to be a SCSI
+INQUIRY command response has a badly formed "additional length" field.
+Thus the corresponding device is unlikely to be a native SCSI device.
+.PP
+The DOS device names given the the CreateFile() call all start with
+a "\\\\.\\" string. That can be given but if not will be supplied
+automatically.
+.PP
+Scanning devices that are hot unplugged and replugged often can be
+problematic, especially with the class device names. Each time a device is
+removed and re\-added it gets a larger class device name (e.g. "PD3"
+becomes "PD4" leaving "PD3" unused). This utility stops scanning class
+devices after it find 8 consecutive "holes".
+.SH EXAMPLES
+The following examples are from a laptop with an internal drive (SATA), a
+CD/DVD drive and a USB attached SATA disk. The latter disk has two volumes
+recognised by Windows.
+.PP
+  # sg_scan
+.br
+PD0     [C]     FUJITSU   MHY2160BH         0000
+.br
+PD1     [DF]    WD        2500BEV External  1.05  WD\-WXE90
+.br
+CDROM0  [E]     MATSHITA DVD/CDRW UJDA775  CB03
+.PP
+Now request bus types as well. BTW That is a SATA disk holding volume C:
+and there is a "Sata" bus type.
+.PP
+  # sg_scan \-b
+.br
+PD0     [C]     <Ata  >  FUJITSU   MHY2160BH         0000
+.br
+PD1     [DF]    <Usb  >  WD        2500BEV External  1.05  WD\-WXE90
+.br
+CDROM0  [E]     <Atapi>  MATSHITA DVD/CDRW UJDA775  CB03
+.PP
+Now request a SCSI adapter scan as well.
+.PP
+  # sg_scan \-b \-s
+.br
+PD0     [C]     <Ata  >  FUJITSU   MHY2160BH         0000
+.br
+PD1     [DF]    <Usb  >  WD        2500BEV External  1.05  WD\-WXE90
+.br
+CDROM0  [E]     <Atapi>  MATSHITA DVD/CDRW UJDA775  CB03
+.br
+
+.br
+SCSI0:0,0,0   claimed=1 pdt=0h  FUJITSU   MHY2160BH         0000
+.br
+SCSI1:0,0,0   claimed=1 pdt=5h  MATSHITA  DVD/CDRW UJDA775  CB03
+.PP
+.SH EXIT STATUS
+The exit status of sg_scan is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2006\-2018 Douglas Gilbert
+.br
+This software is distributed under a FreeBSD license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/sg_seek.8 b/doc/sg_seek.8
new file mode 100644
index 0000000..52ced59
--- /dev/null
+++ b/doc/sg_seek.8
@@ -0,0 +1,146 @@
+.TH SG_SEEK "8" "September 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_seek \- send SCSI SEEK, PRE-FETCH(10) or PRE-FETCH(16) command
+.SH SYNOPSIS
+.B sg_seek
+[\fI\-\-10\fR] [\fI\-\-count=NC\fR] [\fI\-\-grpnum=GN\fR] [\fI\-\-help\fR]
+[\fI\-\-immed\fR] [\fI\-\-lba=LBA\fR] [\fI\-\-num\-blocks=NUM\fR]
+[\fI\-\-pre\-fetch\fR] [\fI\-\-readonly\fR] [\fI\-\-skip=SB\fR]
+[\fI\-\-time\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+[\fI\-\-wrap\-offset=WO\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI SEEK(10), PRE\-FETCH(10) or PRE\-FETCH(16) command to the
+\fIDEVICE\fR. The SEEK command has been obsolete since SBC\-2 (2005) but
+still is supported on some hard disks and even some SSDs (solid state
+disks). The PRE\-FETCH command can be viewed as SEEK's modern replacement.
+Instead of talking about moving the disk heads to the track containing
+the sort after LBA, it talks about bringing the sort after LBA (and a
+given number of blocks) into the disk's cache. Also the PRE\-FETCH commands
+have an IMMED field.
+.PP
+The PRE\-FETCH commands can report "real" errors but usually they will report
+one of two "good" statuses. To do this they return the rarely used CONDITION
+MET status. If the number of blocks does actually fit in the cache (when
+IMMED=0) or there is enough room in the cache when the command arrives (when
+IMMED=1) then a CONDITION MET status is returned. If the requested number of
+blocks did not fit (IMMED=0) or would not fit (IMMED=1) then status GOOD
+is returned. So if a disk has a large cache and PRE\-FETCH is used sparingly
+then the command is more likely to return CONDITION MET than GOOD. This
+presents some SCSI sub\-systems with problems as due to its rareness they
+mishandle CONDITION MET and treat it as an error (see NOTES section below).
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-T\fR, \fB\-\-10\fR
+use a 10 byte cdb command, either SEEK(10) or PRE\-FETCH(10) command. In
+the absence of the \fI\-\-pre\-fetch\fR option, the SEEK(10) command is
+used. If the \fI\-\-pre\-fetch\fR option is given without this option
+then a PRE\-FETCH(16) command is used.
+.TP
+\fB\-c\fR, \fB\-\-count\fR=\fINC\fR
+\fINC\fR is the number of commands (one of SEEK(10), PRE\-FETCH(10) or
+PRE\-FETCH(16)) that will be executed. The default value is 1. If an error
+occurs it is noted and the program continues until \fINC\fR is exhausted.
+If \fINC\fR is 0 then options are checked and the \fIDEVICE\fR is opened
+but no commands are sent.
+.TP
+\fB\-g\fR, \fB\-\-grpnum\fR=\fIGN\fR
+\fIGN\fR is the group number, a value between 0 and 63 (in hex: 0x3f). The
+default value is 0. This option is ignored if the selected command is
+SEEK(10).
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-i\fR, \fB\-\-immed\fR
+this option only applies to PRE\-FETCH(10) and PRE\-FETCH(16), setting
+the IMMED bit. Without this option, the \fIDEVICE\fR returns after it has
+completed transferring all, or part of, the requested blocks into the
+cache. If this option is given the \fIDEVICE\fR returns after it has done
+sanity checks on the cdb (e.g. making sure the \fILBA\fR is greater than
+the number of available blocks) and before it does the transfer into the
+cache.
+.br
+Note that even when this option is given, the return status from the
+PRE\-FETCH commands is still either CONDITION MET status (if the cache seems
+to have enough free space for the transfer) or a GOOD status (if the cache
+does not seem to have enough free space).
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+\fILBA\fR is the starting logical block address that is placed in the
+command descriptor block (cdb) of the selected command. Note that the
+\fILBA\fR field in SEEK(10) and PRE\-FETCH(10) is a 32 bit quantity,
+while with PRE\-FETCH(16) it is a 64 bit quantity. The default value is
+0 .
+.TP
+\fB\-n\fR, \fB\-\-num\-blocks\fR=\fINUM\fR
+\fINUM\fR is the number of blocks, starting at and including \fILBA\fR,
+to place in the \fIDEVICE\fR's cache. The SEEK(10) command does not use
+the \fINUM\fR value. For PRE\-FETCH(10) \fINUM\fR is a 16 bit quantity,
+while for PRE\-FETCH(16) it is a 32 bit quantity. The default value is
+1 . If \fINUM\fR is 0 then the \fIDEVICE\fR will attempt to transfer all
+blocks from the given \fILBA\fR to the end of the medium.
+.TP
+\fB\-p\fR, \fB\-\-pre\-fetch\fR
+this option selects either PRE\-FETCH(10) or PRE\-FETCH(16) commands. With
+the \fI\-\-10\fR also given, the PRE\-FETCH(10) command is selected; without
+that option PRE\-FETCH(16) is selected. The default (in the absence of this
+and other 'selecting' options) the SEEK(10) command is selected.
+.TP
+\fB\-r\fR, \fB\-\-readonly\fR
+this option sets a 'read\-only' flag when the underlying operating system
+opens the given \fIDEVICE\fR. This may not work since operating systems can
+not easily determine whether a pass\-through is a logical read or write
+operation so they take a risk averse stance and require read\-write type
+\fIDEVICE\fR opens irrespective of what is performed by the pass\-through.
+.TP
+\fB\-s\fR, \fB\-\-skip\fR=\fISB\fR
+\fISB\fR is the number of logical block addresses to skip, between repeated
+commands when \fINC\fR is greater than 1. The default value of \fISB\fR is
+1 . \fISB\fR may be set to 0 so that all \fINC\fR PRE\-FETCH commands use
+the same \fILBA\fR.
+.TP
+\fB\-t\fR, \fB\-\-time\fR
+if given the elapsed time to execute \fINC\fR commands is recorded. This is
+printed out before this utility exits. If \fINC\fR is greater than 1 then
+the the "per command" time is also printed.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-w\fR, \fB\-\-wrap\-offset\fR=\fIWO\fR
+\fIWO\fR is the number of blocks, relative to \fILBA\fR, that when exceeded,
+set the next command's logical block address back to \fILBA\fR. Whether
+this "reset\-to\-LBA" action occurs depends on the values \fINC\fR and
+\fISB\fR.
+.SH NOTES
+Prior to Linux kernel 4.17 the CONDITION MET status was logged as an error.
+Recent versions of FreeBSD handle the CONDITION MET status properly.
+.PP
+If either the \fI\-\-count=NC\fR or \fI\-\-verbose\fR option is given then
+a summary line like the following is output:
+.PP
+    Command count=5, number of condition_mets=3, number of goods=2
+.PP
+before the utility exits.
+.SH EXIT STATUS
+The exit status of sg_seek is 0 (GOOD) or 25 (CONDITION_MET) when this
+utility is successful. If multiple commands are executed (e.g. when \fINC\fR
+is greater than 1) then the result of the last executed SEEK or PRE\-FETCH
+command sets the exit status. Otherwise see the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_vpd(sg3_utils); sdparm(sdparm)
diff --git a/doc/sg_senddiag.8 b/doc/sg_senddiag.8
new file mode 100644
index 0000000..e3fa612
--- /dev/null
+++ b/doc/sg_senddiag.8
@@ -0,0 +1,300 @@
+.TH SG_SENDDIAG "8" "May 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_senddiag \- performs a SCSI SEND DIAGNOSTIC command
+.SH SYNOPSIS
+.B sg_senddiag
+[\fI\-\-doff\fR] [\fI\-\-extdur\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-list\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-page=PG\fR] [\fI\-\-pf\fR]
+[\fI\-\-raw=H,H...\fR] [\fI\-\-raw=\-\fR] [\fI\-\-selftest=ST\fR]
+[\fI\-\-test\fR] [\fI\-\-timeout=SECS\fR] [\fI\-\-uoff\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.PP
+.B sg_senddiag
+[\fI\-doff\fR] [\fI\-e\fR] [\fI\-h\fR] [\fI\-H\fR] [\fI\-l\fR] [\fI\-pf\fR]
+[\fI\-raw=H,H...\fR] [\fI\-raw=\-\fR] [\fI\-s=ST\fR] [\fI\-t\fR]
+[\fI\-T=SECS\fR] [\fI\-uoff\fR] [\fI\-v\fR] [\fI\-V\fR] [\fI\-?\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends a SCSI SEND DIAGNOSTIC command to the \fIDEVICE\fR. It
+can issue self\-tests, find supported diagnostic pages or send arbitrary
+diagnostic pages.
+.PP
+When the \fI\-\-list\fR option and a \fIDEVICE\fR are given then the utility
+sends a SCSI RECEIVE DIAGNOSTIC RESULTS command to fetch the response (i.e.
+the page numbers of supported diagnostic pages).
+.PP
+When the \fI\-\-list\fR option is given without a \fIDEVICE\fR then a list of
+diagnostic page names and their numbers, known by this utility, are listed.
+.PP
+This utility supports two command line syntax\-es, the preferred one is
+shown first in the synopsis and explained in this section. A later section
+on the old command line syntax outlines the second group of options.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-d\fR, \fB\-\-doff\fR
+set the Device Offline (DevOffL) bit (default is clear). Only significant
+when \fI\-\-test\fR option is set for the default self\-test. When set other
+operations on any logical units controlled by the this device server (target)
+may be affected (delayed) while a default self\-test is underway.
+.TP
+\fB\-e\fR, \fB\-\-extdur\fR
+outputs the expected extended self\-test duration. The duration is given in
+seconds (and minutes in parentheses). This figure is obtained from mode page
+0xa (i.e. the control mode page).
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+outputs response from RECEIVE DIAGNOSTIC RESULTS in hex rather than decode it.
+Only the Supported Diagnostic Pages diagnostic page (i.e. page_code=0) is
+decoded; other pages (e.g. those used by SES) are output in hex.
+.br
+If \fI\-\-hex\fR is used once, the hex output has a relative address at the
+start of each line. If \fI\-\-hex\fR is used twice, then ASCII is shown to
+the right of each line of hex. If \fI\-\-hex\fR is used three time or more,
+only the hex is output, in two character pairs (i.e. a byte) space separated
+and up to 16 bytes per line. This latter form, if placed in a file or piped
+through to another invocation, is suitable for the \fI\-\-raw=\-\fR option.
+.TP
+\fB\-l\fR, \fB\-\-list\fR
+when a \fIDEVICE\fR is also given lists the names of all diagnostic pages
+supported by this device. The request is sent via a SEND DIAGNOSTIC
+command (with the "pF" bit set) and the response is fetched by a RECEIVE
+DIAGNOSTIC RESULTS command. When used in the absence of a \fI\-\-list\fR
+argument then a list of diagnostic page names and their numbers, known
+by this utility, are listed.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the value placed in the parameter list length field of a
+SEND DIAGNOSTIC command or in the allocation length field of a RECEIVE
+DIAGNOSTIC RESULTS command. This only occurs when the other options imply
+there will be data sent or received by the command. The default value
+is 4096 bytes. \fILEN\fR cannot exceed 65535 or 0xffff in hexadecimal.
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option.
+.TP
+\fB\-P\fR, \fB\-\-page\fR=\fIPG\fR
+where \fIPG\fR is the RECEIVE DIAGNOSTIC RESULTS command page code field.
+If this option is given the PCV bit in that command is set. When this option
+is given then no SEND DIAGNOSTIC command is sent (unlike \fI\-\-list\fR).
+If \fIPG\fR is 0 then the response is decoded as if it is the SPC Supported
+Diagnostic pages diagnostic page. Other \fIPG\fR values (i.e. 1 to 255)
+have their responses output in hex.
+.TP
+\fB\-p\fR, \fB\-\-pf\fR
+set Page Format (PF) bit. By default it is clear (i.e. 0) unless the
+list \fI\-\-list\fR option is given in which case the Page Format
+bit is set (as required by SPC\-3).
+.TP
+\fB\-r\fR, \fB\-\-raw\fR=\fIH,H...\fR
+string of comma separated hex numbers each of which should resolve to
+a byte value (i.e. 0 to ff inclusive). A (single) space separated string
+of hex bytes is also allowed but the list needs to be in quotes. This
+sequence forms a diagnostic page to be sent with the SCSI SEND DIAGNOSTIC
+command. Mostly likely the \fI\-\-pf\fR option should also be given.
+.TP
+\fB\-r\fR, \fB\-\-raw=\-\fR
+reads sequence of bytes from stdin. The sequence may be comma, space, tab
+or linefeed (newline) separated. If a line contains "#" then the remaining
+characters on that line are ignored. Otherwise each non separator character
+should resolve to a byte value (i.e. 0 to ff inclusive). This sequence forms
+a diagnostic page to be sent with the SCSI SEND DIAGNOSTIC command. Mostly
+likely the \fI\-\-pf\fR option should also be given.
+.TP
+\fB\-s\fR, \fB\-\-selftest\fR=\fIST\fR
+where \fIST\fR is the self\-test code. The default value is 0 which is
+inactive. Some other values:
+.br
+  \fB1\fR : background short self\-test
+.br
+  \fB2\fR : background extended self\-test
+.br
+  \fB4\fR : aborts a (background) self\-test that is in progress
+.br
+  \fB5\fR : foreground short self\-test
+.br
+  \fB6\fR : foreground extended self\-test
+.br
+This option is mutually exclusive with default self\-test (i.e.
+can't have (\fIST\fR > 0) and \fI\-\-test\fR).
+.TP
+\fB\-t\fR, \fB\-\-test\fR
+sets the _default_ Self Test (SelfTest) bit. By default this is clear (0).
+The \fI\-\-selftest=ST\fR option should not be active together with this
+option. Both the \fI\-\-doff\fR and/or \fI\-\-uoff\fR options can be used
+with this option.
+.TP
+\fB\-T\fR, \fB\-\-timeout\fR=\fISECS\fR
+where \fISECS\fR is a timeout value (in seconds) for foreground self\-test
+operations. The default value is 7200 seconds (2 hours) and any values
+of \fISECS\fR less than the default are ignored.
+.TP
+\fB\-u\fR, \fB\-\-uoff\fR
+set the Unit Offline (UnitOffL) bit (default is clear). Only significant
+when \fI\-\-test\fR option is set for the default self\-test. When set other
+operations on this logical unit may be affected (delayed) while a default
+self\-test is underway. Some devices (e.g. Fujitsu disks) do more tests
+when this bit is set.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string then exit.
+.SH NOTES
+All devices should support the default self\-test. The 'short' self\-test
+codes should complete in 2 minutes or less. The 'extended' self\-test
+codes' maximum duration is vendor specific (e.g. a little over 10 minutes
+with the author's disks). The foreground self\-test codes wait until they
+are completed while the background self\-test codes return immediately. The
+results of both foreground and background self\-test codes are placed in
+the 'self\-test results' log page (see sg_logs(8)). The SCSI command timeout
+for this utility is set to 60 minutes to allow for slow foreground extended
+self\-tests.
+.PP
+If the \fIDEVICE\fR is a disk then no file systems residing on that disk
+should be mounted during a foreground self\-test. The reason is that other
+SCSI commands may become queued behind the foreground self\-test and timeout.
+.PP
+When the \fI\-\-raw=H,H...\fR option is given then self\-tests should not
+be selected. However the \fB\-\-pf\fR (i.e. "page format") option should be
+given. The length of the diagnostic page to be sent is derived from the
+number of bytes given to the \fI\-\-raw=H,H...\fR option. The diagnostic
+page code (number) should be the first byte of the sequence (i.e. as
+dictated by SPC\-3 diagnostic page format). See the EXAMPLES section below.
+.PP
+Arbitrary diagnostic pages can be read (in hex) with the sg_ses(8)
+utility (not only those defined in SES\-2).
+.PP
+If the utility is used with no options (e.g. "sg_senddiag /dev/sg1")
+Then a degenerate SCSI SEND DIAGNOSTIC command is sent with zero
+in all its fields apart from the opcode. Some devices report this
+as an error while others ignore it. It is not entirely clear from
+SPC\-3 if it is invalid to send such a command.
+.PP
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be a SCSI
+generic (sg) device. In the 2.6 series block devices (e.g. SCSI disks and
+DVD drives) can also be specified.
+.PP
+To access SCSI enclosures see the sg_ses(8) utility. sg_ses uses the
+SCSI SEND DIAGNOSTIC and RECEIVE DIAGNOSTIC RESULTS commands as outlined
+in the SES\-2 (draft) standard.
+.SH EXIT STATUS
+The exit status of sg_senddiag is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.TP
+\fB\-doff\fR
+set the Device Offline (DevOffL) bit (default is clear). Only significant
+when \fI\-t\fR option is set for the default self\-test. Equivalent to
+\fI\-\-doff\fR in the main description.
+.TP
+\fB\-e\fR
+outputs the expected extended self\-test duration. Equivalent to
+\fI\-\-extdur\fR in the main description.
+.TP
+\fB\-h\fR
+outputs response from RECEIVE DIAGNOSTIC RESULTS in hex rather than decode
+it.
+.TP
+\fB\-H\fR
+outputs response from RECEIVE DIAGNOSTIC RESULTS in hex rather than decode it.
+.TP
+\fB\-l\fR
+when a \fIDEVICE\fR is also given lists the names of all diagnostic
+pages supported by this device. The request is sent via a SEND DIAGNOSTIC
+command (with the "pf" bit set) and the response is fetched by a RECEIVE
+DIAGNOSTIC RESULTS command. When used in the absence of a \fIDEVICE\fR
+argument then a list of diagnostic page names and their numbers, known
+by this utility, are listed.
+.TP
+\fB-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-pf\fR
+set Page Format (PF) bit. By default it is clear (i.e. 0) unless
+the \fI\-l\fR option is given in which case the Page Format bit is set
+(as required by SPC\-3).
+.TP
+\fB\-raw\fR=\fIH,H...\fR
+string of comma separated hex numbers each of which should resolve to
+a byte value (i.e. 0 to ff inclusive). This sequence forms a diagnostic
+page to be sent with the SCSI SEND DIAGNOSTIC command. Mostly likely
+the \fI\-pf\fR option should also be given.
+.TP
+\fB\-raw=-\fR
+reads sequence of bytes from stdin. The sequence may be comma, space, tab
+or linefeed (newline) separated. If a line contains "#" then the remaining
+characters on that line are ignored. Otherwise each non separator character
+should resolve to a byte value (i.e. 0 to ff inclusive). This sequence forms
+a diagnostic page to be sent with the SCSI SEND DIAGNOSTIC command. Mostly
+likely the \fI\-pf\fR option should also be given.
+.TP
+\fB\-s\fR=\fIST\fR
+where \fIST\fR is the self\-test code. The default value is 0 which is
+inactive. A value of 1 selects a background short self\-test; 2 selects
+a background extended self\-test; 5 selects a foreground short self\-test;
+6 selects a foreground extended test. A value of 4 will abort
+a (background) self\-test that is in progress. This option is mutually
+exclusive with default self\-test (i.e. \fI\-t\fR).
+.TP
+\fB\-t\fR
+sets the _default_ Self Test (SelfTest) bit. By default this is clear (0).
+The \fI\-s=ST\fR option should not be active together with this option.
+Both the \fI\-doff\fR and/or \fI\-uoff\fR options can be used with this
+option.
+.TP
+\fB\-T\fR=\fISECS\fR
+where \fISECS\fR is a timeout value (in seconds) for foreground self\-test
+operations. See the \fI\-\-timeout=SECS\fR option above.
+.TP
+\fB\-uoff\fR
+set the Unit Offline (UnitOffL) bit (default is clear). Equivalent to
+\fI\-\-uoff\fR in the main description.
+.TP
+\fB\-v\fR
+increase level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.TP
+\fB\-?\fR
+output usage message. Ignore all other parameters.
+.SH EXAMPLES
+The examples sub\-directory in the sg3_utils packages contains two example
+scripts that turn on the CJTPAT (jitter pattern) on some SAS disks (one
+script for each phy). One possible invocation for phy 1 is:
+.PP
+  sg_senddiag \-\-pf \-\-raw=\- /dev/sg2 < sdiag_sas_p1_cjtpat.txt
+.PP
+There is also an example script that turns on the IDLE pattern. Once a
+test pattern has been started it can be turned off by resetting the phy
+or with the STOP phy pattern function:
+.PP
+  sg_senddiag \-\-pf \-\-raw=\- /dev/sg2 < sdiag_sas_p1_stop.txt
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2003\-2018 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_ses(8), sg_logs(8), smartmontools(see net)
diff --git a/doc/sg_ses.8 b/doc/sg_ses.8
new file mode 100644
index 0000000..79c6597
--- /dev/null
+++ b/doc/sg_ses.8
@@ -0,0 +1,801 @@
+.TH SG_SES "8" "October 2021" "sg3_utils\-1.47" SG3_UTILS
+.SH NAME
+sg_ses \- access a SCSI Enclosure Services (SES) device
+.SH SYNOPSIS
+.B sg_ses
+[\fI\-\-all\fR] [\fI\-\-ALL\fR] [\fI\-\-descriptor=DES\fR]
+[\fI\-\-dev\-slot\-num=SN\fR] [\fI\-\-eiioe=A_F\fR] [\fI\-\-filter\fR]
+[\fI\-\-get=STR\fR] [\fI\-\-hex\fR] [\fI\-\-index=IIA\fR |
+\fI\-\-index=TIA,II\fR] [\fI\-\-inner\-hex\fR] [\fI\-\-join\fR]
+[\fI\-\-maxlen=LEN\fR] [\fI\-\-page=PG\fR] [\fI\-\-quiet\fR] [\fI\-\-raw\fR]
+[\fI\-\-readonly\fR] [\fI\-\-sas\-addr=SA\fR] [\fI\-\-status\fR]
+[\fI\-\-verbose\fR] [\fI\-\-warn\fR] \fIDEVICE\fR
+.PP
+.B sg_ses
+\fI\-\-control\fR [\fI\-\-byte1=B1\fR] [\fI\-\-clear=STR\fR]
+[\fI\-\-data=H,H...\fR] [\fI\-\-data=@FN\fR] [\fI\-\-descriptor=DES\fR]
+[\fI\-\-dev\-slot\-num=SN\fR] [\fI\-\-index=IIA\fR | \fI\-\-index=TIA,II\fR]
+[\fI\-\-mask\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-nickname=SEN\fR]
+[\fI\-\-nickid=SEID\fR]  [\fI\-\-page=PG\fR] [\fI\-\-readonly\fR]
+[\fI\-\-sas\-addr=SA\fR] [\fI\-\-set=STR\fR] [\fI\-\-verbose\fR]
+\fIDEVICE\fR
+.PP
+.B sg_ses
+\fI\-\-data=@FN\fR \fI\-\-status\fR [\fI\-\-raw\fR \fI\-\-raw\fR]
+[<all options from first form>]
+.br
+.B sg_ses
+\fI\-\-inhex=FN\fR \fI\-\-status\fR [\fI\-\-raw\fR \fI\-\-raw\fR]
+[<all options from first form>]
+.PP
+.B sg_ses
+[\fI\-\-enumerate\fR] [\fI\-\-index=IIA\fR] [\fI\-\-list\fR] [\fI\-\-help\fR]
+[\fI\-\-version\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Fetches management information from a SCSI Enclosure Service (SES) device.
+This utility can also modify the state of a SES device. The \fIDEVICE\fR
+should be a SES device which may be a dedicated enclosure services
+processor in which case an INQUIRY response's Peripheral Device Type is
+13 [0xd]. Alternatively it may be attached to another type of SCSI
+device (e.g. a disk) in which case the EncServ bit is set in its INQUIRY
+response.
+.PP
+If the \fIDEVICE\fR argument is given with no options then the names of all
+diagnostic pages (dpages) supported are listed. Most, but not necessarily
+all, of the named dpages are defined in the SES standards and drafts. The
+most recent reference for this utility is the draft SCSI Enclosure Services
+4 document T10/BSR INCITS 555 Revision 5 at https://www.t10.org . Existing
+standards for SES, SES\-2 and SES\-3 are ANSI INCITS 305\-1998 and ANSI
+INCITS 448\-2008 and ANSI INCITS 518\-2017 respectively.
+.PP
+SAS expanders typically have a SES device attached via a virtual port.
+Some HBAs (SCSI initiators) choose to expose a SES device internally. That
+means the SCSI subsystem on the host machine can see the SES device, but
+devices connected to that HBA (e.g. a SAS expander) cannot see the HBA's
+SES device. That internal SES device might report on the temperature(s)
+of the HBA and whether anything is connected to its SCSI ports.
+.PP
+The first form shown in the synopsis is for fetching and decoding dpages or
+fields from the SES \fIDEVICE\fR. A SCSI RECEIVE DIAGNOSTIC RESULTS command
+is sent to the \fIDEVICE\fR to obtain each dpage response.  Rather than
+decoding a fetched dpage, it may be output in hex or binary with the
+\fI\-\-hex\fR or \fI\-\-raw \-\-raw\fR options.
+.PP
+The second form in the synopsis is for modifying dpages or fields held in
+the SES \fIDEVICE\fR. A SCSI SEND DIAGNOSTIC command containing a "control"
+dpage is sent to the \fIDEVICE\fR to cause changes. Changing the state of an
+enclosure (e.g. requesting the "ident" (locate) LED to flash on a disk
+carrier in an array) is typically done using a read\-modify\-write cycle.
+See the section on CHANGING STATE below.
+.PP
+The third form in the synopsis has two equivalent invocations shown. They
+decode the contents of a file (named \fIFN\fR) that holds a hexadecimal or
+binary representation of one, or many, SES dpage responses. Typically an
+earlier invocation of the first form of this utility with the '\-HHHH'
+option would have generated that file. Since no SCSI commands are sent, the
+\fIDEVICE\fR argument if given will be ignored.
+.PP
+The last form in the synopsis shows the options for providing command line
+help (i.e. usage information), listing out dpage and field information tables
+held by the utility (\fI\-\-enumerate\fR), or printing the version string
+of this utility.
+.PP
+There is a web page discussing this utility at
+https://sg.danny.cz/sg/sg_ses.html . Support for downloading microcode to
+a SES device has been placed in a separate utility called sg_ses_microcode.
+.PP
+In the following sections "dpage" refers to a diagnostic page, either fetched
+with a SCSI RECEIVE DIAGNOSTIC RESULTS command, sent to the \fIDEVICE\fR with
+a SCSI SEND DIAGNOSTIC command, or fetched from data supplied by the
+\fI\-\-data=\fR option.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+shows (almost) all status dpages, following references and presenting
+the information as a long list whose indentation indicates the level
+of nesting. This option is actually the same as \fI\-\-join\fR, see its
+description for more information.
+.br
+If used twice, adds threshold elements to output (if they are available).
+So it is the same as using \fI\-\-join\fRtwice.
+.TP
+\fB\-z\fR, \fB\-\-ALL\fR
+shows (almost) all status dpages, following references and presenting
+the information as a long list whose indentation indicates the level
+of nesting. Also shows the threshold elements if they are available.
+This option is the same as using  \fI\-\-join\fR rwice.
+.TP
+\fB\-b\fR, \fB\-\-byte1\fR=\fIB1\fR
+some modifiable dpages may need byte 1 (i.e. the second byte) set. In the
+Enclosure Control dpage, byte 1 contains the INFO, NON\-CRIT, CRIT and
+UNRECOV bits. In the Subenclosure String Out, Subenclosure Nickname Control
+and Download Microcode Control dpages, byte 1 is the Subenclosure identifier.
+Active when the \fI\-\-control\fR and \fI\-\-data=H,H...\fR options are used
+and the default value is 0. If the \fI\-\-clear=STR\fR or \fI\-\-set=STR\fR
+option is used then the value read from byte 1 is written back to byte 1.
+\fIB1\fR is in decimal unless it is prefixed by '0x' or '0X' (or has a
+trailing 'h' or 'H').
+.TP
+\fB\-C\fR, \fB\-\-clear\fR=\fISTR\fR
+Used to clear an element field in the Enclosure Control or Threshold Out
+dpage. Must be used together with an indexing option to specify which element
+is to be changed. The Enclosure Control dpage is assumed if the
+\fI\-\-page=PG\fR option is not given. See the STR FORMAT and the CLEAR, GET,
+SET sections below.
+.TP
+\fB\-c\fR, \fB\-\-control\fR
+will send control information to the \fIDEVICE\fR via a SCSI SEND
+DIAGNOSTIC command. Cannot give both this option and \fI\-\-status\fR.
+The Enclosure Control, String Out, Threshold Out, Array Control (obsolete
+in SES\-2), Subenclosure String Out, Subenclosure Nickname Control and
+Download Microcode dpages can be set currently. This option is assumed if
+either the \fI\-\-clear=STR\fR or \fI\-\-set=STR\fR option is given.
+.TP
+\fB\-d\fR, \fB\-\-data\fR=\fIH,H...\fR
+permits a string of comma separated (ASCII) hex bytes to be specified (limit
+1024). A (single) space separated string of hex bytes is also allowed but
+the list needs to be in quotes. This option allows the parameters to a
+control dpage to be specified. The string given should not include the first 4
+bytes (i.e. page code and length). See the DATA SUPPLIED section below.
+.TP
+\fB\-d\fR, \fB\-\-data\fR=\-
+reads one or more data strings from stdin, limit almost 2**16 bytes. stdin
+may provide ASCII hex as a comma separated list (i.e. as with the
+\fI\-\-data=H,H...\fR option). Additionally spaces, tabs and line feeds are
+permitted as separators from stdin . Stops reading stdin when an EOF is
+detected. See the DATA SUPPLIED section below.
+.TP
+\fB\-d\fR, \fB\-\-data\fR=@\fIFN\fR
+reads one or more data strings from the file called \fIFN\fR, limit almost
+2**16 bytes. The contents of the file is decoded in the same fashion as
+stdin described in the previous option. See the DATA SUPPLIED section below.
+.TP
+\fB\-D\fR, \fB\-\-descriptor\fR=\fIDES\fR
+where \fIDES\fR is a descriptor name (string) as found in the Element
+Descriptor dpage. This is a medium level indexing alternative to the low
+level \fI\-\-index=\fR options. If the descriptor name contains a space then
+\fIDES\fR needs to be surrounded by quotes (single or double) or the space
+escaped (e.g. preceded by a backslash). See the DESCRIPTOR NAME, DEVICE SLOT
+NUMBER AND SAS ADDRESS section below.
+.TP
+\fB\-x\fR, \fB\-\-dev\-slot\-num\fR=\fISN\fR, \fB\-\-dsn\fR=\fISN\fR
+where \fISN\fR is a device slot number found in the Additional Element Status
+dpage. Only entries for FCP and SAS devices (with EIP=1) have device slot
+numbers. \fISN\fR must be a number in the range 0 to 255 (inclusive). 255 is
+used to indicate there is no corresponding device slot. This is a medium level
+indexing alternative to the low level \fI\-\-index=\fR options. See the
+DESCRIPTOR NAME, DEVICE SLOT NUMBER AND SAS ADDRESS section below.
+.TP
+\fB\-E\fR, \fB\-\-eiioe\fR=\fIA_F\fR
+\fIA_F\fR is either the string 'auto' or 'force'. There was some fuzziness
+in the interpretation of the 'element index' field in the Additional Element
+Status (AES) dpage between SES\-2 and SES\-3. The EIIOE bit was introduced to
+resolve the problem but not all enclosures have caught up. In the SES\-3
+revision 12 draft the EIIOE bit was expanded to a 2 bit EIIOE field.
+Using '\-\-eiioe=force' will decode the AES dpage as if the EIIOE field is set
+to 1.  Using '\-\-eiioe=auto' will decode the AES dpage as if the EIIOE field
+is set to 1 if the first AES descriptor has its EIP bit set and its element
+index field is 1 (in other words a heuristic to guess whether the EIIOE field
+should be set to 1 or 0).
+.br
+If the enclosure sets the actual EIIOE field to 1 or more then this option has
+no effect. It is recommended that HP JBOD users set \-\-eiioe=auto .
+.TP
+\fB\-e\fR, \fB\-\-enumerate\fR
+enumerate all known diagnostic page (dpage) names and SES elements that this
+utility recognizes plus the abbreviations accepted by this utility. Ignores
+\fIDEVICE\fR if it is given. Essentially it is dumping out tables held
+internally by this utility.
+.br
+If \fI\-\-enumerate\fR is given twice, then the recognised acronyms for the
+\fI\-\-clear=STR\fR, \fI\-\-get=STR\fR and \fI\-\-set=STR\fR options are
+listed. The utility exits after listing this information, so most other
+options and \fIDEVICE\fR are ignored. Since there are many acronyms for
+the Enclosure Control/Status dpage then the output can be further restricted
+by giving the \fI\-\-index=IIA\fR option (e.g. "sg_ses \-ee \-I ts" to only
+show the acronyms associated with the Enclosure Control/Status dpage's
+Temperature Sensor Element Type).
+.TP
+\fB\-f\fR, \fB\-\-filter\fR
+cuts down on the amount of output from the Enclosure Status dpage and the
+Additional Element Status dpage. When this option is given, any line which
+has all its binary flags cleared (i.e. 0) is filtered out (i.e.  ignored).
+If a line has some other value on it (e.g. a temperature) then it is output.
+When this option is used twice only elements associated with the "status=ok"
+field (in the Enclosure status dpage) are output. The \fI\-\-filter\fR option
+is useful for reducing the amount of output generated by the \fI\-\-join\fR
+option.
+.TP
+\fB\-G\fR, \fB\-\-get\fR=\fISTR\fR
+Used to read a field in a status element. Must be used together with a an
+indexing option to specify which element is to be read. By default the
+Enclosure Status dpage is read, the only other dpages that can be read are the
+Threshold In and Additional Element Status dpages. If a value is found it is
+output in decimal to stdout (by default) or in hexadecimal preceded by "0x"
+if the \fI\-\-hex\fR option is also given. See the STR FORMAT and the CLEAR,
+GET, SET sections below.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit. Since there is a lot of information,
+it is split into two pages. The most important is shown on the first page.
+Use this option twice (e.g. '\-hh') to output the second page. Note: the
+\fI\-\-enumerate\fR option might also be viewed as a help or usage type
+option. And like this option it has a "given twice" form: '\-ee'.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+If the \fI\-\-get=STR\fR option is given then output the value found (if
+any) in hexadecimal, with a leading "0x". Otherwise output the response
+in hexadecimal; with trailing ASCII if given once, without it if given
+twice, and simple hex if given three or more times. Ignored when all
+elements from several dpages are being accessed (e.g. when the \fI\-\-join\fR
+option is used). Also see the \fI\-\-raw\fR option which may be used
+with this option.
+.br
+To dump one of more dpage responses to stdout in ASCII parsable hexadecimal
+use \fI\-HHH\fR or \fI\-HHHH\fR. The triple H form only outputs hexadecimals
+which is fine for a single dpage response. When all dpages are dumped (e.g.
+with \fI\-\-page=all\fR) then the quad H form adds the name of each dpage
+following a hash mark ('#'). The \fI\-\-data=\fR option parser ignores
+everything from and including a hash mark to the end of the line. Hence the
+output of the quad H form is still parsable plus it is easier for users to
+view and possibly edit. \fI\-HHHHH\fR (that is 5) adds the page code in
+hex after the page's name in the comment.
+.TP
+\fB\-I\fR, \fB\-\-index\fR=\fIIIA\fR
+where \fIIIA\fR is either an individual index (II) or an Element type
+abbreviation (A). See the INDEXES section below. If the \fI\-\-page=PG\fR
+option is not given then the Enclosure Status (or Control) dpage is assumed.
+May be used with the \fI\-\-join\fR option or one of the \fI\-\-clear=STR\fR,
+\fI\-\-get=STR\fR or \fI\-\-set=STR\fR options. To enumerate the available
+Element type abbreviations use the \fI\-\-enumerate\fR option.
+.TP
+\fB\-I\fR, \fB\-\-index\fR=\fITIA,II\fR
+where \fITIA,II\fR is an type header index (TI) or Element type
+abbreviation (A) followed by an individual index (II). See the INDEXES section
+below. If the \fI\-\-page=PG\fR option is not given then the Enclosure
+Status (or Control) dpage is assumed. May be used with the \fI\-\-join\fR
+option or one of the \fI\-\-clear=STR\fR, \fI\-\-get=STR\fR or
+\fI\-\-set=STR\fR options. To enumerate the available Element type
+abbreviations use the \fI\-\-enumerate\fR option.
+.TP
+\fB\-X\fR, \fB\-\-inhex\fR=\fIFN\fR
+where \fIFN\fR is a filename. It has the equivalent action of the
+\fI\-\-data=@FN\fR option. If \fIFN\fR is '\-' then stdin is read. This
+option has been given for compatibility with other utilities in this
+package that use \fI\-\-inhex=FN\fR (or \fI\-\-in=FN\fR) is a similar
+way. See the "FORMAT OF FILES CONTAINING ASCII HEX" section in the
+sg3_utils manpage for more information.
+.TP
+\fB\-i\fR, \fB\-\-inner\-hex\fR
+the outer levels of a status dpage are decoded and printed out but the
+innermost level (e.g. the Element Status Descriptor) is output in hex. Also
+active with the Additional Element Status and Threshold In dpages. Can be
+used with an indexing option and/or \fI\-\-join\fR options.
+.TP
+\fB\-j\fR, \fB\-\-join\fR
+group elements from the Element Descriptor, Enclosure Status and Additional
+Element Status dpages. If this option is given twice then elements from the
+Threshold In dpage are also grouped. The order is dictated by the Configuration
+dpage.
+.br
+There can be a bewildering amount of information in the "join" output. The
+default is to output everything. Several additional options are provided to
+cut down the amount displayed. If the indexing options is given, only the
+matching elements and their associated fields are output. The \fI\-\-filter\fR
+option (see its description) can be added to reduce the amount of output.
+Also "\-\-page=aes" (or "\-p 0xa") can be added to suppress the output of
+rows that don't have a "aes" dpage component. See the INDEXES and DESCRIPTOR
+NAME, DEVICE SLOT NUMBER AND SAS ADDRESS sections below.
+.TP
+\fB\-l\fR, \fB\-\-list\fR
+This option is equivalent to \fI\-\-enumerate\fR. See that option.
+.TP
+\fB\-M\fR, \fB\-\-mask\fR
+When modifying elements, the default action is a read (status element),
+mask, modify (based on \fI\-\-clear=STR\fR or \fI\-\-set=STR\fR) then write
+back as the control element. The mask step is new in sg_ses version 1.98
+and is based on what is allowable (and in the same location) in draft SES\-3
+revision 6. Those masks may evolve, as they have in the past. This option
+re\-instates the previous logic which was to ignore the mask step. The
+default action (i.e. without this option) is to perform the mask step in
+the read\-mask\-modify\-write sequence.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+\fILEN\fR is placed in the ALLOCATION LENGTH field of the SCSI RECEIVE
+DIAGNOSTIC RESULTS commands sent by the utility. It represents the maximum
+size of data the SES device can return (in bytes). It cannot exceed 65535
+and defaults to 65532 (bytes). Some systems may not permit such large sizes
+hence the need for this option. If \fILEN\fR is less than 0 or greater than
+65535 then an error is generated. If \fILEN\fR is 0 then the default value
+is used, otherwise if it is less than 4 then it is ignored (and a warning is
+sent to stderr).
+.TP
+\fB\-n\fR, \fB\-\-nickname\fR=\fISEN\fR
+where \fISEN\fR is the new Subenclosure Nickname. Only the first 32
+characters (bytes) of \fISEN\fR are used, if more are given they are
+ignored. See the SETTING SUBENCLOSURE NICKNAME section below.
+.TP
+\fB\-N\fR, \fB\-\-nickid\fR=\fISEID\fR
+where \fISEID\fR is the Subenclosure identifier that the new
+Nickname (\fISEN\fR) will be applied to. So \fISEID\fR must be an existing
+Subenclosure identifier. The default value is 0 which is the
+main enclosure.
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPG\fR
+where \fIPG\fR is a dpage abbreviation or code (a number). If \fIPG\fR
+starts with a digit it is assumed to be in decimal unless prefixed by
+0x for hex. Valid range is 0 to 255 (0x0 to 0xff) inclusive. Default is
+dpage 'sdp' which is page_code 0 (i.e. "Supported Diagnostic Pages") if
+no other options are given.
+.br
+Page code 0xff or abbreviation "all" is not a real dpage (as the highest
+real dpage is 0x3f) but instead causes all dpages whose page code is 0x2f
+or less to be output. This can be used with either the \fI\-HHHH\fR or
+\fI\-rr\fR to send either hexadecimal ASCII or binary respectively to
+stdout.
+.br
+To list the available dpage abbreviations give "xxx" for \fIPG\fR; the same
+information can also be found with the \fI\-\-enumerate\fR option.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+this suppresses the number of warnings and messages output. The exit status
+of the utility is unaffected by this option.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+outputs the chosen status dpage in ASCII hex in a format suitable for a
+later invocation using the \fI\-\-data=\fR option. A dpage less its first
+4 bytes (page code and length) is output. When used twice (e.g. \fI\-rr\fR)
+the full dpage contents is output in binary to stdout.
+.br
+when \fI\-rr\fR is used together with the \fI\-\-data=\-\fR or
+\fI\-\-data=@FN\fR then stdin or file FN is decoded as a binary stream that
+continues to be read until an end of file (EOF). Once that data is read then
+the internal raw option is cleared to 0 so the output is not effected. So
+the \fI\-rr\fR option either changes how the input or output is treated,
+but not both.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only (e.g. in Unix with the O_RDONLY flag).
+The default is to open it read\-write.
+.TP
+\fB\-A\fR, \fB\-\-sas\-addr\fR=\fISA\fR
+this is an indexing method for SAS end devices (e.g. SAS disks). The utility
+will try to find the element or slot in the Additional Element Status dpage
+whose SAS address matches \fISA\fR. For a SAS disk or tape that SAS address
+is its target port identifier for the port connected to that element or slot.
+Most SAS disks and tapes have two such target ports, usually numbered
+consecutively.
+.br
+SATA devices in a SAS enclosure often receive "manufactured" target port
+identifiers from a SAS expander; typically will have a SAS address close to,
+but different from, the SAS address of the expander itself. Note that this
+manufactured target port identifier is different from a SATA disk's WWN.
+.br
+\fISA\fR is a hex number that is up to 8 digits long. It may have a
+leading '0x' or '0X' or a trailing 'h' or 'H'. This option is a medium level
+ indexing alternative to the low level \fI\-\-index=\fR options.
+See the DESCRIPTOR NAME, DEVICE SLOT NUMBER AND SAS ADDRESS section below.
+.TP
+\fB\-S\fR, \fB\-\-set\fR=\fISTR\fR
+Used to set an element field in the Enclosure Control or Threshold Out dpage.
+Must be used together with an indexing option to specify which element is to
+be changed. The Enclosure Control dpage is assumed if the \fI\-\-page=PG\fR
+option is not given. See the STR FORMAT and CLEAR, GET, SET sections below.
+.TP
+\fB\-s\fR, \fB\-\-status\fR
+will fetch dpage from the \fIDEVICE\fR via a SCSI RECEIVE DIAGNOSTIC RESULTS
+command (or from \fI\-\-data=@FN\fR). In the absence of other options that
+imply modifying a dpage (e.g.  \fI\-\-control\fR or \fI\-\-set=STR\fR) then
+\fI\-\-status\fR is assumed, except when the \fI\-\-data=\fR option is given.
+When the \fI\-\-data=\fR option is given there is no default action: either
+the \fI\-\-control\fR or this option must be given to distinguish between
+the two different ways that data will be treated.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity. For example when this option is given four
+times (in which case the short form is more convenient: '\-vvvv') then if
+the internal join array has been generated then it is output to stderr in
+a form suitable for debugging.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-w\fR, \fB\-\-warn\fR
+warn about certain irregularities with warnings sent to stderr. The join
+is a complex operation that relies on information from several dpages to be
+synchronized. The quality of SES devices vary and to be fair, the
+descriptions from T10 drafts and standards have been tweaked several
+times (see the EIIOE field) in order to clear up confusion.
+.SH INDEXES
+An enclosure can have information about its disk and tape drives plus other
+supporting components like power supplies spread across several dpages.
+Addressing a specific element (overall or individual) within a dpage is
+complicated. This section describes low level indexing (i.e. choosing a
+single element (or a group of related elements) from a large number of
+elements). If available, the medium level indexing described in the
+following section (DESCRIPTOR NAME, DEVICE SLOT NUMBER AND SAS ADDRESS)
+might be simpler to use.
+.PP
+The Configuration dpage is key to low level indexing: it contains a list
+of "type headers", each of which contains an Element type (e.g. Array
+Device Slot), a Subenclosure identifier (0 for the primary enclosure) and
+a "Number of possible elements". Corresponding to each type header, the
+Enclosure Status dpage has one "overall" element plus "Number of possible
+elements" individual elements all of which have the given Element type. For
+some Element types the "Number of possible elements" will be 0 so the
+Enclosure Status dpage has only one "overall" element corresponding to that
+type header. The Element Descriptor dpage and the Threshold (In and Out)
+dpages follow the same pattern as the Enclosure Status dpage.
+.PP
+The numeric index corresponding to the overall element is "\-1". If the
+Configuration dpage indicates a particular element type has "n" elements
+and n is greater than 0 then its indexes range from 0 to n\-1 .
+.PP
+The Additional Element Status dpage is a bit more complicated. It has
+entries for "Number of possible elements" of certain Element types. It
+does not have entries corresponding to the "overall" elements. To make
+the correspondence a little clearer each descriptor in this dpage optionally
+contains an "Element Index Present" (EIP) indicator. If EIP is set then each
+element's "Element Index" field refers to the position of the corresponding
+element in the Enclosure Status dpage.
+.PP
+Addressing a single overall element or a single individual element is done
+with two indexes: TI and II. Both are origin 0. TI=0 corresponds to the
+first type header entry which must be a Device Slot or Array Device Slot
+Element type (according to the SES\-2 standard). To address the corresponding
+overall instance, II is set to \-1, otherwise II can be set to the individual
+instance index. As an alternative to the type header index (TI), an Element
+type abbreviation (A) optionally followed by a number (e.g. "ps" refers to
+the first Power Supply Element type; "ps1" refers to the second) can be
+given.
+.PP
+One of two command lines variants can be used to specify indexes:
+\fI\-\-index=TIA,II\fR where \fITIA\fR is either an type header index (TI)
+or an Element type abbreviation (A) (e.g. "ps" or "ps1"). \fIII\fR is either
+an individual index or "\-1" to specify the overall element. The second
+variant is \fI\-\-index=IIA\fR where \fIIIA\fR is either an individual
+index (II) or an Element type abbreviation (A). When \fIIIA\fR is an
+individual index then the option is equivalent to \fI\-\-index=0,II\fR. When
+\fIIIA\fR is an Element type abbreviation then the option is equivalent to
+\fI\-\-index=A,\-1\fR.
+.PP
+Wherever an individual index is applicable, it can be replaced by an
+individual index range. It has the form: <first_ii>\-<last_ii>. For
+example: '3\-5' will select individual indexes 3, 4 and 5 .
+.PP
+To cope with vendor specific Element types (whose type codes should be in
+the range 128 to 255) the Element type code can be given as a number with
+a leading underscore. For example these are equivalent: \fI\-\-index=arr\fR
+and \fI\-\-index=_23\fR since the Array Device Slot Element type code is 23.
+Also \fI\-\-index=ps1\fR and \fI\-\-index=_2_1\fR are equivalent.
+.PP
+Another example: if the first type header in the Configuration dpage has
+has Array Device Slot Element type then \fI\-\-index=0,\-1\fR is
+equivalent to \fI\-\-index=arr\fR. Also \fI\-\-index=arr,3\fR is equivalent
+to \fI\-\-index=3\fR.
+.PP
+The \fI\-\-index=\fR options  can be used to reduce the amount of
+output (e.g. only showing the element associated with the second 12 volt
+power supply). They may also be used together with with the
+\fI\-\-clear=STR\fR, \fI\-\-get=STR\fR and \fI\-\-set=STR\fR options which
+are described in the STR section below.
+.SH DESCRIPTOR NAME, DEVICE SLOT NUMBER AND SAS ADDRESS
+The three options: \fI\-\-descriptor=DES\fR, \fI\-\-dev\-slot\-num=SN\fR
+and \fI\-\-sas\-addr=SA\fR allow medium level indexing, as an alternative
+to the low level \fI\-\-index=\fR options. Only one of the three options
+can be used in an invocation. Each of the three options implicitly set the
+\fI\-\-join\fR option since they need either the Element Descriptor dpage
+or the Additional Element Status dpage as well as the dpages needed by the
+\fI\-\-index=\fR option.
+.PP
+These medium level indexing options need support from the SES device and
+that support is optional. For example the \fI\-\-descriptor=DES\fR needs
+the Element Descriptor dpage provided by the SES device however that is
+optional. Also the provided descriptor names need to be useful, and having
+descriptor names which are all "0" is not very useful. Also some
+elements (e.g. overall elements) may not have descriptor names.
+.PP
+These medium level indexing options can be used to reduce the amount of
+output (e.g. only showing the elements related to device slot number 3).
+They may also be used together with with the \fI\-\-clear=STR\fR,
+\fI\-\-get=STR\fR and \fI\-\-set=STR\fR options which are described in the
+following section. Note that even if a field can be set (e.g. "do not
+remove" (dnr)) and that field can be read back with \fI\-\-get=STR\fR
+confirming that change, the disk array may still ignore it (e.g. because it
+does not have the mechanism to lock the disk drawer).
+.SH STR FORMAT
+The \fISTR\fR operands of the \fI\-\-clear=STR\fR, \fI\-\-get=STR\fR and
+\fI\-\-set=STR\fR options all have the same structure. There are two forms:
+.br
+      <acronym>[=<value>]
+.br
+      <start_byte>:<start_bit>[:<num_bits>][=<value>]
+.PP
+The <acronym> is one of a list of common fields (e.g. "ident" and "fault")
+that the utility converts internally into the second form. The <start_byte>
+is usually in the range 0 to 3, the <start_bit> must be in the range 0 to
+7 and the <num_bits> must be in the range 1 to 64 (default 1). The
+number of bits are read in the left to right sense of the element tables
+shown in the various SES draft documents. For example the 8 bits of
+byte 2 would be represented as 2:7:8 with the most significant bit being
+2:7 and the least significant bit being 2:0 .
+.PP
+The <value> is optional but is ignored if provided to \fI\-\-get=STR\fR.
+For \fI\-\-set=STR\fR the default <value> is 1 while for \fI\-\-clear=STR\fR
+the default value is 0 . <value> is assumed to be decimal, hexadecimal
+values can be given in the normal fashion.
+.PP
+The supported list of <acronym>s can be viewed by using the
+\fI\-\-enumerate\fR option twice (or "\-ee").
+.SH CLEAR, GET, SET
+The \fI\-\-clear=STR\fR, \fI\-\-get=STR\fR and \fI\-\-set=STR\fR options can
+be used up to 8 times in the same invocation. Any <acronym>s used in the
+\fISTR\fR operands must refer to the same dpage.
+.PP
+When multiple of these options are used (maximum: 8), they are applied in the
+order in which they appear on the command line. So if options contradict each
+other, the last one appearing on the command line will be enforced. When
+there are multiple \fI\-\-clear=STR\fR and \fI\-\-set=STR\fR options, then
+the dpage they refer to is only written after the last one.
+.SH DATA SUPPLIED
+This section describes the two scenarios that can occur when the
+\fI\-\-data=\fR option is given. These scenarios are the same irrespective
+of whether the argument to the \fI\-\-data=\fR option is a string of
+hex bytes on the command line, stdin (indicated by \fI\-\-data=\-\fR) or
+names a file (e.g. \fI\-\-data=@thresh_in_dpage.hex\fR).
+.PP
+The first scenario is flagged by the \fI\-\-control\fR option. It uses the
+supplied data to build a 'control' dpage that will be sent to the
+\fIDEVICE\fR using the SCSI SCSI SEND DIAGNOSTIC command. The supplied dpage
+data should not include its first 4 bytes. Those 4 bytes are added by this
+utility using the \fI\-\-page=PG\fR option with \fIPG\fR placed at byte
+offset 0). If needed, the \fI\-\-byte1=B1\fR option sets byte offset 1,
+else 0 is placed in that position. The number of bytes decoded from the data
+provided (i.e. its length) goes into byte offsets 2 and 3.
+.PP
+The second scenario is flagged by the \fI\-\-status\fR option. It decodes
+the supplied data assuming that it represents the response to one or more
+SCSI RECEIVE DIAGNOSTIC RESULTS commands. Those responses have typically
+been captured from some earlier invocation(s) of this utility. Those earlier
+invocations could use the '\-HHH' or '\-HHHH' option and file redirection to
+capture that response (or responses) in hexadecimal. The supplied dpage
+response data is decoded according to the other command line options. For
+example the \fI\-\-join\fR option could be given and that would require the
+data from multiple dpages typically:  Configuration, Enclosure status,
+Element descriptor and Additional element status dpages. If in doubt use
+\fI\-\-page=all\fR in the capture phase; having more dpages than needed
+is not a problem.
+.PP
+By default the user supplied data is assumed to be ASCII hexadecimal in
+lines that don't exceed 512 characters. Anything on a line from and
+including a hash mark ('#') to the end of line is ignored. An end of
+line can be a LF or CR,LF and blank lines are ignored. Each separated
+pair (or single) hexadecimal digits represent a byte (and neither a
+leading '0x' nor a trailing 'h' should be given). Separators are either
+space, tab, comma or end of line.
+.PP
+Alternatively binary can be used and this is flagged by the '\-rr' option.
+The \fI\-\-data=H,H...\fR form cannot use binary values for the 'H's, only
+ASCII hexadecimal. The other two forms (\fI\-\-data=\-\fR and
+\fI\-\-data=@FN\fR) may contain binary data. Note that when the '\-rr'
+option is used with \fI\-\-data=@FN\fR that it only changes the
+interpretation of the input data, it does not change the decoding and output
+representation.
+.SH CHANGING STATE
+This utility has various techniques for changing the state of a SES device.
+As noted above this is typically a read\-modify\-write type operation.
+Most modifiable dpages have a "status" (or "in") page that can be read, and
+a corresponding "control" (or "out") dpage that can be written back to change
+the state of the enclosure.
+.PP
+The lower level technique provided by this utility involves outputting
+a "status" dpage in hex with \fI\-\-raw\fR. Then a text editor can be used
+to edit the hex (note: to change an Enclosure Control descriptor the SELECT
+bit needs to be set). Next the control dpage data can fed back with the
+\fI\-\-data=H,H...\fR option together with the \fI\-\-control\fR option;
+the \fI\-\-byte1=B1\fR option may need to be given as well.
+.PP
+Changes to the Enclosure Control dpage (and the Threshold Out dpage) can be
+done at a higher level. This involves choosing a dpage (the default in this
+case is the Enclosure Control dpage). Next choose an individual or overall
+element index (or name it with its Element Descriptor string). Then give
+the element's name (e.g. "ident" for RQST IDENT) or its position within that
+element (e.g. in an Array Device Slot Control element RQST IDENT is byte 2,
+bit 1 and 1 bit long ("2:1:1")). Finally a value can be given, if not the
+value for \fI\-\-set=STR\fR defaults to 1 and for \fI\-\-clear=STR\fR
+defaults to 0.
+.SH SETTING SUBENCLOSURE NICKNAME
+The format of the Subenclosure Nickname control dpage is different from its
+corresponding status dpage. The status dpage reports all Subenclosure
+Nicknames (and Subenclosure identifier 0 is the main enclosure) while the
+control dpage allows only one of them to be changed. Therefore using the
+\fB\-\-data\fR option technique to change a Subenclosure nickname is
+difficult (but still possible).
+.PP
+To simplify changing a Subenclosure nickname the \fI\-\-nickname=SEN\fR and
+\fI\-\-nickid=SEID\fR options have been added. If the \fISEN\fR string
+contains spaces or other punctuation, it should be quoted: surrounded by
+single or double quotes (or the offending characters escaped). If the
+\fI\-\-nickid=SEID\fR is not given then a Subenclosure identifier of 0 is
+assumed. As a guard the \fI\-\-control\fR option must also be given. If
+the \fI\-\-page=PG\fR option is not given then \fI\-\-page=snic\fR is
+assumed.
+.PP
+When \fI\-\-nickname=SEN\fR is given then the Subenclosure Nickname Status
+dpage is read to obtain the Generation Code field. That Generation Code
+together with no more than 32 bytes from the Nickname (\fISEN\fR) and the
+Subenclosure Identifier (\fISEID\fR) are written to the Subenclosure Nickname
+Control dpage.
+.PP
+There is an example of changing a nickname in the EXAMPLES section below.
+.SH NVME ENCLOSURES
+Support has been added to sg_ses (actually, its underlying library) for
+NVMe (also known as NVM Express) Enclosures. It can be considered
+experimental in sg3_utils package version 1.43 and sg_ses version 2.34 .
+.PP
+This support is based on a decision by NVME\-MI (Management Interface)
+developers to support the SES\-3 standard. This was facilitated by adding
+NVME\-MI SES Send and SES Receive commands that tunnel dpage contents as
+used by SES.
+.SH NOTES
+This utility can be used to fetch arbitrary (i.e. non SES) dpages (using
+the SCSI READ DIAGNOSTIC command). To this end the \fI\-\-page=PG\fR and
+\fI\-\-hex\fR options would be appropriate. Non\-SES dpages can be sent to
+a device with the sg_senddiag utility.
+.PP
+The most troublesome part of the join operation is associating Additional
+Element Status descriptors correctly. At least one SES device vendor has
+misinterpreted the SES\-2 standard, specifically with its "element index"
+field interpretation. The code in this utility interprets the "element
+index" field as per the SES\-2 standard and if that yields an inappropriate
+Element type, adjusts its indexing to follow that vendor's
+misinterpretation. The SES\-3 drafts have introduced the EIIOE (Element
+Index Includes Overall Elements) bit which later became a 2 bit field to
+resolve this ambiguity. See the \fI\-\-eiioe=A_F\fR option.
+.PP
+In draft SES\-3 revision 5 the "Door Lock" element name was changed to
+the "Door" (and an OPEN field was added to the status element). As a
+consequence the former 'dl' element type abbreviation has been changed
+to 'do'.
+.PP
+Some RAID controllers hide SES device nodes from the host Operating System.
+It has been reported that some MegaRAID controllers do this and the
+following command is needed to expose them:
+.PP
+   perccli /cx set backplane expose=<on/off>
+.PP
+where perccli is Dell's version of BroadCom's (LSI) storcli utility.
+.PP
+There is a related command set called SAF\-TE (SCSI attached fault\-tolerant
+enclosure) for enclosure (including RAID) status and control. SCSI devices
+that support SAF\-TE report "Processor" peripheral device type (0x3) in their
+INQUIRY response. See the sg_safte utility in this package or the
+safte\-monitor utility on the Internet.
+.PP
+The internal join array is statically allocated and its size is controlled
+by the MX_JOIN_ROWS define. Its current value is 520.
+.SH EXAMPLES
+Examples can also be found at https://sg.danny.cz/sg/sg_ses.html
+.PP
+The following examples use Linux device names. For suitable device names
+in other supported Operating Systems see the sg3_utils(8) man page.
+.PP
+To view the supported dpages:
+.PP
+   sg_ses /dev/bsg/6:0:2:0
+.PP
+To view the Configuration Diagnostic dpage:
+.PP
+   sg_ses \-\-page=cf /dev/bsg/6:0:2:0
+.PP
+To view the Enclosure Status dpage:
+.PP
+   sg_ses \-\-page=es /dev/bsg/6:0:2:0
+.PP
+To get the (attached) SAS address of that device (which is held in the
+Additional Element Sense dpage (dpage 10)) printed on hex:
+.PP
+   sg_ses \-p aes \-D ArrayDevice07 \-G at_sas_addr \-H /dev/sg3
+.PP
+To collate the information in the Enclosure Status, Element Descriptor
+and Additional Element Status dpages the \fI\-\-join\fR option can be used:
+.PP
+   sg_ses \-\-join /dev/sg3
+.PP
+This will produce a lot of output. To filter out lines that don't contain
+much information add the \fI\-\-filter\fR option:
+.PP
+   sg_ses \-\-join \-\-filter /dev/sg3
+.PP
+Fields in the various elements of the Enclosure Control and Threshold dpages
+can be changed with the \fI\-\-clear=STR\fR and \fI\-\-set=STR\fR
+options. [All modifiable dpages can be changed with the \fI\-\-raw\fR and
+\fI\-\-data=H,H...\fR options.] The following example looks at making
+the "ident" LED (also called "locate") flash on "ArrayDevice07" which is a
+disk (or more precisely the carrier drawer the disk is in):
+.PP
+   sg_ses \-\-index=7 \-\-set=2:1:1 /dev/sg3
+.PP
+If the Element Descriptor diagnostic dpage shows that "ArrayDevice07" is
+the descriptor name associated with element index 7 then this invocation
+is equivalent to the previous one:
+.PP
+   sg_ses \-\-descriptor=ArrayDevice07 \-\-set=2:1:1 /dev/sg3
+.PP
+Further the byte 2, bit 1 (for 1 bit) field in the Array Device Slot Control
+element is RQST IDENT for asking a disk carrier to flash a LED so it can
+be located. In this case "ident" (or "locate") is accepted as an acronym
+for that field:
+.PP
+   sg_ses \-\-descriptor=ArrayDevice07 \-\-set=ident /dev/sg3
+.PP
+To stop that LED flashing:
+.PP
+   sg_ses \-\-dev\-slot\-num=7 \-\-clear=ident /dev/sg3
+.PP
+The above assumes the descriptor name 'ArrayDevice07' corresponds to device
+slot number 7.
+.PP
+Now for an example of a more general but lower level technique for changing
+a modifiable diagnostic dpage. The String (In and Out) diagnostics dpage is
+relatively simple (compared with the Enclosure Status/Control dpage). However
+the use of this lower level technique is awkward involving three steps: read,
+modify then write. First check the current String (In) dpage contents:
+.PP
+   sg_ses \-\-page=str /dev/bsg/6:0:2:0
+.PP
+Now the "read" step. The following command will send the contents of the
+String dpage (from byte 4 onwards) to stdout. The output will be in ASCII
+hex with pairs of hex digits representing a byte, 16 pairs per line,
+space separated. The redirection puts stdout in a file called "t":
+.PP
+   sg_ses \-\-page=str \-\-raw /dev/bsg/6:0:2:0 > t
+.PP
+Then with the aid of the SES\-3 document (in revision 3: section 6.1.6)
+use your favourite editor to change t. The changes can be sent to the
+device with:
+.PP
+   sg_ses \-\-page=str \-\-control \-\-data=\- /dev/bsg/6:0:2:0 < t
+.PP
+If the above is successful, the String dpage should have been changed. To
+check try:
+.PP
+   sg_ses \-\-page=str /dev/bsg/6:0:2:0
+.PP
+To change the nickname on the main enclosure:
+.PP
+   sg_ses \-\-nickname='1st enclosure' \-\-control /dev/bsg/6:0:2:0
+.PP
+To capture the whole state of an enclosure (from a SES perspective) for
+later analysis, this can be done:
+.PP
+   sg_ses \-\-page=all \-HHHH /dev/sg5 > enc_sg5_all.hex
+.PP
+Note that if there are errors or warnings they will be sent to stderr so
+they will appear on the command line (since only stdout is redirected).
+A text editor could be used to inspect enc_sg5_all.hex . If all looks in
+order at some later time, potentially on a different machine where
+enc_sg5_all.hex has been copied, a "join" could be done. Note that join
+reflects the state of the enclosure when the capture was done.
+.PP
+   sg_ses \-\-data=@enc_sg5_all.hex \-\-status \-\-join
+.SH EXIT STATUS
+The exit status of sg_ses is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2021 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq, sg_safte, sg_senddiag, sg_ses_microcode, sg3_utils (sg3_utils);
+.B safte\-monitor (Internet)
diff --git a/doc/sg_ses_microcode.8 b/doc/sg_ses_microcode.8
new file mode 100644
index 0000000..7070968
--- /dev/null
+++ b/doc/sg_ses_microcode.8
@@ -0,0 +1,279 @@
+.TH SG_SES_MICROCODE "8" "January 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_ses_microcode \- send microcode to a SCSI enclosure
+.SH SYNOPSIS
+.B sg_ses_microcode
+[\fI\-\-bpw=CS\fR] [\fI\-\-dry\-run\fR] [\fI\-\-ealsd\fR] [\fI\-\-help\fR]
+[\fI\-\-id=ID\fR] [\fI\-\-in=FILE\fR] [\fI\-\-length=LEN\fR]
+[\fI\-\-mode=MO\fR] [\fI\-\-non\fR] [\fI\-\-offset=OFF\fR]
+[\fI\-\-skip=SKIP\fR] [\fI\-\-subenc=MS\fR] [\fI\-\-tlength=TLEN\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility attempts to download microcode to an enclosure (or one of its
+sub\-enclosures) associated with the \fIDEVICE\fR. The process for doing
+this is defined in the SCSI Enclosure Services (SES) standards and drafts
+maintained by the T10 committee.
+.PP
+The process is to send one or more sequences containing a SCSI SEND
+DIAGNOSTIC command followed optionally by a RECEIVE DIAGNOSTIC RESULTS
+command. The former sends a Download microcode Control diagnostic
+page (dpage) and the latter fetches a Download microcode status dpage which
+can be viewed as a report on the former command.
+.PP
+The default action (i.e. when the \fI\-\-mode=MO\fR option is not given)
+is to fetch the Download microcode status dpage and decode it. This does
+not require the microcode (firmware) itself so the \fI\-\-in=FILE\fR option
+is not required.
+.PP
+The most recent reference for this utility is the draft SCSI Enclosure
+Services 3 (SES\-3) document T10/2149\-D Revision 7 at http://www.t10.org .
+Existing standards for SES and SES\-2 are ANSI INCITS 305\-1998 and ANSI
+INCITS 448\-2008 respectively.
+.PP
+Most other support for SES in this package (apart from downloading
+microcode) can be found in the sg_ses utility. Another way of downloading
+firmware to a SCSI device is with the WRITE BUFFER command defined in
+SPC\-4, see the sg_write_buffer utility.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-bpw\fR=\fICS\fR
+where \fICS\fR is the chunk size in bytes and should be a multiple of 4.
+This will be the maximum number of bytes sent per SEND DIAGNOSTIC command.
+So if \fICS\fR is less than the effective length of the microcode then
+multiple SEND DIAGNOSTIC commands are sent, each taking the next chunk
+from the read data and increasing the buffer offset field in the Download
+microcode control dpage by the appropriate amount. The default is
+a chunk size of 0 which is interpreted as a very large number hence only
+one SEND DIAGNOSTIC command will be sent.
+.br
+The number in \fICS\fR can optionally be followed by ",act" or ",activate".
+In this case after the microcode has been successfully sent to the
+\fIDEVICE\fR, an additional Download microcode control dpage with its mode
+set to "Activate deferred microcode" [0xf] is sent.
+.TP
+\fB\-d\fR, \fB\-\-dry\-run\fR
+the actual calls to perform SEND DIAGNOSTIC and RECEIVE DIAGNOSTIC RESULTS
+commands are skipped when this option is given. No SCSI commands are sent
+to the \fIDEVICE\fR but it is still opened and is required to be given.
+A dummy device such as /dev/null (in Unix) can be used.
+.br
+This utility expects a "sensible" response to the RECEIVE DIAGNOSTIC RESULTS
+command it sends (and will abort if it doesn't receive one). So this option
+supplies dummy responses with one primary enclosure and three
+sub\-enclosures. The dummy responses include good status values.
+.TP
+\fB\-e\fR, \fB\-\-ealsd\fR
+exit after last SEND DIAGNOSTIC command. A SES device should not start its
+firmware update immediately after the last received "chunk" of its firmware.
+Rather it should wait till at least one RECEIVE DIAGNOSTIC RESULTS command
+is sent to give the device a chance to report any error. However some
+devices do start the firmware update immediately which causes the trailing
+RECEIVE DIAGNOSTIC RESULTS command to be held up and often be aborted with
+a "target reset" error.
+.br
+This option causes the trailing RECEIVE DIAGNOSTIC RESULTS command to be
+skipped. This option would be typically used with the \fI\-\-bpw=CS\fR
+option.
+.br
+Prior to version 1.10 of this utility [20180112] this (i.e. skipping
+the last RECEIVE DIAGNOSTIC RESULTS command) was the default action.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit. If used multiple times also prints
+the mode names and their acronyms.
+.TP
+\fB\-i\fR, \fB\-\-id\fR=\fIID\fR
+this option sets the BUFFER ID field in the Download microcode control
+dpage. \fIID\fR is a value between 0 (default) and 255 inclusive.
+.TP
+\fB\-I\fR, \fB\-\-in\fR=\fIFILE\fR
+read data from file \fIFILE\fR that will be sent with the SEND DIAGNOSTIC
+command.  If \fIFILE\fR is '\-' then stdin is read until an EOF is
+detected (this is the same action as \fI\-\-raw\fR). Data is read from
+the beginning of \fIFILE\fR except in the case when it is a regular file
+and the \fI\-\-skip=SKIP\fR option is given.
+.TP
+\fB\-l\fR, \fB\-\-length\fR=\fILEN\fR
+where \fILEN\fR is the length, in bytes, of data to be written to the device.
+If not given (and the length cannot be deduced from \fI\-\-in=FILE\fR or
+\fI\-\-raw\fR) then defaults to zero. If the option is given and the length
+deduced from \fI\-\-in=FILE\fR or \fI\-\-raw\fR is less (or no data is
+provided), then bytes of 0xff are used as fill bytes.
+.TP
+\fB\-m\fR, \fB\-\-mode\fR=\fIMO\fR
+this option sets the MODE. \fIMO\fR is a value between
+0 (which is dmc_status and the default) and 255 inclusive. Alternatively
+an abbreviation can be given. See the MODES section below. To list the
+available mode abbreviations at run time give an invalid
+one (e.g. '\-\-mode=xxx') or use the '\-h' option.
+.TP
+\fB\-N\fR, \fB\-\-non\fR
+allow for non\-standard implementations that reset their Download microcode
+engine after a RECEIVE DIAGNOSTIC RESULTS command with the Download microcode
+status dpage is sent. When this option is given sending that command and
+dpage combination is avoided unless an error has already occurred.
+.TP
+\fB\-o\fR, \fB\-\-offset\fR=\fIOFF\fR
+this option sets the BUFFER OFFSET field in the Download microcode control
+dpage. \fIOFF\fR is a value between 0 (default) and 2**32\-1 . It is a
+byte offset. This option is ignored (and a warning sent to stderr) if the
+\fI\-\-bpw=CS\fR option is also given.
+.TP
+\fB\-s\fR, \fB\-\-skip\fR=\fISKIP\fR
+this option is only active when \fI\-\-in=FILE\fR is given and \fIFILE\fR is
+a regular file, rather than stdin. Data is read starting at byte offset
+\fISKIP\fR to the end of file (or the amount given by \fI\-\-length=LEN\fR).
+If not given the byte offset defaults to 0 (i.e. the start of the file).
+.TP
+\fB\-S\fR, \fB\-\-subenc\fR=\fISEID\fR
+\fISEID\fR is the sub\-enclosure identify. It defaults to 0 which is the
+primary enclosure identifier.
+.TP
+\fB\-t\fR, \fB\-\-tlength\fR=\fITLEN\fR
+\fITLEN\fR is the total length in bytes of the microcode to be (or being)
+downloaded. It defaults to 0 which is okay in most cases. This option only
+comes into play when \fITLEN\fR is greater than \fILEN\fR. In this case
+\fITLEN\fR is sent to the SES \fIDEVICE\fR so that it knows when it only
+receives \fILEN\fR bytes from this invocation, that it should expect more
+to be sent in the near future (e.g. by another invocation). This option
+is only needed when sections of microcode are being sent in separate
+invocations of this utility (e.g. the microcode is spread across two files).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH MODES
+Following is a list accepted by the \fIMO\fR argument of this utility.
+First shown is an acronym followed in square brackets by the corresponding
+decimal and hex values that may also be given for \fIMO\fR.
+.TP
+dmc_status  [0, 0x0]
+Use RECEIVE DIAGNOSTIC RESULTS to fetch the Download microcode status dpage
+and print it out.
+.TP
+dmc_offs  [6, 0x6]
+Download microcode with offsets and activate.
+.TP
+dmc_offs_save  [7, 0x7]
+Download microcode with offsets, save, and activate.
+.TP
+dmc_offs_defer  [14, 0xe]
+Download microcode with offsets, save, and defer activate.
+.TP
+activate_mc  [15, 0xf]
+Activate deferred microcode. There is no follow\-up RECEIVE DIAGNOSTIC
+RESULTS to fetch the Download microcode status dpage since the \fIDEVICE\fR
+might be resetting.
+.PP
+Apart from dmc_status, these are placed in the Download microcode mode
+field in the Download microcode control dpage. In the case of dmc_status
+the Download microcode status dpage is fetched with the RECEIVE DIAGNOSTIC
+RESULTS command and decoded.
+.SH WHEN THE DOWNLOAD FAILS
+Firstly, if it succeeds, this utility should stay silent and return.
+Typically vendors will change the "revision" string (which is 4 characters
+long) whenever they release new firmware. That can be seen in the response
+to a SCSI INQUIRY command, for example by using the sg_inq utility.
+It is possible that the device needs to be power cycled before the new
+microcode becomes active. Also if mode dmc_offs_defer [0xe] is used to
+download the microcode, then another invocation with activate_mc may
+be needed.
+.PP
+If something goes wrong, there will typically be messages printed out
+by this utility. The first thing to check is the microcode (firmware)
+file itself. Is it designed for the device model; has it been corrupted,
+and if downgrading (i.e. trying to reinstate older firmware), does
+the vendor allow that?
+.PP
+Getting new firmware on a device is a delicate operation that is not
+always well defined by T10's standards and drafts. One might speculate
+that they are deliberately vague. In testing this utility one vendor's
+interpretation of the standard was somewhat surprising. The \fI\-\-non\fR
+option was added to cope with their interpretation. So if the above
+suggestions don't help, try adding the \fI\-\-non\fR option.
+.SH NOTES
+This utility can handle a maximum size of 128 MB of microcode which
+should be sufficient for most purposes. In a system that is memory
+constrained, such large allocations of memory may fail.
+.PP
+The user should be aware that most operating systems have limits on the
+amount of data that can be sent with one SCSI command. In Linux this
+depends on the pass through mechanism used (e.g. block SG_IO or the sg
+driver) and various setting in sysfs in the Linux lk 2.6/3
+series (e.g. /sys/block/sda/queue/max_sectors_kb). Devices (i.e. logical
+units) also typically have limits on the maximum amount of data they can
+handle in one command. These two limitations suggest that modes
+containing the word "offset" together with the \fI\-\-bpw=CS\fR option
+are required as firmware files get larger and larger. And \fICS\fR
+can be quite small, for example 4096 bytes, resulting in many SEND
+DIAGNOSTIC commands being sent.
+.PP
+The exact error from the non\-standard implementation was a sense key of
+ILLEGAL REQUEST and an asc/ascq code of 0x26,0x0 which is "Invalid field in
+parameter list". If that is seen try again with the \fI\-\-non\fR option.
+.PP
+Downloading incorrect microcode into a device has the ability to render
+that device inoperable. One would hope that the device vendor verifies
+the data before activating it.
+.PP
+A long (operating system) timeout of 7200 seconds is set on each SEND
+DIAGNOSTIC command.
+.PP
+All numbers given with options are assumed to be decimal.
+Alternatively numerical values can be given in hexadecimal preceded by
+either "0x" or "0X" (or has a trailing "h" or "H").
+.SH EXAMPLES
+If no microcode/firmware file is given then this utility fetches and decodes
+the Download microcode status dpage which could possibly show another
+initiator in the process of updating the microcode. Even if that is
+happening, fetching the status page should not cause any problems:
+.PP
+  sg_ses_microcode /dev/sg3
+.br
+Download microcode status diagnostic page:
+.br
+  number of secondary sub\-enclosures: 0
+.br
+  generation code: 0x0
+.br
+   sub\-enclosure identifier: 0 [primary]
+.br
+     download microcode status: No download microcode operation in progress [0x0]
+.br
+     download microcode additional status: 0x0
+.br
+     download microcode maximum size: 1048576 bytes
+.br
+     download microcode expected buffer id: 0x0
+.br
+     download microcode expected buffer id offset: 0
+.PP
+The following sends new microcode/firmware to an enclosure. Sending a 1.5 MB
+file in one command caused the enclosure to lock up temporarily and did
+not update the firmware. Breaking the firmware file into 4 KB chunks (an
+educated guess) was more successful:
+.PP
+  sg_ses_microcode \-b 4k \-m dmc_offs_save \-I firmware.bin /dev/sg4
+.PP
+The firmware update occurred in the following enclosure power cycle. With
+a modern enclosure the Extended Inquiry VPD page gives indications in which
+situations a firmware upgrade will take place.
+.SH EXIT STATUS
+The exit status of sg_ses_microcode is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2014\-2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_ses, sg_write_buffer, sg_inq(sg3_utils)
diff --git a/doc/sg_start.8 b/doc/sg_start.8
new file mode 100644
index 0000000..1b251c1
--- /dev/null
+++ b/doc/sg_start.8
@@ -0,0 +1,283 @@
+.TH SG_START "8" "April 2021" "sg3_utils\-1.47" SG3_UTILS
+.SH NAME
+sg_start \- send SCSI START STOP UNIT command: start, stop, load or eject
+medium
+.SH SYNOPSIS
+.B sg_start
+[\fI0\fR] [\fI1\fR] [\fI\-\-eject\fR] [\fI\-\-help\fR] [\fI\-\-fl=FL\fR]
+[\fI\-\-immed\fR] [\fI\-\-load\fR] [\fI\-\-loej\fR] [\fI\-\-mod=PC_MOD\fR]
+[\fI\-\-noflush\fR] [\fI\-\-pc=PC\fR] [\fI\-\-readonly\fR] [\fI\-\-start\fR]
+[\fI\-\-stop\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.PP
+.B sg_start
+[\fI\-\-eject\fR] [\fI\-\-fl=FL\fR] [\fI\-i\fR] [\fI\-\-imm=0|1\fR]
+[\fI\-\-load\fR] [\fI\-\-loej\fR] [\fI\-\-mod=PC_MOD\fR] [\fI\-\-noflush\fR]
+[\fI\-\-pc=PC\fR] [\fI\-r\fR] [\fI\-\-start\fR] [\fI\-\-stop\fR] [\fI\-v\fR]
+[\fI\-V\fR] [\fI0|1\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+sg_start sends a SCSI START STOP UNIT command to the \fIDEVICE\fR with
+the selected options. The most used options are \fI\-\-stop\fR to spin
+down a disk and \fI\-\-start\fR to spin up a disk. Using \fI\-\-start\fR
+on a disk that is already spinning is harmless. There is also finer grain
+control with "power condition": active, idle or standby. This is set
+with the \fI\-\-pc=PC\fR option. In some contexts the "stop" state can
+be considered an additional power condition.
+.PP
+Devices that contain removable media such as cd/dvds can use the
+\fI\-\-loej\fR option to load the medium when used in conjunction
+with \fI\-\-start\fR (i.e. load medium then spin up). Alternatively
+\fI\-\-loej\fR may be used to eject the medium when used in conjunction
+with \fI\-\-stop\fR (i.e. spin down then eject medium). More simply, the
+loading or ejecting of a removable medium can be requested with the
+\fI\-\-load\fR or \fI\-\-eject\fR' option.
+.PP
+If no option or argument is given then a \fI\-\-start\fR is assumed; as the
+utility's name suggests.
+.PP
+This utility supports two command line syntaxes, the preferred one is
+shown first in the synopsis and explained in this section. A later
+section on the old command line syntax outlines the second group of
+options.
+.PP
+Linux note: best not to use a standard block device name (e.g. /dev/sdc)
+with the \fI\-\-stop\fR option. Use a sg or bsg device node instead (see
+lsscsi(8) ). The block layer will sometimes notice the disk spinning
+down and decide: "that's not right" and spin it up again!
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB0\fR
+same action as \fI\-\-stop\fR.
+.TP
+\fB1\fR
+same action as \fI\-\-start\fR.
+.TP
+\fB\-e\fR, \fB\-\-eject\fR
+stop the medium and eject it from the drive. Only appropriate for a
+device with removable medium. Might be ignored (prevented), see below.
+Note, this is an operation that can be done on a tape drive or CD/DVD/BD
+player, not on a hard disk or SSD!
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-f\fR, \fB\-\-fl\fR=\fIFL\fR
+sets the format layer number for the disc to "jump" to (defined in MMC\-5).
+Values of \fIFL\fR can be 0 to 3. When this option is chosen, the FL, LoEj
+and Start bits are set in the cdb as required by MMC\-5; thus the user does
+not need to set the \fI\-\-start\fR and/or \fI\-\-load\fR options.
+.TP
+\fB\-i\fR, \fB\-\-immed\fR
+sets the IMM bit on the START STOP UNIT command so this utility will
+return immediately and not wait for the media to complete the requested
+action. The default is to wait until the media to complete the requested
+action before returning.
+.TP
+\fB\-l\fR, \fB\-\-load\fR
+load the medium in the drive and start it. Only appropriate for a removable
+medium.
+.TP
+\fB\-L\fR, \fB\-\-loej\fR
+sets the LOEJ bit on the START STOP UNIT command. This loads the media when
+the unit is started or eject it when the unit is stopped (i.e.  works in
+conjunction with START bit in cdb). This option is ignored if 'pc > 0'.
+Default is off (i.e. don't attempt to load or eject media). If a start/start
+indication is not given (i.e. neither \fI\-\-start\fR nor \fI\-\-stop\fR)
+and this option is given then a load and start action is assumed.
+.TP
+\fB\-m\fR, \fB\-\-mod\fR=\fIPC_MOD\fR
+where \fIPC_MOD\fR is the 'power condition modifier' value. 0 to 15 (inclusive)
+are valid and 0 is the default. This  'power condition modifier' field in the
+cdb was added after sbc3r13.
+.TP
+\fB\-n\fR, \fB\-\-noflush\fR
+do not perform a flush to media (e.g. like SYNCHRONIZE CACHE does) before
+a variant of this utility that limits access to the media. Using the
+\fB\-\-stop\fR option is an example of something that limits access to the
+media. This 'noflush' field in the cdb was added after sbc3r13.
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option.
+.TP
+\fB\-p\fR, \fB\-\-pc\fR=\fIPC\fR
+where \fIPC\fR is the 'power conditions' value. 0 to 15 (inclusive) are valid.
+Default value is 0. When '\-\-pc=0' then \fB\-\-eject\fR, \fB\-\-load\fR,
+\fB\-\-loej\fR, \fB\-\-start\fR and \fB\-\-stop\fR are active. Some common
+values are 1 for the "active" power condition (SBC); 2 for the idle power
+condition; 3 for the standby power condition; 5 for sleep power
+condition (MMC); 7 for LU_CONTROL (SBC), 0xa (decimal 10) for
+FORCE_IDLE_0 (SBC) and 0xb (decimal 11) for FORCE_STANDBY_0 (SBC). See recent
+SBC\-3, MMC\-5 and SAS drafts at www.t10.org for more information.
+.TP
+\fB\-r\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR in read\-only mode. Maybe required in Linux to stop a
+nuisance spin\-up if the \fIDEVICE\fR is an ATA disk. The nuisance spin\-up
+may occur at the end of this command negating the effect of the
+\fI\-\-stop\fR option.
+.TP
+\fB\-s\fR, \fB\-\-start\fR
+start (spin\-up) the \fIDEVICE\fR. This sets the START bit in the cdb. Using
+this option on an already started device is harmless. In the absence of
+other options, this option defaults (i.e. set the START cdb bit).
+.TP
+\fB\-S\fR, \fB\-\-stop\fR
+stop (spin\-down) the \fIDEVICE\fR. This clears the START bit in the cdb.
+This operation is typically done on a hard disk or SSD. In the case of a
+SSD it will be placed in low power mode and may need a start operation
+later before normal IO can resume.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity. Can be used multiple times.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string then exit.
+.SH NOTES
+To avoid confusion, only one of \fI0\fR, \fI1\fR \fI\-\-eject\fR,
+\fI\-\-load\fR, \fI\-\-start\fR and \fI\-\-stop\fR should be given.
+.PP
+There is an associated "power condition" mode page (0x1a) in which timer
+values can be set for transitioning to either idle or standby state after
+a period of inactivity. The sdparm utility can be used to view the power
+condition mode page and if required change it. If a \fIDEVICE\fR is in either
+idle or standby power condition state then a REQUEST SENSE command (see
+the sg_requests utility) should yield a sense key of "no sense" and an
+additional sense code of "Low power condition on" on recent SCSI devices.
+.PP
+Ejection of removable media (e.g. 'sg_start \-\-eject /dev/hdd' where
+the \fIDEVICE\fR is an ATAPI cd/dvd drive) may be prevented by a prior
+SCSI PREVENT ALLOW MEDIUM REMOVAL command (see sg_prevent). In this
+case this utility should fail with an error generated by the device:
+illegal request / medium removal prevented. This can be overridden
+using sg_prevent or, for example, 'sdparm \-\-command=unlock /dev/hdd'.
+.PP
+The SCSI TEST UNIT READY command can be used to find out whether a
+\fIDEVICE\fR is ready to transfer data. If rotating media is stopped or
+still coming up to speed, then the TEST UNIT READY command will yield
+a "not ready" sense key and an more informative additional sense
+code. See the sg_turs utility.
+.PP
+In the 2.4 series of Linux kernels the \fIDEVICE\fR must be a SCSI
+generic (sg) device. In the 2.6 series block devices (e.g. SCSI disks
+and DVD drives) can also be specified. For example "sg_start 0 /dev/sda"
+will work in the 2.6 series kernels.
+.PP
+In the Linux 2.6 series, especially with ATA disks, using this utility
+to stop (spin down) a disk may not be sufficient and other mechanisms
+will start the disk again some time later. The user might additionally
+mark the disk as "offline" with 'echo offline > /sys/block/sda/device/state'
+where sda is the block name of the disk. To restart the disk "offline"
+can be replaced with "running". Note that once the 'state' is set to
+offline, no SCSI commands can be sent to the device until it is set back
+to running. Also stopping a disk via a pass\-through
+interface (e.g. /dev/sg1 or /dev/bsg/1:0:0:0) may reduce unwanted side
+effects (such as restarting it again when this utility completes).
+.SH EXIT STATUS
+The exit status of sg_start is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.PP
+Note that the action of \fI\-\-loej\fR is slightly different in the older
+interface: when neither \fI\-\-start\fR nor \fI\-\-stop\fR (nor proxies
+for them) are given, \fI\-\-loej\fR performs an eject operation. In the
+same situation the newer interface will perform a load operation.
+.PP
+Earlier versions of sg_start had a '\-s' option to perform a SYNCHRONIZE
+CACHE command before the START STOP UNIT command was issued. According to
+recent SBC\-2 drafts this is done implicitly if required. Hence the '\-s'
+option has been dropped.
+.PP
+All options, other than '\-v' and '\-V', can be given with a single "\-".
+For example: "sg_start \-stop /dev/sda" and "sg_start \-\-stop /dev/sda"
+are equivalent. The single "\-" form is for backward compatibility.
+.TP
+\fB0\fR
+stop (spin\-down) \fIDEVICE\fR.
+.TP
+\fB1\fR
+start (spin\-up) \fIDEVICE\fR.
+.TP
+\fB\-\-eject\fR
+stop the medium and eject it from the drive.
+.TP
+\fB\-\-fl\fR=\fIFL\fR
+sets the format layer number for the disc to "jump" to (defined in MMC\-5).
+.TP
+\fB\-i\fR
+sets the IMM bit on the START STOP UNIT command so this utility will return
+immediately and not wait for the media to spin down. Same effect
+as '\-\-imm=1'. The default action (without this option or a '\-\-imm=1'
+option) is to wait until the media spins down before returning.
+.TP
+\fB\-\-imm\fR=\fI0|1\fR
+when the immediate bit is 1 then this utility returns immediately after the
+\fIDEVICE\fR has received the command. When this option is 0 (the default)
+then the utility returns once the command has completed its action (i.e. it
+waits until the device is started or stopped).
+.TP
+\fB\-\-load\fR
+load the medium in the drive and start it.
+.TP
+\fB\-\-loej\fR
+sets the LOEJ bit in the START STOP UNIT cdb. When a "start" operation is
+indicated, then a load and start is performed. When a "stop" operation is
+indicated, then a stop and eject is performed. When neither a "start"
+or "stop" operation is indicated does a stop and eject. [Note that the last
+action differs from the new interface in which the option of this name
+defaults to load and start.]
+.TP
+\fB-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-\-mod\fR=\fIPC_MOD\fR
+where \fIPC_MOD\fR is the 'power condition modifier' value. 0 to 15 (inclusive)
+are valid and 0 is the default. This field was added after sbc3r13.
+.TP
+\fB\-\-noflush\fR
+do not perform a flush to media (e.g. like SYNCHRONIZE CACHE does) before
+a variant of this utility that limits access to the media. Using the
+\fB\-\-stop\fR option is an example of something that limits access to the
+media. This field was added after sbc3r13.
+.TP
+\fB\-\-pc\fR=\fIPC\fR
+where \fIPC\fR is the 'power condition' value (in hex). 0 to f (inclusive)
+are valid. Default value is 0.
+.TP
+\fB\-r\fR
+see the \fI\-\-readonly\fR option above. May be useful for ATA disks.
+.TP
+\fB\-\-start\fR
+start (spin\-up) \fIDEVICE\fR.
+.TP
+\fB\-\-stop\fR
+stop (spin\-down) \fIDEVICE\fR. Same meaning as "0" argument.
+.TP
+\fB\-v\fR
+verbose: outputs SCSI command in hex to console before with executing
+it. '\-vv' and '\-vvv' are also accepted yielding greater verbosity.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHOR
+Written by K. Garloff and D. Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2002\-2021 Kurt Garloff, Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_prevent(sg3_utils), sg_requests(sg3_utils), sg_turs(sg3_utils)
+.B sdparm(sdparm), lsscsi(lsscsi)
diff --git a/doc/sg_stpg.8 b/doc/sg_stpg.8
new file mode 100644
index 0000000..5594878
--- /dev/null
+++ b/doc/sg_stpg.8
@@ -0,0 +1,122 @@
+.TH SG_STPG "8" "January 2014" "sg3_utils\-1.38" SG3_UTILS
+.SH NAME
+sg_stpg \- send SCSI SET TARGET PORT GROUPS command
+.SH SYNOPSIS
+.B sg_stpg
+[\fI\-\-active\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-offline\fR]
+[\fI\-\-optimized\fR] [\fI\-\-raw\fR] [\fI\-\-standby\fR]
+[\fI\-\-state=S,S...\fR] [\fI\-\-tp=P,P...\fR] [\fI\-\-unavailable\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send a SCSI SET TARGET PORT GROUPS command to \fIDEVICE\fR. This utility
+has different modes depending on whether the \fI\-\-tp=\fR option is given.
+.PP
+If \fI\-\-tp=\fR is given then the SET TARGET PORT GROUPS command parameter
+block is built with a descriptor for each element in the list given to
+\fI\-\-tp=\fR. The corresponding asymmetric access state value is either
+taken from the \fI\-\-state=\fR list or, if that is not given, from one
+of the explicit state options (e.g. \fI\-\-unavailable\fR), used repeatedly
+if required.
+.PP
+If \fI\-\-tp=\fR is not given then a sequence of SCSI commands are sent to
+the \fIDEVICE\fR leading up to the SET TARGET PORT GROUPS command. First an
+INQUIRY is sent to fetch the device identification VPD page to find
+the (primary) target port group associated with \fIDEVICE\fR. Then a REPORT
+TARGET PORT GROUPS command is issued to find the current state and
+whether a transition to the requested state is supported. If so the
+SET TARGET PORT GROUPS command is sent.
+.PP
+Target port group access is described in SPC\-4 found at www.t10.org
+in sections 5.8 and 5.16 (in rev 36e dated 2012/8/24). The SET TARGET PORT
+GROUPS command is also described in section 6.45 of that document.
+.PP
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-a\fR, \fB\-\-active\fR
+set active/non\-optimized state.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output response to the REPORT TARGET PORT GROUPS command in hex then exit.
+.TP
+\fB\-O\fR, \fB\-l\fR, \fB\-\-offline\fR
+set offline state. This is the appropriate state to set a target port
+to prior to removing the device.  Note that a relative target port identifier
+should be given with this state (rather than a target port group identifier
+that all other states take).
+.TP
+\fB\-o\fR, \fB\-\-optimized\fR
+set active/optimized state. If no other state options or \fI\-\-tp=\fR
+option are given then active/optimized is the default state.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response to the REPORT TARGET PORT GROUPS command in binary to stdout
+then exit.
+.TP
+\fB\-s\fR, \fB\-\-standby\fR
+set standby state. Port group shall accept those commands listed
+for "unavailable" state plus LOG SELECT/SENSE, MODE SELECT/SENSE, RECEIVE
+DIAGNOSTIC RESULTS, SEND DIAGNOSTIC, PERSISTENT RESERVE IN/OUT commands.
+.TP
+\fB\-S\fR, \fB\-\-state\fR=\fIS,S...\fR
+specifies a comma separated list (one element of more) of states. Either
+a number or an abbreviation can be given. A number is assumed to be a
+decimal number unless it is prefixed by "0x" or has a trailing "h" in
+which case a hexadecimal value is assumed. Only the values 0, 1, 2, 3
+or 14 are accepted. The accepted abbreviations are "an", "ao", "o", "s"
+or "u"; which represent active/non\-optimized(1), active/optimized(0),
+offline(14), standby(2) or unavailable(3) respectively.
+.TP
+\fB\-t\fR, \fB\-\-tp\fR=\fIP,P...\fR
+specifies a comma separated list (one element of more). Each elements is
+either a target port group identifier (when the corresponding state is
+other than "offline") or a relative target port identifier (when the
+corresponding state is "offline"). Each element is assumed to be a
+decimal number unless it is prefixed by "0x" or has a trailing "h" in
+which case a hexadecimal value is assumed.
+.TP
+\fB\-u\fR, \fB\-\-unavailable\fR
+set unavailable state. Port group shall only accept INQUIRY, REPORT LUNS,
+REPORT/SET TARGET PORT GROUPS, REQUEST SENSE and READ/WRITE BUFFER commands.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+The SET TARGET PORT GROUPS command should be supported whenever the TPGS
+value in a standard INQUIRY response is 2 or 3. [View with sg_inq utility.]
+.PP
+Notice that the offline state is termed as a "secondary target port
+asymmetric access state" and takes a relative target port identifier (i.e.
+acts on a single target port). All the other states are termed as "primary
+target port asymmetric access states" and each takes a target port group
+identifier (i.e. acts on one or more target ports).
+.PP
+When \fI\-\-tp=\fR is given then the same number of elements should be
+given to the \fI\-\-state=\fR option. If more than one list element is
+given to \fI\-\-tp=\fR and an equal number of elements is _not_ given
+to the \fI\-\-state=\fR option, then if only one state is specified
+then it is repeated.
+.SH EXIT STATUS
+The exit status of sg_stpg is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2007\-2014 Hannes Reinecke, Christophe Varoqui and Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq, sg_rtpg (sg3_utils)
diff --git a/doc/sg_stream_ctl.8 b/doc/sg_stream_ctl.8
new file mode 100644
index 0000000..18d9641
--- /dev/null
+++ b/doc/sg_stream_ctl.8
@@ -0,0 +1,117 @@
+.TH SG_STREAM_CTL "8" "March 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_stream_ctl \- send SCSI STREAM CONTROL or GET STREAM STATUS command
+.SH SYNOPSIS
+.B sg_stream_ctl
+[\fI\-\-brief\fR] [\fI\-\-close\fR] [\fI\-\-ctl=CTL\fR] [\fI\-\-get\fR]
+[\fI\-\-help\fR] [\fI\-\-id=SID\fR] [\fI\-\-maxlen=LEN\fR] [\fI\-\-open\fR]
+[\fI\-\-readonly\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI STREAM CONTROL or GET STREAM STATUS command to the \fIDEVICE\fR.
+These commands, together with WRITE STREAM(16 and 32) and several fields in
+the Block Limits Extension VPD page [0xb7] support the streams concept.
+The stream commands were added in SBC\-4 draft 8 (September 2015).
+.PP
+Both STREAM CONTROL and GET STREAM STATUS commands expect data from the
+\fIDEVICE\fR (referred to as 'data\-in'). In the case of STREAM CONTROL
+only the 'open' (STR_CTL<\-\-0x1) actually needs the data\-in as it contains
+the "Assigned stream id" if the open was successful. The assigned stream
+id should be used by subsequent WRITE STREAM commands and ultimately
+by the STREAM CONTROL close (STR_CTL<\-\-0x2). Valid stream ids are between
+1 and 65535 inclusive.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-b\fR, \fB\-\-brief\fR
+this option reduces the output of the GET STREAM STATUS command to just
+one number (in decimal) per line sent to stdout. Those numbers are the
+currently open stream ids. If an error occurs then \-1 is sent to stdout
+and error related messages are sent to stderr. The default is to print more
+words (and fields) from the GET STREAM STATUS response.
+.TP
+\fB\-c\fR, \fB\-\-close\fR
+selects the STREAM CONTROL command and sets STR_CTL<\-\-0x2 (i.e. 'close').
+The \fI\-\-id=SID\fR option should also be given because it defaults to 0
+which is not a valid stream id.
+.TP
+\fB\-C\fR, \fB\-\-ctl\fR=\fICTL\fR
+\fICTL\fR is the value placed in the STR_CTL field of the STREAM CONTROL
+command (cdb). It is a two bit field so has 4 variants: 0 and 3 are reserved;
+1 opens are new stream and 2 closes the given stream id. '\-\-ctl=1' is
+equivalent to '\-\-open' while '\-\-ctl=2' is equivalent to '\-\-close'.
+.TP
+\fB\-g\fR, \fB\-\-get\fR
+selects the GET STREAM STATUS command. If the \fI\-\-id=SID\fR option is
+also given the the response starts lists open stream ids from and including
+\fISID\fR. If the \fI\-\-id=SID\fR option is not given (or \fISID\fR is 0)
+then all open stream id will be returned in the response (data\-in) as long
+as the allocation length (defaults to 248 bytes which can be overridden by
+the \fI\-\-maxlen=LEN\fR option) is long enough. This is the default action
+of this utility (i.e. GET STREAM STATUS command) if no "selecting" options
+are given.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-i\fR, \fB\-\-id\fR=\fISID\fR
+\fISID\fR is a stream id, a value between 1 and 65535. It is used by STREAM
+CONTROL (close) to identify the stream to close. It is used by the GET
+STREAM STATUS command as the starting stream id (from and including); so
+stream ids that are less than \fISID\fR will not appear in the response.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+\fILEN\fR is the maximum length the response can be. It becomes the
+ALLOCATION LENGTH field in both commands. The default (in the absence of
+this option) is 8 bytes for STREAM CONTROL and 248 bytes for GET STREAM
+STATUS.
+.TP
+\fB\-o\fR, \fB\-\-open\fR
+selects the STREAM CONTROL command and sets STR_CTL<\-\-0x1 (i.e. 'open').
+If the \fI\-\-id=SID\fR option is given then it is ignored. The user should
+observe the response as the "Assigned stream id" is printed on stdout if
+the open is successful, if not '\-1' is sent to stdout and error messages are
+sent to stderr. If the \fI\-\-brief\fR option is also given then the only
+thing sent to stdout is a number of the assigned stream id (1 to
+65535 inclusive) or '\-1' if there is an error.
+.TP
+\fB\-r\fR, \fB\-\-readonly\fR
+this option sets a 'read\-only' flag when the underlying operating system
+opens the given \fIDEVICE\fR. This may not work since operating systems can
+not easily determine whether a pass\-through command is a logical read or
+write operation on the media (or its metadata) so they take a risk averse
+stance and require read\-write type permissions on the \fIDEVICE\fR open
+irrespective of what is performed by the pass\-through.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+There are no special read commands for streams. This implies that "normal"
+READs (6, 10, 12, 16 or 32) can be used. Note that when a stream is closed,
+all resources associated with that stream id are removed, apart from the
+data in the written LBAs. To make sure the reading back data is not delayed
+too much by error recovery (in the presence of media errors) the user may
+set the RECOVERY TIME LIMIT field (RTL, units for non\-zero values:
+milliseconds) in the 'Read\-write error recovery' mode page. This can be done
+with the sdparm utility.
+.PP
+The SCSI WRITE STREAM (16 and 32) commands can be found in the sg_write_x
+utility in this package.
+.SH EXIT STATUS
+The exit status of sg_stream_ctl is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_vpd,sg_write_x(sg3_utils); sdparm(sdparm)
diff --git a/doc/sg_sync.8 b/doc/sg_sync.8
new file mode 100644
index 0000000..7726618
--- /dev/null
+++ b/doc/sg_sync.8
@@ -0,0 +1,97 @@
+.TH SG_SYNC "8" "May 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_sync \- send SCSI SYNCHRONIZE CACHE command
+.SH SYNOPSIS
+.B sg_sync
+[\fI\-\-16\fR] [\fI\-\-count=COUNT\fR] [\fI\-\-group=GN\fR]
+[\fI\-\-help\fR] [\fI\-\-immed\fR] [\fI\-\-lba=LBA\fR] [\fI\-\-sync\-nv\fR]
+[\fI\-\-timeout=SECS\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send SYNCHRONIZE CACHE(10) or SYNCHRONIZE CACHE(16) command to \fIDEVICE\fR.
+These commands are defined for SCSI block devices (see SBC\-3). If successful
+these commands make sure that any blocks whose latest versions are held in
+cache are written to (also termed as "synchronized with") the medium.
+.PP
+If the \fILBA\fR and \fICOUNT\fR arguments are both zero (their defaults)
+then all blocks in the cache are synchronized. If \fILBA\fR is greater than
+zero while \fICOUNT\fR is zero then blocks in the cache whose addresses are
+from and including \fILBA\fR to the highest lba on the device are
+synchronized. If both \fILBA\fR and \fICOUNT\fR are non zero then blocks in
+the cache whose addresses lie in the range \fILBA\fR to
+\fILBA\fR+\fICOUNT\fR\-1 inclusive are synchronized with the medium.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-S\fR, \fB\-\-16\fR
+performs a SYNCHRONIZE CACHE(16) command. Default is to perform a
+SYNCHRONIZE CACHE(10) command.
+.TP
+\fB\-c\fR, \fB\-\-count\fR=\fICOUNT\fR
+where \fICOUNT\fR is the number of blocks to synchronize from and including
+\fILBA\fR. Default value is 0. When 0 then all blocks in the cache from and
+including \fILBA\fR argument to the highest block address are synchronized.
+.TP
+\fB\-g\fR, \fB\-\-group\fR=\fIGN\fR
+where \fIGN\fR is the group number which can be between 0 and 63 inclusive.
+The default value is 0 . Group numbers are used to segregate data collected
+within the device. This is a new feature in SBC\-2 and can probably be
+ignored for the time being.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-i\fR, \fB\-\-immed\fR
+sets the IMMED bit in the SYNCHRONIZE CACHE command. This instructs the
+device, if the format of the command is acceptable, to return a GOOD
+status immediately rather than wait for the blocks in the cache to be
+synchronized with (i.e. written to) the medium.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR is the lowest logical block address in the cache to
+synchronize to the medium. Default value is 0 .
+.TP
+\fB\-s\fR, \fB\-\-sync\-nv\fR
+synchronize the (volatile) cache with the non\-volatile cache. Without this
+option (or if there is no non\-volatile cache in the device) the
+synchronization is with the medium. The SYNC_NV bit was made obsolete in
+SBC\-3 revision 35d.
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fISECS\fR
+where \fISECS\fR is the number of seconds the OS allows the SYNCHRONIZE
+CACHE(16) to complete before it tries to cancel the command. Cancelling
+commands (typically with the task management function "abort task") is
+best avoided. Note this option is only active together with the \fI\-\-16\fR
+option. The default timeout is 60 seconds for both SYNCHRONIZE CACHE(10)
+and SYNCHRONIZE CACHE(16). Note that timeout issues can be avoided with
+the \fI\-\-immed\fR option.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+With the SYNCHRONIZE CACHE(16) command \fILBA\fR can be up to 64 bits
+in size and \fICOUNT\fR up to 32 bits in size. With the SYNCHRONIZ
+CACHE(10) command \fILBA\fR can be up to 32 bits in size and \fICOUNT\fR
+up to 16 bits in size.
+.PP
+Various numeric arguments (e.g. \fILBA\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.SH EXIT STATUS
+The exit status of sg_sync is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_start(sg3_utils)
diff --git a/doc/sg_test_rwbuf.8 b/doc/sg_test_rwbuf.8
new file mode 100644
index 0000000..d610531
--- /dev/null
+++ b/doc/sg_test_rwbuf.8
@@ -0,0 +1,86 @@
+.TH SG_TEST_RWBUF "8" "January 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_test_rwbuf \- test a SCSI host adapter by issuing dummy writes
+and reads
+.SH SYNOPSIS
+.B sg_test_rwbuf
+[\fI\-\-addrd=AR\fR] [\fI\-\-addwr=AW\fR] [\fI\-\-help\fR]
+[\fI\-\-quick\fR] \fI\-\-size=SZ\fR [\fI\-\-times=NUM\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.PP
+or an older deprecated format
+.B sg_test_rwbuf
+\fIDEVICE\fR \fISZ\fR [\fIAW\fR] [\fIAR\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+sg_test_rwbuf writes and reads back \fISZ\fR bytes to the internal buffer of
+\fIDEVICE\fR (e.g. /dev/sda or /dev/sg0). A pseudo random pattern is
+written to the data buffer on the device then read back. If the same pattern
+is found 'Success' is reported. If they do not match (checksums unequal) then
+this is reported and up to 24 bytes from the first point of mismatch are
+reported; the first line shows what was written and the second line shows
+what was received. For testing purposes, you can ask it to write \fIAW\fR or
+read \fIAR\fR additional bytes.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-r\fR, \fB\-\-addrd\fR=\fIAR\fR
+Read an additional \fIAR\fR bytes (more than indicated by \fISZ\fR) from the
+data buffer. Checksum is performed over the first \fISZ\fR bytes.
+.TP
+\fB\-w\fR, \fB\-\-addwr\fR=\fIAW\fR
+Write an additional \fIAW\fR bytes (more than indicated by \fISZ\fR) of
+zeros into the data buffer. Checksum is generated over the first \fISZ\fR
+bytes.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+Print out a usage message the exit.
+.TP
+\fB\-q\fR, \fB\-\-quick\fR
+Perform a READ BUFFER descriptor command to find out the available data
+buffer length and offset, print them out then exit (without testing
+with write/read sequences).
+.TP
+\fB\-s\fR, \fB\-\-size\fR=\fISZ\fR
+where \fISZ\fR is the size of buffer in bytes to be written then read and
+checked. This number needs to be less than or equal to the size of the
+device's data buffer which can be seen from the \fI\-\-quick\fR option.
+Either this option or the \fI\-\-quick\fR option should be given.
+.TP
+\fB\-t\fR, \fB\-\-times\fR=\fINUM\fR
+where \fINUM\fR is the number of times to repeat the write/read to buffer
+test. Default value is 1 .
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase verbosity of output.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print version number (and data of last change) then exit.
+.SH NOTES
+The microcode in a SCSI device is _not_ modified by doing a WRITE BUFFER
+command with its mode set to "data" (0x2) as done by this utility. Therefore
+this utility is safe in that respect. [Mode values 0x4, 0x5, 0x6 and 0x7
+are the dangerous ones :\-)]
+.PP
+\fBWARNING\fR: If you access the device at the same time (e.g. because it's
+a hard disk with a mounted file system on it) the device's buffer may be
+used by the device itself for other data at the same time, and overwriting
+it may or may not cause data corruption! \fBHOWEVER\fR the SPC\-3 draft
+standard does state in its WRITE BUFFER command: "This command shall not
+alter any medium of the logical unit when data mode ... is specified". This
+implies that it _is_ safe to use this utility with devices that have mounted
+file systems on them.
+Following this theme further, a disk with active mounted file systems may
+cause the data read back to be different (due to caching activity) to what
+was written and hence a checksum error.
+.SH EXIT STATUS
+The exit status of sg_test_rwbuf is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by D. Gilbert and K. Garloff
+.SH COPYRIGHT
+Copyright \(co 2000\-2018 Douglas Gilbert, Kurt Garloff
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/sg_timestamp.8 b/doc/sg_timestamp.8
new file mode 100644
index 0000000..dbc1ef1
--- /dev/null
+++ b/doc/sg_timestamp.8
@@ -0,0 +1,155 @@
+.TH SG_TIMESTAMP "8" "April 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_timestamp \- report or set timestamp on SCSI device
+.SH SYNOPSIS
+.B sg_timestamp
+[\fI\-\-elapsed\fR] [\fI\-\-help\fR] [\fI\-\-hex\fR]
+[\fI\-\-milliseconds=MS\fR] [\fI\-\-no\-timestamp\fR] [\fI\-\-origin\fR]
+[\fI\-\-raw\fR] [\fI\-\-readonly\fR] [\fI\-\-seconds=SECS\fR] [\fI\-\-srep\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI REPORT TIMESTAMP or SET TIMESTAMP command to the \fIDEVICE\fR.
+These commands are found in the SPC\-5 draft standard revision
+7 (spc5r07.pdf).
+.PP
+If either the \fI\-\-milliseconds=MS\fR or \fI\-\-seconds=SECS\fR option is
+given (and both can't be given) then the SET TIMESTAMP command is sent;
+otherwise the REPORT TIMESTAMP command is sent.
+.PP
+The timestamp is sent and received from the \fIDEVICE\fR as the number of
+milliseconds since the epoch of 1970\-01\-01 00:00:00 UTC and is held in a 48
+bit unsigned integer. That same epoch is used by Unix machines, but they
+usually hold the number of seconds since that epoch. The Unix date command
+and especially its "+%s" format is useful in converting to and from
+timestamps and more humanly readable forms. See the EXAMPLES section below.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-e\fR, \fB\-\-elapsed\fR
+assume the timestamp in the REPORT TIMESTAMP is an elapsed time from an
+event such as a power cycle or hard reset and format the output as '<n>
+days hh:mm:ss.xxx' where hh is hours (00 to 23 inclusive); mm is
+minutes (00 to 59 inclusive); ss is seconds (00 to 59 inclusive) and xxx
+is milliseconds (000 to 999 inclusive). If the number of days is 0
+then '0 days' is not output unless this option is given two or more times.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response to REPORT TIMESTAMP in ASCII hexadecimal on stderr. The
+response is not decoded.
+.TP
+\fB\-m\fR, \fB\-\-milliseconds\fR=\fIMS\fR
+where \fIMS\fR is the number of milliseconds since 1970\-01\-01 00:00:00 UTC
+to set in the \fIDEVICE\fR with the SCSI SET TIMESTAMP command.
+.TP
+\fB\-N\fR, \fB\-\-no\-timestamp\fR
+when REPORT TIMESTAMP is called this option suppress the output of the
+timestamp value (in either seconds or milliseconds). This may be useful
+in uncluttering the output when trying to decode the timestamp origin (see
+the \fI\-\-origin\fR option).
+.TP
+\fB\-o\fR, \fB\-\-origin\fR
+the REPORT TIMESTAMP returned parameter data contains a "timestamp origin"
+field. When this option is given, that field is decoded and printed out
+before the timestamp value is output. The default action (i.e. when the
+option is not given) is not to print out this decoded field.
+.br
+T10 defines this field as "the most recent event that initialized the
+returned device clock". The value 0 indicates a power up of hard reset
+initialized the clock; 2 indicates a SET TIMESTAMP initialized the
+clock while 3 indicates some other method initialized the clock.
+.br
+When used once a descriptive string is output (in a line before the
+timestamp value). When used twice the value of the TIMESTAMP ORIGIN
+field is output (in decimal, a value between 0 and 7 inclusive). When
+used thrice a line of the form 'TIMESTAMP_ORIGIN=<value>' is output.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output the SCSI REPORT TIMESTAMP response (i.e. the data\-out buffer) in
+binary (to stdout). Note that the \fI\-\-origin\fR and \fI\-\-srep\fR
+options are ignored when this option is given. Also all error and
+verbose messages are output to stderr.
+.TP
+\fB\-R\fR, \fB\-\-readonly\fR
+open the \fIDEVICE\fR read\-only. The default action is to open the
+\fIDEVICE\fR read\-write.
+.TP
+\fB\-s\fR, \fB\-\-seconds\fR=\fISECS\fR
+where \fISECS\fR is the number of seconds since 1970\-01\-01 00:00:00 UTC
+to set in the \fIDEVICE\fR with the SCSI SET TIMESTAMP command. \fISECS\fR
+is multiplied by 1000 before being used in the SET TIMESTAMP command.
+.TP
+\fB\-S\fR, \fB\-\-srep\fR
+report the number of seconds since 1970\-01\-01 00:00:00 UTC. This is done
+by dividing by 1000 the value returned by the SCSI REPORT TIMESTAMP command.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH EXIT STATUS
+The exit status of sg_timestamp is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH NOTES
+The TCMOS and the SCSIP bits in the Control extension mode page (see sdparm)
+modify the actions of the timestamp held by a \fIDEVICE\fR.
+.PP
+Currently only the "Utilization usage rate based on date and time" parameters
+within the Utilization log page (sbc4r09.pdf) use timestamps. See the sg_logs
+utility. Vendor specific commands and pages may also be using timestamps.
+.SH EXAMPLES
+On Unix machines (e.g. Linux, FreeBSD and Solaris) the date command is useful
+when working with timestamps.
+.PP
+To fetch the timestamp from a \fIDEVICE\fR and display it in a humanly
+readable form the following could be used:
+.PP
+   # sg_timestamp \-S /dev/sdb
+.br
+1448993950
+.br
+   # date \-\-date=@1448993950
+.br
+Tue Dec  1 13:19:10 EST 2015
+.br
+   # date \-R \-\-date="@1448993950"
+.br
+Tue, 01 Dec 2015 13:19:10 \-0500
+.PP
+The latter two date commands show different forms of the same date (i.e.
+1448993950 seconds since 1970\-01\-01 00:00:00 UTC). The sg_timestamp and
+date commands can be combined using backquotes:
+.PP
+   # date \-R \-\-date=@`sg_timestamp \-S /dev/sdc`
+.br
+Wed, 16 Dec 2015 20:12:59 \-0500
+.PP
+To set the timestamp on the \fIDEVICE\fR to now (approximately) the
+following could be used:
+.PP
+   # date +%s
+.br
+1448993955
+.br
+   # sg_timestamp \-\-seconds=1448993955 /dev/sdb
+.PP
+Those two command lines could be combined into one by using backquotes:
+.PP
+   # sg_timestamp \-\-seconds=`date +%s` /dev/sdb
+.PP
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2015\-2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sdparm(sdparm), sg_logs(sg3_utils)
diff --git a/doc/sg_turs.8 b/doc/sg_turs.8
new file mode 100644
index 0000000..bec99f5
--- /dev/null
+++ b/doc/sg_turs.8
@@ -0,0 +1,152 @@
+.TH SG_TURS "8" "November 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_turs \- send one or more SCSI TEST UNIT READY commands
+.SH SYNOPSIS
+.B sg_turs
+[\fI\-\-delay=MS\fR] [\fI\-\-help\fR] [\fI\-\-low\fR] [\fI\-\-num=NUM\fR]
+[\fI\-\-number=NUM\fR] [\fI\-\-progress\fR] [\fI\-\-time\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.PP
+.B sg_turs
+[\fI\-d=MS\fR] [\fI\-n=NUM\fR] [\fI\-p\fR]  [\fI\-t\fR] [\fI\-v\fR] [\fI\-V\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility sends one or more SCSI TEST UNIT READY commands to the
+\fIDEVICE\fR. This may be useful for timing the per command overhead.
+Note that TEST UNIT READY has no associated data, just a 6 byte
+command (with each byte a zero) and a returned SCSI status value.
+.PP
+This utility supports two command line syntaxes, the preferred one is
+shown first in the synopsis and explained in this section. A later section
+on the old command line syntax outlines the second group of options.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-d\fR, \fB\-\-delay\fR=\fIMS\fR
+this option causes a delay of \fIMS\fR milliseconds to occur before each
+TEST UNIT READY command is issued.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+print out the usage message then exit.
+.TP
+\fB\-l\fR, \fB\-\-low\fR
+when [\fI\-\-progress\fR] is not being used, this utility tries to complete
+the SCSI TEST UNIT READY command(s) as quickly as possible. Usually it
+calls a library function to do each TUR (sg_ll_test_unit_ready). With this
+option it uses the lower level sg_pt interface (see sg_pt.h) to save a
+little time on each TUR.
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fINUM\fR
+performs TEST UNIT READY \fINUM\fR times. If not given defaults to 1.
+These suffix multipliers are permitted: c C *1; w W *2; b B *512;
+k K KiB *1,024; KB *1,000; m M MiB *1,048,576; MB *1,000,000;
+g G GiB *1,073,741,824; and GB *1,000,000,000 . Also a suffix of the
+form "x<n>" multiplies the leading number by <n>. Alternatively a hex
+number may be given, prefixed by either '0x' or has a trailing 'h'.
+.TP
+\fB\-\-number\fR=\fINUM\fR
+same as \fI\-\-num=NUM\fR. Added for compatibility with sg_requests and
+other utilities in this package. The sg_request utility has taken over the
+role of polling the progress indication which was originally assigned to
+the TEST UNIT READY command. This is a change by T10.
+.TP
+\fB\-O\fR, \fB\-\-old\fR
+Switch to older style options. Please use as first option.
+.TP
+\fB\-p\fR, \fB\-\-progress\fR
+show progress indication (a percentage) if available. If \fI\-\-num=NUM\fR
+is given, \fINUM\fR is greater than 1 and an initial progress indication
+was detected then this utility waits 30 seconds before subsequent checks.
+If the \fI\-\-delay=MS\fR option is given then it will wait for that number
+of milliseconds instead of 30 seconds.
+Exits when \fINUM\fR is reached or there are no more progress indications.
+Ignores \fI\-\-time\fR option. See NOTES section below.
+.TP
+\fB\-t\fR, \fB\-\-time\fR
+after completing the requested number of TEST UNIT READY commands, outputs
+the total duration and the average number of commands executed per second.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase level or verbosity.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print version string then exit.
+.SH NOTES
+The progress indication is optionally part of the sense data. When a prior
+command that takes a long time to complete (and typically precludes other
+media access commands) is still underway, the progress indication can be used
+to determine how long before the device returns to its normal state. Around
+SPC\-3 T10 changed the preferred command for polling the progress indication
+from TEST UNIT READY to REQUEST SENSE (see the sg_requests utility).
+.PP
+The SCSI FORMAT command for disks used with the IMMED bit set is an example
+of an operation that takes a significant amount of time and precludes other
+media access during that time. The IMMED bit set instructs the FORMAT command
+to return control to the application client once the format has commenced (see
+SBC\-3). Several long duration SCSI commands associated with tape drives also
+use the progress indication (see SSC\-3).
+.PP
+The \fIDEVICE\fR is opened with a read\-only flag (e.g. in Unix with the
+O_RDONLY flag).
+.PP
+Early standards suggested that the SCSI TEST UNIT READY command be used for
+polling the progress indication. More recent standards seem to suggest
+the SCSI REQUEST SENSE command should be used instead.
+.SH EXIT STATUS
+The exit status of sg_turs is 0 when it is successful (e.g. in the case of
+a mechanical disk, it is spun up and ready to accept IO commands). For this
+utility the other exit status values of interest are 2, 12 and 13. 12 is for
+the case when the sense key is "not ready" [0x2] and the additional sense
+code ends with "Target port in standby state" [0x4, 0xb]. 13 is for the
+case when the sense key is "not ready" [0x2] and the additional sense code
+is "Target port in unavailable state" [0x4, 0xc]. All other cases when
+the sense key is "not ready" [0x2] will set the exit status to 2.
+For other exit status values see the sg3_utils(8) man page.
+.SH OLDER COMMAND LINE OPTIONS
+The options in this section were the only ones available prior to sg3_utils
+version 1.23 . Since then this utility defaults to the newer command line
+options which can be overridden by using \fI\-\-old\fR (or \fI\-O\fR) as the
+first option. See the ENVIRONMENT VARIABLES section for another way to
+force the use of these older command line options.
+.TP
+\fB\-d\fR, \fB\-\-delay\fR=\fIMS\fR
+this option causes a delay of \fIMS\fR milliseconds to occur before each
+TEST UNIT READY command is issued.
+.TP
+\fB\-n\fR=\fINUM\fR
+performs TEST UNIT READY \fINUM\fR times. If not given defaults to 1.
+Equivalent to \fI\-\-num=NUM\fR in the main description.
+.TP
+\fB-N\fR, \fB\-\-new\fR
+Switch to the newer style options.
+.TP
+\fB\-p\fR
+show progress indication (a percentage) if available.
+Equivalent to \fI\-\-progress\fR in the main description.
+.TP
+\fB\-t\fR
+after completing the requested number of TEST UNIT READY commands, outputs
+the total duration and the average number of commands executed per second.
+Equivalent to \fI\-\-time\fR in the main description.
+.TP
+\fB\-v\fR
+increase level of verbosity.
+.TP
+\fB\-V\fR
+print out version string then exit.
+.SH ENVIRONMENT VARIABLES
+Since sg3_utils version 1.23 the environment variable SG3_UTILS_OLD_OPTS
+can be given. When it is present this utility will expect the older command
+line options. So the presence of this environment variable is equivalent to
+using \fI\-\-old\fR (or \fI\-O\fR) as the first command line option.
+.SH AUTHORS
+Written by D. Gilbert
+.SH COPYRIGHT
+Copyright \(co 2000\-2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq, sg_requests (sg3_utils)
diff --git a/doc/sg_unmap.8 b/doc/sg_unmap.8
new file mode 100644
index 0000000..b72ecb0
--- /dev/null
+++ b/doc/sg_unmap.8
@@ -0,0 +1,166 @@
+.TH SG_UNMAP "8" "March 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_unmap \- send SCSI UNMAP command (known as 'trim' in ATA specs)
+.SH SYNOPSIS
+.B sg_unmap
+[\fI\-\-all=ST,RN[,LA]\fR] [\fI\-\-anchor\fR] [\fI\-\-dry\-run\fR]
+[\fI\-\-force\fR] [\fI\-\-grpnum=GN\fR] [\fI\-\-help\fR] [\fI\-\-in=FILE\fR]
+[\fI\-\-lba=LBA,LBA...\fR] [\fI\-\-num=NUM,NUM...\fR] [\fI\-\-timeout=TO\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send a SCSI UNMAP command to \fIDEVICE\fR to unmap one or more logical
+blocks. This command was introduced in SBC\-3 revision 18 under the broad
+heading of "logical block provisioning". Logical blocks may also be unmapped
+by the SCSI WRITE SAME command; see the sg_write_same utility. The unmap
+capability is closely related to the ATA DATA SET MANAGEMENT command with
+the "Trim" bit set.
+.PP
+Logical blocks to be unmapped can be specified in one of three ways to this
+utility. One way is by supplying the start LBAs to the '\-\-lba=' option
+and the corresponding number(s) to unmap to the '\-\-num=' option. Another
+way is by putting start LBA and number to unmap pairs in a file whose name
+is given to the '\-\-in=' option. Alternatively a large segment or all of
+a disk (SSD) can be unmapped with the \fI\-\-all=ST_RN[,LA]\fR option. All
+values are assumed to be decimal unless prefixed by "0x" (or "0X") or have
+a trailing "h" (or "H") in which case they are interpreted as hexadecimal.
+Suffix multipliers are permitted on decimal values (e.g. '\-\-num=1m').
+.PP
+When the '\-\-lba=' option is given then the '\-\-num=' option must also be
+given. If one has a comma separated list as its argument then the other must
+have the same number of elements in its list. The arguments can use a single
+space as a separator but need to be in quotes or escaped to not be
+misinterpreted by the shell.
+.PP
+With the '\-\-in=FILE' option an even number of values must be found and are
+interpreted as pairs: the first value in each pair is a starting LBA and the
+second value is the number to unmap from that LBA. Everything from and
+including a "#" on a line is ignored as are blank lines. Values may be
+comma, space and tab separated or appear on separate lines. Each line should
+not exceed 1023 bytes in length.
+.PP
+Since a lot of data can be lost with this utility, a 15 second "cooling off"
+period is given before any UNMAP commands are sent. During this period the
+user is reminded what will happen, and to which device, so they can use
+control\-C (or some other technique) to terminate this utility before any
+unmapping takes place. This period can be bypassed with the \fI\-\-force\fR
+option.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-A\fR, \fB\-\-all\fR=\fIST,RN[,LA]\fR
+where \fIST\fR is the starting LBA, \fIRN\fR is the repeat number which is
+the maximum number of blocks in each SCSI UNMAP command, and \fILA\fR, if
+given, is the last LBA to unmap. If \fILA\fR is not given, then the last
+LBA on the \fIDEVICE\fR is used. That is obtained by the SCSI READ CAPACITY
+command.
+.TP
+\fB\-a\fR, \fB\-\-anchor\fR
+sets the 'Anchor' bit in the command (introduced in sbc3r22).
+.TP
+\fB\-d\fR, \fB\-\-dry\-run\fR
+perform all the preparation, including opening \fIDEVICE\fR plus sending
+a 'standard' SCSI INQUIRY command (and optionally a READ CAPACITY), but
+exit before performing any SCSI UNMAP commands.
+.TP
+\fB\-f\fR, \fB\-\-force\fR
+bypass the 15 second warning period that occurs before any UNMAP commands
+are sent.
+.TP
+\fB\-g\fR, \fB\-\-grpnum\fR=\fIGN\fR
+sets the 'Group number' field to \fIGN\fR. Defaults to a value of zero.
+\fIGN\fR should be a value between 0 and 63.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-I\fR, \fB\-\-in\fR=\fIFILE\fR
+where \fIFILE\fR is a file name containing pairs of values. The first
+member of each pair is a starting LBA and the second member of the
+pair is the number of logical blocks to unmap from and including that
+starting LBA. Values are interpreted as decimal unless indicated
+otherwise. This option cannot be present with the '\-\-lba=' option.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA,LBA...\fR
+where \fILBA,LBA...\fR is a string of comma (or space) separated values
+that are interpreted as starting logical block addresses. Each number
+is interpreted as decimal unless prefixed by '0x' or '0X' (or it has a
+trailing 'h' or 'H'). An argument that contains any space separators needs
+to be quoted (or otherwise escaped). When this option is given then
+the '\-\-num=' option must also be given and they must contain the same
+number of elements in their arguments.
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fINUM,NUM...\fR
+where \fINUM,NUM...\fR is a string of comma (or space) separated values
+that are interpreted as a number of logical blocks to unmap. Each number
+is interpreted as decimal unless prefixed by '0x' or '0X' (or it has a
+trailing 'h' or 'H'). Note that 0 blocks is acceptable. An argument that
+contains any space separators needs to be quoted (or otherwise escaped).
+When this option is given then the '\-\-lba=' option must also be given
+and they must contain the same number of elements in their arguments.
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fITO\fR
+where \fITO\fR is a timeout value (in seconds) for the UNMAP command.
+The default value is 60 seconds.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+Some limits: an LBA can be up to 64 bits, a NUM up to 32 bits (imposed
+by structure of UNMAP SCSI command parameter data). The NUM is
+further constrained by the MAXIMUM UNMAP LBA COUNT field in the
+BLOCK LIMITS VPD page (0xb0). The maximum number of LBA,NUM pairs is
+limited to 128 by this utility and may be further constrained by the
+MAXIMUM UNMAP BLOCK DESCRIPTOR COUNT field in the BLOCK LIMITS VPD
+page.
+.PP
+Since it is unclear how long the UNMAP command will take to execute
+a '\-\-timeout=" option has been provided. The default timeout
+period is 60 seconds. If all the logical blocks on a logical unit (e.g.
+a disk drive) are to be unmapped then the FORMAT UNIT SCSI command (see
+the sg_format utility) may be considered as an alternative.
+.PP
+Support for logical block provisioning is indicated by the LBPME bit in the
+response to the SCSI READ CAPACITY (16) command (see the sg_readcap utility).
+.PP
+In SBC\-3 revision 25 the LBPU and ANC_SUP bits where added to the
+Logical Block Provisioning VPD page. When LBPU is set it indicates that
+the device supports the UNMAP command. When the ANC_SUP bit is set it
+indicates the device supports anchored LBAs.
+.PP
+The SCSI UNMAP command does the "right thing" with respect to command
+queueing. However its ATA counterpart: the DATA SET MANAGEMENT command with
+the "Trim" bit set does not interact well with SATA queueing known as NCQ.
+To address this problem T13 have introduced a new command called SFQ DATA SET
+MANAGEMENT which also has a Trim bit.
+.SH EXAMPLES
+In the examples directory of the sg3_utils package there is a
+sg_unmap_example.txt file that shows the format that the '\-\-in='
+option accepts.
+.PP
+To unmap all blocks from and including LBA 0x2000 to the end of the
+device (e.g. disk or SSD) with each SCSI UNMAP command given 1024
+blocks to unmap:
+.PP
+  sg_unmap \-\-all=0x2000,1k /dev/sg2
+.PP
+Add '\-\-force' to bypass the 15 seconds of warnings. So '\-\-force' is
+appropriate for batch files.
+.SH EXIT STATUS
+The exit status of sg_unmap is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2009\-2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_format,sg_get_lba_status,sg_readcap,sg_vpd,sg_write_same(sg3_utils)
diff --git a/doc/sg_verify.8 b/doc/sg_verify.8
new file mode 100644
index 0000000..dae3604
--- /dev/null
+++ b/doc/sg_verify.8
@@ -0,0 +1,219 @@
+.TH SG_VERIFY "8" "December 2019" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_verify \- invoke SCSI VERIFY command(s) on a block device
+.SH SYNOPSIS
+.B sg_verify
+[\fI\-\-0\fR] [\fI\-\-16\fR] [\fI\-\-bpc=BPC\fR] [\fI\-\-count=COUNT\fR]
+[\fI\-\-dpo\fR] [\fI\-\-ff\fR] [\fI\-\-ebytchk=BCH\fR] [\fI\-\-group=GN\fR]
+[\fI\-\-help\fR] [\fI\-\-in=IF\fR] [\fI\-\-lba=LBA\fR] [\fI\-\-ndo=NDO\fR]
+[\fI\-\-quiet\fR] [\fI\-\-readonly\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] [\fI\-\-vrprotect=VRP\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends one or more SCSI VERIFY (10 or 16) commands to \fIDEVICE\fR. These SCSI
+commands are defined in the SBC\-2 and SBC\-3 standards at https://www.t10.org
+and SBC\-4 drafts.
+.PP
+When \fI\-\-ndo=NDO\fR is not given then the verify starts at the logical
+block address given by the \fI\-\-lba=LBA\fR option and continues for
+\fI\-\-count=COUNT\fR blocks. No more than \fI\-\-bpc=BPC\fR blocks are
+verified by each VERIFY command so if necessary multiple VERIFY commands are
+sent. Medium verification operations are performed by the \fIDEVICE\fR (e.g.
+assuming each block has additional EEC data, check this against the logical
+block contents). No news is good news (i.e. if there are no verify errors
+detected then no messages are sent to stderr and the Unix exit status is 0).
+.PP
+When \fI\-\-ndo=NDO\fR is given then the \fI\-\-bpc=BPC\fR option is
+ignored. A single VERIFY command is issued and a comparison starts at the
+logical block address given by the \fI\-\-lba=LBA\fR option and continues for
+\fI\-\-count=COUNT\fR blocks. The VERIFY command has an associated data\-out
+buffer that is \fINDO\fR bytes long. The contents of the data\-out buffer are
+obtained from the \fIFN\fR file (if \fI\-\-in=FN\fR is given) or from stdin.
+A comparison takes place between data\-out buffer and the logical blocks
+on the \fIDEVICE\fR. If the comparison is good then no messages are sent to
+stderr and the Unix exit status is 0. If the comparison fails then a sense
+buffer with a sense key of MISCOMPARE is returned; in this case the Unix exit
+status will be 14. Messages will be sent to stderr associated with MISCOMPARE
+sense buffer unless the \fI\-\-quiet\fR option is given.
+.PP
+In SBC\-3 revision 34 the BYTCHK field in all SCSI VERIFY commands was
+expanded from one to two bits. That required some changes in the options
+of this utility, see the section below on OPTION CHANGES.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-0\fR, \fB\-\-0\fR
+a buffer \fINDO\fR bytes long full of zeros is sent as the data\-out
+part of a VERIFY command. So stdin is not read and if \fI\-\-in=IF\fR
+is given, an error is generated. Useful when \fIBCH\fR is 3 to check
+if some or all of \fIDEVICE\fR (e.g. a disk) is zero filled blocks.
+.TP
+\fB\-S\fR, \fB\-\-16\fR
+uses a VERIFY(16) command (default VERIFY(10)). Even without this option,
+using an \fI\-\-lba=LBA\fR which is too large, will cause the utility
+to issue a VERIFY(16) command.
+.TP
+\fB\-b\fR, \fB\-\-bpc\fR=\fIBPC\fR
+this option is ignored if \fI\-\-ndo=NDO\fR is given. Otherwise \fIBPC\fR
+specifies the maximum number of blocks that will be verified by a single SCSI
+VERIFY command. The default value is 128 blocks which equates to 64 KB for a
+disk with 512 byte blocks. If \fIBPC\fR is less than \fICOUNT\fR then
+multiple SCSI VERIFY commands are sent to the \fIDEVICE\fR. For the default
+VERIFY(10) \fIBPC\fR cannot exceed 0xffff (65,535) while for VERIFY(16)
+\fIBPC\fR cannot exceed 0x7fffffff (2,147,483,647). For recent block
+devices (disks) this value may be constrained by the maximum transfer length
+field in the block limits VPD page.
+.TP
+\fB\-c\fR, \fB\-\-count\fR=\fICOUNT\fR
+where \fICOUNT\fR specifies the number of blocks to verify. The default value
+is 1 . If \fICOUNT\fR is greater than \fIBPC\fR (or its default value of 128)
+and \fINDO\fR is not given, 0 or less than multiple SCSI VERIFY commands are
+sent to the device. Otherwise \fICOUNT\fR becomes the contents of the
+verification length field of the SCSI VERIFY command issued. The
+.B sg_readcap
+utility can be used to find the maximum number of blocks that a block
+device (e.g. a disk) has.
+.TP
+\fB\-d\fR, \fB\-\-dpo\fR
+disable page out changes the cache retention priority of blocks read on
+the device's cache to the lowest priority. This means that blocks read by
+other commands are more likely to remain in the device's cache.
+.TP
+\fB\-E\fR, \fB\-\-ebytchk\fR=\fIBCH\fR
+sets the BYTCHK field to \fIBCH\fR overriding the value (1) set by the
+\fI\-\-ndo=NDO\fR option. Values of 1, 2 or 3 are accepted for \fIBCH\fR
+however sbc3r34 reserves the value 2. If this option is given then
+\fI\-\-ndo=NDO\fR must also be given. If \fIBCH\fR is 3 then \fINDO\fR
+should be the size of one logical block (plus the size of some or all
+of the protection information if \fIVRP\fR is greater
+than 0).
+.TP
+\fB\-f\fR, \fB\-\-ff\fR
+a buffer \fINDO\fR bytes long full of 0xff bytes is sent as the data\-out
+part of a VERIFY command. So stdin is not read and if \fI\-\-in=IF\fR
+is given, an error is generated. Useful when \fIBCH\fR is 3 to check
+if some or all of \fIDEVICE\fR (e.g. a disk) is 0xff byte filled blocks.
+.TP
+\fB\-g\fR, \fB\-\-group\fR=\fIGN\fR
+where \fIGN\fR becomes the contents of the group number field in the SCSI
+VERIFY(16) command. It can be from 0 to 63 inclusive. The default value for
+\fIGN\fR is 0. Note that this option is ignored for the SCSI VERIFY(10)
+command.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIIF\fR
+where \fIIF\fR is the name of a file from which \fINDO\fR bytes will be read
+and placed in the data\-out buffer. This is only done when the
+\fI\-\-ndo=NDO\fR option is given. If this option is not given then stdin
+is read. If \fIIF\fR is "\-" then stdin is also used.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR specifies the logical block address of the first block to
+start the verify operation. \fILBA\fR is assumed to be decimal unless prefixed
+by '0x' or a trailing 'h' (see below). The default value is 0 (i.e. the start
+of the device).
+.TP
+\fB\-n\fR, \fB\-\-ndo\fR=\fINDO\fR
+\fINDO\fR is the number of bytes to obtain from the \fIFN\fR file (if
+\fI\-\-in=FN\fR is given) or from stdin. Those bytes are placed in the
+data\-out buffer associated with the SCSI VERIFY command and \fINDO\fR
+is placed in the verification length field in the cdb. The default value
+for \fINDO\fR is 0 and the maximum value is dependent on the OS. If the
+\fI\-\-ebytchk=BCH\fR option is not given then the BYTCHK field in the cdb
+is set to 1.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+suppress the sense buffer messages associated with a MISCOMPARE sense key
+that would otherwise be sent to stderr. Still set the exit status to 14
+which is the sense key value indicating a MISCOMPARE .
+.TP
+\fB\-r\fR, \fB\-\-readonly\fR
+opens the DEVICE read\-only rather than read\-write which is the
+default. The Linux sg driver needs read\-write access for the SCSI
+VERIFY command but other access methods may require read\-only access.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-P\fR, \fB\-\-vrprotect\fR=\fIVRP\fR
+where \fIVRP\fR is the value in the vrprotect field in the VERIFY command
+cdb. It must be a value between 0 and 7 inclusive. The default value is
+zero.
+.SH BYTCHK
+BYTCHK is the name of a field (two bits wide) in the VERIFY(10) and
+VERIFY(16) commands. When set to 1 or 3 (sbc3r34 reserves the value 2) it
+indicates that associated with the SCSI VERIFY command, a data\-out buffer
+will be sent for the device (disk) to check. Using the \fI\-\-ndo=NDO\fR
+option sets the BYTCHK field to 1 and \fINDO\fR is the number of bytes
+placed in the data\-out buffer. Those bytes are obtained from stdin or
+\fIIF\fR (from the \fI\-\-in=FN\fR option). The \fI\-\-ebytchk=BCH\fR
+option may be used to override the BYTCHK field value of 1 with \fIBCH\fR.
+.PP
+The calculation of \fINDO\fR is left up to the user. Its value depends
+on the logical block size (which can be found with the sg_readcap utility),
+the \fICOUNT\fR and the \fIVRP\fR values. If the \fIVRP\fR is greater than
+0 then each logical block will contain an extra 8 bytes (at least) of
+protection information.
+.PP
+When the BYTCHK field is 0 then the verification process done by the
+device (disk) is vendor specific. It typically involves checking each
+block on the disk against its error correction codes (ECC) which is
+additional data also held on the disk.
+.PP
+Many Operating Systems put limits on the maximum size of the
+data\-out (and data\-in) buffer. For Linux at one time the limit was
+less than 1 MB but has been increased somewhat.
+.SH OPTION CHANGES
+Earlier versions of this utility had a \fI\-\-bytchk=NDO\fR option which
+set the BYTCHK bit and set the cdb verification length field to \fINDO\fR.
+The shorter form of that option was \fI\-B NDO\fR. For backward
+compatibility that option is still present but not documented. In its place
+is the \fI\-\-ndo=NDO\fR whose shorter form of \fI\-n NDO\fR.
+\fI\-\-ndo=NDO\fR sets the BYTCHK field to 1 unless that is overridden by
+the \fI\-\-ebytchk=BCH\fR.
+.SH NOTES
+Various numeric arguments (e.g. \fILBA\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+The amount of error correction and the number of retries attempted before a
+block is considered defective are controlled in part by the Verify Error
+Recovery mode page. A note in the SBC\-3 draft (rev 29 section 6.4.9 on the
+Verify Error Recovery mode page) advises that to minimize the number of
+checks (and hence have the most "sensitive" verify check) do the following
+in that mode page: set the EER bit to 0, the PER bit to 1, the DTE bit to 1,
+the DCR bit to 1, the verify retry count to 0 and the verify recovery time
+limit to 0. Mode pages can be modified with the
+.B sdparm
+utility.
+.PP
+The SCSI VERIFY(6) command defined in the SSC\-2 standard and later (i.e.
+for tape drive systems) is not supported by this utility.
+.SH EXIT STATUS
+The exit status of sg_verify is 0 when it is successful. When \fIBCH\fR is
+other than 0 then a comparison takes place and if it fails then the exit
+status is 14 which happens to be the sense key value of MISCOMPARE.
+Otherwise see the EXIT STATUS section in the sg3_utils(8) man page.
+.PP
+Earlier versions of this utility set an exit status of 98 when there was a
+MISCOMPARE.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2019 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sdparm(sdparm), sg_modes(sg3_utils), sg_readcap(sg3_utils),
+.B sg_inq(sg3_utils)
diff --git a/doc/sg_vpd.8 b/doc/sg_vpd.8
new file mode 100644
index 0000000..8bb88ee
--- /dev/null
+++ b/doc/sg_vpd.8
@@ -0,0 +1,365 @@
+.TH SG_VPD "8" "August 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_vpd \- fetch SCSI VPD page and/or decode its response
+.SH SYNOPSIS
+.B sg_vpd
+[\fI\-\-all\fR] [\fI\-\-enumerate\fR] [\fI\-\-examine\fR] [\fI\-\-force\fR]
+[\fI\-\-help\fR] [\fI\-\-hex\fR] [\fI\-\-ident\fR] [\fI\-\-inhex=FN\fR]
+[\fI\-\-json[=JO]\fR] [\fI\-\-long\fR] [\fI\-\-maxlen=LEN\fR]
+[\fI\-\-page=PG\fR] [\fI\-\-quiet\fR] [\fI\-\-raw\fR]
+[\fI\-\-sinq_inraw=RFN\fR] [\fI\-\-vendor=VP\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] [\fIDEVICE\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility, when \fIDEVICE\fR is given, fetches a Vital Product Data (VPD)
+page and decodes it or outputs it in ASCII hexadecimal or binary. VPD pages
+are fetched with a SCSI INQUIRY command.
+.PP
+Alternatively the \fI\-\-inhex=FN\fR option can be given. In this case
+\fIFN\fR is assumed to be a file name ('\-' for stdin) containing ASCII
+hexadecimal representing a VPD page response. If the \fI\-\-raw\fR option
+is also given then binary input is assumed (rather than ASCII hexadecimal).
+.PP
+Probably the most important page is the Device Identification
+VPD page (page number: 0x83). Since SPC\-3, support for this page
+has been flagged as mandatory. This page can be fetched by
+using the \fI\-\-ident\fR option.
+.PP
+The reference document used for interpreting VPD pages (and the INQUIRY
+standard response) is T10/BSR INCITS 566 Revision 6 which is draft SPC\-6
+dated 22 October 2021. It can be found at https://www.t10.org .
+.PP
+When no options are given, other than a \fIDEVICE\fR, then the "Supported
+VPD pages" (0x0) VPD page is fetched and decoded.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+decode all VPD pages. When used with \fIDEVICE\fR the pages to be decoded
+are found in the "Supported VPD pages" VPD page. Pages that cannot be
+decoded are displayed in hex; add the \fI\-\-long\fR option to have ASCII
+displayed to the right of each line of hex.
+.br
+If this option is used with the \fI\-\-inhex=FN\fR option then the file
+\fIFN\fR is assumed to contain 1 or more VPD pages (in ASCII hex or binary).
+Decoding continues until the file is exhausted (or an error occurs). Sanity
+checks are applied on each VPD page's length and the ascending order of VPD
+page numbers (required by SPC\-4) so bad data may be detected.
+.br
+If the \fI\-\-page=PG\fR option is also given then no VPD page whose page
+number is greater than \fIPG\fR (or its numeric equivalent) is decoded.
+.TP
+\fB\-e\fR, \fB\-\-enumerate\fR
+list the names of the known VPD pages, first the standard pages (i.e.
+those defined by T10), then the vendor specific pages. Each group is sorted
+in abbreviation order. The \fIDEVICE\fR and most other options are ignored
+and this utility exits after listing the VPD page names. May be used together
+with \fI\-\-page=PG\fR where \fIPG\fR is numeric. If so, it searches for the
+summary lines of all VPD pages whose number matches \fIPG\fR. May be used
+with \fI\-\-vendor=VP\fR to restrict output to known vendor specific pages
+for vendor/product \fIVP\fR.
+.TP
+\fB\-E\fR, \fB\-\-examine\fR
+scan part of all of the VPD space (page numbers 0x0 to 0xff) and output any
+pages found. If this option is given once, the scan starts at page 0x80;
+if it is given twice, the scan starts at 0x0; and if given three times the
+scan starts at 0xc0. This option takes no notice of the contents of VPD page
+0x0 which should contain a list of all supported VPD pages. Some vendors
+either forget to list some standard pages or perhaps purposely don't list
+vendor specific pages which are in the range 0xc0 to 0xff.
+.br
+If the \fI\-\-page=PG\fR option is not given then the scan finishes at page
+0xff. if the \fI\-\-page=PG\fR option is given then the scan finishes at
+page \fIPG\fR. A check is made before the scan to make sure the start page
+is less than or equal to the finish page; if not the start and finish page
+numbers are swapped.
+.br
+The sdparm utility which lists mode and VPD pages also has a \fB\-\-examine\fR
+option will similar functionility. Note that T10 has changed most of the
+pages that list supported pages (e.g. VPD, mode and log pages; supported
+commands) to add the weasel words "may or may not list all ...".
+.TP
+\fB\-f\fR, \fB\-\-force\fR
+As a sanity check, the normal action when fetching VPD pages other than
+page 0x0 (the "Supported VPD pages" VPD page), is to first fetch page 0x0
+and only if the requested page is one of the supported pages, to go ahead
+and fetch the requested page.
+.br
+When this option is given, skip checking of VPD page 0x0 before accessing
+the requested VPD page. The prior check of VPD page 0x0 is known to
+crash certain USB devices, so use with care.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs the usage message summarizing command line options then exits.
+Ignores \fIDEVICE\fR if given.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+outputs the requested VPD page in ASCII hexadecimal. Can be used multiple
+times, see section on the ATA information vpd page.
+.br
+To generate output suitable for placing in a file that can be used by a
+later invocation with the \fI\-\-inhex=FN\fR option, use the '\-HHHH'
+option (e.g. 'sg_vpd \-p di \-HHHH /dev/sg3 > dev_id.hex'). The
+reason '\-HHHH' is used is to flag that unadorned hexadecimal (without other
+text or address offsets) is sent to stdout.
+.TP
+\fB\-i\fR, \fB\-\-ident\fR
+decode the device identification (0x83) VPD page. When used once this option
+has the same effect as '\-\-page=di'. When use twice then the short form of
+the device identification VPD page's logical unit designator is decoded. In
+the latter case this option has the same effect as '\-\-quiet \-\-page=di_lu'.
+.TP
+\fB\-I\fR, \fB\-\-inhex\fR=\fIFN\fR
+\fIFN\fR is expected to be a file name (or '\-' for stdin) which contains
+ASCII hexadecimal or binary representing a VPD page (or a standard INQUIRY)
+response. This utility will then decode that response. It is preferable to
+also supply the \fI\-\-page=PG\fR option, if not this utility will attempt
+to guess which VPD page (or standard INQUIRY) the response is associated
+with. The hexadecimal should be arranged as 1 or 2 digits representing a
+byte each of which is whitespace or comma separated. Anything from and
+including a hash mark to the end of line is ignored. If the \fI\-\-raw\fR
+option is also given then \fIFN\fR is treated as binary.
+.TP
+\fB\-j\fR, \fB\-\-json[\fR=\fIJO\fR]
+output is in JSON format instead of human readable form. See sg3_utils_json
+manpage or use '?' for \fIJO\fR for a summary.
+.TP
+\fB\-l\fR, \fB\-\-long\fR
+when decoding some VPD pages, give a little more output. For example the ATA
+Information VPD page only shows the signature (in hex) and the IDENTIFY
+(PACKET) DEVICE (in hex) when this option is given.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in the
+cdb's "allocation length" field. If not given (or \fILEN\fR is zero) then
+252 is used (apart from the ATA Information VPD page which defaults to 572)
+and, if the response indicates this value is insufficient, another INQUIRY
+command is sent with a larger value in the cdb's "allocation length" field.
+If this option is given and \fILEN\fR is greater than 0 then only one INQUIRY
+command is sent. Since many simple devices implement the INQUIRY command
+badly (and do not support VPD pages) then the safest value to use for
+\fILEN\fR is 36. See the sg_inq(8) man page for the more information.
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPG\fR
+where \fIPG\fR is the VPD page to be decoded or output. The \fIPG\fR argument
+can either be an abbreviation, a number or a pair or numbers/abbreviations
+separated by a comma. The VPD page abbreviations can be seen by using the
+\fI\-\-enumerate\fR option. If a number is given it is assumed to be decimal
+unless it has a hexadecimal indicator which is either a leading '0x' or a
+trailing 'h'. If one number is given then it is assumed to be a VPD page
+number. If two numbers (or abbreviations) are given then the second one is
+the same as \fIVP\fR (see the \fI\-\-vendor=VP\fR option). If this option
+is not given (nor '\-i', '\-l' nor '\-V') then the "Supported VPD pages" (0x0)
+VPD page is fetched and decoded. If \fIPG\fR is '\-1' or 'sinq' then the
+standard INQUIRY response is output. This option may also be used with the
+\fI\-\-enumerate\fR (see its description).
+.br
+If \fIPG\fR is not found in the 'Supported VPD pages' VPD page (0x0) then
+EDOM is returned. To bypass this check use the \fI\-\-force\fR option.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+suppress the amount of decoding output.
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+if not used with \fI\-\-inhex=FN\fR then output requested VPD page in binary.
+The output should be piped to a file or another utility when this option is
+used. The binary is sent to stdout, and errors are sent to stderr.
+.br
+if used with \fI\-\-inhex=FN\fR then the contents of \fIFN\fR is treated as
+binary.
+.TP
+\fB\-Q\fR, \fB\-\-sinq_inraw\fR=\fIRFN\fR
+where \fIRFN\fR is a filename containing binary standard INQUIRY response
+data that matches either \fIDEVICE\fR or \fIFN\fR. Linux places this
+standard INQUIRY response in its sysfs pseudo filesystem. A typical
+location is at /sys/class/scsi_disk/<hctl>/device/inquiry where <hctl> is
+a four part numeric tuple separated by colons. This tuple distinguishes
+the device from any others on the system. Linux also places some VPD page
+responses in binary in the same directory with names like "vpd_pg83" where
+the last two digits form the hexadecimal VPD page number whose binary
+contents are therein.
+.br
+Some VPD pages (e.g. the Extended Inquiry VPD page) depend on knowing
+the settings in the standard INQUIRY response to interpret the fields
+in that VPD page. This option together with the \fI\-\-all\fR,
+\fI\-\-examine\fR or \fI\-\-page=PG\fR allows this utility to process
+both the standard INQUIRY response and VPD pages in the same invocation. 
+.TP
+\fB\-M\fR, \fB\-\-vendor\fR=\fIVP\fR
+where \fIVP\fR is a vendor (e.g. "sea" for Seagate) or vendor/product
+acronym (e.g. "hp3par" for the 3PAR array from HP). Many vendors have
+re\-used the numbers at the beginning of the vendor specific VPD page
+range (e.g.  page 0xc0) and this option is a way of selecting only those
+which are of interest. Using a \fIVP\fR of "xxx" will list the available
+acronyms.
+.br
+If this option is used with \fI\-\-page=PG\fR and \fIPG\fR is an acronym
+then this option is ignored. If \fIPG\fR is a number (e.g. 0xc0) then
+\fIVP\fR is used to choose the which vendor specific page (e.g. sharing
+page number 0xc0) to decode.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increases the level or verbosity.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print out version string then exit.
+.SH ATA INFORMATION VPD PAGE
+This VPD page (0x89 or 'ai') is defined by the SCSI to ATA Translation
+standard. It contains information about the SAT layer, the "signature" of
+the ATA device and the response to the ATA IDENTIFY (PACKET) DEVICE
+command. The latter part has 512 bytes of identity, capability and
+settings data which the hdparm utility is capable of decoding (so this
+utility doesn't decode it).
+.PP
+To unclutter the output for this page, the signature and the IDENTIFY (PACKET)
+DEVICE response are not output unless the \fI\-\-long\fR option (or
+\fI\-\-hex\fR or \fI\-\-raw\fR) are given. When the \fI\-\-long\fR option
+is given the IDENTIFY (PACKET) DEVICE response is output as 256 (16 bit)
+words as is the fashion for ATA devices. To see that response as a string of
+bytes use the '\-HH' option. To format the output suitable for hdparm to
+decode use either the '\-HHH' or '\-rr' option. For example if 'dev/sdb' is
+a SATA disk behind a SAT layer then this
+command: 'sg_vpd \-p ai \-HHH /dev/sdb | hdparm \-\-Istdin'
+should decode the ATA IDENTIFY (PACKET) DEVICE response.
+.SH NOTES
+Since some VPD pages (e.g. the Extended INQUIRY page) depend on settings
+in the standard INQUIRY response, then the standard INQUIRY response is
+output as a pseudo VPD page when \fIPG\fR is set to '\-1' or 'sinq'. Also
+the decoding of some fields (e.g. the Extended INQUIRY page's SPT field)
+is expanded when the '\-\-long' option is given using the standard INQUIRY
+response information (e.g. the PDT and the PROTECT fields).
+.PP
+The \fIDEVICE\fR is opened with a read\-only flag (e.g. in Unix with the
+O_RDONLY flag).
+.SH EXIT STATUS
+The exit status of sg_vpd is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH EXAMPLES
+The examples in this page use Linux device names. For suitable device
+names in other supported Operating Systems see the sg3_utils(8) man page.
+.PP
+To see the VPD pages that a device supports, use with no options. The
+command line invocation is shown first followed by a typical response:
+.PP
+   # sg_vpd /dev/sdb
+.br
+Supported VPD pages VPD page:
+.br
+  Supported VPD pages [sv]
+.br
+  Unit serial number [sn]
+.br
+  Device identification [di]
+.br
+  Extended inquiry data [ei]
+.br
+  Block limits (SBC) [bl]
+.PP
+To see the VPD page numbers associated with each supported page then
+add the '\-\-long' option to the above command line. To view a
+VPD page either its number or abbreviation can be given to
+the '\-\-page=' option. The page name abbreviations are shown within
+square brackets above. In the next example the Extended inquiry data
+VPD page is listed:
+.PP
+   # sg_vpd \-\-page=ei /dev/sdb
+.br
+extended INQUIRY data VPD page:
+.br
+  ACTIVATE_MICROCODE=0 SPT=0 GRD_CHK=0 APP_CHK=0 REF_CHK=0
+.br
+  UASK_SUP=0 GROUP_SUP=0 PRIOR_SUP=0 HEADSUP=1 ORDSUP=1 SIMPSUP=1
+.br
+  WU_SUP=0 CRD_SUP=0 NV_SUP=0 V_SUP=0
+.br
+  P_I_I_SUP=0 LUICLR=0 R_SUP=0 CBCS=0
+.br
+  Multi I_T nexus microcode download=0
+.br
+  Extended self\-test completion minutes=0
+.br
+  POA_SUP=0 HRA_SUP=0 VSA_SUP=0
+.PP
+To check if any protection types are supported by a disk use the '\-\-long'
+option on the Extended inquiry data VPD page:
+.PP
+   # sg_vpd \-\-page=ei \-\-long /dev/sdb
+.br
+   extended INQUIRY data VPD page:
+.br
+     ACTIVATE_MICROCODE=0
+.br
+     SPT=1 [protection types 1 and 2 supported]
+.br
+     GRD_CHK=1
+.br
+     ....
+.PP
+Search for the name (and acronym) of all pages that share VPD page number
+0xb0 .
+.PP
+   # sg_vpd \-\-page=0xb0 \-\-enumerate
+.br
+   Matching standard VPD pages:
+.br
+     bl         0xb0      Block limits (SBC)
+.br
+     oi         0xb0      OSD information
+.br
+     sad        0xb0      Sequential access device capabilities (SSC)
+.PP
+Some examples follow using the "\-\-all" option. Send an ASCII hexadecimal
+representation of all VPD pages to a file:
+.PP
+   # sg_vpd \-\-all \-HHHH /dev/sg3 > all_vpds.hex
+.PP
+At some later time that file could be decoded with:
+.PP
+   # sg_vpd \-\-all \-\-inhex=all_vpds.hex
+.PP
+To do the equivalent as the previous example but use a file containing
+binary:
+.PP
+   # sg_vpd \-\-all \-\-raw /dev/sg3 > all_vpds.bin
+.br
+   # sg_vpd \-\-all \-\-raw \-\-inhex=all_vpds.bin
+.PP
+Notice that "\-\-raw" must be given with the second (\-\-inhex) invocation
+to alert the utility that all_vpds.bin contains binary as it assumes ASCII
+hexadecimal by default. Next we only decode T10 specified VPD pages
+excluding vendor specific VPD pages that start at page number 0xc0:
+.PP
+   # sg_vpd \-\-all \-\-page=0xbf \-\-raw \-\-inhex=all_vpds.bin
+.PP
+In Linux, binary images of some important VPD page responses (e.g. 0, 80h
+and 83h) are cached in files within the sysfs pseudo file system. Since
+VPD pages hardly ever change their contents, decoding those files will
+give the same output as probing the device with the added benefit that
+decoding those files doesn't need root permissions. The long and short
+forms are shown:
+.PP
+   sg_vpd \-\-raw \-\-inhex=/sys/class/scsi_generic/sg3/device/vpd_pg83
+.PP
+   sg_vpd \-rI /sys/class/scsi_generic/sg3/device/vpd_pg83
+.PP
+If /dev/sg3 is a disk at 2:0:0:0 , then this invocation should give more
+verbose output but essentially the same as the previous two examples.
+.PP
+   sg_vpd \-v \-r \-I /sys/class/scsi_disk/2:0:0:0/device/vpd_pg83
+.PP
+Further examples can be found on the https://sg.danny.cz/sg/sg3_utils.html
+web page.
+.SH AUTHOR
+Written by Douglas Gilbert
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2006\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_inq(sg3_utils), sg3_utils(sg3_utils), sdparm(sdparm), hdparm(hdparm)
diff --git a/doc/sg_wr_mode.8 b/doc/sg_wr_mode.8
new file mode 100644
index 0000000..e917c76
--- /dev/null
+++ b/doc/sg_wr_mode.8
@@ -0,0 +1,225 @@
+.TH SG_WR_MODE "8" "April 2018" "sg3_utils\-1.43" SG3_UTILS
+.SH NAME
+sg_wr_mode \- write (modify) SCSI mode page
+.SH SYNOPSIS
+.B sg_wr_mode
+[\fI\-\-contents=H,H...\fR] [\fI\-\-dbd\fR] [\fI\-\-force\fR] [\fI\-\-help\fR]
+[\fI\-\-len=10|6\fR] [\fI\-\-mask=M,M...\fR] [\fI\-\-page=PG_H[,SPG_H]\fR]
+[\fI\-\-rtd\fR] [\fI\-\-save\fR] [\fI\-\-six\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Writes a modified mode page to \fIDEVICE\fR. Uses the SCSI MODE SENSE (6
+or 10 byte variant) command to fetch the existing mode data which includes
+a mode page (or subpage). It then combines that with the contents,
+potentially masked, and writes the modified mode page with the SCSI MODE
+SELECT (6 or 10 byte variant) command. This utility does not modify
+the block descriptor(s); if any block descriptors are fetched by the MODE
+SENSE command then the same block descriptors are written back with the
+following MODE SELECT command.
+.PP
+If the \fI\-\-rtd\fR option is given then most other options apart from
+\fI\-\-save\fR, \fI\-\-len=\fR10|6\fR and \fI\-\-six\fR are ignored. In this
+case only a MODE SELECT command is sent to the \fIDEVICE\fR with the RTD
+bit (Revert To Defaults) set. This bit was added to this command in SPC\-5
+revision 11, so older devices may not support it. The Extended Inquiry VPD
+page has the RTD_SUP bit to indicate whether the \fIDEVICE\fR supports the
+RTD bit in the MODE SELECT(6 and 10) commands. When the \fI\-\-rtd\fR option
+is given the rest of this section can be ignored.
+.PP
+If a contents argument is not given then the various components (i.e.
+header, block descriptor(s) and mode page) of the "current" values of
+the existing mode page are printed out. In this case the mode page is
+not altered on the device.
+.PP
+If the contents are specified, and a mask is not specified, then the contents
+must match the existing mode page in various aspects unless the
+\fI\-\-force\fR option is given. These include length, mode page code and
+subpage code if applicable. If all is well then the contents string is
+written to \fIDEVICE\fR as the new mode page.
+.PP
+If both contents and mask strings are specified then only bit positions
+in the contents corresponding to set bits in the mask are taken while the
+existing mode page supplies bit positions corresponding to clear bits.
+When a mask is given then the mask and/or the contents may be shorter
+than the existing mode page. If the mask is shorter than the contents then
+the remaining bytes are taken from the contents. If the contents are shorter
+than the existing mode page then the remaining bytes are taken from the
+existing mod page.
+.PP
+The force option allows the contents string to be written as the new
+mode page without any prior checks on the existing mode page. This should
+only be required for vendor specific mode pages. The existing mode data
+is ignored apart from the block descriptors which can be suppressed with
+the \fI\-\-dbd\fR option if need be.
+.PP
+Changing individual fields in a mode page is probably more easily done
+with the sdparm utility. Fields can be identified by acronym or by a
+numerical descriptor.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-c\fR, \fB\-\-contents\fR=\fIH,H...\fR
+where \fIH,H...\fR is a string of comma separated hex numbers each of
+which should resolve to a byte value (i.e. 0 to ff inclusive). A (single)
+space separated string of hex numbers is also allowed but the list needs to
+be in quotes. This is the new contents of the mode page to be written to
+\fIDEVICE\fR, potentially filtered by the mask string.
+.TP
+\fB\-c\fR, \fB\-\-contents\fR=-
+reads contents string from stdin. The hex numbers in the string may be comma,
+space, tab or linefeed (newline) separated. If a line contains "#" then the
+remaining characters on that line are ignored. Otherwise each non separator
+character should resolve to a byte value (i.e. 0 to ff inclusive). This
+forms the new contents of the mode page to be written to \fIDEVICE\fR,
+potentially filtered by the mask string.
+.TP
+\fB\-d\fR, \fB\-\-dbd\fR
+disable block descriptors (DBD flag in cdb). Some device types include
+block descriptors in the mode data returned by a MODE SENSE command. If
+so the same block descriptors are written by the MODE SELECT command.
+This option instructs the MODE SENSE command not to return any block
+descriptors. This would be a sensible default for this utility apart
+from the fact that not all SCSI devices support the DBD bit in the cdb.
+.TP
+\fB\-f\fR, \fB\-\-force\fR
+force the contents string to be taken as the new mode page, or at least
+doesn't do checks on the existing mode page. Note that \fIDEVICE\fR may
+still reject the new contents for the mode page. Cannot be given with
+the \fI\-\-mask=M,M...\fR option.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-l\fR, \fB\-\-len\fR=10 | 6
+length of the SCSI commands (cdb) sent to \fIDEVICE\fR. The default is 10
+so 10 byte MODE SENSE and MODE SELECT commands are issued. Some old devices
+don't support the 10 byte variants hence this option.
+.TP
+\fB\-m\fR, \fB\-\-mask\fR=\fIM,M...\fR
+where \fIM,M...\fR is a string of comma separated hex numbers each of which
+should resolve to a byte value (i.e. 0 to ff inclusive). A (single) space
+separated string of hex numbers is also allowed but the list needs to be in
+quotes. The mask chooses (bit by bit) whether the new mode page comes from
+the contents (mask bit set) or from the existing mode page (mask bit clear).
+If the mask string is shorter than the contents string then the remaining
+bytes are taken from the contents string. If the contents string is shorter
+than the existing mode page then the remaining bytes are taken from the
+existing mode page (i.e. they are left unaltered).
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPG_H\fR
+where \fIPG_H\fR is the page code value to fetch and modify. The page code
+is in hex and should be between 0 and 3e inclusive. Notice that page code
+3f to fetch all mode pages is disallowed.
+.TP
+\fB\-p\fR, \fB\-\-page\fR=\fIPG_H,SPG_H\fR
+where \fIPG_H\fR is the page code value and \fISPG_H\fR is the subpage code
+value to fetch and modify. Both values are in hex. The subpage code should
+be between 0 and fe inclusive. Notice that subpage code ff to fetch all
+mode subpages (for a given mode page or all mode pages in the case of 3f,ff)
+is disallowed.
+.TP
+\fB\-R\fR, \fB\-\-rtd\fR
+when this option is given most other actions are bypassed and a MODE
+SELECT(6 or 10) command is sent to the \fIDEVICE\fR with the RTD bit set.
+This will cause all current values (and saved values if the \fI\-\-save\fR
+option is also given) of all mode pages to be reverted to their default
+values.
+.TP
+\fB\-s\fR, \fB\-\-save\fR
+changes the "saved" mode page when MODE SELECT is successful. By
+default (i.e. when \fI\-\-save\fR is not used) only the "current" mode page
+values are changed when MODE SELECT is successful. In this case the new mode
+page will stay in effect until the device is reset (e.g.  power cycled).
+When it restarts the "saved" values for the mode page will be re\-instated.
+So to make changes permanent use the \fI\-\-save\fR option.
+.br
+When used with the \fI\-\-rtd\fR option then both the current and saved
+values in each mode page are reverted to their default values. In the
+absence of \fI\-\-save\fR option only the current values in each mode page
+are reverted to their default values.
+.TP
+\fB\-6\fR, \fB\-\-six\fR
+this option will cause the 6 byte variants of MODE SENSE and MODE SELECT
+commands to be used. The default is to use the 10 byte options. This option
+is equivalent to using the \fI\-\-len=6\fR option.
+
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+This utility does not check whether the contents string is trying to
+modify parts of the mode page which are changeable. The device should
+do that and if some part is not changeable then it should
+report: "Invalid field in parameter list".
+.PP
+Some mode pages are not saveable. If so an attempt to use the \fI\-\-save\fR
+option should cause an error to be reported from the device: "Illegal field
+in cdb".
+.PP
+The device is required to do various checks before it accepts a new
+mode page. If these checks fail then the mode page is not altered and
+either a "parameter list length error" or an "invalid field in
+parameter list" error is returned by the device in the sense data.
+.PP
+The recommended way to modify a mode page is to read it with a
+MODE SENSE, modify some part of it then write it back to the
+device with a MODE SELECT command. For example, reading an existing mode
+page can be accomplished with 'sg_modes \-p=1a \-r /dev/sdb > mp_1a.txt' (the
+power condition mode page). The mp_1a.txt file can be edited and then used
+as the contents string to this
+utility (e.g. 'sg_wr_mode \-p 1a \-s \-c \- /dev/sdb < mp_1a.txt').
+.PP
+Two fields differ between what is read from the device with MODE SENSE and
+what is written to the device with MODE SELECT:
+the mode data length is reserved (i.e. zero(es)) in a MODE
+SELECT command while the PS bit ((sub)page byte 0 bit 7) in each
+mode (sub)page is reserved (zero) in a MODE SELECT command.
+The PS bit given in the contents string is zeroed unless
+the \fI\-\-force\fR option is selected.
+.SH EXAMPLES
+This utility can be used together with the sg_modes utility. To re\-instate
+the default mode page values (i.e. the mode page values chosen by the
+manufacturer of the device) as both the current and saved mode page
+values the following sequence could be used:
+.PP
+  $ sg_modes \-\-control=2 \-\-page=1a \-r /dev/sda > t
+.br
+  $ sg_wr_mode \-\-page=1a \-\-contents=\- \-\-save /dev/sda < t
+.PP
+Next is an example of using a mask to modify the "idle condition counter"
+of the "power condition" mode page (0x1a) from 0x28 to 0x37. Note that the
+change is not saved so the "idle condition counter" will revert to 0x28
+after the next power cycle. The output from sg_modes is abridged.
+.PP
+ $ sg_modes \-\-page=1a /dev/hdc
+.br
+ >> Power condition (mmc), page_control: current
+.br
+ 00     1a 0a 00 03 00 00 00 28  00 00 01 2c
+.PP
+ $ sg_wr_mode \-p 1a \-c 0,0,0,0,0,0,0,37 \-m 0,0,0,0,0,0,0,ff /dev/hdc
+.PP
+ $ sg_modes \-p 1a /dev/hdc
+.br
+ >> Power condition (mmc), page_control: current
+.br
+ 00     1a 0a 00 03 00 00 00 37  00 00 01 2c
+.SH EXIT STATUS
+The exit status of sg_wr_mode is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sdparm(sdparm), sg_modes(sg3_utils), sginfo(sg3_utils)
diff --git a/doc/sg_write_buffer.8 b/doc/sg_write_buffer.8
new file mode 100644
index 0000000..23913e3
--- /dev/null
+++ b/doc/sg_write_buffer.8
@@ -0,0 +1,227 @@
+.TH SG_WRITE_BUFFER "8" "November 2018" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_write_buffer \- send SCSI WRITE BUFFER commands
+.SH SYNOPSIS
+.B sg_write_buffer
+[\fI\-\-bpw=CS\fR] [\fI\-\-dry\-run\fR] [\fI\-\-help\fR] [\fI\-\-id=ID\fR]
+[\fI\-\-in=FILE\fR] [\fI\-\-length=LEN\fR] [\fI\-\-mode=MO\fR]
+[\fI\-\-offset=OFF\fR] [\fI\-\-read\-stdin\fR] [\fI\-\-skip=SKIP\fR]
+[\fI\-\-specific=MS\fR] [\fI\-\-timeout=TO\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends one or more SCSI WRITE BUFFER commands to \fIDEVICE\fR, along with data
+provided by the user. In some cases no data is required, or data can be read
+from the file given in the \fI\-\-in=FILE\fR option, or data is read from
+stdin when either \fI\-\-read\-stdin\fR or \fI\-\-in=\-\fR is given.
+.PP
+Some WRITE BUFFER command variants do not have associated data to send to the
+device. For example "activate_mc" activates deferred microcode that was sent
+via prior WRITE BUFFER commands. There is a different method used to download
+microcode to SES devices, see the sg_ses_microcode utility.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-b\fR, \fB\-\-bpw\fR=\fICS\fR
+where \fICS\fR is the chunk size in bytes. This will be the maximum number
+of bytes sent per WRITE BUFFER command. So if \fICS\fR is less than the
+effective length then multiple WRITE BUFFER commands are sent, each taking
+the next chunk from the read data and increasing the buffer offset field
+in the WRITE BUFFER command by the appropriate amount. The default is
+a chunk size of 0 which is interpreted as a very large number hence only
+one WRITE BUFFER command will be sent. This option should only be used with
+modes that "download microcode, with offsets ..."; namely either mode 0x6,
+0x7, 0xd or 0xe.
+.br
+The number in \fICS\fR can optionally be followed by ",act" or ",activate".
+In this case after WRITE BUFFER commands have been sent until the
+effective length is exhausted another WRITE BUFFER command with its mode
+set to "Activate deferred microcode mode" [mode 0xf] is sent.
+.TP
+\fB\-d\fR, \fB\-\-dry\-run\fR
+Do all the command line processing and sanity checks including reading
+the input file. However at the point where a WRITE BUFFER SCSI command(s)
+would be sent, step over that call and assume it completed without errors
+and continue. \fIDEVICE\fR is still opened but can be /dev/null (in Unix).
+It is recommended to use \fI\-\-verbose\fR with this option to get an
+overview of what would have happened.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit. If used multiple times also prints
+the mode names and their acronyms.
+.TP
+\fB\-i\fR, \fB\-\-id\fR=\fIID\fR
+this option sets the buffer id field in the cdb. \fIID\fR is a value between
+0 (default) and 255 inclusive.
+.TP
+\fB\-I\fR, \fB\-\-in\fR=\fIFILE\fR
+read data from file \fIFILE\fR that will be sent with the WRITE BUFFER
+command.  If \fIFILE\fR is '\-' then stdin is read until an EOF is
+detected (this is the same action as \fI\-\-read\-stdin\fR). Data is read
+from the beginning of \fIFILE\fR except in the case when it is a regular file
+and the \fI\-\-skip=SKIP\fR option is given.
+.TP
+\fB\-l\fR, \fB\-\-length\fR=\fILEN\fR
+where \fILEN\fR is the length, in bytes, of data to be written to the device.
+If not given (and the length cannot be deduced from \fI\-\-in=FILE\fR or
+\fI\-\-read\-stdin\fR) then defaults to zero. If the option is given and the
+length deduced from \fI\-\-in=FILE\fR or \fI\-\-read\-stdin\fR is less (or no
+data is provided), then bytes of 0xff are used as fill bytes.
+.TP
+\fB\-m\fR, \fB\-\-mode\fR=\fIMO\fR
+this option sets the MODE field in the cdb. \fIMO\fR is a value between
+0 (default) and 31 inclusive. Alternatively an abbreviation can be given.
+See the MODES section below. To list the available mode abbreviations at
+run time give an invalid one (e.g. '\-\-mode=xxx') or use the '\-hh' option.
+.TP
+\fB\-o\fR, \fB\-\-offset\fR=\fIOFF\fR
+this option sets the BUFFER OFFSET field in the cdb. \fIOFF\fR is a value
+between 0 (default) and 2**24\-1 . It is a byte offset.
+.TP
+\fB\-r\fR, \fB\-\-read\-stdin\fR
+read data from stdin until an EOF is detected. This data is sent with
+the WRITE BUFFER command to \fIDEVICE\fR. The action of this option is the
+same as using '\-\-in=\-'. Previously this option's long name was
+\fI\-\-raw\fR and it may still be used for backward compatibility.
+.TP
+\fB\-s\fR, \fB\-\-skip\fR=\fISKIP\fR
+this option is only active when \fI\-\-in=FILE\fR is given and \fIFILE\fR is
+a regular file, rather than stdin. Data is read starting at byte offset
+\fISKIP\fR to the end of file (or the amount given by \fI\-\-length=LEN\fR).
+If not given the byte offset defaults to 0 (i.e. the start of the file).
+.TP
+\fB\-S\fR, \fB\-\-specific\fR=\fIMS\fR
+\fIMS\fR is the MODE SPECIFIC field in the cdb. This is a 3\-bit field
+so the values 0 to 7 are accepted. This field was introduced in SPC\-4
+revision 32 and can be used to specify additional events that activate
+deferred microcode (when \fIMO\fR is 0xD).
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fITO\fR
+\fITO\fR is the command timeout (in seconds) for each WRITE BUFFER command
+issued by this utility. Its default value is 300 seconds (5 minutes) and
+should only be altered if this is not sufficient.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH MODES
+Following is a list of WRITE BUFFER command settings for the MODE field.
+First is an acronym accepted by the \fIMO\fR argument of this utility.
+Following the acronym in square brackets are the corresponding decimal and
+hex values that may also be given for \fIMO\fR. The following are listed
+in numerical order.
+.TP
+hd  [0, 0x0]
+Combined header and data (obsolete in SPC\-4).
+.TP
+vendor  [1, 0x1]
+Vendor specific.
+.TP
+data  [2, 0x2]
+Data (was called "Write Data" in SPC\-3).
+.TP
+dmc  [4, 0x4]
+Download microcode and activate (was called "Download microcode" in SPC\-3).
+.TP
+dmc_save  [5, 0x5]
+Download microcode, save, and activate (was called "Download microcode and
+save" in SPC\-3).
+.TP
+dmc_offs  [6, 0x6]
+Download microcode with offsets and activate (was called "Download microcode
+with offsets" in SPC\-3).
+.TP
+dmc_offs_save  [7, 0x7]
+Download microcode with offsets, save, and activate (was called "Download
+microcode with offsets and save" in SPC\-3).
+.TP
+echo  [10, 0xa]
+Write data to echo buffer (was called "Echo buffer" in SPC\-3).
+.TP
+dmc_offs_ev_defer  [13, 0xd]
+Download microcode with offsets, select activation events, save, and defer
+activate (introduced in SPC\-4).
+.TP
+dmc_offs_defer  [14, 0xe]
+Download microcode with offsets, save, and defer activate (introduced in
+SPC\-4).
+.TP
+activate_mc  [15, 0xf]
+Activate deferred microcode (introduced in SPC\-4).
+.TP
+en_ex  [26, 0x1A]
+Enable expander communications protocol and Echo buffer (obsolete in SPC\-4).
+.TP
+dis_ex  [27, 0x1B]
+Disable expander communications protocol (obsolete in SPC\-4).
+.TP
+deh  [28, 0x1C]
+Download application client error history (was called "Download application
+log" in SPC\-3).
+.SH NOTES
+If no \fI\-\-length=LEN\fR is given this utility reads up to 8 MiB of data
+from the given file \fIFILE\fR (or stdin). If a larger amount of data is
+required then the \fI\-\-length=LEN\fR option should be given.
+.PP
+The user should be aware that most operating systems have limits on the
+amount of data that can be sent with one SCSI command. In Linux this
+depends on the pass through mechanism used (e.g. block SG_IO or the sg
+driver) and various setting in sysfs in the Linux lk 2.6/3
+series (e.g. /sys/block/sda/queue/max_sectors_kb). Devices (i.e. logical
+units) also typically have limits on the maximum amount of data they can
+handle in one command. These two limitations suggest that modes
+containing the word "offset" together with the \fI\-\-bpw=CS\fR option
+are required as firmware files get larger and larger. And \fICS\fR
+can be quite small, for example 4096 bytes, resulting in many WRITE
+BUFFER commands being sent.
+.PP
+Attempting to download a microcode/firmware file that is too large may
+cause an error to occur in the pass\-through layer (i.e. before the
+SCSI command is issued). In Linux such error reports can be obscure as
+in "pass through os error invalid argument". FreeBSD reports such
+errors well to the machine's console but returns a cryptic error message
+to this utility.
+.PP
+Downloading incorrect microcode into a device has the ability to render
+that device inoperable. One would hope that the device vendor verifies
+the data before activating it. If the SCSI WRITE BUFFER command is given
+values in its cdb (e.g. \fILEN\fR) that are inappropriate (e.g. too large)
+then the device should respond with a sense key of ILLEGAL REQUEST and
+an additional sense code of INVALID FIELD in CDB. If a WRITE BUFFER
+command (or a sequence of them) fails due to device vendor verification
+checks then it should respond with a sense key of ILLEGAL REQUEST and
+an additional sense code of COMMAND SEQUENCE ERROR.
+.PP
+All numbers given with options are assumed to be decimal.
+Alternatively numerical values can be given in hexadecimal preceded by
+either "0x" or "0X" (or has a trailing "h" or "H").
+.SH EXAMPLES
+The following sends new firmware to an enclosure. Sending a 1.5 MB
+file in one WRITE BUFFER command caused the enclosure to lock up
+temporarily and did not update the firmware. Breaking the firmware file
+into 4 KB chunks (an educated guess) was more successful:
+.PP
+  sg_write_buffer \-b 4k \-m dmc_offs_save \-I firmware.bin /dev/sg4
+.PP
+The firmware update occurred in the following enclosure power cycle. With
+a modern enclosure the Extended Inquiry VPD page gives indications in which
+situations a firmware upgrade will take place.
+.SH EXIT STATUS
+The exit status of sg_write_buffer is 0 when it is successful. Otherwise
+see the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Luben Tuikov and Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2006\-2018 Luben Tuikov and Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_read_buffer, sg_ses_microcode(sg3_utils)
diff --git a/doc/sg_write_long.8 b/doc/sg_write_long.8
new file mode 100644
index 0000000..2d5560d
--- /dev/null
+++ b/doc/sg_write_long.8
@@ -0,0 +1,176 @@
+.TH SG_WRITE_LONG "8" "January 2016" "sg3_utils\-1.42" SG3_UTILS
+.SH NAME
+sg_write_long \- send SCSI WRITE LONG command
+.SH SYNOPSIS
+.B sg_write_long
+[\fI\-\-16\fR] [\fI\-\-cor_dis\fR] [\fI\-\-help\fR] [\fI\-\-in=IF\fR]
+[\fI\-\-lba=LBA\fR] [\fI\-\-pblock\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] [\fI\-\-wr_uncor\fR] [\fI\-\-xfer_len=BTL\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Send the SCSI WRITE LONG (10 or 16 byte) command to \fIDEVICE\fR. The buffer
+to be written to the \fIDEVICE\fR is filled with
+.B 0xff
+bytes or read from the \fIIF\fR file. This buffer includes the logical
+data (e.g. 512 bytes) and the ECC bytes.
+.PP
+This utility can be used to generate a MEDIUM ERROR at a specific logical
+block address. This can be useful for testing error handling. Prior to
+such a test, the
+.B sg_dd
+utility could be used to copy the original contents of the logical
+block address to some safe location. After the test the
+.B sg_dd
+utility could be used to write back the original contents of the
+logical block address. An alternate strategy would be to read the "long"
+contents of the logical block address with
+.B sg_read_long
+utility prior to testing and restore it with this utility after testing.
+.PP
+.B Take care:
+If recoverable errors are being injected (e.g. only one or a few bits
+changed so that the ECC is able to correct the data) then care should
+be taken with the settings in the "read write error recovery" mode page.
+Specifically if the ARRE (for reads) and/or AWRE (for writes) are set
+then recovered errors will cause the lba to be reassigned (and the old
+location to be added to the grown defect list (PLIST)). This is not easily
+reversed and uses (one of the finite number of) the spare sectors set
+aside for this purpose. If in doubt it is probably safest to clear the
+ARRE and AWRE bits. These bits can be checked and modified with the
+sdparm utility.  For example: "sdparm \-c AWRE,ARRE /dev/sda" will clear
+the bits until the disk is power cycled.
+.PP
+In SBC\-4 revision 7 all uses of SCSI WRITE LONG (10 and 16 byte) commands
+were made obsolete apart from the case in which the WR_UNCOR bit is set.
+The SCSI READ LONG (10 and 16 byte) commands were made obsolete in the
+same revision.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-S\fR, \fB\-\-16\fR
+send a SCSI WRITE LONG (16) command to \fIDEVICE\fR. The default action (in
+the absence of this option) is to send a SCSI WRITE LONG (10) command.
+.TP
+\fB\-c\fR, \fB\-\-cor_dis\fR
+sets the correction disabled (i.e 'COR_DIS') bit. This inhibits various
+other mechanisms such as automatic block reallocation, error recovery
+and various informational exception conditions being triggered.
+This bit is relatively new in SBC\-3 .
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIIF\fR
+read data (binary) from file named \fIIF\fR and use it for the SCSI WRITE
+LONG command. If \fIIF\fR is "\-" then stdin is read. If this option is
+not given then 0xff bytes are used as fill.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR is the logical block address of the sector to overwrite.
+Defaults to lba 0 which is a dangerous block to overwrite on a disk that is
+in use. Assumed to be in decimal unless prefixed with '0x' or has a
+trailing 'h'. If \fILBA\fR is larger than can fit in 32 bits then the
+\fI\-\-16\fR option should be used.
+.TP
+\fB\-p\fR, \fB\-\-pblock\fR
+sets the physical block (i.e 'PBLOCK') bit. This instructs \fIDEVICE\fR
+to use the given data (unless \fI\-\-wr_uncor\fR is also given) to write
+to the physical block specified by \fILBA\fR. The default action
+is to write to the logical block corresponding to the given lba.
+This bit is relatively new in SBC\-3 .
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the degree of verbosity (debug messages).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+output version string then exit.
+.TP
+\fB\-w\fR, \fB\-\-wr_uncor\fR
+sets the "write uncorrected" (i.e 'WR_UNCOR') bit. This instructs the
+\fIDEVICE\fR to flag the given lba (or the physical block that contains it
+if \fI\-\-pblock\fR is also given) as having an unrecoverable error
+associated with it. Note: no data is transferred to \fIDEVICE\fR,
+other than the command (i.e. the cdb). In the absence of this option, the
+default action is to use the provided data or 0xff
+bytes (\fI\-\-xfer_len=BTL\fR in length) and write it to \fIDEVICE\fR.
+This bit is relatively new in SBC\-3 .
+.TP
+\fB\-x\fR, \fB\-\-xfer_len\fR=\fIBTL\fR
+where \fIBTL\fR is the byte transfer length (default to 520). If the
+given value (or the default) does not match the "long" block size of the
+device, nothing is written to \fIDEVICE\fR and the appropriate xfer_len value
+may be deduced from the error response which is printed (to stderr).
+.SH NOTES
+Various numeric arguments (e.g. \fILBA\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+The 10 byte SCSI WRITE LONG command limits the logical block address
+to a 32 bit quantity. For larger LBAs use the \fI\-\-16\fR option for the
+SCSI WRITE LONG (16) command.
+.SH EXAMPLES
+This section outlines setting up a block with corrupted data, checking the
+error condition, then restoring useful contents to that sector.
+.PP
+First, if the data in a sector is important, save it with the sg_read_long
+utility:
+.PP
+  sg_read_long \-\-lba=0x1234 \-\-out=0x1234_1.img \-x \fIBTL\fR /dev/sda
+.PP
+This utility may need to be executed several time in order to determine
+what the correct value for \fIBTL\fR is.
+Next use this utility to "corrupt" that sector. That might be done with:
+.PP
+  sg_write_long \-\-lba=0x1234 \-x \fIBTL\fR /dev/sda
+.PP
+This will write a sector (and ECC data) of 0xff bytes. Some disks may
+reject this (at least one of the author's does). Another approach is
+to copy the 0x1234_1.img file (to 0x1234_2.img in this example) and
+change some values with a hex editor. Then write the changed image with:
+.PP
+  sg_write_long \-\-lba=0x1234 \-\-in=0x1234_2.img \-x \fIBTL\fR /dev/sda
+.PP
+Yet another approach is to use the \fI\-\-wr_uncor\fR option, if supported:
+.PP
+  sg_write_long \-\-lba=0x1234 \-\-wr_uncor /dev/sda
+.PP
+Next we use the sg_dd utility to check that the sector is corrupted. Here is an
+example:
+.PP
+  sg_dd if=/dev/sda blk_sgio=1 skip=0x1234 of=. bs=512 count=1 verbose=4
+.PP
+Notice that the "blk_sgio=1" option is given. This is to make sure that
+the sector is read (and no others) and the error is fully reported.
+The "blk_sgio=1" option causes the SG_IO ioctl to be used by sg_dd rather
+than the block subsystem.
+.PP
+Finally we should restore sector 0x1234 to a non\-corrupted state. A sector
+full of zeros could be written with:
+.PP
+  sg_dd if=/dev/zero of=/dev/sda blk_sgio=1 seek=0x1234 bs=512 count=1
+.PP
+This will result in a sector (block) with 512 bytes of 0x0 without a
+MEDIUM ERROR since the ECC and associated data will be regenerated and
+thus well formed. The 'blk_sgio=1' option is even more important in this
+case as it may stop the block subsystem doing a read before write (since
+the read will most likely fail).
+Another approach is to write back the original contents:
+.PP
+  sg_write_long \-\-lba=0x1234 \-\-in=0x1234_1.img \-x \fIBTL\fR /dev/sda
+.PP
+.SH EXIT STATUS
+The exit status of sg_write_long is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Saeed Bishara. Further work by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2016 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_read_long, sg_dd (both in sg3_utils), sdparm(sdparm)
diff --git a/doc/sg_write_same.8 b/doc/sg_write_same.8
new file mode 100644
index 0000000..9d4c704
--- /dev/null
+++ b/doc/sg_write_same.8
@@ -0,0 +1,355 @@
+.TH SG_WRITE_SAME "8" "June 2020" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sg_write_same \- send SCSI WRITE SAME command
+.SH SYNOPSIS
+.B sg_write_same
+[\fI\-\-10\fR] [\fI\-\-16\fR] [\fI\-\-32\fR] [\fI\-\-anchor\fR]
+[\fI\-\-ff\fR] [\fI\-\-grpnum=GN\fR] [\fI\-\-help\fR] [\fI\-\-in=IF\fR]
+[\fI\-\-lba=LBA\fR] [\fI\-\-lbdata\fR] [\fI\-\-num=NUM\fR]
+[\fI\-\-ndob\fR] [\fI\-\-pbdata\fR] [\fI\-\-timeout=TO\fR]
+[\fI\-\-unmap\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+[\fI\-\-wrprotect=WPR\fR] [\fI\-\-xferlen=LEN\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+Send the SCSI WRITE SAME (10, 16 or 32 byte) command to \fIDEVICE\fR. This
+command writes the given block \fINUM\fR times to consecutive blocks on
+the \fIDEVICE\fR starting at logical block address \fILBA\fR.
+.PP
+The length of the block to be written multiple times is obtained from either
+the \fILEN\fR argument, or the length of the given input file \fIIF\fR,
+or by calling READ CAPACITY(16) on \fIDEVICE\fR. The contents of the
+block to be written are obtained from the input file \fIIF\fR or
+zeros are used. If READ CAPACITY(16) is called (which implies \fIIF\fR
+was not given) and the PROT_EN bit is set then an extra 8 bytes (i.e.
+more than the logical block size) of 0xff are sent. If READ CAPACITY(16)
+fails then READ CAPACITY(10) is used to determine the block size.
+.PP
+If neither \fI\-\-10\fR, \fI\-\-16\fR nor \fI\-\-32\fR is given then
+WRITE SAME(10) is sent unless one of the following conditions is met.
+If \fILBA\fR (plus \fINUM\fR) exceeds 32 bits, \fINUM\fR exceeds 65535,
+or the \fI\-\-unmap\fR option is given then WRITE SAME(16) is sent.
+The \fI\-\-10\fR, \fI\-\-16\fR and \fI\-\-32\fR options are mutually
+exclusive.
+.PP
+SBC\-3 revision 35d introduced a "No Data\-Out Buffer" (NDOB) bit which, if
+set, bypasses the requirement to send a single block of data to the
+\fIDEVICE\fR together with the command. Only WRITE SAME (16 and 32 byte)
+support the NDOB bit. If given, a user block of zeros is assumed; if
+required, protection information of 0xffs is assumed.
+.PP
+In SBC\-3 revision 26 the UNMAP and ANCHOR bits were added to the
+WRITE SAME (10) command. Since the UNMAP bit has been in WRITE SAME (16)
+and WRITE SAME (32) since SBC\-3 revision 18, the lower of the two (i.e.
+WRITE SAME (16)) is the default when the \fI\-\-unmap\fR option is given.
+To send WRITE SAME (10) use the \fI\-\-10\fR option.
+.PP
+.B Take care:
+The WRITE SAME(10, 16 and 32) commands may interpret a \fINUM\fR of zero as
+write to the end of \fIDEVICE\fR. This utility defaults \fINUM\fR to 1 .
+The WRITE SAME commands have no IMMED bit so if \fINUM\fR is large (or
+zero) then an invocation of this utility could take a long time, potentially
+as long as a FORMAT UNIT command. In such situations the command timeout
+value \fITO\fR may need to be increased from its default value of 60
+seconds. In SBC\-3 revision 26 the WSNZ (write same no zero) bit was added
+to the Block Limits VPD page [0xB0]. If set the WRITE SAME commands will not
+accept a \fINUM\fR of zero. The same SBC\-3 revision added the "Maximum
+Write Same Length" field to the Block Limits VPD page.
+.PP
+The Logical Block Provisioning VPD page [0xB2] contains the LBPWS and
+LBPWS10 bits. If LBPWS is set then WRITE SAME (16) supports the UNMAP bit.
+If LBPWS10 is set then WRITE SAME (10) supports the UNMAP bit. If either
+LBPWS or LBPWS10 is set and the WRITE SAME (32) is supported then WRITE
+SAME (32) supports the UNMAP bit.
+.PP
+As a precaution against an accidental 'sg_write_same /dev/sda' (for example)
+overwriting LBA 0 on /dev/sda with zeros, at least one of the
+\fI\-\-in=IF\fR, \fI\-\-lba=LBA\fR or \fI\-\-num=NUM\fR options must be
+given. Obviously this utility can destroy a lot of user data so check the
+options carefully.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-R\fR, \fB\-\-10\fR
+send a SCSI WRITE SAME (10) command to \fIDEVICE\fR. The ability to
+set the \fI\-\-unmap\fR (and \fI\-\-anchor\fR) options to this command
+was added in SBC\-3 revision 26.
+.TP
+\fB\-S\fR, \fB\-\-16\fR
+send a SCSI WRITE SAME (16) command to \fIDEVICE\fR.
+.TP
+\fB\-T\fR, \fB\-\-32\fR
+send a SCSI WRITE SAME (32) command to \fIDEVICE\fR.
+.TP
+\fB\-a\fR, \fB\-\-anchor\fR
+sets the ANCHOR bit in the cdb. Introduced in SBC\-3 revision 22.
+That draft requires the \fI\-\-unmap\fR option to also be specified.
+.TP
+\fB\-f\fR, \fB\-\-ff\fR
+the data\-out buffer sent with this command is initialized with 0xff bytes
+when this option is given.
+.TP
+\fB\-g\fR, \fB\-\-grpnum\fR=\fIGN\fR
+sets the 'Group number' field to \fIGN\fR. Defaults to a value of zero.
+\fIGN\fR should be a value between 0 and 63.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIIF\fR
+read data (binary) from file named \fIIF\fR and use it as the data\-out
+buffer for the SCSI WRITE SAME command. The length of the data\-out buffer
+is \fI\-\-xferlen=LEN\fR or, if that is not given, the length of the \fIIF\fR
+file. If \fIIF\fR is "\-" then stdin is read. If this option and the
+\fI\-\-ff\fR are not given then 0x00 bytes are used as fill with the length
+of the data\-out buffer obtained from \fI\-\-xferlen=LEN\fR or by calling
+READ CAPACITY(16 or 10).  If the response to READ CAPACITY(16) has the
+PROT_EN bit set then data\- out buffer size is modified accordingly with
+the last 8 bytes set to 0xff.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR is the logical block address to start the WRITE SAME command.
+Defaults to lba 0 which is a dangerous block to overwrite on a disk that is
+in use. Assumed to be in decimal unless prefixed with '0x' or has a
+trailing 'h'.
+.TP
+\fB\-L\fR, \fB\-\-lbdata\fR
+sets the LBDATA bit in the WRITE SAME cdb. This bit was made obsolete in
+sbc3r32 in September 2012.
+.TP
+\fB\-N\fR, \fB\-\-ndob\fR
+sets the NDOB bit in the WRITE SAME (16 and 32 byte) commands. NDOB stands
+for No Data\-Out Buffer. Default is to clear this bit. When this option
+is given then \fI\-\-in=IF\fR is not allowed and \fI\-\-xferlen=LEN\fR can
+only be given if \fILEN\fR is 0 .
+.br
+By default zeros are written in each block, but it is possible that
+the "provisioning initialization pattern" is written depending on other
+settings.
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fINUM\fR
+where \fINUM\fR is the number of blocks, starting at \fILBA\fR, to write the
+data\-out buffer to. The default value for \fINUM\fR is 1. The value
+corresponds to the 'Number of logical blocks' field in the WRITE SAME cdb.
+.br
+Note that a value of 0 in \fINUM\fR may be interpreted as write the data\-out
+buffer on every block starting at \fILBA\fR to the end of the \fIDEVICE\fR.
+If the WSNZ bit (introduced in sbc3r26, January 2011) in the Block Limits VPD
+page is set then the value of 0 is disallowed, yielding an Invalid request
+sense key.
+.TP
+\fB\-P\fR, \fB\-\-pbdata\fR
+sets the PBDATA bit in the WRITE SAME cdb. This bit was made obsolete in
+sbc3r32 in September 2012.
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fITO\fR
+where \fITO\fR is the command timeout value in seconds. The default value is
+60 seconds. If \fINUM\fR is large (or zero) a WRITE SAME command may require
+considerably more time than 60 seconds to complete.
+.TP
+\fB\-U\fR, \fB\-\-unmap\fR
+sets the UNMAP bit in the WRITE SAME(10, 16 and 32) cdb. See UNMAP section
+below.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the degree of verbosity (debug messages).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+output version string then exit.
+.TP
+\fB\-w\fR, \fB\-\-wrprotect\fR=\fIWPR\fR
+sets the "Write protect" field in the WRITE SAME cdb to \fIWPR\fR. The
+default value is zero. \fIWPR\fR should be a value between 0 and 7.
+When \fIWPR\fR is 1 or greater, and the disk's protection type is 1 or
+greater, then 8 extra bytes of protection information are expected or
+generated (to place in the command's data\-out buffer).
+.TP
+\fB\-x\fR, \fB\-\-xferlen\fR=\fILEN\fR
+where \fILEN\fR is the data\-out buffer length. Defaults to the length of
+the \fIIF\fR file or, if that is not given, then the READ CAPACITY(16 or 10)
+command is used to find the 'Logical block length in bytes'. That figure
+may be increased by 8 bytes if the \fIDEVICE\fR's protection type is 1 or
+greater and the WRPROTECT field (see \fI\-\-wrprotect=WPR\fR) is 1 or
+greater. If both this option and the \fIIF\fR option are given and
+\fILEN\fR exceeds the length of the \fIIF\fR file then \fILEN\fR is the
+data\-out buffer length with zeros used as pad bytes.
+.SH UNMAP
+Logical block provisioning is a new term introduced in SBC\-3 revision 25
+for the ability to mark blocks as unused. For large storage arrays, it is a
+way to provision less physical storage than the READ CAPACITY command reports
+is available, potentially allocating more physical storage when WRITE
+commands require it. For flash memory (e.g. SSD drives) it is a way of
+potentially saving power (and perhaps access time) when it is known large
+sections (or almost all) of the flash memory is not in use. SSDs need wear
+levelling algorithms to have acceptable endurance and typically over
+provision to simplify those algorithms; hence they typically contain more
+physical flash storage than their logical size would dictate.
+.PP
+Support for logical block provisioning is indicated by the LBPME bit being
+set in the READ CAPACITY(16) command response (see the sg_readcap utility).
+That implies at least one of the UNMAP or WRITE SAME(16) commands is
+implemented. If the UNMAP command is implemented then
+the "Maximum unmap LBA count" and "Maximum unmap block descriptor count"
+fields in the Block Limits VPD page should both be greater than zero. The
+READ CAPACITY(16) command response also contains a LBPRZ bit which if set
+means that if unmapped blocks are read then zeros will be returned for the
+data (and if protection information is active, 0xff bytes are returned for
+that). In SBC\-3 revision 27 the same LBPRZ bit was added to the Logical
+Block Provisioning VPD page.
+.PP
+In SBC\-3 revision 25 the LBPU and ANC_SUP bits where added to the
+Logical Block Provisioning VPD page. When LBPU is set it indicates that
+the device supports the UNMAP command (see the sg_unmap utility). When the
+ANC_SUP bit is set it indicates the device supports anchored LBAs.
+.PP
+When the UNMAP bit is set in the cdb then the data\-out buffer is also sent.
+Additionally the data section of that data\-out buffer should be full of 0x0
+bytes while the data protection block, 8 bytes at the end if present, should
+be set to 0xff bytes. If these conditions are not met and the LBPRZ bit is
+set then the UNMAP bit is ignored and the data\-out buffer is written to the
+\fIDEVICE\fR as if the UNMAP bit was zero. In the absence of the
+\fI\-\-in=IF\fR option, this utility will attempt build a data\-out buffer
+that meets the requirements for the UNMAP bit in the cdb to be acted on by
+the \fIDEVICE\fR.
+.PP
+Logical blocks may also be unmapped by the SCSI UNMAP and FORMAT UNIT
+commands (see the sg_unmap and sg_format utilities).
+.PP
+The unmap capability in SCSI is closely related to the ATA DATA SET
+MANAGEMENT command with the "Trim" bit set. That ATA trim capability does
+not interact well with SATA command queueing known as NCQ. T13 have
+introduced a new command called the SFQ DATA SET MANAGEMENT command also
+with a the "Trim" bit to address that problem. The SCSI WRITE SAME with
+the UNMAP bit set and the UNMAP commands do not have any problems with
+SCSI queueing.
+.SH NOTES
+Various numeric arguments (e.g. \fILBA\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+In Linux, prior to lk 3.17, the sg driver did not support cdb sizes greater
+than 16 bytes. Hence a device node like /dev/sg1 which is associated with
+the sg driver would fail with this utility if the \fI\-\-32\fR option was
+given (or implied by other options). The bsg driver with device nodes like
+/dev/bsg/6:0:0:1 does support cdb sizes greater than 16 bytes since its
+introduction in lk 2.6.28 .
+.SH EXIT STATUS
+The exit status of sg_write_same is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH EXAMPLES
+BEWARE: all these examples will overwrite the data on one or more blocks,
+potentially CLEARING the WHOLE DISK.
+.PP
+One simple usage is to write blocks of zero from (and including) a given LBA
+for 63 blocks:
+.PP
+  sg_write_same \-\-lba=0x1234 \-\-num=63 /dev/sdc
+.PP
+Since \fI\-\-xferlen=LEN\fR has not been given, then this utility will
+call the READ CAPACITY command on /dev/sdc to determine the number
+of bytes in a logical block.  Let us assume that is 512 bytes. Since
+\fI\-\-in=IF\fR is not given a block of zeros is assumed. So 63 blocks
+of zeros (each block containing 512 bytes) will be written from (and
+including) LBA 0x1234 . Note that only one block of zeros is passed
+to the SCSI WRITE SAME command in the data\-out buffer (as required by
+SBC\-3). Using the WRITE SAME SCSI command to write one or more blocks
+blocks of zeros is equivalent to the NVMe command: Write Zeroes.
+.br
+Now we will write zero blocks to the WHOLE disk. [Note sanitize type commands
+will also clear blocks and metadata that are not directly visible]:
+.PP
+  sg_write_same \-\-lba=0x0 \-\-num=0 /dev/sdc
+.PP
+Yes, in this context \-\-num=0 means the rest of the disk. The above
+invocation may give an error due to the WSNZ bit in the Block Limits VPD
+page being set. To get around that try:
+.PP
+  sg_write_same \-\-lba=0x0 \-\-ndob /dev/sdc
+.PP
+this invocation, if supported, has the added benefit of not sending a data
+out buffer of zeros. Notes that it is possible that the "provisioning
+initialization pattern" is written to each block instead of zeros.
+.PP
+A similar example follows but in this case the blocks
+are "unmapped" ("trimmed" in ATA speak) rather than zeroed:
+.PP
+  sg_write_same \-\-unmap \-L 0x1234 \-n 63 /dev/sdc
+.PP
+Note that if the LBPRZ bit in the READ CAPACITY(16) response is set (i.e.
+LPPRZ is an acronym for logical block provisioning read zeros) then these
+two examples do the same thing, at least seen from the point of view of
+subsequent reads.
+.PP
+This utility can also be used to write protection information (PI) on disks
+formatted with a protection type greater than zero. PI is 8 bytes of extra
+data appended to the user data of a logical block: the first two bytes are a
+CRC (the "guard"), the next two bytes are the "application tag" and the last
+four bytes are the "reference tag". With protection types 1 and 2 if the
+application tag is 0xffff then the guard should not be checked (against the
+user data).
+.PP
+In this example we assume the logical block size (of the user data) is 512
+bytes and the disk has been formatted with protection type 1. Since we are
+going to modify LBA 2468 then we take a copy of it first:
+.PP
+  dd if=/dev/sdb skip=2468 bs=512 of=2468.bin count=1
+.PP
+The following command line sets the user data to zeros and the PI to 8
+0xFF bytes on LBA 2468:
+.PP
+  sg_write_same \-\-lba=2468 /dev/sdb
+.PP
+Reading back that block should be successful because the application tag
+is 0xffff which suppresses the guard (CRC) check (which would otherwise be
+wrong):
+.PP
+  dd if=/dev/sdb skip=2468 bs=512 of=/dev/null count=1
+.PP
+Now an attempt is made to create a binary file with zeros in the user data,
+0x0000 in the application tag and 0xff bytes in the other two PI fields. It
+is awkward to create 0xff bytes in a file (in Unix) as the "tr" command
+below shows:
+.PP
+  dd if=/dev/zero bs=1 count=512 of=ud.bin
+.br
+  tr "\\000" "\\377" < /dev/zero | dd bs=1 of=ff_s.bin count=8
+.br
+  cat ud.bin ff_s.bin > lb.bin
+.br
+  dd if=/dev/zero bs=1 count=2 seek=514 conv=notrunc of=lb.bin
+.PP
+The resulting file can be viewed with 'hexdump \-C lb.bin' and should
+contain 520 bytes. Now that file can be written to LBA 2468 as follows:
+.PP
+  sg_write_same \-\-lba=2468 wrprotect=3 \-\-in=lb.bin /dev/sdb
+.PP
+Note the \fI\-\-wrprotect=3\fR rather than being set to 1, since we want
+the WRITE SAME command to succeed even though the PI data now indicates
+the user data is corrupted. When an attempt is made to read the LBA, an
+error should occur:
+.PP
+  dd if=/dev/sdb skip=2468 bs=512 of=/dev/null count=1
+.PP
+dd errors are not very expressive, if dmesg is checked there should be
+a line something like this: "[sdb]  Add. Sense: Logical block guard check
+failed". The block can be corrected by doing a "sg_write_same \-\-lba=1234
+/dev/sdb" again or restoring the original contents of that LBA:
+.PP
+  dd if=2468.bin bs=512 seek=2468 of=/dev/sdb conv=notrunc count=1
+.PP
+Hopefully the dd command would never try to truncate the output file when
+it is a block device.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2009\-2020 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_format,sg_get_lba_status,sg_readcap,sg_vpd,sg_unmap,
+.B sg_write_x(sg3_utils)
diff --git a/doc/sg_write_verify.8 b/doc/sg_write_verify.8
new file mode 100644
index 0000000..6a784e0
--- /dev/null
+++ b/doc/sg_write_verify.8
@@ -0,0 +1,191 @@
+.TH "WRITE AND VERIFY" "8" "January 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_write_and_verify \- send the SCSI WRITE AND VERIFY command
+.SH SYNOPSIS
+.B sg_write_verify
+[\fI\-\-16\fR] [\fI\-\-bytchk=BC\fR] [\fI\-\-dpo\fR] [\fI\-\-group=GN\fR]
+[\fI\-\-help\fR] [\fI\-\-ilen=ILEN\fR] [\fI\-\-in=IF\fR] \fI\-\-lba=LBA\fR
+[\fI\-\-num=NUM\fR] [\fI\-\-repeat\fR] [\fI\-\-timeout=TO\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] [\fI\-\-wrprotect=WP\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+Send a SCSI WRITE AND VERIFY (10) or (16) command to \fIDEVICE\fR. The
+data to be written is read from the \fIIF\fR file or, in its absence, a
+buffer full of 0xff bytes is used. The length of the data\-out buffer sent
+with the command is \fIILEN\fR bytes or, if that is not given, then it is
+the length of the \fIIF\fR file.
+.PP
+The write operation is to the \fIDEVICE\fR's medium (optionally to its cache)
+starting at logical block address \fILBA\fR for \fINUM\fR logical blocks.
+After the write to medium is performed a verify operation takes place which
+may viewed as a medium read (with appropriate checks) but without the data
+being returned. Additionally, if \fIBS\fR is set to one, the data read back
+from the medium in the verify operation is compared to the original data\-out
+buffer.
+.PP
+The relationship between the number of logical blocks to be written (i.e.
+\fINUM\fR) and the length (in bytes) of the data\-out buffer (i.e.
+\fIILEN\fR) may be simply found by multiplying the former by the logical
+block size. However if the \fIDEVICE\fR has protection information (PI)
+then it becomes a bit more complicated. Hence the calculation is left to
+the user with the default \fIILEN\fR, in the absence of the \fIIF\fR file,
+being set to \fINUM\fR * 512.
+.PP
+For sending large amounts of data to contiguous logical blocks, a single
+WRITE AND VERIFY command may not be appropriate (e.g. due to operating
+system limitations). In such cases see the REPEAT section below.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long option name.
+.TP
+\fB\-S\fR, \fB\-\-16\fR
+Send a WRITE AND VERIFY(16) command. The default is to send a WRITE AND
+VERIFY(10) command unless \fILBA\fR or \fINUM\fR are too large for the
+10 byte variant.
+.TP
+\fB\-b\fR, \fB\-\-bytchk\fR=\fIBC\fR
+where \fIBC\fR is the value to place in the command's BYTCHK field. Values
+between 0 and 3 (inclusive) are accepted. The default is value is 0 which
+implies only a write to the medium then a verify operation are performed. The
+only other value T10 defines currently is 1 which does performs an additional
+comparison between the data\-out buffer that was used by the write operation
+and the contents of the logical blocks read back from the medium.
+.TP
+\fB\-d\fR, \fB\-\-dpo\fR
+Set the DPO (disable page out) bit in the command. The default is to leave
+it clear.
+.TP
+\fB\-g\fR, \fB\-\-group\fR=\fIGN\fR
+where \fIGN\fR is the value to place in the command's GROUP NUMBER field.
+Values between 0 and 63 (inclusive) are accepted. The default is value is 0.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-I\fR, \fB\-\-ilen\fR=\fIILEN\fR
+where \fIILEN\fR is the number of bytes that will be placed in the data\-out
+buffer. If the \fIIF\fR file is given then no more than \fIILEN\fR bytes
+are read from that file. If the \fIIF\fR file does not contain \fIILEN\fR
+bytes then an error is reported. If the  \fIIF\fR file is not given then
+a data\-out buffer with \fIILEN\fR bytes of 0xff is sent.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIIF\fR
+read data (binary) from file named \fIIF\fR. If \fIIF\fR is "\-" then
+stdin is used. This data will become the data\-out buffer and will be written
+to the \fIDEVICE\fR's medium. If \fIBC\fR is 1 then that data\-out buffer
+will be held until after the verify operation and compared to the data read
+back from the medium.
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA\fR
+where \fILBA\fR is the logical block address to start the write to medium.
+Assumed to be in decimal unless prefixed with '0x' or has a trailing 'h'.
+Must be provided.
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fINUM\fR
+where \fINUM\fR is the number of blocks, starting at \fILBA\fR, to write
+to the medium. The default value for \fINUM\fR is 1.
+.TP
+\fB\-R\fR, \fB\-\-repeat\fR
+this option will continue to do WRITE AND VERIFY commands until the \fIIF\fR
+file is exhausted. This option requires both the \fI\-\-ilen=ILEN\fR and
+\fI\-\-in=IF\fR options to be given. Each command starts at the next logical
+block address and is for no more than \fINUM\fR blocks. The last command may
+be shorter with the number of blocks scaled as required. If there are
+residue bytes a warning is sent to stderr. See the REPEAT section.
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fITO\fR
+where \fITO\fR is the command timeout value in seconds. The default value is
+60 seconds. If \fINUM\fR is large then command may require considerably more
+time than 60 seconds to complete.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the degree of verbosity (debug messages).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+output version string then exit.
+.TP
+\fB\-w\fR, \fB\-\-wrprotect\fR=\fIWP\fR
+set the WRPROTECT field in the cdb to \fIWP\fR. The default value is 0 which
+implies no protection information is sent (along with the user data) in the
+data\-out buffer.
+.SH REPEAT
+For data sizes around a megabyte and larger, it may be appropriate to send
+multiple SCSI WRITE AND VERIFY commands due to operating system
+limitations (e.g. pass\-through SCSI interfaces often limit the amount
+of data that can be passed with a SCSI command). With this utility the
+mechanism for doing that is the \fI\-\-repeat\fR option.
+.PP
+In this mode the \fI\-\-ilen=ILEN\fR and \fI\-\-in=IF\fR options must be
+given. The \fIILEN\fR and \fINUM\fR values are treated as a per SCSI command
+parameters. Up to \fIILEN\fR bytes will be read from the \fIIF\fR file
+continually until it is exhausted. If the \fIIF\fR file is stdin, reading
+continues until an EOF is detected. The data read from each iteration becomes
+the data\-out buffer for a new WRITE AND VERIFY command.
+.PP
+The last read from the file (or stdin) may read less than \fIILEN\fR bytes
+in which case the number of logical blocks sent to the last WRITE AND VERIFY
+is scaled back accordingly. If there is a residual number of bytes left
+after that scaling then that is reported to stderr.
+.PP
+If an error occurs then that is reported to stderr and via the exit status
+and the utility stops at that point.
+.SH NOTES
+Other SCSI WRITE commands have a Force Unit Access (FUA) bit but that is
+set (implicitly) by WRITE AND VERIFY commands hence there is no option to set
+it. The data\-out buffer may still additionally be placed in the
+\fIDEVICE\fR's cache and setting the DPO bit is a hint not to do that.
+.PP
+Normal SCSI WRITEs can be done with the ddpt and the sg_dd utilities. The
+SCSI WRITE SAME command can be done with the sg_write_same utility while
+the SCSI COMPARE AND WRITE command (sg_compare_and_write utility) offers
+a "test and set" facility.
+.PP
+Various numeric arguments (e.g. \fILBA\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.SH EXIT STATUS
+The exit status of sg_write_verify is 0 when it is successful. If the verify
+operation fails that is typically indicated with a medium error which leads
+to an exit status of 3.
+.PP
+If \fIBC\fR is set to 1 and the comparison it causes fails this utility will
+indicate the miscompare with an exit status of 14. For other exit status
+values see the EXIT STATUS section in the sg3_utils(8) man page.
+.SH EXAMPLES
+To start with, a simple example: write 1 block of data held in file t.bin
+that is 512 bytes long then write that block to LBA 0x1234 on /dev/sg4 .
+.PP
+  # sg_write_verify \-\-lba=0x1234 \-\-in=t.bin /dev/sg4
+.PP
+Since '\-\-num=' is not given then it defaults to 1. Further the \fIILEN\fR
+value is obtained from the file size of t.bin . To additionally do a
+data\-out comparison to the read back data:
+.PP
+  # sg_write_verify \-l 0x1234 \-i t.bin --bytchk=1 /dev/sg4
+.PP
+The ddpt command can do copies between SCSI devices using READ and WRITE
+commands. However, currently it has no facility to promote those WRITES
+to WRITE AND VERIFY commands. Using a pipe, that could be done like this:
+.PP
+  # ddpt if=/dev/sg2 bs=512 bpt=8 count=11 of=\- |
+.br
+sg_write_verify \-\-in=\- \-l 0x567 \-n 8 \-\-ilen=4096 \-\-repeat /dev/sg4
+.PP
+Both ddpt and sg_write_verify are configured for segments of 8 512 byte
+logical blocks. Since 11 logical blocks are read then first 8 logical blocks
+are copied followed by a copy of the remaining 3 blocks. Since it is assumed
+that there is no protection information then the data\-in and data\-out
+buffers will be 4096 bytes each. For sg_write_verify this needs to be stated
+explicitly with the \-\-ilen=4096 option.
+.SH AUTHORS
+Bruno Goncalves and Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2014\-2018 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B ddpt(in a package of that name), sg_compare_and_write(8), sg_dd(8),
+.B sg_write_same(8)
diff --git a/doc/sg_write_x.8 b/doc/sg_write_x.8
new file mode 100644
index 0000000..90fefc6
--- /dev/null
+++ b/doc/sg_write_x.8
@@ -0,0 +1,600 @@
+.TH SG_WRITE_X "8" "October 2021" "sg3_utils\-1.47" SG3_UTILS
+.SH NAME
+sg_write_x \- SCSI WRITE normal/ATOMIC/SAME/SCATTERED/STREAM, ORWRITE commands
+.SH SYNOPSIS
+.B sg_write_x
+[\fI\-\-16\fR] [\fI\-\-32\fR] [\fI\-\-app\-tag=AT\fR] [\fI\-\-atomic=AB\fR]
+[\fI\-\-bmop=OP,PGP\fR] [\fI\-\-bs=BS\fR] [\fI\-\-combined=DOF\fR]
+[\fI\-\-dld=DLD\fR] [\fI\-\-dpo\fR] [\fI\-\-dry\-run\fR] [\fI\-\-fua\fR]
+[\fI\-\-generation=EOG,NOG\fR] [\fI\-\-grpnum=GN\fR] [\fI\-\-help\fR]
+\fI\-\-in=IF\fR [\fI\-\-lba=LBA[,LBA...]\fR] [\fI\-\-normal\fR]
+[\fI\-\-num=NUM[,NUM...]\fR] [\fI\-\-offset=OFF[,DLEN]\fR] [\fI\-\-or\fR]
+[\fI\-\-quiet\fR] [\fI\-\-ref\-tag=RT\fR] [\fI\-\-same=NDOB\fR]
+[\fI\-\-scat\-file=SF\fR] [\fI\-\-scat\-raw\fR] [\fI\-\-scattered=RD\fR]
+[\fI\-\-stream=ID\fR] [\fI\-\-strict\fR] [\fI\-\-tag\-mask=TM\fR]
+[\fI\-\-timeout=TO\fR] [\fI\-\-unmap=U_A\fR] [\fI\-\-verbose\fR]
+[\fI\-\-version\fR] [\fI\-\-wrprotect=WPR\fR] \fIDEVICE\fR
+.PP
+Synopsis per supported command:
+.PP
+.B sg_write_x
+\fI\-\-normal\fR \fI\-\-in=IF\fR [\fI\-\-16\fR] [\fI\-\-32\fR]
+[\fI\-\-app\-tag=AT\fR] [\fI\-\-bs=BS\fR] [\fI\-\-dld=DLD\fR] [\fI\-\-dpo\fR]
+[\fI\-\-fua\fR] [\fI\-\-grpnum=GN\fR] [\fI\-\-lba=LBA\fR] [\fI\-\-num=NUM\fR]
+[\fI\-\-offset=OFF[,DLEN]\fR] [\fI\-\-ref\-tag=RT\fR] [\fI\-\-strict\fR]
+[\fI\-\-tag\-mask=TM\fR] [\fI\-\-timeout=TO\fR] [\fI\-\-wrprotect=WPR\fR]
+\fIDEVICE\fR
+.PP
+.B sg_write_x
+\fI\-\-or\fR \fI\-\-in=IF\fR [\fI\-\-16\fR] [\fI\-\-32\fR]
+[\fI\-\-bmop=OP,PGP\fR] [\fI\-\-bs=BS\fR] [\fI\-\-dpo\fR] [\fI\-\-fua\fR]
+[\fI\-\-generation=EOG,NOG\fR] [\fI\-\-grpnum=GN\fR] [\fI\-\-lba=LBA\fR]
+[\fI\-\-num=NUM\fR] [\fI\-\-offset=OFF[,DLEN]\fR] [\fI\-\-strict\fR]
+[\fI\-\-timeout=TO\fR] [\fI\-\-wrprotect=OPR\fR] \fIDEVICE\fR
+.PP
+.B sg_write_x
+\fI\-\-atomic=AB\fR \fI\-\-in=IF\fR [\fI\-\-16\fR] [\fI\-\-32\fR]
+[\fI\-\-app-tag=AT\fR] [\fI\-\-bs=BS\fR] [\fI\-\-dpo\fR] [\fI\-\-fua\fR]
+[\fI\-\-grpnum=GN\fR] [\fI\-\-lba=LBA\fR] [\fI\-\-num=NUM\fR]
+[\fI\-\-offset=OFF[,DLEN]\fR] [\fI\-\-ref\-tag=RT\fR] [\fI\-\-strict\fR]
+[\fI\-\-timeout=TO\fR] [\fI\-\-wrprotect=WPR\fR] \fIDEVICE\fR
+.PP
+.B sg_write_x
+\fI\-\-same=NDOB\fR [\fI\-\-16\fR] [\fI\-\-32\fR] [\fI\-\-app-tag=AT\fR]
+[\fI\-\-bs=BS\fR] [\fI\-\-dpo\fR] [\fI\-\-fua\fR] [\fI\-\-grpnum=GN\fR]
+[\fI\-\-in=IF\fR] [\fI\-\-lba=LBA\fR] [\fI\-\-num=NUM\fR]
+[\fI\-\-offset=OFF[,DLEN]\fR] [\fI\-\-ref\-tag=RT\fR] [\fI\-\-strict\fR]
+[\fI\-\-timeout=TO\fR] [\fI\-\-unmap=U_A\fR]
+[\fI\-\-wrprotect=WPR\fR] \fIDEVICE\fR
+.PP
+.B sg_write_x
+\fI\-\-scattered=RD\fR \fI\-\-in=IF\fR [\fI\-\-16\fR] [\fI\-\-32\fR]
+[\fI\-\-app-tag=AT\fR] [\fI\-\-bs=BS\fR] [\fI\-\-dld=DLD\fR] [\fI\-\-dpo\fR]
+[\fI\-\-fua\fR] [\fI\-\-grpnum=GN\fR] [\fI\-\-lba=LBA[,LBA...]\fR]
+[\fI\-\-num=NUM[,NUM...]\fR] [\fI\-\-offset=OFF[,DLEN]\fR]
+[\fI\-\-ref\-tag=RT\fR] [\fI\-\-scat\-file=SF\fR] [\fI\-\-scat\-raw\fR]
+[\fI\-\-strict\fR] [\fI\-\-tag\-mask=TM\fR] [\fI\-\-timeout=TO\fR]
+[\fI\-\-wrprotect=WPR\fR] \fIDEVICE\fR
+.PP
+.B sg_write_x
+\fI\-\-stream=ID\fR \fI\-\-in=IF\fR [\fI\-\-16\fR] [\fI\-\-32\fR]
+[\fI\-\-app-tag=AT\fR] [\fI\-\-bs=BS\fR] [\fI\-\-dpo\fR] [\fI\-\-fua\fR]
+[\fI\-\-grpnum=GN\fR] [\fI\-\-lba=LBA\fR] [\fI\-\-num=NUM\fR]
+[\fI\-\-offset=OFF[,DLEN]\fR] [\fI\-\-ref\-tag=RT\fR] [\fI\-\-strict\fR]
+[\fI\-\-tag\-mask=TM\fR] [\fI\-\-timeout=TO\fR] [\fI\-\-wrprotect=WPR\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+This utility will send one of six SCSI commands, all associated with writing
+data to the given \fIDEVICE\fR. They are a "normal" WRITE, ORWRITE, WRITE
+ATOMIC, WRITE SAME, WRITE SCATTERED or WRITE STREAM. This utility supports
+the 16 and 32 byte variants of all six commands. Hence some closely related
+commands are not supported (e.g. WRITE(10)). All 32 byte variants, apart from
+ORWRITE(32), require the \fIDEVICE\fR to be formatted with type 1, 2 or 3
+Protection Information (PI), making all logical blocks 8 bytes (or a multiple
+of 8 bytes) longer on the media.
+.PP
+The command line interface is a little crowded with over thirty options. Hence
+the SYNOPSIS, after listing all the (long) options, lists those applicable
+to each supported command. For each command synopsis, the option that selects
+the SCSI command is shown first followed by any required options. If no
+command option is given then a "normal" WRITE is assumed. Even though the
+\fI\-\-scat\-file=SF\fR option can be given for every command, it is only
+shown for WRITE SCATTERED where it is most useful. If the
+\fI\-\-scat\-file=SF\fR option is given then neither the
+\fI\-\-lba=LBA[,LBA...]\fR nor the \fI\-\-num=NUM[,NUM...]\fR options
+should be given. Only the first item of the \fI\-\-lba=LBA[,LBA...]\fR and
+the \fI\-\-num=NUM[,NUM...]\fR options (or first pair (or quintet) from the
+\fI\-\-scat\-file=SF\fR option) is used for all but the WRITE SCATTERED
+command. All commands can take \fI\-\-dry\-run\fR and \fI\-\-verbose\fR in
+addition to those shown in the SYNOPSIS.
+.PP
+The logical block size in bytes can be given explicitly with the
+\fI\-\-bs=BS\fR option, as long as \fIBS\fR is greater than zero. It
+is typically a power of two, 512 or greater. If the \fI\-\-bs=BS\fR option
+is not given or \fIBS\fR is zero then the SCSI READ CAPACITY command is
+used to find the logical block size. First the READ CAPACITY(16) command is
+tried and if successful the logical block size in the response is typically
+used as the actual block size for this utility. The exception is when
+PROT_EN is set in the response and the \fI\-\-wrprotect=WPR\fR option is
+given and non\-zero; in which case 8 (bytes) is added to the logical block
+size to yield the actual block size used by this utility. If READ
+CAPACITY(16) fails then READ CAPACITY(10) is tried and if that works then
+the logical block size in the response is used as the actual block size.
+.PP
+The number of bytes this utility will attempt to read from the file named by
+\fIIF\fR is the product of the actual block size and the
+number_of_blocks (\fINUM\fR or the sum of \fINUM\fR arguments). If less bytes
+are read from the file \fIIF\fR and the \fI\-\-strict\fR option is given then
+this utility exits with an exit status of SG_LIB_FILE_ERROR. If less bytes
+are read from the file \fIIF\fR and the \fI\-\-strict\fR option is not
+given then bytes of zero are substituted for the "missing" bytes and this
+utility continues.
+.PP
+Attempts to write multi megabyte data with a single command are likely to fail
+for one of several reasons. First the operating system might object to
+allocating a buffer that large. Next the SCSI pass\-through usually limits
+data blocks to a few megabytes or less. Finally the storage device might
+have a limited amount of RAM to support a write operation such as atomic (as
+it may need to roll back). The storage device can inform the application
+client of its limitations via the block limits VPD page (0xb0), with the
+maximum atomic transfer length field amongst others.
+.PP
+A degenerate LBA (Logical Block Address) range descriptor with no PI has
+an LBA and NUM of zero. A degenerate LBA range descriptor with PI
+additionally has its RT, AT and TM fields set to zero (note: that is not
+the default values for RT, AT and TM). They are degenerate in the sense
+that they are indistinguishable from a pad of zeros that follow the scatter
+list in the data\-out buffer. SBC\-4 makes clear that a degenerate LBA
+range descriptor is valid. This may become an issue if \fIRD\fR given in the
+\fI\-\-scattered=RD\fR option has the value 0. In this case the logic may
+need to scan the user provided data to calculate the number of LBA
+range descriptors which is required by the WRITE SCATTERED cdb. In the
+absence of other information the logic will take a degenerate LBA range
+descriptor as a terminator of the scatter list.
+.PP
+The reference for these commands is SBC\-4 (T10/BSR INCITS 506\-2021)
+and draft SBC\-5 INCITS 571 revision 1 dated 21 May 2021. All six SCSI
+commands are described in those documents. WRITE ATOMIC was added in
+SBC\-4 revision 3; WRITE STREAM was added in SBC\-4 revision 7; WRITE
+SCATTERED was added in SBC\-4 revision 11 while the others are in the
+previous SBC\-3 standard.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+The options are arranged in alphabetical order based on the long
+option name.
+.TP
+\fB\-6\fR, \fB\-\-16\fR
+send the 16 byte cdb variant of the selected SCSI command. If no command
+is selected then the (normal) SCSI WRITE(16) command is sent. If neither
+this option nor the \fI\-\-32\fR option is given then this option is
+assumed.
+.TP
+\fB\-3\fR, \fB\-\-32\fR
+send the 32 byte cdb variant of the selected SCSI command. If no command
+is selected then the (normal) SCSI WRITE(32) command is sent. If neither
+this option nor the \fI\-\-16\fR option is given then then the
+\fI\-\-16\fR option is assumed. If both this option and the \fI\-\-16\fR
+option are given then this option takes precedence. Note that apart
+from ORWRITE(32) all other 32 byte cdb variants require a \fIDEVICE\fR
+formatted with type 1, 2 or 3 protection information.
+.TP
+\fB\-a\fR, \fB\-\-app\-tag\fR=\fIAT\fR
+where \fIAT\fR is the "expected logical block application tag" field found in
+most of the 32 byte cdb variants (the exception is ORWRITE(32)). \fIAT\fR is
+a 16 bit field which means the maximum value is 0xffff. The default value
+is 0xffff .
+.TP
+\fB\-A\fR, \fB\-\-atomic\fR=\fIAB\fR
+selects the WRITE ATOMIC command and \fIAB\fR is placed in the Atomic
+Boundary field of its cdb. It is a 16 bit field so the maximum value
+is 0xffff. If unsure what value to set, try 0 which will attempt to
+write the whole data\-out buffer in a single atomic operation.
+.TP
+\fB\-B\fR, \fB\-\-bmop\fR=\fIOP,PGP\fR
+where \fIOP\fR and \fIPGP\fR are the values to be placed in ORWRITE(32)'s
+BMOP and 'Previous Generation Processing' fields respectively. BMOP is
+a 3 bit field (ranges from 0 to 7) and PGP is a 4 bit field (ranges from
+0 to 15). Both fields default to 0.
+.TP
+\fB\-b\fR, \fB\-\-bs\fR=\fIBS\fR
+where \fIBS\fR is the logical block size or the actual block size which
+will be slightly bigger. The default value is zero. If this option
+is not given or is given with a \fIBS\fR of zero then the SCSI READ
+CAPACITY(16) command is sent to \fIDEVICE\fR. If that fails then the READ
+CAPACITY(10) command is sent. The logical and actual block size will be
+derived from the response of the READ CAPACITY command.
+.br
+This section assumes \fIBS\fR is greater than zero. If \fIBS\fR is less than
+512 (bytes) or not a multiple of 8, a warning is issued and the utility
+continues unless the \fI\-\-strict\fR option is also given. If \fIBS\fR
+is a power of two (e.g. 512) then the logical and actual block size is
+set to \fIBS\fR (e.g. 512). If \fIBS\fR is not a power of two (e.g. 520)
+then the logical block size is set to the closest power of two less than
+\fIBS\fR (e.g. 512) and the actual block size is set to \fIBS\fR (e.g.
+520).
+.br
+If the logical and actual block size are different then a later check
+will reduce the actual block size back to the logical block size unless
+\fI\-\-wrprotect=WPR\fR is greater than zero.
+.TP
+\fB\-c\fR, \fB\-\-combined\fR=\fIDOF\fR
+This option only applies to WRITE SCATTERED and assumes the whole data\-out
+buffer can be read from \fIIF\fR given by the \fI\-\-in=IF\fR option. The
+whole data\-out buffer is the parameter list header, followed by zero or more
+LBA range descriptors, optionally followed by some pad bytes and then the
+data to be written to the media. If the \fI\-\-lba=LBA[,LBA...]\fR,
+\fI\-\-num=NUM[,NUM...]\fR or \fI\-\-scat\-file=SF\fR options are also given
+then an error is generated. The \fIDOF\fR argument should be the value
+suitable for the 'Logical Block Data Offset' field in the WRITE SCATTERED
+cdb. This is the offset in the data\-out buffer where the data to write
+to the media commences. The unit of that field is the actual block size
+which is the logical block size plus a multiple of 8, if protection
+information (PI) is being sent. When \fIWPR\fR (from \fI\-\-wrprotect=WPR\fR)
+is greater than zero then PI is expected. SBC\-4 revision 15 does not state
+it but it would appear that a \fIDOF\fR value of 0 is invalid. It is
+suggested that this option be used with the \fI\-\-strict\fR option while
+experimenting as random or incorrect data fed in via the \fI\-\-in=IF\fR
+option could write a lot of "interesting" data all over the \fIDEVICE\fR.
+If \fIDOF\fR is given as 0 the utility will scan the data in \fIIF\fR until
+\fIRD\fR LBA range descriptors are found; or if \fIRD\fR is also 0 until a
+degenerate LBA range descriptor is found.
+.TP
+\fB\-D\fR, \fB\-\-dld\fR=\fIDLD\fR
+where \fIDLD\fR is the duration limits descriptor spread across 3 bits in
+the SCSI WRITE(16) and the WRITE SCATTERED(16) cdbs. \fIDLD\fR is between 0
+to 7 inclusive with a default of zero. The DLD0 field in WRITE(16) and WRITE
+SCATTERED(16) is set if (0x1 & \fIDLD\fR) is non\-zero. The DLD1 field in
+both cdbs is set if (0x2 & \fIDLD\fR) is non\-zero. The DLD2 field in both
+cdbs is set if (0x4 & \fIDLD\fR) is non\-zero.
+.TP
+\fB\-d\fR, \fB\-\-dpo\fR
+if this option is given then the DPO (disable page out) bit field in the
+cdb is set. The default is to clear this bit field. Applies to all
+commands supported by thus utility except WRITE SAME.
+.TP
+\fB\-x\fR, \fB\-\-dry\-run\fR
+this option exits (with a status of 0) just before it would otherwise send
+the selected SCSI write command. It may still send a SCSI READ CAPACITY
+command (16 byte variant and perhaps 10 byte variant as well) so the
+\fIDEVICE\fR is still required. It reads the data in and processes it if the
+\fI\-\-in=IF\fR and/or the \fI\-\-scat\-file=SF\fR options are given. All
+command line processing and sanity checks (e.g. if the \fI\-\-strict\fR
+option is given) will be performed and if there is an error then there will
+be a non zero exit status value.
+.br
+If this option is given twice (e.g. \-xx) then instead of performing the
+selected write SCSI command, the data\-out buffer is written to a file
+called sg_write_x.bin . If it doesn't exist then that file is created in
+the current directory and is truncated if it previously did exist with
+longer contents. The data\-out buffer is written in binary with some
+information about it written to stdout. For writes other than scattered
+the filename and its length in bytes is output to stdout. For write
+scattered additionally its number of LBA range descriptors and its
+logical block data offset written to stdout.
+.TP
+\fB\-f\fR, \fB\-\-fua\fR
+if this option is given then the FUA (force unit access) bit field in the
+cdb is set. The default is to clear this bit field. Applies to all
+commands supported by thus utility except WRITE SAME.
+.TP
+\fB\-G\fR, \fB\-\-generation\fR=\fIEOG,NOG\fR
+the arguments for this option are used by the ORWITE(32) command only.
+\fIEOG\fR is placed in the "Expected ORWgeneration" field while \fINOG\fR
+is placed in the "New ORWgeneration" field. Both are 32 bits long and
+default to zero.
+.TP
+\fB\-g\fR, \fB\-\-grpnum\fR=\fIGN\fR
+sets the 'Group number' field to \fIGN\fR. Defaults to a value of zero.
+\fIGN\fR should be a value between 0 and 63.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit. Use multiple times for more help.
+Currently '\-h' to '\-hhhh' provide different output.
+.TP
+\fB\-i\fR, \fB\-\-in\fR=\fIIF\fR
+read data (in binary) from a file named \fIIF\fR in a single OS system
+call (in Unix: read(2)). That data is placed in a continuous buffer and then
+used as the data\-out buffer for all SCSI write commands apart from WRITE
+SCATTERED(16 or 32) which may include other data in the data\-out buffer.
+For WRITE SCATTERED (16 or 32) the data\-out buffer is made up of 3 or 4
+components in this order: a parameter list header (32 zero bytes); zero or
+more LBA range descriptors, optionally some pad bytes (zeros) and then data
+to write to the media. For WRITE SCATTERED \fIIF\fR only provides the data
+to write to the media unless \fI\-\-combined=DOF\fR is given. When the
+\fI\-\-combined=DOF\fR option is given \fIIF\fR contains all components of
+the WRITE SCATTERED data\-out buffer in binary. The data read from \fIIF\fR
+starts from byte offset \fIOFF\fR which defaults to zero and no more than
+\fIDLEN\fR bytes are read from that point (i.e. from the file byte offset
+\fIOFF\fR). If \fIDLEN\fR is zero or not given the rest of the file \fIIF\fR
+is read. This option is mandatory apart from when \-\-same=1 is given (that
+sets the NDOB bit which stands for "No Data Out Buffer"). In Unix based
+OSes, any number of zeros can be produced by using the /dev/zero device file.
+.br
+\fIIF\fR may be "\-" which is taken as stdin. In this case the
+\fI\-\-offset=OFF,DLEN\fR can be given with \fIOFF\fR set to 0 and
+\fILEN\fR set to a non\-zero value, preferably a multiple of the actual block
+size. The utility can also deduce how long the \fIIF\fR should be from
+\fINUM\fR (or the sum of them in the case of a scatter list).
+.TP
+\fB\-l\fR, \fB\-\-lba\fR=\fILBA[,LBA...]\fR
+where the argument is a single Logical Block Address (LBA) or a comma
+separated list of \fILBA\fRs each of which is the address of the first block
+written by the selected write command. Only the WRITE SCATTERED command
+can usefully take more than one \fILBA\fR. Whatever number of \fILBA\fRs is
+given, there needs to be an equal number of \fINUM\fRs given to the
+\fI\-\-num=NUM[,NUM...]\fR option. The first given \fILBA\fR joins with the
+first given \fINUM\fR to form the first LBA range descriptor (which T10
+number from zero in SBC\-4). The second \fILBA\fR joins with the second
+\fILBA\fR to form the second LBA range descriptor, etc. A more convenient
+way to define a large number of LBA range descriptors is with the
+\fI\-\-scat\-file=SF\fR option. Defaults to logical block 0 (which could be
+dangerous) while \fINUM\fR defaults to 0 which makes the combination harmless.
+\fILBA\fR is assumed to be in decimal unless prefixed with '0x' or has a
+trailing 'h'.
+.TP
+\fB\-N\fR, \fB\-\-normal\fR
+the choice of a "normal" WRITE (16 or 32) command can be made explicitly
+with this option. In the absence of selecting any other command (e.g.
+\fI\-\-atomic=AB\fR ), the choice of a "normal" WRITE is the default.
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fINUM[,NUM...]\fR
+where the argument is a single NUMber of blocks (NUM) or a comma separated
+list of \fINUM\fRs that pair with the corresponding entries in the
+\fI\-\-lba=LBA[,LBA...]\fR option. If a \fINUM\fR is given and is not
+provided by another method (e.g. by using the \fI\-\-scat\-file=SF\fR option)
+then it defaults to the number of blocks derived from the size of the file
+named by \fIIF\fR (starting at byte offset \fIOFF\fR to the end or the file
+or \fIDLEN\fR). Apart from the \fI\-\-combined=DOF\fR option, an LBA must
+be explicitly given (either with \fII\-\-lba=LBA\fR or via
+\fI\-\-scat\-file=SF\fR), if not \fINUM\fR defaults to 0 as a safety measure.
+.TP
+\fB\-o\fR, \fB\-\-offset\fR=\fIOFF[,DLEN]\fR
+where \fIOFF\fR is the byte offset within the file named \fIIF\fR to start
+reading from. The default value of \fIOFF\fR is zero which is the beginning
+of file named \fIIF\fR. \fIDLEN\fR is the maximum number of bytes to read,
+starting at byte offset \fIOFF\fR, from the file named \fIIF\fR. Less bytes
+will be read if an end of file occurs before \fIDLEN\fR is exhausted. If
+\fIDLEN\fR is zero or not given then reading from byte offset \fIOFF\fR to
+the end of the file named \fIIF\fR is assumed.
+.TP
+\fB\-O\fR, \fB\-\-or\fR
+selects the ORWRITE command. ORWRITE(16) has similar fields to WRITE(16)
+apart from the WRPROTECT field being named ORPROTECT with slightly different
+semantics and the absence of the 3 DLD bit fields. ORWRITE(32) has four
+extra fields that are set with the \fI\-\-bmop=OP,PGP\fR and
+\fI\-\-generation=EOG,NOG\fR options. ORWRITE(32) is the only 32 byte cdb
+command in this utility that does not require a \fIDEVICE\fR formatted with
+type 1, 2 or 3 PI (although it will still work if it is formatted with PI).
+.TP
+\fB\-Q\fR, \fB\-\-quiet\fR
+suppress some informational messages such as the ones associated with
+detected errors when this utility is about to exit. The exit status value
+is still returned to the operating system when this utility exits.
+.TP
+\fB\-r\fR, \fB\-\-ref\-tag\fR=\fIRT\fR
+where \fIRT\fR is the "expected initial logical block reference tag" field
+found in the 32 byte cdb variants of WRITE, WRITE ATOMIC, WRITE SAME and
+WRITE STREAM.  The field is also found in the WRITE SCATTERED(32) LBA range
+descriptors. It is a 32 bit field which means the maximum value is
+0xffffffff. The default value is 0xffffffff.
+.TP
+\fB\-S\fR, \fB\-\-same\fR=\fINDOB\fR
+selects the WRITE SAME command with the NDOB field set to \fINDOB\fR which
+stands for No Data\-Out Buffer. \fINDOB\fR can take values 0 or 1 (i.e. it
+is a single bit field). When \-\-same=1 all options associated with the
+data\-out buffer are ignored.
+.TP
+\fB\-q\fR, \fB\-\-scat\-file\fR=\fISF\fR
+where \fISF\fR is the name of an auxiliary file containing the scatter list
+for the WRITE SCATTERED command. If the \fI\-\-scat\-raw\fR option is also
+given then \fISF\fR is assumed to contain both the parameter list header (32
+bytes of zeros) followed by zero or more LBA range descriptors which are
+also 32 bytes long each. These components are as defined by SBC\-4 (i.e.
+in binary with integers in big endian format). If the \fI\-\-scat\-raw\fR
+option is not given then a file of ACSII hexadecimal is expected as described
+in the SCATTERED FILE ASCII FORMAT section below.
+.br
+If this option is given with the \fI\-\-combined=DOF\fR option then this
+utility will exit with a syntax error. \fISF\fR must not be "\-", a way
+of stopping the user trying to redirect stdin.
+.TP
+\fB\-R\fR, \fB\-\-scat\-raw\fR
+this option only effects the way that the file named \fISF\fR from the
+\fI\-\-scat\-file=SF\fR option for WRITE SCATTERED is interpreted. By
+default (i.e. without this option), \fISF\fR is parsed as ASCII hexadecimal
+with blank lines and line contents from and including '#' to the end of
+line ignored. Hence it can contain comments and other indications. When
+this option is given, the file named \fISF\fR is interpreted as binary.
+As binary it is assumed to contain 32 bytes of zeros (the WRITE SCATTERED
+parameter list header) followed by zero or more LBA range descriptors (which
+are 32 bytes each). If the \fI\-\-strict\fR option is given the reserved
+field in those two items are checked with any non zero bytes causing an
+error.
+.TP
+\fB\-S\fR, \fB\-\-scattered\fR=\fIRD\fR
+selects the WRITE SCATTERED command with \fIRD\fR being the number of LBA
+range descriptors that will be placed in the data\-out buffer. If \fIRD\fR
+is zero then the logic will try and determine the number of range descriptors
+by other means (e.g. by parsing the file named by \fISF\fR, if there is one).
+The LBA range descriptors differ between the 16 and 32 byte cdb variants of
+WRITE SCATTERED. In the 16 byte cdb variant the 32 byte LBA range descriptor
+is made up of an 8 byte LBA, followed by a 4 byte number_of_blocks followed
+by 20 bytes of zeros. In the 32 byte variant the LBA and number_of_blocks
+are followed by a RT (4 bytes), an AT (2 bytes) and a TM (2 bytes) then
+12 bytes of zeros.
+.br
+This paragraph applies when \fIRD\fR is greater than zero.
+If \fIRD\fR is less than the number of LBA range descriptors built from
+command line options, from the \fI\-\-scat\-file=SF\fR option or
+decoded from \fIIF\fR (when the \fI\-\-combined=DOF\fR option is given)
+then \fIRD\fR takes precedence; so \fIRD\fR is placed in the "Number of
+LBA Range Descriptors" field in the cdb. If \fIRD\fR is greater than
+the number of LBA range descriptors found from the provided data and
+options, then an error is generated.
+.TP
+\fB\-T\fR, \fB\-\-stream\fR=\fIID\fR
+selects the WRITE STREAM command with the STR_ID field set to \fIID\fR.
+\fIID\fR can take values from 0 to 0xffff (i.e. it is a 16 bit field).
+.TP
+\fB\-s\fR, \fB\-\-strict\fR
+when this option is present, more things (e.g. that reserved fields contain
+zeros) and any irregularities will terminate the utility with a message to
+stderr and an indicative exit status. While experimenting with these commands,
+especially WRITE SCATTERED, it is recommended to use this option.
+.TP
+\fB\-t\fR, \fB\-\-tag\-mask\fR=\fITM\fR
+where \fITM\fR is the "logical block application tag mask" field  found in the
+32 byte cdb variants of WRITE, WRITE ATOMIC, WRITE SAME and WRITE STREAM. The
+field is also found in the WRITE SCATTERED(32) LBA range descriptors. It is a
+16 bit field which means the maximum value is 0xffff. The default value is
+0xffff.
+.TP
+\fB\-I\fR, \fB\-\-timeout\fR=\fITO\fR
+where \fITO\fR is the command timeout value in seconds. The default value is
+120 seconds. If \fINUM\fR is large on slow media then these WRITE commands
+may require considerably more time than 120 seconds to complete.
+.TP
+\fB\-u\fR, \fB\-\-unmap\fR=\fIU_A\fR
+where \fIU_A\fR is OR\-ed bit values used to set the UNMAP and ANCHOR bit
+fields in the WRITE SAME (16 or 32) cdb. If \fIU_A\fR is 1 then the UNMAP
+bit field is set; if \fIU_A\fR is 2 then the ANCHOR bit field is set; if
+\fIU_A\fR is 3 then both the UNMAP and ANCHOR bit fields are set. The
+default value for both bit fields is clear (0); setting \fIU_A\fR to 0 will
+also clear both bit fields.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the degree of verbosity (debug messages). These messages are usually
+written to stderr.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+output version string then exit.
+.TP
+\fB\-w\fR, \fB\-\-wrprotect\fR=\fIWPR\fR
+sets the WRPROTECT field (3 bits) in all sg_write_x commands apart from
+ORWRITE which has a 3 bit ORPROTECT field (and the synopsis shows \fIOPR\fR
+to highlight the difference). In all cases \fIWPR\fR is placed
+in that 3 bit field. The default value is zero which does not send any PI
+in the data\-out buffer. \fIWPR\fR should be a value between 0 and 7.
+.SH SCATTERED FILE ASCII FORMAT
+All commands in this utility can take a \fI\-\-scat\-file=SF\fR and that
+option can be seen as a replacement for the \fI\-\-lba=LBA[,LBA...]\fR and
+\fI\-\-num=NUM[,NUM...]\fR options. if both the \fI\-\-scat\-file=SF\fR and
+\fI\-\-scat\-raw\fR options are given then the file named \fISF\fR is
+expected to be binary and contain the parameter list header (32 bytes of
+zeros for both the 16 and 32 byte variants) followed by zero or more LBA
+range descriptors, each of 32 bytes each. This section describes what is
+expected in \fISF\fR when the \fI\-\-scat\-raw\fR option is not given.
+.PP
+The ASCII hexadecimal "scatter file" (named by \fISF\fR) can contain
+comments, empty lines and numbers. If multiple numbers appear on one line
+they can be separated by spaces, tabs or a single comma. Numbers are parsed
+as decimal unless prefixed by "0x" (or "0X") or have a suffix of "h". Ox is
+the prefix of hexadecimal number is the C language while T10 uses the "h"
+suffix for the same purpose. Anything from and including a "#" character
+to the end\-of\-line is ignored, so comments can be placed there.
+.PP
+For the WRITE SCATTERED (16) command, its LBA range descriptors contain two
+items per descriptor: an 8 byte LBA followed by a 4 byte number_of_blocks.
+The remaining 20 bytes of the descriptor are zeros. The format accepted
+is relatively loose with each decoded value being placed in an LBA and
+then a number_of_blocks until the end\-of\-file is reached. The pattern
+starts with a LBA and if it doesn't finish with a number_of_blocks (i.e.
+an odd number of values are parsed) an error occurs. So the number of
+LBA range descriptors generated will be half the number of values parsed
+in \fISF\fR.
+.PP
+For the WRITE SCATTERED (32) command, its LBA range descriptors contain five
+items per descriptor: an 8 byte LBA followed by a 4 byte number_of_blocks,
+then a 4 byte RT, a 2 byte AT, and a 2 byte TM. The last three items are
+associated with protection information (PI). The accepted format in the
+\fISF\fR file is more constrained than the 16 byte cdb variant. The items
+for each LBA range descriptor must be found on one line with adjacent items
+being comma separated. The first two items (LBA and number_of_blocks) must be
+given, and if no more items are on the line then RT, AT and TM are given
+their default values (all "ff" bytes). Spaces and tabs may appear between
+items but commas are the separators. Two commas with no value between them
+will cause the "missing" item to receive its default value.
+.SH NOTES
+Various numeric arguments (e.g. \fILBA\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+In Linux, prior to lk 3.17, the sg driver did not support cdb sizes greater
+than 16 bytes. Hence a device node like /dev/sg1 which is associated with
+the sg driver would fail with this utility if the \fI\-\-32\fR option was
+given (or implied by other options). The bsg driver with device nodes like
+/dev/bsg/6:0:0:1 does support cdb sizes greater than 16 bytes since its
+introduction in lk 2.6.28 .
+.SH EXIT STATUS
+The exit status of sg_write_x is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH EXAMPLES
+One simple usage is to write 4 blocks of zeros from (and including) a given
+LBA according to the rules of WRITE ATOMIC with an atomic boundary of 0.
+Since no cdb size option is given, the 16 byte cdb will be assumed (i.e.
+WRITE ATOMIC(16)):
+.PP
+  sg_write_x \-\-atomic=0 \-\-in=/dev/zero \-\-lba=0x1234 \-\-num=4 /dev/sdc
+.PP
+Since \fI\-\-bs=BS\fR has not been given, then this utility will call the
+READ CAPACITY(16) command on /dev/sdc to determine the number of bytes in a
+logical block. If the READ CAPACITY(16) command fails then the READ
+CAPACITY(10) command is tried. Let us assume one of them works and that
+the number of bytes in each logical block is 512 bytes. So 4 blocks of
+zeros (each block containing 512 bytes) will be written from (and including)
+LBA 0x1234 . Now to bypass the need for the READ CAPACITY command(s) the
+\fI\-\-bs=BS\fR option can be used:
+.PP
+  sg_write_x \-\-atomic=0 \-\-bs=512 \-\-in=/dev/zero \-\-lba=0x1234 \-\-num=4
+/dev/sdc
+.PP
+Since \-\-bs= is given and its value (512) is a power of 2, then the actual
+block size is also 512. If instead 520 was given then the logical block size
+would be 512 (the highest power of 2 less than 520) and the actual block size
+would be 520 bytes. To send the 32 byte variant add \-\-32 as in:
+.PP
+  sg_write_x \-\-atomic=0 \-\-32 \-\-bs=512 \-\-in=/dev/zero \-\-lba=0x1234
+\-\-num=4 /dev/sdc
+.PP
+For examples using 'sg_write_x \-\-same=NDOB' see the manpage for
+sg_write_same(8). The syntax is a little different but the semantics are the
+same.
+.PP
+To send a WRITE STREAM(32) with a STR_ID of 1 use the following:
+.PP
+  sg_write_x \-\-stream=1 \-\-32 \-\-bs=512 \-\-in=/dev/zero \-\-lba=0x1234
+\-\-num=4 /dev/sdc
+.PP
+Next is a WRITE SCATTERED(16) command with the scatter list, split between
+the \-\-lba= and \-\-num= options, on the command line:
+.PP
+  sg_write_x  \-\-scattered=2 \-\-lba=2,0x33 \-\-num=4,1 -i /dev/zero /dev/sg1
+.PP
+Example of a WRITE SCATTERED(16) command with a degenerate LBA range
+descriptor (first element to \-\-lba= and \-\-num=):
+.PP
+  sg_write_x  \-\-scattered=2 \-\-lba=0,0x33 \-\-num=0,1 -i /dev/zero /dev/sg1
+.PP
+Example of a WRITE SCATTERED(16) command with the scatter list in
+scat_file.txt
+.PP
+  sg_write_x  \-\-scattered=3 \-q scat_file.txt \-i /dev/zero /dev/sg1
+.PP
+Next a WRITE SCATTERED(16) command with its scatter list and data in a
+single file. Note that the argument to \-\-scattered= is 0 so the number of
+LBA range descriptors is calculated by analyzing the first two blocks of
+scat_data.bin (because the argument to \-\-combined= is 2) :
+.PP
+  sg_write_x  \-\-scattered=0 \-\-combined=2 \-i scat_data.bin /dev/sg1
+.PP
+When the \-xx option is used, a WRITE SCATTERED command is not executed
+but instead the contents of the data\-out buffer are written to a file
+called sg_write_x.bin . In the case of WRITE SCATTERED that binary file
+is suitable for supplying to a later invocation to do the actual write
+to media. For example:
+.PP
+  sg_write_x  \-\-scattered=3 \-q scat_file.txt \-xx \-i /dev/zero /dev/sg1
+.br
+Wrote 8192 bytes to sg_write_x.bin, LB data offset: 1
+.br
+Number of LBA range descriptors: 3
+.br
+  sg_write_x  \-\-scattered=0 \-\-combined=1 \-i sg_write_x.bin /dev/sg1
+.PP
+Notice when the sg_write_x.bin is written (and nothing is written to the
+media), a summary of what has happened is sent to stdout. The value shown
+for "LB data offset:" (1) should be given to the \-\-combined= option
+when the write to media actually occurs (i.e. the second invocation shown
+directly above).
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2017\-2021 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_readcap,sg_vpd,sg_write_same,sg_stream_ctl(sg3_utils)
diff --git a/doc/sg_xcopy.8 b/doc/sg_xcopy.8
new file mode 100644
index 0000000..1c2cd17
--- /dev/null
+++ b/doc/sg_xcopy.8
@@ -0,0 +1,381 @@
+.TH SG_XCOPY "8" "September 2021" "sg3_utils\-1.47" SG3_UTILS
+.SH NAME
+sg_xcopy \- copy data to and from files and devices using SCSI EXTENDED
+COPY (XCOPY)
+.SH SYNOPSIS
+.B sg_xcopy
+[\fIbs=BS\fR] [\fIconv=CONV\fR] [\fIcount=COUNT\fR] [\fIibs=BS\fR]
+[\fIif=IFILE\fR] [\fIiflag=FLAGS\fR] [\fIobs=BS\fR] [\fIof=OFILE\fR]
+[\fIoflag=FLAGS\fR] [\fIseek=SEEK\fR] [\fIskip=SKIP\fR] [\fI\-\-help\fR]
+[\fI\-\-version\fR]
+.PP
+[\fIapp=\fR0|1] [\fIbpt=BPT\fR] [\fIcat=\fR0|1] [\fIdc=\fR0|1] [\fIfco=\fR0|1]
+[\fIid_usage=\fR{hold|discard|disable}] [\fIlist_id=ID\fR] [\fIprio=PRIO\fR]
+[\fItime=\fR0|1] [\fIverbose=VERB\fR] [\fI\-\-on_dst|\-\-on_src\fR]
+[\fI\-\-verbose\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Copy data to and from any files. Specialized for "files" that are Linux SCSI
+devices that support the SCSI EXTENDED COPY (XCOPY) command.
+.PP
+This utility
+has similar syntax and semantics to
+.B dd(1)
+but with no "conversions" is supported.
+.PP
+The first group in the synopsis above are "standard" Unix
+.B dd(1)
+operands. The second group are extra options added by this utility.
+Both groups are defined below in combined, alphabetical order.
+.PP
+By default the XCOPY command is sent to \fIOFILE\fR. This can be changed
+with the \fI\-\-on_src\fR or \fIiflag=xflag\fR options which cause the XCOPY
+command to be sent to \fIIFILE\fR instead. Also see the section on
+ENVIRONMENT VARIABLES.
+.PP
+In the SPC\-4 standard the T10 committee has expanded the XCOPY command so
+that it now has two variants: "LID1" (for a List Identifier length of 1 byte)
+and "LID4" (for a List Identifier length of 4 bytes). This utility supports
+the older, LID1 variant which is also found in SPC\-3 and earlier. While the
+LID1 variant in SPC\-4 is command level (binary) compatible with XCOPY as
+defined in SPC\-3, some of the command naming has changed. This utility uses
+the older, SPC\-3 XCOPY names.
+.PP
+The ddpt utility supports the same xcopy(LID1) functionality as this utility
+with the same options and flags. Additionally ddpt supports a subset of
+xcopy(LID4) functionality variously called "xcopy version 2, lite" or ODX.
+ODX is a market name and stands for Offloaded Data Xfer (i.e. transfer).
+.SH OPTIONS
+.TP
+\fBapp\fR={0|1}
+if 1 start the destination of the copy at the end of OFILE. This assumes
+that OFILE is a regular file. The default is 0 in which case the destination
+of the copy starts at the beginning of OFILE (possibly offset be SEEK). This
+option cannot be used with the \fIseek=SEEK\fR option.
+.TP
+\fBbpt\fR=\fIBPT\fR
+each IO transaction will be made using \fIBPT\fR blocks (or less if near
+the end of the copy). Default is 128 for logical block sizes less that 2048
+bytes, otherwise the default is 32. So for bs=512 the reads and writes
+will each convey 64 KiB of data by default (less if near the end of the
+transfer or memory restrictions). When cd/dvd drives are accessed, the
+logical block size is typically 2048 bytes and bpt defaults to 32 which again
+implies 64 KiB transfers.
+.TP
+\fBbs\fR=\fIBS\fR
+where \fIBS\fR
+.B must
+be the logical block size of the physical device (if either the input or
+output files are accessed via SCSI commands). Note that this differs from
+.B dd(1)
+which permits \fIBS\fR to be an integral multiple. Defaults to the
+device logical block size.
+.TP
+\fBcat\fR={0|1}
+sets the SCSI EXTENDED COPY command segment descriptor CAT bit to 0 or
+1 (default: 0). The CAT bit (in conjunction with the PAD bit) controls
+the handling of residual data. See section
+.B HANDLING OF RESIDUAL DATA
+for details.
+.TP
+\fBconv\fR=\fBCONV\fR
+all \fBCONV\fR arguments are ignored.
+.TP
+\fBcount\fR=\fICOUNT\fR
+copy \fICOUNT\fR blocks from \fIIFILE\fR to \fIOFILE\fR. Default is the
+minimum (\fIIFILE\fR if \fIdc=0\fR or \fIOFILE\fR if \fIdc=1\fR)
+number of blocks that SCSI devices report from SCSI READ CAPACITY
+commands or that block devices (or their partitions) report. Normal
+files are not probed for their size. If \fIskip=SKIP\fR or
+\fIseek=SEEK\fR are given and the count is derived (i.e. not
+explicitly given) then the derived count is scaled back so that the
+copy will not overrun the device. If the file name is a block device
+partition and \fICOUNT\fR is not given then the size of the partition
+rather than the size of the whole device is used. If \fICOUNT\fR is
+not given (or \fIcount=\-1\fR) and cannot be derived then an error
+message is issued and no copy takes place.
+.TP
+\fBdc\fR={0|1}
+sets the SCSI EXTENDED COPY command segment descriptor DC bit to 0 or
+1 (default: 0). The DC bit controls whether \fICOUNT\fR
+refers to the source (\fIdc=0\fR) or the target (\fIdc=1\fR) descriptor.
+.TP
+\fBfco\fR={0|1}
+sets the SCSI EXTENDED COPY command segment descriptor FCO bit to 0 or
+1 (default: 0). The Fast Copy Only (FCO) bit set will result in the
+copy being done but a technique faster than SCSI READ and WRITE commands.
+If the copy cannot but done in a faster manner then a sense key of "Copy
+aborted" with and additional sense of "Fast copy not possible" is
+returned.
+.TP
+\fBibs\fR=\fIBS\fR
+if given must be the same as \fIBS\fR given to 'bs=' option.
+.TP
+\fBid_usage\fR={hold|discard|disable}
+sets the SCSI EXTENDED COPY command parameter list field called LIST ID
+USAGE to 0 if the argument is 'hold', to 2 if the argument is 'discard',
+or to '3' if the argument is 'disable'.
+.br
+If the device has the ability to hold data (as indicated by "held data
+limit" being greater than zero) then \fIid_usage\fR defaults to 'hold'
+otherwise it defaults to 'discard'.
+.TP
+\fBif\fR=\fIIFILE\fR
+read from \fIIFILE\fR instead of stdin. If \fIIFILE\fR is '\-' then stdin
+is read. Starts reading at the beginning of \fIIFILE\fR unless \fISKIP\fR
+is given.
+.TP
+\fBiflag\fR=\fIFLAGS\fR
+where \fIFLAGS\fR is a comma separated list of one or more flags outlined
+below.  These flags are associated with \fIIFILE\fR and are ignored when
+\fIIFILE\fR is stdin.
+.TP
+\fBlist_id\fR=\fIID\fR
+sets the SCSI EXTENDED COPY command parameter list field called LIST
+IDENTIFIER to \fIID\fR. \fIID\fR should be a value between 0 and
+255 (inclusive). \fIID\fR usually defaults to 1 unless
+\fIid_usage=disable\fR in which case it defaults to 0.
+.TP
+\fBobs\fR=\fIBS\fR
+if given must be the same as \fIBS\fR given to 'bs=' option.
+.TP
+\fBof\fR=\fIOFILE\fR
+write to \fIOFILE\fR instead of stdout. If \fIOFILE\fR is '\-' then writes
+to stdout.  If \fIOFILE\fR is /dev/null then no actual writes are performed.
+If \fIOFILE\fR is '.' (period) then it is treated the same way as
+/dev/null (this is a shorthand notation). If \fIOFILE\fR exists then it
+is _not_ truncated; it is overwritten from the start of \fIOFILE\fR
+unless 'oflag=append' or \fISEEK\fR is given.
+.TP
+\fBoflag\fR=\fIFLAGS\fR
+where \fIFLAGS\fR is a comma separated list of one or more flags outlined
+below.  These flags are associated with \fIOFILE\fR and are ignored when
+\fIOFILE\fR is /dev/null, '.' (period), or stdout.
+.TP
+\fBprio\fR=\fIPRIO\fR
+sets the SCSI EXTENDED COPY command parameter list field called PRIORITY
+to \fIPRIO\fR.  The default value is 1.
+.TP
+\fBseek\fR=\fISEEK\fR
+start writing \fISEEK\fR bs\-sized blocks from the start of \fIOFILE\fR.
+Default is block 0 (i.e. start of file).
+.TP
+\fBskip\fR=\fISKIP\fR
+start reading \fISKIP\fR bs\-sized blocks from the start of \fIIFILE\fR.
+Default is block 0 (i.e. start of file).
+.TP
+\fBtime\fR={0|1}
+when 1, times transfer and does throughput calculation, outputting the
+results (to stderr) at completion. When 0 (default) doesn't perform timing.
+.TP
+\fBverbose\fR=\fIVERB\fR
+as \fIVERB\fR increases so does the amount of debug output sent to stderr.
+Default value is zero which yields the minimum amount of debug output.
+A value of 1 reports extra information that is not repetitive. A value
+2 reports cdbs and responses for SCSI commands that are not repetitive
+(i.e. other that READ and WRITE). Error processing is not considered
+repetitive. Values of 3 and 4 yield output for all SCSI commands (and
+Unix read() and write() calls) so there can be a lot of output.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs usage message and exits.
+.TP
+\fB\-\-on_dst\fR
+send the XCOPY command to the output file/device (i.e. \fIOFILE\fR). This is
+the default unless overridden by the \fI\-\-on_src\fR or \fIiflag=xflag\fR
+options. Also see the section below on ENVIRONMENT VARIABLES.
+.TP
+\fB\-\-on_src\fR
+send the XCOPY command to the input file/device (i.e. \fIIFILE\fR).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+equivalent to \fIverbose=1\fR. When used twice, equivalent to
+\fIverbose=2\fR, etc.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+outputs version number information and exits.
+.SH FLAGS
+Here is a list of flags and their meanings:
+.TP
+append
+causes the O_APPEND flag to be added to the open of \fIOFILE\fR. For regular
+files this will lead to data appended to the end of any existing data.
+Cannot be used together with the \fIseek=SEEK\fR option as they conflict.
+The default action of this utility is to overwrite any existing data
+from the beginning of the file or, if \fISEEK\fR is given, starting at
+block \fISEEK\fR. Note that attempting to 'append' to a device file (e.g.
+a disk) will usually be ignored or may cause an error to be reported.
+.TP
+excl
+causes the O_EXCL flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR.
+.TP
+flock
+after opening the associated file (i.e. \fIIFILE\fR and/or \fIOFILE\fR)
+an attempt is made to get an advisory exclusive lock with the flock()
+system call. The flock arguments are "FLOCK_EX | FLOCK_NB" which will
+cause the lock to be taken if available else a "temporarily unavailable"
+error is generated. An exit status of 90 is produced in the latter case
+and no copy is done.
+.TP
+null
+has no affect, just a placeholder.
+.TP
+pad
+sets the SCSI EXTENDED COPY command segment descriptor PAD bit. The
+PAD bit (in conjunction with the CAT bit) controls the handling of
+residual data.(See section
+.B HANDLING OF RESIDUAL DATA
+for details.
+.TP
+xcopy
+has no affect; for compatibility with ddpt.
+.SH HANDLING OF RESIDUAL DATA
+The \fIpad\fR and \fIcat\fR bits control the handling of residual
+data. As the data can be specified either in terms of source or target
+logical block size and both might have different block sizes residual data
+is likely to happen in these cases.
+If both logical block sizes are identical these bits have no effect as
+residual data will not occur.
+.PP
+If none of these bits are set, the EXTENDED COPY command will be
+aborted with additional sense 'UNEXPECTED INEXACT SEGMENT'.
+.PP
+If only the \fIcat\fR bit is set the residual data will be retained
+and made available for subsequent segment descriptors. Residual data
+will be discarded for the last segment descriptor.
+.PP
+If the \fIpad\fR bit is set for the source descriptor only, any
+residual data for both source or destination will be discarded.
+.PP
+If the \fIpad\fR bit is set for the target descriptor only any
+residual source data will be handled as if the \fIcat\fR bit is set,
+but any residual destination data will be padded to make a whole block
+transfer.
+.PP
+If the \fIpad\fR bit is set for both source and target any residual
+source data will be discarded, and any residual destination data will
+be padded.
+.SH ENVIRONMENT VARIABLES
+If the command line invocation does not explicitly (and unambiguously)
+indicate whether the XCOPY SCSI command should be sent to \fIIFILE\fR (i.e.
+the source) or \fIOFILE\fR (i.e. the destination) then a check is
+made for the presence of the XCOPY_TO_SRC and XCOPY_TO_DST environment
+variables. If either one exists (but not both) then it indicates where
+the SCSI XCOPY command will be sent. By default the XCOPY command is
+sent to \fIOFILE\fR.
+.SH RETIRED OPTIONS
+Here are some retired options that are still present:
+.TP
+append=0 | 1
+when set, equivalent to 'oflag=append'. When clear the action is
+to overwrite the existing file (if it exists); this is the default.
+See the 'append' flag.
+.SH NOTES
+Copying data behind an Operating System's back can cause problems. In the
+case of Linux, users should look at this link:
+https://linux\-mm.org/Drop_Caches
+.br
+This command sequence may be useful:
+.br
+  sync; echo 3 > /proc/sys/vm/drop_caches
+.PP
+Various numeric arguments (e.g. \fISKIP\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+The \fICOUNT\fR, \fISKIP\fR and \fISEEK\fR arguments can take 64 bit
+values (i.e. very big numbers). Other values are limited to what can fit in
+a signed 32 bit number.
+.PP
+All informative, warning and error output is sent to stderr so that
+dd's output file can be stdout and remain unpolluted. If no options
+are given, then the usage message is output and nothing else happens.
+.PP
+If a device supports xcopy operations then it should set the 3PC
+field (3PC stands for Third Party Copy) in its standard INQUIRY response.
+This utility will attempt a xcopy operation irrespective of the value
+in the 3PC field but if it is zero (cleared) one would expect the
+xcopy operation to fail.
+.PP
+The status of the SCSI EXTENDED COPY command can be queried with
+.B sg_copy_results(sg3_utils)
+.PP
+Currently only block\-to\-block transfers are implemented; \fIIFILE\fR
+and \fIOFILE\fR must refer to a SCSI block device.
+.PP
+No account is taken of partitions so, for example, /dev/sbc2, /dev/sdc,
+/dev/sg2, and /dev/bsg/3:0:0:1 would all refer to the same thing: the
+whole logical unit (i.e. the whole disk) starting at LBA 0. So any
+partition indication (e.g. /dev/sdc2) is ignored. The user should set
+\fISKIP\fR,  \fISEEK\fR and \fICOUNT\fR with information obtained
+from a command like 'fdisk \-l \-u /dev/sdc' to account for partitions.
+.PP
+XCOPY (LID1) capability has been added to the ddpt utility which is in
+a package of the same name. The ddpt utility will run on other
+OSes (e.g. FreeBSD and Windows) while sg_xcopy only runs on Linux. Also
+ddpt permits the arguments to \fIibs=\fR and \fIibs=\fR to be different.
+.SH EXAMPLES
+Copy 2M of data from the start of one device to another:
+.PP
+# sg_xcopy if=/dev/sdo of=/dev/sdp count=2048 list_id=2 dc=1
+.br
+sg_xcopy: if=/dev/sdo skip=0 of=/dev/sdp seek=0 count=1024
+.br
+Start of loop, count=1024, bpt=65535, lba_in=0, lba_out=0
+.br
+sg_xcopy: 1024 blocks, 1 command
+.PP
+Check the status of the EXTENDED COPY command:
+.PP
+# sg_copy_results \-\-status \-\-list_id=2 /dev/sdp
+.br
+Receive copy results (copy status):
+    Held data discarded: Yes
+    Copy manager status: Operation completed without errors
+    Segments processed: 1
+    Transfer count units: 0
+    Transfer count: 0
+.SH SIGNALS
+The signal handling has been borrowed from dd: SIGINT, SIGQUIT and
+SIGPIPE output the number of remaining blocks to be transferred and
+the records in + out counts; then they have their default action.
+SIGUSR1 causes the same information to be output yet the copy continues.
+All output caused by signals is sent to stderr.
+.SH EXIT STATUS
+The exit status of sg_xcopy is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.PP
+An additional exit status of 90 is generated if the flock flag is given
+and some other process holds the advisory exclusive lock.
+.SH AUTHORS
+Written by Hannes Reinecke and Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2000\-2021 Hannes Reinecke and Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+There is a web page discussing sg_dd at https://sg.danny.cz/sg/sg_dd.html
+.PP
+A POSIX threads version of this utility called
+.B sgp_dd
+is in the sg3_utils package. Another version from that package is called
+.B sgm_dd
+and it uses memory mapped IO to speed transfers from sg devices.
+.PP
+The lmbench package contains
+.B lmdd
+which is also interesting. For moving data to and from tapes see
+.B dt
+which is found at https://www.scsifaq.org/RMiller_Tools/index.html
+.PP
+To change mode parameters that effect a SCSI device's caching and error
+recovery see
+.B sdparm(sdparm)
+.PP
+See also
+.B dd(1), sg_copy_results(sg3_utils), ddrescue(GNU), ddpt,ddptctl(ddpt)
diff --git a/doc/sg_z_act_query.8 b/doc/sg_z_act_query.8
new file mode 100644
index 0000000..7cda1e2
--- /dev/null
+++ b/doc/sg_z_act_query.8
@@ -0,0 +1,115 @@
+.TH SG_Z_ACT_QUERY "8" "December 2021" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_z_act_query \- send a SCSI ZONE ACTIVATE or ZONE QUERY command
+.SH SYNOPSIS
+.B sg_z_act_query
+[\fI\-\-activate\fR] [\fI\-\-all\fR] [\fI\-\-force\fR] [\fI\-\-help\fR]
+[\fI\-\-hex\fR] [\fI\-\-inhex=FN\fR] [\fI\-\-maxlen=LEN\fR]
+[\fI\-\-num=ZS\fR] [\fI\-\-other=ZDID\fR] [\fI\-\-query\fR] [\fI\-\-raw\fR]
+[\fI\-\-verbose\fR] [\fI\-\-version\fR] [\fI\-\-zone=ID\fR]
+\fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI ZONE ACTIVATE or ZONE QUERY command to the \fIDEVICE\fR. If the
+\fI\-\-activate\fR option is not given, then a ZONE QUERY command is sent.
+These commands were added in the ZBC\-2 draft revision 4 (zbc2r04.pdf).
+.PP
+Both of these commands have similar cdb_s and responses hence they are both
+placed in this utility. The difference is that only the ZONE ACTIVATE command
+will potentially activate or deactivate zones. Both commands will perform
+a "Verify activations operation" as defined in ZBC\-2 .
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-A\fR, \fB\-\-activate\fR
+sends a ZONE ACTIVATE command to the \fIDEVICE\fR. The default (i.e. without
+this option) is to send a ZONE QUERY command.
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+sets the ALL field in the cdb.
+.TP
+\fB\-f\fR, \fB\-\-force\fR
+when decoding the response to this command, certain sanity checks are
+done and if they fail a message is sent to stderr and a non\-zero
+exit status is set. If this option is given those sanity checks are
+bypassed.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR, \fB\-\-hex\fR
+output the response in hexadecimal to stdout. When used once the whole
+response is output in ASCII hexadecimal with a leading address (starting at
+0) on each line. When used twice each zone activation descriptor in the
+response is output separately in hexadecimal. When used thrice the whole
+response is output in hexadecimal with no leading address (on each line).
+.br
+The output format when this option is given thrice is suitable for a later
+invocation with the \fI\-\-inhex=FN\fR option.
+.TP
+\fB\-i\fR, \fB\-\-inhex\fR=\fIFN\fR
+where \fIFN\fR is a file name whose contents are assumed to be ASCII
+hexadecimal. If \fIDEVICE\fR is also given then \fIDEVICE\fR is ignored,
+a warning is issued and the utility continues, decoding the file named
+\fIFN\fR. See the "FORMAT OF FILES CONTAINING ASCII HEX" section in the
+sg3_utils manpage for more information. If the \fI\-\-raw\fR option is
+also given then the contents of \fIFN\fR are treated as binary.
+.br
+By default it is assumed the response is from a ZONE QUERY command but
+that shouldn't matter because the response of the ZONE ACTIVATE and
+ZONE QUERY commands is of the same form.
+.TP
+\fB\-m\fR, \fB\-\-maxlen\fR=\fILEN\fR
+where \fILEN\fR is the (maximum) response length in bytes. It is placed in
+the cdb's "allocation length" field. If not given (or \fILEN\fR is zero)
+then 8192 is used. The maximum allowed value of \fILEN\fR is 1048576.
+.br
+The draft standard disallows allocation lengths less than 64.
+.TP
+\fB\-n\fR, \fB\-\-num\fR=\fIZS\fR
+where \fIZS\fR is placed in the "Number of zones" field in the cdb. This
+option is usually ignored if the \fI\-\-all\fR option is given. If the
+\fI\-\-all\fR option is not given, the default value of this field is 1 .
+.TP
+\fB\-o\fR, \fB\-\-other\fR=\fIZDID\fR
+where the \fIZDID\fR value will be placed in the "Other zone domain ID"
+field of the cdb to be sent to the \fIDEVICE\fR.
+.TP
+\fB\-q\fR, \fB\-\-query\fR
+causes the ZONE QUERY command to be sent to the \fIDEVICE\fR. Since this
+is the default action, this option is typically not needed. If both this
+option and the \fI\-\-activate\fR option are given, an error will be
+reported (and no command will be sent).
+.TP
+\fB\-r\fR, \fB\-\-raw\fR
+output response in binary (to stdout) unless the \fI\-\-inhex=FN\fR option
+is also given. In that case the input file name (\fIFN\fR) is decoded as
+binary (and the output is _not_ in binary (but may be hex)).
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-z\fR, \fB\-\-zone\fR=\fIID\fR
+where \fIID\fR is placed in the cdb's ZONE ID field. A zone id is a zone
+start logical block address (LBA). The default value is 0. \fIID\fR is
+assumed to be in decimal unless prefixed with '0x' or has a trailing 'h'
+which indicate hexadecimal. The maximum value that can be given is
+2^64 - 2. In the unlikely event of wanting to give 2^64 - 1, enter "\-1".
+.SH EXIT STATUS
+The exit status of sg_z_act_query is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2021 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_zone,sg_rep_zones,sg_reset_wp(sg3_utils)
diff --git a/doc/sg_zone.8 b/doc/sg_zone.8
new file mode 100644
index 0000000..256009e
--- /dev/null
+++ b/doc/sg_zone.8
@@ -0,0 +1,97 @@
+.TH SG_ZONE "8" "June 2022" "sg3_utils\-1.48" SG3_UTILS
+.SH NAME
+sg_zone \- send a SCSI ZONE modifying command
+.SH SYNOPSIS
+.B sg_zone
+[\fI\-\-all\fR] [\fI\-\-close\fR] [\fI\-\-count=ZC\fR] [\fI\-\-element=EID\fR]
+[\fI\-\-finish\fR] [\fI\-\-help\fR] [\fI\-\-open\fR] [\fI\-\-remove\fR]
+[\fI\-\-sequentialize\fR] [\fI\-\-verbose\fR] [\fI\-\-version\fR]
+[\fI\-\-zone=ID\fR] \fIDEVICE\fR
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Sends a SCSI OPEN ZONE, CLOSE ZONE, FINISH ZONE, REMOVE ELEMENT AND MODIFY
+ZONES or SEQUENTIALIZE ZONE command to the \fIDEVICE\fR. All but the last
+two are found in the ZBC standard (INCITS 536\-2016). The REMOVE ELEMENT AND
+MODIFY ZONES command was added in zbc2r07 while the SEQUENTIALIZE ZONE command
+was added in zbc2r01b.
+.PP
+One and only one of the \fI\-\-open\fR, \fI\-\-close\fR, \fI\-\-finish\fR,
+\fI\-\-remove\fR and \fI\-\-sequentialize\fR options can be chosen.
+.PP
+The REPORT ZONES, REPORT REALMS and REPORT ZONE DOMAINS commands may be
+accessed via the sg_rep_zones utility. The ZONE ACTIVATE and ZONE QUERY
+commands may be accessed via the sg_z_act_query utility. The RESET WRITE
+POINTER command may be accessed via the sg_reset_wp utility.
+.SH OPTIONS
+Arguments to long options are mandatory for short options as well.
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+sets the ALL field in the cdb.
+.TP
+\fB\-c\fR, \fB\-\-close\fR
+causes the CLOSE ZONE command to be sent to the \fIDEVICE\fR.
+.TP
+\fB\-C\fR, \fB\-\-count\fR=\fIZC\fR
+ZC is placed in the Zone Count field in the cdb of all four commands
+supported by this utility. ZC should be a value from 0 to 65535 (0xffff)
+inclusive.
+.TP
+\fB\-e\fR, \fB\-\-element\fR=\fIEID\fR
+where \fIEID\fR is an element identifier which is a 32 bit unsigned integer
+starting at one. This field is used by the REMOVE ELEMENT AND MODIFY ZONES
+command and its default value is zero (which is invalid). So the user needs
+to supply a valid element identifier when \fI\-\-remove\fR is used.
+.TP
+\fB\-f\fR, \fB\-\-finish\fR
+causes the FINISH ZONE command to be sent to the \fIDEVICE\fR.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+output the usage message then exit.
+.TP
+\fB\-o\fR, \fB\-\-open\fR
+causes the OPEN ZONE command to be sent to the \fIDEVICE\fR.
+.TP
+\fB\-r\fR, \fB\-\-remove\fR
+causes the REMOVE ELEMENT AND MODIFY ZONES command to be sent to the
+\fIDEVICE\fR. In practice, \fI\-\-element=EID\fR needs to be also given.
+.TP
+\fB\-S\fR, \fB\-\-sequentialize\fR
+causes the SEQUENTIALIZE ZONE command to be sent to the \fIDEVICE\fR.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+increase the level of verbosity, (i.e. debug output).
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.TP
+\fB\-z\fR, \fB\-\-zone\fR=\fIID\fR
+where \fIID\fR is placed in the cdb's ZONE ID field. A zone id is a zone
+start logical block address (LBA). The default value is 0. \fIID\fR is
+assumed to be in decimal unless prefixed with '0x' or has a trailing 'h'
+which indicate hexadecimal.
+.SH NOTES
+After a REMOVE ELEMENT AND MODIFY ZONES command has completed, the element
+in question is said to be depopulated and any affected zones are placed in
+the 'offline' zone condition.
+.PP
+SBC\-4 has a similar command to REMOVE ELEMENT AND MODIFY ZONES called REMOVE
+ELEMENT AND TRUNCATE. The difference is that the latter "changes the
+association between LBAs and physical blocks" and the former does not change
+that association. In both cases, depopulated elements that have
+the 'Restoration Allowed' (RALWD) bit set (see sg_get_elem_status) may be
+restored with the RESTORE ELEMENTS AND REBUILD command (see sg_rem_rest_elem).
+.SH EXIT STATUS
+The exit status of sg_zone is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page.
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2014\-2022 Douglas Gilbert
+.br
+This software is distributed under a BSD\-2\-Clause license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B sg_rem_rest_elem,sg_rep_zones,sg_reset_wp,sg_z_act_query(sg3_utils)
diff --git a/doc/sginfo.8 b/doc/sginfo.8
new file mode 100644
index 0000000..66e4fb8
--- /dev/null
+++ b/doc/sginfo.8
@@ -0,0 +1,325 @@
+.TH SGINFO "8" "January 2014" "sg3_utils\-1.38" SG3_UTILS
+.SH NAME
+sginfo \- access mode page information for a SCSI (or ATAPI) device
+.SH SYNOPSIS
+.B sginfo
+[\fIOPTIONS\fR]
+[\fIDEVICE\fR]
+[\fIREPLACEMENT_PARAMETERS\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+sginfo is a port of the Linux
+.B scsiinfo
+program by Eric Youngdale. It uses SCSI generic (sg) devices; however in
+some cases the high level device name (i.e. sd, sr, st, osst, or hd) can
+also be used. The primary role of this program is to access mode page
+information. If permitted, mode page information can be altered. In
+addition information from the INQUIRY and READ DEFECTS commands are also
+available.
+.PP
+This utility is in legacy mode, only obvious bugs will be fixed. Options
+like \fI\-l\fR (to list devices) are broken in recent versions of
+Linux (e.g. 2.6 series and later); the lsscsi(8) utility can be used
+instead. Also mode pages are not being updated as https://www.t10.org
+adds and modifies mode page fields. Those interested in SCSI mode pages
+may find the
+.B sdparm
+utility more up to date and easier use, especially for changing parameters.
+.PP
+Four sets of values are maintained by a SCSI device for each mode
+page: current (active), default (manufacturer's supplied values),
+saved (values that are retained if the SCSI device is powered down),
+and changeable (mask indicating those values that can be changed).
+By default when a mode page is displayed the current values are
+shown. This can be overridden by "\-M" (defaults), "\-S" (saved)
+or "\-m" (modifiable (i.e. changeable)).
+.PP
+Many mode pages are decoded: for disks (see SBC\-2), for CD/DVDs (see
+MMC\-2/3/4/5), for tapes (see SSC\-2) and for enclosures (see SES\-2).
+Some mode pages common to all SCSI peripheral device types are defined
+in SPC\-4 (primary commands). A decoded mode page has its field names
+in the first column and the corresponding value in the second column.
+A "hex" mode page (and subpage) has its byte position in the first
+column (in hex and starting at 0x2) and the corresponding hex value
+in the second column. Decoded pages can be viewed with the '\-t' option
+or with a specific option (e.g. 'c' for the caching mode page).
+Naturally decoded pages must be supplied by the \fIDEVICE\fR and
+recognised by this program. If supported by the device, decoded pages
+may be modified. All mode pages (and subpages) that the device supports
+can be viewed in hex (and potentially modified) via the "\-u" option
+.PP
+If no options are given that will cause mode page(s) or INQUIRY data
+to be printed out, then a brief INQUIRY response is output. This
+includes the vendor, product and revision level of the device.
+.SH OPTIONS
+.TP
+\fB\-6\fR
+Perform 6 byte MODE SENSE and MODE SELECT commands; by default the
+10 byte variants are used.
+.TP
+\fB\-a\fR
+Display some INQUIRY data and the unit serial number followed by
+all mode pages reported by the device. It is similar to
+the '\-t 0x3f' option. If the mode page is known then it is output
+in decoded form otherwise it is output in hexadecimal.
+.TP
+\fB\-A\fR
+Display some INQUIRY data and the unit serial number followed by
+all mode pages and all mode subpages reported by the device.
+It is similar to the '\-t 0x3f,0xff' option. If a mode (sub)page
+is known then it is output in decoded form otherwise it is output in
+hexadecimal.
+.TP
+\fB\-c\fR
+Access information in the Caching mode page.
+.TP
+\fB\-C\fR
+Access information in the Control mode Page.
+.TP
+\fB\-d\fR
+Display defect lists (default format: index).
+.TP
+\fB\-D\fR
+Access information in the Disconnect\-Reconnect mode page.
+.TP
+\fB\-e\fR
+Access information in the Error Recovery mode page.
+.TP
+\fB\-E\fR
+Access information in the Control Extension mode page.
+.TP
+\fB\-f\fR
+Access information in the Format Device mode page.
+.TP
+\fB\-F\fR\fIarg\fR
+Format of the defect lists:
+                \-Flogical  \- logical block addresses (32 bit)
+                \-Flba64    \- logical block addresses (64 bit)
+                \-Fphysical \- physical blocks
+                \-Findex    \- defect bytes from index
+                \-Fhead     \- sort by head
+.br
+Used in conjunction with "\-d" or "\-G". If a format is not given "index" is
+assumed.
+.TP
+\fB\-g\fR
+Access information in the Rigid Disk Drive Geometry mode page.
+.TP
+\fB\-G\fR
+Display grown defect list (default format: index).
+.TP
+\fB\-i\fR
+Display the response to a standard INQUIRY command.
+.TP
+\fB\-I\fR
+Access the Informational Exceptions mode page.
+.TP
+\fB\-l\fR
+Deprecated. Only use in old versions of Linux (e.g. 2.4 and
+earlier). Please use lsscsi(8) in the Linux 2.6 series and
+later. List known SCSI devices on the system.
+.TP
+\fB\-n\fR
+Access information in the Notch and Partition mode page.
+.TP
+\fB\-N\fR
+Negate (i.e. stop) mode page changes being placed in the "saved"
+page (by default changes go to the current and the saved page).
+Only active when used together with '\-R'.
+.TP
+\fB\-P\fR
+Access information in the Power Condition mode page.
+.TP
+\fB\-r\fR
+Display all raw (or primary) SCSI device names visible in the /dev
+directory. Examples are /dev/sda, /dev/st1 and /dev/scd2. Does not
+list sg device names so devices such as a SCSI enclosure which only
+have an sg device name are not listed.
+.TP
+\fB\-s\fR
+Display information in the unit serial number page which is a
+INQUIRY command variant.
+.TP
+\fB\-t\fR \fIPN\fR[,\fISPN\fR]
+Display information from mode page number \fIPN\fR (and optionally sub
+page number \fISPN\fR) in decoded format (if known, otherwise in hex form).
+\fIPN\fR is a mode page number in a decimal number from 0 to 63 inclusive.
+\fISPN\fR is the mode subpage number and is assumed to be 0 if not given.
+\fISPN\fR is a decimal number from 1 to 255 inclusive. A page number of 63
+returns all pages supported by the device in ascending order except for
+page 0 which, if present, is last. Page 0 is vendor specific and not
+necessarily in mode page format. Alternatively hex values can be given for
+both \fIPN\fR and \fISPN\fR (both prefixed by '0x').
+.TP
+\fB\-T\fR
+Trace commands to obtain more verbose output (for debugging). When used once
+SCSI commands are shown (in hex) and any errors from these SCSI commands are
+spelt out (i.e.  with a decoded and raw sense buffer). When used twice, the
+additional data sent with mode select and the response from mode sense are
+shown (in hex).
+.TP
+\fB\-u\fR \fIPN\fR[,\fISPN\fR]
+Display information from mode page number \fIPN\fR (and optionally \fISPN\fR)
+in hex form. \fIPN\fR is a mode page number in a decimal number from 0 to 63
+inclusive. \fISPN\fR is the mode subpage number and is assumed to be 0 if
+not given. \fISPN\fR is a decimal number from 1 to 255 inclusive. A page
+number of 63 returns all pages supported by the device in ascending order
+except for page 0 which, if present, is last. Page 0 is vendor specific and
+not necessarily in mode page format. Alternatively hex values can be given
+for both \fIPN\fR and \fISPN\fR (both prefixed by '0x'). For example 63 and
+0x3f are equivalent.
+.TP
+\fB\-v\fR
+Display version string then exit. [N.B. This option increases verbosity for
+most other utilities in this package as outlined in 'man 8 sg3_utils'.
+This odd usage is for backward compatibility with the scsiinfo utility.]
+.TP
+\fB\-V\fR
+Access information in the Verify Error Recovery mode page. [N.B. This
+option prints the version string then exits in most other utilities in
+this package as outlined in 'man 8 sg3_utils'. This odd usage is for
+backward compatibility with the scsiinfo utility.]
+.TP
+\fB\-z\fR
+do a single fetch for mode pages (over\-estimating the expected length
+of the returned response). The default action is to do a double
+fetch, the first fetch is to find the response length that could be
+returned. Devices that closely adhere to SCSI standards should not
+require this option, some real world devices do require it.
+.SH ADVANCED OPTIONS
+Only one of the following three options can be specified.
+None of these three implies the current values are returned.
+.TP
+\fB\-m\fR
+Display modifiable fields instead of current values
+.TP
+\fB\-M\fR
+Display manufacturer's defaults instead of current values
+.TP
+\fB\-S\fR
+Display saved defaults instead of current values
+.PP
+The following are advanced options, not generally suited for most users:
+.TP
+\fB\-X\fR
+Display output values in a list. Make them suitable for editing and
+being given back to the '\-R' (replace command).
+.TP
+\fB\-R\fR
+Replace parameters \- best used with \-X (expert use only)
+.SH CHANGING MODE PAGE PARAMETERS
+Firstly you should know what you are doing before changing existing
+parameters. Taking the control page as an example, first list it out
+normally (e.g. "sginfo \-C /dev/sda") and
+decide which parameter is to be changed (note its position relative
+to the other lines output). Then execute the same sginfo command with
+the "\-X" option added; this will output the parameter values in a
+single row in the same relative positions as the previous command. Now
+execute "sginfo \-CXR /dev/sda ..." with the "..." replaced by the
+single row of values output by the previous command, with the relevant
+parameter changed. Here is a simplified example:
+.PP
+   $ sginfo \-C /dev/sda
+.br
+   Control mode page (0xa)
+.br
+   \-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
+.br
+   TST                        0
+.br
+   D_SENSE                    0
+.br
+   GLTSD                      1
+.br
+   RLEC                       0
+.PP
+[Actually the Control page has more parameters that shown above.] Next
+output those parameters in single line form:
+.PP
+   $ sginfo \-CX /dev/sda
+.br
+   0 0 1 0
+.PP
+Let us assume that the GLTSD bit is to be cleared. The command that
+will clear it is:
+.PP
+   $ sginfo \-CXR /dev/sda 0 0 0 0
+.PP
+The same number of parameters output by the "\-CX" command needs to be
+placed at the end of the "\-CXR" command line (after the device name).
+Now check that the change took effect:
+.PP
+   $ sginfo \-C /dev/sda
+.br
+   Control mode page (0xa)
+.br
+   \-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
+.br
+   TST                        0
+.br
+   D_SENSE                    0
+.br
+   GLTSD                      0
+.br
+   RLEC                       0
+.PP
+When a mode page is "replaced" the default action is to change both the
+current page and the saved page. [For some reason versions of sginfo and
+scsiinfo prior to 2.0 did not change the "saved" page.] To change only
+the current mode page but not the corresponding saved page use the "\-N"
+option.
+.PP
+.SH GENERATING SCRIPT FILES AND HEX PAGES
+The "\-aX" or "\-AX" option generates output suitable for a script file.
+Mode pages are output in list format (after the INQUIRY and serial
+number) one page per line. To facilitate running the output as (part
+of) a script file to assert chosen mode page values, each line is
+prefixed by "sginfo \-t \fIPN\fR[,\fISPN\fR] \-XR ". When such a script
+file is run, it will have the effect of re\-asserting the mode
+page values to what they were when the "\-aX" generated the output.
+.PP
+All mode pages (and subpages) supported by the device can be accessed via
+the \-t and \-u options. To see all
+mode pages supported by the device use "\-u 63". [To see all mode pages
+and all subpages use "\-u 63,255".] To list the control mode page in
+hex (mode page index in the first column and the corresponding byte
+value in the second column) use "\-u 0xa". Mode pages (subpage code == 0)
+start at index position 2 while subpages start at index position 4.
+If the "\-Xu ..." option is used then a list a hex values each value
+prefixed by "@" is output. Mode (sub)page values can then be modified
+with the "\-RXu ..." option.
+.PP
+.SH RESTRICTIONS
+The SCSI MODE SENSE command yields block descriptors as well as a mode
+page(s). This utility ignores block descriptors and does not display
+them. The "disable block descriptor" switch (DBD) in the MODE SENSE command
+is not set since some devices yield errors when it is set. When mode page
+values are being changed (the "\-R" option), the same block descriptor
+obtained by reading the mode page (i.e. via a MODE SENSE command) is sent
+back when the mode page is written (i.e. via a MODE SELECT command).
+.PP
+.SH REFERENCES
+SCSI (draft) standards can be found at https://www.t10.org . The relevant
+documents are SPC\-4 (mode pages common to all device types),
+SBC\-2 (direct access devices [e.g. disks]), MMC\-4 (CDs and DVDs) and
+SSC\-2 (tapes).
+.PP
+.SH AUTHORS
+Written by Eric Youngdale, Michael Weller, Douglas Gilbert, Kurt Garloff,
+Thomas Steudten
+.PP
+.SH HISTORY
+scsiinfo version 1.0 was released by Eric Youngdale on 1st November 1993.
+The most recent version of scsiinfo is version 1.7 with the last patches
+by Michael Weller. sginfo is derived from scsiinfo and uses the sg
+interface to get around the 4 KB buffer limit in scsiinfo that cramped
+the display of defect lists especially. sginfo was written by Douglas
+Gilbert with patches from Kurt Garloff. This manpage corresponds with
+version 2.25 of sginfo.
+.PP
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B lsscsi(lsscsi), scsiinfo(internet); sg_modes, sg_inq, sg_vpd (sg3_utils),
+.B sdparm(sdparm)
diff --git a/doc/sgm_dd.8 b/doc/sgm_dd.8
new file mode 100644
index 0000000..58782d7
--- /dev/null
+++ b/doc/sgm_dd.8
@@ -0,0 +1,284 @@
+.TH SGM_DD "8" "February 2019" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+sgm_dd \- copy data to and from files and devices, especially SCSI
+devices
+.SH SYNOPSIS
+.B sgm_dd
+[\fIbs=BS\fR] [\fIcount=COUNT\fR] [\fIibs=BS\fR] [\fIif=IFILE\fR]
+[\fIiflag=FLAGS\fR] [\fIobs=BS\fR] [\fIof=OFILE\fR] [\fIoflag=FLAGS\fR]
+[\fIseek=SEEK\fR] [\fIskip=SKIP\fR] [\fI\-\-help\fR] [\fI\-\-version\fR]
+.PP
+[\fIbpt=BPT\fR] [\fIcdbsz=\fR6|10|12|16] [\fIdio=\fR0|1] [\fIsync=\fR0|1]
+[\fItime=\fR0|1] [\fIverbose=VERB\fR] [\fI\-\-dry\-run\fR]
+[\fI\-\-verbose\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Copy data to and from any files. Specialized for "files" that are
+Linux SCSI generic (sg) devices and raw devices. Uses memory mapped
+transfers on sg devices. Similar syntax and semantics to
+.B dd(1)
+but does not perform any conversions.
+.PP
+Will only perform memory mapped transfers when \fIIFILE\fR or \fIOFILE\fR
+are SCSI generic (sg) devices.
+.PP
+If both \fIIFILE\fR and \fIOFILE\fR are sg devices then memory mapped
+transfers are performed on \fIIFILE\fR. If no other flags are specified
+then indirect IO is performed on \fIOFILE\fR. If 'oflag=dio' is given then
+direct IO is attempted on \fIOFILE\fR. If direct IO is not available, then
+this utility falls back to indirect IO and reports this at the end of the
+copy.
+.PP
+The first group in the synopsis above are "standard" Unix
+.B dd(1)
+operands. The second group are extra options added by this utility.
+Both groups are defined below.
+.SH OPTIONS
+.TP
+\fBbpt\fR=\fIBPT\fR
+each IO transaction will be made using \fIBPT\fR blocks (or less if
+near the end of the copy). Default is 128 for block sizes less that 2048
+bytes, otherwise the default is 32. So for bs=512 the reads and writes
+will each convey 64 KiB of data by default (less if near the end of the
+transfer or memory restrictions). When cd/dvd drives are accessed, the
+block size is typically 2048 bytes and bpt defaults to 32 which again
+implies 64 KiB transfers.
+.TP
+\fBbs\fR=\fIBS\fR
+where \fIBS\fR
+.B must
+be the block size of the physical device. Note that this differs from
+.B dd(1)
+which permits \fIBS\fR to be an integral multiple. Default is 512 which
+is usually correct for disks but incorrect for cdroms (which normally
+have 2048 byte blocks). For this utility the maximum size of each individual
+IO operation is \fIBS\fR * \fIBPT\fR bytes.
+.TP
+\fBcdbsz\fR=6 | 10 | 12 | 16
+size of SCSI READ and/or WRITE commands issued on sg device names.
+Default is 10 byte SCSI command blocks (unless calculations indicate
+that a 4 byte block number may be exceeded, in which case it defaults
+to 16 byte SCSI commands).
+.TP
+\fBcount\fR=\fICOUNT\fR
+copy \fICOUNT\fR blocks from \fIIFILE\fR to \fIOFILE\fR. Default is the
+minimum (of \fIIFILE\fR and \fIOFILE\fR) number of blocks that sg devices
+report from SCSI READ CAPACITY commands or that block devices (or their
+partitions) report. Normal files are not probed for their size. If
+\fIskip=SKIP\fR or \fIseek=SEEK\fR are given and the count is derived (i.e.
+not explicitly given) then the derived count is scaled back so that the
+copy will not overrun the device. If the file name is a block device
+partition and \fICOUNT\fR is not given then the size of the partition rather
+than the size of the whole device is used. If \fICOUNT\fR is not given and
+cannot be derived then an error message is issued and no copy takes place.
+.TP
+\fBdio\fR=0 | 1
+permits direct IO to be selected on the write\-side (i.e. on \fIOFILE\fR).
+Only allowed when the read\-side (i.e. \fIIFILE\fR) is a sg device. When
+1 there may be a "zero copy" copy (i.e. mmap\-ed transfer on the read into
+the user space and direct IO from there on the write, potentially two DMAs
+and no data copying from the CPU). Default is 0.
+The same action as 'dio=1' is also available with 'oflag=dio'.
+.TP
+\fBibs\fR=\fIBS\fR
+if given must be the same as \fIBS\fR given to 'bs=' option.
+.TP
+\fBif\fR=\fIIFILE\fR
+read from \fIIFILE\fR instead of stdin. If \fIIFILE\fR is '\-' then stdin
+is read. Starts reading at the beginning of \fIIFILE\fR unless \fISKIP\fR
+is given.
+.TP
+\fBiflag\fR=\fIFLAGS\fR
+where \fIFLAGS\fR is a comma separated list of one or more flags outlined
+below.  These flags are associated with \fIIFILE\fR and are ignored when
+\fIIFILE\fR is stdin.
+.TP
+\fBobs\fR=\fIBS\fR
+if given must be the same as \fIBS\fR given to 'bs=' option.
+.TP
+\fBof\fR=\fIOFILE\fR
+write to \fIOFILE\fR instead of stdout. If \fIOFILE\fR is '\-' then writes
+to stdout. If \fIOFILE\fR is /dev/null then no actual writes are performed.
+If \fIOFILE\fR is '.' (period) then it is treated the same way as
+/dev/null (this is a shorthand notation). If \fIOFILE\fR exists then it
+is _not_ truncated; it is overwritten from the start of \fIOFILE\fR
+unless 'oflag=append' or \fISEEK\fR is given.
+.TP
+\fBoflag\fR=\fIFLAGS\fR
+where \fIFLAGS\fR is a comma separated list of one or more flags outlined
+below.  These flags are associated with \fIOFILE\fR and are ignored when
+\fIOFILE\fR is /dev/null, '.' (period), or stdout.
+.TP
+\fBseek\fR=\fISEEK\fR
+start writing \fISEEK\fR bs\-sized blocks from the start of \fIOFILE\fR.
+Default is block 0 (i.e. start of file).
+.TP
+\fBskip\fR=\fISKIP\fR
+start reading \fISKIP\fR bs\-sized blocks from the start of \fIIFILE\fR.
+Default is block 0 (i.e. start of file).
+.TP
+\fBsync\fR=0 | 1
+when 1, does SYNCHRONIZE CACHE command on \fIOFILE\fR at the end of the
+transfer. Only active when \fIOFILE\fR is a sg device file name.
+.TP
+\fBtime\fR=0 | 1
+when 1, times transfer and does throughput calculation, outputting the
+results (to stderr) at completion. When 0 (default) doesn't perform timing.
+.TP
+\fBverbose\fR=\fIVERB\fR
+as \fIVERB\fR increases so does the amount of debug output sent to stderr.
+Default value is zero which yields the minimum amount of debug output.
+A value of 1 reports extra information that is not repetitive. A value
+2 reports cdbs and responses for SCSI commands that are not repetitive
+(i.e. other that READ and WRITE). Error processing is not considered
+repetitive. Values of 3 and 4 yield output for all SCSI commands (and
+Unix read() and write() calls) so there can be a lot of output.
+.TP
+\fB\-d\fR, \fB\-\-dry\-run\fR
+does all the command line parsing and preparation but bypasses the actual
+copy or read. That preparation may include opening \fIIFILE\fR or
+\fIOFILE\fR to determine their lengths. This option may be useful for
+testing the syntax of complex command line invocations in advance of
+executing them.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs usage message and exits.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+when used once, this is equivalent to \fIverbose=1\fR. When used
+twice (e.g. "\-vv") this is equivalent to \fIverbose=2\fR, etc.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+outputs version number information and exits.
+.SH FLAGS
+Here is a list of flags and their meanings:
+.TP
+append
+causes the O_APPEND flag to be added to the open of \fIOFILE\fR. For normal
+files this will lead to data appended to the end of any existing data.
+Cannot be used together with the \fIseek=SEEK\fR option as they conflict.
+The default action of this utility is to overwrite any existing data
+from the beginning of the file or, if \fISEEK\fR is given, starting at
+block \fISEEK\fR. Note that attempting to 'append' to a device file (e.g.
+a disk) will usually be ignored or may cause an error to be reported.
+.TP
+dio
+is only active with oflag (i.e. 'oflag=dio'). Its action is described in
+the 'dio=1' option description above.
+.TP
+direct
+causes the O_DIRECT flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR. This flag requires some memory alignment on IO. Hence user
+memory buffers are aligned to the page size. Has no effect on sg, normal
+or raw files.
+.TP
+dpo
+set the DPO bit (disable page out) in SCSI READ and WRITE commands. Not
+supported for 6 byte cdb variants of READ and WRITE. Indicates that
+data is unlikely to be required to stay in device (e.g. disk) cache.
+May speed media copy and/or cause a media copy to have less impact
+on other device users.
+.TP
+dsync
+causes the O_SYNC flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR. The "d" is prepended to lower confusion with the 'sync=0|1'
+option which has another action (i.e. a synchronisation to media at the
+end of the transfer).
+.TP
+excl
+causes the O_EXCL flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR.
+.TP
+fua
+causes the FUA (force unit access) bit to be set in SCSI READ and/or WRITE
+commands. This only has effect with sg devices. The 6 byte variants
+of the SCSI READ and WRITE commands do not support the FUA bit.
+Only active for sg device file names.
+.TP
+null
+has no affect, just a placeholder.
+.SH RETIRED OPTIONS
+Here are some retired options that are still present:
+.TP
+fua=0 | 1 | 2 | 3
+force unit access bit. When 3, fua is set on both \fIIFILE\fR and
+\fIOFILE\fR; when 2, fua is set on \fIIFILE\fR; when 1, fua is set on
+\fIOFILE\fR; when 0 (default), fua is cleared on both. See the 'fua' flag.
+.SH NOTES
+A raw device must be bound to a block device prior to using sgm_dd.
+See
+.B raw(8)
+for more information about binding raw devices. To be safe, the sg device
+mapping to SCSI block devices should be checked with the lsscsi utility
+before use.
+.PP
+Raw device partition information can often be found with
+.B fdisk(8)
+[the "\-ul" argument is useful in this respect].
+.PP
+Various numeric arguments (e.g. \fISKIP\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+The count, skip and seek parameters can take 64 bit values (i.e. very
+big numbers). Other values are limited to what can fit in a signed
+32 bit number.
+.PP
+Data usually gets to the user space in a 2 stage process: first the
+SCSI adapter DMAs into kernel buffers and then the sg driver copies
+this data into user memory (write operations reverse this sequence).
+With memory mapped transfers a kernel buffer reserved by sg is memory
+mapped (see the
+.B mmap(2)
+system call) into the user space. When this is done
+the second (redundant) copy from kernel buffers to user space is
+not needed. Hence the transfer is faster and requires less "grunt"
+from the CPU.
+.PP
+All informative, warning and error output is sent to stderr so that
+dd's output file can be stdout and remain unpolluted. If no options
+are given, then the usage message is output and nothing else happens.
+.PP
+For sg devices this utility issues SCSI READ and WRITE (SBC) commands which
+are appropriate for disks and reading from CD/DVD/BD drives. Those commands
+are not formatted correctly for tape devices so sgm_dd should not be used
+on tape devices.
+.PP
+This utility stops the copy if any error is encountered. For more
+advanced "copy on error" logic see the
+.B sg_dd
+utility (and its 'coe' flag).
+.SH EXAMPLES
+See the examples given in the man page for
+.B sg_dd(8).
+.SH SIGNALS
+The signal handling has been borrowed from dd: SIGINT, SIGQUIT and
+SIGPIPE output the number of remaining blocks to be transferred and
+the records in + out counts; then they have their default action.
+SIGUSR1 causes the same information to be output yet the copy continues.
+All output caused by signals is sent to stderr.
+.SH EXIT STATUS
+The exit status of sgm_dd is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page. Since this utility works at a higher level
+than individual commands, and there are 'coe' and 'retries' flags,
+individual SCSI command failures do not necessary cause the process
+to exit.
+.SH AUTHORS
+Written by Douglas Gilbert and Peter Allworth.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2000\-2019 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+The simplest variant of this utility is called
+.B sg_dd.
+A POSIX threads version of this utility called
+.B sgp_dd
+is in the sg3_utils package. The lmbench package contains
+.B lmdd
+which is also interesting.
+.B dd(1), ddpt(ddpt), raw(8)
diff --git a/doc/sgp_dd.8 b/doc/sgp_dd.8
new file mode 100644
index 0000000..b6184a0
--- /dev/null
+++ b/doc/sgp_dd.8
@@ -0,0 +1,345 @@
+.TH SGP_DD "8" "August 2022" "sg3_utils\-1.47" SG3_UTILS
+.SH NAME
+sgp_dd \- copy data to and from files and devices, especially SCSI
+devices
+.SH SYNOPSIS
+.B sgp_dd
+[\fIbs=BS\fR] [\fIcount=COUNT\fR] [\fIibs=BS\fR] [\fIif=IFILE\fR]
+[\fIiflag=FLAGS\fR] [\fIobs=BS\fR] [\fIof=OFILE\fR] [\fIoflag=FLAGS\fR]
+[\fIseek=SEEK\fR] [\fIskip=SKIP\fR] [\fI\-\-help\fR] [\fI\-\-version\fR]
+.PP
+[\fIbpt=BPT\fR] [\fIcoe=\fR0|1] [\fIcdbsz=\fR6|10|12|16] [\fIdeb=VERB\fR]
+[\fIdio=\fR0|1] [\fIsync=\fR0|1] [\fIthr=THR\fR] [\fItime=\fR0|1]
+[\fIverbose=VERB\fR] [\fI\-\-chkaddr\fR] [\fI\-\-dry\-run\fR]
+[\fI\-\-verbose\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+Copy data to and from any files. Specialised for "files" that are
+Linux SCSI generic (sg) and raw devices. Similar syntax and semantics to
+.B dd(1)
+but does not perform any conversions. Uses POSIX threads (often
+called "pthreads") to increase the amount of parallelism. This improves
+speed in some cases.
+.PP
+The first group in the synopsis above are "standard" Unix
+.B dd(1)
+operands. The second group are extra options added by this utility.
+Both groups are defined below.
+.SH OPTIONS
+.TP
+\fBbpt\fR=\fIBPT\fR
+each IO transaction will be made using \fIBPT\fR blocks (or less if
+near the end of the copy). Default is 128 for block sizes less that 2048
+bytes, otherwise the default is 32. So for bs=512 the reads and writes
+will each convey 64 KiB of data by default (less if near the end of the
+transfer or memory restrictions). When cd/dvd drives are accessed, the
+block size is typically 2048 bytes and bpt defaults to 32 which again
+implies 64 KiB transfers.
+.TP
+\fBbs\fR=\fIBS\fR
+where \fIBS\fR
+.B must
+be the block size of the physical device. Note that this differs from
+.B dd(1)
+which permits 'bs' to be an integral multiple of the actual device block
+size. Default is 512 which is usually correct for disks but incorrect for
+cdroms (which normally have 2048 byte blocks).
+.TP
+\fBcdbsz\fR=6 | 10 | 12 | 16
+size of SCSI READ and/or WRITE commands issued on sg device names.
+Default is 10 byte SCSI command blocks (unless calculations indicate
+that a 4 byte block number may be exceeded, in which case it defaults
+to 16 byte SCSI commands).
+.TP
+\fBcoe\fR=0 | 1
+set to 1 for continue on error. Only applies to errors on sg devices.
+Thus errors on other files will stop sgp_dd. Default is 0 which
+implies stop on any error. See the 'coe' flag for more information.
+.TP
+\fBcount\fR=\fICOUNT\fR
+copy \fICOUNT\fR blocks from \fIIFILE\fR to \fIOFILE\fR. Default is the
+minimum (of \fIIFILE\fR and \fIOFILE\fR) number of blocks that sg devices
+report from SCSI READ CAPACITY commands or that block devices (or their
+partitions) report. Normal files are not probed for their size. If
+\fIskip=SKIP\fR or \fIseek=SEEK\fR are given and the count is deduced (i.e.
+not explicitly given) then that count is scaled back so that the copy will
+not overrun the device. If the file name is a block device partition and
+\fICOUNT\fR is not given then the size of the partition rather than the
+size of the whole device is used. If \fICOUNT\fR is not given and cannot be
+deduced then an error message is issued and no copy takes place.
+.TP
+\fBdeb\fR=\fIVERB\fR
+outputs debug information. If \fIVERB\fR is 0 (default) then there is
+minimal debug information and as \fIVERB\fR increases so does the amount
+of debug (max debug output when \fIVERB\fR is 9).
+.TP
+\fBdio\fR=0 | 1
+default is 0 which selects indirect IO. Value of 1 attempts direct
+IO which, if not available, falls back to indirect IO and notes this
+at completion. If direct IO is selected and /sys/module/sg/parameters/allow_dio
+has the value of 0 then a warning is issued (and indirect IO is performed)
+For finer grain control use 'iflag=dio' or 'oflag=dio'.
+.TP
+\fBibs\fR=\fIBS\fR
+if given must be the same as \fIBS\fR given to 'bs=' option.
+.TP
+\fBif\fR=\fIIFILE\fR
+read from \fIIFILE\fR instead of stdin. If \fIIFILE\fR is '\-' then stdin
+is read. Starts reading at the beginning of \fIIFILE\fR unless \fISKIP\fR
+is given.
+.TP
+\fBiflag\fR=\fIFLAGS\fR
+where \fIFLAGS\fR is a comma separated list of one or more flags outlined
+below.  These flags are associated with \fIIFILE\fR and are ignored when
+\fIIFILE\fR is stdin.
+.TP
+\fBobs\fR=\fIBS\fR
+if given must be the same as \fIBS\fR given to 'bs=' option.
+.TP
+\fBof\fR=\fIOFILE\fR
+write to \fIOFILE\fR instead of stdout. If \fIOFILE\fR is '\-' then writes
+to stdout.  If \fIOFILE\fR is /dev/null then no actual writes are performed.
+If \fIOFILE\fR is '.' (period) then it is treated the same way as
+/dev/null (this is a shorthand notation). If \fIOFILE\fR exists then it
+is _not_ truncated; it is overwritten from the start of \fIOFILE\fR
+unless 'oflag=append' or \fISEEK\fR is given.
+.TP
+\fBoflag\fR=\fIFLAGS\fR
+where \fIFLAGS\fR is a comma separated list of one or more flags outlined
+below.  These flags are associated with \fIOFILE\fR and are ignored when
+\fIOFILE\fR is /dev/null, '.' (period), or stdout.
+.TP
+\fBseek\fR=\fISEEK\fR
+start writing \fISEEK\fR bs\-sized blocks from the start of \fIOFILE\fR.
+Default is block 0 (i.e. start of file).
+.TP
+\fBskip\fR=\fISKIP\fR
+start reading \fISKIP\fR bs\-sized blocks from the start of \fIIFILE\fR.
+Default is block 0 (i.e. start of file).
+.TP
+\fBsync\fR=0 | 1
+when 1, does SYNCHRONIZE CACHE command on \fIOFILE\fR at the end of the
+transfer. Only active when \fIOFILE\fR is a sg device file name.
+.TP
+\fBthr\fR=\fITHR\fR
+where \fITHR\fR is the number or worker threads (default 4) that attempt to
+copy in parallel. Minimum is 1 and maximum is 1024.
+.TP
+\fBtime\fR=0 | 1
+when 1, the transfer is timed and throughput calculation is
+performed, outputting the results (to stderr) at completion. When
+0 (default) no timing is performed.
+.TP
+\fBverbose\fR=\fIVERB\fR
+increase verbosity. Same as \fIdeb=VERB\fR. Added for compatibility with
+sg_dd and sgm_dd.
+.TP
+\fB\-c\fR, \fB\-\-chkaddr\fR
+this option checks that every block read contains the (32 bit) block address
+of that block. If that check fails, the copy exits with a miscompare error.
+This check complements the 'sg_dd iflag=00,ff' generation of blocks that
+contain their own (32 bit, big endian) block address. When \fI\-\-chkaddr\fR
+is used once, only the first block address in each block is checked. When
+used twice, each block address (that fits in a block) is checked.
+.TP
+\fB\-d\fR, \fB\-\-dry\-run\fR
+does all the command line parsing and preparation but bypasses the actual
+copy or read. That preparation may include opening \fIIFILE\fR or
+\fIOFILE\fR to determine their lengths. This option may be useful for
+testing the syntax of complex command line invocations in advance of
+executing them.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+outputs usage message and exits.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+when used once, this is equivalent to \fIverbose=1\fR. When used
+twice (e.g. "\-vv") this is equivalent to \fIverbose=2\fR, etc.
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+outputs version number information and exits.
+.SH FLAGS
+Here is a list of flags and their meanings:
+.TP
+append
+causes the O_APPEND flag to be added to the open of \fIOFILE\fR. For normal
+files this will lead to data appended to the end of any existing data.
+Cannot be used together with the \fIseek=SEEK\fR option as they conflict.
+The default action of this utility is to overwrite any existing data
+from the beginning of the file or, if \fISEEK\fR is given, starting at
+block \fISEEK\fR. Note that attempting to 'append' to a device file (e.g.
+a disk) will usually be ignored or may cause an error to be reported.
+.TP
+coe
+continue on error. When given with 'iflag=', an error that is detected
+in a single SCSI command (typically 'bpt' blocks) is noted (by an error
+message sent to stderr), then zeros are substituted into the buffer
+for the corresponding write operation and the copy continues. Note that the
+.B sg_dd
+utility is more sophisticated in such error situations when 'iflag=coe'.
+When given with 'oflag=', any error reported by a SCSI WRITE command is
+reported to stderr and the copy continues (as if nothing went wrong).
+.TP
+dio
+request the sg device node associated with this flag does direct IO.
+If direct IO is not available, falls back to indirect IO and notes
+this at completion. If direct IO is selected and
+/sys/module/sg/parameters/allow_dio has the value of 0 then a warning is
+issued (and indirect IO is performed).
+.TP
+direct
+causes the O_DIRECT flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR. This flag requires some memory alignment on IO. Hence user
+memory buffers are aligned to the page size. Has no effect on sg, normal
+or raw files.
+.TP
+dpo
+set the DPO bit (disable page out) in SCSI READ and WRITE commands. Not
+supported for 6 byte cdb variants of READ and WRITE. Indicates that
+data is unlikely to be required to stay in device (e.g. disk) cache.
+May speed media copy and/or cause a media copy to have less impact
+on other device users.
+.TP
+dsync
+causes the O_SYNC flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR. The 'd' is prepended to lower confusion with the 'sync=0|1'
+option which has another action (i.e. a synchronisation to media at the
+end of the transfer).
+.TP
+excl
+causes the O_EXCL flag to be added to the open of \fIIFILE\fR and/or
+\fIOFILE\fR.
+.TP
+mmap
+can only be used in the \fIiflag=FLAGS\fR or the \fIoflag=FLAGS\fR argument
+list but not both. The nominated side of the copy will use memory mapped IO
+based on the mmap(2) system call. The sg driver will remap its DMA
+destination or source buffer into the user space when the mmap(2) system call
+is used on a sg device.
+.TP
+fua
+causes the FUA (force unit access) bit to be set in SCSI READ and/or WRITE
+commands. This only has effect with sg devices. The 6 byte variants
+of the SCSI READ and WRITE commands do not support the FUA bit.
+Only active for sg device file names.
+.TP
+null
+has no affect, just a placeholder.
+.SH RETIRED OPTIONS
+Here are some retired options that are still present:
+.TP
+coe=0 | 1
+continue on error is 0 (off) by default. When it is 1, it is
+equivalent to 'iflag=coe oflag=coe' described in the FLAGS section
+above.  Similar to 'conv=noerror,sync' in
+.B dd(1)
+utility. Default is 0 which implies stop on error. More advanced
+coe=1 processing on reads is performed by the sg_dd utility.
+.TP
+.TP
+fua=0 | 1 | 2 | 3
+force unit access bit. When 3, fua is set on both \fIIFILE\fR and
+\fIOFILE\fR; when 2, fua is set on \fIIFILE\fR;, when 1, fua is set on
+\fIOFILE\fR; when 0 (default), fua is cleared on both. See the 'fua' flag.
+.SH NOTES
+A raw device must be bound to a block device prior to using sgp_dd.
+See
+.B raw(8)
+for more information about binding raw devices. To be safe, the sg device
+mapping to SCSI block devices should be checked with 'sg_map'
+before use.
+.PP
+Raw device partition information can often be found with
+.B fdisk(8)
+[the "\-ul" argument is useful in this respect].
+.PP
+Various numeric arguments (e.g. \fISKIP\fR) may include multiplicative
+suffixes or be given in hexadecimal. See the "NUMERIC ARGUMENTS" section
+in the sg3_utils(8) man page.
+.PP
+The \fICOUNT\fR, \fISKIP\fR and \fISEEK\fR arguments can take 64 bit
+values (i.e. very big numbers). Other values are limited to what can fit in
+a signed 32 bit number.
+.PP
+Data usually gets to the user space in a 2 stage process: first the
+SCSI adapter DMAs into kernel buffers and then the sg driver copies
+this data into user memory (write operations reverse this sequence).
+This is called "indirect IO" and there is a 'dio' option to select
+"direct IO" which will DMA directly into user memory. Due to some
+issues "direct IO" is disabled in the sg driver and needs a
+configuration change to activate it.
+.PP
+All informative, warning and error output is sent to stderr so that
+dd's output file can be stdout and remain unpolluted. If no options
+are given, then the usage message is output and nothing else happens.
+.PP
+Why use sgp_dd? Because in some cases it is twice as fast as dd
+(mainly with sg devices, raw devices give some improvement).
+Another reason is that big copies fill the block device caches
+which has a negative impact on other machine activity.
+.SH SIGNALS
+The signal handling has been borrowed from dd: SIGINT, SIGQUIT and
+SIGPIPE output the number of remaining blocks to be transferred and
+the records in + out counts; then they have their default action.
+SIGUSR1 causes the same information to be output yet the copy continues.
+All output caused by signals is sent to stderr.
+.SH EXAMPLES
+.PP
+Looks quite similar in usage to dd:
+.PP
+   sgp_dd if=/dev/sg0 of=t bs=512 count=1MB
+.PP
+This will copy 1 million 512 byte blocks from the device associated with
+/dev/sg0 (which should have 512 byte blocks) to a file called t.
+Assuming /dev/sda and /dev/sg0 are the same device then the above is
+equivalent to:
+.PP
+   dd if=/dev/sda of=t bs=512 count=1000000
+.PP
+although dd's speed may improve if bs was larger and count was
+correspondingly scaled. Using a raw device to do something similar on a
+ATA disk:
+.PP
+   raw /dev/raw/raw1 /dev/hda
+.br
+   sgp_dd if=/dev/raw/raw1 of=t bs=512 count=1MB
+.PP
+To copy a SCSI disk partition to an ATA disk partition:
+.PP
+   raw /dev/raw/raw2 /dev/hda3
+.br
+   sgp_dd if=/dev/sg0 skip=10123456 of=/dev/raw/raw2 bs=512
+.PP
+This assumes a valid partition is found on the SCSI disk at the given
+skip block address (past the 5 GB point of that disk) and that
+the partition goes to the end of the SCSI disk. An explicit count
+is probably a safer option.
+.PP
+To do a fast copy from one SCSI disk to another one with similar
+geometry (stepping over errors on the source disk):
+.PP
+   sgp_dd if=/dev/sg0 of=/dev/sg1 bs=512 coe=1
+.SH EXIT STATUS
+The exit status of sgp_dd is 0 when it is successful. Otherwise see
+the sg3_utils(8) man page. Since this utility works at a higher level
+than individual commands, and there are 'coe' and 'retries' flags,
+individual SCSI command failures do not necessary cause the process
+to exit.
+.SH AUTHORS
+Written by Douglas Gilbert and Peter Allworth.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2000\-2022 Douglas Gilbert
+.br
+This software is distributed under the GPL version 2. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+A simpler, non\-threaded version of this utility but with more
+advanced "continue on error" logic is called
+.B sg_dd
+and is also found in the sg3_utils package. The lmbench package contains
+.B lmdd
+which is also interesting.
+.B raw(8), dd(1)
diff --git a/examples/Makefile b/examples/Makefile
new file mode 100644
index 0000000..d815674
--- /dev/null
+++ b/examples/Makefile
@@ -0,0 +1,127 @@
+SHELL = /bin/sh
+
+PREFIX=/usr/local
+INSTDIR=$(DESTDIR)/$(PREFIX)/bin
+MANDIR=$(DESTDIR)/$(PREFIX)/man
+
+# In Linux the default C compiler is GCC while in FreeBSD (since release 10 ?)
+# the default C compiler is clang. Swap the comment marks (lines starting
+# with '#') on the next 4 (non-blank) lines.
+CC = gcc
+# CC = clang
+
+LD = gcc
+# LD = clang
+
+
+EXECS = sg_simple1 sg_simple2 sg_simple3 sg_simple4 sg_simple16 \
+	scsi_inquiry sg_excl sg_simple5 sg__sat_identify \
+	sg__sat_phy_event sg__sat_set_features sg_sat_chk_power \
+	sg_sat_smart_rd_data
+
+EXTRAS = sgq_dd
+
+BSG_EXTRAS =
+
+
+MAN_PGS =
+MAN_PREF = man8
+
+LARGE_FILE_FLAGS = -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64
+
+CPPFLAGS = -iquote ../include -D_REENTRANT $(LARGE_FILE_FLAGS)
+# CPPFLAGS = -iquote ../include -D_REENTRANT $(LARGE_FILE_FLAGS) -DDEBUG
+
+CFLAGS = -g -O2 -W -Wall
+# CFLAGS = -g -O2 -Wall -DSG_KERNEL_INCLUDES
+# CFLAGS = -g -O2 -Wall -pedantic
+
+LDFLAGS =
+
+LIBFILESOLD = ../lib/sg_lib.o ../lib/sg_lib_data.o ../lib/sg_io_linux.o
+LIBFILESNEW = ../lib/sg_lib.o ../lib/sg_lib_data.o ../lib/sg_pt_common.o ../lib/sg_pt_linux.o ../lib/sg_pt_linux_nvme.o
+
+all: $(EXECS)
+
+extras: $(EXTRAS)
+
+bsg: $(BSG_EXTRAS)
+
+
+depend dep:
+	for i in *.c; do $(CC) $(INCLUDES) $(CFLAGS) -M $$i; \
+	done > .depend
+
+clean:
+	/bin/rm -f *.o $(EXECS) $(EXTRAS) $(BSG_EXTRAS) core .depend
+
+sg_simple1: sg_simple1.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_simple2: sg_simple2.o
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_simple3: sg_simple3.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_simple4: sg_simple4.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_simple16: sg_simple16.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+scsi_inquiry: scsi_inquiry.o
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_excl: sg_excl.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_simple5: sg_simple5.o $(LIBFILESNEW)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg__sat_identify: sg__sat_identify.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg__sat_phy_event: sg__sat_phy_event.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg__sat_set_features: sg__sat_set_features.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_sat_chk_power: sg_sat_chk_power.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_sat_smart_rd_data: sg_sat_smart_rd_data.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sgq_dd: sgq_dd.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+install: $(EXECS)
+	install -d $(INSTDIR)
+	for name in $^; \
+	 do install -s -o root -g root -m 755 $$name $(INSTDIR); \
+	done
+	install -d $(MANDIR)/$(MAN_PREF)
+	for mp in $(MAN_PGS); \
+	 do install -o root -g root -m 644 $$mp $(MANDIR)/$(MAN_PREF); \
+	 gzip -9f $(MANDIR)/$(MAN_PREF)/$$mp; \
+	done
+
+uninstall:
+	dists="$(EXECS)"; \
+	for name in $$dists; do \
+	 rm -f $(INSTDIR)/$$name; \
+	done
+	for mp in $(MAN_PGS); do \
+	 rm -f $(MANDIR)/$(MAN_PREF)/$$mp.gz; \
+	done
+
+# Linux uses GNU make and FreeBSD uses Berkely make. The following lines
+# only work in Linux. Possible solutions in FreeBSD:
+#    a) use 'gmake'; b) comment out the next 3 lines, starting with 'ifeq'
+#        c) build with 'make -f Makefile.freebsd'
+# In Linux one can install bmake (but that won't help here).
+ifeq (.depend,$(wildcard .depend))
+include .depend
+endif
diff --git a/examples/Makefile.freebsd b/examples/Makefile.freebsd
new file mode 100644
index 0000000..d983736
--- /dev/null
+++ b/examples/Makefile.freebsd
@@ -0,0 +1,82 @@
+SHELL = /bin/sh
+
+PREFIX=/usr/local
+INSTDIR=$(DESTDIR)/$(PREFIX)/bin
+MANDIR=$(DESTDIR)/$(PREFIX)/man
+
+# In Linux the default C compiler is GCC while in FreeBSD (since release 10 ?)
+# the default C compiler is clang. Swap the comment marks (lines starting
+# with '#') on the next 4 (non-blank) lines.
+# CC = gcc
+# CC = clang
+
+# LD = gcc
+# LD = clang
+
+
+EXECS = sg_simple5
+
+# EXTRAS = sgq_dd
+
+MAN_PGS =
+MAN_PREF = man8
+
+OS_FLAGS = -DSG_LIB_FREEBSD
+EXTRA_FLAGS = $(OS_FLAGS)
+
+# CFLAGS = -O2 -Wall -W $(EXTRA_FLAGS) -I ../include
+CFLAGS = -g -O2 -Wall -W $(EXTRA_FLAGS) -I ../include
+# CFLAGS = -g -O2 -Wall -W -pedantic -std=c99 $(EXTRA_FLAGS) -I ../include
+
+CFLAGS_PTHREADS = -D_REENTRANT
+
+# there is no rule to make the following in the parent directory,
+# it is assumed they are already built.
+D_FILES = ../lib/sg_lib.o ../lib/sg_lib_data.o ../lib/sg_cmds_basic.o ../lib/sg_pt_common.o ../lib/sg_pt_freebsd.o
+
+LDFLAGS = -lcam
+
+all: $(EXECS)
+
+extras: $(EXTRAS)
+
+
+depend dep:
+	for i in *.c; do $(CC) $(INCLUDES) $(CFLAGS) -M $$i; \
+	done > .depend
+
+clean:
+	/bin/rm -f *.o $(EXECS) $(EXTRAS) core .depend
+
+sg_simple5: sg_simple5.o $(D_FILES)
+	$(CC) -o $@ $(LDFLAGS) $@.o $(D_FILES)
+
+
+install: $(EXECS)
+	install -d $(INSTDIR)
+	for name in $^; \
+	 do install -s -o root -g root -m 755 $$name $(INSTDIR); \
+	done
+	install -d $(MANDIR)/$(MAN_PREF)
+	for mp in $(MAN_PGS); \
+	 do install -o root -g root -m 644 $$mp $(MANDIR)/$(MAN_PREF); \
+	 gzip -9f $(MANDIR)/$(MAN_PREF)/$$mp; \
+	done
+
+uninstall:
+	dists="$(EXECS)"; \
+	for name in $$dists; do \
+	 rm -f $(INSTDIR)/$$name; \
+	done
+	for mp in $(MAN_PGS); do \
+	 rm -f $(MANDIR)/$(MAN_PREF)/$$mp.gz; \
+	done
+
+# Linux uses GNU make and FreeBSD uses Berkely make. The following lines
+# only work in Linux. Possible solutions in FreeBSD:
+#    a) use 'gmake'; b) comment out the next 3 lines, starting with 'ifeq'
+#        c) build with 'make -f Makefile.freebsd'
+# In Linux one can install bmake (but that won't help here).
+# ifeq (.depend,$(wildcard .depend))
+# include .depend
+# endif
diff --git a/examples/README b/examples/README
new file mode 100644
index 0000000..7a5ae28
--- /dev/null
+++ b/examples/README
@@ -0,0 +1,17 @@
+Building files in this directory depends on several files being already
+built in the ../lib directory. So to build files here, the ./configure
+needs to be executed in the parent directory followed by changing
+directory to the lib directory and calling 'make' there.
+Another way is to do a top level 'make' after the ./configure which
+will make the libraries followed by all the utilities in the src/
+directory. To make them in FreeBSD use 'make -f Makefile.freebsd' .
+
+There is an brief explanation of each example in the README file in
+the main (i.e. this directory's parent) directory. There are also
+some notes at the top of each source file.
+
+Some files that were previously in this directory have been moved to
+the 'testing' directory.
+
+Douglas Gilbert
+19th January 2018
diff --git a/examples/reassign_addr.txt b/examples/reassign_addr.txt
new file mode 100644
index 0000000..9d48d00
--- /dev/null
+++ b/examples/reassign_addr.txt
@@ -0,0 +1,11 @@
+# This file is an example for the sg_reassign utility.
+# That utility can take one or more logical block addresses from stdin when
+# either the '--address=-" or "-a -" option is given on the command line.
+
+# To see logical block addresses placed in the command parameter block
+# without executing them command try something like:
+# 'sg_reassign --address=- --dummy -vv /dev/sda < reassign_addr.txt
+
+1,34,0x33,0X444  0x89abcde	0xdeadbeef  	# 6 lba's
+
+# dpg 20070130
diff --git a/examples/scsi_inquiry.c b/examples/scsi_inquiry.c
new file mode 100644
index 0000000..9949749
--- /dev/null
+++ b/examples/scsi_inquiry.c
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 1999-2018 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Test code for D. Gilbert's extensions to the Linux OS SCSI generic ("sg")
+ * device driver.
+ * This program does a SCSI inquiry command on the given device and
+ * outputs some of the result. This program highlights the use of the
+ * SCSI_IOCTL_SEND_COMMAND ioctl. This should be able to be applied to
+ * any SCSI device file descriptor (not just one related to sg). [Whether
+ * this is a good idea on a disk while it is mounted is debatable.
+ * No detrimental effects when this was tested ...]
+ *
+ * Version 0.16 20181207
+ */
+
+#include <unistd.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <scsi/scsi.h>
+/* #include <scsi/scsi_ioctl.h> */ /* glibc hides this file sometimes */
+
+typedef struct my_scsi_ioctl_command {
+        unsigned int inlen;  /* _excluding_ scsi command length */
+        unsigned int outlen;
+        unsigned char data[1];  /* was 0 but that's not ISO C!! */
+                /* on input, scsi command starts here then opt. data */
+} My_Scsi_Ioctl_Command;
+
+#define OFF (2 * sizeof(unsigned int))
+
+#ifndef SCSI_IOCTL_SEND_COMMAND
+#define SCSI_IOCTL_SEND_COMMAND 1
+#endif
+
+#define INQUIRY_CMD     0x12
+#define INQUIRY_CMDLEN  6
+#define INQUIRY_REPLY_LEN 96
+
+
+int main(int argc, char * argv[])
+{
+    int s_fd, res, k, to;
+    unsigned char inq_cdb [INQUIRY_CMDLEN] = {INQUIRY_CMD, 0, 0, 0,
+                                                INQUIRY_REPLY_LEN, 0};
+    unsigned char * inqBuff = (unsigned char *)
+                                malloc(OFF + sizeof(inq_cdb) + 512);
+    unsigned char * buffp = inqBuff + OFF;
+    My_Scsi_Ioctl_Command * ishp = (My_Scsi_Ioctl_Command *)inqBuff;
+    char * file_name = 0;
+    int do_nonblock = 0;
+    int oflags = 0;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == strcmp(argv[k], "-n"))
+            do_nonblock = 1;
+        else if (*argv[k] != '-')
+            file_name = argv[k];
+        else {
+            printf("Unrecognized argument '%s'\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'scsi_inquiry [-n] <scsi_device>'\n");
+        printf("     where: -n   open device in non-blocking mode\n");
+        printf("  Examples: scsi_inquiry /dev/sda\n");
+        printf("            scsi_inquiry /dev/sg0\n");
+        printf("            scsi_inquiry -n /dev/scd0\n");
+        return 1;
+    }
+
+    if (do_nonblock)
+        oflags = O_NONBLOCK;
+    s_fd = open(file_name, oflags | O_RDWR);
+    if (s_fd < 0) {
+        if ((EROFS == errno) || (EACCES == errno)) {
+            s_fd = open(file_name, oflags | O_RDONLY);
+            if (s_fd < 0) {
+                perror("scsi_inquiry: open error");
+                return 1;
+            }
+        }
+        else {
+            perror("scsi_inquiry: open error");
+            return 1;
+        }
+    }
+    /* Don't worry, being very careful not to write to a none-scsi file ... */
+    res = ioctl(s_fd, SCSI_IOCTL_GET_BUS_NUMBER, &to);
+    if (res < 0) {
+        /* perror("ioctl on scsi device, error"); */
+        printf("scsi_inquiry: not a scsi device\n");
+        return 1;
+    }
+
+    ishp->inlen = 0;
+    ishp->outlen = INQUIRY_REPLY_LEN;
+    memcpy(buffp, inq_cdb, INQUIRY_CMDLEN);
+    res = ioctl(s_fd, SCSI_IOCTL_SEND_COMMAND, inqBuff);
+    if (0 == res) {
+        to = (int)*(buffp + 7);
+        printf("    %.8s  %.16s  %.4s, byte_7=0x%x\n", buffp + 8,
+               buffp + 16, buffp + 32, to);
+    }
+    else if (res < 0)
+        perror("scsi_inquiry: SCSI_IOCTL_SEND_COMMAND err");
+    else
+        printf("scsi_inquiry: SCSI_IOCTL_SEND_COMMAND status=0x%x\n", res);
+
+    res = close(s_fd);
+    if (res < 0) {
+        perror("scsi_inquiry: close error");
+        return 1;
+    }
+    return 0;
+}
diff --git a/examples/sdiag_sas_p0_cjtpat.txt b/examples/sdiag_sas_p0_cjtpat.txt
new file mode 100644
index 0000000..7281092
--- /dev/null
+++ b/examples/sdiag_sas_p0_cjtpat.txt
@@ -0,0 +1,12 @@
+# This is the hex for a SAS protocol specific diagnostic
+# page. It will attempt to put phy identifier 0 of the
+# given device into CJTPAT (jitter pattern) generation mode.
+# Physical transmission speed is 3 Gbps
+# N.B. This will turn the receiver off on phy id 0.
+#
+# Usage example: 'sg_senddiag --pf --raw=- /dev/sg2 < {this_file}'
+#
+3f,6,0,1c,0,1,2,9,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
diff --git a/examples/sdiag_sas_p0_prbs9.txt b/examples/sdiag_sas_p0_prbs9.txt
new file mode 100644
index 0000000..1b96f99
--- /dev/null
+++ b/examples/sdiag_sas_p0_prbs9.txt
@@ -0,0 +1,12 @@
+# This is the hex for a SAS protocol specific diagnostic
+# page. It will attempt to put phy identifier 0 of the
+# given device into PRBS9 (jitter pattern) generation mode.
+# Physical transmission speed is 22.5 Gbps
+# N.B. This will turn the receiver off on phy id 0.
+#
+# Usage example: 'sg_senddiag --pf --raw=- /dev/sg2 < {this_file}'
+#
+3f,6,0,1c,0,1,3,c,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
diff --git a/examples/sdiag_sas_p1_cjtpat.txt b/examples/sdiag_sas_p1_cjtpat.txt
new file mode 100644
index 0000000..c82a558
--- /dev/null
+++ b/examples/sdiag_sas_p1_cjtpat.txt
@@ -0,0 +1,13 @@
+# This is the hex for a SAS protocol specific diagnostic
+# page. It will attempt to put phy identifier 1 of the
+# given device into CJTPAT (jitter pattern) generation mode.
+# Physical transmission speed is 3 Gbps
+# See sdiag_sas_p1_stop.txt to turn off this test pattern.
+# N.B. This will turn the receiver off on phy id 1.
+#
+# Usage example: 'sg_senddiag --pf --raw=- /dev/sg2 < {this_file}'
+#
+3f,6,0,1c,1,1,2,9,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
diff --git a/examples/sdiag_sas_p1_idle.txt b/examples/sdiag_sas_p1_idle.txt
new file mode 100644
index 0000000..39091ce
--- /dev/null
+++ b/examples/sdiag_sas_p1_idle.txt
@@ -0,0 +1,14 @@
+# This is the hex for a SAS protocol specific diagnostic
+# page. It will attempt to put phy identifier 1 of the
+# given device into IDLE (continuously transmit idle dwords) mode.
+# Physical transmission speed is 3 Gbps (last number on first
+# active line can be 8 for 1.5Gbps, 9 for 3Gbps and 10 for 6Gbps).
+# See sdiag_sas_p1_stop.txt to turn off this test pattern.
+# N.B. This will turn the receiver off on phy id 1.
+#
+# Usage example: 'sg_senddiag --pf --raw=- /dev/sg2 < {this_file}'
+#
+3f,6,0,1c,1,1,12,9,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
diff --git a/examples/sdiag_sas_p1_prbs15.txt b/examples/sdiag_sas_p1_prbs15.txt
new file mode 100644
index 0000000..1248ab3
--- /dev/null
+++ b/examples/sdiag_sas_p1_prbs15.txt
@@ -0,0 +1,12 @@
+# This is the hex for a SAS protocol specific diagnostic
+# page. It will attempt to put phy identifier 1 of the
+# given device into PRBS15 (jitter pattern) generation mode.
+# Physical transmission speed is 22.5 Gbps
+# N.B. This will turn the receiver off on phy id 1.
+#
+# Usage example: 'sg_senddiag --pf --raw=- /dev/sg2 < {this_file}'
+#
+3f,6,0,1c,1,1,4,c,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
diff --git a/examples/sdiag_sas_p1_stop.txt b/examples/sdiag_sas_p1_stop.txt
new file mode 100644
index 0000000..55b95c3
--- /dev/null
+++ b/examples/sdiag_sas_p1_stop.txt
@@ -0,0 +1,11 @@
+# This is the hex for a SAS protocol specific diagnostic
+# page. It will attempt to stop phy identifier 1 of the
+# given device producing a test pattern.
+# N.B. This should make phy id 1 usable for SAS protocols again.
+#
+# Usage example: 'sg_senddiag --pf --raw=- /dev/sg2 < {this_file}'
+#
+3f,6,0,1c,1,0,2,9,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
diff --git a/examples/sg__sat_identify.c b/examples/sg__sat_identify.c
new file mode 100644
index 0000000..c70eec0
--- /dev/null
+++ b/examples/sg__sat_identify.c
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2006-2018 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+/* This program uses a ATA PASS-THROUGH (16) SCSI command to package
+   an ATA IDENTIFY DEVICE (A1h) command. If the '-p' option is given,
+   it will package an ATA IDENTIFY PACKET DEVICE (Ech) instead (for
+   ATAPI device like cd/dvd drives) See http://www.t10.org
+   SAT draft at time of writing: sat-r08a.pdf
+
+   Invocation: sg__sat_identify [-p] [-v] [-V] <device>
+
+   With SAT, the user can find out whether a device is an ATA disk or
+   an ATAPI device. The ATA Information VPD page contains a "command
+   code" field in byte 56. Its values are either ECh for a (s/p)ATA
+   disk, A1h for a (s/p)ATAPI device, or 0 for unknown.
+
+*/
+
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return (sense) Descriptor */
+
+#define ATA_IDENTIFY_DEVICE 0xec
+#define ATA_IDENTIFY_PACKET_DEVICE 0xa1
+#define ID_RESPONSE_LEN 512
+
+#define EBUFF_SZ 256
+
+static char * version_str = "1.04 20180220";
+
+static void usage()
+{
+    fprintf(stderr, "Usage: "
+          "sg__sat_identify [-p] [-v] [-V] <device>\n"
+          "  where: -p    do IDENTIFY PACKET DEVICE (def: IDENTIFY "
+          "DEVICE) command\n"
+          "         -v    increase verbosity\n"
+          "         -V    print version string and exit\n\n"
+          "Performs a IDENTIFY (PACKET) DEVICE ATA command via a SAT "
+          "pass through\n");
+}
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k, ok;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t inBuff[ID_RESPONSE_LEN];
+    uint8_t sense_buffer[32];
+    int do_packet = 0;
+    int verbose = 0;
+    int extend = 0;
+    int chk_cond = 0;   /* set to 1 to read register(s) back */
+    int protocol = 4;   /* PIO data-in */
+    int t_dir = 1;      /* 0 -> to device, 1 -> from device */
+    int byte_block = 1; /* 0 -> bytes, 1 -> 512 byte blocks */
+    int t_length = 2;   /* 0 -> no data transferred, 2 -> sector count */
+    const uint8_t * cucp;
+
+    memset(inBuff, 0, sizeof(inBuff));
+    for (k = 1; k < argc; ++k) {
+        if (0 == strcmp(argv[k], "-p"))
+            ++do_packet;
+        else if (0 == strcmp(argv[k], "-v"))
+            ++verbose;
+        else if (0 == strcmp(argv[k], "-vv"))
+            verbose += 2;
+        else if (0 == strcmp(argv[k], "-vvv"))
+            verbose += 3;
+        else if (0 == strcmp(argv[k], "-V")) {
+            fprintf(stderr, "version: %s\n", version_str);
+            exit(0);
+        } else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        usage();
+        return 1;
+    }
+
+    if ((sg_fd = open(file_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg__sat_identify: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+
+    /* Prepare ATA PASS-THROUGH COMMAND (16) command */
+    apt_cdb[6] = 1;   /* sector count */
+    apt_cdb[14] = (do_packet ? ATA_IDENTIFY_PACKET_DEVICE :
+                                 ATA_IDENTIFY_DEVICE);
+    apt_cdb[1] = (protocol << 1) | extend;
+    apt_cdb[2] = (chk_cond << 5) | (t_dir << 3) |
+                 (byte_block << 2) | t_length;
+    if (verbose) {
+        fprintf(stderr, "    ata pass through(16) cdb: ");
+        for (k = 0; k < SAT_ATA_PASS_THROUGH16_LEN; ++k)
+            fprintf(stderr, "%02x ", apt_cdb[k]);
+        fprintf(stderr, "\n");
+    }
+
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(apt_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = ID_RESPONSE_LEN;
+    io_hdr.dxferp = inBuff;
+    io_hdr.cmdp = apt_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg__sat_identify: SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        if (verbose)
+            sg_chk_n_print3(">>> ATA_16 command", &io_hdr, 1);
+        /* check for ATA Return Descriptor */
+        cucp = sg_scsi_sense_desc_find(io_hdr.sbp, io_hdr.sb_len_wr,
+                                       SAT_ATA_RETURN_DESC);
+        if (cucp && (cucp[3])) {
+            if (cucp[3] & 0x4) {
+                printf("error in returned FIS: aborted command\n");
+                printf("    try again with%s '-p' option\n",
+                       (do_packet ? "out" : ""));
+                break;
+            }
+        }
+        ok = 1;         /* not sure what is happening so output response */
+        if (0 == verbose) {
+            printf(">>> Recovered error on ATA_16, may have failed\n");
+            printf("    Add '-v' for more information\n");
+        }
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("ATA_16 command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok) { /* output result if it is available */
+        printf("Response for IDENTIFY %sDEVICE ATA command:\n",
+               (do_packet ? "PACKET " : ""));
+        dWordHex((const unsigned short *)inBuff, 256, 0,
+                 sg_is_big_endian());
+    }
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg__sat_phy_event.c b/examples/sg__sat_phy_event.c
new file mode 100644
index 0000000..40f38e1
--- /dev/null
+++ b/examples/sg__sat_phy_event.c
@@ -0,0 +1,350 @@
+/*
+ * Copyright (c) 2006-2018 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+/* This program uses a ATA PASS-THROUGH (16) SCSI command defined
+   by SAT to package an ATA READ LOG EXT (2Fh) command to fetch
+   log page 11h. That page contains SATA phy event counters.
+   For SAT see http://www.t10.org [draft prior to standard: sat-r09.pdf]
+   For ATA READ LOG EXT command see ATA-8/ACS at www.t13.org .
+   For SATA phy counter definitions see SATA 2.5 .
+
+   Invocation: sg_sat_phy_event [-v] [-V] <device>
+
+*/
+
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return Descriptor */
+
+#define ATA_READ_LOG_EXT 0x2f
+#define SATA_PHY_EVENT_LPAGE 0x11
+#define READ_LOG_EXT_RESPONSE_LEN 512
+
+#define EBUFF_SZ 256
+
+static const char * version_str = "1.03 20180220";
+
+static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"ignore", no_argument, 0, 'i'},
+        {"raw", no_argument, 0, 'r'},
+        {"reset", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void usage()
+{
+    fprintf(stderr, "Usage: "
+          "sg_sat_phy_event [--help] [--hex] [--raw] [--reset] [--verbose]\n"
+          "                        [--version] DEVICE\n"
+          "  where:\n"
+          "    --help|-h       print this usage message then exit\n"
+          "    --hex|-H        output response in hex bytes, use twice for\n"
+          "                    hex words\n"
+          "    --ignore|-i     ignore identifier names, output id value "
+          "instead\n"
+          "    --raw|-r        output response in binary to stdout\n"
+          "    --reset|-R      reset counters (after read)\n"
+          "    --verbose|-v    increase verbosity\n"
+          "    --version|-V    print version string then exit\n\n"
+          "Sends an ATA READ LOG EXT command via a SAT pass through to "
+          "fetch\nlog page 11h which contains SATA phy event counters\n");
+}
+
+struct phy_event_t {
+    int id;
+    const char * desc;
+};
+
+static struct phy_event_t phy_event_arr[] = {
+    {0x1, "Command failed and ICRC error bit set in Error register"},
+    {0x2, "R_ERR(p) response for data FIS"},
+    {0x3, "R_ERR(p) response for device-to-host data FIS"},
+    {0x4, "R_ERR(p) response for host-to-device data FIS"},
+    {0x5, "R_ERR(p) response for non-data FIS"},
+    {0x6, "R_ERR(p) response for device-to-host non-data FIS"},
+    {0x7, "R_ERR(p) response for host-to-device non-data FIS"},
+    {0x8, "Device-to-host non-data FIS retries"},
+    {0x9, "Transition from drive PHYRDY to drive PHYRDYn"},
+    {0xa, "Signature device-to-host register FISes due to COMRESET"},
+    {0xb, "CRC errors within host-to-device FIS"},
+    {0xd, "non CRC errors within host-to-device FIS"},
+    {0xf, "R_ERR(p) response for host-to-device data FIS, CRC"},
+    {0x10, "R_ERR(p) response for host-to-device data FIS, non-CRC"},
+    {0x12, "R_ERR(p) response for host-to-device non-data FIS, CRC"},
+    {0x13, "R_ERR(p) response for host-to-device non-data FIS, non-CRC"},
+    {0xc00, "PM: host-to-device non-data FIS, R_ERR(p) due to collision"},
+    {0xc01, "PM: signature register - device-to-host FISes"},
+    {0xc02, "PM: corrupts CRC propagation of device-to-host FISes"},
+    {0x0, NULL},
+};
+
+static const char * find_phy_desc(int id)
+{
+    const struct phy_event_t * pep;
+
+    for (pep = phy_event_arr; pep->desc; ++pep) {
+        if ((id & 0xfff) == pep->id)
+            return pep->desc;
+    }
+    return NULL;
+}
+
+static void dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0 ; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, c, k, j, ok, res, id, len, vendor;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    sg_io_hdr_t io_hdr;
+    char * device_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t inBuff[READ_LOG_EXT_RESPONSE_LEN];
+    uint8_t sense_buffer[64];
+    int hex = 0;
+    int ignore = 0;
+    int raw = 0;
+    int reset = 0;
+    int verbose = 0;
+    int extend = 0;
+    int chk_cond = 0;   /* set to 1 to read register(s) back */
+    int protocol = 4;   /* PIO data-in */
+    int t_dir = 1;      /* 0 -> to device, 1 -> from device */
+    int byte_block = 1; /* 0 -> bytes, 1 -> 512 byte blocks */
+    int t_length = 2;   /* 0 -> no data transferred, 2 -> sector count */
+    const uint8_t * cucp;
+    int ret = 0;
+    uint64_t ull;
+    const char * cp;
+
+    memset(inBuff, 0, sizeof(inBuff));
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "hHirRvV",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'h':
+            usage();
+            exit(0);
+        case 'H':
+            ++hex;
+            break;
+        case 'i':
+            ++ignore;
+            break;
+        case 'r':
+            ++raw;
+            break;
+        case 'R':
+            ++reset;
+            break;
+        case 'v':
+            ++verbose;
+            break;
+        case 'V':
+            fprintf(stderr, "version: %s\n", version_str);
+            exit(0);
+        default:
+            fprintf(stderr, "unrecognised option code %c [0x%x]\n", c, c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                fprintf(stderr, "Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (0 == device_name) {
+        fprintf(stderr, "no DEVICE name detected\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if ((sg_fd = open(device_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_sat_phy_event: error opening file: %s", device_name);
+        perror(ebuff);
+        return SG_LIB_FILE_ERROR;
+    }
+
+    /* Prepare SCSI ATA PASS-THROUGH COMMAND (16) command */
+    if (reset > 0)
+        apt_cdb[4] = 1;                       /* features (7:0) */
+    apt_cdb[6] = 1;                           /* sector count */
+    apt_cdb[8] = SATA_PHY_EVENT_LPAGE;        /* lba_low (7:0) */
+    apt_cdb[14] = ATA_READ_LOG_EXT;           /* command */
+    apt_cdb[1] = (protocol << 1) | extend;
+    apt_cdb[2] = (chk_cond << 5) | (t_dir << 3) | (byte_block << 2) |
+                 t_length;
+    if (verbose) {
+        fprintf(stderr, "    ata pass through(16) cdb: ");
+        for (k = 0; k < SAT_ATA_PASS_THROUGH16_LEN; ++k)
+            fprintf(stderr, "%02x ", apt_cdb[k]);
+        fprintf(stderr, "\n");
+    }
+
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(apt_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = READ_LOG_EXT_RESPONSE_LEN;
+    io_hdr.dxferp = inBuff;
+    io_hdr.cmdp = apt_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_sat_phy_event: SG_IO ioctl error");
+        close(sg_fd);
+        return SG_LIB_CAT_OTHER;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    ret = sg_err_category3(&io_hdr);
+    switch (ret) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        if (verbose)
+            sg_chk_n_print3(">>> ATA_16 command", &io_hdr, 1);
+        /* check for ATA Return Descriptor */
+        cucp = sg_scsi_sense_desc_find(io_hdr.sbp, io_hdr.sb_len_wr,
+                                       SAT_ATA_RETURN_DESC);
+        if (cucp && (cucp[3])) {
+            if (cucp[3] & 0x4) {
+                fprintf(stderr, "error in returned FIS: aborted command\n");
+                break;
+            }
+        }
+        ret = 0;
+        ok = 1;         /* not sure what is happening so output response */
+        if (0 == verbose) {
+            fprintf(stderr, ">>> Recovered error on ATA_16, may have "
+                    "failed\n");
+            fprintf(stderr, "    Add '-v' for more information\n");
+        }
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("ATA_16 command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok) { /* output result if it is available */
+        if (raw > 0)
+            dStrRaw(inBuff, 512);
+        else {
+            if (verbose && hex)
+                fprintf(stderr, "Response to READ LOG EXT (page=11h):\n");
+            if (1 == hex)
+                hex2stdout(inBuff, 512, 0);
+            else if (hex > 1)
+                dWordHex((const unsigned short *)inBuff, 256, 0,
+                         sg_is_big_endian());
+            else {
+                printf("SATA phy event counters:\n");
+                for (k = 4; k < 512; k += (len + 2)) {
+                    id = (inBuff[k + 1] << 8) + inBuff[k];
+                    if (0 == id)
+                        break;
+                    len = ((id >> 12) & 0x7) * 2;
+                    vendor = !!(id & 0x8000);
+                    id = id & 0xfff;
+                    ull = 0;
+                    for (j = len - 1; j >= 0; --j) {
+                        if (j < (len - 1))
+                            ull <<= 8;
+                        ull |= inBuff[k + 2 + j];
+                    }
+                    cp = NULL;
+                    if ((0 == vendor) && (0 == ignore))
+                        cp = find_phy_desc(id);
+                    if (cp)
+                        printf("  %s: %" PRIu64 "\n", cp, ull);
+                    else
+                        printf("  id=0x%x, vendor=%d, data_len=%d, "
+                               "val=%" PRIu64 "\n", id, vendor, len, ull);
+                }
+            }
+        }
+    }
+    res = close(sg_fd);
+    if (res < 0) {
+        fprintf(stderr, "close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            return SG_LIB_FILE_ERROR;
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/examples/sg__sat_set_features.c b/examples/sg__sat_set_features.c
new file mode 100644
index 0000000..0b20047
--- /dev/null
+++ b/examples/sg__sat_set_features.c
@@ -0,0 +1,280 @@
+/*
+ * Copyright (c) 2006-2020 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ *    derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+/* This program performs a ATA PASS-THROUGH (16) SCSI command in order
+   to perform an ATA SET FEATURES command. See http://www.t10.org
+   SAT draft at time of writing: sat-r09.pdf
+
+   Invocation:
+        sg_sat_set_features [-c <n>] [-f <n>] [-h] [-L <n>] [-v] [-V] <device>
+
+*/
+
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return (sense) Descriptor */
+
+#define ATA_SET_FEATURES 0xef
+
+#define EBUFF_SZ 512
+
+static char * version_str = "1.06 20201125";
+
+static struct option long_options[] = {
+        {"count", required_argument, 0, 'c'},
+        {"chk_cond", no_argument, 0, 'C'},
+        {"feature", required_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"lba", required_argument, 0, 'L'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+void usage()
+{
+    fprintf(stderr, "Usage: "
+          "sg_sat_set_features [--count=C] [--chk_cond] [--feature=F] "
+          "[--help]\n"
+          "                           [-lba=LBA] [--verbose] [--version] "
+          "DEVICE\n"
+          "  where:\n"
+          "    --count=C|-c C       count field contents (def: 0)\n"
+          "    --chk_cond|-C        set chk_cond field in pass-through "
+          "(def: 0)\n"
+          "    --feature=F|-f F     feature field contents (def: 0)\n"
+          "    --help|-h            output this usage message\n"
+          "    --lba=LBA| -L LBA    LBA field contents (def: 0)\n"
+          "    --verbose|-v         increase verbosity\n"
+          "    --version|-V         print version string and exit\n\n"
+          "Sends an ATA SET FEATURES command via a SAT pass through.\n"
+          "Primary feature code is placed in '--feature=F' with '--count=C' "
+          "and\n"
+          "'--lba=LBA' being auxiliaries for some features.  The arguments C, "
+          "F and LBA\n"
+          "are decimal unless prefixed by '0x' or have a trailing 'h'.\n"
+          "Example enabling write cache: 'sg_sat_set_feature --feature=2 "
+          "/dev/sdc'\n");
+}
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, c, k;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    sg_io_hdr_t io_hdr;
+    char device_name[256];
+    char ebuff[EBUFF_SZ];
+    uint8_t sense_buffer[64];
+    int count = 0;
+    int feature = 0;
+    int lba = 0;
+    int verbose = 0;
+    int extend = 0;
+    int chk_cond = 0;   /* set to 1 to read register(s) back */
+    int protocol = 3;   /* non-data data-in */
+    int t_dir = 1;      /* 0 -> to device, 1 -> from device */
+    int byte_block = 1; /* 0 -> bytes, 1 -> 512 byte blocks */
+    int t_length = 0;   /* 0 -> no data transferred, 2 -> sector count */
+    const uint8_t * bp = NULL;
+
+    memset(device_name, 0, sizeof(device_name));
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "c:Cf:hL:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            count = sg_get_num(optarg);
+            if ((count < 0) || (count > 255)) {
+                fprintf(stderr, "bad argument for '--count'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'C':
+            chk_cond = 1;
+            break;
+        case 'f':
+            feature = sg_get_num(optarg);
+            if ((feature < 0) || (feature > 255)) {
+                fprintf(stderr, "bad argument for '--feature'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'L':
+            lba = sg_get_num(optarg);
+            if ((lba < 0) || (lba > 255)) {
+                fprintf(stderr, "bad argument for '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            ++verbose;
+            break;
+        case 'V':
+            fprintf(stderr, "version: %s\n", version_str);
+            return 0;
+        default:
+            fprintf(stderr, "unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if ('\0' == device_name[0]) {
+            strncpy(device_name, argv[optind], sizeof(device_name) - 1);
+            device_name[sizeof(device_name) - 1] = '\0';
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                fprintf(stderr, "Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    if ('\0' == device_name[0]) {
+        fprintf(stderr, "missing device name!\n");
+        usage();
+        return 1;
+    }
+
+    if ((sg_fd = open(device_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_sat_set_features: error opening file: %s", device_name);
+        perror(ebuff);
+        return 1;
+    }
+
+    /* Prepare ATA PASS-THROUGH COMMAND (16) command */
+    apt_cdb[14] = ATA_SET_FEATURES;
+    apt_cdb[1] = (protocol << 1) | extend;
+    apt_cdb[2] = (chk_cond << 5) | (t_dir << 3) | (byte_block << 2) |
+                 t_length;
+    apt_cdb[4] = feature;
+    apt_cdb[6] = count;
+    apt_cdb[8] = lba & 0xff;
+    apt_cdb[10] = (lba >> 8) & 0xff;
+    apt_cdb[12] = (lba >> 16) & 0xff;
+    if (verbose) {
+        fprintf(stderr, "    ata pass through(16) cdb: ");
+        for (k = 0; k < SAT_ATA_PASS_THROUGH16_LEN; ++k)
+            fprintf(stderr, "%02x ", apt_cdb[k]);
+        fprintf(stderr, "\n");
+    }
+
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(apt_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_NONE;
+    io_hdr.dxfer_len = 0;
+    io_hdr.dxferp = NULL;
+    io_hdr.cmdp = apt_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_sat_set_features: SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* error processing: N.B. expect check condition, no sense ... !! */
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        break;
+    case SG_LIB_CAT_RECOVERED:  /* sat-r09 uses this sk */
+    case SG_LIB_CAT_NO_SENSE:   /* earlier SAT drafts used this */
+        bp = sg_scsi_sense_desc_find(sense_buffer, sizeof(sense_buffer),
+                                     SAT_ATA_RETURN_DESC);
+        if (NULL == bp) {
+            if (verbose > 1)
+                printf("ATA Return Descriptor expected in sense but not "
+                       "found\n");
+            sg_chk_n_print3("ATA_16 command error", &io_hdr, 1);
+        } else if (verbose)
+            sg_chk_n_print3("ATA Return Descriptor", &io_hdr, 1);
+        if (bp && bp[3]) {
+            if (bp[3] & 0x4)
+                printf("error in returned FIS: aborted command\n");
+            else
+                printf("error=0x%x, status=0x%x\n", bp[3], bp[13]);
+        }
+        break;
+    default:
+        fprintf(stderr, "unexpected SCSI sense category\n");
+        bp = sg_scsi_sense_desc_find(sense_buffer, sizeof(sense_buffer),
+                                     SAT_ATA_RETURN_DESC);
+        if (NULL == bp)
+            sg_chk_n_print3("ATA_16 command error", &io_hdr, 1);
+        else if (verbose)
+            sg_chk_n_print3("ATA Return Descriptor, as expected",
+                             &io_hdr, 1);
+        if (bp && bp[3]) {
+            if (bp[3] & 0x4)
+                printf("error in returned FIS: aborted command\n");
+            else
+                printf("error=0x%x, status=0x%x\n", bp[3], bp[13]);
+        }
+        break;
+    }
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_compare_and_write.txt b/examples/sg_compare_and_write.txt
new file mode 100644
index 0000000..86b166b
--- /dev/null
+++ b/examples/sg_compare_and_write.txt
@@ -0,0 +1,67 @@
+#		sg_compare_and_write.txt
+# This file provides a usage example of sg_compare_and_write.
+# sg_compare_and_write accepts a buffer containing 2 logical instances:
+# - the verify instance: used to match the current content of the LBA range
+# - the write instance: used to write to the LBA if the verify succeeds
+#
+# In case of failure to verify the data, the command will return with check
+# condition with the sense code set to MISCOMPARE DURING VERIFY OPERATION.
+#
+# The following example shows initialization, successful and unsuccessful
+# compare and write using sg3_utils. I am using caw_buf_zero2one and
+# caw_buf_one2zero as shown below.
+
+$ hexdump /tmp/caw_buf_zero2one
+0000000 0000 0000 0000 0000 0000 0000 0000 0000
+*
+0000200 1111 1111 1111 1111 1111 1111 1111 1111
+*
+0000400
+
+$ hexdump /tmp/caw_buf_one2zero
+0000000 1111 1111 1111 1111 1111 1111 1111 1111
+*
+0000200 0000 0000 0000 0000 0000 0000 0000 0000
+*
+0000400
+
+$ sg_map -i -x
+/dev/sg0  0 0 0 0  0  /dev/sda  ATA       ST3320613AS       CC2H
+/dev/sg1  3 0 0 0  5  /dev/scd0  HL-DT-ST  DVD-RAM GH22NS30  1.01
+/dev/sg2  5 0 0 0  0  /dev/sdb  KMNRIO    K2                0000
+/dev/sg3  5 0 0 1  0  /dev/sdc  KMNRIO    K2                0000
+
+# First I zero out the volume to make sure that the first compare and write
+# will succeed
+$ sg_write_same --16 -i /dev/zero -n 0x200000  -x 512 /dev/sdc
+
+$ dd if=/dev/sdc bs=512 count=1 skip=100 2>/dev/null | hexdump
+0000000 0000 0000 0000 0000 0000 0000 0000 0000
+*
+0000200
+
+$ ./sg_compare_and_write --in=/tmp/caw_buf_zero2one --lba=100 --xferlen=1024 /dev/sdc
+
+# contents of LBA 100 are a block of ones
+$ dd if=/dev/sdc bs=512 count=1 skip=100 2>/dev/null | hexdump
+0000000 1111 1111 1111 1111 1111 1111 1111 1111
+*
+0000200
+
+# We repeat the same compare and write command (zero2one input buffer).
+# compare and write fails since the verify failed (compared the zero block to
+# the actual 1 block in LBA 100
+$ ./sg_compare_and_write --in=/tmp/caw_buf_zero2one --lba=100 --xferlen=1024 /dev/sdc
+COMPARE AND WRITE:  Fixed format, current;  Sense key: Miscompare
+ Additional sense: Miscompare during verify operation
+sg_compare_and_write: SCSI COMPARE AND WRITE failed
+
+# Now we use the second buffer (one2zero)
+$ ./sg_compare_and_write --in=/tmp/caw_buf_one2zero --lba=100 --xferlen=1024 /dev/sdc
+
+# operation succeeded, contents of LBA 100 are back to zero
+$ dd if=/dev/sdc bs=512 count=1 skip=100 2>/dev/null | hexdump
+0000000 0000 0000 0000 0000 0000 0000 0000 0000
+*
+0000200
+
diff --git a/examples/sg_excl.c b/examples/sg_excl.c
new file mode 100644
index 0000000..7e589b2
--- /dev/null
+++ b/examples/sg_excl.c
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2003-2018 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This is a simple program that tests the O_EXCL flag in sg while
+ * executing a SCSI INQUIRY command and a
+ * TEST UNIT READY command using the SCSI generic (sg) driver
+ *
+ * Invocation: sg_excl [-x] <sg_device>
+ *
+ * Version 3.62 (20181227)
+ *
+ * 6 byte INQUIRY command:
+ * [0x12][   |lu][pg cde][res   ][al len][cntrl ]
+ *
+ * 6 byte TEST UNIT READY command:
+ * [0x00][   |lu][res   ][res   ][res   ][res   ]
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+#define TUR_CMD_LEN 6
+
+#define EBUFF_SZ 256
+
+#define ME "sg_excl: "
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k, ok /*, sg_fd2 */;
+    uint8_t inq_cdb [INQ_CMD_LEN] = {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    uint8_t tur_cdb [TUR_CMD_LEN] = {0x00, 0, 0, 0, 0, 0};
+    uint8_t inqBuff[INQ_REPLY_LEN];
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t sense_buffer[32];
+    int do_extra = 0;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-x", argv[k], 2))
+            do_extra = 1;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_excl [-x] <sg_device>'\n");
+        return 1;
+    }
+
+    /* N.B. An access mode of O_RDWR is required for some SCSI commands */
+    if ((sg_fd = open(file_name, O_RDWR | O_EXCL | O_NONBLOCK)) < 0) {
+        snprintf(ebuff, EBUFF_SZ, ME "error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    /* Just to be safe, check we have a new sg device by trying an ioctl */
+    if ((ioctl(sg_fd, SG_GET_VERSION_NUM, &k) < 0) || (k < 30000)) {
+        printf(ME "%s doesn't seem to be an new sg device\n",
+               file_name);
+        close(sg_fd);
+        return 1;
+    }
+#if 0
+    if ((sg_fd2 = open(file_name, O_RDWR | O_EXCL)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 ME "error opening file: %s a second time", file_name);
+        perror(ebuff);
+        return 1;
+    } else {
+        printf(ME "second open of %s in violation of O_EXCL\n", file_name);
+        close(sg_fd2);
+    }
+#endif
+
+    /* Prepare INQUIRY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(inq_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = INQ_REPLY_LEN;
+    io_hdr.dxferp = inqBuff;
+    io_hdr.cmdp = inq_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror(ME "Inquiry SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on INQUIRY, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("INQUIRY command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok) { /* output result if it is available */
+        char * p = (char *)inqBuff;
+        int f = (int)*(p + 7);
+        printf("Some of the INQUIRY command's results:\n");
+        printf("    %.8s  %.16s  %.4s  ", p + 8, p + 16, p + 32);
+        printf("[wide=%d sync=%d cmdque=%d sftre=%d]\n",
+               !!(f & 0x20), !!(f & 0x10), !!(f & 2), !!(f & 1));
+        /* Extra info, not necessary to look at */
+        if (do_extra)
+            printf("INQUIRY duration=%u millisecs, resid=%d, msg_status=%d\n",
+                   io_hdr.duration, io_hdr.resid, (int)io_hdr.msg_status);
+    }
+
+
+    /* Prepare TEST UNIT READY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(tur_cdb);
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_NONE;
+    io_hdr.cmdp = tur_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror(ME "Test Unit Ready SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on Test Unit Ready, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("Test Unit Ready command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok)
+        printf("Test Unit Ready successful so unit is ready!\n");
+    else
+        printf("Test Unit Ready failed so unit may _not_ be ready!\n");
+
+    if (do_extra)
+        printf("TEST UNIT READY duration=%u millisecs, resid=%d, "
+               "msg_status=%d\n", io_hdr.duration, io_hdr.resid,
+                (int)io_hdr.msg_status);
+
+    printf("Wait for 60 seconds with O_EXCL help on %s\n", file_name);
+    sleep(60);
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_persist_tst.sh b/examples/sg_persist_tst.sh
new file mode 100755
index 0000000..a75f997
--- /dev/null
+++ b/examples/sg_persist_tst.sh
@@ -0,0 +1,130 @@
+#!/bin/sh
+# This script is meant as an example of using the sg_persist utility
+# in the sg3_utils package. This script works as expected on the
+# author's Fujitsu MAM3184, Seagate ST373455 and ST9146803SS disks.
+#
+#  Version 2.0 20171104
+
+# N.B. make sure the device name is correct for your environment.
+
+key="123abc"
+key2="333aaa"
+kk=${key}
+rtype="1"
+verbose=""
+
+usage()
+{
+  echo "Usage: sg_persist_tst.sh [-e] [-h] [-s] [-v] <device>"
+  echo "  where:"
+  echo "    -e, --exclusive      exclusive access (def: write " \
+   "exclusive)"
+  echo "    -h, --help           print usage message"
+  echo "    -s, --second         use second key"
+  echo "    -v, --verbose        more verbose output"
+  echo "    -vv                  even more verbose output"
+  echo "    -vvv                 even more verbose output"
+  echo ""
+  echo "Test SCSI Persistent Reservations with sg_persist utility."
+  echo "Default key is ${key} and alternate, second key is ${key2} ."
+  echo "Should be harmless (unless one of those keys is already in use)."
+  echo "The APTPL bit is not set in the PR register so a power cycle"
+  echo "on the device will clear the reservation if this script stops"
+  echo "(or is stopped) before clearing it. Tape drives only seem to "
+  echo "support 'exclusive access' type (so use '-e')."
+}
+
+opt="$1"
+while test ! -z "$opt" -a -z "${opt##-*}"; do
+  opt=${opt#-}
+  case "$opt" in
+    e|-exclusive) rtype="3" ;;
+    h|-help) usage ; exit 0 ;;
+    s|-second) kk=${key2} ;;
+    vvv) verbose="-vvv" ;;
+    vv) verbose="-vv" ;;
+    v|-verbose) verbose="-v" ;;
+    *) echo "Unknown option: -$opt " ; exit 1 ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ $# -lt 1 ]
+  then
+    usage
+    exit 1
+fi
+
+echo ">>> try to report capabilities:"
+sg_persist -c ${verbose} "$1"
+res=$?
+case "$res" in
+    0) ;;
+    1) echo "  syntax error" ;;
+    2) echo "  not ready" ;;
+    3) echo "  medium error" ;;
+    5) echo "  illegal request, report capabilities not supported?" ;;
+    6) echo "  unit attention" ;;
+    9) echo "  illegal request, Persistent Reserve (In) not supported" ;;
+    11) echo "  aborted command" ;;
+    15) echo "  file error with $1 " ;;
+    20) echo "  no sense" ;;
+    21) echo "  recovered error" ;;
+    33) echo "  timeout" ;;
+    97) echo "  response fails sanity" ;;
+    98) echo "  other SCSI error" ;;
+    99) echo "  other error" ;;
+    *) echo "  unknown exit status for sg_persist: $res" ;;
+esac
+echo ""
+sleep 1
+
+echo ">>> check if any keys are registered:"
+sg_persist --no-inquiry --read-keys ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> register a key:"
+sg_persist -n --out --register --param-sark=${kk} ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> now key ${kk} should be registered:"
+sg_persist -n --read-keys ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> reserve the device (based on key ${kk}):"
+sg_persist -n --out --reserve --param-rk=${kk} --prout-type=${rtype} ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> check if the device is reserved (it should be now):"
+sg_persist -n --read-reservation ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> try to 'read full status' (may not be supported):"
+sg_persist -n --read-full-status ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> now release reservation:"
+sg_persist -n --out --release --param-rk=${kk} --prout-type=${rtype} ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> check if the device is reserved (it should _not_ be now):"
+sg_persist -n --read-reservation ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> unregister key ${kk}:"
+sg_persist -n --out --register --param-rk=${kk} ${verbose} "$1"
+sleep 1
+
+echo
+echo ">>> now key ${kk} should not be registered:"
+sg_persist -n -k ${verbose} "$1"
+sleep 1
diff --git a/examples/sg_sat_chk_power.c b/examples/sg_sat_chk_power.c
new file mode 100644
index 0000000..ca24f66
--- /dev/null
+++ b/examples/sg_sat_chk_power.c
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2006-2018 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "sg_lib.h"
+#include "sg_pr2serr.h"
+#include "sg_io_linux.h"
+
+/* This program performs a ATA PASS-THROUGH (16) SCSI command in order
+   to perform an ATA CHECK POWER MODE command. See http://www.t10.org
+   SAT draft at time of writing: sat-r09.pdf
+
+   Invocation: sg_sat_chk_power [-v] [-V] <device>
+
+*/
+
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return (sense) Descriptor */
+#define ASCQ_ATA_PT_INFO_AVAILABLE 0x1d
+
+#define ATA_CHECK_POWER_MODE 0xe5
+
+#define EBUFF_SZ 256
+
+static const char * version_str = "1.08 20181207";
+
+
+#if 0
+/* Returns length of decoded fixed format sense for SAT ATA pass-through
+ * command, else returns 0. If returns 0 (expected sense data not found)
+ * then '\0' placed in first byte of bp. */
+static int
+sg_sat_decode_fixed_sense(const uint8_t * sp, int slen, char * bp,
+                          int max_blen, int verbose)
+{
+    int n;
+
+    if ((NULL == bp) || (NULL == sp) || (max_blen < 1) || (slen < 14))
+        return 0;
+    bp[0] = '\0';
+    if ((0x70 != (0x7f & sp[0])) ||
+        (SPC_SK_RECOVERED_ERROR != (0xf & sp[2])) ||
+        (0 != sp[12]) || (ASCQ_ATA_PT_INFO_AVAILABLE != sp[13]))
+        return 0;
+    n = sg_scnpr(bp, max_blen, "error=0x%x, status=0x%x, device=0x%x, "
+                 "sector_count(7:0)=0x%x%c\n", sp[3], sp[4], sp[5], sp[6],
+                 ((0x40 & sp[8]) ? '+' : ' '));
+    if (n >= max_blen)
+        return max_blen - 1;
+    n += sg_scnpr(bp + n, max_blen - n, "extend=%d, log_index=0x%x, "
+                  "lba_high,mid,low(7:0)=0x%x,0x%x,0x%x%c\n",
+                  (!!(0x80 & sp[8])), (0xf & sp[8]), sp[9], sp[10], sp[11],
+                  ((0x20 & sp[8]) ? '+' : ' '));
+    if (n >= max_blen)
+        return max_blen - 1;
+    if (verbose)
+        n += sg_scnpr(bp + n, max_blen - n, "  sector_count_upper_nonzero="
+                      "%d, lba_upper_nonzero=%d\n", !!(0x40 & sp[8]),
+                      !!(0x20 & sp[8]));
+    return (n >= max_blen) ? max_blen - 1 : n;
+}
+#endif
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t sense_buffer[64];
+    int verbose = 0;
+    int extend = 0;
+    int chk_cond = 1;   /* set to 1 to read register(s) back */
+    int protocol = 3;   /* non-dat data-in */
+    int t_dir = 1;      /* 0 -> to device, 1 -> from device */
+    int byte_block = 1; /* 0 -> bytes, 1 -> 512 byte blocks */
+    int t_length = 0;   /* 0 -> no data transferred, 2 -> sector count */
+    const uint8_t * bp = NULL;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == strcmp(argv[k], "-v"))
+            ++verbose;
+        else if (0 == strcmp(argv[k], "-vv"))
+            verbose += 2;
+        else if (0 == strcmp(argv[k], "-vvv"))
+            verbose += 3;
+        else if (0 == strcmp(argv[k], "-V")) {
+            fprintf(stderr, "version: %s\n", version_str);
+            exit(0);
+        } else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_sat_chk_power [-v] [-V] <device>'\n");
+        return 1;
+    }
+
+    if ((sg_fd = open(file_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_sat_chk_power: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+
+    /* Prepare ATA PASS-THROUGH COMMAND (16) command */
+    apt_cdb[14] = ATA_CHECK_POWER_MODE;
+    apt_cdb[1] = (protocol << 1) | extend;
+    apt_cdb[2] = (chk_cond << 5) | (t_dir << 3) |
+                   (byte_block << 2) | t_length;
+    if (verbose) {
+        fprintf(stderr, "    ata pass through(16) cdb: ");
+        for (k = 0; k < SAT_ATA_PASS_THROUGH16_LEN; ++k)
+            fprintf(stderr, "%02x ", apt_cdb[k]);
+        fprintf(stderr, "\n");
+    }
+
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(apt_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_NONE;
+    io_hdr.dxfer_len = 0;
+    io_hdr.dxferp = NULL;
+    io_hdr.cmdp = apt_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_sat_chk_power: SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* error processing: N.B. expect check condition, no sense ... !! */
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        break;
+    case SG_LIB_CAT_RECOVERED:  /* sat-r09 (latest) uses this sk */
+    case SG_LIB_CAT_NO_SENSE:   /* earlier SAT drafts used this */
+        /* XXX: Until the spec decides which one to go with. 20060607 */
+        bp = sg_scsi_sense_desc_find(sense_buffer, sizeof(sense_buffer),
+                                     SAT_ATA_RETURN_DESC);
+        if (NULL == bp) {
+            if (verbose > 1)
+                printf("ATA Return Descriptor expected in sense but not "
+                       "found\n");
+            sg_chk_n_print3("ATA_16 command error", &io_hdr, 1);
+        } else if (verbose)
+            sg_chk_n_print3("ATA Return Descriptor, as expected",
+                             &io_hdr, 1);
+        if (bp && bp[3]) {
+            if (bp[3] & 0x4)
+                printf("error in returned FIS: aborted command\n");
+            else
+                printf("error=0x%x, status=0x%x\n", bp[3], bp[13]);
+        }
+        break;
+    default:
+        fprintf(stderr, "unexpected SCSI sense category\n");
+        bp = sg_scsi_sense_desc_find(sense_buffer, sizeof(sense_buffer),
+                                     SAT_ATA_RETURN_DESC);
+        if (NULL == bp)
+            sg_chk_n_print3("ATA_16 command error", &io_hdr, 1);
+        else if (verbose)
+            sg_chk_n_print3("ATA Return Descriptor, as expected",
+                             &io_hdr, 1);
+        if (bp && bp[3]) {
+            if (bp[3] & 0x4)
+                printf("error in returned FIS: aborted command\n");
+            else
+                printf("error=0x%x, status=0x%x\n", bp[3], bp[13]);
+        }
+        break;
+    }
+
+    if (bp) {
+        switch (bp[5]) {       /* sector_count (7:0) */
+        case 0xff:
+            printf("In active mode or idle mode\n");
+            break;
+        case 0x80:
+            printf("In idle mode\n");
+            break;
+        case 0x41:
+            printf("In NV power mode and spindle is spun or spinning up\n");
+            break;
+        case 0x40:
+            printf("In NV power mode and spindle is spun or spinning down\n");
+            break;
+        case 0x0:
+            printf("In standby mode\n");
+            break;
+        default:
+            printf("unknown power mode (sector count) value=0x%x\n", bp[5]);
+            break;
+        }
+    } else
+        fprintf(stderr, "Expecting a ATA Return Descriptor in sense and "
+                "didn't receive it\n");
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_sat_smart_rd_data.c b/examples/sg_sat_smart_rd_data.c
new file mode 100644
index 0000000..223da2a
--- /dev/null
+++ b/examples/sg_sat_smart_rd_data.c
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2006-2018 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+/* This program performs a ATA PASS-THROUGH (16) SCSI command in order
+   to perform an ATA SMART/READ DATA command. See http://www.t10.org
+   SAT draft at time of writing: sat-r08.pdf
+
+   Invocation: sg_sat_smart_rd_data [-v] [-V] <device>
+
+*/
+
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return (sense) Descriptor */
+
+#define ATA_SMART 0xb0
+#define ATA_SMART_READ_DATA 0xd0
+#define SMART_READ_DATA_RESPONSE_LEN 512
+
+#define EBUFF_SZ 256
+
+static char * version_str = "1.05 20181207";
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k, ok;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t inBuff[SMART_READ_DATA_RESPONSE_LEN];
+    uint8_t sense_buffer[32];
+    int verbose = 0;
+    int extend = 0;
+    int chk_cond = 0;   /* set to 1 to read register(s) back */
+    int protocol = 4;   /* PIO data-in */
+    int t_dir = 1;      /* 0 -> to device, 1 -> from device */
+    int byte_block = 1; /* 0 -> bytes, 1 -> 512 byte blocks */
+    int t_length = 2;   /* 0 -> no data transferred, 2 -> sector count */
+    const uint8_t * bp = NULL;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == strcmp(argv[k], "-v"))
+            ++verbose;
+        else if (0 == strcmp(argv[k], "-vv"))
+            verbose += 2;
+        else if (0 == strcmp(argv[k], "-vvv"))
+            verbose += 3;
+        else if (0 == strcmp(argv[k], "-V")) {
+            fprintf(stderr, "version: %s\n", version_str);
+            exit(0);
+        } else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_sat_smart_rd_data [-v] [-V] <device>'\n");
+        return 1;
+    }
+
+    if ((sg_fd = open(file_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_sat_smart_rd_data: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+
+    /* Prepare ATA PASS-THROUGH COMMAND (16) command */
+    apt_cdb[4] = ATA_SMART_READ_DATA;   /* feature (7:0) */
+    apt_cdb[6] = 1;   /* number of block (sector count) */
+    apt_cdb[10] = 0x4f;    /* lba_mid (7:0) */
+    apt_cdb[12] = 0xc2;    /* lba_high (7:0) */
+    apt_cdb[14] = ATA_SMART;
+    apt_cdb[1] = (protocol << 1) | extend;
+    apt_cdb[2] = (chk_cond << 5) | (t_dir << 3) | (byte_block << 2) |
+                 t_length;
+    if (verbose) {
+        fprintf(stderr, "    ata pass through(16) cdb: ");
+        for (k = 0; k < SAT_ATA_PASS_THROUGH16_LEN; ++k)
+            fprintf(stderr, "%02x ", apt_cdb[k]);
+        fprintf(stderr, "\n");
+    }
+
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(apt_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = SMART_READ_DATA_RESPONSE_LEN;
+    io_hdr.dxferp = inBuff;
+    io_hdr.cmdp = apt_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_sat_smart_rd_data: SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        bp = sg_scsi_sense_desc_find(sense_buffer, sizeof(sense_buffer),
+                                     SAT_ATA_RETURN_DESC);
+        if (NULL == bp) {
+            if (verbose > 1)
+                printf("ATA Return Descriptor expected in sense but not "
+                       "found\n");
+            sg_chk_n_print3("ATA_16 command error", &io_hdr, 1);
+        } else if (verbose)
+            sg_chk_n_print3("ATA Return Descriptor", &io_hdr, 1);
+        if (bp && bp[3])
+            printf("error=0x%x, status=0x%x\n", bp[3], bp[13]);
+        else
+            ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("ATA_16 command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok) { /* output result if it is available */
+        printf("Response:\n");
+        dWordHex((const unsigned short *)inBuff, 256, 0,
+                 sg_is_big_endian());
+    }
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_simple1.c b/examples/sg_simple1.c
new file mode 100644
index 0000000..b8ef349
--- /dev/null
+++ b/examples/sg_simple1.c
@@ -0,0 +1,193 @@
+/*
+ *   Copyright (C) 1999-2018 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This is a simple program executing a SCSI INQUIRY command and a
+ * TEST UNIT READY command using the SCSI generic (sg) driver
+ * There is another variant of this program called "sg_simple2"
+ * which does not include the sg_lib.h header and logic and so has
+ * simpler but more primitive error processing.
+ * In the lk 2.6 series devices nodes such as /dev/sda also support
+ * the SG_IO ioctl.
+ *
+ *   Invocation: sg_simple1 [-x] <scsi_device>
+ *
+ *   Version 3.60 (20181207)
+ *
+ * 6 byte INQUIRY command:
+ * [0x12][   |lu][pg cde][res   ][al len][cntrl ]
+ *
+ * 6 byte TEST UNIT READY command:
+ * [0x00][   |lu][res   ][res   ][res   ][res   ]
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+#define TUR_CMD_LEN 6
+
+#define EBUFF_SZ 256
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k, ok;
+    unsigned char inq_cdb [INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    unsigned char tur_cdb [TUR_CMD_LEN] =
+                                {0x00, 0, 0, 0, 0, 0};
+    unsigned char inqBuff[INQ_REPLY_LEN];
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    unsigned char sense_buffer[32];
+    int do_extra = 0;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-x", argv[k], 2))
+            do_extra = 1;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_simple1 [-x] <sg_device>'\n");
+        return 1;
+    }
+
+    /* N.B. An access mode of O_RDWR is required for some SCSI commands */
+    if ((sg_fd = open(file_name, O_RDONLY)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_simple1: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    /* Just to be safe, check we have a new sg device by trying an ioctl */
+    if ((ioctl(sg_fd, SG_GET_VERSION_NUM, &k) < 0) || (k < 30000)) {
+        printf("sg_simple1: %s doesn't seem to be an new sg device\n",
+               file_name);
+        close(sg_fd);
+        return 1;
+    }
+
+    /* Prepare INQUIRY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(inq_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = INQ_REPLY_LEN;
+    io_hdr.dxferp = inqBuff;
+    io_hdr.cmdp = inq_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple1: Inquiry SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on INQUIRY, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("INQUIRY command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok) { /* output result if it is available */
+        char * p = (char *)inqBuff;
+        int f = (int)*(p + 7);
+        printf("Some of the INQUIRY command's results:\n");
+        printf("    %.8s  %.16s  %.4s  ", p + 8, p + 16, p + 32);
+        printf("[wide=%d sync=%d cmdque=%d sftre=%d]\n",
+               !!(f & 0x20), !!(f & 0x10), !!(f & 2), !!(f & 1));
+        /* Extra info, not necessary to look at */
+        if (do_extra)
+            printf("INQUIRY duration=%u millisecs, resid=%d, msg_status=%d\n",
+                   io_hdr.duration, io_hdr.resid, (int)io_hdr.msg_status);
+    }
+
+
+    /* Prepare TEST UNIT READY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(tur_cdb);
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_NONE;
+    io_hdr.cmdp = tur_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple1: Test Unit Ready SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on Test Unit Ready, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("Test Unit Ready command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok)
+        printf("Test Unit Ready successful so unit is ready!\n");
+    else
+        printf("Test Unit Ready failed so unit may _not_ be ready!\n");
+
+    if (do_extra)
+        printf("TEST UNIT READY duration=%u millisecs, resid=%d, "
+               "msg_status=%d\n", io_hdr.duration, io_hdr.resid,
+               (int)io_hdr.msg_status);
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_simple16.c b/examples/sg_simple16.c
new file mode 100644
index 0000000..e119ca3
--- /dev/null
+++ b/examples/sg_simple16.c
@@ -0,0 +1,122 @@
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+/* This program performs a READ_16 command as scsi mid-level support
+   16 byte commands from lk 2.4.15
+
+*  Copyright (C) 2001-2018 D. Gilbert
+*  This program is free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2, or (at your option)
+*  any later version.
+
+   Invocation: sg_simple16 <scsi_device>
+
+   Version 1.04 (20180218)
+
+*/
+
+#define READ16_REPLY_LEN 512
+#define READ16_CMD_LEN 16
+
+#define EBUFF_SZ 256
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k, ok;
+    uint8_t r16_cdb [READ16_CMD_LEN] =
+                {0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t inBuff[READ16_REPLY_LEN];
+    uint8_t sense_buffer[32];
+
+    for (k = 1; k < argc; ++k) {
+        if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_simple16 <sg_device>'\n");
+        return 1;
+    }
+
+    if ((sg_fd = open(file_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_simple16: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    /* Just to be safe, check we have a new sg device by trying an ioctl */
+    if ((ioctl(sg_fd, SG_GET_VERSION_NUM, &k) < 0) || (k < 30000)) {
+        printf("sg_simple16: %s doesn't seem to be an new sg device\n",
+               file_name);
+        close(sg_fd);
+        return 1;
+    }
+
+    /* Prepare READ_16 command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(r16_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = READ16_REPLY_LEN;
+    io_hdr.dxferp = inBuff;
+    io_hdr.cmdp = r16_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple16: Inquiry SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on READ_16, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("READ_16 command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok) { /* output result if it is available */
+        printf("READ_16 duration=%u millisecs, resid=%d, msg_status=%d\n",
+               io_hdr.duration, io_hdr.resid, (int)io_hdr.msg_status);
+    }
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_simple2.c b/examples/sg_simple2.c
new file mode 100644
index 0000000..a0710be
--- /dev/null
+++ b/examples/sg_simple2.c
@@ -0,0 +1,197 @@
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_linux_inc.h"
+
+/* This is a simple program executing a SCSI INQUIRY command and a
+   TEST UNIT READY command using the SCSI generic (sg) driver.
+   There is another variant of this program called "sg_simple1"
+   which includes the sg_lib.h header and logic and so has more
+   advanced error processing.
+   This version demonstrates the "sg3" interface.
+   In the lk 2.6 series devices nodes such as /dev/sda also support
+   the SG_IO ioctl.
+
+*  Copyright (C) 1999-2016 D. Gilbert
+*  This program is free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2, or (at your option)
+*  any later version.
+
+   Invocation: sg_simple2 [-x] <scsi_device>
+
+   Version 03.59 (20160528)
+
+6 byte INQUIRY command:
+[0x12][   |lu][pg cde][res   ][al len][cntrl ]
+
+6 byte TEST UNIT READY command:
+[0x00][   |lu][res   ][res   ][res   ][res   ]
+
+*/
+
+#define INQ_REPLY_LEN 96        /* logic assumes >= sizeof(inq_cdb) */
+#define INQ_CMD_LEN 6
+#define TUR_CMD_LEN 6
+
+#define EBUFF_SZ 256
+
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k;
+    unsigned char inq_cdb[INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    unsigned char tur_cdb[TUR_CMD_LEN] =
+                                {0x00, 0, 0, 0, 0, 0};
+    unsigned char inqBuff[INQ_REPLY_LEN];
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    unsigned char sense_buffer[32];
+    int do_extra = 0;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-x", argv[k], 2))
+            do_extra = 1;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_simple2 [-x] <sg_device>'\n");
+        return 1;
+    }
+
+    /* N.B. An access mode of O_RDWR is required for some SCSI commands */
+    if ((sg_fd = open(file_name, O_RDONLY)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_simple2: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    /* Just to be safe, check we have a new sg device by trying an ioctl */
+    if ((ioctl(sg_fd, SG_GET_VERSION_NUM, &k) < 0) || (k < 30000)) {
+        printf("sg_simple2: %s doesn't seem to be an new sg device\n",
+               file_name);
+        close(sg_fd);
+        return 1;
+    }
+
+    /* Prepare INQUIRY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(inq_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = INQ_REPLY_LEN;
+    io_hdr.dxferp = inqBuff;
+    io_hdr.cmdp = inq_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple2: Inquiry SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    if ((io_hdr.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
+        if (io_hdr.sb_len_wr > 0) {
+            printf("INQUIRY sense data: ");
+            for (k = 0; k < io_hdr.sb_len_wr; ++k) {
+                if ((k > 0) && (0 == (k % 10)))
+                    printf("\n  ");
+                printf("0x%02x ", sense_buffer[k]);
+            }
+            printf("\n");
+        }
+        if (io_hdr.masked_status)
+            printf("INQUIRY SCSI status=0x%x\n", io_hdr.status);
+        if (io_hdr.host_status)
+            printf("INQUIRY host_status=0x%x\n", io_hdr.host_status);
+        if (io_hdr.driver_status)
+            printf("INQUIRY driver_status=0x%x\n", io_hdr.driver_status);
+    }
+    else {  /* output result if it is available */
+        char * p = (char *)inqBuff;
+        int f = (int)*(p + 7);
+        printf("Some of the INQUIRY command's results:\n");
+        printf("    %.8s  %.16s  %.4s  ", p + 8, p + 16, p + 32);
+        printf("[wide=%d sync=%d cmdque=%d sftre=%d]\n",
+               !!(f & 0x20), !!(f & 0x10), !!(f & 2), !!(f & 1));
+    }
+    /* Extra info, not necessary to look at */
+    if (do_extra)
+        printf("INQUIRY duration=%u millisecs, resid=%d, msg_status=%d\n",
+               io_hdr.duration, io_hdr.resid, (int)io_hdr.msg_status);
+
+    /* Prepare TEST UNIT READY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(tur_cdb);
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_NONE;
+    io_hdr.cmdp = tur_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple2: Test Unit Ready SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    if ((io_hdr.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
+        if (io_hdr.sb_len_wr > 0) {
+            printf("TEST UNIT READY sense data: ");
+            for (k = 0; k < io_hdr.sb_len_wr; ++k) {
+                if ((k > 0) && (0 == (k % 10)))
+                    printf("\n  ");
+                printf("0x%02x ", sense_buffer[k]);
+            }
+            printf("\n");
+        }
+        else if (io_hdr.masked_status)
+            printf("TEST UNIT READY SCSI status=0x%x\n", io_hdr.status);
+        else if (io_hdr.host_status)
+            printf("TEST UNIT READY host_status=0x%x\n", io_hdr.host_status);
+        else if (io_hdr.driver_status)
+            printf("TEST UNIT READY driver_status=0x%x\n",
+                   io_hdr.driver_status);
+        else
+            printf("TEST UNIT READY unexpected error\n");
+        printf("Test Unit Ready failed so unit may _not_ be ready!\n");
+    }
+    else
+        printf("Test Unit Ready successful so unit is ready!\n");
+    /* Extra info, not necessary to look at */
+    if (do_extra)
+        printf("TEST UNIT READY duration=%u millisecs, resid=%d, "
+               "msg_status=%d\n", io_hdr.duration, io_hdr.resid,
+               (int)io_hdr.msg_status);
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_simple3.c b/examples/sg_simple3.c
new file mode 100644
index 0000000..4927a92
--- /dev/null
+++ b/examples/sg_simple3.c
@@ -0,0 +1,205 @@
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+/* This is a simple program executing a SCSI INQUIRY command and a
+   TEST UNIT READY command using the SCSI generic (sg) driver.
+   There is another variant of this program called "sg_simple1".
+   This variant demonstrates using the scatter gather facility in
+   the sg_io_hdr interface to break an INQUIRY response into its
+   component parts.
+
+*  Copyright (C) 1999-2016 D. Gilbert
+*  This program is free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2, or (at your option)
+*  any later version.
+
+   Invocation: sg_simple3 [-x] <sg_device>
+
+   Version 03.59 (20160528)
+
+6 byte INQUIRY command:
+[0x12][   |lu][pg cde][res   ][al len][cntrl ]
+
+6 byte TEST UNIT READY command:
+[0x00][   |lu][res   ][res   ][res   ][res   ]
+
+*/
+
+#define INQ_REPLY_BASE_LEN 8
+#define INQ_REPLY_VID_LEN 8
+#define INQ_REPLY_PID_LEN 16
+#define INQ_REPLY_PREV_LEN 4
+#define INQ_REPLY_IOVEC_COUNT 4
+#define INQ_CMD_LEN 6
+#define TUR_CMD_LEN 6
+
+#define EBUFF_SZ 256
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k, ok;
+    unsigned char inq_cdb[INQ_CMD_LEN] = {0x12, 0, 0, 0,
+                    INQ_REPLY_BASE_LEN + INQ_REPLY_VID_LEN +
+                    INQ_REPLY_PID_LEN + INQ_REPLY_PREV_LEN, 0};
+    unsigned char tur_cdb[TUR_CMD_LEN] = {0x00, 0, 0, 0, 0, 0};
+    sg_iovec_t iovec[INQ_REPLY_IOVEC_COUNT];
+    unsigned char inqBaseBuff[INQ_REPLY_BASE_LEN];
+    char inqVidBuff[INQ_REPLY_VID_LEN];
+    char inqPidBuff[INQ_REPLY_PID_LEN];
+    char inqPRevBuff[INQ_REPLY_PREV_LEN];
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    unsigned char sense_buffer[32];
+    int do_extra = 0;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-x", argv[k], 2))
+            do_extra = 1;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_simple3 [-x] <sg_device>'\n");
+        return 1;
+    }
+
+    /* N.B. An access mode of O_RDWR is required for some SCSI commands */
+    if ((sg_fd = open(file_name, O_RDONLY)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_simple3: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    /* Just to be safe, check we have a new sg device by trying an ioctl */
+    if ((ioctl(sg_fd, SG_GET_VERSION_NUM, &k) < 0) || (k < 30000)) {
+        printf("sg_simple3: %s doesn't seem to be an new sg device\n",
+               file_name);
+        close(sg_fd);
+        return 1;
+    }
+
+    /* Prepare INQUIRY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(inq_cdb);
+    io_hdr.iovec_count = INQ_REPLY_IOVEC_COUNT;
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = INQ_REPLY_BASE_LEN + INQ_REPLY_VID_LEN +
+                       INQ_REPLY_PID_LEN + INQ_REPLY_PREV_LEN;
+    iovec[0].iov_base = inqBaseBuff;
+    iovec[0].iov_len = INQ_REPLY_BASE_LEN;
+    iovec[1].iov_base = inqVidBuff;
+    iovec[1].iov_len = INQ_REPLY_VID_LEN;
+    iovec[2].iov_base = inqPidBuff;
+    iovec[2].iov_len = INQ_REPLY_PID_LEN;
+    iovec[3].iov_base = inqPRevBuff;
+    iovec[3].iov_len = INQ_REPLY_PREV_LEN;
+    io_hdr.dxferp = iovec;
+    io_hdr.cmdp = inq_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* io_hdr.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple3: Inquiry SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on INQUIRY, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("INQUIRY command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok) { /* output result if it is available */
+        char * p = (char *)inqBaseBuff;
+        int f = (int)*(p + 7);
+        printf("Some of the INQUIRY command's results:\n");
+        printf("  %.8s  %.16s  %.4s  ", inqVidBuff, inqPidBuff, inqPRevBuff);
+        printf("[wide=%d sync=%d cmdque=%d sftre=%d]\n",
+               !!(f & 0x20), !!(f & 0x10), !!(f & 2), !!(f & 1));
+        /* Extra info, not necessary to look at */
+        if (do_extra)
+            printf("INQUIRY duration=%u millisecs, resid=%d, msg_status=%d\n",
+                   io_hdr.duration, io_hdr.resid, (int)io_hdr.msg_status);
+    }
+
+
+    /* Prepare TEST UNIT READY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(tur_cdb);
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_NONE;
+    io_hdr.cmdp = tur_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple3: Test Unit Ready SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on Test Unit Ready, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("Test Unit Ready command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok)
+        printf("Test Unit Ready successful so unit is ready!\n");
+    else
+        printf("Test Unit Ready failed so unit may _not_ be ready!\n");
+
+    if (do_extra)
+        printf("TEST UNIT READY duration=%u millisecs, resid=%d, "
+                "msg_status=%d\n", io_hdr.duration, io_hdr.resid,
+                (int)io_hdr.msg_status);
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_simple4.c b/examples/sg_simple4.c
new file mode 100644
index 0000000..95a5023
--- /dev/null
+++ b/examples/sg_simple4.c
@@ -0,0 +1,238 @@
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+
+/* This is a simple program executing a SCSI INQUIRY command and a
+   TEST UNIT READY command using the SCSI generic (sg) driver
+   This variant shows mmap-ed IO being used to read the data returned
+   by the INQUIRY command.
+
+*  Copyright (C) 2001-2016 D. Gilbert
+*  This program is free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2, or (at your option)
+*  any later version.
+
+   Invocation: sg_simple4 [-x] <sg_device>
+
+   Version 1.02 (20160528)
+
+6 byte INQUIRY command:
+[0x12][   |lu][pg cde][res   ][al len][cntrl ]
+
+6 byte TEST UNIT READY command:
+[0x00][   |lu][res   ][res   ][res   ][res   ]
+
+*/
+
+#ifndef SG_FLAG_MMAP_IO
+#define SG_FLAG_MMAP_IO 4
+#endif  /* since /usr/include/scsi/sg.h doesn't know about this yet */
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+#define TUR_CMD_LEN 6
+
+#define EBUFF_SZ 256
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k, ok;
+    unsigned char inq_cdb[INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    unsigned char tur_cdb[TUR_CMD_LEN] =
+                                {0x00, 0, 0, 0, 0, 0};
+    unsigned char * inqBuff;
+    unsigned char * inqBuff2;
+    sg_io_hdr_t io_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    unsigned char sense_buffer[32];
+    int do_extra = 0;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-x", argv[k], 2))
+            do_extra = 1;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_simple4 [-x] <sg_device>'\n");
+        return 1;
+    }
+
+    /* N.B. An access mode of O_RDWR is required for some SCSI commands */
+    if ((sg_fd = open(file_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_simple4: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    /* Just to be safe, check we have a new sg device by trying an ioctl */
+    if ((ioctl(sg_fd, SG_GET_VERSION_NUM, &k) < 0) || (k < 30122)) {
+        printf("sg_simple4: %s needs sg driver version >= 3.1.22\n",
+               file_name);
+        close(sg_fd);
+        return 1;
+    }
+
+    /* since I know this program will only read from inqBuff then I use
+       PROT_READ rather than PROT_READ | PROT_WRITE */
+    inqBuff = (unsigned char *)mmap(NULL, 8000, PROT_READ | PROT_WRITE,
+                                    MAP_SHARED, sg_fd, 0);
+    if (MAP_FAILED == inqBuff) {
+        snprintf(ebuff, EBUFF_SZ, "sg_simple4: error using mmap() on "
+                 "file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    if (inqBuff[0])
+        printf("non-null char at inqBuff[0]\n");
+    if (inqBuff[5000])
+        printf("non-null char at inqBuff[5000]\n");
+
+    /* Prepare INQUIRY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(inq_cdb);
+    /* io_hdr.iovec_count = 0; */  /* memset takes care of this */
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = INQ_REPLY_LEN;
+    /* io_hdr.dxferp = inqBuff; // ignored in mmap-ed IO */
+    io_hdr.cmdp = inq_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    io_hdr.flags = SG_FLAG_MMAP_IO;
+    /* io_hdr.pack_id = 0; */
+    /* io_hdr.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple4: Inquiry SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on INQUIRY, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("INQUIRY command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok) { /* output result if it is available */
+        char * p = (char *)inqBuff;
+        int f = (int)*(p + 7);
+        printf("Some of the INQUIRY command's results:\n");
+        printf("    %.8s  %.16s  %.4s  ", p + 8, p + 16, p + 32);
+        printf("[wide=%d sync=%d cmdque=%d sftre=%d]\n",
+               !!(f & 0x20), !!(f & 0x10), !!(f & 2), !!(f & 1));
+        /* Extra info, not necessary to look at */
+        if (do_extra)
+            printf("INQUIRY duration=%u millisecs, resid=%d, msg_status=%d\n",
+                   io_hdr.duration, io_hdr.resid, (int)io_hdr.msg_status);
+    }
+
+
+    /* Prepare TEST UNIT READY command */
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(tur_cdb);
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_NONE;
+    io_hdr.cmdp = tur_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("sg_simple4: Test Unit Ready SG_IO ioctl error");
+        close(sg_fd);
+        return 1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        printf("Recovered error on Test Unit Ready, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("Test Unit Ready command error", &io_hdr, 1);
+        break;
+    }
+
+    if (ok)
+        printf("Test Unit Ready successful so unit is ready!\n");
+    else
+        printf("Test Unit Ready failed so unit may _not_ be ready!\n");
+
+    if (do_extra)
+        printf("TEST UNIT READY duration=%u millisecs, resid=%d, "
+               "msg_status=%d\n",
+               io_hdr.duration, io_hdr.resid, (int)io_hdr.msg_status);
+
+    /* munmap(inqBuff, 8000); */
+    /* could call munmap(inqBuff, INQ_REPLY_LEN) here but following close()
+       causes this too happen anyway */
+#if 1
+    inqBuff2 = (unsigned char *)mmap(NULL, 8000, PROT_READ | PROT_WRITE,
+                                     MAP_SHARED, sg_fd, 0);
+    if (MAP_FAILED == inqBuff2) {
+        snprintf(ebuff, EBUFF_SZ, "sg_simple4: error using mmap() 2 on "
+                 "file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    if (inqBuff2[0])
+        printf("non-null char at inqBuff2[0]\n");
+    if (inqBuff2[5000])
+        printf("non-null char at inqBuff2[5000]\n");
+    {
+        pid_t pid;
+        pid = fork();
+        if (pid) {
+            inqBuff2[5000] = 33;
+            munmap(inqBuff, 8000);
+            sleep(3);
+        }
+        else {
+            inqBuff[5000] = 0xaa;
+            munmap(inqBuff, 8000);
+            sleep(1);
+        }
+    }
+#endif
+    close(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_simple5.c b/examples/sg_simple5.c
new file mode 100644
index 0000000..47bf7a0
--- /dev/null
+++ b/examples/sg_simple5.c
@@ -0,0 +1,236 @@
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+
+/* This is a simple program executing a SCSI INQUIRY command and a
+   TEST UNIT READY command using the SCSI generic pass through
+   interface. This allows this example program to be ported to
+   OSes other than linux.
+
+*  Copyright (C) 2006-20018 D. Gilbert
+*  This program is free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2, or (at your option)
+*  any later version.
+
+   Invocation: sg_simple5 [-x] <scsi_device>
+
+   Version 1.03 (20180220)
+
+*/
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+#define TUR_CMD_LEN 6
+
+#define CMD_TIMEOUT_SECS 60
+
+
+int main(int argc, char * argv[])
+{
+    int sg_fd, k, ok, dsize, res, duration, resid, cat, got, slen;
+    uint8_t inq_cdb [INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    uint8_t tur_cdb [TUR_CMD_LEN] =
+                                {0x00, 0, 0, 0, 0, 0};
+    uint8_t inqBuff[INQ_REPLY_LEN];
+    char * file_name = 0;
+    char b[512];
+    uint8_t sense_b[32];
+    int verbose = 0;
+    struct sg_pt_base * ptvp;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == strcmp("-v", argv[k]))
+            verbose = 1;
+        else if (0 == strcmp("-vv", argv[k]))
+            verbose = 2;
+        else if (0 == strcmp("-vvv", argv[k]))
+            verbose = 3;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_simple5 [-v|-vv|-vvv] <device>'\n");
+        return 1;
+    }
+
+    sg_fd = scsi_pt_open_device(file_name, 1 /* ro */, 0);
+    /* N.B. An access mode of O_RDWR is required for some SCSI commands */
+    if (sg_fd < 0) {
+        fprintf(stderr, "error opening file: %s: %s\n",
+                file_name, safe_strerror(-sg_fd));
+        return 1;
+    }
+
+    dsize = sizeof(inqBuff);
+    ok = 0;
+
+    ptvp = construct_scsi_pt_obj();     /* one object per command */
+    if (NULL == ptvp) {
+        fprintf(stderr, "sg_simple5: out of memory\n");
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, inq_cdb, sizeof(inq_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, inqBuff, dsize);
+    res = do_scsi_pt(ptvp, sg_fd, CMD_TIMEOUT_SECS, verbose);
+    if (res < 0) {
+        fprintf(stderr, "  pass through os error: %s\n",
+                safe_strerror(-res));
+        goto finish_inq;
+    } else if (SCSI_PT_DO_BAD_PARAMS == res) {
+        fprintf(stderr, "  bad pass through setup\n");
+        goto finish_inq;
+    } else if (SCSI_PT_DO_TIMEOUT == res) {
+        fprintf(stderr, "  pass through timeout\n");
+        goto finish_inq;
+    }
+    if ((verbose > 1) && ((duration = get_scsi_pt_duration_ms(ptvp)) >= 0))
+        fprintf(stderr, "      duration=%d ms\n", duration);
+    resid = get_scsi_pt_resid(ptvp);
+    switch ((cat = get_scsi_pt_result_category(ptvp))) {
+    case SCSI_PT_RESULT_GOOD:
+        got = dsize - resid;
+        if (verbose && (resid > 0))
+            fprintf(stderr, "    requested %d bytes but "
+                    "got %d bytes)\n", dsize, got);
+        break;
+    case SCSI_PT_RESULT_STATUS: /* other than GOOD and CHECK CONDITION */
+        if (verbose) {
+            sg_get_scsi_status_str(get_scsi_pt_status_response(ptvp),
+                                   sizeof(b), b);
+            fprintf(stderr, "  scsi status: %s\n", b);
+        }
+        goto finish_inq;
+    case SCSI_PT_RESULT_SENSE:
+        slen = get_scsi_pt_sense_len(ptvp);
+        if (verbose) {
+            sg_get_sense_str("", sense_b, slen, (verbose > 1),
+                             sizeof(b), b);
+            fprintf(stderr, "%s", b);
+        }
+        if (verbose && (resid > 0)) {
+            got = dsize - resid;
+            if ((verbose) || (got > 0))
+                fprintf(stderr, "    requested %d bytes but "
+                        "got %d bytes\n", dsize, got);
+        }
+        goto finish_inq;
+    case SCSI_PT_RESULT_TRANSPORT_ERR:
+        if (verbose) {
+            get_scsi_pt_transport_err_str(ptvp, sizeof(b), b);
+            fprintf(stderr, "  transport: %s", b);
+        }
+        goto finish_inq;
+    case SCSI_PT_RESULT_OS_ERR:
+        if (verbose) {
+            get_scsi_pt_os_err_str(ptvp, sizeof(b), b);
+            fprintf(stderr, "  os: %s", b);
+        }
+        goto finish_inq;
+    default:
+        fprintf(stderr, "  unknown pass through result "
+                "category (%d)\n", cat);
+        goto finish_inq;
+    }
+
+    ok = 1;
+finish_inq:
+    destruct_scsi_pt_obj(ptvp);
+
+    if (ok) { /* output result if it is available */
+        char * p = (char *)inqBuff;
+
+        printf("Some of the INQUIRY command's results:\n");
+        printf("    %.8s  %.16s  %.4s\n", p + 8, p + 16, p + 32);
+    }
+    ok = 0;
+
+
+    /* Now prepare TEST UNIT READY command */
+    ptvp = construct_scsi_pt_obj();     /* one object per command */
+    if (NULL == ptvp) {
+        fprintf(stderr, "sg_simple5: out of memory\n");
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, tur_cdb, sizeof(tur_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    /* no data in or out */
+    res = do_scsi_pt(ptvp, sg_fd, CMD_TIMEOUT_SECS, verbose);
+    if (res < 0) {
+        fprintf(stderr, "  pass through os error: %s\n",
+                safe_strerror(-res));
+        goto finish_inq;
+    } else if (SCSI_PT_DO_BAD_PARAMS == res) {
+        fprintf(stderr, "  bad pass through setup\n");
+        goto finish_inq;
+    } else if (SCSI_PT_DO_TIMEOUT == res) {
+        fprintf(stderr, "  pass through timeout\n");
+        goto finish_inq;
+    }
+    if ((verbose > 1) && ((duration = get_scsi_pt_duration_ms(ptvp)) >= 0))
+        fprintf(stderr, "      duration=%d ms\n", duration);
+    resid = get_scsi_pt_resid(ptvp);
+    switch ((cat = get_scsi_pt_result_category(ptvp))) {
+    case SCSI_PT_RESULT_GOOD:
+        break;
+    case SCSI_PT_RESULT_STATUS: /* other than GOOD and CHECK CONDITION */
+        if (verbose) {
+            sg_get_scsi_status_str(get_scsi_pt_status_response(ptvp),
+                                   sizeof(b), b);
+            fprintf(stderr, "  scsi status: %s\n", b);
+        }
+        goto finish_tur;
+    case SCSI_PT_RESULT_SENSE:
+        slen = get_scsi_pt_sense_len(ptvp);
+        if (verbose) {
+            sg_get_sense_str("", sense_b, slen, (verbose > 1),
+                             sizeof(b), b);
+            fprintf(stderr, "%s", b);
+        }
+        goto finish_tur;
+    case SCSI_PT_RESULT_TRANSPORT_ERR:
+        if (verbose) {
+            get_scsi_pt_transport_err_str(ptvp, sizeof(b), b);
+            fprintf(stderr, "  transport: %s", b);
+        }
+        goto finish_tur;
+    case SCSI_PT_RESULT_OS_ERR:
+        if (verbose) {
+            get_scsi_pt_os_err_str(ptvp, sizeof(b), b);
+            fprintf(stderr, "  os: %s", b);
+        }
+        goto finish_tur;
+    default:
+        fprintf(stderr, "  unknown pass through result "
+                "category (%d)\n", cat);
+        goto finish_tur;
+    }
+
+    ok = 1;
+finish_tur:
+    destruct_scsi_pt_obj(ptvp);
+
+    if (ok)
+        printf("Test Unit Ready successful so unit is ready!\n");
+    else
+        printf("Test Unit Ready failed so unit may _not_ be ready!\n");
+
+    scsi_pt_close_device(sg_fd);
+    return 0;
+}
diff --git a/examples/sg_unmap_example.txt b/examples/sg_unmap_example.txt
new file mode 100644
index 0000000..2b07dba
--- /dev/null
+++ b/examples/sg_unmap_example.txt
@@ -0,0 +1,40 @@
+#                sg_unmap_example.txt
+# This is an example of the contents of a file that can be given to sg_unmap
+# For example, assume the /dev/sdc is a scratch disk (e.g. one made by the
+# scsi_debug kernel module) then:
+#    sg_unmap --in=sg_unmap_example.txt /dev/sdc
+
+0x12345677,1	# unmap LBA 0x12345677
+0x12345678 2	# unmap LBA 0x12345678 and 0x12345679
+0x12340000 3333	# unmap 3333 blocks starting at LBA 0x12340000
+
+0X5a5a5a5a5a	0  # unmaps 0 blocks (i.e. does nothing)
+
+    a5a5a5h
+7		# unmap 7 blocks starting at LBA 0xa5a5a5
+
+# Note that there can be leading and trailing whitespace and whitespace
+# (plus comma) can be a separator.
+#
+# Example invocation:
+# $ sg_unmap --in=../examples/sg_unmap_example.txt -vv /dev/sdc
+# open /dev/sg2 with flags=0x802
+#     unmap cdb: 42 00 00 00 00 00 00 00 58 00
+#     unmap parameter list:
+#         00 56 00 50 00 00 00 00  00 00 00 00 12 34 56 77
+#         00 00 00 01 00 00 00 00  00 00 00 00 12 34 56 78
+#         00 00 00 02 00 00 00 00  00 00 00 00 12 34 00 00
+#         00 00 0d 05 00 00 00 00  00 00 00 5a 5a 5a 5a 5a
+#         00 00 00 00 00 00 00 00  00 00 00 00 00 a5 a5 a5
+#         00 00 00 07 00 00 00 00
+# sg_cmds_process_resp: slen=18
+# unmap:  Fixed format, current;  Sense key: Illegal Request
+#  Additional sense: Invalid command operation code
+#  Raw sense data (in hex):
+#         70 00 05 00 00 00 00 0a  00 00 00 00 20 00 00 00
+#         00 00
+# UNMAP not supported
+#
+# --------------------------------------------------------
+# Notice the 8 byte header then 5 descriptors in the parameter
+# list
diff --git a/examples/sgq_dd.c b/examples/sgq_dd.c
new file mode 100644
index 0000000..80e8817
--- /dev/null
+++ b/examples/sgq_dd.c
@@ -0,0 +1,1230 @@
+/*
+ * A utility program for the Linux OS SCSI generic ("sg") device driver.
+ * Copyright (C) 1999-2010 D. Gilbert and P. Allworth
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is a specialization of the Unix "dd" command in which
+ * one or both of the given files is a scsi generic device or a raw
+ * device. A block size ('bs') is assumed to be 512 if not given. This
+ * program complains if 'ibs' or 'obs' are given with some other value
+ * than 'bs'. If 'if' is not given or 'if=-' then stdin is assumed. If
+ * 'of' is not given or 'of=-' then stdout assumed.  Multipliers:
+ *    'c','C'  *1       'b','B' *512      'k' *1024      'K' *1000
+ *    'm' *(1024^2)     'M' *(1000^2)     'g' *(1024^3)  'G' *(1000^3)
+ *
+ * A non-standard argument "bpt" (blocks per transfer) is added to control
+ * the maximum number of blocks in each transfer. The default value is 128.
+ * For example if "bs=512" and "bpt=32" then a maximum of 32 blocks (16KB
+ * in this case) are transferred to or from the sg device in a single SCSI
+ * command.
+ *
+ * This version should compile with Linux sg drivers with version numbers
+ * >= 30000 . This version uses queuing within the Linux sg driver.
+ */
+
+#define _XOPEN_SOURCE 500
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <signal.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <linux/major.h>
+#include <sys/time.h>
+typedef uint8_t u_char;   /* horrible, for scsi.h */
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+
+
+static char * version_str = "0.63 20190324";
+/* resurrected from "0.55 20020509" */
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BLOCKS_PER_TRANSFER 128
+
+
+#define SENSE_BUFF_LEN 32       /* Arbitrary, could be larger */
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs == 60 seconds */
+#define S_RW_LEN 10             /* Use SCSI READ(10) and WRITE(10) */
+
+#define SGP_READ10 0x28
+#define SGP_WRITE10 0x2a
+#define DEF_NUM_THREADS 4       /* actually degree of concurrency */
+#define MAX_NUM_THREADS 1024
+
+#ifndef RAW_MAJOR
+#define RAW_MAJOR 255   /*unlikely value */
+#endif
+
+#define FT_OTHER 0              /* filetype other than sg or raw device */
+#define FT_SG 1                 /* filetype is sg char device */
+#define FT_RAW 2                /* filetype is raw char device */
+
+#define QS_IDLE 0               /* ready to start a copy cycle */
+#define QS_IN_STARTED 1         /* commenced read */
+#define QS_IN_FINISHED 2        /* finished read, ready for write */
+#define QS_OUT_STARTED 3        /* commenced write */
+
+#define QS_IN_POLL 11
+#define QS_OUT_POLL 12
+
+#define STR_SZ 1024
+#define INOUTF_SZ 512
+#define EBUFF_SZ 512
+
+
+struct request_element;
+
+typedef struct request_collection
+{       /* one instance visible to all threads */
+    int infd;
+    int skip;
+    int in_type;
+    int in_scsi_type;
+    int in_blk;                 /* next block address to read */
+    int in_count;               /* blocks remaining for next read */
+    int in_done_count;          /* count of completed in blocks */
+    int in_partial;
+    int outfd;
+    int seek;
+    int out_type;
+    int out_scsi_type;
+    int out_blk;                /* next block address to write */
+    int out_count;              /* blocks remaining for next write */
+    int out_done_count;         /* count of completed out blocks */
+    int out_partial;
+    int bs;
+    int bpt;
+    int dio;
+    int dio_incomplete;
+    int sum_of_resids;
+    int coe;
+    int debug;
+    int num_rq_elems;
+    struct request_element * req_arr;
+} Rq_coll;
+
+typedef struct request_element
+{       /* one instance per worker thread */
+    int qstate;                 /* "QS" state */
+    int infd;
+    int outfd;
+    int wr;
+    int blk;
+    int num_blks;
+    uint8_t * buffp;
+    uint8_t * alloc_bp;
+    sg_io_hdr_t io_hdr;
+    uint8_t cmd[S_RW_LEN];
+    uint8_t sb[SENSE_BUFF_LEN];
+    int bs;
+    int dio;
+    int dio_incomplete;
+    int resid;
+    int in_scsi_type;
+    int out_scsi_type;
+    int debug;
+} Rq_elem;
+
+static Rq_coll rcoll;
+static struct pollfd in_pollfd_arr[MAX_NUM_THREADS];
+static struct pollfd out_pollfd_arr[MAX_NUM_THREADS];
+static int dd_count = -1;
+
+static const char * sg_allow_dio = "/sys/module/sg/parameters/allow_dio";
+
+static int sg_finish_io(int wr, Rq_elem * rep);
+
+
+/* Returns the number of times 'ch' is found in string 's' given the
+ * string's length. */
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+static void
+install_handler (int sig_num, void (*sig_handler) (int sig))
+{
+    struct sigaction sigact;
+    sigaction (sig_num, NULL, &sigact);
+    if (sigact.sa_handler != SIG_IGN)
+    {
+        sigact.sa_handler = sig_handler;
+        sigemptyset (&sigact.sa_mask);
+        sigact.sa_flags = 0;
+        sigaction (sig_num, &sigact, NULL);
+    }
+}
+
+static void
+print_stats()
+{
+    int infull, outfull;
+
+    if (0 != rcoll.out_count)
+        fprintf(stderr, "  remaining block count=%d\n", rcoll.out_count);
+    infull = dd_count - rcoll.in_done_count - rcoll.in_partial;
+    fprintf(stderr, "%d+%d records in\n", infull, rcoll.in_partial);
+    outfull = dd_count - rcoll.out_done_count - rcoll.out_partial;
+    fprintf(stderr, "%d+%d records out\n", outfull, rcoll.out_partial);
+}
+
+static void
+interrupt_handler(int sig)
+{
+    struct sigaction sigact;
+
+    sigact.sa_handler = SIG_DFL;
+    sigemptyset (&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigaction (sig, &sigact, NULL);
+    fprintf(stderr, "Interrupted by signal,");
+    print_stats ();
+    kill (getpid (), sig);
+}
+
+static void
+siginfo_handler(int sig)
+{
+    fprintf(stderr, "Progress report, continuing ...\n");
+    print_stats ();
+    if (sig) { }        /* suppress unused warning */
+}
+
+static int
+dd_filetype(const char * filename)
+{
+    struct stat st;
+
+    if (stat(filename, &st) < 0)
+        return FT_OTHER;
+    if (S_ISCHR(st.st_mode)) {
+        if (RAW_MAJOR == major(st.st_rdev))
+            return FT_RAW;
+        else if (SCSI_GENERIC_MAJOR == major(st.st_rdev))
+            return FT_SG;
+    }
+    return FT_OTHER;
+}
+
+static void
+usage()
+{
+    fprintf(stderr, "Usage: "
+           "sgq_dd  [if=<infile>] [skip=<n>] [of=<ofile>] [seek=<n>] "
+           "[bs=<num>]\n"
+           "            [bpt=<num>] [count=<n>] [dio=0|1] [thr=<n>] "
+           "[coe=0|1] [gen=<n>]\n"
+           "            [time=0|1] [deb=<n>] [--version]\n"
+           "         usually either 'if' or 'of' is a sg or raw device\n"
+           " 'bpt' is blocks_per_transfer (default is 128)\n"
+           " 'dio' is direct IO, 1->attempt, 0->indirect IO (def)\n"
+           " 'thr' is number of queues, must be > 0, default 4, max 1024\n");
+    fprintf(stderr, " 'coe' continue on sg error, 0->exit (def), "
+           "1->zero + continue\n"
+           " 'time' 0->no timing(def), 1->time plus calculate throughput\n"
+           " 'gen' 0-> 1 file is special(def), 1-> any files allowed\n"
+           " 'deb' is debug, 0->none (def), > 0->varying degrees of debug\n");
+}
+
+/* Returns -1 for error, 0 for nothing found, QS_IN_POLL or QS_OUT_POLL */
+static int
+do_poll(Rq_coll * clp, int timeout, int * req_indexp)
+{
+    int k, res;
+
+    if (FT_SG == clp->out_type) {
+        while (((res = poll(out_pollfd_arr, clp->num_rq_elems, timeout)) < 0)
+               && (EINTR == errno))
+            ;
+        if (res < 0) {
+            perror("poll error on output fds");
+            return -1;
+        }
+        else if (res > 0) {
+            for (k = 0; k < clp->num_rq_elems; ++k) {
+                if (out_pollfd_arr[k].revents & POLLIN) {
+                    if (req_indexp)
+                        *req_indexp = k;
+                    return QS_OUT_POLL;
+                }
+            }
+        }
+    }
+    if (FT_SG == clp->in_type) {
+        while (((res = poll(in_pollfd_arr, clp->num_rq_elems, timeout)) < 0)
+               && (EINTR == errno))
+            ;
+        if (res < 0) {
+            perror("poll error on input fds");
+            return -1;
+        }
+        else if (res > 0) {
+            for (k = 0; k < clp->num_rq_elems; ++k) {
+                if (in_pollfd_arr[k].revents & POLLIN) {
+                    if (req_indexp)
+                        *req_indexp = k;
+                    return QS_IN_POLL;
+                }
+            }
+        }
+    }
+    return 0;
+}
+
+
+/* Return of 0 -> success, -1 -> failure, 2 -> try again */
+static int
+read_capacity(int sg_fd, int * num_sect, int * sect_sz)
+{
+    int res;
+    uint8_t rc_cdb [10] = {0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t rcBuff[64];
+    uint8_t sense_b[64];
+    sg_io_hdr_t io_hdr;
+
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(rc_cdb);
+    io_hdr.mx_sb_len = sizeof(sense_b);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = sizeof(rcBuff);
+    io_hdr.dxferp = rcBuff;
+    io_hdr.cmdp = rc_cdb;
+    io_hdr.sbp = sense_b;
+    io_hdr.timeout = DEF_TIMEOUT;
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("read_capacity (SG_IO) error");
+        return -1;
+    }
+    res = sg_err_category3(&io_hdr);
+    if (SG_LIB_CAT_UNIT_ATTENTION == res)
+        return 2; /* probably have another go ... */
+    else if (SG_LIB_CAT_CLEAN != res) {
+        sg_chk_n_print3("read capacity", &io_hdr, 1);
+        return -1;
+    }
+    *num_sect = 1 + sg_get_unaligned_be32(rcBuff + 0);
+    *sect_sz = sg_get_unaligned_be32(rcBuff + 4);
+#ifdef DEBUG
+    fprintf(stderr, "number of sectors=%d, sector size=%d\n",
+            *num_sect, *sect_sz);
+#endif
+    return 0;
+}
+
+/* 0 -> ok, 1 -> short read, -1 -> error */
+static int
+normal_in_operation(Rq_coll * clp, Rq_elem * rep, int blocks)
+{
+    int res;
+    int stop_after_write = 0;
+
+    rep->qstate = QS_IN_STARTED;
+    if (rep->debug > 8)
+        fprintf(stderr, "normal_in_operation: start blk=%d num_blks=%d\n",
+                rep->blk, rep->num_blks);
+    while (((res = read(rep->infd, rep->buffp,
+                        blocks * rep->bs)) < 0) && (EINTR == errno))
+        ;
+    if (res < 0) {
+        fprintf(stderr, "sgq_dd: reading, in_blk=%d, errno=%d\n", rep->blk,
+                errno);
+        return -1;
+    }
+    if (res < blocks * rep->bs) {
+        int o_blocks = blocks;
+        stop_after_write = 1;
+        blocks = res / rep->bs;
+        if ((res % rep->bs) > 0) {
+            blocks++;
+            clp->in_partial++;
+        }
+        /* Reverse out + re-apply blocks on clp */
+        clp->in_blk -= o_blocks;
+        clp->in_count += o_blocks;
+        rep->num_blks = blocks;
+        clp->in_blk += blocks;
+        clp->in_count -= blocks;
+    }
+    clp->in_done_count -= blocks;
+    rep->qstate = QS_IN_FINISHED;
+    return stop_after_write;
+}
+
+/* 0 -> ok, -1 -> error */
+static int
+normal_out_operation(Rq_coll * clp, Rq_elem * rep, int blocks)
+{
+    int res;
+
+    rep->qstate = QS_OUT_STARTED;
+    if (rep->debug > 8)
+        fprintf(stderr, "normal_out_operation: start blk=%d num_blks=%d\n",
+                rep->blk, rep->num_blks);
+    while (((res = write(rep->outfd, rep->buffp,
+                 rep->num_blks * rep->bs)) < 0) && (EINTR == errno))
+        ;
+    if (res < 0) {
+        fprintf(stderr, "sgq_dd: output, out_blk=%d, errno=%d\n", rep->blk,
+                errno);
+        return -1;
+    }
+    if (res < blocks * rep->bs) {
+        blocks = res / rep->bs;
+        if ((res % rep->bs) > 0) {
+            blocks++;
+            clp->out_partial++;
+        }
+        rep->num_blks = blocks;
+    }
+    clp->out_done_count -= blocks;
+    rep->qstate = QS_IDLE;
+    return 0;
+}
+
+/* Returns 1 for retryable, 0 for ok, -ve for error */
+static int
+sg_fin_in_operation(Rq_coll * clp, Rq_elem * rep)
+{
+    int res;
+
+    rep->qstate = QS_IN_FINISHED;
+    res = sg_finish_io(rep->wr, rep);
+    if (res < 0) {
+        if (clp->coe) {
+            memset(rep->buffp, 0, rep->num_blks * rep->bs);
+            fprintf(stderr, ">> substituted zeros for in blk=%d for "
+                    "%d bytes\n", rep->blk, rep->num_blks * rep->bs);
+            res = 0;
+        }
+        else {
+            fprintf(stderr, "error finishing sg in command\n");
+            return res;
+        }
+    }
+    if (0 == res) { /* looks good, going to return */
+        if (rep->dio_incomplete || rep->resid) {
+            clp->dio_incomplete += rep->dio_incomplete;
+            clp->sum_of_resids += rep->resid;
+        }
+        clp->in_done_count -= rep->num_blks;
+    }
+    return res;
+}
+
+/* Returns 1 for retryable, 0 for ok, -ve for error */
+static int
+sg_fin_out_operation(Rq_coll * clp, Rq_elem * rep)
+{
+    int res;
+
+    rep->qstate = QS_IDLE;
+    res = sg_finish_io(rep->wr, rep);
+    if (res < 0) {
+        if (clp->coe) {
+            fprintf(stderr, ">> ignored error for out blk=%d for "
+                    "%d bytes\n", rep->blk, rep->num_blks * rep->bs);
+            res = 0;
+        }
+        else {
+            fprintf(stderr, "error finishing sg out command\n");
+            return res;
+        }
+    }
+    if (0 == res) {
+        if (rep->dio_incomplete || rep->resid) {
+            clp->dio_incomplete += rep->dio_incomplete;
+            clp->sum_of_resids += rep->resid;
+        }
+        clp->out_done_count -= rep->num_blks;
+    }
+    return res;
+}
+
+static int
+sg_start_io(Rq_elem * rep)
+{
+    sg_io_hdr_t * hp = &rep->io_hdr;
+    int res;
+
+    rep->qstate = rep->wr ? QS_OUT_STARTED : QS_IN_STARTED;
+    memset(rep->cmd, 0, sizeof(rep->cmd));
+    rep->cmd[0] = rep->wr ? SGP_WRITE10 : SGP_READ10;
+    sg_put_unaligned_be32((uint32_t)rep->blk, rep->cmd + 2);
+    sg_put_unaligned_be16((uint16_t)rep->num_blks, rep->cmd + 7);
+    memset(hp, 0, sizeof(sg_io_hdr_t));
+    hp->interface_id = 'S';
+    hp->cmd_len = sizeof(rep->cmd);
+    hp->cmdp = rep->cmd;
+    hp->dxfer_direction = rep->wr ? SG_DXFER_TO_DEV : SG_DXFER_FROM_DEV;
+    hp->dxfer_len = rep->bs * rep->num_blks;
+    hp->dxferp = rep->buffp;
+    hp->mx_sb_len = sizeof(rep->sb);
+    hp->sbp = rep->sb;
+    hp->timeout = DEF_TIMEOUT;
+    hp->usr_ptr = rep;
+    hp->pack_id = rep->blk;
+    if (rep->dio)
+        hp->flags |= SG_FLAG_DIRECT_IO;
+    if (rep->debug > 8) {
+        fprintf(stderr, "sg_start_io: SCSI %s, blk=%d num_blks=%d\n",
+               rep->wr ? "WRITE" : "READ", rep->blk, rep->num_blks);
+        sg_print_command(hp->cmdp);
+        fprintf(stderr, " len=%d, dxfrp=%p, cmd_len=%d\n",
+                hp->dxfer_len, hp->dxferp, hp->cmd_len);
+    }
+
+    while (((res = write(rep->wr ? rep->outfd : rep->infd, hp,
+                         sizeof(sg_io_hdr_t))) < 0) && (EINTR == errno))
+        ;
+    if (res < 0) {
+        if (ENOMEM == errno)
+            return 1;
+        return res;
+    }
+    return 0;
+}
+
+/* -1 -> unrecoverable error, 0 -> successful, 1 -> try again */
+static int
+sg_finish_io(int wr, Rq_elem * rep)
+{
+    int res;
+    sg_io_hdr_t io_hdr;
+    sg_io_hdr_t * hp;
+#if 0
+    static int testing = 0;     /* thread dubious! */
+#endif
+
+    memset(&io_hdr, 0 , sizeof(sg_io_hdr_t));
+    /* FORCE_PACK_ID active set only read packet with matching pack_id */
+    io_hdr.interface_id = 'S';
+    io_hdr.dxfer_direction = rep->wr ? SG_DXFER_TO_DEV : SG_DXFER_FROM_DEV;
+    io_hdr.pack_id = rep->blk;
+
+    while (((res = read(wr ? rep->outfd : rep->infd, &io_hdr,
+                        sizeof(sg_io_hdr_t))) < 0) && (EINTR == errno))
+        ;
+    if (res < 0) {
+        perror("finishing io on sg device, error");
+        return -1;
+    }
+    if (rep != (Rq_elem *)io_hdr.usr_ptr) {
+        fprintf(stderr,
+                "sg_finish_io: bad usr_ptr, request-response mismatch\n");
+        exit(1);
+    }
+    memcpy(&rep->io_hdr, &io_hdr, sizeof(sg_io_hdr_t));
+    hp = &rep->io_hdr;
+
+    switch (sg_err_category3(hp)) {
+        case SG_LIB_CAT_CLEAN:
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            fprintf(stderr, "Recovered error on block=%d, num=%d\n",
+                    rep->blk, rep->num_blks);
+            break;
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            return 1;
+        default:
+            {
+                char ebuff[EBUFF_SZ];
+                snprintf(ebuff, EBUFF_SZ, "%s blk=%d",
+                         rep->wr ? "writing": "reading", rep->blk);
+                sg_chk_n_print3(ebuff, hp, 1);
+                return -1;
+            }
+    }
+#if 0
+    if (0 == (++testing % 100)) return -1;
+#endif
+    if (rep->dio &&
+        ((hp->info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+        rep->dio_incomplete = 1; /* count dios done as indirect IO */
+    else
+        rep->dio_incomplete = 0;
+    rep->resid = hp->resid;
+    if (rep->debug > 8)
+        fprintf(stderr, "sg_finish_io: completed %s, blk=%d\n",
+                wr ? "WRITE" : "READ", rep->blk);
+    return 0;
+}
+
+/* Returns scsi_type or -1 for error */
+static int
+sg_prepare(int fd, int sz)
+{
+    int res, t;
+    struct sg_scsi_id info;
+
+    res = ioctl(fd, SG_GET_VERSION_NUM, &t);
+    if ((res < 0) || (t < 30000)) {
+        fprintf(stderr, "sgq_dd: sg driver prior to 3.x.y\n");
+        return -1;
+    }
+    res = ioctl(fd, SG_SET_RESERVED_SIZE, &sz);
+    if (res < 0)
+        perror("sgq_dd: SG_SET_RESERVED_SIZE error");
+#if 0
+    t = 1;
+    res = ioctl(fd, SG_SET_FORCE_PACK_ID, &t);
+    if (res < 0)
+        perror("sgq_dd: SG_SET_FORCE_PACK_ID error");
+#endif
+    res = ioctl(fd, SG_GET_SCSI_ID, &info);
+    if (res < 0) {
+        perror("sgq_dd: SG_SET_SCSI_ID error");
+        return -1;
+    }
+    else
+        return info.scsi_type;
+}
+
+/* Return 0 for ok, anything else for errors */
+static int
+prepare_rq_elems(Rq_coll * clp, const char * inf, const char * outf)
+{
+    int k;
+    Rq_elem * rep;
+    size_t psz;
+    char ebuff[EBUFF_SZ];
+    int sz = clp->bpt * clp->bs;
+    int scsi_type;
+
+    clp->req_arr = malloc(sizeof(Rq_elem) * clp->num_rq_elems);
+    if (NULL == clp->req_arr)
+        return 1;
+    for (k = 0; k < clp->num_rq_elems; ++k) {
+        rep = &clp->req_arr[k];
+        memset(rep, 0, sizeof(Rq_elem));
+        psz = getpagesize();
+        if (NULL == (rep->alloc_bp = malloc(sz + psz)))
+            return 1;
+        rep->buffp = (uint8_t *)
+                (((unsigned long)rep->alloc_bp + psz - 1) & (~(psz - 1)));
+        rep->qstate = QS_IDLE;
+        rep->bs = clp->bs;
+        rep->dio = clp->dio;
+        rep->debug = clp->debug;
+        rep->out_scsi_type = clp->out_scsi_type;
+        if (FT_SG == clp->in_type) {
+            if (0 == k)
+                rep->infd = clp->infd;
+            else {
+                if ((rep->infd = open(inf, O_RDWR)) < 0) {
+                    snprintf(ebuff, EBUFF_SZ,
+                             "sgq_dd: could not open %s for sg reading", inf);
+                    perror(ebuff);
+                    return 1;
+                }
+            }
+            in_pollfd_arr[k].fd = rep->infd;
+            in_pollfd_arr[k].events = POLLIN;
+            if ((scsi_type = sg_prepare(rep->infd, sz)) < 0)
+                return 1;
+            if (0 == k)
+                clp->in_scsi_type = scsi_type;
+            rep->in_scsi_type = clp->in_scsi_type;
+        }
+        else
+            rep->infd = clp->infd;
+
+        if (FT_SG == clp->out_type) {
+            if (0 == k)
+                rep->outfd = clp->outfd;
+            else {
+                if ((rep->outfd = open(outf, O_RDWR)) < 0) {
+                    snprintf(ebuff, EBUFF_SZ,
+                             "sgq_dd: could not open %s for sg writing", outf);
+                    perror(ebuff);
+                    return 1;
+                }
+            }
+            out_pollfd_arr[k].fd = rep->outfd;
+            out_pollfd_arr[k].events = POLLIN;
+            if ((scsi_type = sg_prepare(rep->outfd, sz)) < 0)
+                return 1;
+            if (0 == k)
+                clp->out_scsi_type = scsi_type;
+            rep->out_scsi_type = clp->out_scsi_type;
+        }
+        else
+            rep->outfd = clp->outfd;
+    }
+    return 0;
+}
+
+/* Returns a "QS" code and req index, or QS_IDLE and position of first idle
+   (-1 if no idle position). Returns -1 on poll error. */
+static int
+decider(Rq_coll * clp, int first_xfer, int * req_indexp)
+{
+    int k, res;
+    Rq_elem * rep;
+    int first_idle_index = -1;
+    int lowest_blk_index = -1;
+    int times;
+    int try_poll = 0;
+    int lowest_blk = INT_MAX;
+
+    times = first_xfer ? 1 : clp->num_rq_elems;
+    for (k = 0; k < times; ++k) {
+        rep = &clp->req_arr[k];
+        if ((QS_IN_STARTED == rep->qstate) ||
+            (QS_OUT_STARTED == rep->qstate))
+            try_poll = 1;
+        else if ((QS_IN_FINISHED == rep->qstate) && (rep->blk < lowest_blk)) {
+            lowest_blk = rep->blk;
+            lowest_blk_index = k;
+        }
+        else if ((QS_IDLE == rep->qstate) && (first_idle_index < 0))
+            first_idle_index = k;
+    }
+    if (try_poll) {
+        res = do_poll(clp, 0, req_indexp);
+        if (0 != res)
+            return res;
+    }
+
+    if (lowest_blk_index >= 0) {
+        if (req_indexp)
+            *req_indexp = lowest_blk_index;
+        return QS_IN_FINISHED;
+    }
+#if 0
+    if (try_poll) {
+        res = do_poll(clp, 2, req_indexp);
+        if (0 != res)
+            return res;
+    }
+#endif
+    if (req_indexp)
+        *req_indexp = first_idle_index;
+    return QS_IDLE;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool verbose_given = false;
+    bool version_given = false;
+    int skip = 0;
+    int seek = 0;
+    int ibs = 0;
+    int obs = 0;
+    char str[STR_SZ];
+    char * key;
+    char * buf;
+    char inf[INOUTF_SZ];
+    char outf[INOUTF_SZ];
+    int res, k, n, keylen;
+    int in_num_sect = 0;
+    int out_num_sect = 0;
+    int num_threads = DEF_NUM_THREADS;
+    int gen = 0;
+    int do_time = 0;
+    int in_sect_sz, out_sect_sz, first_xfer, qstate, req_index, seek_skip;
+    int blocks, stop_after_write, terminate;
+    char ebuff[EBUFF_SZ];
+    Rq_elem * rep;
+    struct timeval start_tm, end_tm;
+
+    memset(&rcoll, 0, sizeof(Rq_coll));
+    rcoll.bpt = DEF_BLOCKS_PER_TRANSFER;
+    rcoll.in_type = FT_OTHER;
+    rcoll.out_type = FT_OTHER;
+    inf[0] = '\0';
+    outf[0] = '\0';
+
+    for(k = 1; k < argc; k++) {
+        if (argv[k])
+            strncpy(str, argv[k], STR_SZ);
+        else
+            continue;
+        for(key = str, buf = key; *buf && *buf != '=';)
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        keylen = strlen(key);
+        if (strcmp(key,"if") == 0)
+            strncpy(inf, buf, INOUTF_SZ);
+        else if (strcmp(key,"of") == 0)
+            strncpy(outf, buf, INOUTF_SZ);
+        else if (0 == strcmp(key,"ibs"))
+            ibs = sg_get_num(buf);
+        else if (0 == strcmp(key,"obs"))
+            obs = sg_get_num(buf);
+        else if (0 == strcmp(key,"bs"))
+            rcoll.bs = sg_get_num(buf);
+        else if (0 == strcmp(key,"bpt"))
+            rcoll.bpt = sg_get_num(buf);
+        else if (0 == strcmp(key,"skip"))
+            skip = sg_get_num(buf);
+        else if (0 == strcmp(key,"seek"))
+            seek = sg_get_num(buf);
+        else if (0 == strcmp(key,"count"))
+            dd_count = sg_get_num(buf);
+        else if (0 == strcmp(key,"dio"))
+            rcoll.dio = sg_get_num(buf);
+        else if (0 == strcmp(key,"thr"))
+            num_threads = sg_get_num(buf);
+        else if (0 == strcmp(key,"coe"))
+            rcoll.coe = sg_get_num(buf);
+        else if (0 == strcmp(key,"gen"))
+            gen = sg_get_num(buf);
+        else if ((0 == strncmp(key,"deb", 3)) || (0 == strncmp(key,"verb", 4)))
+            rcoll.debug = sg_get_num(buf);
+        else if (0 == strcmp(key,"time"))
+            do_time = sg_get_num(buf);
+        else if ((keylen > 1) && ('-' == key[0]) && ('-' != key[1])) {
+            res = 0;
+            n = num_chs_in_str(key + 1, keylen - 1, 'h');
+            if (n > 0) {
+                usage();
+                return 0;
+            }
+            n = num_chs_in_str(key + 1, keylen - 1, 'v');
+            if (n > 0)
+                verbose_given = true;
+            rcoll.debug += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'V');
+            if (n > 0)
+                version_given = true;
+            res += n;
+            if (res < (keylen - 1)) {
+                fprintf(stderr, "Unrecognised short option in '%s', try "
+                        "'--help'\n", key);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strncmp(key, "--help", 6)) {
+            usage();
+            return 0;
+        } else if (0 == strncmp(key, "--verb", 6)) {
+            verbose_given = true;
+            ++rcoll.debug;
+        } else if (0 == strncmp(key, "--vers", 6))
+            version_given = true;
+        else {
+            fprintf(stderr, "Unrecognized argument '%s'\n", key);
+            usage();
+            return 1;
+        }
+    }
+#ifdef DEBUG
+    fprintf(stderr, "In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        fprintf(stderr, "but override: '-vV' given, zero verbose and "
+                "continue\n");
+        verbose_given = false;
+        version_given = false;
+        rcoll.debug = 0;
+    } else if (! verbose_given) {
+        fprintf(stderr, "set '-vv'\n");
+        rcoll.debug = 2;
+    } else
+        fprintf(stderr, "keep verbose=%d\n", rcoll.debug);
+#else
+    if (verbose_given && version_given)
+        fprintf(stderr, "Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        fprintf(stderr, "sgq_dd for sg version 3 driver: %s\n",
+                version_str);
+            return 0;
+        return 0;
+    }
+
+    if (argc < 2) {
+        usage();
+        return 1;
+    }
+    if (rcoll.bs <= 0) {
+        rcoll.bs = DEF_BLOCK_SIZE;
+        fprintf(stderr, "Assume default 'bs' (block size) of %d bytes\n",
+                rcoll.bs);
+    }
+    if ((ibs && (ibs != rcoll.bs)) || (obs && (obs != rcoll.bs))) {
+        fprintf(stderr, "If 'ibs' or 'obs' given must be same as 'bs'\n");
+        usage();
+        return 1;
+    }
+    if ((skip < 0) || (seek < 0)) {
+        fprintf(stderr, "skip and seek cannot be negative\n");
+        return 1;
+    }
+    if ((num_threads < 1) || (num_threads > MAX_NUM_THREADS)) {
+        fprintf(stderr, "too few or too many threads requested\n");
+        usage();
+        return 1;
+    }
+    if (rcoll.debug)
+        fprintf(stderr, "sgq_dd: if=%s skip=%d of=%s seek=%d count=%d\n",
+               inf, skip, outf, seek, dd_count);
+    install_handler (SIGINT, interrupt_handler);
+    install_handler (SIGQUIT, interrupt_handler);
+    install_handler (SIGPIPE, interrupt_handler);
+    install_handler (SIGUSR1, siginfo_handler);
+
+    rcoll.infd = STDIN_FILENO;
+    rcoll.outfd = STDOUT_FILENO;
+    if (inf[0] && ('-' != inf[0])) {
+        rcoll.in_type = dd_filetype(inf);
+
+        if (FT_SG == rcoll.in_type) {
+            if ((rcoll.infd = open(inf, O_RDWR)) < 0) {
+                snprintf(ebuff, EBUFF_SZ,
+                         "sgq_dd: could not open %s for sg reading", inf);
+                perror(ebuff);
+                return 1;
+            }
+        }
+        if (FT_SG != rcoll.in_type) {
+            if ((rcoll.infd = open(inf, O_RDONLY)) < 0) {
+                snprintf(ebuff, EBUFF_SZ,
+                         "sgq_dd: could not open %s for reading", inf);
+                perror(ebuff);
+                return 1;
+            }
+            else if (skip > 0) {
+                loff_t offset = skip;
+
+                offset *= rcoll.bs;       /* could exceed 32 here! */
+                if (lseek(rcoll.infd, offset, SEEK_SET) < 0) {
+                    snprintf(ebuff, EBUFF_SZ,
+                "sgq_dd: couldn't skip to required position on %s", inf);
+                    perror(ebuff);
+                    return 1;
+                }
+            }
+        }
+    }
+    if (outf[0] && ('-' != outf[0])) {
+        rcoll.out_type = dd_filetype(outf);
+
+        if (FT_SG == rcoll.out_type) {
+            if ((rcoll.outfd = open(outf, O_RDWR)) < 0) {
+                snprintf(ebuff, EBUFF_SZ,
+                        "sgq_dd: could not open %s for sg writing", outf);
+                perror(ebuff);
+                return 1;
+            }
+        }
+        else {
+            if (FT_OTHER == rcoll.out_type) {
+                if ((rcoll.outfd = open(outf, O_WRONLY | O_CREAT, 0666)) < 0) {
+                    snprintf(ebuff, EBUFF_SZ,
+                            "sgq_dd: could not open %s for writing", outf);
+                    perror(ebuff);
+                    return 1;
+                }
+            }
+            else {
+                if ((rcoll.outfd = open(outf, O_WRONLY)) < 0) {
+                    snprintf(ebuff, EBUFF_SZ,
+                            "sgq_dd: could not open %s for raw writing", outf);
+                    perror(ebuff);
+                    return 1;
+                }
+            }
+            if (seek > 0) {
+                loff_t offset = seek;
+
+                offset *= rcoll.bs;       /* could exceed 32 bits here! */
+                if (lseek(rcoll.outfd, offset, SEEK_SET) < 0) {
+                    snprintf(ebuff, EBUFF_SZ,
+                "sgq_dd: couldn't seek to required position on %s", outf);
+                    perror(ebuff);
+                    return 1;
+                }
+            }
+        }
+    }
+    if ((STDIN_FILENO == rcoll.infd) && (STDOUT_FILENO == rcoll.outfd)) {
+        fprintf(stderr, "Disallow both if and of to be stdin and stdout\n");
+        return 1;
+    }
+    if ((FT_OTHER == rcoll.in_type) && (FT_OTHER == rcoll.out_type) && !gen) {
+        fprintf(stderr, "Either 'if' or 'of' must be a sg or raw device\n");
+        return 1;
+    }
+    if (0 == dd_count)
+        return 0;
+    else if (dd_count < 0) {
+        if (FT_SG == rcoll.in_type) {
+            res = read_capacity(rcoll.infd, &in_num_sect, &in_sect_sz);
+            if (2 == res) {
+                fprintf(stderr, "Unit attention, media changed(in), repeat\n");
+                res = read_capacity(rcoll.infd, &in_num_sect, &in_sect_sz);
+            }
+            if (0 != res) {
+                fprintf(stderr, "Unable to read capacity on %s\n", inf);
+                in_num_sect = -1;
+            }
+            else {
+                if (in_num_sect > skip)
+                    in_num_sect -= skip;
+            }
+        }
+        if (FT_SG == rcoll.out_type) {
+            res = read_capacity(rcoll.outfd, &out_num_sect, &out_sect_sz);
+            if (2 == res) {
+                fprintf(stderr, "Unit attention, media changed(out), "
+                        "repeat\n");
+                res = read_capacity(rcoll.outfd, &out_num_sect, &out_sect_sz);
+            }
+            if (0 != res) {
+                fprintf(stderr, "Unable to read capacity on %s\n", outf);
+                out_num_sect = -1;
+            }
+            else {
+                if (out_num_sect > seek)
+                    out_num_sect -= seek;
+            }
+        }
+        if (in_num_sect > 0) {
+            if (out_num_sect > 0)
+                dd_count = (in_num_sect > out_num_sect) ? out_num_sect :
+                                                       in_num_sect;
+            else
+                dd_count = in_num_sect;
+        }
+        else
+            dd_count = out_num_sect;
+    }
+    if (rcoll.debug > 1)
+        fprintf(stderr, "Start of loop, count=%d, in_num_sect=%d, "
+                "out_num_sect=%d\n", dd_count, in_num_sect, out_num_sect);
+    if (dd_count <= 0) {
+        fprintf(stderr, "Couldn't calculate count, please give one\n");
+        return 1;
+    }
+
+    rcoll.in_count = dd_count;
+    rcoll.in_done_count = dd_count;
+    rcoll.skip = skip;
+    rcoll.in_blk = skip;
+    rcoll.out_count = dd_count;
+    rcoll.out_done_count = dd_count;
+    rcoll.seek = seek;
+    rcoll.out_blk = seek;
+
+    if ((FT_SG == rcoll.in_type) || (FT_SG == rcoll.out_type))
+        rcoll.num_rq_elems = num_threads;
+    else
+        rcoll.num_rq_elems = 1;
+    if (prepare_rq_elems(&rcoll, inf, outf)) {
+        fprintf(stderr, "Setup failure, perhaps no memory\n");
+        return 1;
+    }
+
+    first_xfer = 1;
+    stop_after_write = 0;
+    terminate = 0;
+    seek_skip =  rcoll.seek - rcoll.skip;
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+    }
+    while (rcoll.out_done_count > 0) { /* >>>>>>>>> main loop */
+        req_index = -1;
+        qstate = decider(&rcoll, first_xfer, &req_index);
+        rep = (req_index < 0) ? NULL : (rcoll.req_arr + req_index);
+        switch (qstate) {
+        case QS_IDLE:
+            if ((NULL == rep) || (rcoll.in_count <= 0)) {
+                /* usleep(1000); */
+                /* do_poll(&rcoll, 10, NULL); */
+                /* do_poll(&rcoll, 0, NULL); */
+                break;
+            }
+            if (rcoll.debug > 8)
+                fprintf(stderr, "    sgq_dd: non-sleeping QS_IDLE state, "
+                                "req_index=%d\n", req_index);
+            if (first_xfer >= 2)
+                first_xfer = 0;
+            else if (1 == first_xfer)
+                ++first_xfer;
+            if (stop_after_write) {
+                terminate = 1;
+                break;
+            }
+            blocks = (rcoll.in_count > rcoll.bpt) ? rcoll.bpt : rcoll.in_count;
+            rep->wr = 0;
+            rep->blk = rcoll.in_blk;
+            rep->num_blks = blocks;
+            rcoll.in_blk += blocks;
+            rcoll.in_count -= blocks;
+
+            if (FT_SG == rcoll.in_type) {
+                res = sg_start_io(rep);
+                if (0 != res) {
+                    if (1 == res)
+                        fprintf(stderr, "Out of memory starting sg io\n");
+                    terminate = 1;
+                }
+            }
+            else {
+                res = normal_in_operation(&rcoll, rep, blocks);
+                if (res < 0)
+                    terminate = 1;
+                else if (res > 0)
+                    stop_after_write = 1;
+            }
+            break;
+        case QS_IN_FINISHED:
+            if (rcoll.debug > 8)
+                fprintf(stderr, "    sgq_dd: state is QS_IN_FINISHED, "
+                                "req_index=%d\n", req_index);
+            if ((rep->blk + seek_skip) != rcoll.out_blk) {
+                /* if write would be out of sequence then wait */
+                if (rcoll.debug > 4)
+                    fprintf(stderr, "    sgq_dd: QS_IN_FINISHED, "
+                            "out of sequence\n");
+                usleep(200);
+                break;
+            }
+            rep->wr = 1;
+            rep->blk = rcoll.out_blk;
+            blocks = rep->num_blks;
+            rcoll.out_blk += blocks;
+            rcoll.out_count -= blocks;
+
+            if (FT_SG == rcoll.out_type) {
+                res = sg_start_io(rep);
+                if (0 != res) {
+                    if (1 == res)
+                        fprintf(stderr, "Out of memory starting sg io\n");
+                    terminate = 1;
+                }
+            }
+            else {
+                if (normal_out_operation(&rcoll, rep, blocks) < 0)
+                    terminate = 1;
+            }
+            break;
+        case QS_IN_POLL:
+            if (rcoll.debug > 8)
+                fprintf(stderr, "    sgq_dd: state is QS_IN_POLL, "
+                                "req_index=%d\n", req_index);
+            res = sg_fin_in_operation(&rcoll, rep);
+            if (res < 0)
+                terminate = 1;
+            else if (res > 1) {
+                if (first_xfer) {
+                    /* only retry on first xfer */
+                    if (0 != sg_start_io(rep))
+                        terminate = 1;
+                }
+                else
+                    terminate = 1;
+            }
+            break;
+        case QS_OUT_POLL:
+            if (rcoll.debug > 8)
+                fprintf(stderr, "    sgq_dd: state is QS_OUT_POLL, "
+                                "req_index=%d\n", req_index);
+            res = sg_fin_out_operation(&rcoll, rep);
+            if (res < 0)
+                terminate = 1;
+            else if (res > 1) {
+                if (first_xfer) {
+                    /* only retry on first xfer */
+                    if (0 != sg_start_io(rep))
+                        terminate = 1;
+                }
+                else
+                    terminate = 1;
+            }
+            break;
+        default:
+            if (rcoll.debug > 8)
+                fprintf(stderr, "    sgq_dd: state is ?????\n");
+            terminate = 1;
+            break;
+        }
+        if (terminate)
+            break;
+    } /* >>>>>>>>>>>>> end of main loop */
+
+    if ((do_time) && (start_tm.tv_sec || start_tm.tv_usec)) {
+        struct timeval res_tm;
+        double a, b;
+
+        gettimeofday(&end_tm, NULL);
+        res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+        res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+        if (res_tm.tv_usec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_usec += 1000000;
+        }
+        a = res_tm.tv_sec;
+        a += (0.000001 * res_tm.tv_usec);
+        b = (double)rcoll.bs * (dd_count - rcoll.out_done_count);
+        printf("time to transfer data was %d.%06d secs",
+               (int)res_tm.tv_sec, (int)res_tm.tv_usec);
+        if ((a > 0.00001) && (b > 511))
+            printf(", %.2f MB/sec\n", b / (a * 1000000.0));
+        else
+            printf("\n");
+    }
+
+    if (STDIN_FILENO != rcoll.infd)
+        close(rcoll.infd);
+    if (STDOUT_FILENO != rcoll.outfd)
+        close(rcoll.outfd);
+    res = 0;
+    if (0 != rcoll.out_count) {
+        fprintf(stderr, ">>>> Some error occurred,\n");
+        res = 2;
+    }
+    print_stats();
+    if (rcoll.dio_incomplete) {
+        int fd;
+        char c;
+
+        fprintf(stderr, ">> Direct IO requested but incomplete %d times\n",
+                rcoll.dio_incomplete);
+        if ((fd = open(sg_allow_dio, O_RDONLY)) >= 0) {
+            if (1 == read(fd, &c, 1)) {
+                if ('0' == c)
+                    fprintf(stderr, ">>> %s set to '0' but should be set "
+                            "to '1' for direct IO\n", sg_allow_dio);
+            }
+            close(fd);
+        }
+    }
+    if (rcoll.sum_of_resids)
+        fprintf(stderr, ">> Non-zero sum of residual counts=%d\n",
+               rcoll.sum_of_resids);
+    return res;
+}
diff --git a/examples/transport_ids.txt b/examples/transport_ids.txt
new file mode 100644
index 0000000..2657af2
--- /dev/null
+++ b/examples/transport_ids.txt
@@ -0,0 +1,31 @@
+# This file is an example for the sg_persist utility.
+# It discusses using "TransportID"s which are defined (most recently)
+# in SPC-4 revision 20 section 7.5.4 titled: "TransportID identifiers".
+#
+# The sg_persist utility can take one or more "transportID"s from stdin when
+# either the '--transport-id=-" or "-X -" option is given on the command
+# line.
+
+# To see transport IDs decoded after they have been read in (e.g. to check
+# they are well formed) use the verbose flag 3 times (i.e. "... -vvv ...").
+
+# Here is a simple example (for SPI) of a comma separated hex list:
+1,0,0,7,0,0,0,1         # SPI, initiator address=7, relative_port_num=1
+
+# and here is the transport specific format for the same thing:
+# spi,1,7
+
+# An example for SAS follows, first as a hex string, then in transport
+# specific format (incremented by 1)
+6,0,0,0,50,6,5,b0,0,6,f2,60
+sas,500605b00006f261
+
+# For iSCSI the hex list form is awkward, better to use the transport
+# specific format. [The leading spaces are ignored.]
+    iqn.5886.com.acme.diskarrays-sn-a8675309
+
+
+        # Leading spaces and tabs before a '#' are ok.
+
+
+# dpg 20090824
diff --git a/getopt_long/getopt.h b/getopt_long/getopt.h
new file mode 100644
index 0000000..4d6543b
--- /dev/null
+++ b/getopt_long/getopt.h
@@ -0,0 +1,86 @@
+/*	$NetBSD: getopt.h,v 1.7 2005/02/03 04:39:32 perry Exp $	*/
+
+/*-
+ * Copyright (c) 2000 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Dieter Baron and Thomas Klausner.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ *    must display the following acknowledgement:
+ *        This product includes software developed by the NetBSD
+ *        Foundation, Inc. and its contributors.
+ * 4. Neither the name of The NetBSD Foundation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * modified May 12, 2005 by Jim Basney <jbasney@ncsa.uiuc.edu>
+ *
+ * removed #include of non-POSIX <sys/cdefs.h> and <sys/featuretest.h>
+ * removed references to _NETBSD_SOURCE and HAVE_NBTOOL_CONFIG_H
+ * added #if !HAVE_GETOPT_LONG
+ * removed __BEGIN_DECLS and __END_DECLS
+ */
+
+#ifndef _MYPROXY_GETOPT_H_
+#define _MYPROXY_GETOPT_H_
+
+#if !HAVE_GETOPT_LONG
+
+#include <unistd.h>
+
+/*
+ * Gnu like getopt_long() and BSD4.4 getsubopt()/optreset extensions
+ */
+#define no_argument        0
+#define required_argument  1
+#define optional_argument  2
+
+extern char *optarg;
+extern int optind;
+extern int optopt;
+extern int opterr;
+
+struct option {
+	/* name of long option */
+	const char *name;
+	/*
+	 * one of no_argument, required_argument, and optional_argument:
+	 * whether option takes an argument
+	 */
+	int has_arg;
+	/* if not NULL, set *flag to val when option found */
+	int *flag;
+	/* if flag not NULL, value to set *flag to; else return value */
+	int val;
+};
+
+int getopt_long(int, char * const *, const char *,
+    const struct option *, int *);
+ 
+#endif /* !HAVE_GETOPT_LONG */
+
+#endif /* !_MYPROXY_GETOPT_H_ */
diff --git a/getopt_long/getopt_long.c b/getopt_long/getopt_long.c
new file mode 100644
index 0000000..f94dcae
--- /dev/null
+++ b/getopt_long/getopt_long.c
@@ -0,0 +1,434 @@
+/*	$NetBSD: getopt_long.c,v 1.17 2004/06/20 22:20:15 jmc Exp $	*/
+
+/*-
+ * Copyright (c) 2000 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Dieter Baron and Thomas Klausner.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ *    must display the following acknowledgement:
+ *        This product includes software developed by the NetBSD
+ *        Foundation, Inc. and its contributors.
+ * 4. Neither the name of The NetBSD Foundation nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * modified May 12, 2005 by Jim Basney <jbasney@ncsa.uiuc.edu>
+ *
+ * removed #include of non-POSIX <sys/cdefs.h> <err.h>
+ * removed #include of "namespace.h"
+ * use local "port_getopt.h" instead of <getopt.h>
+ * removed REPLACE_GETOPT and HAVE_NBTOOL_CONFIG_H sections
+ * removed __P() from function declarations
+ * use ANSI C function parameter lists
+ * removed optreset support
+ * replace _DIAGASSERT() with assert()
+ * replace non-POSIX warnx(...) with fprintf(stderr, ...)
+ * added extern declarations for optarg, optind, opterr, and optopt
+ */
+
+#if defined(LIBC_SCCS) && !defined(lint)
+__RCSID("$NetBSD: getopt_long.c,v 1.17 2004/06/20 22:20:15 jmc Exp $");
+#endif /* LIBC_SCCS and not lint */
+
+#include <assert.h>
+#include <errno.h>
+#include "getopt.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef __weak_alias
+__weak_alias(getopt_long,_getopt_long)
+#endif
+
+#if !HAVE_GETOPT_LONG
+#define IGNORE_FIRST	(*options == '-' || *options == '+')
+#define PRINT_ERROR	((opterr) && ((*options != ':') \
+				      || (IGNORE_FIRST && options[1] != ':')))
+#define IS_POSIXLY_CORRECT (getenv("POSIXLY_CORRECT") != NULL)
+#define PERMUTE         (!IS_POSIXLY_CORRECT && !IGNORE_FIRST)
+/* XXX: GNU ignores PC if *options == '-' */
+#define IN_ORDER        (!IS_POSIXLY_CORRECT && *options == '-')
+
+/* return values */
+#define	BADCH	(int)'?'
+#define	BADARG		((IGNORE_FIRST && options[1] == ':') \
+			 || (*options == ':') ? (int)':' : (int)'?')
+#define INORDER (int)1
+
+#define	EMSG	""
+
+extern char *optarg;
+extern int optind, opterr, optopt;
+
+static int getopt_internal (int, char * const *, const char *);
+static int gcd (int, int);
+static void permute_args (int, int, int, char * const *);
+
+static char *place = EMSG; /* option letter processing */
+
+static int nonopt_start = -1; /* first non option argument (for permute) */
+static int nonopt_end = -1;   /* first option after non options (for permute) */
+
+/* Error messages */
+static const char recargchar[] = "option requires an argument -- %c";
+static const char recargstring[] = "option requires an argument -- %s";
+static const char ambig[] = "ambiguous option -- %.*s";
+static const char noarg[] = "option doesn't take an argument -- %.*s";
+static const char illoptchar[] = "unknown option -- %c";
+static const char illoptstring[] = "unknown option -- %s";
+
+
+/*
+ * Compute the greatest common divisor of a and b.
+ */
+static int
+gcd(int a, int b)
+{
+	int c;
+
+	c = a % b;
+	while (c != 0) {
+		a = b;
+		b = c;
+		c = a % b;
+	}
+	   
+	return b;
+}
+
+/*
+ * Exchange the block from nonopt_start to nonopt_end with the block
+ * from nonopt_end to opt_end (keeping the same order of arguments
+ * in each block).
+ */
+static void
+permute_args(int panonopt_start, int panonopt_end, int opt_end,
+	     char * const *nargv)
+{
+	int cstart, cyclelen, i, j, ncycle, nnonopts, nopts, pos;
+	char *swap;
+
+	assert(nargv != NULL);
+
+	/*
+	 * compute lengths of blocks and number and size of cycles
+	 */
+	nnonopts = panonopt_end - panonopt_start;
+	nopts = opt_end - panonopt_end;
+	ncycle = gcd(nnonopts, nopts);
+	cyclelen = (opt_end - panonopt_start) / ncycle;
+
+	for (i = 0; i < ncycle; i++) {
+		cstart = panonopt_end+i;
+		pos = cstart;
+		for (j = 0; j < cyclelen; j++) {
+			if (pos >= panonopt_end)
+				pos -= nnonopts;
+			else
+				pos += nopts;
+			swap = nargv[pos];
+			/* LINTED const cast */
+			((char **) nargv)[pos] = nargv[cstart];
+			/* LINTED const cast */
+			((char **)nargv)[cstart] = swap;
+		}
+	}
+}
+
+/*
+ * getopt_internal --
+ *	Parse argc/argv argument vector.  Called by user level routines.
+ *  Returns -2 if -- is found (can be long option or end of options marker).
+ */
+static int
+getopt_internal(int nargc, char * const *nargv, const char *options)
+{
+	char *oli;				/* option letter list index */
+	int optchar;
+
+	assert(nargv != NULL);
+	assert(options != NULL);
+
+	optarg = NULL;
+
+	/*
+	 * XXX Some programs (like rsyncd) expect to be able to
+	 * XXX re-initialize optind to 0 and have getopt_long(3)
+	 * XXX properly function again.  Work around this braindamage.
+	 */
+	if (optind == 0)
+		optind = 1;
+
+start:
+	if (!*place) {		                /* update scanning pointer */
+		if (optind >= nargc) {          /* end of argument vector */
+			place = EMSG;
+			if (nonopt_end != -1) {
+				/* do permutation, if we have to */
+				permute_args(nonopt_start, nonopt_end,
+				    optind, nargv);
+				optind -= nonopt_end - nonopt_start;
+			}
+			else if (nonopt_start != -1) {
+				/*
+				 * If we skipped non-options, set optind
+				 * to the first of them.
+				 */
+				optind = nonopt_start;
+			}
+			nonopt_start = nonopt_end = -1;
+			return -1;
+		}
+		if ((*(place = nargv[optind]) != '-')
+		    || (place[1] == '\0')) {    /* found non-option */
+			place = EMSG;
+			if (IN_ORDER) {
+				/*
+				 * GNU extension: 
+				 * return non-option as argument to option 1
+				 */
+				optarg = nargv[optind++];
+				return INORDER;
+			}
+			if (!PERMUTE) {
+				/*
+				 * if no permutation wanted, stop parsing
+				 * at first non-option
+				 */
+				return -1;
+			}
+			/* do permutation */
+			if (nonopt_start == -1)
+				nonopt_start = optind;
+			else if (nonopt_end != -1) {
+				permute_args(nonopt_start, nonopt_end,
+				    optind, nargv);
+				nonopt_start = optind -
+				    (nonopt_end - nonopt_start);
+				nonopt_end = -1;
+			}
+			optind++;
+			/* process next argument */
+			goto start;
+		}
+		if (nonopt_start != -1 && nonopt_end == -1)
+			nonopt_end = optind;
+		if (place[1] && *++place == '-') {	/* found "--" */
+			place++;
+			return -2;
+		}
+	}
+	if ((optchar = (int)*place++) == (int)':' ||
+	    (oli = strchr(options + (IGNORE_FIRST ? 1 : 0), optchar)) == NULL) {
+		/* option letter unknown or ':' */
+		if (!*place)
+			++optind;
+		if (PRINT_ERROR)
+			fprintf(stderr, illoptchar, optchar);
+		optopt = optchar;
+		return BADCH;
+	}
+	if (optchar == 'W' && oli[1] == ';') {		/* -W long-option */
+		/* XXX: what if no long options provided (called by getopt)? */
+		if (*place) 
+			return -2;
+
+		if (++optind >= nargc) {	/* no arg */
+			place = EMSG;
+			if (PRINT_ERROR)
+				fprintf(stderr, recargchar, optchar);
+			optopt = optchar;
+			return BADARG;
+		} else				/* white space */
+			place = nargv[optind];
+		/*
+		 * Handle -W arg the same as --arg (which causes getopt to
+		 * stop parsing).
+		 */
+		return -2;
+	}
+	if (*++oli != ':') {			/* doesn't take argument */
+		if (!*place)
+			++optind;
+	} else {				/* takes (optional) argument */
+		optarg = NULL;
+		if (*place)			/* no white space */
+			optarg = place;
+		/* XXX: disable test for :: if PC? (GNU doesn't) */
+		else if (oli[1] != ':') {	/* arg not optional */
+			if (++optind >= nargc) {	/* no arg */
+				place = EMSG;
+				if (PRINT_ERROR)
+					fprintf(stderr, recargchar, optchar);
+				optopt = optchar;
+				return BADARG;
+			} else
+				optarg = nargv[optind];
+		}
+		place = EMSG;
+		++optind;
+	}
+	/* dump back option letter */
+	return optchar;
+}
+
+/*
+ * getopt_long --
+ *	Parse argc/argv argument vector.
+ */
+int
+getopt_long(int nargc, char * const *nargv, const char *options,
+	    const struct option *long_options, int *idx)
+{
+	int retval;
+
+	assert(nargv != NULL);
+	assert(options != NULL);
+	assert(long_options != NULL);
+	/* idx may be NULL */
+
+	if ((retval = getopt_internal(nargc, nargv, options)) == -2) {
+		char *current_argv, *has_equal;
+		size_t current_argv_len;
+		int i, match;
+
+		current_argv = place;
+		match = -1;
+
+		optind++;
+		place = EMSG;
+
+		if (*current_argv == '\0') {		/* found "--" */
+			/*
+			 * We found an option (--), so if we skipped
+			 * non-options, we have to permute.
+			 */
+			if (nonopt_end != -1) {
+				permute_args(nonopt_start, nonopt_end,
+				    optind, nargv);
+				optind -= nonopt_end - nonopt_start;
+			}
+			nonopt_start = nonopt_end = -1;
+			return -1;
+		}
+		if ((has_equal = strchr(current_argv, '=')) != NULL) {
+			/* argument found (--option=arg) */
+			current_argv_len = has_equal - current_argv;
+			has_equal++;
+		} else
+			current_argv_len = strlen(current_argv);
+	    
+		for (i = 0; long_options[i].name; i++) {
+			/* find matching long option */
+			if (strncmp(current_argv, long_options[i].name,
+			    current_argv_len))
+				continue;
+
+			if (strlen(long_options[i].name) ==
+			    (unsigned)current_argv_len) {
+				/* exact match */
+				match = i;
+				break;
+			}
+			if (match == -1)		/* partial match */
+				match = i;
+			else {
+				/* ambiguous abbreviation */
+				if (PRINT_ERROR)
+					fprintf(stderr, ambig, (int)current_argv_len,
+					     current_argv);
+				optopt = 0;
+				return BADCH;
+			}
+		}
+		if (match != -1) {			/* option found */
+		        if (long_options[match].has_arg == no_argument
+			    && has_equal) {
+				if (PRINT_ERROR)
+					fprintf(stderr, noarg, (int)current_argv_len,
+					     current_argv);
+				/*
+				 * XXX: GNU sets optopt to val regardless of
+				 * flag
+				 */
+				if (long_options[match].flag == NULL)
+					optopt = long_options[match].val;
+				else
+					optopt = 0;
+				return BADARG;
+			}
+			if (long_options[match].has_arg == required_argument ||
+			    long_options[match].has_arg == optional_argument) {
+				if (has_equal)
+					optarg = has_equal;
+				else if (long_options[match].has_arg ==
+				    required_argument) {
+					/*
+					 * optional argument doesn't use
+					 * next nargv
+					 */
+					optarg = nargv[optind++];
+				}
+			}
+			if ((long_options[match].has_arg == required_argument)
+			    && (optarg == NULL)) {
+				/*
+				 * Missing argument; leading ':'
+				 * indicates no error should be generated
+				 */
+				if (PRINT_ERROR)
+					fprintf(stderr, recargstring, current_argv);
+				/*
+				 * XXX: GNU sets optopt to val regardless
+				 * of flag
+				 */
+				if (long_options[match].flag == NULL)
+					optopt = long_options[match].val;
+				else
+					optopt = 0;
+				--optind;
+				return BADARG;
+			}
+		} else {			/* unknown option */
+			if (PRINT_ERROR)
+				fprintf(stderr, illoptstring, current_argv);
+			optopt = 0;
+			return BADCH;
+		}
+		if (long_options[match].flag) {
+			*long_options[match].flag = long_options[match].val;
+			retval = 0;
+		} else 
+			retval = long_options[match].val;
+		if (idx)
+			*idx = match;
+	}
+	return retval;
+}
+#endif /* !GETOPT_LONG */
diff --git a/include/Makefile.am b/include/Makefile.am
new file mode 100644
index 0000000..4cf82f1
--- /dev/null
+++ b/include/Makefile.am
@@ -0,0 +1,63 @@
+
+scsiincludedir      = $(includedir)/scsi
+
+scsiinclude_HEADERS = \
+	sg_lib.h \
+	sg_lib_data.h \
+	sg_lib_names.h \
+	sg_cmds.h \
+	sg_cmds_basic.h \
+	sg_cmds_extra.h \
+	sg_cmds_mmc.h \
+	sg_pr2serr.h \
+	sg_unaligned.h \
+	sg_pt.h \
+	sg_pt_nvme.h
+
+if OS_LINUX
+scsiinclude_HEADERS += \
+	sg_linux_inc.h \
+	sg_io_linux.h \
+	sg_pt_linux.h
+	
+noinst_HEADERS = \
+	sg_pt_win32.h
+endif
+
+if OS_WIN32_MINGW
+scsiinclude_HEADERS += sg_pt_win32.h
+	
+noinst_HEADERS = \
+	sg_linux_inc.h \
+	sg_io_linux.h
+endif
+
+if OS_WIN32_CYGWIN
+scsiinclude_HEADERS += sg_pt_win32.h
+	
+noinst_HEADERS = \
+	sg_linux_inc.h \
+	sg_io_linux.h
+endif
+
+if OS_FREEBSD
+noinst_HEADERS = \
+	sg_linux_inc.h \
+	sg_io_linux.h \
+	sg_pt_win32.h
+endif
+
+if OS_SOLARIS
+noinst_HEADERS = \
+	sg_linux_inc.h \
+	sg_io_linux.h \
+	sg_pt_win32.h
+endif
+
+if OS_OSF
+noinst_HEADERS = \
+	sg_linux_inc.h \
+	sg_io_linux.h \
+	sg_pt_win32.h
+endif
+
diff --git a/include/Makefile.in b/include/Makefile.in
new file mode 100644
index 0000000..6067757
--- /dev/null
+++ b/include/Makefile.in
@@ -0,0 +1,606 @@
+# Makefile.in generated by automake 1.16.5 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2021 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+  if test -z '$(MAKELEVEL)'; then \
+    false; \
+  elif test -n '$(MAKE_HOST)'; then \
+    true; \
+  elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+    true; \
+  else \
+    false; \
+  fi; \
+}
+am__make_running_with_option = \
+  case $${target_option-} in \
+      ?) ;; \
+      *) echo "am__make_running_with_option: internal error: invalid" \
+              "target option '$${target_option-}' specified" >&2; \
+         exit 1;; \
+  esac; \
+  has_opt=no; \
+  sane_makeflags=$$MAKEFLAGS; \
+  if $(am__is_gnu_make); then \
+    sane_makeflags=$$MFLAGS; \
+  else \
+    case $$MAKEFLAGS in \
+      *\\[\ \	]*) \
+        bs=\\; \
+        sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+          | sed "s/$$bs$$bs[$$bs $$bs	]*//g"`;; \
+    esac; \
+  fi; \
+  skip_next=no; \
+  strip_trailopt () \
+  { \
+    flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+  }; \
+  for flg in $$sane_makeflags; do \
+    test $$skip_next = yes && { skip_next=no; continue; }; \
+    case $$flg in \
+      *=*|--*) continue;; \
+        -*I) strip_trailopt 'I'; skip_next=yes;; \
+      -*I?*) strip_trailopt 'I';; \
+        -*O) strip_trailopt 'O'; skip_next=yes;; \
+      -*O?*) strip_trailopt 'O';; \
+        -*l) strip_trailopt 'l'; skip_next=yes;; \
+      -*l?*) strip_trailopt 'l';; \
+      -[dEDm]) skip_next=yes;; \
+      -[JT]) skip_next=yes;; \
+    esac; \
+    case $$flg in \
+      *$$target_option*) has_opt=yes; break;; \
+    esac; \
+  done; \
+  test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@OS_LINUX_TRUE@am__append_1 = \
+@OS_LINUX_TRUE@	sg_linux_inc.h \
+@OS_LINUX_TRUE@	sg_io_linux.h \
+@OS_LINUX_TRUE@	sg_pt_linux.h
+
+@OS_WIN32_MINGW_TRUE@am__append_2 = sg_pt_win32.h
+@OS_WIN32_CYGWIN_TRUE@am__append_3 = sg_pt_win32.h
+subdir = include
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+	$(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__noinst_HEADERS_DIST) \
+	$(am__scsiinclude_HEADERS_DIST) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo "  GEN     " $@;
+am__v_GEN_1 = 
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 = 
+SOURCES =
+DIST_SOURCES =
+am__can_run_installinfo = \
+  case $$AM_UPDATE_INFO_DIR in \
+    n|no|NO) false;; \
+    *) (install-info --version) >/dev/null 2>&1;; \
+  esac
+am__noinst_HEADERS_DIST = sg_linux_inc.h sg_io_linux.h sg_pt_win32.h
+am__scsiinclude_HEADERS_DIST = sg_lib.h sg_lib_data.h sg_lib_names.h \
+	sg_cmds.h sg_cmds_basic.h sg_cmds_extra.h sg_cmds_mmc.h \
+	sg_pr2serr.h sg_unaligned.h sg_pt.h sg_pt_nvme.h \
+	sg_linux_inc.h sg_io_linux.h sg_pt_linux.h sg_pt_win32.h
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+    $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+    *) f=$$p;; \
+  esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+  srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+  for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+  for p in $$list; do echo "$$p $$p"; done | \
+  sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+  $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+    if (++n[$$2] == $(am__install_max)) \
+      { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+    END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+  sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+  sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+  test -z "$$files" \
+    || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+    || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+         $(am__cd) "$$dir" && rm -f $$files; }; \
+  }
+am__installdirs = "$(DESTDIR)$(scsiincludedir)"
+HEADERS = $(noinst_HEADERS) $(scsiinclude_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates.  Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+  BEGIN { nonempty = 0; } \
+  { items[$$0] = 1; nonempty = 1; } \
+  END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique.  This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+  list='$(am__tagged_files)'; \
+  unique=`for i in $$list; do \
+    if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+  done | $(am__uniquify_input)`
+am__DIST_COMMON = $(srcdir)/Makefile.in
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CSCOPE = @CSCOPE@
+CTAGS = @CTAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+ETAGS = @ETAGS@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FILECMD = @FILECMD@
+GETOPT_O_FILES = @GETOPT_O_FILES@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PTHREAD_LIB = @PTHREAD_LIB@
+RANLIB = @RANLIB@
+RT_LIB = @RT_LIB@
+SED = @SED@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+STRIP = @STRIP@
+VERSION = @VERSION@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+os_cflags = @os_cflags@
+os_libs = @os_libs@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+scsiincludedir = $(includedir)/scsi
+scsiinclude_HEADERS = sg_lib.h sg_lib_data.h sg_lib_names.h sg_cmds.h \
+	sg_cmds_basic.h sg_cmds_extra.h sg_cmds_mmc.h sg_pr2serr.h \
+	sg_unaligned.h sg_pt.h sg_pt_nvme.h $(am__append_1) \
+	$(am__append_2) $(am__append_3)
+@OS_FREEBSD_TRUE@noinst_HEADERS = \
+@OS_FREEBSD_TRUE@	sg_linux_inc.h \
+@OS_FREEBSD_TRUE@	sg_io_linux.h \
+@OS_FREEBSD_TRUE@	sg_pt_win32.h
+
+@OS_LINUX_TRUE@noinst_HEADERS = \
+@OS_LINUX_TRUE@	sg_pt_win32.h
+
+@OS_OSF_TRUE@noinst_HEADERS = \
+@OS_OSF_TRUE@	sg_linux_inc.h \
+@OS_OSF_TRUE@	sg_io_linux.h \
+@OS_OSF_TRUE@	sg_pt_win32.h
+
+@OS_SOLARIS_TRUE@noinst_HEADERS = \
+@OS_SOLARIS_TRUE@	sg_linux_inc.h \
+@OS_SOLARIS_TRUE@	sg_io_linux.h \
+@OS_SOLARIS_TRUE@	sg_pt_win32.h
+
+@OS_WIN32_CYGWIN_TRUE@noinst_HEADERS = \
+@OS_WIN32_CYGWIN_TRUE@	sg_linux_inc.h \
+@OS_WIN32_CYGWIN_TRUE@	sg_io_linux.h
+
+@OS_WIN32_MINGW_TRUE@noinst_HEADERS = \
+@OS_WIN32_MINGW_TRUE@	sg_linux_inc.h \
+@OS_WIN32_MINGW_TRUE@	sg_io_linux.h
+
+all: all-am
+
+.SUFFIXES:
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am  $(am__configure_deps)
+	@for dep in $?; do \
+	  case '$(am__configure_deps)' in \
+	    *$$dep*) \
+	      ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+	        && { if test -f $@; then exit 0; else break; fi; }; \
+	      exit 1;; \
+	  esac; \
+	done; \
+	echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign include/Makefile'; \
+	$(am__cd) $(top_srcdir) && \
+	  $(AUTOMAKE) --foreign include/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+	@case '$?' in \
+	  *config.status*) \
+	    cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+	  *) \
+	    echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+	    cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+	esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+mostlyclean-libtool:
+	-rm -f *.lo
+
+clean-libtool:
+	-rm -rf .libs _libs
+install-scsiincludeHEADERS: $(scsiinclude_HEADERS)
+	@$(NORMAL_INSTALL)
+	@list='$(scsiinclude_HEADERS)'; test -n "$(scsiincludedir)" || list=; \
+	if test -n "$$list"; then \
+	  echo " $(MKDIR_P) '$(DESTDIR)$(scsiincludedir)'"; \
+	  $(MKDIR_P) "$(DESTDIR)$(scsiincludedir)" || exit 1; \
+	fi; \
+	for p in $$list; do \
+	  if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+	  echo "$$d$$p"; \
+	done | $(am__base_list) | \
+	while read files; do \
+	  echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(scsiincludedir)'"; \
+	  $(INSTALL_HEADER) $$files "$(DESTDIR)$(scsiincludedir)" || exit $$?; \
+	done
+
+uninstall-scsiincludeHEADERS:
+	@$(NORMAL_UNINSTALL)
+	@list='$(scsiinclude_HEADERS)'; test -n "$(scsiincludedir)" || list=; \
+	files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+	dir='$(DESTDIR)$(scsiincludedir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+	$(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+	set x; \
+	here=`pwd`; \
+	$(am__define_uniq_tagged_files); \
+	shift; \
+	if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+	  test -n "$$unique" || unique=$$empty_fix; \
+	  if test $$# -gt 0; then \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      "$$@" $$unique; \
+	  else \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      $$unique; \
+	  fi; \
+	fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+	$(am__define_uniq_tagged_files); \
+	test -z "$(CTAGS_ARGS)$$unique" \
+	  || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+	     $$unique
+
+GTAGS:
+	here=`$(am__cd) $(top_builddir) && pwd` \
+	  && $(am__cd) $(top_srcdir) \
+	  && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+	list='$(am__tagged_files)'; \
+	case "$(srcdir)" in \
+	  [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+	  *) sdir=$(subdir)/$(srcdir) ;; \
+	esac; \
+	for i in $$list; do \
+	  if test -f "$$i"; then \
+	    echo "$(subdir)/$$i"; \
+	  else \
+	    echo "$$sdir/$$i"; \
+	  fi; \
+	done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+	-rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+distdir: $(BUILT_SOURCES)
+	$(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+	@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	list='$(DISTFILES)'; \
+	  dist_files=`for file in $$list; do echo $$file; done | \
+	  sed -e "s|^$$srcdirstrip/||;t" \
+	      -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+	case $$dist_files in \
+	  */*) $(MKDIR_P) `echo "$$dist_files" | \
+			   sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+			   sort -u` ;; \
+	esac; \
+	for file in $$dist_files; do \
+	  if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+	  if test -d $$d/$$file; then \
+	    dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+	    if test -d "$(distdir)/$$file"; then \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+	      cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+	  else \
+	    test -f "$(distdir)/$$file" \
+	    || cp -p $$d/$$file "$(distdir)/$$file" \
+	    || exit 1; \
+	  fi; \
+	done
+check-am: all-am
+check: check-am
+all-am: Makefile $(HEADERS)
+installdirs:
+	for dir in "$(DESTDIR)$(scsiincludedir)"; do \
+	  test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+	done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+	@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+	if test -z '$(STRIP)'; then \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	      install; \
+	else \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	    "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+	fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+	-test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+	-test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+	@echo "This command is intended for maintainers to use"
+	@echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool mostlyclean-am
+
+distclean: distclean-am
+	-rm -f Makefile
+distclean-am: clean-am distclean-generic distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-scsiincludeHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+	-rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-generic mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-scsiincludeHEADERS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am check check-am clean clean-generic \
+	clean-libtool cscopelist-am ctags ctags-am distclean \
+	distclean-generic distclean-libtool distclean-tags distdir dvi \
+	dvi-am html html-am info info-am install install-am \
+	install-data install-data-am install-dvi install-dvi-am \
+	install-exec install-exec-am install-html install-html-am \
+	install-info install-info-am install-man install-pdf \
+	install-pdf-am install-ps install-ps-am \
+	install-scsiincludeHEADERS install-strip installcheck \
+	installcheck-am installdirs maintainer-clean \
+	maintainer-clean-generic mostlyclean mostlyclean-generic \
+	mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \
+	uninstall-am uninstall-scsiincludeHEADERS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/include/freebsd_nvme_ioctl.h b/include/freebsd_nvme_ioctl.h
new file mode 100644
index 0000000..0b79d85
--- /dev/null
+++ b/include/freebsd_nvme_ioctl.h
@@ -0,0 +1,175 @@
+#ifndef FREEBSD_NVME_IOCTL_H
+#define FREEBSD_NVME_IOCTL_H
+
+/*-
+ * Copyright (C) 2012-2013 Intel Corporation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * $FreeBSD$
+ */
+
+
+#include <sys/param.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define NVME_PASSTHROUGH_CMD    _IOWR('n', 0, struct nvme_pt_command)
+
+#if __FreeBSD_version < 1100110
+
+#define NVME_STATUS_GET_SC(st)  (st.sc)
+#define NVME_STATUS_GET_SCT(st)  (st.sct)
+
+
+struct nvme_command
+{
+        /* dword 0 */
+        uint16_t opc    :  8;   /* opcode */
+        uint16_t fuse   :  2;   /* fused operation */
+        uint16_t rsvd1  :  6;
+        uint16_t cid;           /* command identifier */
+
+        /* dword 1 */
+        uint32_t nsid;          /* namespace identifier */
+
+        /* dword 2-3 */
+        uint32_t rsvd2;
+        uint32_t rsvd3;
+
+        /* dword 4-5 */
+        uint64_t mptr;          /* metadata pointer */
+
+        /* dword 6-7 */
+        uint64_t prp1;          /* prp entry 1 */
+
+        /* dword 8-9 */
+        uint64_t prp2;          /* prp entry 2 */
+
+        /* dword 10-15 */
+        uint32_t cdw10;         /* command-specific */
+        uint32_t cdw11;         /* command-specific */
+        uint32_t cdw12;         /* command-specific */
+        uint32_t cdw13;         /* command-specific */
+        uint32_t cdw14;         /* command-specific */
+        uint32_t cdw15;         /* command-specific */
+} __packed;
+
+struct nvme_status {
+
+        uint16_t p      :  1;   /* phase tag */
+        uint16_t sc     :  8;   /* status code */
+        uint16_t sct    :  3;   /* status code type */
+        uint16_t rsvd2  :  2;
+        uint16_t m      :  1;   /* more */
+        uint16_t dnr    :  1;   /* do not retry */
+} __packed;
+
+struct nvme_completion {
+
+        /* dword 0 */
+        uint32_t                cdw0;   /* command-specific */
+
+        /* dword 1 */
+        uint32_t                rsvd1;
+
+        /* dword 2 */
+        uint16_t                sqhd;   /* submission queue head pointer */
+        uint16_t                sqid;   /* submission queue identifier */
+
+        /* dword 3 */
+        uint16_t                cid;    /* command identifier */
+        struct nvme_status      status;
+} __packed;
+
+struct nvme_pt_command {
+
+        /*
+         * cmd is used to specify a passthrough command to a controller or
+         *  namespace.
+         *
+         * The following fields from cmd may be specified by the caller:
+         *      * opc  (opcode)
+         *      * nsid (namespace id) - for admin commands only
+         *      * cdw10-cdw15
+         *
+         * Remaining fields must be set to 0 by the caller.
+         */
+        struct nvme_command     cmd;
+
+        /*
+         * cpl returns completion status for the passthrough command
+         *  specified by cmd.
+         *
+         * The following fields will be filled out by the driver, for
+         *  consumption by the caller:
+         *      * cdw0
+         *      * status (except for phase)
+         *
+         * Remaining fields will be set to 0 by the driver.
+         */
+        struct nvme_completion  cpl;
+
+        /* buf is the data buffer associated with this passthrough command. */
+        void *                  buf;
+
+        /*
+         * len is the length of the data buffer associated with this
+         *  passthrough command.
+         */
+        uint32_t                len;
+
+        /*
+         * is_read = 1 if the passthrough command will read data into the
+         *  supplied buffer from the controller.
+         *
+         * is_read = 0 if the passthrough command will write data from the
+         *  supplied buffer to the controller.
+         */
+        uint32_t                is_read;
+
+        /*
+         * driver_lock is used by the driver only.  It must be set to 0
+         *  by the caller.
+         */
+        struct mtx *            driver_lock;
+};
+#else		/* not    __FreeBSD_version < 1100110 */
+#include <dev/nvme/nvme.h>
+#endif		/* __FreeBSD_version < 1100110 */
+
+#ifndef nvme_completion_is_error
+#define nvme_completion_is_error(cpl)                                   \
+        ((cpl)->status.sc != 0 || (cpl)->status.sct != 0)
+#endif
+
+#define NVME_CTRLR_PREFIX       "/dev/nvme"
+#define NVME_NS_PREFIX          "ns"
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif          /* for FREEBSD_NVME_IOCTL_H */
diff --git a/include/sg_cmds.h b/include/sg_cmds.h
new file mode 100644
index 0000000..690f53a
--- /dev/null
+++ b/include/sg_cmds.h
@@ -0,0 +1,21 @@
+#ifndef SG_CMDS_H
+#define SG_CMDS_H
+
+/********************************************************************
+ * This header did contain wrapper declarations for many SCSI commands
+ * up until sg3_utils version 1.22 . In that version, the command
+ * wrappers were broken into two groups, the 'basic' ones found in the
+ * "sg_cmds_basic.h" header and the 'extra' ones found in the
+ * "sg_cmds_extra.h" header. This header now simply includes those two
+ * headers.
+ * In sg3_utils version 1.26 the sg_cmds_mmc.h header was added and
+ * contains some MMC specific commands.
+ * The corresponding function definitions are found in the sg_cmds_basic.c,
+ * sg_cmds_extra.c and sg_cmds_mmc.c files.
+ ********************************************************************/
+
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_cmds_mmc.h"
+
+#endif
diff --git a/include/sg_cmds_basic.h b/include/sg_cmds_basic.h
new file mode 100644
index 0000000..48aabc9
--- /dev/null
+++ b/include/sg_cmds_basic.h
@@ -0,0 +1,364 @@
+#ifndef SG_CMDS_BASIC_H
+#define SG_CMDS_BASIC_H
+
+/*
+ * Copyright (c) 2004-2020 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Error, warning and verbose output is sent to the file pointed to by
+ * sg_warnings_strm which is declared in sg_lib.h and can be set with
+ * the sg_set_warnings_strm() function. If not given sg_warnings_strm
+ * defaults to stderr.
+ * If 'noisy' is false and 'verbose' is zero then following functions should
+ * not output anything to sg_warnings_strm. If 'noisy' is true and 'verbose'
+ * is zero then Unit Attention, Recovered, Medium and Hardware errors (sense
+ * keys) send output to sg_warnings_strm. Increasing values of 'verbose'
+ * send increasing amounts of (debug) output to sg_warnings_strm.
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Functions with the "_pt" suffix take a pointer to an object (derived from)
+ * sg_pt_base rather than an open file descriptor as their first argument.
+ * That object is assumed to be constructed and have a device file descriptor
+ * associated with it. clear_scsi_pt_obj() is called at the start of each
+ * "_pt" function. Caller is responsible for lifetime of ptp.
+ * If the sense buffer is accessed outside the "_pt" function then the caller
+ * must invoke set_scsi_pt_sense() _prior_ to the "_pt" function. Otherwise
+ * a sense buffer local to "_pt" function is used.
+ * Usually the cdb pointer will be NULL going into the "_pt" functions but
+ * could be given by the caller in which case it will used rather than a
+ * locally generated one. */
+
+struct sg_pt_base;
+
+
+/* Invokes a SCSI INQUIRY command and yields the response
+ * Returns 0 when successful, SG_LIB_CAT_INVALID_OP -> not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb,
+ * SG_LIB_CAT_ABORTED_COMMAND, a negated errno or -1 -> other errors */
+int sg_ll_inquiry(int sg_fd, bool cmddt, bool evpd, int pg_op, void * resp,
+                  int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI INQUIRY command and yields the response. Returns 0 when
+ * successful, various SG_LIB_CAT_* positive values, negated error or -1
+ * for other errors. The CMDDT field is obsolete in the INQUIRY cdb (since
+ * spc3r16 in 2003) so * an argument to set it has been removed (use the
+ * REPORT SUPPORTED OPERATION CODES command instead). Adds the ability to
+ * set the command abort timeout and the ability to report the residual
+ * count. If timeout_secs is zero or less the default command abort timeout
+ * (60 seconds) is used. If residp is non-NULL then the residual value is
+ * written where residp points. A residual value of 0 implies mx_resp_len
+ * bytes have be written where resp points. If the residual value equals
+ * mx_resp_len then no bytes have been written. */
+int sg_ll_inquiry_v2(int sg_fd, bool evpd, int pg_op, void * resp,
+                     int mx_resp_len, int timeout_secs, int * residp,
+                     bool noisy, int verbose);
+
+/* Similar to sg_ll_inquiry_v2(). See note above about "_pt" suffix. */
+int sg_ll_inquiry_pt(struct sg_pt_base * ptp, bool evpd, int pg_op,
+                     void * resp, int mx_resp_len, int timeout_secs,
+                     int * residp, bool noisy, int verbose);
+
+/* Invokes a SCSI LOG SELECT command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Log Select not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_ABORTED_COMMAND, * SG_LIB_CAT_NOT_READY -> device not ready,
+ * -1 -> other failure */
+int sg_ll_log_select(int sg_fd, bool pcr, bool sp, int pc, int pg_code,
+                     int subpg_code, uint8_t * paramp, int param_len,
+                     bool noisy, int verbose);
+
+/* Invokes a SCSI LOG SENSE command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Log Sense not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_log_sense(int sg_fd, bool ppc, bool sp, int pc, int pg_code,
+                    int subpg_code, int paramp, uint8_t * resp,
+                    int mx_resp_len, bool noisy, int verbose);
+
+/* Same as sg_ll_log_sense() apart from timeout_secs and residp. See
+ * sg_ll_inquiry_v2() for their description */
+int sg_ll_log_sense_v2(int sg_fd, bool ppc, bool sp, int pc, int pg_code,
+                       int subpg_code, int paramp, uint8_t * resp,
+                       int mx_resp_len, int timeout_secs, int * residp,
+                       bool noisy, int verbose);
+
+/* Invokes a SCSI MODE SELECT (6) command.  Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> invalid opcode, SG_LIB_CAT_ILLEGAL_REQ ->
+ * bad field in cdb, * SG_LIB_CAT_NOT_READY -> device not ready,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_mode_select6(int sg_fd, bool pf, bool sp, void * paramp,
+                        int param_len, bool noisy, int verbose);
+/* v2 adds RTD (revert to defaults) bit, added in spc5r11 */
+int sg_ll_mode_select6_v2(int sg_fd, bool pf, bool rtd, bool sp,
+                          void * paramp, int param_len, bool noisy,
+                          int verbose);
+
+/* Invokes a SCSI MODE SELECT (10) command.  Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> invalid opcode, SG_LIB_CAT_ILLEGAL_REQ ->
+ * bad field in cdb, * SG_LIB_CAT_NOT_READY -> device not ready,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_mode_select10(int sg_fd, bool pf, bool sp, void * paramp,
+                        int param_len, bool noisy, int verbose);
+/* v2 adds RTD (revert to defaults) bit, added in spc5r11 */
+int sg_ll_mode_select10_v2(int sg_fd, bool pf, bool rtd, bool sp,
+                           void * paramp, int param_len, bool noisy,
+                           int verbose);
+
+/* Invokes a SCSI MODE SENSE (6) command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> invalid opcode, SG_LIB_CAT_ILLEGAL_REQ ->
+ * bad field in cdb, * SG_LIB_CAT_NOT_READY -> device not ready,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_mode_sense6(int sg_fd, bool dbd, int pc, int pg_code,
+                      int sub_pg_code, void * resp, int mx_resp_len,
+                      bool noisy, int verbose);
+
+/* Invokes a SCSI MODE SENSE (10) command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> invalid opcode, SG_LIB_CAT_ILLEGAL_REQ ->
+ * bad field in cdb, * SG_LIB_CAT_NOT_READY -> device not ready,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_mode_sense10(int sg_fd, bool llbaa, bool dbd, int pc, int pg_code,
+                       int sub_pg_code, void * resp, int mx_resp_len,
+                       bool noisy, int verbose);
+
+/* Same as sg_ll_mode_sense10() apart from timeout_secs and residp. See
+ * sg_ll_inquiry_v2() for their description */
+int sg_ll_mode_sense10_v2(int sg_fd, bool llbaa, bool dbd, int pc,
+                          int pg_code, int sub_pg_code, void * resp,
+                          int mx_resp_len, int timeout_secs, int * residp,
+                          bool noisy, int verbose);
+
+/* Invokes a SCSI PREVENT ALLOW MEDIUM REMOVAL command (SPC-3)
+ * prevent==0 allows removal, prevent==1 prevents removal ...
+ * Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> command not supported
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_prevent_allow(int sg_fd, int prevent, bool noisy, int verbose);
+
+/* Invokes a SCSI READ CAPACITY (10) command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> invalid opcode, SG_LIB_CAT_UNIT_ATTENTION
+ * -> perhaps media changed, SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_readcap_10(int sg_fd, bool pmi, unsigned int lba, void * resp,
+                     int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI READ CAPACITY (16) command. Returns 0 -> success,
+ * SG_LIB_CAT_UNIT_ATTENTION -> media changed??, SG_LIB_CAT_INVALID_OP
+ *  -> cdb not supported, SG_LIB_CAT_IlLEGAL_REQ -> bad field in cdb
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_readcap_16(int sg_fd, bool pmi, uint64_t llba, void * resp,
+                     int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI REPORT LUNS command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Report Luns not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_NOT_READY (shouldn't happen), -1 -> other failure */
+int sg_ll_report_luns(int sg_fd, int select_report, void * resp,
+                      int mx_resp_len, bool noisy, int verbose);
+
+/* Similar to sg_ll_report_luns(). See note above about "_pt" suffix. */
+int sg_ll_report_luns_pt(struct sg_pt_base * ptp, int select_report,
+                         void * resp, int mx_resp_len, bool noisy,
+                         int verbose);
+
+/* Invokes a SCSI REQUEST SENSE command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Request Sense not supported??,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_request_sense(int sg_fd, bool desc, void * resp, int mx_resp_len,
+                        bool noisy, int verbose);
+
+/* Similar to sg_ll_request_sense(). See note above about "_pt" suffix. */
+int sg_ll_request_sense_pt(struct sg_pt_base * ptp, bool desc, void * resp,
+                           int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI START STOP UNIT command (SBC + MMC).
+ * Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Start stop unit not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure
+ * SBC-3 and MMC partially overlap on the power_condition_modifier(sbc) and
+ * format_layer_number(mmc) fields. They also overlap on the noflush(sbc)
+ * and fl(mmc) one bit field. This is the cause of the awkardly named
+ * pc_mod__fl_num and noflush__fl arguments to this function.  */
+int sg_ll_start_stop_unit(int sg_fd, bool immed, int pc_mod__fl_num,
+                          int power_cond, bool noflush__fl, bool loej,
+                          bool start, bool noisy, int verbose);
+
+/* Similar to sg_ll_start_stop_unit(). See note above about "_pt" suffix. */
+int sg_ll_start_stop_unit_pt(struct sg_pt_base * ptp, bool immed,
+                             int pc_mod__fl_num, int power_cond,
+                             bool noflush__fl, bool loej, bool start,
+                             bool noisy, int verbose);
+
+/* Invokes a SCSI SYNCHRONIZE CACHE (10) command. Return of 0 -> success,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_INVALID_OP -> cdb not supported,
+ * SG_LIB_CAT_IlLEGAL_REQ -> bad field in cdb
+ * SG_LIB_CAT_NOT_READY -> device not ready, -1 -> other failure */
+int sg_ll_sync_cache_10(int sg_fd, bool sync_nv, bool immed, int group,
+                        unsigned int lba, unsigned int count, bool noisy,
+                        int verbose);
+
+/* Invokes a SCSI TEST UNIT READY command.
+ * 'pack_id' is just for diagnostics, safe to set to 0.
+ * Return of 0 -> success, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready,
+ * SG_LIB_CAT_ABORTED_COMMAND, -1 -> other failure */
+int sg_ll_test_unit_ready(int sg_fd, int pack_id, bool noisy, int verbose);
+
+/* Similar to sg_ll_test_unit_ready(). See note above about "_pt" suffix. */
+int sg_ll_test_unit_ready_pt(struct sg_pt_base * ptp, int pack_id,
+                             bool noisy, int verbose);
+
+/* Invokes a SCSI TEST UNIT READY command.
+ * 'pack_id' is just for diagnostics, safe to set to 0.
+ * Looks for progress indicator if 'progress' non-NULL;
+ * if found writes value [0..65535] else write -1.
+ * Return of 0 -> success, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_ABORTED_COMMAND, SG_LIB_CAT_NOT_READY ->
+ * device not ready, -1 -> other failure */
+int sg_ll_test_unit_ready_progress(int sg_fd, int pack_id, int * progress,
+                                   bool noisy, int verbose);
+
+/* Similar to sg_ll_test_unit_ready_progress(). See note above about "_pt"
+ * suffix. */
+int sg_ll_test_unit_ready_progress_pt(struct sg_pt_base * ptp, int pack_id,
+                                     int * progress, bool noisy, int verbose);
+
+
+struct sg_simple_inquiry_resp {
+    uint8_t peripheral_qualifier;
+    uint8_t peripheral_type;
+    uint8_t byte_1;             /* was 'rmb' prior to version 1.39 */
+                                /* now rmb == !!(0x80 & byte_1) */
+    uint8_t version;            /* as per recent drafts: whole of byte 2 */
+    uint8_t byte_3;
+    uint8_t byte_5;
+    uint8_t byte_6;
+    uint8_t byte_7;
+    char vendor[9];             /* T10 field is 8 bytes, NUL char appended */
+    char product[17];
+    char revision[5];
+};
+
+/* Yields most of first 36 bytes of a standard INQUIRY (evpd==0) response.
+ * Returns 0 when successful, SG_LIB_CAT_INVALID_OP -> not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * a negated errno or -1 -> other errors */
+int sg_simple_inquiry(int sg_fd, struct sg_simple_inquiry_resp * inq_data,
+                      bool noisy, int verbose);
+
+/* Similar to sg_simple_inquiry(). See note above about "_pt" suffix. */
+int sg_simple_inquiry_pt(struct sg_pt_base * ptvp,
+                         struct sg_simple_inquiry_resp * inq_data, bool noisy,
+                         int verbose);
+
+/* MODE SENSE commands yield a response that has header then zero or more
+ * block descriptors followed by mode pages. In most cases users are
+ * interested in the first mode page. This function returns the (byte)
+ * offset of the start of the first mode page. Set mode_sense_6 to true for
+ * MODE SENSE (6) and false for MODE SENSE (10). Returns >= 0 is successful
+ * or -1 if failure. If there is a failure a message is written to err_buff
+ * if it is non-NULL and err_buff_len > 0. */
+int sg_mode_page_offset(const uint8_t * resp, int resp_len,
+                        bool mode_sense_6, char * err_buff, int err_buff_len);
+
+/* MODE SENSE commands yield a response that has header then zero or more
+ * block descriptors followed by mode pages. This functions returns the
+ * length (in bytes) of those three components. Note that the return value
+ * can exceed resp_len in which case the MODE SENSE command should be
+ * re-issued with a larger response buffer. If bd_lenp is non-NULL and if
+ * successful the block descriptor length (in bytes) is written to *bd_lenp.
+ * Set mode_sense_6 to true for MODE SENSE (6) and false for MODE SENSE (10)
+ * responses. Returns -1 if there is an error (e.g. response too short). */
+int sg_msense_calc_length(const uint8_t * resp, int resp_len,
+                          bool mode_sense_6, int * bd_lenp);
+
+/* Fetches current, changeable, default and/or saveable modes pages as
+ * indicated by pcontrol_arr for given pg_code and sub_pg_code. If
+ * mode6 is true then use MODE SENSE (6) else use MODE SENSE (10). If
+ * flexible true and mode data length seems wrong then try and
+ * fix (compensating hack for bad device or driver). pcontrol_arr
+ * should have 4 elements for output of current, changeable, default
+ * and saved values respectively. Each element should be NULL or
+ * at least mx_mpage_len bytes long.
+ * Return of 0 -> overall success, SG_LIB_CAT_INVALID_OP -> invalid opcode,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready,
+ * SG_LIB_CAT_MALFORMED -> bad response, -1 -> other failure.
+ * If success_mask pointer is not NULL then first zeros it. Then set bits
+ * 0, 1, 2 and/or 3 if the current, changeable, default and saved values
+ * respectively have been fetched. If error on current page
+ * then stops and returns that error; otherwise continues if an error is
+ * detected but returns the first error encountered.  */
+int sg_get_mode_page_controls(int sg_fd, bool mode6, int pg_code,
+                              int sub_pg_code, bool dbd, bool flexible,
+                              int mx_mpage_len, int * success_mask,
+                              void * pcontrol_arr[], int * reported_lenp,
+                              int verbose);
+
+/* Returns file descriptor >= 0 if successful. If error in Unix returns
+   negated errno. Implementation calls scsi_pt_open_device(). */
+int sg_cmds_open_device(const char * device_name, bool read_only, int verbose);
+
+/* Returns file descriptor >= 0 if successful. If error in Unix returns
+   negated errno. Implementation calls scsi_pt_open_flags(). */
+int sg_cmds_open_flags(const char * device_name, int flags, int verbose);
+
+/* Returns 0 if successful. If error in Unix returns negated errno.
+   Implementation calls scsi_pt_close_device(). */
+int sg_cmds_close_device(int device_fd);
+
+const char * sg_cmds_version();
+
+#define SG_NO_DATA_IN 0
+
+
+/* This is a helper function used by sg_cmds_* implementations after the
+ * call to the pass-through. pt_res is returned from do_scsi_pt(). If valid
+ * sense data is found it is decoded and output to sg_warnings_strm (def:
+ * stderr); depending on the 'noisy' and 'verbose' settings. Returns -2 for
+ * sense data (may not be fatal), -1 for failed, 0, or a positive number. If
+ * 'mx_di_len > 0' then asks pass-through for resid and returns
+ * (mx_di_len - resid); otherwise returns 0. So for data-in it should return
+ * the actual number of bytes received. For data-out (to device) or no data
+ * call with 'mx_di_len' set to 0 or less. If -2 returned then sense category
+ * output via 'o_sense_cat' pointer (if not NULL). Note that several sense
+ * categories also have data in bytes received; -2 is still returned. */
+int sg_cmds_process_resp(struct sg_pt_base * ptvp, const char * leadin,
+                         int pt_res, bool noisy, int verbose,
+                         int * o_sense_cat);
+
+/* NVMe devices use a different command set. This function will return true
+ * if the device associated with 'pvtp' is a NVME device, else it will
+ * return false (e.g. for SCSI devices). */
+bool sg_cmds_is_nvme(const struct sg_pt_base * ptvp);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/sg_cmds_extra.h b/include/sg_cmds_extra.h
new file mode 100644
index 0000000..6e24c6e
--- /dev/null
+++ b/include/sg_cmds_extra.h
@@ -0,0 +1,391 @@
+#ifndef SG_CMDS_EXTRA_H
+#define SG_CMDS_EXTRA_H
+
+/*
+ * Copyright (c) 2004-2018 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Note: all functions that have an 'int timeout_secs' argument will use
+ * that value if it is > 0. Otherwise they will set an internal default
+ * which is currently 60 seconds. This timeout is typically applied in the
+ * SCSI stack above the initiator. If it goes off then the SCSI command is
+ * aborted and there can be other unwelcome side effects. Note that some
+ * commands (e.g. FORMAT UNIT and the Third Party copy commands) can take
+ * a lot longer than the default timeout. */
+
+/* Functions with the "_pt" suffix ^^^ take a pointer to an object (derived
+ * from) sg_pt_base rather than an open file descriptor as their first
+ * argument. That object is assumed to be constructed and have a device file
+ * descriptor * associated with it. Caller is responsible for lifetime of
+ * ptp.
+ *    ^^^ apart from sg_ll_ata_pt() as 'pass-through' is part of its name. */
+
+struct sg_pt_base;
+
+
+/* Invokes a ATA PASS-THROUGH (12, 16 or 32) SCSI command (SAT). This is
+ * selected by the cdb_len argument that can take values of 12, 16 or 32
+ * only (else -1 is returned). The byte at offset 0 (and bytes 0 to 9
+ * inclusive for ATA PT(32)) pointed to be cdbp are ignored and apart from
+ * the control byte, the rest is copied into an internal cdb which is then
+ * sent to the device. The control byte is byte 11 for ATA PT(12), byte 15
+ * for ATA PT(16) and byte 1 for ATA PT(32). If timeout_secs <= 0 then the
+ * timeout is set to 60 seconds. For data in or out transfers set dinp or
+ * doutp, and dlen to the number of bytes to transfer. If dlen is zero then
+ * no data transfer is assumed. If sense buffer obtained then it is written
+ * to sensep, else sensep[0] is set to 0x0. If ATA return descriptor is
+ * obtained then written to ata_return_dp, else ata_return_dp[0] is set to
+ * 0x0. Either sensep or ata_return_dp (or both) may be NULL pointers.
+ * Returns SCSI status value (>= 0) or -1 if other error. Users are
+ * expected to check the sense buffer themselves. If available the data in
+ * resid is written to residp. Note in SAT-2 and later, fixed format sense
+ * data may be placed in *sensep in which case sensep[0]==0x70, prior to
+ * SAT-2 descriptor sense format was required (i.e. sensep[0]==0x72).
+ */
+int sg_ll_ata_pt(int sg_fd, const uint8_t * cdbp, int cdb_len,
+                 int timeout_secs,  void * dinp, void * doutp, int dlen,
+                 uint8_t * sensep, int max_sense_len, uint8_t * ata_return_dp,
+                 int max_ata_return_len, int * residp, int verbose);
+
+/* Invokes a FORMAT UNIT (SBC-3) command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Format unit not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure. Note that sg_ll_format_unit2() and
+ * sg_ll_format_unit_v2() are the same, both add the ffmt argument. */
+int sg_ll_format_unit(int sg_fd, int fmtpinfo, bool longlist, bool fmtdata,
+                      bool cmplist, int dlist_format, int timeout_secs,
+                      void * paramp, int param_len, bool noisy, int verbose);
+int sg_ll_format_unit2(int sg_fd, int fmtpinfo, bool longlist, bool fmtdata,
+                       bool cmplist, int dlist_format, int ffmt,
+                       int timeout_secs, void * paramp, int param_len,
+                       bool noisy, int verbose);
+int sg_ll_format_unit_v2(int sg_fd, int fmtpinfo, bool longlist, bool fmtdata,
+                         bool cmplist, int dlist_format, int ffmt,
+                         int timeout_secs, void * paramp, int param_len,
+                         bool noisy, int verbose);
+
+/* Invokes a SCSI GET LBA STATUS(16) or GET LBA STATUS(32) command (SBC).
+ * Returns 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> GET LBA STATUS(16 or 32) not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_NOT_READY -> device not ready, -1 -> other failure.
+ * sg_ll_get_lba_status() calls the 16 byte variant with rt=0 . */
+int sg_ll_get_lba_status(int sg_fd, uint64_t start_llba, void * resp,
+                         int alloc_len, bool noisy, int verbose);
+int sg_ll_get_lba_status16(int sg_fd, uint64_t start_llba, uint8_t rt,
+                           void * resp, int alloc_len, bool noisy,
+                           int verbose);
+int sg_ll_get_lba_status32(int sg_fd, uint64_t start_llba, uint32_t scan_len,
+                           uint32_t element_id, uint8_t rt,
+                           void * resp, int alloc_len, bool noisy,
+                           int verbose);
+
+/* Invokes a SCSI PERSISTENT RESERVE IN command (SPC). Returns 0
+ * when successful, SG_LIB_CAT_INVALID_OP if command not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ if field in cdb not supported,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND, else -1 */
+int sg_ll_persistent_reserve_in(int sg_fd, int rq_servact, void * resp,
+                                int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI PERSISTENT RESERVE OUT command (SPC). Returns 0
+ * when successful, SG_LIB_CAT_INVALID_OP if command not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ if field in cdb not supported,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND, else -1 */
+int sg_ll_persistent_reserve_out(int sg_fd, int rq_servact, int rq_scope,
+                                 unsigned int rq_type, void * paramp,
+                                 int param_len, bool noisy, int verbose);
+
+/* Invokes a SCSI READ BLOCK LIMITS command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> READ BLOCK LIMITS not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_NOT_READY (shouldn't happen), -1 -> other failure */
+int sg_ll_read_block_limits(int sg_fd, void * resp, int mx_resp_len,
+                            bool noisy, int verbose);
+int sg_ll_read_block_limits_v2(int sg_fd, bool mloi, void * resp,
+                               int mx_resp_len, int * residp, bool noisy,
+                               int verbose);
+
+/* Invokes a SCSI READ BUFFER command (SPC). Return of 0 ->
+ * success, SG_LIB_CAT_INVALID_OP -> invalid opcode,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_read_buffer(int sg_fd, int mode, int buffer_id, int buffer_offset,
+                      void * resp, int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI READ DEFECT DATA (10) command (SBC). Return of 0 ->
+ * success, SG_LIB_CAT_INVALID_OP -> invalid opcode,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_read_defect10(int sg_fd, bool req_plist, bool req_glist,
+                        int dl_format, void * resp, int mx_resp_len,
+                        bool noisy, int verbose);
+
+/* Invokes a SCSI READ LONG (10) command (SBC). Note that 'xfer_len'
+ * is in bytes. Returns 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> READ LONG(10) not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb,
+ * SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO -> bad field in cdb, with info
+ * field written to 'offsetp', SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_read_long10(int sg_fd, bool pblock, bool correct, unsigned int lba,
+                      void * resp, int xfer_len, int * offsetp, bool noisy,
+                      int verbose);
+
+/* Invokes a SCSI READ LONG (16) command (SBC). Note that 'xfer_len'
+ * is in bytes. Returns 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> READ LONG(16) not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb,
+ * SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO -> bad field in cdb, with info
+ * field written to 'offsetp', SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ *  -1 -> other failure */
+int sg_ll_read_long16(int sg_fd, bool pblock, bool correct, uint64_t llba,
+                      void * resp, int xfer_len, int * offsetp, bool noisy,
+                      int verbose);
+
+/* Invokes a SCSI READ MEDIA SERIAL NUMBER command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Read media serial number not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_read_media_serial_num(int sg_fd, void * resp, int mx_resp_len,
+                                bool noisy, int verbose);
+
+/* Invokes a SCSI REASSIGN BLOCKS command.  Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> invalid opcode, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_NOT_READY -> device not ready, -1 -> other failure */
+int sg_ll_reassign_blocks(int sg_fd, bool longlba, bool longlist,
+                          void * paramp, int param_len, bool noisy,
+                          int verbose);
+
+/* Invokes a SCSI RECEIVE DIAGNOSTIC RESULTS command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Receive diagnostic results not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_receive_diag(int sg_fd, bool pcv, int pg_code, void * resp,
+                       int mx_resp_len, bool noisy, int verbose);
+
+/* Same as sg_ll_receive_diag() but with added timeout_secs and residp
+ * arguments. Adds the ability to set the command abort timeout
+ * and the ability to report the residual count. If timeout_secs is zero
+ * or less the default command abort timeout (60 seconds) is used.
+ * If residp is non-NULL then the residual value is written where residp
+ * points. A residual value of 0 implies mx_resp_len bytes have be written
+ * where resp points. If the residual value equals mx_resp_len then no
+ * bytes have been written. */
+int sg_ll_receive_diag_v2(int sg_fd, bool pcv, int pg_code, void * resp,
+                          int mx_resp_len, int timeout_secs, int * residp,
+                          bool noisy, int verbose);
+
+int sg_ll_receive_diag_pt(struct sg_pt_base * ptp, bool pcv, int pg_code,
+                          void * resp, int mx_resp_len, int timeout_secs,
+                          int * residp, bool noisy, int verbose);
+
+/* Invokes a SCSI REPORT IDENTIFYING INFORMATION command. This command was
+ * called REPORT DEVICE IDENTIFIER prior to spc4r07. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Report identifying information not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_report_id_info(int sg_fd, int itype, void * resp, int max_resp_len,
+                         bool noisy, int verbose);
+
+/* Invokes a SCSI REPORT TARGET PORT GROUPS command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Report Target Port Groups not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_UNIT_ATTENTION, -1 -> other failure */
+int sg_ll_report_tgt_prt_grp(int sg_fd, void * resp, int mx_resp_len,
+                             bool noisy, int verbose);
+int sg_ll_report_tgt_prt_grp2(int sg_fd, void * resp, int mx_resp_len,
+                              bool extended, bool noisy, int verbose);
+
+/* Invokes a SCSI SET TARGET PORT GROUPS command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Report Target Port Groups not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_UNIT_ATTENTION, -1 -> other failure */
+int sg_ll_set_tgt_prt_grp(int sg_fd, void * paramp, int param_len, bool noisy,
+                          int verbose);
+
+/* Invokes a SCSI REPORT REFERRALS command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Report Referrals not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_UNIT_ATTENTION, -1 -> other failure */
+int sg_ll_report_referrals(int sg_fd, uint64_t start_llba, bool one_seg,
+                           void * resp, int mx_resp_len, bool noisy,
+                           int verbose);
+
+/* Invokes a SCSI SEND DIAGNOSTIC command. Foreground, extended self tests can
+ * take a long time, if so set long_duration flag in which case the timeout
+ * is set to 7200 seconds; if the value of long_duration is > 7200 then that
+ * value is taken as the timeout value in seconds. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Send diagnostic not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_send_diag(int sg_fd, int st_code, bool pf_bit, bool st_bit,
+                    bool devofl_bit, bool unitofl_bit, int long_duration,
+                    void * paramp, int param_len, bool noisy, int verbose);
+
+int sg_ll_send_diag_pt(struct sg_pt_base * ptp, int st_code, bool pf_bit,
+                       bool st_bit, bool devofl_bit, bool unitofl_bit,
+                       int long_duration, void * paramp, int param_len,
+                       bool noisy, int verbose);
+
+/* Invokes a SCSI SET IDENTIFYING INFORMATION command. This command was
+ * called SET DEVICE IDENTIFIER prior to spc4r07. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Set identifying information not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_set_id_info(int sg_fd, int itype, void * paramp, int param_len,
+                      bool noisy, int verbose);
+
+/* Invokes a SCSI UNMAP (SBC-3) command. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> command not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_UNIT_ATTENTION, -1 -> other failure */
+int sg_ll_unmap(int sg_fd, int group_num, int timeout_secs, void * paramp,
+                int param_len, bool noisy, int verbose);
+/* Invokes a SCSI UNMAP (SBC-3) command. Version 2 adds anchor field
+ * (sbc3r22). Otherwise same as sg_ll_unmap() . */
+int sg_ll_unmap_v2(int sg_fd, bool anchor, int group_num, int timeout_secs,
+                   void * paramp, int param_len, bool noisy, int verbose);
+
+/* Invokes a SCSI VERIFY (10) command (SBC and MMC).
+ * Note that 'veri_len' is in blocks while 'data_out_len' is in bytes.
+ * Returns of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Verify(10) not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_MEDIUM_HARD -> medium or hardware error, no valid info,
+ * SG_LIB_CAT_MEDIUM_HARD_WITH_INFO -> as previous, with valid info,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_MISCOMPARE, -1 -> other failure */
+int sg_ll_verify10(int sg_fd, int vrprotect, bool dpo, int bytechk,
+                   unsigned int lba, int veri_len, void * data_out,
+                   int data_out_len, unsigned int * infop, bool noisy,
+                   int verbose);
+
+/* Invokes a SCSI VERIFY (16) command (SBC).
+ * Note that 'veri_len' is in blocks while 'data_out_len' is in bytes.
+ * Returns of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Verify(16) not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_MEDIUM_HARD -> medium or hardware error, no valid info,
+ * SG_LIB_CAT_MEDIUM_HARD_WITH_INFO -> as previous, with valid info,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_MISCOMPARE, -1 -> other failure */
+int sg_ll_verify16(int sg_fd, int vrprotect, bool dpo, int bytechk,
+                   uint64_t llba, int veri_len, int group_num,
+                   void * data_out, int data_out_len, uint64_t * infop,
+                   bool noisy, int verbose);
+
+/* Invokes a SCSI WRITE BUFFER command (SPC). Return of 0 ->
+ * success, SG_LIB_CAT_INVALID_OP -> invalid opcode,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_write_buffer(int sg_fd, int mode, int buffer_id, int buffer_offset,
+                       void * paramp, int param_len, bool noisy, int verbose);
+
+/* Invokes a SCSI WRITE BUFFER command (SPC). Return of 0 ->
+ * success, SG_LIB_CAT_INVALID_OP -> invalid opcode,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure. Adds mode specific field (spc4r32) and timeout
+ *  to command abort to override default of 60 seconds. If timeout_secs is
+ *  0 or less then the default timeout is used instead. */
+int
+sg_ll_write_buffer_v2(int sg_fd, int mode, int m_specific, int buffer_id,
+                      uint32_t buffer_offset, void * paramp,
+                      uint32_t param_len, int timeout_secs, bool noisy,
+                      int verbose);
+
+/* Invokes a SCSI WRITE LONG (10) command (SBC). Note that 'xfer_len'
+ * is in bytes. Returns 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> WRITE LONG(10) not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb,
+ * SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO -> bad field in cdb, with info
+ * field written to 'offsetp', SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_write_long10(int sg_fd, bool cor_dis, bool wr_uncor, bool pblock,
+                       unsigned int lba, void * data_out, int xfer_len,
+                       int * offsetp, bool noisy, int verbose);
+
+/* Invokes a SCSI WRITE LONG (16) command (SBC). Note that 'xfer_len'
+ * is in bytes. Returns 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> WRITE LONG(16) not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb,
+ * SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO -> bad field in cdb, with info
+ * field written to 'offsetp', SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_write_long16(int sg_fd, bool cor_dis, bool wr_uncor, bool pblock,
+                       uint64_t llba, void * data_out, int xfer_len,
+                       int * offsetp, bool noisy, int verbose);
+
+/* Invokes a SPC-3 SCSI RECEIVE COPY RESULTS command. In SPC-4 this function
+ * supports all service action variants of the THIRD-PARTY COPY IN opcode.
+ * SG_LIB_CAT_INVALID_OP -> Receive copy results not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_receive_copy_results(int sg_fd, int sa, int list_id, void * resp,
+                               int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI EXTENDED COPY(LID1) command. For EXTENDED COPY(LID4)
+ * including POPULATE TOKEN and WRITE USING TOKEN use
+ * sg_ll_3party_copy_out().  Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Extended copy not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_extended_copy(int sg_fd, void * paramp, int param_len, bool noisy,
+                        int verbose);
+
+/* Handles various service actions associated with opcode 0x83 which is
+ * called THIRD PARTY COPY OUT. These include the EXTENDED COPY(LID4),
+ * POPULATE TOKEN and WRITE USING TOKEN commands. Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> opcode 0x83 not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_3party_copy_out(int sg_fd, int sa, unsigned int list_id,
+                          int group_num, int timeout_secs, void * paramp,
+                          int param_len, bool noisy, int verbose);
+
+/* Invokes a SCSI PRE-FETCH(10), PRE-FETCH(16) or SEEK(10) command (SBC).
+ * Returns 0 -> success, 25 (SG_LIB_CAT_CONDITION_MET), various SG_LIB_CAT_*
+ * positive values or -1 -> other errors. Note that CONDITION MET status
+ * is returned when immed=true and num_blocks can fit in device's cache,
+ * somewaht strangely, GOOD status (return 0) is returned if num_blocks
+ * cannot fit in device's cache. If do_seek10==true then does a SEEK(10)
+ * command with given lba, if that LBA is < 2**32 . Unclear what SEEK(10)
+ * does, assume it is like PRE-FETCH. If timeout_secs is 0 (or less) then
+ * use DEF_PT_TIMEOUT (60 seconds) as command timeout. */
+int sg_ll_pre_fetch_x(int sg_fd, bool do_seek10, bool cdb16, bool immed,
+                      uint64_t lba, uint32_t num_blocks, int group_num,
+                      int timeout_secs, bool noisy, int verbose);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/sg_cmds_mmc.h b/include/sg_cmds_mmc.h
new file mode 100644
index 0000000..c9b7d73
--- /dev/null
+++ b/include/sg_cmds_mmc.h
@@ -0,0 +1,54 @@
+#ifndef SG_CMDS_MMC_H
+#define SG_CMDS_MMC_H
+
+/*
+ * Copyright (c) 2008-2017 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/* Invokes a SCSI GET CONFIGURATION command (MMC-3...6).
+ * Returns 0 when successful, SG_LIB_CAT_INVALID_OP if command not
+ * supported, SG_LIB_CAT_ILLEGAL_REQ if field in cdb not supported,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND, else -1 */
+int sg_ll_get_config(int sg_fd, int rt, int starting, void * resp,
+                     int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI GET PERFORMANCE command (MMC-3...6).
+ * Returns 0 when successful, SG_LIB_CAT_INVALID_OP if command not
+ * supported, SG_LIB_CAT_ILLEGAL_REQ if field in cdb not supported,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND, else -1 */
+int sg_ll_get_performance(int sg_fd, int data_type, unsigned int starting_lba,
+                          int max_num_desc, int type, void * resp,
+                          int mx_resp_len, bool noisy, int verbose);
+
+/* Invokes a SCSI SET CD SPEED command (MMC).
+ * Return of 0 -> success, SG_LIB_CAT_INVALID_OP -> command not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int sg_ll_set_cd_speed(int sg_fd, int rot_control, int drv_read_speed,
+                       int drv_write_speed, bool noisy, int verbose);
+
+/* Invokes a SCSI SET STREAMING command (MMC). Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Set Streaming not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_NOT_READY -> device not ready,
+ * -1 -> other failure */
+int sg_ll_set_streaming(int sg_fd, int type, void * paramp, int param_len,
+                        bool noisy, int verbose);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/sg_io_linux.h b/include/sg_io_linux.h
new file mode 100644
index 0000000..7b3567c
--- /dev/null
+++ b/include/sg_io_linux.h
@@ -0,0 +1,202 @@
+#ifndef SG_IO_LINUX_H
+#define SG_IO_LINUX_H
+
+/*
+ * Copyright (c) 2004-2020 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Version 1.08 [20201102]
+ */
+
+/*
+ * This header file contains Linux specific information related to the SCSI
+ * command pass through in the SCSI generic (sg) driver and the Linux
+ * block layer.
+ */
+
+#include "sg_lib.h"
+#include "sg_linux_inc.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* host_bytes: DID_* are Linux SCSI result (a 32 bit variable) bits 16:23 */
+#ifndef DID_OK
+#define DID_OK 0x00
+#endif
+#ifndef DID_NO_CONNECT
+#define DID_NO_CONNECT 0x01     /* Unable to connect before timeout */
+#define DID_BUS_BUSY 0x02       /* Bus remain busy until timeout */
+#define DID_TIME_OUT 0x03       /* Timed out for some other reason */
+#define DID_BAD_TARGET 0x04     /* Bad target (id?) */
+#define DID_ABORT 0x05          /* Told to abort for some other reason */
+#define DID_PARITY 0x06         /* Parity error (on SCSI bus) */
+#define DID_ERROR 0x07          /* Internal error */
+#define DID_RESET 0x08          /* Reset by somebody */
+#define DID_BAD_INTR 0x09       /* Received an unexpected interrupt */
+#define DID_PASSTHROUGH 0x0a    /* Force command past mid-level */
+#define DID_SOFT_ERROR 0x0b     /* The low-level driver wants a retry */
+#endif
+#ifndef DID_IMM_RETRY
+#define DID_IMM_RETRY 0x0c      /* Retry without decrementing retry count  */
+#endif
+#ifndef DID_REQUEUE
+#define DID_REQUEUE 0x0d        /* Requeue command (no immediate retry) also
+                                 * without decrementing the retry count    */
+#endif
+#ifndef DID_TRANSPORT_DISRUPTED
+#define DID_TRANSPORT_DISRUPTED 0xe
+#endif
+#ifndef DID_TRANSPORT_FAILFAST
+#define DID_TRANSPORT_FAILFAST 0xf
+#endif
+#ifndef DID_TARGET_FAILURE
+#define DID_TARGET_FAILURE 0x10
+#endif
+#ifndef DID_NEXUS_FAILURE
+#define DID_NEXUS_FAILURE 0x11
+#endif
+
+/* These defines are to isolate applications from kernel define changes */
+#define SG_LIB_DID_OK           DID_OK
+#define SG_LIB_DID_NO_CONNECT   DID_NO_CONNECT
+#define SG_LIB_DID_BUS_BUSY     DID_BUS_BUSY
+#define SG_LIB_DID_TIME_OUT     DID_TIME_OUT
+#define SG_LIB_DID_BAD_TARGET   DID_BAD_TARGET
+#define SG_LIB_DID_ABORT        DID_ABORT
+#define SG_LIB_DID_PARITY       DID_PARITY
+#define SG_LIB_DID_ERROR        DID_ERROR
+#define SG_LIB_DID_RESET        DID_RESET
+#define SG_LIB_DID_BAD_INTR     DID_BAD_INTR
+#define SG_LIB_DID_PASSTHROUGH  DID_PASSTHROUGH
+#define SG_LIB_DID_SOFT_ERROR   DID_SOFT_ERROR
+#define SG_LIB_DID_IMM_RETRY    DID_IMM_RETRY
+#define SG_LIB_DID_REQUEUE      DID_REQUEUE
+#define SG_LIB_TRANSPORT_DISRUPTED      DID_TRANSPORT_DISRUPTED
+#define SG_LIB_DID_TRANSPORT_FAILFAST   DID_TRANSPORT_FAILFAST
+#define SG_LIB_DID_TARGET_FAILURE       DID_TARGET_FAILURE
+#define SG_LIB_DID_NEXUS_FAILURE        DID_NEXUS_FAILURE
+
+/* DRIVER_* are Linux SCSI result (a 32 bit variable) bits 24:27 */
+#ifndef DRIVER_OK
+#define DRIVER_OK 0x00
+#endif
+#ifndef DRIVER_BUSY
+#define DRIVER_BUSY 0x01
+#define DRIVER_SOFT 0x02
+#define DRIVER_MEDIA 0x03
+#define DRIVER_ERROR 0x04
+#define DRIVER_INVALID 0x05
+#define DRIVER_TIMEOUT 0x06
+#define DRIVER_HARD 0x07
+#define DRIVER_SENSE 0x08       /* Sense_buffer has been set */
+
+/* SUGGEST_* are Linux SCSI result (a 32 bit variable) bits 28:31 */
+/* N.B. the SUGGEST_* codes are no longer used in Linux and are only kept
+ * to stop compilation breakages.
+ * Following "suggests" are "or-ed" with one of previous 8 entries */
+#define SUGGEST_RETRY 0x10
+#define SUGGEST_ABORT 0x20
+#define SUGGEST_REMAP 0x30
+#define SUGGEST_DIE 0x40
+#define SUGGEST_SENSE 0x80
+#define SUGGEST_IS_OK 0xff
+#endif
+
+#ifndef DRIVER_MASK
+#define DRIVER_MASK 0x0f
+#endif
+#ifndef SUGGEST_MASK
+#define SUGGEST_MASK 0xf0
+#endif
+
+/* These defines are to isolate applications from kernel define changes */
+#define SG_LIB_DRIVER_OK        DRIVER_OK
+#define SG_LIB_DRIVER_BUSY      DRIVER_BUSY
+#define SG_LIB_DRIVER_SOFT      DRIVER_SOFT
+#define SG_LIB_DRIVER_MEDIA     DRIVER_MEDIA
+#define SG_LIB_DRIVER_ERROR     DRIVER_ERROR
+#define SG_LIB_DRIVER_INVALID   DRIVER_INVALID
+#define SG_LIB_DRIVER_TIMEOUT   DRIVER_TIMEOUT
+#define SG_LIB_DRIVER_HARD      DRIVER_HARD
+#define SG_LIB_DRIVER_SENSE     DRIVER_SENSE
+
+
+/* N.B. the SUGGEST_* codes are no longer used in Linux and are only kept
+ * to stop compilation breakages. */
+#define SG_LIB_SUGGEST_RETRY    SUGGEST_RETRY
+#define SG_LIB_SUGGEST_ABORT    SUGGEST_ABORT
+#define SG_LIB_SUGGEST_REMAP    SUGGEST_REMAP
+#define SG_LIB_SUGGEST_DIE      SUGGEST_DIE
+#define SG_LIB_SUGGEST_SENSE    SUGGEST_SENSE
+#define SG_LIB_SUGGEST_IS_OK    SUGGEST_IS_OK
+#define SG_LIB_DRIVER_MASK      DRIVER_MASK
+#define SG_LIB_SUGGEST_MASK     SUGGEST_MASK
+
+void sg_print_masked_status(int masked_status);
+void sg_print_host_status(int host_status);
+void sg_print_driver_status(int driver_status);
+
+/* sg_chk_n_print() returns 1 quietly if there are no errors/warnings
+ * else it prints errors/warnings (prefixed by 'leadin') to
+ * 'sg_warnings_fd' and returns 0. raw_sinfo indicates whether the
+ * raw sense buffer (in ASCII hex) should be printed. */
+int sg_chk_n_print(const char * leadin, int masked_status, int host_status,
+                   int driver_status, const uint8_t * sense_buffer,
+                   int sb_len, bool raw_sinfo);
+
+/* The following function declaration is for the sg version 3 driver. */
+struct sg_io_hdr;
+
+/* sg_chk_n_print3() returns 1 quietly if there are no errors/warnings;
+ * else it prints errors/warnings (prefixed by 'leadin') to
+ * 'sg_warnings_fd' and returns 0. For sg_io_v4 interface use
+ * sg_linux_sense_print() instead. */
+int sg_chk_n_print3(const char * leadin, struct sg_io_hdr * hp,
+                    bool raw_sinfo);
+
+/* Returns 1 if no errors found and thus nothing printed; otherwise
+ * prints error/warning (prefix by 'leadin') to stderr (pr2ws) and
+ * returns 0. */
+int sg_linux_sense_print(const char * leadin, int scsi_status,
+                         int host_status, int driver_status,
+                         const uint8_t * sense_buffer, int sb_len,
+                         bool raw_sinfo);
+
+/* Calls sg_scsi_normalize_sense() after obtaining the sense buffer and
+ * its length from the struct sg_io_hdr pointer. If these cannot be
+ * obtained, false is returned. For sg_io_v4 interface use
+ * sg_scsi_normalize_sense() function instead [see sg_lib.h].  */
+bool sg_normalize_sense(const struct sg_io_hdr * hp,
+                        struct sg_scsi_sense_hdr * sshp);
+
+/* Returns SG_LIB_CAT_* value. */
+int sg_err_category(int masked_status, int host_status, int driver_status,
+                    const uint8_t * sense_buffer, int sb_len);
+
+/* Returns SG_LIB_CAT_* value. */
+int sg_err_category_new(int scsi_status, int host_status, int driver_status,
+                        const uint8_t * sense_buffer, int sb_len);
+
+/* The following function declaration is for the sg version 3 driver. for
+ * sg_io_v4 interface use sg_err_category_new() function instead */
+int sg_err_category3(struct sg_io_hdr * hp);
+
+
+/* Note about SCSI status codes found in older versions of Linux.
+ * Linux has traditionally used a 1 bit right shifted and masked
+ * version of SCSI standard status codes. Now CHECK_CONDITION
+ * and friends (in <scsi/scsi.h>) are deprecated. */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/sg_lib.h b/include/sg_lib.h
new file mode 100644
index 0000000..41545ee
--- /dev/null
+++ b/include/sg_lib.h
@@ -0,0 +1,813 @@
+#ifndef SG_LIB_H
+#define SG_LIB_H
+
+/*
+ * Copyright (c) 2004-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ *
+ * On 5th October 2004 a FreeBSD license was added to this file.
+ * The intention is to keep this file and the related sg_lib.c file
+ * as open source and encourage their unencumbered use.
+ *
+ * Current version number of this library is in the sg_lib_data.c file and
+ * can be accessed with the sg_lib_version() function.
+ */
+
+
+/*
+ * This header file contains defines and function declarations that may
+ * be useful to applications that communicate with devices that use a
+ * SCSI command set. These command sets have names like SPC-4, SBC-3,
+ * SSC-3, SES-2 and draft standards defining them can be found at
+ * https://www.t10.org . Virtually all devices in the Linux SCSI subsystem
+ * utilize SCSI command sets. Many devices in other Linux device subsystems
+ * utilize SCSI command sets either natively or via emulation (e.g. a
+ * SATA disk in a USB enclosure).
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* SCSI Peripheral Device Types (PDT) [5 bit field] */
+#define PDT_DISK 0x0    /* direct access block device (disk) */
+#define PDT_TAPE 0x1    /* sequential access device (magnetic tape) */
+#define PDT_PRINTER 0x2 /* printer device (see SSC-1) */
+#define PDT_PROCESSOR 0x3       /* processor device (e.g. SAFTE device) */
+#define PDT_WO 0x4      /* write once device (some optical disks) */
+#define PDT_MMC 0x5     /* CD/DVD/BD (multi-media) */
+#define PDT_SCANNER 0x6 /* obsolete */
+#define PDT_OPTICAL 0x7 /* optical memory device (some optical disks) */
+#define PDT_MCHANGER 0x8        /* media changer device (e.g. tape robot) */
+#define PDT_COMMS 0x9   /* communications device (obsolete) */
+#define PDT_SAC 0xc     /* storage array controller device */
+#define PDT_SES 0xd     /* SCSI Enclosure Services (SES) device */
+#define PDT_RBC 0xe     /* Reduced Block Commands (simplified PDT_DISK) */
+#define PDT_OCRW 0xf    /* optical card read/write device */
+#define PDT_BCC 0x10    /* bridge controller commands */
+#define PDT_OSD 0x11    /* Object Storage Device (OSD) */
+#define PDT_ADC 0x12    /* Automation/drive commands (ADC) */
+#define PDT_SMD 0x13    /* Security Manager Device (SMD) */
+#define PDT_ZBC 0x14    /* Zoned Block Commands (ZBC) */
+#define PDT_WLUN 0x1e   /* Well known logical unit (WLUN) */
+#define PDT_UNKNOWN 0x1f        /* Unknown or no device type */
+#define PDT_MASK 0x1f   /* For byte 0 of INQUIRY response */
+#define PDT_MAX 0x1f
+
+#define GRPNUM_MASK 0x3f
+
+/* ZBC disks use either PDT_ZBC (if 'host managed') or PDT_DISK .
+ * So squeeze two PDTs into one integer. Use sg_pdt_s_eq() to compare.
+ * N.B. Must not use PDT_DISK as upper */
+#define PDT_DISK_ZBC (PDT_DISK | (PDT_ZBC << 8))
+#define PDT_ALL (-1)    /* for common to all PDTs */
+#define PDT_LOWER_MASK 0xff
+#define PDT_UPPER_MASK (~PDT_LOWER_MASK)
+
+#ifndef SAM_STAT_GOOD
+/* The SCSI status codes as found in SAM-4 at www.t10.org */
+#define SAM_STAT_GOOD 0x0
+#define SAM_STAT_CHECK_CONDITION 0x2
+#define SAM_STAT_CONDITION_MET 0x4                /* this is not an error */
+#define SAM_STAT_BUSY 0x8
+#define SAM_STAT_INTERMEDIATE 0x10                /* obsolete in SAM-4 */
+#define SAM_STAT_INTERMEDIATE_CONDITION_MET 0x14  /* obsolete in SAM-4 */
+#define SAM_STAT_RESERVATION_CONFLICT 0x18
+#define SAM_STAT_COMMAND_TERMINATED 0x22          /* obsolete in SAM-3 */
+#define SAM_STAT_TASK_SET_FULL 0x28
+#define SAM_STAT_ACA_ACTIVE 0x30
+#define SAM_STAT_TASK_ABORTED 0x40
+#endif
+
+/* The SCSI sense key codes as found in SPC-4 at www.t10.org */
+#define SPC_SK_NO_SENSE 0x0
+#define SPC_SK_RECOVERED_ERROR 0x1
+#define SPC_SK_NOT_READY 0x2
+#define SPC_SK_MEDIUM_ERROR 0x3
+#define SPC_SK_HARDWARE_ERROR 0x4
+#define SPC_SK_ILLEGAL_REQUEST 0x5
+#define SPC_SK_UNIT_ATTENTION 0x6
+#define SPC_SK_DATA_PROTECT 0x7
+#define SPC_SK_BLANK_CHECK 0x8
+#define SPC_SK_VENDOR_SPECIFIC 0x9
+#define SPC_SK_COPY_ABORTED 0xa
+#define SPC_SK_ABORTED_COMMAND 0xb
+#define SPC_SK_RESERVED 0xc
+#define SPC_SK_VOLUME_OVERFLOW 0xd
+#define SPC_SK_MISCOMPARE 0xe
+#define SPC_SK_COMPLETED 0xf
+
+/* Transport protocol identifiers or just Protocol identifiers */
+#define TPROTO_FCP 0
+#define TPROTO_SPI 1
+#define TPROTO_SSA 2
+#define TPROTO_1394 3
+#define TPROTO_SRP 4            /* SCSI over RDMA */
+#define TPROTO_ISCSI 5
+#define TPROTO_SAS 6
+#define TPROTO_ADT 7
+#define TPROTO_ATA 8
+#define TPROTO_UAS 9            /* USB attached SCSI */
+#define TPROTO_SOP 0xa          /* SCSI over PCIe */
+#define TPROTO_PCIE 0xb         /* includes NVMe */
+#define TPROTO_NONE 0xf
+
+/* SCSI Feature Sets (sfs) */
+#define SCSI_FS_SPC_DISCOVERY_2016 0x1
+#define SCSI_FS_SBC_BASE_2010 0x102
+#define SCSI_FS_SBC_BASE_2016 0x101
+#define SCSI_FS_SBC_BASIC_PROV_2016 0x103
+#define SCSI_FS_SBC_DRIVE_MAINT_2016 0x104
+#define SCSI_FS_ZBC_HOST_AWARE_2020 0x300
+#define SCSI_FS_ZBC_HOST_MANAGED_2020 0x301
+#define SCSI_FS_ZBC_DOMAINS_REALMS_2020 0x302
+
+/* Often SCSI responses use the highest integer that can fit in a field
+ * to indicate "unbounded" or limit does not apply. Sometimes represented
+ * in output as "-1" for brevity */
+#define SG_LIB_UNBOUNDED_16BIT 0xffff
+#define SG_LIB_UNBOUNDED_32BIT 0xffffffffU
+#define SG_LIB_UNBOUNDED_64BIT 0xffffffffffffffffULL
+
+#if (__STDC_VERSION__ >= 199901L)  /* C99 or later */
+    typedef uintptr_t sg_uintptr_t;
+#else
+    typedef unsigned long sg_uintptr_t;
+#endif
+
+/* Borrowed from Linux kernel; no check that 'arr' actually is one */
+#define SG_ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+
+/* Doesn't seem to be a common C and C++ technique for clearing an
+ * aggregrate (e.g. a struct instance) on the stack. Hence this hack: */
+#ifdef __cplusplus
+#define SG_C_CPP_ZERO_INIT {}
+#else
+#define SG_C_CPP_ZERO_INIT ={0}
+#endif
+
+
+/* The format of the version string is like this: "2.26 20170906" */
+const char * sg_lib_version();
+
+/* Returns length of SCSI command given the opcode (first byte).
+ * Yields the wrong answer for variable length commands (opcode=0x7f)
+ * and potentially some vendor specific commands. */
+int sg_get_command_size(uint8_t cdb_byte0);
+
+/* Command name given pointer to the cdb. Certain command names
+ * depend on peripheral type (give 0 or -1 if unknown). Places command
+ * name into buff and will write no more than buff_len bytes. */
+void sg_get_command_name(const uint8_t * cdbp, int peri_type, int buff_len,
+                         char * buff);
+
+/* Command name given only the first byte (byte 0) of a cdb and
+ * peripheral type (give 0 or -1 if unknown). */
+void sg_get_opcode_name(uint8_t cdb_byte0, int peri_type, int buff_len,
+                        char * buff);
+
+/* Command name given opcode (byte 0), service action and peripheral type.
+ * If no service action give 0, if unknown peripheral type give 0 or -1 . */
+void sg_get_opcode_sa_name(uint8_t cdb_byte0, int service_action,
+                           int peri_type, int buff_len, char * buff);
+
+/* Fetch NVMe command name given first byte (byte offset 0 in 64 byte
+ * command) of command. Gets Admin NVMe command name if 'admin' is true
+ * (e.g. opcode=0x6 -> Identify), otherwise gets NVM command set name
+ * (e.g. opcode=0 -> Flush). Returns 'buff'. */
+char * sg_get_nvme_opcode_name(uint8_t cmd_byte0, bool admin, int buff_len,
+                               char * buff);
+
+/* Fetch scsi status string. */
+void sg_get_scsi_status_str(int scsi_status, int buff_len, char * buff);
+
+/* This is a slightly stretched SCSI sense "descriptor" format header.
+ * The addition is to allow the 0x70 and 0x71 response codes. The idea
+ * is to place the salient data of both "fixed" and "descriptor" sense
+ * format into one structure to ease application processing.
+ * The original sense buffer should be kept around for those cases
+ * in which more information is required (e.g. the LBA of a MEDIUM ERROR). */
+struct sg_scsi_sense_hdr {
+    uint8_t response_code; /* permit: 0x0, 0x70, 0x71, 0x72, 0x73 */
+    uint8_t sense_key;
+    uint8_t asc;
+    uint8_t ascq;
+    uint8_t byte4;      /* descriptor: SDAT_OVFL; fixed: lower three ... */
+    uint8_t byte5;      /* ... bytes of INFO field */
+    uint8_t byte6;
+    uint8_t additional_length;  /* zero for fixed format sense data */
+};
+
+/* The '_is_good()' returns true when status is SAM_STAT_GOOD or
+ * SAM_STAT_CONDITION_MET, returns false otherwise. Ignores bit 0. The
+ * '_is_bad() variant is the logical inverse. */
+bool sg_scsi_status_is_good(int sstatus);
+bool sg_scsi_status_is_bad(int sstatus);
+
+/* Maps the salient data from a sense buffer which is in either fixed or
+ * descriptor format into a structure mimicking a descriptor format
+ * header (i.e. the first 8 bytes of sense descriptor format).
+ * If zero response code returns false. Otherwise returns true and if 'sshp'
+ * is non-NULL then zero all fields and then set the appropriate fields in
+ * that structure. sshp::additional_length is always 0 for response
+ * codes 0x70 and 0x71 (fixed format). */
+bool sg_scsi_normalize_sense(const uint8_t * sensep, int sense_len,
+                             struct sg_scsi_sense_hdr * sshp);
+
+/* Attempt to find the first SCSI sense data descriptor that matches the
+ * given 'desc_type'. If found return pointer to start of sense data
+ * descriptor; otherwise (including fixed format sense data) returns NULL. */
+const uint8_t * sg_scsi_sense_desc_find(const uint8_t * sensep, int sense_len,
+                                        int desc_type);
+
+/* Get sense key from sense buffer. If successful returns a sense key value
+ * between 0 and 15. If sense buffer cannot be decode, returns -1 . */
+int sg_get_sense_key(const uint8_t * sensep, int sense_len);
+
+/* Yield string associated with sense_key value. Returns 'buff'. */
+char * sg_get_sense_key_str(int sense_key, int buff_len, char * buff);
+
+/* Yield string associated with ASC/ASCQ values. Returns 'buff'. Prefixes
+ * any valid additional sense found with "Additional sense: ". */
+char * sg_get_asc_ascq_str(int asc, int ascq, int buff_len, char * buff);
+
+/* Same as sg_get_asc_ascq_str() when add_sense_leadin is true. When it is
+ * false this function does _not_ prefix any valid additional sense found
+ * with "Additional sense: ". */
+char * sg_get_additional_sense_str(int asc, int ascq, bool add_sense_leadin,
+                                   int buff_len, char * buff);
+
+/* Returns true if valid bit set, false if valid bit clear. Irrespective the
+ * information field is written out via 'info_outp' (except when it is
+ * NULL). Handles both fixed and descriptor sense formats. */
+bool sg_get_sense_info_fld(const uint8_t * sensep, int sb_len,
+                           uint64_t * info_outp);
+
+/* Returns true if fixed format or command specific information descriptor
+ * is found in the descriptor sense; else false. If available the command
+ * specific information field (4 byte integer in fixed format, 8 byte
+ * integer in descriptor format) is written out via 'cmd_spec_outp'.
+ * Handles both fixed and descriptor sense formats. */
+bool sg_get_sense_cmd_spec_fld(const uint8_t * sensep, int sb_len,
+                               uint64_t * cmd_spec_outp);
+
+/* Returns true if any of the 3 bits (i.e. FILEMARK, EOM or ILI) are set.
+ * In descriptor format if the stream commands descriptor not found
+ * then returns false. Writes true or false corresponding to these bits to
+ * the last three arguments if they are non-NULL. */
+bool sg_get_sense_filemark_eom_ili(const uint8_t * sensep, int sb_len,
+                                   bool * filemark_p, bool * eom_p,
+                                   bool * ili_p);
+
+/* Returns true if SKSV is set and sense key is NO_SENSE or NOT_READY. Also
+ * returns true if progress indication sense data descriptor found. Places
+ * progress field from sense data where progress_outp points. If progress
+ * field is not available returns false. Handles both fixed and descriptor
+ * sense formats. N.B. App should multiply by 100 and divide by 65536
+ * to get percentage completion from given value. */
+bool sg_get_sense_progress_fld(const uint8_t * sensep, int sb_len,
+                               int * progress_outp);
+
+/* Closely related to sg_print_sense(). Puts decoded sense data in 'buff'.
+ * Usually multiline with multiple '\n' including one trailing. If
+ * 'raw_sinfo' set appends sense buffer in hex. 'leadin' is string prepended
+ * to each line written to 'buff', NULL treated as "". Returns the number of
+ * bytes written to 'buff' excluding the trailing '\0'.
+ * N.B. prior to sg3_utils v 1.42 'leadin' was only prepended to the first
+ * line output. Also this function returned type void. */
+int sg_get_sense_str(const char * leadin, const uint8_t * sense_buffer,
+                     int sb_len, bool raw_sinfo, int buff_len, char * buff);
+
+/* Decode descriptor format sense descriptors (assumes sense buffer is
+ * in descriptor format). 'leadin' is string prepended to each line written
+ * to 'b', NULL treated as "". Returns the number of bytes written to 'b'
+ * excluding the trailing '\0'. If problem, returns 0. */
+int sg_get_sense_descriptors_str(const char * leadin,
+                                 const uint8_t * sense_buffer,
+                                 int sb_len, int blen, char * b);
+
+/* Decodes a designation descriptor (e.g. as found in the Device
+ * Identification VPD page (0x83)) into string 'b' whose maximum length is
+ * blen. 'leadin' is string prepended to each line written to 'b', NULL
+ * treated as "". Returns the number of bytes written to 'b' excluding the
+ * trailing '\0'. */
+int sg_get_designation_descriptor_str(const char * leadin,
+                                      const uint8_t * ddp, int dd_len,
+                                      bool print_assoc, bool do_long,
+                                      int blen, char * b);
+
+/* Expects a T10 UUID designator (as found in the Device Identification VPD
+ * page) pointed to by 'dp'. To not produce an error string in 'b', c_set
+ * should be 1 (binary) and dlen should be 18. Currently T10 only supports
+ * locally assigned UUIDs. Writes output to string 'b' of no more than blen
+ * bytes and returns the number of bytes actually written to 'b' but doesn't
+ * count the trailing null character it always appends (if blen > 0). 'lip'
+ * is lead-in string (on each line) than may be NULL. skip_prefix avoids
+ * outputting: '   Locally assigned UUID: ' before the UUID. */
+int sg_t10_uuid_desig2str(const uint8_t * dp, int dlen, int c_set,
+                          bool do_long, bool skip_prefix,
+                          const char * lip, int blen, char * b);
+
+/* Yield string associated with peripheral device type (pdt). Returns
+ * 'buff'. If 'pdt' out of range yields "bad pdt" string. */
+char * sg_get_pdt_str(int pdt, int buff_len, char * buff);
+
+/* Some lesser used PDTs share a lot in common with a more used PDT.
+ * Examples are PDT_ADC decaying to PDT_TAPE and PDT_ZBC to PDT_DISK.
+ * If such a lesser used 'dev_pdt' is given to this function, then it will
+ * return the more used PDT (i.e. "decays to"); otherwise 'dev_pdt' is
+ * returned. Valid for 'pdt' 0 to 31, for other values returns 0. */
+int sg_lib_pdt_decay(int dev_pdt);
+
+/* Yield string associated with transport protocol identifier (tpi). Returns
+ * 'buff'. If 'tpi' out of range yields "bad tpi" string. */
+char * sg_get_trans_proto_str(int tpi, int buff_len, char * buff);
+
+/* Decode TransportID pointed to by 'bp' of length 'bplen'. Place decoded
+ * string output in 'buff' which is also the return value. Each new line
+ * is prefixed by 'leadin'. If leadin NULL treat as "". */
+char * sg_decode_transportid_str(const char * leadin, uint8_t * bp, int bplen,
+                                 bool only_one, int buff_len, char * buff);
+
+/* Returns a designator's type string given 'val' (0 to 15 inclusive),
+ * otherwise returns NULL. */
+const char * sg_get_desig_type_str(int val);
+
+/* Returns a designator's code_set string given 'val' (0 to 15 inclusive),
+ * otherwise returns NULL. */
+const char * sg_get_desig_code_set_str(int val);
+
+/* Returns a designator's association string given 'val' (0 to 3 inclusive),
+ * otherwise returns NULL. */
+const char * sg_get_desig_assoc_str(int val);
+
+/* Yield string associated with zone type (see ZBC and ZBC-2) [e.g. REPORT
+ * ZONES command response]. Returns 'buff' unless buff_len < 1 in which
+ * NULL is returned. */
+char * sg_get_zone_type_str(uint8_t zt, int buff_len, char * buff);
+
+/* Yield SCSI Feature Set (sfs) string. When 'peri_type' is < -1 (or > 31)
+ * returns pointer to string (same as 'buff') associated with 'sfs_code'.
+ * When 'peri_type' is between -1 (for SPC) and 31 (inclusive) then a match
+ * on both 'sfs_code' and 'peri_type' is required. If 'foundp' is not NULL
+ * then where it points is set to true if a match is found else it is set to
+ * false. If 'buff' is not NULL then in the case of a match a descriptive
+ * string is written to 'buff' while if there is not a not then a string
+ * ending in "Reserved" is written (and may be prefixed with SPC, SBC, SSC
+ * or ZBC). Returns 'buff' (i.e. a pointer value) even if it is NULL.
+ * Example:
+ *    char b[64];
+ *    ...
+ *    printf("%s\n", sg_get_sfs_str(sfs_code, -2, sizeof(b), b, NULL, 0));
+ */
+const char * sg_get_sfs_str(uint16_t sfs_code, int peri_type, int buff_len,
+                            char * buff, bool * foundp, int verbose);
+
+/* This is a heuristic that takes into account the command bytes and length
+ * to decide whether the presented unstructured sequence of bytes could be
+ * a SCSI command. If so it returns true otherwise false. Vendor specific
+ * SCSI commands (i.e. opcodes from 0xc0 to 0xff), if presented, are assumed
+ * to follow SCSI conventions (i.e. length of 6, 10, 12 or 16 bytes). The
+ * only SCSI commands considered above 16 bytes of length are the Variable
+ * Length Commands (opcode 0x7f) and the XCDB wrapped commands (opcode 0x7e).
+ * Both have an inbuilt length field which can be cross checked with clen.
+ * No NVMe commands (64 bytes long plus some extra added by some OSes) have
+ * opcodes 0x7e or 0x7f yet. ATA is register based but SATA has FIS
+ * structures that are sent across the wire. The 'FIS register' structure is
+ * used to move a command from a SATA host to device, but the ATA 'command'
+ * is not the first byte. So it is harder to say what will happen if a
+ * FIS structure is presented as a SCSI command, hopefully there is a low
+ * probability this function will yield true in that case. */
+bool sg_is_scsi_cdb(const uint8_t * cdbp, int clen);
+
+/* Yield string associated with NVMe command status value in sct_sc. It
+ * expects to decode DW3 bits 27:17 from the completion queue. Bits 27:25
+ * are the Status Code Type (SCT) and bits 24:17 are the Status Code (SC).
+ * Bit 17 in DW3 should be bit 0 in sct_sc. If no status string is found
+ * a string of the form "Reserved [0x<sct_sc_in_hex>]" is generated.
+ * Returns 'buff'. Does nothing if buff_len<=0 or if buff is NULL.*/
+char * sg_get_nvme_cmd_status_str(uint16_t sct_sc, int buff_len, char * buff);
+
+/* Attempts to map NVMe status value ((SCT << 8) | SC) n sct_sc to a SCSI
+ * status, sense_key, asc and ascq tuple. If successful returns true and
+ * writes to non-NULL pointer arguments; otherwise returns false. */
+bool sg_nvme_status2scsi(uint16_t sct_sc, uint8_t * status_p, uint8_t * sk_p,
+                         uint8_t * asc_p, uint8_t * ascq_p);
+
+/* Add vendor (sg3_utils) specific sense descriptor for the NVMe Status
+ * field. Assumes descriptor (i.e. not fixed) sense. Assume sbp has room. */
+void sg_nvme_desc2sense(uint8_t * sbp, bool dnr, bool more, uint16_t sct_sc);
+
+/* Build minimum sense buffer, either descriptor type (desc=true) or fixed
+ * type (desc=false). Assume sbp has enough room (8 or 14 bytes
+ * respectively). sbp should have room for 32 or 18 bytes respectively */
+void sg_build_sense_buffer(bool desc, uint8_t *sbp, uint8_t skey,
+                           uint8_t asc, uint8_t ascq);
+
+/* Returns true if left argument is "equal" to the right argument. l_pdt_s
+ * is a compound PDT (SCSI Peripheral Device Type) or a negative number
+ * which represents a wildcard (i.e. match anything). r_pdt_s has a similar
+ * form. PDT values are 5 bits long (0 to 31) and a compound pdt_s is
+ * formed by shifting the second (upper) PDT by eight bits to the left and
+ * OR-ing it with the first PDT. The pdt_s values must be defined so
+ * PDT_DISK (0) is _not_ the upper value in a compound pdt_s. */
+bool sg_pdt_s_eq(int l_pdt_s, int r_pdt_s);
+
+extern FILE * sg_warnings_strm;
+
+void sg_set_warnings_strm(FILE * warnings_strm);
+
+/* Given a SCSI command pointed to by cdbp of sz bytes this function forms a
+ * SCSI command in ASCII hex surrounded by square brackets in 'b'. 'b' is at
+ * least blen bytes long. If cmd_name is true then the command is prefixed
+ * by its SCSI command name (e.g.  "VERIFY(10) [2f ...]". The command is
+ * shown as spaced separated pairs of hexadecimal digits (i.e. 0-9, a-f).
+ * Each pair represents byte. The leftmost pair of digits is cdbp[0] . If
+ * sz <= 0 then this function tries to guess the length of the command. */
+char *
+sg_get_command_str(const uint8_t * cdbp, int sz, bool cmd_name, int blen,
+                   char * b);
+
+/* The following "print" functions send ASCII to 'sg_warnings_strm' file
+ * descriptor (default value is stderr). 'leadin' is string prepended to
+ * each line printed out, NULL treated as "". */
+void sg_print_command_len(const uint8_t * command, int len);
+void sg_print_command(const uint8_t * command);
+void sg_print_scsi_status(int scsi_status);
+
+/* DSENSE is 'descriptor sense' as opposed to the older 'fixed sense'. Reads
+ * environment variable SG3_UTILS_DSENSE. Only (currently) used in SNTL. */
+bool sg_get_initial_dsense(void);
+
+/* 'leadin' is string prepended to each line printed out, NULL treated as
+ * "". N.B. prior to sg3_utils v 1.42 'leadin' was only prepended to the
+ * first line printed. */
+void sg_print_sense(const char * leadin, const uint8_t * sense_buffer,
+                    int sb_len, bool raw_info);
+
+/* This examines exit_status and if an error message is known it is output
+ * to stdout/stderr and true is returned. If no error message is
+ * available nothing is output and false is returned. If exit_status is
+ * zero (no error) nothing is output and true is returned. If exit_status
+ * is negative then nothing is output and false is returned. If leadin is
+ * non-NULL then it is printed before the error message. All messages are
+ * a single line with a trailing LF. */
+bool sg_if_can2stdout(const char * leadin, int exit_status);
+bool sg_if_can2stderr(const char * leadin, int exit_status);
+
+/* This examines exit_status and if an error message is known it is output
+ * as a string to 'b' and true is returned. If 'longer' is true and extra
+ * information is available then it is added to the output. If no error
+ * message is available a null character is output and false is returned.
+ * If exit_status is zero (no error) and 'longer' is true then the string
+ * 'No errors' is output; if 'longer' is false then a null character is
+ * output; in both cases true is returned. If exit_status is negative then
+ * a null character is output and false is returned. All messages are a
+ * single line (less than 80 characters) with no trailing LF. The output
+ * string including the trailing null character is no longer than b_len. */
+bool sg_exit2str(int exit_status, bool longer, int b_len, char * b);
+
+/* Utilities can use these exit status values for syntax errors and
+ * file (device node) problems (e.g. not found or permissions). */
+#define SG_LIB_SYNTAX_ERROR 1   /* command line syntax problem */
+
+/* The sg_err_category_sense() function returns one of the following.
+ * These may be used as exit status values (from a process). Notice that
+ * some of the lower values correspond to SCSI sense key values. */
+#define SG_LIB_CAT_CLEAN 0      /* No errors or other information */
+#define SG_LIB_OK_TRUE SG_LIB_CAT_CLEAN  /* No error, reporting true */
+/* Value 1 left unused for utilities to use SG_LIB_SYNTAX_ERROR */
+#define SG_LIB_CAT_NOT_READY 2  /* sense key: not ready, see 12 and 13
+                                 *  [sk,asc,ascq: 0x2,<most>,<most>] */
+#define SG_LIB_CAT_MEDIUM_HARD 3 /* medium or hardware error, blank check
+                                  *       [sk,asc,ascq: 0x3/0x4/0x8,*,*] */
+#define SG_LIB_CAT_ILLEGAL_REQ 5 /* Illegal request (other than invalid
+                                  * opcode):   [sk,asc,ascq: 0x5,*,*] */
+#define SG_LIB_CAT_UNIT_ATTENTION 6 /* sense key, device state changed
+                                     *       [sk,asc,ascq: 0x6,*,*] */
+        /* was SG_LIB_CAT_MEDIA_CHANGED earlier [sk,asc,ascq: 0x6,0x28,*] */
+#define SG_LIB_CAT_DATA_PROTECT 7 /* sense key, media write protected?
+                                   *       [sk,asc,ascq: 0x7,*,*] */
+#define SG_LIB_CAT_INVALID_OP 9 /* (Illegal request,) Invalid opcode:
+                                 *       [sk,asc,ascq: 0x5,0x20,0x0] */
+#define SG_LIB_CAT_COPY_ABORTED 10 /* sense key, some data transferred
+                                    *       [sk,asc,ascq: 0xa,*,*] */
+#define SG_LIB_CAT_ABORTED_COMMAND 11 /* interpreted from sense buffer
+                                       *       [sk,asc,ascq: 0xb,! 0x10,*] */
+#define SG_LIB_CAT_STANDBY 12   /* sense key: not ready, special case
+                                 *      [sk,asc, ascq: 0x2, 0x4, 0xb] */
+#define SG_LIB_CAT_UNAVAILABLE 13 /* sense key: not ready, special case
+                                   *      [sk,asc, ascq: 0x2, 0x4, 0xc] */
+#define SG_LIB_CAT_MISCOMPARE 14 /* sense key, probably verify
+                                  *       [sk,asc,ascq: 0xe,*,*] */
+#define SG_LIB_FILE_ERROR 15    /* device or other file problem */
+/* for 17 and 18, see below */
+#define SG_LIB_CAT_NO_SENSE 20  /* sense data with key of "no sense"
+                                 *       [sk,asc,ascq: 0x0,*,*] */
+#define SG_LIB_CAT_RECOVERED 21 /* Successful command after recovered err
+                                 *       [sk,asc,ascq: 0x1,*,*] */
+#define SG_LIB_LBA_OUT_OF_RANGE 22 /* Illegal request, LBA Out Of Range
+                                    *    [sk,asc,ascq: 0x5,0x21,0x0] */
+#define SG_LIB_CAT_RES_CONFLICT SAM_STAT_RESERVATION_CONFLICT
+                                /* 24: this is a SCSI status, not sense.
+                                 * It indicates reservation by another
+                                 * machine blocks this command */
+#define SG_LIB_CAT_CONDITION_MET 25 /* SCSI status, not sense key.
+                                     * Only from PRE-FETCH (SBC-4) */
+#define SG_LIB_CAT_BUSY       26 /* SCSI status, not sense. Invites retry */
+#define SG_LIB_CAT_TS_FULL    27 /* SCSI status, not sense. Wait then retry */
+#define SG_LIB_CAT_ACA_ACTIVE 28 /* SCSI status; ACA seldom used */
+#define SG_LIB_CAT_TASK_ABORTED 29 /* SCSI status, this command aborted by? */
+#define SG_LIB_CONTRADICT 31    /* error involving two or more cl options */
+#define SG_LIB_LOGIC_ERROR 32   /* unexpected situation in code */
+/* for 33 see SG_LIB_CAT_TIMEOUT below */
+#define SG_LIB_WINDOWS_ERR 34   /* Windows error number don't fit in 7 bits so
+                                 * map to a single value for exit statuses */
+#define SG_LIB_TRANSPORT_ERROR 35       /* driver or interconnect */
+#define SG_LIB_OK_FALSE 36      /* no error, reporting false (cf. no error,
+                                 * reporting true is SG_LIB_OK_TRUE(0) ) */
+#define SG_LIB_CAT_PROTECTION 40 /* subset of aborted command (for PI, DIF)
+                                  *       [sk,asc,ascq: 0xb,0x10,*] */
+/* 47: flock error used in ddpt utility */
+#define SG_LIB_NVME_STATUS 48   /* NVMe Status Field (SF) other than 0 */
+#define SG_LIB_WILD_RESID 49    /* Residual value for data-in transfer of a
+                                 * SCSI command is nonsensical */
+#define SG_LIB_OS_BASE_ERR 50   /* in Linux: values found in:
+                                 * include/uapi/asm-generic/errno-base.h
+                                 * Example: ENOMEM reported as 62 (=50+12)
+                                 * if errno > 46 then use this value */
+/* 51-->96 set aside for Unix errno values shifted by SG_LIB_OS_BASE_ERR */
+#define SG_LIB_CAT_MALFORMED 97 /* Response to SCSI command malformed */
+#define SG_LIB_CAT_SENSE 98     /* Something else is in the sense buffer */
+#define SG_LIB_CAT_OTHER 99     /* Some other error/warning has occurred
+                                 * (e.g. a transport or driver error) */
+/* 100 to 120 (inclusive) used by ddpt utility */
+#define SG_LIB_UNUSED_ABOVE 120  /* Put extra errors in holes below this */
+
+/* Returns a SG_LIB_CAT_* value. If cannot decode sense_buffer or a less
+ * common sense key then return SG_LIB_CAT_SENSE .*/
+int sg_err_category_sense(const uint8_t * sense_buffer, int sb_len);
+
+/* Here are some additional sense data categories that are not returned
+ * by sg_err_category_sense() but are returned by some related functions. */
+#define SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO 17 /* Illegal request (other than */
+                                /* invalid opcode) plus 'info' field: */
+                                /*  [sk,asc,ascq: 0x5,*,*] */
+#define SG_LIB_CAT_MEDIUM_HARD_WITH_INFO 18 /* medium or hardware error */
+                                /* sense key plus 'info' field: */
+                                /*       [sk,asc,ascq: 0x3/0x4,*,*] */
+#define SG_LIB_CAT_TIMEOUT 33   /* SCSI command timeout */
+#define SG_LIB_CAT_PROTECTION_WITH_INFO 41 /* aborted command sense key, */
+                                /* protection plus 'info' field: */
+                                /*  [sk,asc,ascq: 0xb,0x10,*] */
+
+/* Yield string associated with sense category. Returns 'buff' (or pointer
+ * to "Bad sense category" if 'buff' is NULL). If sense_cat unknown then
+ * yield "Sense category: <sense_cat)val>" string. The original 'sense
+ * category' concept has been expanded to most detected errors and is
+ * returned by these utilities as their exit status value (an (unsigned)
+ * 8 bit value where 0 means good (i.e. no errors)).  Uses the
+ * sg_exit2str() function. */
+const char * sg_get_category_sense_str(int sense_cat, int buff_len,
+                                       char * buff, int verbose);
+
+
+/* Iterates to next designation descriptor in the device identification
+ * VPD page. The 'initial_desig_desc' should point to start of first
+ * descriptor with 'page_len' being the number of valid bytes in that
+ * and following descriptors. To start, 'off' should point to a negative
+ * value, thereafter it should point to the value yielded by the previous
+ * call. If 0 returned then 'initial_desig_desc + *off' should be a valid
+ * descriptor; returns -1 if normal end condition and -2 for an abnormal
+ * termination. Matches association, designator_type and/or code_set when
+ * any of those values are greater than or equal to zero. */
+int sg_vpd_dev_id_iter(const uint8_t * initial_desig_desc, int page_len,
+                       int * off, int m_assoc, int m_desig_type,
+                       int m_code_set);
+
+
+/* <<< General purpose (i.e. not SCSI specific) utility functions >>> */
+
+/* Always returns valid string even if errnum is wild (or library problem).
+ * If errnum is negative, flip its sign. */
+char * safe_strerror(int errnum);
+
+/* Not all platforms support the Unix sleep(seconds) function. */
+void sg_sleep_secs(int num_secs);
+
+/* There are several SCSI commands that are very destructive for the user
+ * data stored on a device. The FORMAT UNIT command is the prime example
+ * but there are an increasing number of newer SCSI commands that remove or
+ * destroy some or all of the user's data. This function takes 15 seconds,
+ * divided into three parts, saying that 'cmd_name' will be executed on
+ * 'dev_name' and then waits for 5 seconds inviting the user to press
+ * control-C to abort the operation. After three such prompts the function
+ * returns and the utility start to execute the "dangerous" SCSI command,
+ * Utilities that use this function usually have a --quick option to bypass
+ * this call. That may be appropriate if the utility in question is called
+ * from a script or in background processing. If 'stress_all' is true then
+ * state "ALL data" will be lost, if false drop the "ALL". */
+void
+sg_warn_and_wait(const char * cmd_name, const char * dev_name,
+                 bool stress_all);
+
+
+/* Print (to stdout) 'str' of bytes in hex, 16 bytes per line optionally
+ * followed at the right hand side of the line with an ASCII interpretation.
+ * Each line is prefixed with an address, starting at 0 for str[0]..str[15].
+ * All output numbers are in hex.
+ * 'no_ascii' selects on of  3 output format types:
+ *     > 0     each line has address then up to 16 ASCII-hex bytes
+ *     = 0     in addition, the bytes are listed in ASCII to the right
+ *     < 0     only the ASCII-hex bytes are listed (i.e. without address)
+*/
+void dStrHex(const char * str, int len, int no_ascii);
+
+/* Print (to sg_warnings_strm (stderr)) 'str' of bytes in hex, 16 bytes per
+ * line optionally followed at right by its ASCII interpretation. Same
+ * logic as dStrHex() with different output stream (i.e. stderr). */
+void dStrHexErr(const char * str, int len, int no_ascii);
+
+/* Read binary starting at 'str' for 'len' bytes and output as ASCII
+ * hexadecimal into file pointer (fp). 16 bytes per line are output with an
+ * additional space between 8th and 9th byte on each line (for readability).
+ * 'no_ascii' selects one of 3 output format types as shown in dStrHex() . */
+void dStrHexFp(const char* str, int len, int no_ascii, FILE * fp);
+
+/* Read 'len' bytes from 'str' and output as ASCII-Hex bytes (space separated)
+ * to 'b' not to exceed 'b_len' characters. Each line starts with 'leadin'
+ * (NULL for no leadin) and there are 16 bytes per line with an extra space
+ * between the 8th and 9th bytes. 'oformat' is 0 for repeat in printable ASCII
+ * ('.' for non printable chars) to right of each line; 1 don't (so just
+ * output ASCII hex). If 'oformat' is 2 output same as 1 but any LFs are
+ * replaced by space (and trailing spaces are trimmed). Note that an address
+ * is _not_ printed on each line preceding the hex data. Returns number of
+ * bytes written to 'b' excluding the trailing '\0'. The only difference
+ * between dStrHexStr() and hex2str() is the type of the first argument. */
+int dStrHexStr(const char * str, int len, const char * leadin, int oformat,
+               int cb_len, char * cbp);
+int hex2str(const uint8_t * b_str, int len, const char * leadin, int oformat,
+            int cb_len, char * cbp);
+
+/* Similar to hex2str() but outputs to file pointed to be fp */
+void hex2fp(const uint8_t * b_str, int len, const char * leadin, int oformat,
+            FILE * fp);
+
+/* The following 2 functions are equivalent to dStrHex() and dStrHexErr()
+ * respectively. The difference is only the type of the first of argument:
+ * uint8_t instead of char. The name of the argument is changed to b_str to
+ * stress it is a pointer to the start of a binary string. */
+void hex2stdout(const uint8_t * b_str, int len, int no_ascii);
+void hex2stderr(const uint8_t * b_str, int len, int no_ascii);
+
+/* Read ASCII hex bytes or binary from fname (a file named '-' taken as
+ * stdin). If reading ASCII hex then there should be either one entry per
+ * line or a comma, space, hyphen or tab separated list of bytes. If no_space
+ * is set then a string of ACSII hex digits is expected, 2 per byte.
+ * Everything from and including a '#' on a line is ignored. Returns 0 if ok,
+ * or an error code. If the error code is SG_LIB_LBA_OUT_OF_RANGE then mp_arr
+ * would be exceeded and both mp_arr and mp_arr_len are written to.
+ * The max_arr_len_and argument may carry extra information: when it is
+ * negative its absolute value is used for the maximum number of bytes to
+ * write to mp_arr _and_ the first hexadecimal value on each line is skipped.
+ * Many hexadecimal output programs place a running address (index) as the
+ * first field on each line. When as_binary and/or no_space are true, the
+ * absolute value of max_arr_len_and is used. */
+int sg_f2hex_arr(const char * fname, bool as_binary, bool no_space,
+                 uint8_t * mp_arr, int * mp_arr_len, int max_arr_len_and);
+
+/* Returns true when executed on big endian machine; else returns false.
+ * Useful for displaying ATA identify words (which need swapping on a
+ * big endian machine). */
+bool sg_is_big_endian();
+
+/* Returns true if byte sequence starting at bp with a length of b_len is
+ * all zeros (for sg_all_zeros()) or all 0xff_s (for sg_all_ffs());
+ * otherwise returns false. If bp is NULL or b_len <= 0 returns false. */
+bool sg_all_zeros(const uint8_t * bp, int b_len);
+bool sg_all_ffs(const uint8_t * bp, int b_len);
+
+/* Extract character sequence from ATA words as in the model string
+ * in a IDENTIFY DEVICE response. Returns number of characters
+ * written to 'ochars' before 0 character is found or 'num' words
+ * are processed. */
+int sg_ata_get_chars(const uint16_t * word_arr, int start_word,
+                     int num_words, bool is_big_endian, char * ochars);
+
+/* Print (to stdout) 16 bit 'words' in hex, 8 words per line optionally
+ * followed at the right hand side of the line with an ASCII interpretation
+ * (pairs of ASCII characters in big endian order (upper first)).
+ * Each line is prefixed with an address, starting at 0.
+ * All output numbers are in hex. 'no_ascii' allows for 3 output types:
+ *     > 0     each line has address then up to 8 ASCII-hex words
+ *     = 0     in addition, the words are listed in ASCII pairs to the right
+ *     = -1    only the ASCII-hex words are listed (i.e. without address)
+ *     = -2    only the ASCII-hex words, formatted for "hdparm --Istdin"
+ *     < -2    same as -1
+ * If 'swapb' is true then bytes in each word swapped. Needs to be set
+ * for ATA IDENTIFY DEVICE response on big-endian machines.
+*/
+void dWordHex(const uint16_t * words, int num, int no_ascii, bool swapb);
+
+/* If the number in 'buf' can not be decoded or the multiplier is unknown
+ * then -1 is returned. Accepts a hex prefix (0x or 0X) or a decimal
+ * multiplier suffix (as per GNU's dd (since 2002: SI and IEC 60027-2)).
+ * Main (SI) multipliers supported: K, M, G. Ignore leading spaces and
+ * tabs; accept comma, hyphen, space, tab and hash as terminator.
+ * Handles zero and positive values up to 2**31-1 .
+ * Experimental: left argument (must in with hexadecimal digit) added
+ * to, or multiplied, by right argument. No embedded spaces.
+ * Examples: '3+1k' (evaluates to 1027) and '0xf+0x3'. */
+int sg_get_num(const char * buf);
+
+/* If the number in 'buf' can not be decoded then -1 is returned. Accepts a
+ * hex prefix (0x or 0X) or a 'h' (or 'H') suffix; otherwise decimal is
+ * assumed. Does not accept multipliers. Accept a comma (","), hyphen ("-"),
+ * a whitespace or newline as terminator. Only decimal numbers can represent
+ * negative numbers and '-1' must be treated separately. */
+int sg_get_num_nomult(const char * buf);
+
+/* If the number in 'buf' can not be decoded or the multiplier is unknown
+ * then -1LL is returned. Accepts a hex prefix (0x or 0X), hex suffix
+ * (h or H), or a decimal multiplier suffix (as per GNU's dd (since 2002:
+ * SI and IEC 60027-2)).  Main (SI) multipliers supported: K, M, G, T, P
+ * and E. Ignore leading spaces and tabs; accept comma, hyphen, space, tab
+ * and hash as terminator. Handles zero and positive values up to 2**63-1 .
+ * Experimental: the left argument (must end in with hexadecimal digit)
+ * added to, or multiplied by, the right argument. No embedded spaces.
+ * Examples: '3+1k' (evaluates to 1027) and '0xf+0x3'. */
+int64_t sg_get_llnum(const char * buf);
+
+/* If the number in 'buf' can not be decoded then -1 is returned. Accepts a
+ * hex prefix (0x or 0X) or a 'h' (or 'H') suffix; otherwise decimal is
+ * assumed. Does not accept multipliers. Accept a comma (","), hyphen ("-"),
+ * a whitespace or newline as terminator. Only decimal numbers can represent
+ * negative numbers and '-1' must be treated separately. */
+int64_t sg_get_llnum_nomult(const char * buf);
+
+/* Returns pointer to heap (or NULL) that is aligned to a align_to byte
+ * boundary. Sends back *buff_to_free pointer in third argument that may be
+ * different from the return value. If it is different then the *buff_to_free
+ * pointer should be freed (rather than the returned value) when the heap is
+ * no longer needed. If align_to is 0 then aligns to OS's page size. Sets all
+ * returned heap to zeros. If num_bytes is 0 then set to page size. */
+uint8_t * sg_memalign(uint32_t num_bytes, uint32_t align_to,
+                      uint8_t ** buff_to_free, bool vb);
+
+/* Returns OS page size in bytes. If uncertain returns 4096. */
+uint32_t sg_get_page_size(void);
+
+/* If byte_count is 0 or less then the OS page size is used as denominator.
+ * Returns true  if the remainder of ((unsigned)pointer % byte_count) is 0,
+ * else returns false. */
+bool sg_is_aligned(const void * pointer, int byte_count);
+
+/* Does similar job to sg_get_unaligned_be*() but this function starts at
+ * a given start_bit (i.e. within byte, so 7 is MSbit of byte and 0 is LSbit)
+ * offset. Maximum number of num_bits is 64. For example, these two
+ * invocations are equivalent (and should yield the same result);
+ *       sg_get_big_endian(from_bp, 7, 16)
+ *       sg_get_unaligned_be16(from_bp)  */
+uint64_t sg_get_big_endian(const uint8_t * from_bp,
+                           int start_bit /* 0 to 7 */,
+                           int num_bits /* 1 to 64 */);
+
+/* Does similar job to sg_put_unaligned_be*() but this function starts at
+ * a given start_bit offset. Maximum number of num_bits is 64. Preserves
+ * residual bits in partially written bytes. start_bit 7 is MSb. */
+void sg_set_big_endian(uint64_t val, uint8_t * to, int start_bit /* 0 to 7 */,
+                       int num_bits /* 1 to 64 */);
+
+/* If os_err_num is within bounds then the returned value is 'os_err_num +
+ * SG_LIB_OS_BASE_ERR' otherwise SG_LIB_OS_BASE_ERR is returned. If
+ * os_err_num is 0 then 0 is returned. */
+int sg_convert_errno(int os_err_num);
+
+
+/* <<< Architectural support functions [is there a better place?] >>> */
+
+/* Non Unix OSes distinguish between text and binary files.
+ * Set text mode on fd. Does nothing in Unix. Returns negative number on
+ * failure. */
+int sg_set_text_mode(int fd);
+
+/* Set binary mode on fd. Does nothing in Unix. Returns negative number on
+ * failure. */
+int sg_set_binary_mode(int fd);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif          /* SG_LIB_H */
diff --git a/include/sg_lib_data.h b/include/sg_lib_data.h
new file mode 100644
index 0000000..b27b7c8
--- /dev/null
+++ b/include/sg_lib_data.h
@@ -0,0 +1,145 @@
+#ifndef SG_LIB_DATA_H
+#define SG_LIB_DATA_H
+
+/*
+ * Copyright (c) 2007-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * This header file contains some structure declarations and array name
+ * declarations which are defined in the sg_lib_data.c .
+ * Typically this header does not need to be exposed to users of the
+ * sg_lib interface declared in sg_libs.h .
+ */
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Operation codes with associated service actions that change or qualify
+ * the command name */
+#define SG_EXTENDED_COPY 0x83 /* since spc4r34 became next entry */
+#define SG_3PARTY_COPY_OUT 0x83 /* new in spc4r34: Third party copy out */
+#define SG_RECEIVE_COPY 0x84  /* since spc4r34 became next entry */
+#define SG_3PARTY_COPY_IN 0x84 /* new in spc4r34: Third party copy in */
+#define SG_MAINTENANCE_IN 0xa3
+#define SG_MAINTENANCE_OUT 0xa4
+#define SG_PERSISTENT_RESERVE_IN 0x5e
+#define SG_PERSISTENT_RESERVE_OUT 0x5f
+#define SG_READ_ATTRIBUTE 0x8c
+#define SG_READ_BUFFER 0x3c     /* now READ BUFFER(10) */
+#define SG_READ_BUFFER_16 0x9b
+#define SG_READ_POSITION 0x34   /* SSC command with service actions */
+#define SG_SANITIZE 0x48
+#define SG_SERVICE_ACTION_BIDI 0x9d
+#define SG_SERVICE_ACTION_IN_12 0xab
+#define SG_SERVICE_ACTION_IN_16 0x9e
+#define SG_SERVICE_ACTION_OUT_12 0xa9
+#define SG_SERVICE_ACTION_OUT_16 0x9f
+#define SG_VARIABLE_LENGTH_CMD 0x7f
+#define SG_WRITE_BUFFER 0x3b
+#define SG_ZONING_OUT 0x94
+#define SG_ZBC_OUT SG_ZONING_OUT        /* as SPC calls them */
+#define SG_ZONING_IN 0x95
+#define SG_ZBC_IN SG_ZONING_IN          /* as SPC calls them */
+
+
+
+struct sg_lib_simple_value_name_t {
+    int value;
+    const char * name;
+};
+
+struct sg_lib_value_name_t {
+    int value;
+    int peri_dev_type; /* 0 -> SPC and/or PDT_DISK, >0 -> PDT */
+    const char * name;
+};
+
+struct sg_value_2names_t {
+    int value;
+    const char * name;
+    const char * name2;
+};
+
+struct sg_lib_asc_ascq_t {
+    uint8_t asc;          /* additional sense code */
+    uint8_t ascq;         /* additional sense code qualifier */
+    const char * text;
+};
+
+struct sg_lib_asc_ascq_range_t {
+    uint8_t asc;          /* additional sense code (ASC) */
+    uint8_t ascq_min;     /* ASCQ minimum in range */
+    uint8_t ascq_max;     /* ASCQ maximum in range */
+    const char * text;
+};
+
+/* First use: SCSI status, sense_key, asc, ascq tuple */
+struct sg_lib_4tuple_u8 {
+    uint8_t t1;
+    uint8_t t2;
+    uint8_t t3;
+    uint8_t t4;
+};
+
+struct sg_cmd_response_t {
+    int din_len;
+    int dout_len;
+    int resid;
+    int resid2;
+    const uint8_t * sbp;
+};
+
+
+extern const char * sg_lib_version_str;
+
+extern struct sg_lib_value_name_t sg_lib_normal_opcodes[];
+extern struct sg_lib_value_name_t sg_lib_read_buff_arr[];
+extern struct sg_lib_value_name_t sg_lib_write_buff_arr[];
+extern struct sg_lib_value_name_t sg_lib_maint_in_arr[];
+extern struct sg_lib_value_name_t sg_lib_maint_out_arr[];
+extern struct sg_lib_value_name_t sg_lib_pr_in_arr[];
+extern struct sg_lib_value_name_t sg_lib_pr_out_arr[];
+extern struct sg_lib_value_name_t sg_lib_sanitize_sa_arr[];
+extern struct sg_lib_value_name_t sg_lib_serv_in12_arr[];
+extern struct sg_lib_value_name_t sg_lib_serv_out12_arr[];
+extern struct sg_lib_value_name_t sg_lib_serv_in16_arr[];
+extern struct sg_lib_value_name_t sg_lib_serv_out16_arr[];
+extern struct sg_lib_value_name_t sg_lib_serv_bidi_arr[];
+extern struct sg_lib_value_name_t sg_lib_xcopy_sa_arr[];
+extern struct sg_lib_value_name_t sg_lib_rec_copy_sa_arr[];
+extern struct sg_lib_value_name_t sg_lib_variable_length_arr[];
+extern struct sg_lib_value_name_t sg_lib_zoning_out_arr[];
+extern struct sg_lib_value_name_t sg_lib_zoning_in_arr[];
+extern struct sg_lib_value_name_t sg_lib_read_attr_arr[];
+extern struct sg_lib_value_name_t sg_lib_read_pos_arr[];
+extern struct sg_lib_asc_ascq_range_t sg_lib_asc_ascq_range[];
+extern struct sg_lib_simple_value_name_t sg_lib_sstatus_str_arr[];
+extern struct sg_lib_asc_ascq_t sg_lib_asc_ascq[];
+extern struct sg_lib_value_name_t sg_lib_scsi_feature_sets[];
+extern const char * sg_lib_sense_key_desc[];
+extern const char * sg_lib_pdt_strs[];
+extern const char * sg_lib_transport_proto_strs[];
+extern const char * sg_lib_tapealert_strs[];
+extern int sg_lib_pdt_decay_arr[];
+
+extern struct sg_lib_simple_value_name_t sg_lib_nvme_admin_cmd_arr[];
+extern struct sg_lib_simple_value_name_t sg_lib_nvme_nvm_cmd_arr[];
+extern struct sg_lib_value_name_t sg_lib_nvme_cmd_status_arr[];
+extern struct sg_lib_4tuple_u8 sg_lib_scsi_status_sense_arr[];
+
+extern struct sg_value_2names_t sg_exit_str_arr[];
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/sg_lib_names.h b/include/sg_lib_names.h
new file mode 100644
index 0000000..df997ed
--- /dev/null
+++ b/include/sg_lib_names.h
@@ -0,0 +1,31 @@
+#ifndef SG_LIB_NAMES_H
+#define SG_LIB_NAMES_H
+
+/*
+ * Copyright (c) 2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdint.h>
+
+#include "sg_lib_data.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct sg_lib_simple_value_name_t sg_lib_names_mode_arr[];
+extern struct sg_lib_simple_value_name_t sg_lib_names_vpd_arr[];
+
+extern const size_t sg_lib_names_mode_len;
+extern const size_t sg_lib_names_vpd_len;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* end of SG_LIB_NAMES */
diff --git a/include/sg_linux_inc.h b/include/sg_linux_inc.h
new file mode 100644
index 0000000..e6f6b52
--- /dev/null
+++ b/include/sg_linux_inc.h
@@ -0,0 +1,58 @@
+#ifndef SG_LINUX_INC_H
+#define SG_LINUX_INC_H
+
+#ifdef SG_KERNEL_INCLUDES
+  #include <stdint.h>   /* C99 header for exact integer types */
+  #define __user
+  typedef uint8_t u8;
+  #include "/usr/src/linux/include/scsi/sg.h"
+  #include "/usr/src/linux/include/scsi/scsi.h"
+#else
+  #ifdef SG_TRICK_GNU_INCLUDES
+    #include <linux/../scsi/sg.h>
+    #include <linux/../scsi/scsi.h>
+  #else
+    #define __user
+    #include <scsi/sg.h>
+    #include <scsi/scsi.h>
+  #endif
+#endif
+
+#ifdef BLKGETSIZE64
+  #ifndef u64
+    #include <stdint.h>   /* C99 header for exact integer types */
+    typedef uint64_t u64; /* problems with BLKGETSIZE64 ioctl in lk 2.4 */
+  #endif
+#endif
+
+/*
+  Getting the correct include files for the sg interface can be an ordeal.
+  In a perfect world, one would just write:
+    #include <scsi/sg.h>
+    #include <scsi/scsi.h>
+  This would include the files found in the /usr/include/scsi directory.
+  Those files are maintained with the GNU library which may or may not
+  agree with the kernel and version of sg driver that is running. Any
+  many cases this will not matter. However in some it might, for example
+  glibc 2.1's include files match the sg driver found in the lk 2.2
+  series. Hence if glibc 2.1 is used with lk 2.4 then the additional
+  sg v3 interface will not be visible.
+  If this is a problem then defining SG_KERNEL_INCLUDES will access the
+  kernel supplied header files (assuming they are in the normal place).
+  The GNU library maintainers and various kernel people don't like
+  this approach (but it does work).
+  The technique selected by defining SG_TRICK_GNU_INCLUDES worked (and
+  was used) prior to glibc 2.2 . Prior to that version /usr/include/linux
+  was a symbolic link to /usr/src/linux/include/linux .
+
+  There are other approaches if this include "mixup" causes pain. These
+  would involve include files being copied or symbolic links being
+  introduced.
+
+  Sorry about the inconvenience. Typically neither SG_KERNEL_INCLUDES
+  nor SG_TRICK_GNU_INCLUDES is defined.
+
+  dpg 20010415, 20030522
+*/
+
+#endif
diff --git a/include/sg_pr2serr.h b/include/sg_pr2serr.h
new file mode 100644
index 0000000..4a57d83
--- /dev/null
+++ b/include/sg_pr2serr.h
@@ -0,0 +1,376 @@
+#ifndef SG_PR2SERR_H
+#define SG_PR2SERR_H
+
+/*
+ * Copyright (c) 2004-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* pr2serr and pr2ws are convenience functions that replace the somewhat
+ * long-winded fprintf(stderr, ....). The second form (i.e. pr2ws() ) is for
+ * internal library use and may place its output somewhere other than stderr;
+ * it depends on the external variable sg_warnings_strm which can be set
+ * with sg_set_warnings_strm(). By default it uses stderr. */
+
+#if __USE_MINGW_ANSI_STDIO -0 == 1
+#define __printf(a, b) __attribute__((__format__(gnu_printf, a, b)))
+#elif defined(__GNUC__) || defined(__clang__)
+#define __printf(a, b) __attribute__((__format__(printf, a, b)))
+#else
+#define __printf(a, b)
+#endif
+
+int pr2serr(const char * fmt, ...) __printf(1, 2);
+
+int pr2ws(const char * fmt, ...) __printf(1, 2);
+
+/* Want safe, 'n += snprintf(b + n, blen - n, ...)' style sequence of
+ * functions that can be called multiple times. Returns number of chars
+ * placed in cp excluding the trailing null char. So for cp_max_len > 0 the
+ * return value is always < cp_max_len; for cp_max_len <= 1 the return value
+ * is 0 and no chars are written to cp. Note this means that when
+ * cp_max_len = 1, this function assumes that cp[0] is the null character
+ * and does nothing (and returns 0). Linux kernel has a similar function
+ * called  scnprintf().  */
+int sg_scnpr(char * cp, int cp_max_len, const char * fmt, ...) __printf(3, 4);
+
+/* JSON support functions and structures follow. The prefix "sgj_" is used
+ * for sg3_utils JSON functions, types and values. */
+
+enum sgj_separator_t {
+    SGJ_SEP_NONE = 0,
+    SGJ_SEP_SPACE_1,
+    SGJ_SEP_SPACE_2,
+    SGJ_SEP_SPACE_3,
+    SGJ_SEP_SPACE_4,
+    SGJ_SEP_EQUAL_NO_SPACE,
+    SGJ_SEP_EQUAL_1_SPACE,
+    SGJ_SEP_COLON_NO_SPACE,
+    SGJ_SEP_COLON_1_SPACE,
+};
+
+typedef void * sgj_opaque_p;
+
+/* Apart from the state information at the end of this structure, the earlier
+ * fields are initialized from the command line argument given to the
+ * --json= option. If there is no argument then they initialized as shown. */
+typedef struct sgj_state_t {
+    /* the following set by default, the SG3_UTILS_JSON_OPTS environment
+     * variable or command line argument to --json option, in that order. */
+    bool pr_as_json;            /* = false (def: is human readable output) */
+    bool pr_exit_status;        /* 'e' (def: true) */
+    bool pr_hex;                /* 'h' (def: false) */
+    bool pr_leadin;             /* 'l' (def: true) */
+    bool pr_name_ex;        /* 'n' name_extra (information) (def: false) */
+    bool pr_out_hr;             /* 'o' (def: false) */
+    bool pr_packed;             /* 'k' (def: false) only when !pr_pretty */
+    bool pr_pretty;             /* 'p' (def: true) */
+    bool pr_string;             /* 's' (def: true) */
+    char pr_format;             /*  (def: '\0') */
+    int pr_indent_size;         /* digit (def: 4) */
+    int verbose;                /* 'v' (def: 0) incremented each appearance */
+
+    /* the following hold state information */
+    int first_bad_char;         /* = '\0' */
+    sgj_opaque_p basep;         /* base JSON object pointer */
+    sgj_opaque_p out_hrp;       /* JSON array pointer when pr_out_hr set */
+    sgj_opaque_p userp;         /* for temporary usage */
+} sgj_state;
+
+/* This function tries to convert the in_name C string to the "snake_case"
+ * convention so the output sname only contains lower case ASCII letters,
+ * numerals and "_" as a separator. Any leading or trailing underscores
+ * are removed as are repeated underscores (e.g. "_Snake __ case" becomes
+ * "snake_case"). Parentheses and the characters between them are removed.
+ * Returns sname (i.e. the pointer to the output buffer).
+ * Note: strlen(in_name) should be <= max_sname_len . */
+char * sgj_convert_to_snake_name(const char * in_name, char * sname,
+                                 int max_sname_len);
+bool sgj_is_snake_name(const char * in_name);
+
+/* There are many variants of JSON supporting functions below and some
+ * abbreviations are used to shorten their function names:
+ *    sgj_  - prefix of all the functions related to (non-)JSON output
+ *    hr    - human readable form (as it was before JSON)
+ *    js    - JSON only output
+ *    haj   - human readable and JSON output, hr goes in 'output' array
+ *    pr    - has printf() like variadic arguments
+ *    _r    - suffix indicating the return value should/must be used
+ *    nv    - adds a name-value JSON field (or several)
+ *    o     - value is the provided JSON object (or array)
+ *    i     - value is a JSON integer object (int64_t or uint64_t)
+ *    b     - value is a JSON boolean object
+ *    s     - value is a JSON string object
+ *    str   - same as s
+ *    hex   - value is hexadecimal in a JSON string object
+ *    _nex  - extra 'name_extra' JSON string object about name
+ *    new   - object that needs sgj_free_unattached() if not attached
+ *
+ *    */
+
+/* If jsp in non-NULL and jsp->pr_as_json is true then this call is ignored
+ * unless jsp->pr_out_hrp is true. Otherwise this function prints to stdout
+ * like printf(fmt, ...); note that no LF is added. In the jsp->pr_out_hrp is
+ * true case, nothing is printed to stdout but instead is placed into a JSON
+ * array (jsp->out_hrp) after some preprocessing. That preprocessing involves
+ * removing a leading LF from 'fmt' (if present) and up to two trailing LF
+ * characters. */
+void sgj_pr_hr(sgj_state * jsp, const char * fmt, ...) __printf(2, 3);
+
+/* Initializes the state object pointed to by jsp based on the argument
+ * given to the right of --json= pointed to by j_optarg. If it is NULL
+ * then state object gets its default values. Returns true if argument
+ * to --json= is decoded properly, else returns false and places the
+ * first "bad" character in jsp->first_bad_char . Note that no JSON
+ * in-core tree needs to exist when this function is called. */
+bool sgj_init_state(sgj_state * jsp, const char * j_optarg);
+
+/* sgj_start() creates a JSON in-core tree and returns a pointer to it (or
+ * NULL if the associated heap allocation fails). It should be paired with
+ * sgj_finish() to clean up (i.e. remove all heap allocations) all the
+ * elements (i.e. JSON objects and arrays) that have been placed in that
+ * in-core tree. If jsp is NULL nothing further happens. Otherwise the pointer
+ * to be returned is placed in jsp->basep. If jsp->pr_leadin is true and
+ * util_name is non-NULL then a "utility_invoked" JSON object is made with
+ * "name", and "version_date" object fields. If the jsp->pr_out_hr field is
+ * true a named array called "output" is added to the "utility_invoked" object
+ * (creating it in the case when jsp->pr_leadin is false) and a pointer to
+ * that array object is placed in jsp->objectp . The returned pointer is not
+ * usually needed but if it is NULL then a heap allocation has failed. */
+sgj_opaque_p sgj_start_r(const char * util_name, const char * ver_str,
+                         int argc, char *argv[], sgj_state * jsp);
+
+/* These are low level functions returning a pointer to a newly created JSON
+ * object or array. If jsp is NULL or jsp->pr_as_json is false nothing happens
+ * and NULL is returned. Note that this JSON object is _not_ placed in the
+ * in-core tree controlled by jsp (jsp->basep); it may be added later as the
+ * fourth argument to sgj_js_nv_o(), for example. */
+sgj_opaque_p sgj_new_unattached_object_r(sgj_state * jsp);
+sgj_opaque_p sgj_new_unattached_array_r(sgj_state * jsp);
+
+/* If jsp is NULL or jsp->pr_as_json is false nothing happens and NULL is
+ * returned. Otherwise it creates a new named object (whose name is what
+ * 'name' points to) at 'jop' with an empty object as its value; a pointer
+ * to that empty object is returned. If 'jop' is NULL then jsp->basep is
+ * used instead. The returned value should always be checked (for NULL)
+ * and if not, used. */
+sgj_opaque_p sgj_named_subobject_r(sgj_state * jsp, sgj_opaque_p jop,
+                                   const char * name);
+sgj_opaque_p sgj_snake_named_subobject_r(sgj_state * jsp, sgj_opaque_p jop,
+                                         const char * conv2sname);
+
+/* If jsp is NULL or jsp->pr_as_json is false nothing happens and NULL is
+ * returned. Otherwise it creates a new named object (whose name is what
+ * 'name' points to) at 'jop' with an empty array as its value; a pointer
+ * to that empty array is returned.  If 'jop' is NULL then jsp->basep is
+ * used instead. The returned value should always * be checked (for NULL)
+ * and if not, used. */
+sgj_opaque_p sgj_named_subarray_r(sgj_state * jsp, sgj_opaque_p jop,
+                                  const char * name);
+sgj_opaque_p sgj_snake_named_subarray_r(sgj_state * jsp, sgj_opaque_p jop,
+                                        const char * conv2sname);
+
+/* If either jsp or value is NULL or jsp->pr_as_json is false then nothing
+ * happens and NULL is returned. The insertion point is at jop but if it is
+ * NULL jsp->basep is used. If 'name' is non-NULL a new named JSON object is
+ * added using 'name' and the associated value is a JSON string formed from
+ * 'value'. If 'name' is NULL then 'jop' is assumed to be a JSON array and
+ * a JSON string formed from 'value' is added. Note that the jsp->pr_string
+ * setting is ignored by this function. If successful returns a * a pointer
+ * newly formed JSON string. */
+sgj_opaque_p sgj_js_nv_s(sgj_state * jsp, sgj_opaque_p jop,
+                         const char * name, const char * value);
+sgj_opaque_p sgj_js_nv_s_len(sgj_state * jsp, sgj_opaque_p jop,
+                             const char * name,
+                             const char * value, int slen);
+
+/* If either jsp is NULL or jsp->pr_as_json is false then nothing happens and
+ * NULL is returned. The insertion point is at jop but if it is NULL
+ * jsp->basep is used. If 'name' is non-NULL a new named JSON object is
+ * added using 'name' and the associated value is a JSON integer formed from
+ * 'value'. If 'name' is NULL then 'jop' is assumed to be a JSON array and
+ * a JSON integer formed from 'value' is added. If successful returns a
+ * a pointer newly formed JSON integer. */
+sgj_opaque_p sgj_js_nv_i(sgj_state * jsp, sgj_opaque_p jop,
+                         const char * name, int64_t value);
+
+/* If either jsp is NULL or jsp->pr_as_json is false then nothing happens and
+ * NULL is returned. The insertion point is at jop but if it is NULL
+ * jsp->basep is used. If 'name' is non-NULL a new named JSON object is
+ * added using 'name' and the associated value is a JSON boolean formed from
+ * 'value'. If 'name' is NULL then 'jop' is assumed to be a JSON array and
+ * a JSON boolean formed from 'value' is added. If successful returns a
+ * a pointer newly formed JSON boolean. */
+sgj_opaque_p sgj_js_nv_b(sgj_state * jsp, sgj_opaque_p jop,
+                         const char * name, bool value);
+
+/* If jsp is NULL, jsp->pr_as_json is false or ua_jop is NULL nothing then
+ * happens and NULL is returned. 'jop' is the insertion point but if it is
+ * NULL jsp->basep is used instead. If 'name' is non-NULL a new named JSON
+ * object is added using 'name' and the associated value is ua_jop. If 'name'
+ * is NULL then 'jop' is assumed to be a JSON array and ua_jop is added to
+ * it. If successful returns ua_jop . The "ua_" prefix stands for unattached.
+ * That should be the case before invocation and it will be attached to jop
+ * after a successful invocation. This means that ua_jop must have been
+ * created by sgj_new_unattached_object_r() or similar. */
+sgj_opaque_p sgj_js_nv_o(sgj_state * jsp, sgj_opaque_p jop,
+                         const char * name, sgj_opaque_p ua_jop);
+
+/* This function only produces JSON output if jsp is non-NULL and
+ * jsp->pr_as_json is true. It adds a named object at 'jop' (or jop->basep
+ * if jop is NULL) along with a value. If jsp->pr_hex is true then that
+ * value is two sub-objects, one named 'i' with a 'value' as a JSON integer,
+ * the other one named 'hex' with 'value' rendered as hex in a JSON string.
+ * If jsp->pr_hex is false then there are no sub-objects and the 'value' is
+ * rendered as JSON integer. */
+void sgj_js_nv_ihex(sgj_state * jsp, sgj_opaque_p jop,
+                    const char * name, uint64_t value);
+
+/* This function only produces JSON output if jsp is non-NULL and
+ * jsp->pr_as_json is true. It adds a named object at 'jop' (or jop->basep
+ * if jop is NULL) along with a value. If jsp->pr_string is true then that
+ * value is two sub-objects, one named 'i' with a 'val_i' as a JSON integer,
+ * the other one named str_name with val_s rendered as a JSON string. If
+ * str_name is NULL then "meaning" will be used. If jsp->pr_string is false
+ * then there are no sub-objects and the 'val_i' is rendered as a JSON
+ * integer. */
+void sgj_js_nv_istr(sgj_state * jsp, sgj_opaque_p jop,
+                    const char * name, int64_t val_i,
+                    const char * str_name, const char * val_s);
+
+/* Similar to sgj_js_nv_istr(). The hex output is conditional jsp->pr_hex . */
+void sgj_js_nv_ihexstr(sgj_state * jsp, sgj_opaque_p jop,
+                       const char * name, int64_t val_i,
+                       const char * str_name, const char * val_s);
+
+/* This function only produces JSON output if jsp is non-NULL and
+ * jsp->pr_as_json is true. It adds a named object at 'jop' (or jop->basep
+ * if jop is NULL) along with a value. If jsp->pr_name_ex is true then that
+ * value is two sub-objects, one named 'i' with a 'val_i' as a JSON integer,
+ * the other one named "abbreviated_name_expansion" with value nex_s rendered
+ * as a JSON string. If jsp->pr_hex and 'hex_as_well' are true, then a
+ * sub-object named 'hex' with a value rendered as a hex string equal to
+ * val_i. If jsp->pr_name_ex is false and either jsp->pr_hex or hex_as_well are
+ * false then there are no sub-objects and the 'val_i' is rendered as a JSON
+ * integer. */
+void sgj_js_nv_ihex_nex(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+                        int64_t val_i, bool hex_as_well, const char * nex_s);
+
+void sgj_js_nv_ihexstr_nex(sgj_state * jsp, sgj_opaque_p jop,
+                           const char * name, int64_t val_i, bool hex_as_well,
+                           const char * str_name, const char * val_s,
+                           const char * nex_s);
+
+/* Add named field whose value is a (large) JSON string made up of num_bytes
+ * ASCII hexadecimal bytes (each two hex digits separated by a space) starting
+ * at byte_arr. The heap is used for intermediate storage so num_bytes can
+ * be arbitrarily large. */
+void sgj_js_nv_hex_bytes(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+                         const uint8_t * byte_arr, int num_bytes);
+
+/* The '_haj_' refers to generating output both for human readable and/or
+ * JSON with a single invocation. If jsp is non-NULL and jsp->pr_out_hr is
+ * true then both JSON and human readable output is formed (and the latter is
+ * placed in the jsp->out_hrp JSON array). The human readable form will have
+ * leadin_sp spaces followed by 'name' then a separator, then 'value' with a
+ * trailing LF. If 'name' is NULL then it and the separator are ignored. If
+ * there is JSON output, then leadin_sp and sep are ignored. If 'jop' is NULL
+ * then basep->basep is used. If 'name' is NULL then a JSON string object,
+ * made from 'value' is added to the JSON array pointed to by 'jop'.
+ * Otherwise a 'name'-d JSON object whose value is a JSON string object made
+ * from 'value' is added at 'jop'. */
+void sgj_haj_vs(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+                const char * name, enum sgj_separator_t sep,
+                const char * value);
+
+/* Similar to sgj_haj_vs()'s description with 'JSON string object'
+ * replaced by 'JSON integer object'. */
+void sgj_haj_vi(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+                const char * name, enum sgj_separator_t sep,
+                int64_t value, bool hex_as_well);
+void sgj_haj_vistr(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+                   const char * name, enum sgj_separator_t sep,
+                   int64_t value, bool hex_as_well, const char * val_s);
+
+/* The '_nex' refers to a "name_extra" (information) sub-object (a JSON
+ * string) which explains a bit more about the 'name' entry. This is useful
+ * when T10 specifies the name as an abbreviation (e.g. SYSV). Whether this
+ * sub-object is shown in the JSON output is controlled by the 'n' control
+ * character. */
+void sgj_haj_vi_nex(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+                    const char * name, enum sgj_separator_t sep,
+                    int64_t value, bool hex_as_well, const char * nex_s);
+void sgj_haj_vistr_nex(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+                       const char * name, enum sgj_separator_t sep,
+                       int64_t value, bool hex_as_well,
+                       const char * val_s, const char * nex_s);
+
+/* Similar to above '_haj_' calls but a named sub-object is always formed
+ * containing a JSON integer object named "i" whose value is 'value'. The
+ * returned pointer is to that sub-object. */
+sgj_opaque_p sgj_haj_subo_r(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+                            const char * name, enum sgj_separator_t sep,
+                            int64_t value, bool hex_as_well);
+
+/* Similar to sgj_haj_vs()'s description with 'JSON string object' replaced
+ * by 'JSON boolean object'. */
+void sgj_haj_vb(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+                const char * name, enum sgj_separator_t sep, bool value);
+
+/* Breaks up the string pointed to by 'sp' into lines and adds them to the
+ * jsp->out_hrp array. Treat '\n' in sp as line breaks. Consumes characters
+ * from sp until either a '\0' is found or slen is exhausted. Add each line
+ * to jsp->out_hrp JSON array (if conditions met). */
+void sgj_js_str_out(sgj_state * jsp, const char * sp, int slen);
+
+/* This function only produces JSON output if jsp is non-NULL and
+ * jsp->pr_as_json is true. 'sbp' is assumed to point to sense data as
+ * defined by T10 with a length of 'sb_len' bytes. Returns false if an
+ * issue is detected, else it returns true. */
+bool sgj_js_sense(sgj_state * jsp, sgj_opaque_p jop, const uint8_t * sbp,
+                  int sb_len);
+
+bool sgj_js_designation_descriptor(sgj_state * jsp, sgj_opaque_p jop,
+                                   const uint8_t * ddp, int dd_len);
+
+/* Nothing in the in-core JSON tree is actually printed to 'fp' (typically
+ * stdout) until this call is made. If jsp is NULL, jsp->pr_as_json is false
+ * or jsp->basep is NULL then this function does nothing. If jsp->exit_status
+ * is true then a new JSON object named "exit_status" and the 'exit_status'
+ * value rendered as a JSON integer is appended to jsp->basep. The in-core
+ * JSON tree with jsp->basep as its root is streamed to 'fp'. */
+void sgj_js2file(sgj_state * jsp, sgj_opaque_p jop, int exit_status,
+                 FILE * fp);
+
+/* This function is only needed if the pointer returned from either
+ * sgj_new_unattached_object_r() or sgj_new_unattached_array_r() has not
+ * been attached into the in-core JSON tree whose root is jsp->basep . */
+void sgj_free_unattached(sgj_opaque_p jop);
+
+/* If jsp is NULL or jsp->basep is NULL then this function does nothing.
+ * This function does bottom up, heap freeing of all the in-core JSON
+ * objects and arrays attached to the root JSON object assumed to be
+ * found at jsp->basep . After this call jsp->basep, jsp->out_hrp and
+ * jsp->userp will all be set to NULL.  */
+void sgj_finish(sgj_state * jsp);
+
+char * sg_json_usage(int char_if_not_j, char * b, int blen);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/include/sg_pt.h b/include/sg_pt.h
new file mode 100644
index 0000000..a1017f5
--- /dev/null
+++ b/include/sg_pt.h
@@ -0,0 +1,292 @@
+#ifndef SG_PT_H
+#define SG_PT_H
+
+/*
+ * Copyright (c) 2005-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* This declaration hides the fact that each implementation has its own
+ * structure "derived" (using a C++ term) from this one. It compiles
+ * because 'struct sg_pt_base' is only referenced (by pointer: 'objp')
+ * in this interface. An instance of this structure represents the
+ * context of one synchronous SCSI (or NVME) command and the context
+ * can be re-used. If an instance of sg_pt_base is shared across several
+ * threads then it is up to the application to take care of multi-threaded
+ * issues with that instance. */
+struct sg_pt_base;
+
+
+/* The format of the version string is like this: "3.04 20180213".
+ * The leading digit will be incremented if this interface changes
+ * in a way that may impact backward compatibility. */
+const char * scsi_pt_version();
+const char * sg_pt_version();   /* both functions give same result */
+
+
+/* Returns file descriptor or file handle and is >= 0 if successful.
+ * If error in Unix returns negated errno. */
+int scsi_pt_open_device(const char * device_name, bool read_only,
+                        int verbose);
+
+/* Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed
+ * together. Returns valid file descriptor or handle ( >= 0 ) if successful,
+ * otherwise returns -1 or a negated errno.
+ * In Win32 O_EXCL translated to equivalent. */
+int scsi_pt_open_flags(const char * device_name, int flags, int verbose);
+
+/* Returns 0 if successful. 'device_fd' should be a value that was previously
+ * returned by scsi_pt_open_device() or scsi_pt_open_flags() that has not
+ * already been closed. If error in Unix returns negated errno. */
+int scsi_pt_close_device(int device_fd);
+
+/* Assumes dev_fd is an "open" file handle associated with device_name. If
+ * the implementation (possibly for one OS) cannot determine from dev_fd if
+ * a SCSI or NVMe pass-through is referenced, then it might guess based on
+ * device_name. Returns 1 if SCSI generic pass-though device, returns 2 if
+ * secondary SCSI pass-through device (in Linux a bsg device); returns 3 is
+ * char NVMe device (i.e. no NSID); returns 4 if block NVMe device (includes
+ * NSID), 5 is also a NVMe device (FreeBSD CAM NVMe (e.g. /dev/nda0)) or 0
+ * if something else (e.g. ATA block device) or dev_fd < 0.
+ * The return value differs somewhat by OS.
+ * If error, returns negated errno (operating system) value. */
+int check_pt_file_handle(int dev_fd, const char * device_name, int verbose);
+
+
+/* Creates an object that can be used to issue one or more SCSI commands
+ * (or task management functions). Returns NULL if problem.
+ * Once this object has been created it should be destroyed with
+ * destruct_scsi_pt_obj() when it is no longer needed. */
+struct sg_pt_base * construct_scsi_pt_obj(void);
+
+/* An alternate and preferred way to create an object that can be used to
+ * issue one or more SCSI (or NVMe) commands (or task management functions).
+ * This variant associates a device file descriptor (handle) with the object
+ * and a verbose argument that causes messages to be written to stderr if
+ * errors occur. The reason for this is to optionally allow the detection of
+ * NVMe devices that will cause pt_device_is_nvme() to return true. Set
+ * dev_fd to -1 if no open device file descriptor is available. Caller
+ * should additionally call get_scsi_pt_os_err() after this call to check
+ * for errors. The dev_fd argument may be -1 to indicate no device file
+ * descriptor. */
+struct sg_pt_base *
+        construct_scsi_pt_obj_with_fd(int dev_fd, int verbose);
+
+/* Forget any previous dev_fd and install the one given. May attempt to
+ * find file type (e.g. if pass-though) from OS so there could be an error.
+ * Returns 0 for success or the same value as get_scsi_pt_os_err()
+ * will return. dev_fd should be >= 0 for a valid file handle or -1 . */
+int set_pt_file_handle(struct sg_pt_base * objp, int dev_fd, int verbose);
+
+/* Valid file handles (which is the return value) are >= 0 . Returns -1
+ * if there is no valid file handle. */
+int get_pt_file_handle(const struct sg_pt_base * objp);
+
+/* Clear state information held in *objp . This allows this object to be
+ * used to issue more than one SCSI command. The dev_fd is remembered.
+ * Use set_pt_file_handle() to change dev_fd. */
+void clear_scsi_pt_obj(struct sg_pt_base * objp);
+
+/* Partially clear state information held in *objp . Any error settings and
+ * the data-in and data-out settings are cleared. So dev_fd, cdb and sense
+ * settings are kept. */
+void partial_clear_scsi_pt_obj(struct sg_pt_base * objp);
+
+/* Set the CDB (command descriptor block). May also be a NVMe Admin command
+ * which will be 64 bytes long.
+ *
+ * Note that the sg_cmds_is_nvme() function found in sg_cmds_basic.h can be
+ * called after this function to "guess" which command set the given command
+ * belongs to. It is valid to supply a cdb value of NULL. */
+void set_scsi_pt_cdb(struct sg_pt_base * objp, const uint8_t * cdb,
+                     int cdb_len);
+
+/* Set the sense buffer and the maximum length of that buffer. For NVMe
+ * commands this "sense" buffer will receive the 4 DWORDs of from the
+ * completion queue. It is valid to supply a sense value of NULL. */
+void set_scsi_pt_sense(struct sg_pt_base * objp, uint8_t * sense,
+                       int max_sense_len);
+
+/* Set a pointer and length to be used for data transferred from device */
+void set_scsi_pt_data_in(struct sg_pt_base * objp,   /* from device */
+                         uint8_t * dxferp, int dxfer_ilen);
+
+/* Set a pointer and length to be used for data transferred to device */
+void set_scsi_pt_data_out(struct sg_pt_base * objp,    /* to device */
+                          const uint8_t * dxferp, int dxfer_olen);
+
+/* Set a pointer and length to be used for metadata transferred to
+ * (out_true=true) or from (out_true=false) device (NVMe only) */
+void set_pt_metadata_xfer(struct sg_pt_base * objp, uint8_t * mdxferp,
+                          uint32_t mdxfer_len, bool out_true);
+
+/* The following "set_"s implementations may be dummies */
+void set_scsi_pt_packet_id(struct sg_pt_base * objp, int pack_id);
+void set_scsi_pt_tag(struct sg_pt_base * objp, uint64_t tag);
+void set_scsi_pt_task_management(struct sg_pt_base * objp, int tmf_code);
+void set_scsi_pt_task_attr(struct sg_pt_base * objp, int attribute,
+                           int priority);
+
+/* Following is a guard which is defined when set_scsi_pt_flags() is
+ * present. Older versions of this library may not have this function. */
+#define SCSI_PT_FLAGS_FUNCTION 1
+/* If neither QUEUE_AT_HEAD nor QUEUE_AT_TAIL are given, or both
+ * are given, use the pass-through default. */
+#define SCSI_PT_FLAGS_QUEUE_AT_TAIL 0x10
+#define SCSI_PT_FLAGS_QUEUE_AT_HEAD 0x20
+/* Set (potentially OS dependent) flags for pass-through mechanism.
+ * Apart from contradictions, flags can be OR-ed together. */
+void set_scsi_pt_flags(struct sg_pt_base * objp, int flags);
+
+#define SCSI_PT_DO_START_OK 0
+#define SCSI_PT_DO_BAD_PARAMS 1
+#define SCSI_PT_DO_TIMEOUT 2
+#define SCSI_PT_DO_NOT_SUPPORTED 4
+#define SCSI_PT_DO_NVME_STATUS 48       /* == SG_LIB_NVME_STATUS */
+/* If OS error prior to or during command submission then returns negated
+ * error value (e.g. Unix '-errno'). This includes interrupted system calls
+ * (e.g. by a signal) in which case -EINTR would be returned. Note that
+ * system call errors also can be fetched with get_scsi_pt_os_err().
+ * Return 0 if okay (i.e. at the very least: command sent). Positive
+ * return values are errors (see SCSI_PT_DO_* defines). If a file descriptor
+ * has already been provided by construct_scsi_pt_obj_with_fd() then the
+ * given 'fd' can be -1 or the same value as given to the constructor. */
+int do_scsi_pt(struct sg_pt_base * objp, int fd, int timeout_secs,
+               int verbose);
+
+/* NVMe Admin commands can be sent directly to do_scsi_pt(). Unfortunately
+ * NVMe has at least one other command set: "NVM" to access user data and
+ * the opcodes in the NVM command set overlap with the Admin command set.
+ * So NVMe Admin commands should be sent do_scsi_pt() while NVMe "NVM"
+ * commands should be sent to this function. No SCSI commands should be
+ * sent to this function. Currently submq is not implemented and all
+ * submitted NVM commands are sent on queue 0, the same queue use for
+ * Admin commands. The return values follow the same pattern as do_scsi_pt(),
+ * with 0 returned being good.  The NVMe device file descriptor must either
+ * be given to the obj constructor, or a prior set_pt_file_handle() call. */
+int do_nvm_pt(struct sg_pt_base * objp, int submq, int timeout_secs,
+              int verbose);
+
+#define SCSI_PT_RESULT_GOOD 0
+#define SCSI_PT_RESULT_STATUS 1 /* other than GOOD and CHECK CONDITION */
+#define SCSI_PT_RESULT_SENSE 2
+#define SCSI_PT_RESULT_TRANSPORT_ERR 3
+#define SCSI_PT_RESULT_OS_ERR 4
+/* This function, called soon after do_scsi_pt(), returns one of the above
+ * result categories. The highest numbered applicable category is returned.
+ *
+ * Note that the sg_cmds_process_resp() function found in sg_cmds_basic.h
+ * is useful for processing SCSI command responses.
+ * And the sg_cmds_is_nvme() function found in sg_cmds_basic.h can be called
+ * after set_scsi_pt_cdb() to "guess" which command set the given command
+ * belongs to. */
+int get_scsi_pt_result_category(const struct sg_pt_base * objp);
+
+/* If not available return 0 which implies there is no residual value. If
+ * supported it is the number of bytes requested to transfer less the
+ * number actually transferred. This it typically important for data-in
+ * transfers. For data-out (only) transfers, the 'dout_req_len -
+ * dout_act_len' is returned. For bidi transfer the data-in residual is
+ * returned. */
+int get_scsi_pt_resid(const struct sg_pt_base * objp);
+
+/* Returns SCSI status value (from device that received the command). If an
+ * NVMe command was issued directly (i.e. through do_scsi_pt() then return
+ * NVMe status (i.e. ((SCT << 8) | SC)). If problem returns -1. */
+int get_scsi_pt_status_response(const struct sg_pt_base * objp);
+
+/* Returns SCSI status value or, if NVMe command given to do_scsi_pt(),
+ * then returns NVMe result (i.e. DWord(0) from completion queue). If
+ * 'objp' is NULL then returns 0xffffffff. */
+uint32_t get_pt_result(const struct sg_pt_base * objp);
+
+/* These two get functions should just echo what has been given to
+ * set_scsi_pt_cdb(). If it has not been called or clear_scsi_pt_obj()
+ * has been called then they return 0 and NULL respectively. */
+int get_scsi_pt_cdb_len(const struct sg_pt_base * objp);
+uint8_t * get_scsi_pt_cdb_buf(const struct sg_pt_base * objp);
+
+/* Actual sense length returned. If sense data is present but
+   actual sense length is not known, return 'max_sense_len' */
+int get_scsi_pt_sense_len(const struct sg_pt_base * objp);
+uint8_t * get_scsi_pt_sense_buf(const struct sg_pt_base * objp);
+
+/* If not available return 0 (for success). */
+int get_scsi_pt_os_err(const struct sg_pt_base * objp);
+char * get_scsi_pt_os_err_str(const struct sg_pt_base * objp, int max_b_len,
+                              char * b);
+
+/* If not available return 0 (for success) */
+int get_scsi_pt_transport_err(const struct sg_pt_base * objp);
+void set_scsi_pt_transport_err(struct sg_pt_base * objp, int err);
+char * get_scsi_pt_transport_err_str(const struct sg_pt_base * objp,
+                                     int max_b_len, char * b);
+
+/* If not available return -1 otherwise return number of milliseconds
+ * that the lower layers (and hardware) took to execute the previous
+ * command. */
+int get_scsi_pt_duration_ms(const struct sg_pt_base * objp);
+
+/* If not available return 0 otherwise return number of nanoseconds that the
+ * lower layers (and hardware) took to execute the command just completed. */
+uint64_t get_pt_duration_ns(const struct sg_pt_base * objp);
+
+/* The two functions yield requested and actual data transfer lengths in
+ * bytes. The second argument is a pointer to the data-in length; the third
+ * argument is a pointer to the data-out length. The pointers may be NULL.
+ * The _actual_ values are related to resid (residual count from DMA) */
+void get_pt_req_lengths(const struct sg_pt_base * objp, int * req_dinp,
+                        int * req_doutp);
+void get_pt_actual_lengths(const struct sg_pt_base * objp, int * act_dinp,
+                           int * act_doutp);
+
+/* Return true if device associated with 'objp' uses NVMe command set. To
+ * be useful (in modifying the type of command sent (SCSI or NVMe) then
+ * construct_scsi_pt_obj_with_fd() should be used followed by an invocation
+ * of this function. */
+bool pt_device_is_nvme(const struct sg_pt_base * objp);
+
+/* If a NVMe block device (which includes the NSID) handle is associated
+ * with 'objp', then its NSID is returned (values range from 0x1 to
+ * 0xffffffe). Otherwise 0 is returned. */
+uint32_t get_pt_nvme_nsid(const struct sg_pt_base * objp);
+
+
+/* Should be invoked once per objp after other processing is complete in
+ * order to clean up resources. For ever successful construct_scsi_pt_obj()
+ * call there should be one destruct_scsi_pt_obj(). If the
+ * construct_scsi_pt_obj_with_fd() function was used to create this object
+ * then the dev_fd provided to that constructor is not altered by this
+ * destructor. So the user should still close dev_fd (perhaps with
+ * scsi_pt_close_device() ).  */
+void destruct_scsi_pt_obj(struct sg_pt_base * objp);
+
+#ifdef SG_LIB_WIN32
+#define SG_LIB_WIN32_DIRECT 1
+
+/* Request SPT direct interface when state_direct is 1, state_direct set
+ * to 0 for the SPT indirect interface. Default setting selected by build
+ * (i.e. library compile time) and is usually indirect. */
+void scsi_pt_win32_direct(int state_direct);
+
+/* Returns current SPT interface state, 1 for direct, 0 for indirect */
+int scsi_pt_win32_spt_state(void);
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif          /* SG_PT_H */
diff --git a/include/sg_pt_linux.h b/include/sg_pt_linux.h
new file mode 100644
index 0000000..548a763
--- /dev/null
+++ b/include/sg_pt_linux.h
@@ -0,0 +1,202 @@
+#ifndef SG_PT_LINUX_H
+#define SG_PT_LINUX_H
+
+/*
+ * Copyright (c) 2017-2018 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <linux/types.h>
+
+#include "sg_pt_nvme.h"
+
+/* This header is for internal use by the sg3_utils library (libsgutils)
+ * and is Linux specific. Best not to include it directly in code that
+ * is meant to be OS independent. */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef HAVE_LINUX_BSG_H
+
+#define BSG_PROTOCOL_SCSI               0
+
+#define BSG_SUB_PROTOCOL_SCSI_CMD       0
+#define BSG_SUB_PROTOCOL_SCSI_TMF       1
+#define BSG_SUB_PROTOCOL_SCSI_TRANSPORT 2
+
+/*
+ * For flag constants below:
+ * sg.h sg_io_hdr also has bits defined for it's flags member. These
+ * two flag values (0x10 and 0x20) have the same meaning in sg.h . For
+ * bsg the BSG_FLAG_Q_AT_HEAD flag is ignored since it is the default.
+ */
+#define BSG_FLAG_Q_AT_TAIL 0x10 /* default is Q_AT_HEAD */
+#define BSG_FLAG_Q_AT_HEAD 0x20
+
+#ifndef SGV4_FLAG_YIELD_TAG
+#define SGV4_FLAG_YIELD_TAG 0x8
+#endif
+#ifndef SGV4_FLAG_FIND_BY_TAG
+#define SGV4_FLAG_FIND_BY_TAG 0x100
+#endif
+#ifndef SGV4_FLAG_IMMED
+#define SGV4_FLAG_IMMED 0x400
+#endif
+#ifndef SGV4_FLAG_IMMED
+#define SGV4_FLAG_IMMED 0x400
+#endif
+#ifndef SGV4_FLAG_DEV_SCOPE
+#define SGV4_FLAG_DEV_SCOPE 0x1000
+#endif
+#ifndef SGV4_FLAG_SHARE
+#define SGV4_FLAG_SHARE 0x2000
+#endif
+
+struct sg_io_v4 {
+        __s32 guard;            /* [i] 'Q' to differentiate from v3 */
+        __u32 protocol;         /* [i] 0 -> SCSI , .... */
+        __u32 subprotocol;      /* [i] 0 -> SCSI command, 1 -> SCSI task
+                                   management function, .... */
+
+        __u32 request_len;      /* [i] in bytes */
+        __u64 request;          /* [i], [*i] {SCSI: cdb} */
+        __u64 request_tag;      /* [i] {in sg 4.0+ this is out parameter} */
+        __u32 request_attr;     /* [i] {SCSI: task attribute} */
+        __u32 request_priority; /* [i] {SCSI: task priority} */
+        __u32 request_extra;    /* [i] {used for pack_id} */
+        __u32 max_response_len; /* [i] in bytes */
+        __u64 response;         /* [i], [*o] {SCSI: (auto)sense data} */
+
+        /* "dout_": data out (to device); "din_": data in (from device) */
+        __u32 dout_iovec_count; /* [i] 0 -> "flat" dout transfer else
+                                   dout_xfer points to array of iovec */
+        __u32 dout_xfer_len;    /* [i] bytes to be transferred to device */
+        __u32 din_iovec_count;  /* [i] 0 -> "flat" din transfer */
+        __u32 din_xfer_len;     /* [i] bytes to be transferred from device */
+        __u64 dout_xferp;       /* [i], [*i] */
+        __u64 din_xferp;        /* [i], [*o] */
+
+        __u32 timeout;          /* [i] units: millisecond */
+        __u32 flags;            /* [i] bit mask */
+        __u64 usr_ptr;          /* [i->o] unused internally */
+        __u32 spare_in;         /* [i] */
+
+        __u32 driver_status;    /* [o] 0 -> ok */
+        __u32 transport_status; /* [o] 0 -> ok */
+        __u32 device_status;    /* [o] {SCSI: command completion status} */
+        __u32 retry_delay;      /* [o] {SCSI: status auxiliary information} */
+        __u32 info;             /* [o] additional information */
+        __u32 duration;         /* [o] time to complete, in milliseconds */
+        __u32 response_len;     /* [o] bytes of response actually written */
+        __s32 din_resid;        /* [o] din_xfer_len - actual_din_xfer_len */
+        __s32 dout_resid;       /* [o] dout_xfer_len - actual_dout_xfer_len */
+        __u64 generated_tag;    /* [o] {SCSI: transport generated task tag} */
+        __u32 spare_out;        /* [o] */
+
+        __u32 padding;
+};
+
+#else
+
+#include <linux/bsg.h>
+
+#endif
+
+
+struct sg_pt_linux_scsi {
+    struct sg_io_v4 io_hdr;     /* use v4 header as it is more general */
+    /* Leave io_hdr in first place of this structure */
+    bool is_sg;
+    bool is_bsg;
+    bool is_nvme;       /* OS device type, if false ignore nvme_our_sntl */
+    bool nvme_our_sntl; /* true: our SNTL; false: received NVMe command */
+    bool nvme_stat_dnr; /* Do No Retry, part of completion status field */
+    bool nvme_stat_more; /* More, part of completion status field */
+    bool mdxfer_out;    /* direction of metadata xfer, true->data-out */
+    int dev_fd;                 /* -1 if not given (yet) */
+    int in_err;
+    int os_err;
+    int sg_version;     /* for deciding whether to use v3 or v4 interface */
+    uint32_t nvme_nsid;         /* 1 to 0xfffffffe are possibly valid, 0
+                                 * implies dev_fd is not a NVMe device
+                                 * (is_nvme=false) or it is a NVMe char
+                                 * device (e.g. /dev/nvme0 ) */
+    uint32_t nvme_result;       /* DW0 from completion queue */
+    uint32_t nvme_status;       /* SCT|SC: DW3 27:17 from completion queue,
+                                 * note: the DNR+More bit are not there.
+                                 * The whole 16 byte completion q entry is
+                                 * sent back as sense data */
+    uint32_t mdxfer_len;
+    struct sg_sntl_dev_state_t dev_stat;
+    void * mdxferp;
+    uint8_t * nvme_id_ctlp;     /* cached response to controller IDENTIFY */
+    uint8_t * free_nvme_id_ctlp;
+    uint8_t tmf_request[4];
+};
+
+struct sg_pt_base {
+    struct sg_pt_linux_scsi impl;
+};
+
+
+#ifndef sg_nvme_admin_cmd
+#define sg_nvme_admin_cmd sg_nvme_passthru_cmd
+#endif
+
+/* Linux NVMe related ioctls */
+#ifndef NVME_IOCTL_ID
+#define NVME_IOCTL_ID           _IO('N', 0x40)
+#endif
+#ifndef NVME_IOCTL_ADMIN_CMD
+#define NVME_IOCTL_ADMIN_CMD    _IOWR('N', 0x41, struct sg_nvme_admin_cmd)
+#endif
+#ifndef NVME_IOCTL_SUBMIT_IO
+#define NVME_IOCTL_SUBMIT_IO    _IOW('N', 0x42, struct sg_nvme_user_io)
+#endif
+#ifndef NVME_IOCTL_IO_CMD
+#define NVME_IOCTL_IO_CMD       _IOWR('N', 0x43, struct sg_nvme_passthru_cmd)
+#endif
+#ifndef NVME_IOCTL_RESET
+#define NVME_IOCTL_RESET        _IO('N', 0x44)
+#endif
+#ifndef NVME_IOCTL_SUBSYS_RESET
+#define NVME_IOCTL_SUBSYS_RESET _IO('N', 0x45)
+#endif
+#ifndef NVME_IOCTL_RESCAN
+#define NVME_IOCTL_RESCAN       _IO('N', 0x46)
+#endif
+#if 0
+#define NVME_IOCTL_ADMIN64_CMD  _IOWR('N', 0x47, struct nvme_passthru_cmd64)
+#define NVME_IOCTL_IO64_CMD     _IOWR('N', 0x48, struct nvme_passthru_cmd64)
+#endif
+
+extern bool sg_bsg_nvme_char_major_checked;
+extern int sg_bsg_major;
+extern volatile int sg_nvme_char_major;
+extern long sg_lin_page_size;
+
+void sg_find_bsg_nvme_char_major(int verbose);
+int sg_do_nvme_pt(struct sg_pt_base * vp, int fd, int time_secs, int vb);
+int sg_linux_get_sg_version(const struct sg_pt_base * vp);
+
+/* This trims given NVMe block device name in Linux (e.g. /dev/nvme0n1p5)
+ * to the name of its associated char device (e.g. /dev/nvme0). If this
+ * occurs true is returned and the char device name is placed in 'b' (as
+ * long as b_len is sufficient). Otherwise false is returned. */
+bool sg_get_nvme_char_devname(const char * nvme_block_devname, uint32_t b_len,
+                              char * b);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif          /* end of SG_PT_LINUX_H */
diff --git a/include/sg_pt_nvme.h b/include/sg_pt_nvme.h
new file mode 100644
index 0000000..06f8f0f
--- /dev/null
+++ b/include/sg_pt_nvme.h
@@ -0,0 +1,224 @@
+#ifndef SG_PT_NVME_H
+#define SG_PT_NVME_H
+
+/*
+ * Copyright (c) 2017-2019 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* structures copied and slightly modified from <linux/nvme_ioctl.h> which
+ * is Copyright (c) 2011-2014, Intel Corporation.  */
+
+
+/* Note that the command input structure is in (packed) "cpu" format. That
+ * means, for example, if the CPU is little endian (most are) then so is the
+ * structure. However what comes out in the data-in buffer (e.g. for the
+ * Admin Identify command response) is almost all little endian following ATA
+ * (but no SCSI and IP which are big endian) and Intel's preference. There
+ * are exceptions, for example the EUI-64 identifiers in the Admin Identify
+ * response are big endian.
+ *
+ * Code online (e.g. nvme-cli at github.com) seems to favour packed
+ * structures, while the author prefers byte offset plus a range of unaligned
+ * integer builders such as those in sg_unaligned.h .
+ */
+
+#ifdef __GNUC__
+#ifndef __clang__
+  struct __attribute__((__packed__)) sg_nvme_user_io
+#else
+  struct sg_nvme_user_io
+#endif
+#else
+struct sg_nvme_user_io
+#endif
+{
+        uint8_t opcode;
+        uint8_t flags;
+        uint16_t control;
+        uint16_t nblocks;
+        uint16_t rsvd;
+        uint64_t metadata;
+        uint64_t addr;
+        uint64_t slba;
+        uint32_t dsmgmt;
+        uint32_t reftag;
+        uint16_t apptag;
+        uint16_t appmask;
+}
+#ifdef SG_LIB_FREEBSD
+__packed;
+#else
+;
+#endif
+
+/* Using byte offsets and unaligned be/le copies safer than packed
+ * structures. These are for sg_nvme_user_io . */
+#define SG_NVME_IO_OPCODE 0
+#define SG_NVME_IO_FLAGS 1
+#define SG_NVME_IO_CONTROL 2
+#define SG_NVME_IO_NBLOCKS 4
+#define SG_NVME_IO_RSVD 6
+#define SG_NVME_IO_METADATA 8
+#define SG_NVME_IO_ADDR 16
+#define SG_NVME_IO_SLBA 24
+#define SG_NVME_IO_DSMGMT 32
+#define SG_NVME_IO_REFTAG 36
+#define SG_NVME_IO_APPTAG 40
+#define SG_NVME_IO_APPMASK 42
+
+#ifdef __GNUC__
+#ifndef __clang__
+  struct __attribute__((__packed__)) sg_nvme_passthru_cmd
+#else
+  struct sg_nvme_passthru_cmd
+#endif
+#else
+struct sg_nvme_passthru_cmd
+#endif
+{
+        uint8_t opcode;
+        uint8_t flags;
+        uint16_t rsvd1;
+        uint32_t nsid;
+        uint32_t cdw2;
+        uint32_t cdw3;
+        uint64_t metadata;
+        uint64_t addr;
+        uint32_t metadata_len;
+        uint32_t data_len;
+        uint32_t cdw10;
+        uint32_t cdw11;
+        uint32_t cdw12;
+        uint32_t cdw13;
+        uint32_t cdw14;
+        uint32_t cdw15;
+#ifdef SG_LIB_LINUX
+        uint32_t timeout_ms;
+        uint32_t result;        /* out: DWord(0) from completion queue */
+#endif
+}
+#ifdef SG_LIB_FREEBSD
+__packed;
+#else
+;
+#endif
+
+struct sg_sntl_dev_state_t {
+    uint8_t scsi_dsense;
+    uint8_t enclosure_override; /* ENC_OV in sdparm */
+    uint8_t pdt;        /* 6 bit value in INQUIRY response */
+    uint8_t enc_serv;   /* single bit in INQUIRY response */
+    uint8_t id_ctl253;  /* NVMSR field of Identify controller (byte 253) */
+    bool wce;		/* Write Cache Enable (WCE) setting */
+    bool wce_changed;	/* WCE setting has been changed */
+};
+
+struct sg_sntl_result_t {
+    uint8_t sstatus;
+    uint8_t sk;
+    uint8_t asc;
+    uint8_t ascq;
+    uint8_t in_byte;
+    uint8_t in_bit;     /* use 255 for 'no bit position given' */
+};
+
+struct sg_opcode_info_t {
+    uint8_t opcode;
+    uint16_t sa;            /* service action, 0 for none */
+    uint32_t flags;         /* OR-ed set of F_* flags */
+    uint8_t len_mask[16];   /* len=len_mask[0], then mask for cdb[1]... */
+                            /* ignore cdb bytes after position 15 */
+};
+
+/* Using byte offsets and unaligned be/le copies safer than packed
+ * structures. These are for sg_nvme_passthru_cmd . */
+#define SG_NVME_PT_OPCODE 0             /* length: 1 byte */
+#define SG_NVME_PT_FLAGS 1              /* length: 1 byte */
+#define SG_NVME_PT_RSVD1 2              /* length: 2 bytes */
+#define SG_NVME_PT_NSID 4               /* length: 4 bytes */
+#define SG_NVME_PT_CDW2 8               /* length: 4 bytes */
+#define SG_NVME_PT_CDW3 12              /* length: 4 bytes */
+#define SG_NVME_PT_METADATA 16          /* length: 8 bytes */
+#define SG_NVME_PT_ADDR 24              /* length: 8 bytes */
+#define SG_NVME_PT_METADATA_LEN 32      /* length: 4 bytes */
+#define SG_NVME_PT_DATA_LEN 36          /* length: 4 bytes */
+#define SG_NVME_PT_CDW10 40             /* length: 4 bytes */
+#define SG_NVME_PT_CDW11 44             /* length: 4 bytes */
+#define SG_NVME_PT_CDW12 48             /* length: 4 bytes */
+#define SG_NVME_PT_CDW13 52             /* length: 4 bytes */
+#define SG_NVME_PT_CDW14 56             /* length: 4 bytes */
+#define SG_NVME_PT_CDW15 60             /* length: 4 bytes */
+
+#ifdef SG_LIB_LINUX
+/* General references state that "all NVMe commands are 64 bytes long". If
+ * so then the following are add-ons by Linux, go to the OS and not the
+ * the NVMe device. */
+#define SG_NVME_PT_TIMEOUT_MS 64        /* length: 4 bytes */
+#define SG_NVME_PT_RESULT 68            /* length: 4 bytes */
+#endif
+
+/* Byte offset of Result and Status (plus phase bit) in CQ */
+#define SG_NVME_PT_CQ_RESULT 0          /* CDW0, length: 4 bytes */
+#define SG_NVME_PT_CQ_DW0 0             /* CDW0, length: 4 bytes */
+#define SG_NVME_PT_CQ_DW1 4             /* CDW1, length: 4 bytes */
+#define SG_NVME_PT_CQ_DW2 8             /* CDW2, length: 4 bytes */
+#define SG_NVME_PT_CQ_DW3 12            /* CDW3, length: 4 bytes */
+#define SG_NVME_PT_CQ_STATUS_P 14       /* CDW3 31:16, length: 2 bytes */
+
+
+/* Valid namespace IDs (nsid_s) range from 1 to 0xfffffffe, leaving: */
+#define SG_NVME_BROADCAST_NSID 0xffffffff       /* all namespaces */
+#define SG_NVME_CTL_NSID 0x0            /* the "controller's" namespace */
+
+/* Vendor specific (sg3_utils) VPD pages */
+#define SG_NVME_VPD_NICR 0xde   /* NVME Identify controller response */
+
+
+/* Given the NVMe Identify Controller response and optionally the NVMe
+ * Identify Namespace response (NULL otherwise), generate the SCSI VPD
+ * page 0x83 (device identification) descriptor(s) in dop. Return the
+ * number of bytes written which will not exceed max_do_len. Probably use
+ * Peripheral Device Type (pdt) of 0 (disk) for don't know. Transport
+ * protocol (tproto) should be -1 if not known, else SCSI value.
+ * N.B. Does not write total VPD page length into dop[2:3] . */
+int sg_make_vpd_devid_for_nvme(const uint8_t * nvme_id_ctl_p,
+                               const uint8_t * nvme_id_ns_p, int pdt,
+                               int tproto, uint8_t * dop, int max_do_len);
+
+/* Initialize dev_stat pointed to by dsp */
+void sntl_init_dev_stat(struct sg_sntl_dev_state_t * dsp);
+
+/* Internal function (common to all OSes) to support the SNTL SCSI MODE
+ * SENSE(10) command. Has a vendor specific Unit Attention mpage which
+ * has only one field currently: ENC_OV (enclosure override) */
+int sntl_resp_mode_sense10(const struct sg_sntl_dev_state_t * dsp,
+                           const uint8_t * cdbp, uint8_t * dip, int mx_di_len,
+                           struct sg_sntl_result_t * resp);
+
+/* Internal function (common to all OSes) to support the SNTL SCSI MODE
+ * SELECT(10) command. */
+int sntl_resp_mode_select10(struct sg_sntl_dev_state_t * dsp,
+                            const uint8_t * cdbp, const uint8_t * dop,
+                            int do_len, struct sg_sntl_result_t * resp);
+
+/* Returns pointer to array of struct sg_opcode_info_t of SCSI commands
+ * translated to NVMe. */
+const struct sg_opcode_info_t * sg_get_opcode_translation(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif          /* SG_PT_NVME_H */
diff --git a/include/sg_pt_win32.h b/include/sg_pt_win32.h
new file mode 100644
index 0000000..a19485d
--- /dev/null
+++ b/include/sg_pt_win32.h
@@ -0,0 +1,473 @@
+#ifndef SG_PT_WIN32_H
+#define SG_PT_WIN32_H
+/*
+ * The information in this file was obtained from scsi-wnt.h by
+ * Richard Stemmer, rs@epost.de . He in turn gives credit to
+ * Jay A. Key (for scsipt.c).
+ * The plscsi program (by Pat LaVarre <p.lavarre@ieee.org>) has
+ * also been used as a reference.
+ * Much of the information in this header can also be obtained
+ * from msdn.microsoft.com .
+ * Updated for cygwin version 1.7.17 changes 20121026
+ */
+
+/* WIN32_LEAN_AND_MEAN may be required to prevent inclusion of <winioctl.h> */
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define SCSI_MAX_SENSE_LEN 64
+#define SCSI_MAX_CDB_LEN 16
+#define SCSI_MAX_INDIRECT_DATA 16384
+
+typedef struct {
+    USHORT          Length;
+    UCHAR           ScsiStatus;
+    UCHAR           PathId;
+    UCHAR           TargetId;
+    UCHAR           Lun;
+    UCHAR           CdbLength;
+    UCHAR           SenseInfoLength;
+    UCHAR           DataIn;
+    ULONG           DataTransferLength;
+    ULONG           TimeOutValue;
+    ULONG_PTR       DataBufferOffset;  /* was ULONG; problem in 64 bit */
+    ULONG           SenseInfoOffset;
+    UCHAR           Cdb[SCSI_MAX_CDB_LEN];
+} SCSI_PASS_THROUGH, *PSCSI_PASS_THROUGH;
+
+
+typedef struct {
+    USHORT          Length;
+    UCHAR           ScsiStatus;
+    UCHAR           PathId;
+    UCHAR           TargetId;
+    UCHAR           Lun;
+    UCHAR           CdbLength;
+    UCHAR           SenseInfoLength;
+    UCHAR           DataIn;
+    ULONG           DataTransferLength;
+    ULONG           TimeOutValue;
+    PVOID           DataBuffer;
+    ULONG           SenseInfoOffset;
+    UCHAR           Cdb[SCSI_MAX_CDB_LEN];
+} SCSI_PASS_THROUGH_DIRECT, *PSCSI_PASS_THROUGH_DIRECT;
+
+
+typedef struct {
+    SCSI_PASS_THROUGH spt;
+    /* plscsi shows a follow on 16 bytes allowing 32 byte cdb */
+    ULONG           Filler;
+    UCHAR           ucSenseBuf[SCSI_MAX_SENSE_LEN];
+    UCHAR           ucDataBuf[SCSI_MAX_INDIRECT_DATA];
+} SCSI_PASS_THROUGH_WITH_BUFFERS, *PSCSI_PASS_THROUGH_WITH_BUFFERS;
+
+
+typedef struct {
+    SCSI_PASS_THROUGH_DIRECT spt;
+    ULONG           Filler;
+    UCHAR           ucSenseBuf[SCSI_MAX_SENSE_LEN];
+} SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER, *PSCSI_PASS_THROUGH_DIRECT_WITH_BUFFER;
+
+
+
+typedef struct {
+    UCHAR           NumberOfLogicalUnits;
+    UCHAR           InitiatorBusId;
+    ULONG           InquiryDataOffset;
+} SCSI_BUS_DATA, *PSCSI_BUS_DATA;
+
+
+typedef struct {
+    UCHAR           NumberOfBusses;
+    SCSI_BUS_DATA   BusData[1];
+} SCSI_ADAPTER_BUS_INFO, *PSCSI_ADAPTER_BUS_INFO;
+
+
+typedef struct {
+    UCHAR           PathId;
+    UCHAR           TargetId;
+    UCHAR           Lun;
+    BOOLEAN         DeviceClaimed;
+    ULONG           InquiryDataLength;
+    ULONG           NextInquiryDataOffset;
+    UCHAR           InquiryData[1];
+} SCSI_INQUIRY_DATA, *PSCSI_INQUIRY_DATA;
+
+
+typedef struct {
+    ULONG           Length;
+    UCHAR           PortNumber;
+    UCHAR           PathId;
+    UCHAR           TargetId;
+    UCHAR           Lun;
+} SCSI_ADDRESS, *PSCSI_ADDRESS;
+
+/*
+ * Standard IOCTL define
+ */
+#ifndef CTL_CODE
+#define CTL_CODE(DevType, Function, Method, Access)             \
+        (((DevType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
+#endif
+
+/*
+ * file access values
+ */
+#ifndef FILE_ANY_ACCESS
+#define FILE_ANY_ACCESS         0
+#endif
+#ifndef FILE_READ_ACCESS
+#define FILE_READ_ACCESS        0x0001
+#endif
+#ifndef FILE_WRITE_ACCESS
+#define FILE_WRITE_ACCESS       0x0002
+#endif
+
+// IOCTL_STORAGE_QUERY_PROPERTY
+
+#define FILE_DEVICE_MASS_STORAGE    0x0000002d
+#define IOCTL_STORAGE_BASE          FILE_DEVICE_MASS_STORAGE
+#define FILE_ANY_ACCESS             0
+
+// #define METHOD_BUFFERED             0
+
+#define IOCTL_STORAGE_QUERY_PROPERTY \
+    CTL_CODE(IOCTL_STORAGE_BASE, 0x0500, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+
+#ifndef _DEVIOCTL_
+typedef enum _STORAGE_BUS_TYPE {
+    BusTypeUnknown      = 0x00,
+    BusTypeScsi         = 0x01,
+    BusTypeAtapi        = 0x02,
+    BusTypeAta          = 0x03,
+    BusType1394         = 0x04,
+    BusTypeSsa          = 0x05,
+    BusTypeFibre        = 0x06,
+    BusTypeUsb          = 0x07,
+    BusTypeRAID         = 0x08,
+    BusTypeiScsi        = 0x09,
+    BusTypeSas          = 0x0A,
+    BusTypeSata         = 0x0B,
+    BusTypeSd           = 0x0C,
+    BusTypeMmc          = 0x0D,
+    BusTypeVirtual             = 0xE,
+    BusTypeFileBackedVirtual   = 0xF,
+    BusTypeSpaces       = 0x10,
+    BusTypeNvme         = 0x11,
+    BusTypeSCM          = 0x12,
+    BusTypeUfs          = 0x13,
+    BusTypeMax          = 0x14,
+    BusTypeMaxReserved  = 0x7F
+} STORAGE_BUS_TYPE, *PSTORAGE_BUS_TYPE;
+
+typedef enum _STORAGE_PROTOCOL_TYPE {
+    ProtocolTypeUnknown = 0,
+    ProtocolTypeScsi,
+    ProtocolTypeAta,
+    ProtocolTypeNvme,
+    ProtocolTypeSd
+} STORAGE_PROTOCOL_TYPE;
+
+typedef enum _STORAGE_PROTOCOL_NVME_DATA_TYPE {
+    NVMeDataTypeUnknown = 0,
+    NVMeDataTypeIdentify,
+    NVMeDataTypeLogPage,
+    NVMeDataTypeFeature
+} STORAGE_PROTOCOL_NVME_DATA_TYPE;
+
+typedef struct _STORAGE_PROTOCOL_SPECIFIC_DATA {
+    STORAGE_PROTOCOL_TYPE ProtocolType;
+    ULONG DataType;
+    ULONG ProtocolDataRequestValue;
+    ULONG ProtocolDataRequestSubValue;
+    ULONG ProtocolDataOffset;
+    ULONG ProtocolDataLength;
+    ULONG FixedProtocolReturnData;
+    ULONG Reserved[3];
+} STORAGE_PROTOCOL_SPECIFIC_DATA;
+
+
+typedef struct _STORAGE_DEVICE_DESCRIPTOR {
+    ULONG Version;
+    ULONG Size;
+    UCHAR DeviceType;
+    UCHAR DeviceTypeModifier;
+    BOOLEAN RemovableMedia;
+    BOOLEAN CommandQueueing;
+    ULONG VendorIdOffset;       /* 0 if not available */
+    ULONG ProductIdOffset;      /* 0 if not available */
+    ULONG ProductRevisionOffset;/* 0 if not available */
+    ULONG SerialNumberOffset;   /* -1 if not available ?? */
+    STORAGE_BUS_TYPE BusType;
+    ULONG RawPropertiesLength;
+    UCHAR RawDeviceProperties[1];
+} STORAGE_DEVICE_DESCRIPTOR, *PSTORAGE_DEVICE_DESCRIPTOR;
+
+#define STORAGE_PROTOCOL_STRUCTURE_VERSION 0x1
+
+#define IOCTL_STORAGE_PROTOCOL_COMMAND \
+        CTL_CODE(IOCTL_STORAGE_BASE, 0x04F0, METHOD_BUFFERED, \
+                FILE_READ_ACCESS | FILE_WRITE_ACCESS)
+
+typedef struct _STORAGE_PROTOCOL_COMMAND {
+    DWORD         Version;        /* STORAGE_PROTOCOL_STRUCTURE_VERSION */
+    DWORD         Length;
+    STORAGE_PROTOCOL_TYPE   ProtocolType;
+    DWORD         Flags;
+    DWORD         ReturnStatus;
+    DWORD         ErrorCode;
+    DWORD         CommandLength;
+    DWORD         ErrorInfoLength;
+    DWORD         DataToDeviceTransferLength;
+    DWORD         DataFromDeviceTransferLength;
+    DWORD         TimeOutValue;
+    DWORD         ErrorInfoOffset;
+    DWORD         DataToDeviceBufferOffset;
+    DWORD         DataFromDeviceBufferOffset;
+    DWORD         CommandSpecific;
+    DWORD         Reserved0;
+    DWORD         FixedProtocolReturnData;
+    DWORD         Reserved1[3];
+    BYTE          Command[1];     /* has CommandLength elements */
+} STORAGE_PROTOCOL_COMMAND, *PSTORAGE_PROTOCOL_COMMAND;
+
+#endif          /* _DEVIOCTL_ */
+
+typedef struct _STORAGE_DEVICE_UNIQUE_IDENTIFIER {
+    ULONG  Version;
+    ULONG  Size;
+    ULONG  StorageDeviceIdOffset;
+    ULONG  StorageDeviceOffset;
+    ULONG  DriveLayoutSignatureOffset;
+} STORAGE_DEVICE_UNIQUE_IDENTIFIER, *PSTORAGE_DEVICE_UNIQUE_IDENTIFIER;
+
+// Use CompareStorageDuids(PSTORAGE_DEVICE_UNIQUE_IDENTIFIER duid1, duid2)
+// to test for equality
+
+#ifndef _DEVIOCTL_
+typedef enum _STORAGE_QUERY_TYPE {
+    PropertyStandardQuery = 0,
+    PropertyExistsQuery,
+    PropertyMaskQuery,
+    PropertyQueryMaxDefined
+} STORAGE_QUERY_TYPE, *PSTORAGE_QUERY_TYPE;
+
+typedef enum _STORAGE_PROPERTY_ID {
+    StorageDeviceProperty = 0,
+    StorageAdapterProperty,
+    StorageDeviceIdProperty,
+    StorageDeviceUniqueIdProperty,
+    StorageDeviceWriteCacheProperty,
+    StorageMiniportProperty,
+    StorageAccessAlignmentProperty,
+    /* Identify controller goes to adapter; Identify namespace to device */
+    StorageAdapterProtocolSpecificProperty = 49,
+    StorageDeviceProtocolSpecificProperty = 50
+} STORAGE_PROPERTY_ID, *PSTORAGE_PROPERTY_ID;
+
+typedef struct _STORAGE_PROPERTY_QUERY {
+    STORAGE_PROPERTY_ID PropertyId;
+    STORAGE_QUERY_TYPE QueryType;
+    UCHAR AdditionalParameters[1];
+} STORAGE_PROPERTY_QUERY, *PSTORAGE_PROPERTY_QUERY;
+
+typedef struct _STORAGE_PROTOCOL_DATA_DESCRIPTOR {
+    DWORD  Version;
+    DWORD  Size;
+    STORAGE_PROTOCOL_SPECIFIC_DATA ProtocolSpecificData;
+} STORAGE_PROTOCOL_DATA_DESCRIPTOR, *PSTORAGE_PROTOCOL_DATA_DESCRIPTOR;
+
+// Command completion status
+// The "Phase Tag" field and "Status Field" are separated in spec. We define
+// them in the same data structure to ease the memory access from software.
+//
+typedef union {
+    struct {
+        USHORT  P           : 1;        // Phase Tag (P)
+
+        USHORT  SC          : 8;        // Status Code (SC)
+        USHORT  SCT         : 3;        // Status Code Type (SCT)
+        USHORT  Reserved    : 2;
+        USHORT  M           : 1;        // More (M)
+        USHORT  DNR         : 1;        // Do Not Retry (DNR)
+    } DUMMYSTRUCTNAME;
+    USHORT AsUshort;
+} NVME_COMMAND_STATUS, *PNVME_COMMAND_STATUS;
+
+// Information of log: NVME_LOG_PAGE_ERROR_INFO. Size: 64 bytes
+//
+typedef struct {
+    ULONGLONG  ErrorCount;
+    USHORT     SQID;           // Submission Queue ID
+    USHORT     CMDID;          // Command ID
+    NVME_COMMAND_STATUS Status; // Status Field: This field indicates the
+                                // Status Field for the command that
+                                // completed. The Status Field is located in
+                                // bits 15:01, bit 00 corresponds to the Phase
+                                // Tag posted for the command.
+    struct {
+        USHORT  Byte        : 8;   // Byte in command that contained error
+        USHORT  Bit         : 3;   // Bit in command that contained error
+        USHORT  Reserved    : 5;
+    } ParameterErrorLocation;
+
+    ULONGLONG  Lba;            // LBA: This field indicates the first LBA
+                               // that experienced the error condition, if
+                               // applicable.
+    ULONG      NameSpace;      // Namespace: This field indicates the nsid
+                               // that the error is associated with, if
+                               // applicable.
+    UCHAR      VendorInfoAvailable;    // Vendor Specific Information Available
+    UCHAR      Reserved0[3];
+    ULONGLONG  CommandSpecificInfo;    // This field contains command specific
+                                       // information. If used, the command
+                                       // definition specifies the information
+                                       // returned.
+    UCHAR      Reserved1[24];
+} NVME_ERROR_INFO_LOG, *PNVME_ERROR_INFO_LOG;
+
+typedef struct {
+
+    ULONG   DW0;
+    ULONG   Reserved;
+
+    union {
+        struct {
+            USHORT  SQHD;               // SQ Head Pointer (SQHD)
+            USHORT  SQID;               // SQ Identifier (SQID)
+        } DUMMYSTRUCTNAME;
+
+        ULONG   AsUlong;
+    } DW2;
+
+    union {
+        struct {
+            USHORT              CID;    // Command Identifier (CID)
+            NVME_COMMAND_STATUS Status;
+        } DUMMYSTRUCTNAME;
+
+        ULONG   AsUlong;
+    } DW3;
+
+} NVME_COMPLETION_ENTRY, *PNVME_COMPLETION_ENTRY;
+
+
+// Bit-mask values for STORAGE_PROTOCOL_COMMAND - "Flags" field.
+//
+// Flag indicates the request targeting to adapter instead of device.
+#define STORAGE_PROTOCOL_COMMAND_FLAG_ADAPTER_REQUEST    0x80000000
+
+//
+// Status values for STORAGE_PROTOCOL_COMMAND - "ReturnStatus" field.
+//
+#define STORAGE_PROTOCOL_STATUS_PENDING                 0x0
+#define STORAGE_PROTOCOL_STATUS_SUCCESS                 0x1
+#define STORAGE_PROTOCOL_STATUS_ERROR                   0x2
+#define STORAGE_PROTOCOL_STATUS_INVALID_REQUEST         0x3
+#define STORAGE_PROTOCOL_STATUS_NO_DEVICE               0x4
+#define STORAGE_PROTOCOL_STATUS_BUSY                    0x5
+#define STORAGE_PROTOCOL_STATUS_DATA_OVERRUN            0x6
+#define STORAGE_PROTOCOL_STATUS_INSUFFICIENT_RESOURCES  0x7
+
+#define STORAGE_PROTOCOL_STATUS_NOT_SUPPORTED           0xFF
+
+// Command Length for Storage Protocols.
+//
+// NVMe commands are always 64 bytes.
+#define STORAGE_PROTOCOL_COMMAND_LENGTH_NVME            0x40
+
+// Command Specific Information for Storage Protocols - CommandSpecific field
+//
+#define STORAGE_PROTOCOL_SPECIFIC_NVME_ADMIN_COMMAND    0x01
+#define STORAGE_PROTOCOL_SPECIFIC_NVME_NVM_COMMAND 0x02
+
+#endif          /* _DEVIOCTL_ */
+
+
+// NVME_PASS_THROUGH
+
+#ifndef STB_IO_CONTROL
+typedef struct _SRB_IO_CONTROL {
+    ULONG HeaderLength;
+    UCHAR Signature[8];
+    ULONG Timeout;
+    ULONG ControlCode;
+    ULONG ReturnCode;
+    ULONG Length;
+} SRB_IO_CONTROL, *PSRB_IO_CONTROL;
+#endif
+
+#ifndef NVME_PASS_THROUGH_SRB_IO_CODE
+
+#define NVME_SIG_STR "NvmeMini"
+#define NVME_STORPORT_DRIVER 0xe000
+
+#define NVME_PASS_THROUGH_SRB_IO_CODE \
+  CTL_CODE(NVME_STORPORT_DRIVER, 0x0800, METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#pragma pack(1)
+
+/* Following is pre-Win10; used with DeviceIoControl(IOCTL_SCSI_MINIPORT),
+ * in Win10 need DeviceIoControl(IOCTL_STORAGE_PROTOCOL_COMMAND) for pure
+ * pass-through. Win10 also has "Protocol specific queries" for things like
+ * Identify and Get feature. */
+typedef struct _NVME_PASS_THROUGH_IOCTL
+{
+    SRB_IO_CONTROL SrbIoCtrl;
+    ULONG VendorSpecific[6];
+    ULONG NVMeCmd[16];      /* Command DW[0...15] */
+    ULONG CplEntry[4];      /* Completion DW[0...3] */
+    ULONG Direction;        /* 0=None, 1=Out, 2=In, 3=I/O */
+    ULONG QueueId;          /* 0=AdminQ */
+    ULONG DataBufferLen;    /* sizeof(DataBuffer) if Data In */
+    ULONG MetaDataLen;
+    ULONG ReturnBufferLen;  /* offsetof(DataBuffer), plus
+                             * sizeof(DataBuffer) if Data Out */
+    UCHAR DataBuffer[1];
+} NVME_PASS_THROUGH_IOCTL;
+#pragma pack()
+
+#endif // NVME_PASS_THROUGH_SRB_IO_CODE
+
+
+/*
+ * method codes
+ */
+#define METHOD_BUFFERED         0
+#define METHOD_IN_DIRECT        1
+#define METHOD_OUT_DIRECT       2
+#define METHOD_NEITHER          3
+
+
+#define IOCTL_SCSI_BASE    0x00000004
+
+/*
+ * constants for DataIn member of SCSI_PASS_THROUGH* structures
+ */
+#define SCSI_IOCTL_DATA_OUT             0
+#define SCSI_IOCTL_DATA_IN              1
+#define SCSI_IOCTL_DATA_UNSPECIFIED     2
+
+#define IOCTL_SCSI_PASS_THROUGH         CTL_CODE(IOCTL_SCSI_BASE, 0x0401, \
+        METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)
+#define IOCTL_SCSI_MINIPORT             CTL_CODE(IOCTL_SCSI_BASE, 0x0402, \
+        METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)
+#define IOCTL_SCSI_GET_INQUIRY_DATA     CTL_CODE(IOCTL_SCSI_BASE, 0x0403, \
+        METHOD_BUFFERED, FILE_ANY_ACCESS)
+#define IOCTL_SCSI_GET_CAPABILITIES     CTL_CODE(IOCTL_SCSI_BASE, 0x0404, \
+        METHOD_BUFFERED, FILE_ANY_ACCESS)
+#define IOCTL_SCSI_PASS_THROUGH_DIRECT  CTL_CODE(IOCTL_SCSI_BASE, 0x0405, \
+        METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)
+#define IOCTL_SCSI_GET_ADDRESS          CTL_CODE(IOCTL_SCSI_BASE, 0x0406, \
+        METHOD_BUFFERED, FILE_ANY_ACCESS)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif          /* SG_PT_WIN32_H */
diff --git a/include/sg_unaligned.h b/include/sg_unaligned.h
new file mode 100644
index 0000000..0a65b39
--- /dev/null
+++ b/include/sg_unaligned.h
@@ -0,0 +1,491 @@
+#ifndef SG_UNALIGNED_H
+#define SG_UNALIGNED_H
+
+/*
+ * Copyright (c) 2014-2018 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdbool.h>
+#include <stdint.h>     /* for uint8_t and friends */
+#include <string.h>     /* for memcpy */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* These inline functions convert integers (always unsigned) to byte streams
+ * and vice versa. They have two goals:
+ *   - change the byte ordering of integers between host order and big
+ *     endian ("_be") or little endian ("_le")
+ *   - copy the big or little endian byte stream so it complies with any
+ *     alignment that host integers require
+ *
+ * Host integer to given endian byte stream is a "_put_" function taking
+ * two arguments (integer and pointer to byte stream) returning void.
+ * Given endian byte stream to host integer is a "_get_" function that takes
+ * one argument and returns an integer of appropriate size (uint32_t for 24
+ * bit operations, uint64_t for 48 bit operations).
+ *
+ * Big endian byte format "on the wire" is the default used by SCSI
+ * standards (www.t10.org). Big endian is also the network byte order.
+ * Little endian is used by ATA, PCI and NVMe.
+ */
+
+/* The generic form of these routines was borrowed from the Linux kernel,
+ * via mhvtl. There is a specialised version of the main functions for
+ * little endian or big endian provided that not-quite-standard defines for
+ * endianness are available from the compiler and the <byteswap.h> header
+ * (a GNU extension) has been detected by ./configure . To force the
+ * generic version, use './configure --disable-fast-lebe ' . */
+
+/* Note: Assumes that the source and destination locations do not overlap.
+ * An example of overlapping source and destination:
+ *     sg_put_unaligned_le64(j, ((uint8_t *)&j) + 1);
+ * Best not to do things like that.
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"     /* need this to see if HAVE_BYTESWAP_H */
+#endif
+
+#undef GOT_UNALIGNED_SPECIALS   /* just in case */
+
+#if defined(__BYTE_ORDER__) && defined(HAVE_BYTESWAP_H) && \
+    ! defined(IGNORE_FAST_LEBE)
+
+#if defined(__LITTLE_ENDIAN__) || (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+
+#define GOT_UNALIGNED_SPECIALS 1
+
+#include <byteswap.h>           /* for bswap_16(), bswap_32() and bswap_64() */
+
+// #warning ">>>>>> Doing Little endian special unaligneds"
+
+static inline uint16_t sg_get_unaligned_be16(const void *p)
+{
+        uint16_t u;
+
+        memcpy(&u, p, 2);
+        return bswap_16(u);
+}
+
+static inline uint32_t sg_get_unaligned_be32(const void *p)
+{
+        uint32_t u;
+
+        memcpy(&u, p, 4);
+        return bswap_32(u);
+}
+
+static inline uint64_t sg_get_unaligned_be64(const void *p)
+{
+        uint64_t u;
+
+        memcpy(&u, p, 8);
+        return bswap_64(u);
+}
+
+static inline void sg_put_unaligned_be16(uint16_t val, void *p)
+{
+        uint16_t u = bswap_16(val);
+
+        memcpy(p, &u, 2);
+}
+
+static inline void sg_put_unaligned_be32(uint32_t val, void *p)
+{
+        uint32_t u = bswap_32(val);
+
+        memcpy(p, &u, 4);
+}
+
+static inline void sg_put_unaligned_be64(uint64_t val, void *p)
+{
+        uint64_t u = bswap_64(val);
+
+        memcpy(p, &u, 8);
+}
+
+static inline uint16_t sg_get_unaligned_le16(const void *p)
+{
+        uint16_t u;
+
+        memcpy(&u, p, 2);
+        return u;
+}
+
+static inline uint32_t sg_get_unaligned_le32(const void *p)
+{
+        uint32_t u;
+
+        memcpy(&u, p, 4);
+        return u;
+}
+
+static inline uint64_t sg_get_unaligned_le64(const void *p)
+{
+        uint64_t u;
+
+        memcpy(&u, p, 8);
+        return u;
+}
+
+static inline void sg_put_unaligned_le16(uint16_t val, void *p)
+{
+        memcpy(p, &val, 2);
+}
+
+static inline void sg_put_unaligned_le32(uint32_t val, void *p)
+{
+        memcpy(p, &val, 4);
+}
+
+static inline void sg_put_unaligned_le64(uint64_t val, void *p)
+{
+        memcpy(p, &val, 8);
+}
+
+#elif defined(__BIG_ENDIAN__) || (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
+
+#define GOT_UNALIGNED_SPECIALS 1
+
+#include <byteswap.h>
+
+// #warning ">>>>>> Doing BIG endian special unaligneds"
+
+static inline uint16_t sg_get_unaligned_le16(const void *p)
+{
+        uint16_t u;
+
+        memcpy(&u, p, 2);
+        return bswap_16(u);
+}
+
+static inline uint32_t sg_get_unaligned_le32(const void *p)
+{
+        uint32_t u;
+
+        memcpy(&u, p, 4);
+        return bswap_32(u);
+}
+
+static inline uint64_t sg_get_unaligned_le64(const void *p)
+{
+        uint64_t u;
+
+        memcpy(&u, p, 8);
+        return bswap_64(u);
+}
+
+static inline void sg_put_unaligned_le16(uint16_t val, void *p)
+{
+        uint16_t u = bswap_16(val);
+
+        memcpy(p, &u, 2);
+}
+
+static inline void sg_put_unaligned_le32(uint32_t val, void *p)
+{
+        uint32_t u = bswap_32(val);
+
+        memcpy(p, &u, 4);
+}
+
+static inline void sg_put_unaligned_le64(uint64_t val, void *p)
+{
+        uint64_t u = bswap_64(val);
+
+        memcpy(p, &u, 8);
+}
+
+static inline uint16_t sg_get_unaligned_be16(const void *p)
+{
+        uint16_t u;
+
+        memcpy(&u, p, 2);
+        return u;
+}
+
+static inline uint32_t sg_get_unaligned_be32(const void *p)
+{
+        uint32_t u;
+
+        memcpy(&u, p, 4);
+        return u;
+}
+
+static inline uint64_t sg_get_unaligned_be64(const void *p)
+{
+        uint64_t u;
+
+        memcpy(&u, p, 8);
+        return u;
+}
+
+static inline void sg_put_unaligned_be16(uint16_t val, void *p)
+{
+        memcpy(p, &val, 2);
+}
+
+static inline void sg_put_unaligned_be32(uint32_t val, void *p)
+{
+        memcpy(p, &val, 4);
+}
+
+static inline void sg_put_unaligned_be64(uint64_t val, void *p)
+{
+        memcpy(p, &val, 8);
+}
+
+#endif          /* __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__  */
+#endif          /* #if defined __BYTE_ORDER__ && defined <byteswap.h> &&
+                 *     ! defined IGNORE_FAST_LEBE */
+
+
+#ifndef GOT_UNALIGNED_SPECIALS
+
+/* Now we have no tricks left, so use the only way this can be done
+ * correctly in C safely: lots of shifts. */
+
+// #warning ">>>>>> Doing GENERIC unaligneds"
+
+static inline uint16_t sg_get_unaligned_be16(const void *p)
+{
+        return ((const uint8_t *)p)[0] << 8 | ((const uint8_t *)p)[1];
+}
+
+static inline uint32_t sg_get_unaligned_be32(const void *p)
+{
+        return ((const uint8_t *)p)[0] << 24 | ((const uint8_t *)p)[1] << 16 |
+                ((const uint8_t *)p)[2] << 8 | ((const uint8_t *)p)[3];
+}
+
+static inline uint64_t sg_get_unaligned_be64(const void *p)
+{
+        return (uint64_t)sg_get_unaligned_be32(p) << 32 |
+               sg_get_unaligned_be32((const uint8_t *)p + 4);
+}
+
+static inline void sg_put_unaligned_be16(uint16_t val, void *p)
+{
+        ((uint8_t *)p)[0] = (uint8_t)(val >> 8);
+        ((uint8_t *)p)[1] = (uint8_t)val;
+}
+
+static inline void sg_put_unaligned_be32(uint32_t val, void *p)
+{
+        sg_put_unaligned_be16(val >> 16, p);
+        sg_put_unaligned_be16(val, (uint8_t *)p + 2);
+}
+
+static inline void sg_put_unaligned_be64(uint64_t val, void *p)
+{
+        sg_put_unaligned_be32(val >> 32, p);
+        sg_put_unaligned_be32(val, (uint8_t *)p + 4);
+}
+
+
+static inline uint16_t sg_get_unaligned_le16(const void *p)
+{
+        return ((const uint8_t *)p)[1] << 8 | ((const uint8_t *)p)[0];
+}
+
+static inline uint32_t sg_get_unaligned_le32(const void *p)
+{
+        return ((const uint8_t *)p)[3] << 24 | ((const uint8_t *)p)[2] << 16 |
+                ((const uint8_t *)p)[1] << 8 | ((const uint8_t *)p)[0];
+}
+
+static inline uint64_t sg_get_unaligned_le64(const void *p)
+{
+        return (uint64_t)sg_get_unaligned_le32((const uint8_t *)p + 4) << 32 |
+               sg_get_unaligned_le32(p);
+}
+
+static inline void sg_put_unaligned_le16(uint16_t val, void *p)
+{
+        ((uint8_t *)p)[0] = val & 0xff;
+        ((uint8_t *)p)[1] = val >> 8;
+}
+
+static inline void sg_put_unaligned_le32(uint32_t val, void *p)
+{
+        sg_put_unaligned_le16(val >> 16, (uint8_t *)p + 2);
+        sg_put_unaligned_le16(val, p);
+}
+
+static inline void sg_put_unaligned_le64(uint64_t val, void *p)
+{
+        sg_put_unaligned_le32(val >> 32, (uint8_t *)p + 4);
+        sg_put_unaligned_le32(val, p);
+}
+
+#endif          /* #ifndef GOT_UNALIGNED_SPECIALS */
+
+/* Following are lesser used conversions that don't have specializations
+ * for endianness; big endian first. In summary these are the 24, 48 bit and
+ * given-length conversions plus the "nz" conditional put conversions. */
+
+/* Now big endian, get 24+48 then put 24+48 */
+static inline uint32_t sg_get_unaligned_be24(const void *p)
+{
+        return ((const uint8_t *)p)[0] << 16 | ((const uint8_t *)p)[1] << 8 |
+               ((const uint8_t *)p)[2];
+}
+
+/* Assume 48 bit value placed in uint64_t */
+static inline uint64_t sg_get_unaligned_be48(const void *p)
+{
+        return (uint64_t)sg_get_unaligned_be16(p) << 32 |
+               sg_get_unaligned_be32((const uint8_t *)p + 2);
+}
+
+/* Returns 0 if 'num_bytes' is less than or equal to 0 or greater than
+ * 8 (i.e. sizeof(uint64_t)). Else returns result in uint64_t which is
+ * an 8 byte unsigned integer. */
+static inline uint64_t sg_get_unaligned_be(int num_bytes, const void *p)
+{
+        if ((num_bytes <= 0) || (num_bytes > (int)sizeof(uint64_t)))
+                return 0;
+        else {
+                const uint8_t * xp = (const uint8_t *)p;
+                uint64_t res = *xp;
+
+                for (++xp; num_bytes > 1; ++xp, --num_bytes)
+                        res = (res << 8) | *xp;
+                return res;
+        }
+}
+
+static inline void sg_put_unaligned_be24(uint32_t val, void *p)
+{
+        ((uint8_t *)p)[0] = (val >> 16) & 0xff;
+        ((uint8_t *)p)[1] = (val >> 8) & 0xff;
+        ((uint8_t *)p)[2] = val & 0xff;
+}
+
+/* Assume 48 bit value placed in uint64_t */
+static inline void sg_put_unaligned_be48(uint64_t val, void *p)
+{
+        sg_put_unaligned_be16(val >> 32, p);
+        sg_put_unaligned_be32(val, (uint8_t *)p + 2);
+}
+
+/* Now little endian, get 24+48 then put 24+48 */
+static inline uint32_t sg_get_unaligned_le24(const void *p)
+{
+        return (uint32_t)sg_get_unaligned_le16(p) |
+               ((const uint8_t *)p)[2] << 16;
+}
+
+/* Assume 48 bit value placed in uint64_t */
+static inline uint64_t sg_get_unaligned_le48(const void *p)
+{
+        return (uint64_t)sg_get_unaligned_le16((const uint8_t *)p + 4) << 32 |
+               sg_get_unaligned_le32(p);
+}
+
+static inline void sg_put_unaligned_le24(uint32_t val, void *p)
+{
+        ((uint8_t *)p)[2] = (val >> 16) & 0xff;
+        ((uint8_t *)p)[1] = (val >> 8) & 0xff;
+        ((uint8_t *)p)[0] = val & 0xff;
+}
+
+/* Assume 48 bit value placed in uint64_t */
+static inline void sg_put_unaligned_le48(uint64_t val, void *p)
+{
+        ((uint8_t *)p)[5] = (val >> 40) & 0xff;
+        ((uint8_t *)p)[4] = (val >> 32) & 0xff;
+        ((uint8_t *)p)[3] = (val >> 24) & 0xff;
+        ((uint8_t *)p)[2] = (val >> 16) & 0xff;
+        ((uint8_t *)p)[1] = (val >> 8) & 0xff;
+        ((uint8_t *)p)[0] = val & 0xff;
+}
+
+/* Returns 0 if 'num_bytes' is less than or equal to 0 or greater than
+ * 8 (i.e. sizeof(uint64_t)). Else returns result in uint64_t which is
+ * an 8 byte unsigned integer. */
+static inline uint64_t sg_get_unaligned_le(int num_bytes, const void *p)
+{
+        if ((num_bytes <= 0) || (num_bytes > (int)sizeof(uint64_t)))
+                return 0;
+        else {
+                const uint8_t * xp = (const uint8_t *)p + (num_bytes - 1);
+                uint64_t res = *xp;
+
+                for (--xp; num_bytes > 1; --xp, --num_bytes)
+                        res = (res << 8) | *xp;
+                return res;
+        }
+}
+
+/* Since cdb and parameter blocks are often memset to zero before these
+ * unaligned function partially fill them, then check for a val of zero
+ * and ignore if it is with these variants. First big endian, then little */
+static inline void sg_nz_put_unaligned_be16(uint16_t val, void *p)
+{
+        if (val)
+                sg_put_unaligned_be16(val, p);
+}
+
+static inline void sg_nz_put_unaligned_be24(uint32_t val, void *p)
+{
+        if (val) {
+                ((uint8_t *)p)[0] = (val >> 16) & 0xff;
+                ((uint8_t *)p)[1] = (val >> 8) & 0xff;
+                ((uint8_t *)p)[2] = val & 0xff;
+        }
+}
+
+static inline void sg_nz_put_unaligned_be32(uint32_t val, void *p)
+{
+        if (val)
+                sg_put_unaligned_be32(val, p);
+}
+
+static inline void sg_nz_put_unaligned_be64(uint64_t val, void *p)
+{
+        if (val)
+            sg_put_unaligned_be64(val, p);
+}
+
+static inline void sg_nz_put_unaligned_le16(uint16_t val, void *p)
+{
+        if (val)
+                sg_put_unaligned_le16(val, p);
+}
+
+static inline void sg_nz_put_unaligned_le24(uint32_t val, void *p)
+{
+        if (val) {
+                ((uint8_t *)p)[2] = (val >> 16) & 0xff;
+                ((uint8_t *)p)[1] = (val >> 8) & 0xff;
+                ((uint8_t *)p)[0] = val & 0xff;
+        }
+}
+
+static inline void sg_nz_put_unaligned_le32(uint32_t val, void *p)
+{
+        if (val)
+                sg_put_unaligned_le32(val, p);
+}
+
+static inline void sg_nz_put_unaligned_le64(uint64_t val, void *p)
+{
+        if (val)
+            sg_put_unaligned_le64(val, p);
+}
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SG_UNALIGNED_H */
diff --git a/inhex/README b/inhex/README
new file mode 100644
index 0000000..77557a0
--- /dev/null
+++ b/inhex/README
@@ -0,0 +1,94 @@
+        Hex data for various sg3_utils utilities
+        ========================================
+
+The files in this folder contain hexadecimal data (in ASCII) and associated
+comments (prefixed with the hash mark symbol: '#' ). Files containing
+hexadecimal data have the extension ".hex". There is at least one file
+containing binary data and it has the extension ".raw".
+
+The utility that each hex file is associated with can be determined in most
+case by prepending "sg_" to these filenames. Then go to the 'src' folder (a
+sibling folder to this one) and look for a match or partial match on
+the name.
+
+For example:
+    vpd_dev_id.hex
+after prepending 'sg_' becomes:
+    sg_vpd_dev_id.hex
+which is a partial match on the sg_vpd utility.
+The remaining 'dev_id.hex' is meant to suggest the 'device identifier'
+VPD page which is a mandatory VPD page for SCSI devices..
+
+Assuming sg3_utils is installed, it can be tested like this:
+    sg_vpd --inhex=<folder_holding_sg3_utils>/inhex/vpd_dev_id.hex
+
+And should output this:
+
+Device Identification VPD page:
+  Addressed logical unit:
+    designator type: NAA,  code set: Binary
+      0x5000c5003011cb2b
+  Target port:
+    designator type: NAA,  code set: Binary
+     transport: Serial Attached SCSI Protocol (SPL-4)
+      0x5000c5003011cb29
+    designator type: Relative target port,  code set: Binary
+     transport: Serial Attached SCSI Protocol (SPL-4)
+      Relative target port: 0x1
+  Target device that contains addressed lu:
+    designator type: NAA,  code set: Binary
+     transport: Serial Attached SCSI Protocol (SPL-4)
+      0x5000c5003011cb28
+    designator type: SCSI name string,  code set: UTF-8
+      SCSI name string:
+      naa.5000C5003011CB28
+
+Not all the hex files follow the "prepend sg_" pattern. Those hex files
+starting with 'nvme_' are examples of invoking NVMe commands with the
+sg_raw utility.
+
+Binary <--> Hexadecimal
+-----------------------
+The vpd_zbdc.raw file is binary and was created by:
+    sg_decode_sense --inhex=vpd_zbdc.hex --nodecode --write=vpd_zbdc.raw
+as an example of converting a file in ASCII hexadecimal byte oriented
+format to binary.
+
+Turning binary output into hexadecimal can be done several ways. For
+viewing in byte oriented ASCII hex these Unix commands can be used:
+    od -t x1 vpd_zbdc.raw
+    hexdump -C vpd_zbdc.raw
+
+Each line starting with a "input offset" which is a running count of
+bytes, starting at zero. The hexdump examples shows an ASCII rendering
+of those 16 bytes to the right of each line. The sg_decode_sense utility
+may also be used:
+    sg_decode_sense --binary=vpd_zbdc.raw -H 
+    sg_decode_sense --binary=vpd_zbdc.raw -HH 
+
+The second form of sg_decode_sense appends an ASCII rendering of the 16
+bytes to the right of each line.
+
+When ASCII hexadecimal is being used as input to a utility in this
+package, the "input offset" at the start of each line (and the optional
+ASCII rendering to the right of each line) must not be given.
+That can be done with hexdump:
+   hexdump -An -C -v vpd_zbdc.raw
+           ^^^
+That is a syntax error, there is no 'A' option <<<<<<<< check
+
+And the sg_decode_sense utility can do it with (with the --nodecode option):
+   sg_decode_sense -N --binary=vpd_zbdc.raw -HHH
+That will print suitable lines of hexadecimal (16 bytes per line) to the
+console (stdout) To go the other way (i.e. hexadecimal to binary):
+   sg_decode_sense -N --inhex=vpd_zbdc.hex --write=vpd_zbdc.bin
+
+
+Conclusion
+----------
+Users are encouraged to send the author any ASCII hex files for utilities
+that support --inhex and don't have hex data already. Special cases are
+also welcome. They help the author test this code.
+
+Douglas Gilbert
+18th July 2022
diff --git a/inhex/descriptor_sense.hex b/inhex/descriptor_sense.hex
new file mode 100644
index 0000000..c0fcba9
--- /dev/null
+++ b/inhex/descriptor_sense.hex
@@ -0,0 +1,30 @@
+# Test descriptor format sense data. Values are in hex.
+# Invocation: 'sg_decode_sense -f descriptor_sense.hex'   [dpg 20220626]
+
+
+# unrec_err, excessive_writes, sdat_ovfl, additional_len=? 
+72 01 03 02 80 00 00 4c
+
+# Information: 0x11223344556677bb
+00 0a 80 00 11 22 33 44 55 66 77 bb
+
+# command specific: 0x3344556677bbccff
+01 0a 00 00 33 44 55 66 77 bb cc ff
+
+# sense key specific: SKSV=1, actual_count=257 (hex: 0x101)
+02 06 00 00 80 01 01 00
+
+# field replaceable code=0x45
+03 02 00 45
+
+# another progress report indicator
+0a 06 02 01 02 00 32 01
+
+# incorrect length indicator (ILI)
+05 02 00 20
+
+# user data segment referral
+0b 1a 01 00
+00 00 00 01 01 02 03 04 05 06 07 08
+01 02 03 04 55 06 07 08
+02 00 12 34
diff --git a/inhex/fixed_sense.hex b/inhex/fixed_sense.hex
new file mode 100644
index 0000000..1c6464b
--- /dev/null
+++ b/inhex/fixed_sense.hex
@@ -0,0 +1,5 @@
+# Test fixed format sense data. Values are in hex.
+# Invocation: 'sg_decode_sense -f fixed_sense.hex'   [dpg 20220622]
+
+f0 00 03 00 00 12 34 0a  00 00 00 00 11 00 77 80
+01 98
diff --git a/inhex/forwarded_sense.hex b/inhex/forwarded_sense.hex
new file mode 100644
index 0000000..968eb2b
--- /dev/null
+++ b/inhex/forwarded_sense.hex
@@ -0,0 +1,16 @@
+# Test forwarded sense data. Values are in hex.
+# Invocation: 'sg_decode_sense -f forwarded_sense.hex'   [dpg 20210330]
+
+# descriptor header
+72 6 18 7 0 0 0 1c
+
+# Forwarded sense [len=12]
+c a 1 2
+72 6 18 7 0 0 0 0
+
+# Information [len=12]
+0 a 80 0 11 22 33 44 55 66 77 88
+
+# FRU [len=4]
+3 2 0
+ 99
diff --git a/inhex/get_elem_status.hex b/inhex/get_elem_status.hex
new file mode 100644
index 0000000..5e72149
--- /dev/null
+++ b/inhex/get_elem_status.hex
@@ -0,0 +1,51 @@
+#
+# The is a real response to the SCSI GET PHYSICAL ELEMENT STATUS command.
+# The storage elements are associated with heads on this hard disk and
+# one of them (element_id: 10) is "out of spec". The same output was
+# obtained with --report-type=1 (report only storage elements) as this
+# invocation (where --report-type defaults to 0: report all physical
+# elements):
+#    sg_get_elem_status -HHH <dev>
+#
+# The hard disk had 9 platters and thus had 18 heads as both side of each
+# platter are used.
+
+00 00 00 12 00 00 00 12  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 01  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 02  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 03  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 04  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 05  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 06  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 07  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 08  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 09  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 0a  00 00 00 00 00 00 01 65
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 0b  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 0c  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 0d  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 0e  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 0f  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 10  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 11  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 12  00 00 00 00 00 00 01 01
+ff ff ff ff ff ff ff ff  00 00 00 00 00 00 00 00
+
diff --git a/inhex/get_lba_status.hex b/inhex/get_lba_status.hex
new file mode 100644
index 0000000..97934fa
--- /dev/null
+++ b/inhex/get_lba_status.hex
@@ -0,0 +1,14 @@
+
+
+0 0 0 24
+0 0 0 6
+
+0 0 0 0  0 0 0 0
+11 22 33 0
+
+0 0 0 0
+
+0 0 0 0  11 22 33 0
+0 0 0 44
+
+1 1 0 0
diff --git a/inhex/inq_standard.hex b/inhex/inq_standard.hex
new file mode 100644
index 0000000..73c80f3
--- /dev/null
+++ b/inhex/inq_standard.hex
@@ -0,0 +1,23 @@
+# This file contains the ASCII hex of a SCSI INQUIRY command
+# 'standard' response. In this case non-standard responses refers
+# to responses contain VPD page that are also fetched with the
+# SCSI INQUIRY command.
+
+# The response in this file can be decoded with:
+#     sg_inq --inhex=inq_standard.hex
+# or
+#     sg_vpd --inhex=inq_standard.hex --page=sinq
+#
+# The sg_inq utility defaults to the 'standard' INQUIRY while the
+# sg_vpd utility defaults to the "Supported VPD pages" VPD page.
+# Hence sg_vpd needs the extra option '--page=sinq' which says the
+# VPD page is the standard inquiry. Strictly speaking the standard
+# INQUIRY is not a VPD page but probably would be if SCSI was not
+# 40 years old and highly values backward compatibility.
+
+00 00 07 02 5b 00 10 0a  4c 69 6e 75 78 20 20 20
+73 63 73 69 5f 64 65 62  75 67 20 20 20 20 20 20
+30 31 39 31 32 30 32 31  30 35 32 30 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 c0 05 c0 06 00
+21 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
diff --git a/inhex/logs_last_n.hex b/inhex/logs_last_n.hex
new file mode 100644
index 0000000..b6384d1
--- /dev/null
+++ b/inhex/logs_last_n.hex
@@ -0,0 +1,41 @@
+# This file contains the ASCII hex of a SCSI LOG SENSE command responses
+# for the various "Last n" log (sub)pages concaternated together.
+
+# The response in this file can be decoded with:
+#     sg_logs --inhex=logs_last_n.hex
+# or
+#     sg_logs --inhex=logs_last_n.hex --brief
+# or
+#     sg_logs --inhex=logs_last_n.hex --exclude
+
+# Last n mode page data changed log subpage
+4b 02 00 28
+00 00 03 0c 00 00 00 04  00 00 00 02 00 00 00 01
+00 01 03 04 0a 00 00 00
+00 02 03 04 5a 01 00 00
+00 03 03 04 5c 02 00 00
+
+# Last n INQUIRY data changed log subpage
+4b 01 00 28
+00 00 03 0c 00 00 00 01  00 00 00 03 00 00 00 02
+00 01 03 04 00 00 00 00
+00 02 03 04 01 80 00 00
+00 03 03 04 01 83 00 00
+
+# Last n deferred errors or asynchronous events log subpage
+0b 00 00 5a
+00 00 03 40
+73,0,0,0,0,0,0     38
+b,36,1,0
+0,0,0,2,11,11,11,11,22,22,22,22,55,55,55,55,66,66,66,66   1,0,0,7,  2,0,0,8
+0,0,0,1,77,77,77,77,77,77,77,77,88,88,88,88,88,88,88,88,  3,0,0,5
+00 01 03 12
+f1 00 03 00 00 12 34 0a 00 00 00 00 11 00 00 00 00 00
+
+# Last n error events log page
+07 00 00 31
+00 00 01 0c
+6d 65 64 69 75 6d 20 65  72 72 6f 72
+00 01 01 1d
+55 41 3a 20 63 61 70 61  63 69 74 79 20 64 61 74
+61 20 68 61 73 20 63 68  61 6e 67 65 64 
diff --git a/inhex/nvme_dev_self_test.hex b/inhex/nvme_dev_self_test.hex
new file mode 100644
index 0000000..1ef87c6
--- /dev/null
+++ b/inhex/nvme_dev_self_test.hex
@@ -0,0 +1,20 @@
+# 64 byte NVMe Device Self Test command (an Admin command) that is suitable
+# for:
+#       sg_raw --cmdfile=<this_file_name> <nvme_device>
+#
+# There is no data-in or data-out associated with this command. This command
+# is optional so check the Identify controller command response to see if
+# it is supported.
+#
+# The following invocation will self test the controller and all its
+# namespaces (since nsid=0xffffffff) and does a "short" self test on each
+# one (since CDW10 is 0x1).
+
+14 00 00 00 ff ff ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# A typical invocation in Linux and FreeBSD would look like this:
+#    sg_raw --cmdfile=nvme_dev_self_test.hex /dev/nvme0
+#
diff --git a/inhex/nvme_identify_ctl.hex b/inhex/nvme_identify_ctl.hex
new file mode 100644
index 0000000..f22141e
--- /dev/null
+++ b/inhex/nvme_identify_ctl.hex
@@ -0,0 +1,27 @@
+# 64 byte NVMe Identify controller command (an Admin command) that is
+# suitable for:
+#       sg_raw --cmdfile=<this_file_name> --request=4096 <nvme_device>
+#
+# The address field (at byte offset 24, 8 bytes and little endian) gives
+# special meaning to the highest address pointers:
+#    ffffffff fffffffe         use address of data-in buffer
+#    ffffffff fffffffd         use address of data-out buffer
+#
+# The data length field (at byte offset 36, 4 bytes and little endian)
+# gives special meaning to the highest block counts:
+#    fffffffe                  use byte length of data-in buffer
+#    fffffffd                  use byte length of data-out buffer
+#
+# Since The Identify command reads data "in" from the device, then the
+# data-in buffer is appropriate.
+
+06 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  fe ff ff ff ff ff ff ff
+00 00 00 00 fe ff ff ff  01 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# A typical invocation in Linux and FreeBSD would look like this:
+#    sg_raw --cmdfile=nvme_identify_ctl.hex -r 4k /dev/nvme0
+#
+# NVMe likes "4k" (4096 bytes) buffer size, preferably aligned to
+# a 4096 byte (or "page") boundary.
diff --git a/inhex/nvme_read_ctl.hex b/inhex/nvme_read_ctl.hex
new file mode 100644
index 0000000..7996a1a
--- /dev/null
+++ b/inhex/nvme_read_ctl.hex
@@ -0,0 +1,39 @@
+# 64 byte NVMe, Read command (a NVM command) that is suitable for:
+#       sg_raw --cmdfile=<this_file_name> --nvm --request=2048 <nvme_device>
+#
+# The address field (at byte offset 24, 8 bytes and little endian) gives
+# special meaning to the highest address pointers:
+#    ffffffff fffffffe         use address of data-in buffer
+#    ffffffff fffffffd         use address of data-out buffer
+#
+# The data length field (at byte offset 36, 4 bytes and little endian)
+# gives special meaning to the highest block counts:
+#    fffffffe                  use byte length of data-in buffer
+#    fffffffd                  use byte length of data-out buffer
+#
+# 512 byte logical block size is assumed. Read 4 blocks hence 2048 bytes.
+# The first LBA read is 0x12345 and the namespace is 1. If successful
+# the four blocks will be read into the data-in buffer. Submission queue
+# 0 is used (the same queue that Admin commands use). The NVM opcode for
+# the Read command is 0x2 and appears in the first command byte.
+
+02 00 00 00 01 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  fe ff ff ff ff ff ff ff
+00 00 00 00 fe ff ff ff  45 23 01 00 00 00 00 00
+03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# Notice NVMe uses its quirky "0's based" number of blocks so
+# 03 appears at byte offset 48 to mean "read 4 blocks".
+#
+# A typical invocation in Linux and FreeBSD would look like this:
+#    sg_raw --cmdfile=nvme_read_ctl.hex --nvm -r 2048
+#           --outfile=t.bin /dev/nvme0n1
+# In FreeBSD the device name would be /dev/nvme0ns1
+#
+# Notice the '--nvm' option which is needed to distinguish a NVM
+# command from an Admin command as Admin commands are the default
+# in this utility.
+#
+# This utility (and most others in the package) aligns data-in and
+# data-out buffers to the beginning of pages which are 4096 bytes
+# long at a minimum. This is the way NVMe likes things as well.
diff --git a/inhex/nvme_read_oob_ctl.hex b/inhex/nvme_read_oob_ctl.hex
new file mode 100644
index 0000000..81d7a30
--- /dev/null
+++ b/inhex/nvme_read_oob_ctl.hex
@@ -0,0 +1,47 @@
+# 64 byte NVMe, Read command (a NVM command) which what should be an
+# Out-of-Bounds LBA (around 377 TB with 512 byte sectors. This file is
+# suitable for:
+#       sg_raw --cmdfile=<this_file_name> --nvm --request=2048 <nvme_device>
+#
+# The address field (at byte offset 24, 8 bytes and little endian) gives
+# special meaning to the highest address pointers:
+#    ffffffff fffffffe         use address of data-in buffer
+#    ffffffff fffffffd         use address of data-out buffer
+#
+# The data length field (at byte offset 36, 4 bytes and little endian)
+# gives special meaning to the highest block counts:
+#    fffffffe                  use byte length of data-in buffer
+#    fffffffd                  use byte length of data-out buffer
+#
+# vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+# This NVMe (NVM) Read command purposely has a very large starting LBA
+# in order to get a "Attempted write to read only range" error. This is
+# to test error reporting.
+# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+#
+# 512 byte logical block size is assumed. Read 4 blocks hence 2048 bytes.
+# The first LBA read is 0xabcd012345 and the namespace is 1. If successful
+# the four blocks will be read into the data-in buffer. Submission queue
+# 0 is used (the same queue that Admin commands use). The NVM opcode for
+# the Read command is 0x2 and appears in the first command byte.
+
+02 00 00 00 01 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  fe ff ff ff ff ff ff ff
+00 00 00 00 fe ff ff ff  45 23 01 cd ab 00 00 00
+03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# Notice NVMe uses its quirky "0's based" number of blocks so
+# 03 appears at byte offset 48 to mean "read 4 blocks".
+#
+# A typical invocation in Linux and FreeBSD would look like this:
+#    sg_raw --cmdfile=nvme_read_oob_ctl.hex --nvm -r 2048
+#           --outfile=t.bin /dev/nvme0n1
+# In FreeBSD the device name would be /dev/nvme0ns1
+#
+# Notice the '--nvm' option which is needed to distinguish a NVM
+# command from an Admin command as Admin commands are the default
+# in this utility.
+#
+# This utility (and most others in the package) aligns data-in and
+# data-out buffers to the beginning of pages which are 4096 bytes
+# long at a minimum. This is the way NVMe likes things as well.
diff --git a/inhex/nvme_write_ctl.hex b/inhex/nvme_write_ctl.hex
new file mode 100644
index 0000000..9e6c112
--- /dev/null
+++ b/inhex/nvme_write_ctl.hex
@@ -0,0 +1,38 @@
+# 64 byte NVMe, Write command (a NVM command) that is suitable for:
+#       sg_raw --cmdfile=<this_file_name> --nvm --request=2048 <nvme_device>
+#
+# The address field (at byte offset 24, 8 bytes and little endian) gives
+# special meaning to the highest address pointers:
+#    ffffffff fffffffe         use address of data-in buffer
+#    ffffffff fffffffd         use address of data-out buffer
+#
+# The data length field (at byte offset 36, 4 bytes and little endian)
+# gives special meaning to the highest block counts:
+#    fffffffe                  use byte length of data-in buffer
+#    fffffffd                  use byte length of data-out buffer
+#
+# 512 byte logical block size is assumed. Write 4 blocks hence 2048 bytes.
+# The first LBA written is 0x12345 and the namespace is 1. If successful the
+# four blocks will be written out of the data-out buffer. Submission queue 
+# is used (the same queue that Admin commands use). The NVM opcode for the
+# Write command is 0x1 and appears in the first command byte.
+
+01 00 00 00 01 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  fd ff ff ff ff ff ff ff
+00 00 00 00 fd ff ff ff  45 23 01 00 00 00 00 00
+03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# Notice NVMe uses its quirky "0's based" number of blocks so
+# 03 appears at byte offset 48 to mean "write 4 blocks".
+#
+# A typical invocation in Linux and FreeBSD would look like this:
+#    sg_raw --cmdfile=nvme_write_ctl.hex --nvm -s 2048
+#           --infile=t.bin /dev/nvme0
+#
+# Notice the '--nvm' option which is needed to distinguish a NVM
+# command from an Admin command as Admin commands are the default
+# in this utility.
+#
+# This utility (and most others in the package) aligns data-in and
+# data-out buffers to the beginning of pages which are 4096 bytes
+# long at a minimum. This is the way NVMe likes things as well.
diff --git a/inhex/opcodes.hex b/inhex/opcodes.hex
new file mode 100644
index 0000000..8833ab0
--- /dev/null
+++ b/inhex/opcodes.hex
@@ -0,0 +1,27 @@
+00 00 01 a0 12 00 00 00  00 00 00 06 a0 00 00 00
+00 00 00 0c 03 00 00 00  00 00 00 06 00 00 00 00
+00 00 00 06 5a 00 00 00  00 00 00 0a 1a 00 00 00
+00 00 00 06 55 00 00 00  00 00 00 0a 15 00 00 00
+00 00 00 06 4d 00 00 00  00 00 00 0a 25 00 00 00
+00 00 00 0a 88 00 00 00  00 00 00 10 28 00 00 00
+00 00 00 0a 08 00 00 00  00 00 00 06 a8 00 00 00
+00 00 00 0c 8a 00 00 00  00 00 00 10 2a 00 00 00
+00 00 00 0a 0a 00 00 00  00 00 00 06 aa 00 00 00
+00 00 00 0c 1b 00 00 00  00 00 00 06 9e 00 00 10
+00 01 00 10 9e 00 00 12  00 01 00 10 9f 00 00 12
+00 01 00 10 a3 00 00 0a  00 01 00 0c a3 00 00 0c
+00 01 00 0c a3 00 00 0d  00 01 00 0c 8f 00 00 00
+00 00 00 10 2f 00 00 00  00 00 00 0a 7f 00 00 09
+00 01 00 20 7f 00 00 0b  00 01 00 20 7f 00 00 11
+00 01 00 20 56 00 00 00  00 00 00 0a 16 00 00 00
+00 00 00 06 57 00 00 00  00 00 00 0a 17 00 00 00
+00 00 00 06 1e 00 00 00  00 00 00 06 01 00 00 00
+00 00 00 06 1d 00 00 02  00 00 00 06 42 00 00 00
+00 00 00 0a 3b 00 00 00  00 00 00 0a 41 00 00 00
+00 00 00 0a 93 00 00 00  00 00 00 10 35 00 00 00
+00 00 00 0a 91 00 00 00  00 00 00 10 89 00 00 00
+00 00 00 10 34 00 00 00  00 00 00 0a 90 00 00 00
+00 00 00 10 94 00 00 03  00 01 00 10 94 00 00 01
+00 01 00 10 94 00 00 02  00 01 00 10 94 00 00 04
+00 01 00 10 95 00 00 00  00 01 00 10 95 00 00 06
+00 01 00 10
diff --git a/inhex/ref_sense.hex b/inhex/ref_sense.hex
new file mode 100644
index 0000000..e6a6a37
--- /dev/null
+++ b/inhex/ref_sense.hex
@@ -0,0 +1,7 @@
+# Test User data segment referral sense data. Values are in hex.
+# Invocation: 'sg_decode_sense -f ref_sense.hex'   [dpg 20210330]
+
+72,0,0,0,0,0,0     38
+b,36,1,0
+0,0,0,2,11,11,11,11,22,22,22,22,55,55,55,55,66,66,66,66   1,0,0,7,  2,0,0,8
+0,0,0,1,77,77,77,77,77,77,77,77,88,88,88,88,88,88,88,88,  3,0,0,5
diff --git a/inhex/rep_density.hex b/inhex/rep_density.hex
new file mode 100644
index 0000000..0aba1b5
--- /dev/null
+++ b/inhex/rep_density.hex
@@ -0,0 +1,18 @@
+#
+# This file contains the response to SCSI REPORT DENSITY SUPPORTED command
+# using the sg_rep_density utility. This file was generated with:
+#      sg_rep_density -HHH /dev/sg4 > rep_density.hex
+# where /dev/sg4 was a LTO-4 tape drive. To decode that file containing
+# hexadecimal in ASCII use:
+#      sg_rep_density --inhex=rep_density.hex
+
+00 9e 00 00 44 44 00 00  00 00 25 a6 00 7f 02 c0
+00 06 1a 80 4c 54 4f 2d  43 56 45 20 55 2d 33 31
+36 20 20 20 55 6c 74 72  69 75 6d 20 33 2f 31 36
+54 20 20 20 20 20 20 20  46 46 80 00 00 00 31 b5
+00 7f 03 80 00 0c 35 00  4c 54 4f 2d 43 56 45 20
+55 2d 34 31 36 20 20 20  55 6c 74 72 69 75 6d 20
+34 2f 31 36 54 20 20 20  20 20 20 20 58 58 a0 00
+00 00 3b 26 00 7f 05 00  00 16 e3 60 4c 54 4f 2d
+43 56 45 20 55 2d 35 31  36 20 20 20 55 6c 74 72
+69 75 6d 20 35 2f 31 36  54 20 20 20 20 20 20 20
diff --git a/inhex/rep_density_media.hex b/inhex/rep_density_media.hex
new file mode 100644
index 0000000..22312fd
--- /dev/null
+++ b/inhex/rep_density_media.hex
@@ -0,0 +1,13 @@
+#
+# This file contains the response to SCSI REPORT DENSITY SUPPORTED command
+# using the sg_rep_density utility. This file was generated with:
+#      sg_rep_density --media -HHH /dev/sg4 > rep_density_media.hex
+# where /dev/sg4 was a LTO-4 tape drive. To decode that file containing
+# hexadecimal in ASCII use:
+#      sg_rep_density --inhex=rep_density_media.hex
+# The --media option is not needed in the decode invocation.
+
+00 36 00 00 58 58 a0 00  00 00 3b 26 00 7f 05 00
+00 17 85 3e 4c 54 4f 2d  43 56 45 20 55 2d 35 31
+36 20 20 20 55 6c 74 72  69 75 6d 20 35 2f 31 36
+54 20 20 20 20 20 20 20
diff --git a/inhex/rep_density_media_typem.hex b/inhex/rep_density_media_typem.hex
new file mode 100644
index 0000000..57918fe
--- /dev/null
+++ b/inhex/rep_density_media_typem.hex
@@ -0,0 +1,13 @@
+#
+# This file contains the response to SCSI REPORT DENSITY SUPPORTED command
+# using the sg_rep_density utility. This file was generated with:
+#      sg_rep_density -M -t -HHH /dev/sg4 > rep_density_media_typem.hex
+# where /dev/sg4 was a LTO-4 tape drive. To decode that file containing
+# hexadecimal in ASCII use:
+#      sg_rep_density --typem -i rep_density_media_typem.hex
+# The --typem option is required in the decode invocation.
+
+00 3a 00 00 00 00 00 34  01 58 00 00 00 00 00 00
+00 00 00 7f 03 4e 00 00  48 50 20 20 20 20 20 20
+4c 54 4f 35 44 61 74 61  55 6c 74 72 69 75 6d 20
+35 20 44 61 74 61 20 54  61 70 65 20
diff --git a/inhex/rep_density_typem.hex b/inhex/rep_density_typem.hex
new file mode 100644
index 0000000..315d690
--- /dev/null
+++ b/inhex/rep_density_typem.hex
@@ -0,0 +1,31 @@
+#
+# This file contains the response to SCSI REPORT DENSITY SUPPORTED command
+# using the sg_rep_density utility. This file was generated with:
+#      sg_rep_density --typem -HHH /dev/sg4 > rep_density_typem.hex
+# where /dev/sg4 was a LTO-4 tape drive. To decode that file containing
+# hexadecimal in ASCII use:
+#      sg_rep_density --typem -i rep_density_typem.hex
+# The --typem option is required in the decode invocation.
+
+01 52 00 00 00 00 00 34  01 44 00 00 00 00 00 00
+00 00 00 7f 02 a8 00 00  48 50 20 20 20 20 20 20
+4c 54 4f 33 44 61 74 61  55 6c 74 72 69 75 6d 20
+33 20 44 61 74 61 20 54  61 70 65 20 00 00 00 34
+01 46 00 00 00 00 00 00  00 00 00 7f 03 34 00 00
+48 50 20 20 20 20 20 20  4c 54 4f 34 44 61 74 61
+55 6c 74 72 69 75 6d 20  34 20 44 61 74 61 20 54
+61 70 65 20 00 00 00 34  01 58 00 00 00 00 00 00
+00 00 00 7f 03 4e 00 00  48 50 20 20 20 20 20 20
+4c 54 4f 35 44 61 74 61  55 6c 74 72 69 75 6d 20
+35 20 44 61 74 61 20 54  61 70 65 20 01 00 00 34
+01 44 00 00 00 00 00 00  00 00 00 7f 02 a8 00 00
+48 50 20 20 20 20 20 20  4c 54 4f 33 57 4f 52 4d
+55 6c 74 72 69 75 6d 20  33 20 57 4f 52 4d 20 54
+61 70 65 20 01 00 00 34  01 46 00 00 00 00 00 00
+00 00 00 7f 03 34 00 00  48 50 20 20 20 20 20 20
+4c 54 4f 34 57 4f 52 4d  55 6c 74 72 69 75 6d 20
+34 20 57 4f 52 4d 20 54  61 70 65 20 01 00 00 34
+01 58 00 00 00 00 00 00  00 00 00 7f 03 4e 00 00
+48 50 20 20 20 20 20 20  4c 54 4f 35 57 4f 52 4d
+55 6c 74 72 69 75 6d 20  35 20 57 4f 52 4d 20 54
+61 70 65 20
diff --git a/inhex/rep_realms.hex b/inhex/rep_realms.hex
new file mode 100644
index 0000000..08df66b
--- /dev/null
+++ b/inhex/rep_realms.hex
@@ -0,0 +1,35 @@
+# This is the output (in hex) of the SCSI REPORT REALMS command.
+# This page is constructed from the command description in zbc2r10
+# with three realms, and two zone domains
+
+# A typical example:
+#    sg_rep_zones --realm --inhex=rep_realms.hex
+
+
+# parameter data header (64 bytes)
+00 00 00 90 00 00 00 03  00 00 00 30 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# first zone domain descriptor
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+# first realm descriptor, zone domain 0 start,last
+00 00 00 00 00 00 00 00  00 00 00 00 00 03 ff ff
+# first realm descriptor, zone domain 1 start,last
+00 00 00 00 00 04 00 00  00 00 00 00 00 07 ff ff
+
+# second zone domain descriptor
+00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00
+# second realm descriptor, zone domain 0 start,last
+00 00 00 00 00 08 00 00  00 00 00 00 00 0b ff ff
+# second realm descriptor, zone domain 1 start,last
+00 00 00 00 00 0c 00 00  00 00 00 00 00 0f ff ff
+
+# third realm descriptor
+00 00 00 02 00 00 00 00  00 00 00 00 00 00 00 00
+# second realm descriptor, zone domain 0 start,last
+00 00 00 00 00 10 00 00  00 00 00 00 00 13 ff ff
+# second realm descriptor, zone domain 1 start,last
+00 00 00 00 00 14 00 00  00 00 00 00 00 17 ff ff
+
diff --git a/inhex/rep_zdomains.hex b/inhex/rep_zdomains.hex
new file mode 100644
index 0000000..346f2c0
--- /dev/null
+++ b/inhex/rep_zdomains.hex
@@ -0,0 +1,29 @@
+# This is the output (in hex) of the SCSI REPORT ZONE DOMAINS command.
+# This page is constructed from the command description in zbc2r10
+# with two zone domains.
+
+# A typical example:
+#    sg_rep_zones --domain --inhex=rep_zdomains.hex
+
+# parameter data header (64 bytes)
+00 00 00 c0 00 00 00 c0  02 02 00 30 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# first zone domain
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 03  00 00 00 00 00 00 00 00
+00 00 00 00 00 13 ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# second zone domain
+01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 03  00 00 00 00 00 04 00 00
+00 00 00 00 00 17 ff ff  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
diff --git a/inhex/rep_zones.hex b/inhex/rep_zones.hex
new file mode 100644
index 0000000..702c93c
--- /dev/null
+++ b/inhex/rep_zones.hex
@@ -0,0 +1,39 @@
+# This is the output (in hex) of the SCSI REPORT ZONES command
+# from a simulated ZBC device from the scsi_debug driver in Linux.
+# The parameters to the scsi_debug driver given to modprobe were:
+#     dev_size_mb=512 zbc=managed zone_size_mb=128 zone_nr_conv=1
+#
+# The hex bytes in this file were generated by:
+#     sg_rep_zones /dev/sg1 -HHH > /tmp/rep_zones.hex
+# where /dev/sg1 was a scsi_debug device.
+
+# An example invocation:
+#    sg_rep_zones --inhex=rep_zones.hex
+
+
+# parameter data header (64 bytes)
+00 00 01 00 00 00 00 00  00 00 00 00 00 0f ff ff
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# first zone descriptor, zone type: conventional
+01 00 00 00 00 00 00 00  00 00 00 00 00 04 00 00
+00 00 00 00 00 00 00 00  ff ff ff ff ff ff ff ff
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+# second zone descriptor, zone type: sequential write required
+02 10 00 00 00 00 00 00  00 00 00 00 00 04 00 00
+00 00 00 00 00 04 00 00  00 00 00 00 00 04 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+# third zone descriptor, zone type: sequential write required
+02 10 00 00 00 00 00 00  00 00 00 00 00 04 00 00
+00 00 00 00 00 08 00 00  00 00 00 00 00 08 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+# fourth and last zone descriptor, zone type: sequential write required
+02 10 00 00 00 00 00 00  00 00 00 00 00 04 00 00
+00 00 00 00 00 0c 00 00  00 00 00 00 00 0c 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
diff --git a/inhex/ses_areca_all.hex b/inhex/ses_areca_all.hex
new file mode 100644
index 0000000..b744321
--- /dev/null
+++ b/inhex/ses_areca_all.hex
@@ -0,0 +1,195 @@
+#
+# This file was generated by something like:
+#    sg_ses --page=all -HHHH /dev/sg5 > ses_areca_all.hex
+# where /dev/sg5 was an Areca 8028 SAS-3 expander
+
+# An example invocation to decode the hex data below:
+#    sg_ses --all --status --inhex=ses_areca_all.hex
+#
+#  vvvvvvvvvv   generated part of file below   vvvvvvvvvvvvvv
+
+# Supported Diagnostic Pages dpage:
+00 00 00 0b 00 01 02 04  05 07 0a 0d 0e 0f 3f
+
+# Configuration (SES) dpage:
+01 00 01 28 00 00 00 00  11 00 09 2c d5 b4 01 50
+3f c0 ec 16 41 72 65 63  61 20 20 20 41 52 43 2d
+38 30 32 38 30 31 2e 33  33 2e 36 33 30 31 33 33
+11 22 33 44 55 00 00 00  17 18 00 18 0e 01 00 1c
+18 01 00 0c 03 05 00 1a  04 02 00 17 12 02 00 1a
+19 03 00 16 02 02 00 17  06 01 00 18 41 72 72 61
+79 44 65 76 69 63 65 73  49 6e 53 75 62 45 6e 63
+6c 73 72 30 45 6e 63 6c  6f 73 75 72 65 45 6c 65
+6d 65 6e 74 49 6e 53 75  62 45 6e 63 6c 73 72 30
+53 41 53 20 45 78 70 61  6e 64 65 72 43 6f 6f 6c
+69 6e 67 45 6c 65 6d 65  6e 74 49 6e 53 75 62 45
+6e 63 6c 73 72 30 54 65  6d 70 53 65 6e 73 6f 72
+73 49 6e 53 75 62 45 6e  63 6c 73 72 30 56 6f 6c
+74 61 67 65 53 65 6e 73  6f 72 73 49 6e 53 75 62
+45 6e 63 6c 73 72 30 43  6f 6e 6e 65 63 74 6f 72
+73 49 6e 53 75 62 45 6e  63 6c 73 72 30 50 6f 77
+65 72 53 75 70 70 6c 79  49 6e 53 75 62 45 6e 63
+6c 73 72 30 41 75 64 69  62 6c 65 41 6c 61 72 6d
+49 6e 53 75 62 45 6e 63  6c 73 72 30
+
+# Enclosure Status (SES) dpage:
+02 02 00 cc 00 00 00 00  00 00 00 00 05 00 00 00
+05 00 00 00 05 00 00 00  05 00 00 00 05 00 00 00
+05 00 00 00 05 00 00 00  05 00 00 00 05 00 00 00
+05 00 00 00 05 00 00 00  05 00 00 00 05 00 00 00
+05 00 00 00 05 00 00 00  05 00 00 00 05 00 00 00
+05 00 00 00 01 00 00 00  05 00 00 00 05 00 00 00
+05 00 00 00 05 00 00 00  05 00 00 00 00 00 00 00
+01 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00
+05 00 00 10 05 00 00 10  05 00 00 10 05 00 00 10
+01 02 ee 07 00 00 00 00  01 00 45 00 01 00 56 00
+00 00 00 00 01 00 00 5e  01 00 00 b4 00 00 00 00
+01 05 00 00 01 05 00 00  01 05 00 00 00 00 00 00
+05 00 00 20 05 00 00 20  00 00 00 00 01 00 00 00
+
+# String In (SES) dpage:
+04 00 00 2e 57 65 20 68  61 76 65 20 69 6d 70 6c
+65 6d 65 6e 74 65 64 20  53 74 72 69 6e 67 20 49
+6e 20 44 69 61 67 6e 6f  73 74 69 63 20 50 61 67
+65 00
+
+# Threshold In (SES) dpage:
+05 00 00 c4 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  63 50 19 14 73 6e 19 14
+00 00 00 00 82 7f 70 6d  7a 77 69 66 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+
+# Element Descriptor (SES) dpage:
+07 00 03 0e 00 00 00 00  00 00 00 18 41 72 72 61
+79 44 65 76 69 63 65 73  49 6e 53 75 62 45 6e 63
+6c 73 72 30 00 00 00 08  53 4c 4f 54 20 30 31 00
+00 00 00 08 53 4c 4f 54  20 30 32 00 00 00 00 08
+53 4c 4f 54 20 30 33 00  00 00 00 08 53 4c 4f 54
+20 30 34 00 00 00 00 08  53 4c 4f 54 20 30 35 00
+00 00 00 08 53 4c 4f 54  20 30 36 00 00 00 00 08
+53 4c 4f 54 20 30 37 00  00 00 00 08 53 4c 4f 54
+20 30 38 00 00 00 00 08  53 4c 4f 54 20 30 39 00
+00 00 00 08 53 4c 4f 54  20 31 30 00 00 00 00 08
+53 4c 4f 54 20 31 31 00  00 00 00 08 53 4c 4f 54
+20 31 32 00 00 00 00 08  53 4c 4f 54 20 31 33 00
+00 00 00 08 53 4c 4f 54  20 31 34 00 00 00 00 08
+53 4c 4f 54 20 31 35 00  00 00 00 08 53 4c 4f 54
+20 31 36 00 00 00 00 08  53 4c 4f 54 20 31 37 00
+00 00 00 08 53 4c 4f 54  20 31 38 00 00 00 00 08
+53 4c 4f 54 20 31 39 00  00 00 00 08 53 4c 4f 54
+20 32 30 00 00 00 00 08  53 4c 4f 54 20 32 31 00
+00 00 00 08 53 4c 4f 54  20 32 32 00 00 00 00 08
+53 4c 4f 54 20 32 33 00  00 00 00 08 53 4c 4f 54
+20 32 34 00 00 00 00 1c  45 6e 63 6c 6f 73 75 72
+65 45 6c 65 6d 65 6e 74  49 6e 53 75 62 45 6e 63
+6c 73 72 30 00 00 00 12  45 6e 63 6c 6f 73 75 72
+65 45 6c 65 6d 65 6e 74  30 31 00 00 00 0c 53 41
+53 20 45 78 70 61 6e 64  65 72 00 00 00 09 45 78
+70 61 6e 64 65 72 30 00  00 00 1a 43 6f 6f 6c 69
+6e 67 45 6c 65 6d 65 6e  74 49 6e 53 75 62 45 6e
+63 6c 73 72 30 00 00 00  07 46 61 6e 20 30 31 00
+00 00 00 07 46 61 6e 20  30 32 00 00 00 00 07 46
+61 6e 20 30 33 00 00 00  00 07 46 61 6e 20 30 34
+00 00 00 00 07 43 50 55  46 61 6e 00 00 00 00 17
+54 65 6d 70 53 65 6e 73  6f 72 73 49 6e 53 75 62
+45 6e 63 6c 73 72 30 00  00 00 0c 45 4e 43 2e 20
+54 65 6d 70 20 20 00 00  00 00 0c 43 68 69 70 20
+54 65 6d 70 20 20 00 00  00 00 1a 56 6f 6c 74 61
+67 65 53 65 6e 73 6f 72  73 49 6e 53 75 62 45 6e
+63 6c 73 72 30 00 00 00  07 30 2e 39 35 56 20 00
+00 00 00 07 31 2e 38 56  20 20 00 00 00 00 16 43
+6f 6e 6e 65 63 74 6f 72  73 49 6e 53 75 62 45 6e
+63 6c 73 72 30 00 00 00  0c 43 6f 6e 6e 65 63 74
+6f 72 30 30 00 00 00 00  0c 43 6f 6e 6e 65 63 74
+6f 72 30 31 00 00 00 00  0c 43 6f 6e 6e 65 63 74
+6f 72 30 32 00 00 00 00  17 50 6f 77 65 72 53 75
+70 70 6c 79 49 6e 53 75  62 45 6e 63 6c 73 72 30
+00 00 00 0e 50 6f 77 65  72 53 75 70 70 6c 79 30
+31 00 00 00 00 0e 50 6f  77 65 72 53 75 70 70 6c
+79 30 32 00 00 00 00 18  41 75 64 69 62 6c 65 41
+6c 61 72 6d 49 6e 53 75  62 45 6e 63 6c 73 72 30
+00 00 00 0e 41 75 64 69  62 6c 65 2d 41 6c 61 72
+6d 00
+
+# Additional Element Status (SES-2) dpage:
+0a 00 03 bc 00 00 00 00  16 22 00 00 01 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 16 22 00 01
+01 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+16 22 00 02 01 00 00 02  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 16 22 00 03  01 00 00 03 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  16 22 00 04 01 00 00 04
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 16 22 00 05
+01 00 00 05 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+16 22 00 06 01 00 00 06  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 16 22 00 07  01 00 00 07 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  16 22 00 08 01 00 00 08
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 16 22 00 09
+01 00 00 09 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+16 22 00 0a 01 00 00 0a  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 16 22 00 0b  01 00 00 0b 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  16 22 00 0c 01 00 00 0c
+20 00 00 02 50 01 b4 d5  16 ec c0 3f 50 01 51 7e
+85 c3 ef ff 14 00 00 00  00 00 00 00 16 22 00 0d
+01 00 00 0d 20 00 00 02  50 01 b4 d5 16 ec c0 3f
+50 01 51 7e 85 c3 ef ff  15 00 00 00 00 00 00 00
+16 22 00 0e 01 00 00 0e  20 00 00 02 50 01 b4 d5
+16 ec c0 3f 50 01 51 7e  85 c3 ef ff 16 00 00 00
+00 00 00 00 16 22 00 0f  01 00 00 0f 20 00 00 02
+50 01 b4 d5 16 ec c0 3f  50 01 51 7e 85 c3 ef ff
+17 00 00 00 00 00 00 00  16 22 00 10 01 00 00 10
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 16 22 00 11
+01 00 00 11 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+16 22 00 12 01 00 00 12  10 00 00 08 50 01 b4 d5
+16 ec c0 3f 50 00 c5 00  30 11 cb 29 00 00 00 00
+00 00 00 00 16 22 00 13  01 00 00 13 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  16 22 00 14 01 00 00 14
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 16 22 00 15
+01 00 00 15 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+16 22 00 16 01 00 00 16  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 16 22 00 17  01 00 00 17 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  16 56 00 00 24 40 00 00
+50 01 b4 d5 16 ec c0 3f  ff 0d ff 0c ff 0e ff 0f
+ff 09 ff 08 ff 0a ff 0b  ff 05 ff 04 ff 06 ff 07
+ff 01 ff 00 ff 02 ff 03  02 ff 02 ff 02 ff 02 ff
+01 ff 01 ff 01 ff 01 ff  00 ff 00 ff 00 ff 00 ff
+ff 11 ff 10 ff 12 ff 13  ff 15 ff 14 ff 16 ff 17
+
+# Supported SES Diagnostic Pages (SES-2) dpage:
+0d 00 00 0c 01 02 04 05  07 0a 0d 0e 0f 00 00 00
+
+# Download Microcode (SES-2) dpage:
+0e 00 00 14 00 00 00 00  00 00 00 00 00 10 00 00
+00 00 00 00 00 00 00 00
+
+# Subenclosure Nickname (SES-2) dpage:
+0f 00 00 2c 00 00 00 00  00 00 00 00 00 00 00 00
+45 76 61 6c 20 42 6f 61  72 64 20 4e 69 63 6b 6e
+61 6d 65 20 53 69 6d 75  6c 61 74 6f 72 20 20 20
diff --git a/inhex/vpd_bdce.hex b/inhex/vpd_bdce.hex
new file mode 100644
index 0000000..d20c530
--- /dev/null
+++ b/inhex/vpd_bdce.hex
@@ -0,0 +1,18 @@
+#
+# This is a manufactured response to an INQUIRY for the
+# Block device characteristics extension VPD page (0xb5  ["bdce"]).
+# It may have been generated by a call like this:
+#        sg_vpd -p bdce /dev/sg3 -HHHH
+
+# Block device characteristics extension VPD page
+00 b5 00 7c
+00 03 05 0e 00 00 00 06  00 00 00 03
+
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
diff --git a/inhex/vpd_consistuents.hex b/inhex/vpd_consistuents.hex
new file mode 100644
index 0000000..4a67b74
--- /dev/null
+++ b/inhex/vpd_consistuents.hex
@@ -0,0 +1,47 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_consistuents.hex
+
+
+# Device constituent VPD page header
+00 8b 00 c2
+
+# First constituent descriptor, fixed part
+00 03 00 00
+41 42 43 44 20 20 00 00
+41 42 43 44 45 46 47 48  41 42 43 44 44 44 44 44
+30 31 32 33
+00 00
+00 2a
+
+# inner constituent specific descriptor (for VPD page)
+01 00 00 10
+# ... the VPD page
+00 b3 00 0c  00 00 00 00
+00 20 00 00
+00 00 00 04
+
+# another inner constituent specific descriptor (for VPD page)
+01 00 00 12
+# ... the VPD page
+00 92 00 0e 00 00 00 00
+00 01 01 01 01 02 02 01
+09 09
+
+
+# Second constituent descriptor, fixed part
+00 03 00 00
+53 45 41 47 41 54 45 20
+53 54 32 30 30 46 4d 30  30 37 33 20 20 20 20 20
+30 30 30 37
+00 00
+00 50
+
+# inner constituent specific descriptor ("di" VPD page)
+01 00 00 4c
+# ... the VPD page
+00 83 00 48 01 03 00 08  50 00 c5 00 30 11 cb 2b
+61 93 00 08 50 00 c5 00  30 11 cb 29 61 94 00 04
+00 00 00 01 61 a3 00 08  50 00 c5 00 30 11 cb 28
+03 28 00 18 6e 61 61 2e  35 30 30 30 43 35 30 30
+33 30 31 31 43 42 32 38  00 00 00 00
diff --git a/inhex/vpd_cpr.hex b/inhex/vpd_cpr.hex
new file mode 100644
index 0000000..130c5c1
--- /dev/null
+++ b/inhex/vpd_cpr.hex
@@ -0,0 +1,18 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_cpr.hex
+
+# Dummy data for Concurrent positioning ranges VPD page
+00 b9 00 7c 00 00 00 00
+ 0 0 0 0 0 0 0 0  0 0 0 0 0 0 0 0  0 0 0 0 0 0 0 0  0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0  0 0 0 0 0 0 0 0  0 0 0 0 0 0 0 0 
+# after 64 byte header there is the first LBA range descriptor (32 bytes)
+01 02  00 00 00 00 00 00
+00 00 00 00 00 00 00 00
+00 00 00 00 10 00 00 00
+ 00 00 00 00 00 00 00 00
+# second LBA range descriptor (32 bytes)
+02 02  00 00 00 00 00 00
+00 00 00 00 10 00 00 00
+00 00 00 00 10 00 00 00
+ 00 00 00 00 00 00 00 00
diff --git a/inhex/vpd_dev_id.hex b/inhex/vpd_dev_id.hex
new file mode 100644
index 0000000..e9cb990
--- /dev/null
+++ b/inhex/vpd_dev_id.hex
@@ -0,0 +1,9 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_dev_id.hex
+
+00 83 00 48 01 03 00 08  50 00 c5 00 30 11 cb 2b
+61 93 00 08 50 00 c5 00  30 11 cb 29 61 94 00 04
+00 00 00 01 61 a3 00 08  50 00 c5 00 30 11 cb 28
+03 28 00 18 6e 61 61 2e  35 30 30 30 43 35 30 30
+33 30 31 31 43 42 32 38  00 00 00 00
diff --git a/inhex/vpd_di_all.hex b/inhex/vpd_di_all.hex
new file mode 100644
index 0000000..b0c9376
--- /dev/null
+++ b/inhex/vpd_di_all.hex
@@ -0,0 +1,51 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_di_all.hex
+
+00 83 01 04
+
+# Vendor specific designator
+01 00 00 16 11 22 33 44  55 66 77 88 99 aa bb cc
+dd ee ff ed cb a9 87 65  43 21
+
+# T10 vendor ID
+02 01 00 14
+41 42 43 20 20 20 20 20
+58 59 5a 31 32 33 34 35  36 37 38 39
+
+# EUI-64
+01 02 00 08 11 22 33 44  55 66 77 88
+01 02 00 0c 11 22 33 44  55 66 77 88 00 00 01 23
+01 02 00 10 01 23 45 67  89 ab cd ef 11 22 33 44  55 66 77 88
+
+# NAA
+01 03 00 08 51 22 33 44  55 66 77 88
+01 03 00 10 61 22 33 44  55 66 77 88 aa bb cc dd  ee ff ee dd
+
+# Relative target port
+01 14 00 04 00 00 00 02
+
+# Target port group
+01 15 00 04 00 00 00 03
+
+# Logical unit group
+01 06 00 04 00 00 00 04
+
+# MD5 logical unitp
+01 07 00 10 ff ee dd cc  bb aa 99 88 77 66 55 44  33 22 11 00
+
+# SCSI name string: iqn.5886.com.acme.diskarrays-sn-a8675309
+02 28 00 28
+69 71 6e 2e 35 38 38 36  2e 63 6f 6d 2e 61 63 6d    
+65 2e 64 69 73 6b 61 72  72 61 79 73 2d 73 6e 2d    
+61 38 36 37 35 33 30 39
+
+# Protocol specific
+# USB
+91 99 00 04 04 00 02 00
+# PCIe
+a1 99 00 08 01 23 00 00  00 00 00 00
+
+# UUID
+01 0a 00 12 10 00 11 22  33 44 55 66 77 88 99 aa
+bb cc dd ee fe dc
diff --git a/inhex/vpd_fp.hex b/inhex/vpd_fp.hex
new file mode 100644
index 0000000..967080f
--- /dev/null
+++ b/inhex/vpd_fp.hex
@@ -0,0 +1,31 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_fp.hex
+
+# Dummy data for Format presets VPD page
+00 b8 00 80
+
+# after 4 byte header here is the first Format preset descriptor (64 bytes)
+00 00 01 00
+01  0 0  00
+01 00 00 00
+0 0 0 0
+00 00 00 00  00 ff ff ff	# last LBA
+0 0 0 0 0 0 0 0  0 0 0 0 0 0
+00 00		# FMPTINFO, Protection field usage and protection interval exp
+0 0 0 0 0 0 0 0  0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0  
+
+# second Format preset descriptor (64 bytes)
+00 00 01 01
+02  0 0  00
+01 00 00 00
+0 0 0 0
+00 00 00 00  01 ff ff ff	# last LBA
+0 0 0 0 0 0 0 0  0 0 0 0 0 0
+00 00		# FMPTINFO, Protection field usage and protection interval exp
+# host-aware zones schema type specific information
+5 ff
+0 0 0 0 0 0 0 0  0 0
+20 00 00 00
+0 0 0 0  
diff --git a/inhex/vpd_lbpro.hex b/inhex/vpd_lbpro.hex
new file mode 100644
index 0000000..188666c
--- /dev/null
+++ b/inhex/vpd_lbpro.hex
@@ -0,0 +1,7 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_lbpro.hex
+
+01 b5 00 10 00 00 00 00
+03 01 20 e0
+07 02 10 80 00 00 00 00
diff --git a/inhex/vpd_lbpv.hex b/inhex/vpd_lbpv.hex
new file mode 100644
index 0000000..a89af17
--- /dev/null
+++ b/inhex/vpd_lbpv.hex
@@ -0,0 +1,9 @@
+#
+# This is a manufactured response to an INQUIRY for the
+# Logical Block Provisioning VPD page (0xb2  ["lbpv"]). It may
+# have been generated by a call like this:
+#        sg_vpd -p lbpv /dev/sg3 -HHHH
+
+# Logical block provisioning VPD page
+00 b2 00 10 00 01 00 00
+01 03 00 08 51 22 33 44  55 66 77 88
diff --git a/inhex/vpd_ref.hex b/inhex/vpd_ref.hex
new file mode 100644
index 0000000..618f911
--- /dev/null
+++ b/inhex/vpd_ref.hex
@@ -0,0 +1,9 @@
+#
+# This is a manufactured response to an INQUIRY for the
+# Referrals VPD page (0xb3  ["ref"]). It may have been
+# generated by a call like this:
+#        sg_vpd -p ref /dev/sg3 -HHHH
+
+# Referrals VPD page
+00 b3 00 0c  00 00 00 00
+11 22 33 44  00 00 10 00
diff --git a/inhex/vpd_sbl.hex b/inhex/vpd_sbl.hex
new file mode 100644
index 0000000..da9e9b1
--- /dev/null
+++ b/inhex/vpd_sbl.hex
@@ -0,0 +1,10 @@
+#
+# This is a manufactured response to an INQUIRY for the
+# Supported block lengths and protection types VPD page (0xb4  ["sbl"]).
+# It may have been generated by a call like this:
+#        sg_vpd -p sbl /dev/sg3 -HHHH
+
+# Supported block lengths and protection types VPD page
+00 b4 00 10
+00 00 02 00 47 07 00 00
+00 00 10 00 47 07 00 00
diff --git a/inhex/vpd_sdeb.hex b/inhex/vpd_sdeb.hex
new file mode 100644
index 0000000..2f50c89
--- /dev/null
+++ b/inhex/vpd_sdeb.hex
@@ -0,0 +1,99 @@
+#
+# The VPD responses in this file where generated from a dummy device
+# (ramdisk) associated with the Linux scsi_debug driver as follows:
+#        sg_vpd -a /dev/sg3 -HHHH
+
+# Supported VPD pages VPD page
+00 00 00 0c 00 80 83 84  85 86 87 88 89 b0 b1 b2
+
+# Unit serial number VPD page
+00 80 00 04 32 30 30 30
+
+# Device identification VPD page
+00 83 00 70 02 01 00 1c  4c 69 6e 75 78 20 20 20
+73 63 73 69 5f 64 65 62  75 67 20 20 20 20 20 20
+32 30 30 30 01 03 00 08  33 33 33 30 00 00 07 d0
+61 94 00 04 00 00 00 01  61 93 00 08 32 22 22 20
+00 00 07 ce 61 95 00 04  00 00 01 00 61 a3 00 08
+32 22 22 20 00 00 07 cd  63 a8 00 18 6e 61 61 2e
+33 32 32 32 32 32 32 30  30 30 30 30 30 37 43 44
+00 00 00 00
+
+# Software interface identification VPD page
+00 84 00 12 22 22 22 00  bb 00 22 22 22 00 bb 01
+22 22 22 00 bb 02
+
+# Management network addresses VPD page
+00 85 00 44 01 00 00 20  68 74 74 70 73 3a 2f 2f
+77 77 77 2e 6b 65 72 6e  65 6c 2e 6f 72 67 2f 63
+6f 6e 66 69 67 00 00 00  04 00 00 1c 68 74 74 70
+3a 2f 2f 77 77 77 2e 6b  65 72 6e 65 6c 2e 6f 72
+67 2f 6c 6f 67 00 00 00
+
+# extended INQUIRY data VPD page
+00 86 00 3c 00 07 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# Mode page policy VPD page
+00 87 00 08 02 00 80 00  18 00 82 00
+
+# SCSI Ports VPD page
+00 88 00 30 00 00 00 01  00 00 00 00 00 00 00 0c
+61 93 00 08 32 22 22 20  00 00 07 ce 00 00 00 02
+00 00 00 00 00 00 00 0c  61 93 00 08 32 22 22 20
+00 00 07 cf
+
+# ATA information VPD page
+00 89 02 38 00 00 00 00  6c 69 6e 75 78 20 20 20
+53 41 54 20 73 63 73 69  5f 64 65 62 75 67 20 20
+31 32 33 34 34 00 00 00  01 00 00 00 00 00 00 00
+01 00 00 00 00 00 00 00  ec 00 00 00 5a 0c ff 3f
+37 c8 10 00 00 00 00 00  3f 00 00 00 00 00 00 00
+58 58 58 58 58 58 58 58  20 20 20 20 20 20 20 20
+20 20 20 20 00 00 00 40  04 00 2e 33 38 31 20 20
+20 20 54 53 38 33 30 30  33 31 53 41 20 20 20 20
+20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20
+20 20 20 20 20 20 20 20  20 20 10 80 00 00 00 2f
+00 00 00 02 00 02 07 00  ff ff 01 00 3f 00 c1 ff
+3e 00 10 01 b0 f8 50 09  00 00 07 00 03 00 78 00
+78 00 f0 00 78 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 02 00 00 00  00 00 00 00 7e 00 1b 00
+6b 34 01 7d 03 40 69 34  01 3c 03 40 7f 40 00 00
+00 00 fe fe 00 00 00 00  00 fe 00 00 00 00 00 00
+00 00 00 00 b0 f8 50 09  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 01 00 b0 f8
+50 09 b0 f8 50 09 20 20  02 00 b6 42 00 80 8a 00
+06 3c 0a 3c ff ff c6 07  00 01 00 08 f0 0f 00 10
+02 00 30 00 00 00 00 00  00 00 06 fe 00 00 02 00
+50 00 8a 00 4f 95 00 00  21 00 0b 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 a5 51
+
+# Block limits VPD page
+00 b0 00 3c 00 00 00 01  00 00 40 00 00 00 04 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 01
+00 00 00 00 00 00 00 00  00 00 ff ff 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# Block device characteristics VPD page
+00 b1 00 3c 00 01 00 05  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# Logical block provisioning VPD page
+00 b2 00 04 00 00 00 00
diff --git a/inhex/vpd_sfs.hex b/inhex/vpd_sfs.hex
new file mode 100644
index 0000000..f4c2ab0
--- /dev/null
+++ b/inhex/vpd_sfs.hex
@@ -0,0 +1,7 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_sfs.hex
+
+00 92 00 0e 00 00 00 00
+00 01 01 01 01 02 02 01
+09 09
diff --git a/inhex/vpd_tpc.hex b/inhex/vpd_tpc.hex
new file mode 100644
index 0000000..e3e9f38
--- /dev/null
+++ b/inhex/vpd_tpc.hex
@@ -0,0 +1,43 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_tpc.hex
+
+00 8f 00 8c
+
+00 00 00 20
+00 00 00 00 00 00 01 23  00 00 00 3c
+00 00 00 1e 00 00 00 99  88 77 66 55 00 00 00 44
+55 66 77 88
+
+00 01 00 10
+0d
+00 00
+03 00
+12 00
+83 02 10 11
+84 01 07
+00 00 	# pad
+
+00 04 00 1c
+00 00 00 00 00 1c 00 40  00 01 22 33
+00 99 88 77 00 00 00 00  00 00 00 00 00 00 00 00
+
+00 08 00 04
+02
+02 e9
+00	# pad
+
+00 0c 00 0c
+00 08
+00 00
+00 01
+c0 00
+ff ff
+00 00	# pad
+
+00 0d 00 14
+12
+# UUID
+10 00 11 22 33 44 55 66  77 88 99 aa bb cc dd ee
+fe dc
+00   # pad
diff --git a/inhex/vpd_zbdc.hex b/inhex/vpd_zbdc.hex
new file mode 100644
index 0000000..d8d1ad3
--- /dev/null
+++ b/inhex/vpd_zbdc.hex
@@ -0,0 +1,29 @@
+#
+# An example invocation:
+#    sg_vpd --inhex=vpd_zbdc.hex
+
+# Zoned block device characteristics VPD page [0xb6]
+# Host managed zoned block device model; pdt=0x14
+14 b6 00 3c
+# ZBD extension=0; AAORb=0; URSWRZ=0
+00 00 00 00
+ 
+# Optimal # of open sequential write preferred
+00 00 00 00
+ 
+# Optimal # of open non-sequentailly written sequential write preferred
+00 00 00 00
+ 
+# maximum # of open sequential write required
+00 00 00 08
+ 
+# Zone alignment mode=1 (constant zone lengths)
+00 00 00 01
+ 
+# Zone starting LBA granularity
+00 00 00 02  00 00 00 00
+
+
+# pad to total length of 64 bytes
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
diff --git a/inhex/vpd_zbdc.raw b/inhex/vpd_zbdc.raw
new file mode 100644
index 0000000..249dbd5
--- /dev/null
+++ b/inhex/vpd_zbdc.raw
Binary files differ
diff --git a/inhex/z_act_query.hex b/inhex/z_act_query.hex
new file mode 100644
index 0000000..16d094e
--- /dev/null
+++ b/inhex/z_act_query.hex
@@ -0,0 +1,28 @@
+# This is the output (in hex) of a simulated SCSI ZONE QUERY command.
+#
+# The hex bytes in this file may be generated by:
+#     sg_z_act_query /dev/sg1 -HHH > /tmp/z_act_query.hex
+# where /dev/sg1 was a SCSI device implementing ZBC-2.
+
+# An example invocation:
+#    sg_z_act_query --inhex=z_act_query.hex
+
+
+# parameter data header (64 bytes)
+00 00 00 80 00 00 00 80  80 00 03 00 00 00 00 00
+00 00 00 04 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
+
+# first zone activation descriptor, zone type: conventional
+01 00 01 00 00 00 00 00  00 00 00 00 00 00 00 04
+00 00 00 20 00 00 00 00  00 00 00 00 00 00 00 00
+# second zone activation descriptor, zone type: sequential write required
+02 10 02 00 00 00 00 00  00 00 00 00 00 00 00 05
+00 00 00 40 00 00 00 00  00 00 00 00 00 00 00 00
+# third zone descriptor, zone type: sequential write required
+02 10 03 00 00 00 00 00  00 00 00 00 00 00 00 06
+00 00 00 60 00 00 00 00  00 00 00 00 00 00 00 00
+# fourth and last zone activation descriptor, zone type: sequential write required
+02 10 04 00 00 00 00 00  00 00 00 00 00 00 00 07
+00 00 00 80 00 00 00 00  00 00 00 00 00 00 00 00
diff --git a/install-sh b/install-sh
new file mode 100755
index 0000000..ec298b5
--- /dev/null
+++ b/install-sh
@@ -0,0 +1,541 @@
+#!/bin/sh
+# install - install a program, script, or datafile
+
+scriptversion=2020-11-14.01; # UTC
+
+# This originates from X11R5 (mit/util/scripts/install.sh), which was
+# later released in X11R6 (xc/config/util/install.sh) with the
+# following copyright and license.
+#
+# Copyright (C) 1994 X Consortium
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+# X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-
+# TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# Except as contained in this notice, the name of the X Consortium shall not
+# be used in advertising or otherwise to promote the sale, use or other deal-
+# ings in this Software without prior written authorization from the X Consor-
+# tium.
+#
+#
+# FSF changes to this file are in the public domain.
+#
+# Calling this script install-sh is preferred over install.sh, to prevent
+# 'make' implicit rules from creating a file called install from it
+# when there is no Makefile.
+#
+# This script is compatible with the BSD install script, but was written
+# from scratch.
+
+tab='	'
+nl='
+'
+IFS=" $tab$nl"
+
+# Set DOITPROG to "echo" to test this script.
+
+doit=${DOITPROG-}
+doit_exec=${doit:-exec}
+
+# Put in absolute file names if you don't have them in your path;
+# or use environment vars.
+
+chgrpprog=${CHGRPPROG-chgrp}
+chmodprog=${CHMODPROG-chmod}
+chownprog=${CHOWNPROG-chown}
+cmpprog=${CMPPROG-cmp}
+cpprog=${CPPROG-cp}
+mkdirprog=${MKDIRPROG-mkdir}
+mvprog=${MVPROG-mv}
+rmprog=${RMPROG-rm}
+stripprog=${STRIPPROG-strip}
+
+posix_mkdir=
+
+# Desired mode of installed file.
+mode=0755
+
+# Create dirs (including intermediate dirs) using mode 755.
+# This is like GNU 'install' as of coreutils 8.32 (2020).
+mkdir_umask=22
+
+backupsuffix=
+chgrpcmd=
+chmodcmd=$chmodprog
+chowncmd=
+mvcmd=$mvprog
+rmcmd="$rmprog -f"
+stripcmd=
+
+src=
+dst=
+dir_arg=
+dst_arg=
+
+copy_on_change=false
+is_target_a_directory=possibly
+
+usage="\
+Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE
+   or: $0 [OPTION]... SRCFILES... DIRECTORY
+   or: $0 [OPTION]... -t DIRECTORY SRCFILES...
+   or: $0 [OPTION]... -d DIRECTORIES...
+
+In the 1st form, copy SRCFILE to DSTFILE.
+In the 2nd and 3rd, copy all SRCFILES to DIRECTORY.
+In the 4th, create DIRECTORIES.
+
+Options:
+     --help     display this help and exit.
+     --version  display version info and exit.
+
+  -c            (ignored)
+  -C            install only if different (preserve data modification time)
+  -d            create directories instead of installing files.
+  -g GROUP      $chgrpprog installed files to GROUP.
+  -m MODE       $chmodprog installed files to MODE.
+  -o USER       $chownprog installed files to USER.
+  -p            pass -p to $cpprog.
+  -s            $stripprog installed files.
+  -S SUFFIX     attempt to back up existing files, with suffix SUFFIX.
+  -t DIRECTORY  install into DIRECTORY.
+  -T            report an error if DSTFILE is a directory.
+
+Environment variables override the default commands:
+  CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG
+  RMPROG STRIPPROG
+
+By default, rm is invoked with -f; when overridden with RMPROG,
+it's up to you to specify -f if you want it.
+
+If -S is not specified, no backups are attempted.
+
+Email bug reports to bug-automake@gnu.org.
+Automake home page: https://www.gnu.org/software/automake/
+"
+
+while test $# -ne 0; do
+  case $1 in
+    -c) ;;
+
+    -C) copy_on_change=true;;
+
+    -d) dir_arg=true;;
+
+    -g) chgrpcmd="$chgrpprog $2"
+        shift;;
+
+    --help) echo "$usage"; exit $?;;
+
+    -m) mode=$2
+        case $mode in
+          *' '* | *"$tab"* | *"$nl"* | *'*'* | *'?'* | *'['*)
+            echo "$0: invalid mode: $mode" >&2
+            exit 1;;
+        esac
+        shift;;
+
+    -o) chowncmd="$chownprog $2"
+        shift;;
+
+    -p) cpprog="$cpprog -p";;
+
+    -s) stripcmd=$stripprog;;
+
+    -S) backupsuffix="$2"
+        shift;;
+
+    -t)
+        is_target_a_directory=always
+        dst_arg=$2
+        # Protect names problematic for 'test' and other utilities.
+        case $dst_arg in
+          -* | [=\(\)!]) dst_arg=./$dst_arg;;
+        esac
+        shift;;
+
+    -T) is_target_a_directory=never;;
+
+    --version) echo "$0 $scriptversion"; exit $?;;
+
+    --) shift
+        break;;
+
+    -*) echo "$0: invalid option: $1" >&2
+        exit 1;;
+
+    *)  break;;
+  esac
+  shift
+done
+
+# We allow the use of options -d and -T together, by making -d
+# take the precedence; this is for compatibility with GNU install.
+
+if test -n "$dir_arg"; then
+  if test -n "$dst_arg"; then
+    echo "$0: target directory not allowed when installing a directory." >&2
+    exit 1
+  fi
+fi
+
+if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then
+  # When -d is used, all remaining arguments are directories to create.
+  # When -t is used, the destination is already specified.
+  # Otherwise, the last argument is the destination.  Remove it from $@.
+  for arg
+  do
+    if test -n "$dst_arg"; then
+      # $@ is not empty: it contains at least $arg.
+      set fnord "$@" "$dst_arg"
+      shift # fnord
+    fi
+    shift # arg
+    dst_arg=$arg
+    # Protect names problematic for 'test' and other utilities.
+    case $dst_arg in
+      -* | [=\(\)!]) dst_arg=./$dst_arg;;
+    esac
+  done
+fi
+
+if test $# -eq 0; then
+  if test -z "$dir_arg"; then
+    echo "$0: no input file specified." >&2
+    exit 1
+  fi
+  # It's OK to call 'install-sh -d' without argument.
+  # This can happen when creating conditional directories.
+  exit 0
+fi
+
+if test -z "$dir_arg"; then
+  if test $# -gt 1 || test "$is_target_a_directory" = always; then
+    if test ! -d "$dst_arg"; then
+      echo "$0: $dst_arg: Is not a directory." >&2
+      exit 1
+    fi
+  fi
+fi
+
+if test -z "$dir_arg"; then
+  do_exit='(exit $ret); exit $ret'
+  trap "ret=129; $do_exit" 1
+  trap "ret=130; $do_exit" 2
+  trap "ret=141; $do_exit" 13
+  trap "ret=143; $do_exit" 15
+
+  # Set umask so as not to create temps with too-generous modes.
+  # However, 'strip' requires both read and write access to temps.
+  case $mode in
+    # Optimize common cases.
+    *644) cp_umask=133;;
+    *755) cp_umask=22;;
+
+    *[0-7])
+      if test -z "$stripcmd"; then
+        u_plus_rw=
+      else
+        u_plus_rw='% 200'
+      fi
+      cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;;
+    *)
+      if test -z "$stripcmd"; then
+        u_plus_rw=
+      else
+        u_plus_rw=,u+rw
+      fi
+      cp_umask=$mode$u_plus_rw;;
+  esac
+fi
+
+for src
+do
+  # Protect names problematic for 'test' and other utilities.
+  case $src in
+    -* | [=\(\)!]) src=./$src;;
+  esac
+
+  if test -n "$dir_arg"; then
+    dst=$src
+    dstdir=$dst
+    test -d "$dstdir"
+    dstdir_status=$?
+    # Don't chown directories that already exist.
+    if test $dstdir_status = 0; then
+      chowncmd=""
+    fi
+  else
+
+    # Waiting for this to be detected by the "$cpprog $src $dsttmp" command
+    # might cause directories to be created, which would be especially bad
+    # if $src (and thus $dsttmp) contains '*'.
+    if test ! -f "$src" && test ! -d "$src"; then
+      echo "$0: $src does not exist." >&2
+      exit 1
+    fi
+
+    if test -z "$dst_arg"; then
+      echo "$0: no destination specified." >&2
+      exit 1
+    fi
+    dst=$dst_arg
+
+    # If destination is a directory, append the input filename.
+    if test -d "$dst"; then
+      if test "$is_target_a_directory" = never; then
+        echo "$0: $dst_arg: Is a directory" >&2
+        exit 1
+      fi
+      dstdir=$dst
+      dstbase=`basename "$src"`
+      case $dst in
+	*/) dst=$dst$dstbase;;
+	*)  dst=$dst/$dstbase;;
+      esac
+      dstdir_status=0
+    else
+      dstdir=`dirname "$dst"`
+      test -d "$dstdir"
+      dstdir_status=$?
+    fi
+  fi
+
+  case $dstdir in
+    */) dstdirslash=$dstdir;;
+    *)  dstdirslash=$dstdir/;;
+  esac
+
+  obsolete_mkdir_used=false
+
+  if test $dstdir_status != 0; then
+    case $posix_mkdir in
+      '')
+        # With -d, create the new directory with the user-specified mode.
+        # Otherwise, rely on $mkdir_umask.
+        if test -n "$dir_arg"; then
+          mkdir_mode=-m$mode
+        else
+          mkdir_mode=
+        fi
+
+        posix_mkdir=false
+	# The $RANDOM variable is not portable (e.g., dash).  Use it
+	# here however when possible just to lower collision chance.
+	tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$
+
+	trap '
+	  ret=$?
+	  rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" 2>/dev/null
+	  exit $ret
+	' 0
+
+	# Because "mkdir -p" follows existing symlinks and we likely work
+	# directly in world-writeable /tmp, make sure that the '$tmpdir'
+	# directory is successfully created first before we actually test
+	# 'mkdir -p'.
+	if (umask $mkdir_umask &&
+	    $mkdirprog $mkdir_mode "$tmpdir" &&
+	    exec $mkdirprog $mkdir_mode -p -- "$tmpdir/a/b") >/dev/null 2>&1
+	then
+	  if test -z "$dir_arg" || {
+	       # Check for POSIX incompatibilities with -m.
+	       # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or
+	       # other-writable bit of parent directory when it shouldn't.
+	       # FreeBSD 6.1 mkdir -m -p sets mode of existing directory.
+	       test_tmpdir="$tmpdir/a"
+	       ls_ld_tmpdir=`ls -ld "$test_tmpdir"`
+	       case $ls_ld_tmpdir in
+		 d????-?r-*) different_mode=700;;
+		 d????-?--*) different_mode=755;;
+		 *) false;;
+	       esac &&
+	       $mkdirprog -m$different_mode -p -- "$test_tmpdir" && {
+		 ls_ld_tmpdir_1=`ls -ld "$test_tmpdir"`
+		 test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1"
+	       }
+	     }
+	  then posix_mkdir=:
+	  fi
+	  rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir"
+	else
+	  # Remove any dirs left behind by ancient mkdir implementations.
+	  rmdir ./$mkdir_mode ./-p ./-- "$tmpdir" 2>/dev/null
+	fi
+	trap '' 0;;
+    esac
+
+    if
+      $posix_mkdir && (
+        umask $mkdir_umask &&
+        $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir"
+      )
+    then :
+    else
+
+      # mkdir does not conform to POSIX,
+      # or it failed possibly due to a race condition.  Create the
+      # directory the slow way, step by step, checking for races as we go.
+
+      case $dstdir in
+        /*) prefix='/';;
+        [-=\(\)!]*) prefix='./';;
+        *)  prefix='';;
+      esac
+
+      oIFS=$IFS
+      IFS=/
+      set -f
+      set fnord $dstdir
+      shift
+      set +f
+      IFS=$oIFS
+
+      prefixes=
+
+      for d
+      do
+        test X"$d" = X && continue
+
+        prefix=$prefix$d
+        if test -d "$prefix"; then
+          prefixes=
+        else
+          if $posix_mkdir; then
+            (umask $mkdir_umask &&
+             $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break
+            # Don't fail if two instances are running concurrently.
+            test -d "$prefix" || exit 1
+          else
+            case $prefix in
+              *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;;
+              *) qprefix=$prefix;;
+            esac
+            prefixes="$prefixes '$qprefix'"
+          fi
+        fi
+        prefix=$prefix/
+      done
+
+      if test -n "$prefixes"; then
+        # Don't fail if two instances are running concurrently.
+        (umask $mkdir_umask &&
+         eval "\$doit_exec \$mkdirprog $prefixes") ||
+          test -d "$dstdir" || exit 1
+        obsolete_mkdir_used=true
+      fi
+    fi
+  fi
+
+  if test -n "$dir_arg"; then
+    { test -z "$chowncmd" || $doit $chowncmd "$dst"; } &&
+    { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } &&
+    { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false ||
+      test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1
+  else
+
+    # Make a couple of temp file names in the proper directory.
+    dsttmp=${dstdirslash}_inst.$$_
+    rmtmp=${dstdirslash}_rm.$$_
+
+    # Trap to clean up those temp files at exit.
+    trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0
+
+    # Copy the file name to the temp name.
+    (umask $cp_umask &&
+     { test -z "$stripcmd" || {
+	 # Create $dsttmp read-write so that cp doesn't create it read-only,
+	 # which would cause strip to fail.
+	 if test -z "$doit"; then
+	   : >"$dsttmp" # No need to fork-exec 'touch'.
+	 else
+	   $doit touch "$dsttmp"
+	 fi
+       }
+     } &&
+     $doit_exec $cpprog "$src" "$dsttmp") &&
+
+    # and set any options; do chmod last to preserve setuid bits.
+    #
+    # If any of these fail, we abort the whole thing.  If we want to
+    # ignore errors from any of these, just make sure not to ignore
+    # errors from the above "$doit $cpprog $src $dsttmp" command.
+    #
+    { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } &&
+    { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } &&
+    { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } &&
+    { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } &&
+
+    # If -C, don't bother to copy if it wouldn't change the file.
+    if $copy_on_change &&
+       old=`LC_ALL=C ls -dlL "$dst"     2>/dev/null` &&
+       new=`LC_ALL=C ls -dlL "$dsttmp"  2>/dev/null` &&
+       set -f &&
+       set X $old && old=:$2:$4:$5:$6 &&
+       set X $new && new=:$2:$4:$5:$6 &&
+       set +f &&
+       test "$old" = "$new" &&
+       $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1
+    then
+      rm -f "$dsttmp"
+    else
+      # If $backupsuffix is set, and the file being installed
+      # already exists, attempt a backup.  Don't worry if it fails,
+      # e.g., if mv doesn't support -f.
+      if test -n "$backupsuffix" && test -f "$dst"; then
+        $doit $mvcmd -f "$dst" "$dst$backupsuffix" 2>/dev/null
+      fi
+
+      # Rename the file to the real destination.
+      $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null ||
+
+      # The rename failed, perhaps because mv can't rename something else
+      # to itself, or perhaps because mv is so ancient that it does not
+      # support -f.
+      {
+        # Now remove or move aside any old file at destination location.
+        # We try this two ways since rm can't unlink itself on some
+        # systems and the destination file might be busy for other
+        # reasons.  In this case, the final cleanup might fail but the new
+        # file should still install successfully.
+        {
+          test ! -f "$dst" ||
+          $doit $rmcmd "$dst" 2>/dev/null ||
+          { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null &&
+            { $doit $rmcmd "$rmtmp" 2>/dev/null; :; }
+          } ||
+          { echo "$0: cannot unlink or rename $dst" >&2
+            (exit 1); exit 1
+          }
+        } &&
+
+        # Now rename the file to the real destination.
+        $doit $mvcmd "$dsttmp" "$dst"
+      }
+    fi || exit 1
+
+    trap '' 0
+  fi
+done
+
+# Local variables:
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/lib/BSD_LICENSE b/lib/BSD_LICENSE
new file mode 100644
index 0000000..15e3da3
--- /dev/null
+++ b/lib/BSD_LICENSE
@@ -0,0 +1,26 @@
+
+Copyright (c) 1999-2019, Douglas Gilbert
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Above is the:
+SPDX-License-Identifier: BSD-2-Clause
diff --git a/lib/Makefile.am b/lib/Makefile.am
new file mode 100644
index 0000000..2c19b1b
--- /dev/null
+++ b/lib/Makefile.am
@@ -0,0 +1,109 @@
+libsgutils2_la_SOURCES = \
+	sg_lib.c \
+	sg_pr2serr.c \
+	sg_lib_data.c \
+	sg_lib_names.c \
+	sg_cmds_basic.c \
+	sg_cmds_basic2.c \
+	sg_cmds_extra.c \
+	sg_cmds_mmc.c \
+	sg_pt_common.c \
+	sg_json_builder.c
+
+if OS_LINUX
+if PT_DUMMY
+libsgutils2_la_SOURCES += sg_pt_dummy.c
+else
+libsgutils2_la_SOURCES += \
+	sg_pt_linux.c \
+	sg_io_linux.c \
+	sg_pt_linux_nvme.c
+endif
+endif
+
+if OS_WIN32_MINGW
+libsgutils2_la_SOURCES += sg_pt_win32.c
+endif
+
+if OS_WIN32_CYGWIN
+libsgutils2_la_SOURCES += sg_pt_win32.c
+endif
+
+if OS_FREEBSD
+if PT_DUMMY
+libsgutils2_la_SOURCES += sg_pt_dummy.c
+else
+libsgutils2_la_SOURCES += sg_pt_freebsd.c
+endif
+endif
+
+if OS_SOLARIS
+libsgutils2_la_SOURCES += sg_pt_solaris.c
+endif
+
+if OS_OSF
+libsgutils2_la_SOURCES += sg_pt_osf1.c
+endif
+
+if OS_HAIKU
+if PT_DUMMY
+libsgutils2_la_SOURCES += sg_pt_dummy.c
+else
+libsgutils2_la_SOURCES += sg_pt_haiku.c
+endif
+endif
+
+if OS_NETBSD
+libsgutils2_la_SOURCES += sg_pt_dummy.c
+endif
+
+if OS_OPENBSD
+libsgutils2_la_SOURCES += sg_pt_dummy.c
+endif
+
+if OS_OTHER
+libsgutils2_la_SOURCES += sg_pt_dummy.c
+endif
+
+if DEBUG
+# This is active if --enable-debug given to ./configure
+# removed -Wduplicated-branches because needs gcc-8
+DBG_CFLAGS = -Wextra -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -Wshadow -Wjump-misses-init
+DBG_CPPFLAGS = -DDEBUG
+else
+DBG_CFLAGS =
+DBG_CPPFLAGS =
+endif
+
+# For C++/clang testing
+## CC = gcc-9
+## CXX = g++
+## CC = clang
+## CXX = clang++
+## CC = clang++
+## CC = powerpc64-linux-gnu-gcc
+
+# -std=<s> can be c99, c11, gnu11, etc. Default is gnu11 for C code
+# -Wall is no longer all warnings. Add -W (since renamed to -Wextra) for more
+AM_CPPFLAGS = -iquote ${top_srcdir}/include -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 $(DBG_CPPFLAGS)
+AM_CFLAGS = -Wall -W $(DBG_CFLAGS)
+# AM_CFLAGS = -Wall -W $(DBG_CFLAGS) -fanalyzer
+# AM_CFLAGS = -Wall -W -Wextra -Wmisleading-indentation -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wnull-dereference -Wshadow -Wjump-misses-init
+# AM_CFLAGS = -Wall -W -pedantic -std=c11
+# AM_CFLAGS = -Wall -W -pedantic -std=c11 --analyze
+# AM_CFLAGS = -Wall -W -pedantic -std=c++11
+# AM_CFLAGS = -Wall -W -pedantic -std=c++14
+# AM_CFLAGS = -Wall -W -pedantic -std=c++1z
+# AM_CFLAGS = -Wall -W -pedantic -std=c++20
+# AM_CFLAGS = -Wall -W -pedantic -std=c++23
+
+lib_LTLIBRARIES = libsgutils2.la
+
+libsgutils2_la_LDFLAGS = -version-info 2:0:0 -no-undefined -release ${PACKAGE_VERSION}
+
+libsgutils2_la_LIBADD = @GETOPT_O_FILES@
+libsgutils2_la_DEPENDENCIES = @GETOPT_O_FILES@
+
+EXTRA_DIST = \
+	sg_json_builder.h \
+	BSD_LICENSE
diff --git a/lib/Makefile.in b/lib/Makefile.in
new file mode 100644
index 0000000..529c7d1
--- /dev/null
+++ b/lib/Makefile.in
@@ -0,0 +1,800 @@
+# Makefile.in generated by automake 1.16.5 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2021 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+  if test -z '$(MAKELEVEL)'; then \
+    false; \
+  elif test -n '$(MAKE_HOST)'; then \
+    true; \
+  elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+    true; \
+  else \
+    false; \
+  fi; \
+}
+am__make_running_with_option = \
+  case $${target_option-} in \
+      ?) ;; \
+      *) echo "am__make_running_with_option: internal error: invalid" \
+              "target option '$${target_option-}' specified" >&2; \
+         exit 1;; \
+  esac; \
+  has_opt=no; \
+  sane_makeflags=$$MAKEFLAGS; \
+  if $(am__is_gnu_make); then \
+    sane_makeflags=$$MFLAGS; \
+  else \
+    case $$MAKEFLAGS in \
+      *\\[\ \	]*) \
+        bs=\\; \
+        sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+          | sed "s/$$bs$$bs[$$bs $$bs	]*//g"`;; \
+    esac; \
+  fi; \
+  skip_next=no; \
+  strip_trailopt () \
+  { \
+    flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+  }; \
+  for flg in $$sane_makeflags; do \
+    test $$skip_next = yes && { skip_next=no; continue; }; \
+    case $$flg in \
+      *=*|--*) continue;; \
+        -*I) strip_trailopt 'I'; skip_next=yes;; \
+      -*I?*) strip_trailopt 'I';; \
+        -*O) strip_trailopt 'O'; skip_next=yes;; \
+      -*O?*) strip_trailopt 'O';; \
+        -*l) strip_trailopt 'l'; skip_next=yes;; \
+      -*l?*) strip_trailopt 'l';; \
+      -[dEDm]) skip_next=yes;; \
+      -[JT]) skip_next=yes;; \
+    esac; \
+    case $$flg in \
+      *$$target_option*) has_opt=yes; break;; \
+    esac; \
+  done; \
+  test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@OS_LINUX_TRUE@@PT_DUMMY_TRUE@am__append_1 = sg_pt_dummy.c
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@am__append_2 = \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_pt_linux.c \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_io_linux.c \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_pt_linux_nvme.c
+
+@OS_WIN32_MINGW_TRUE@am__append_3 = sg_pt_win32.c
+@OS_WIN32_CYGWIN_TRUE@am__append_4 = sg_pt_win32.c
+@OS_FREEBSD_TRUE@@PT_DUMMY_TRUE@am__append_5 = sg_pt_dummy.c
+@OS_FREEBSD_TRUE@@PT_DUMMY_FALSE@am__append_6 = sg_pt_freebsd.c
+@OS_SOLARIS_TRUE@am__append_7 = sg_pt_solaris.c
+@OS_OSF_TRUE@am__append_8 = sg_pt_osf1.c
+@OS_HAIKU_TRUE@@PT_DUMMY_TRUE@am__append_9 = sg_pt_dummy.c
+@OS_HAIKU_TRUE@@PT_DUMMY_FALSE@am__append_10 = sg_pt_haiku.c
+@OS_NETBSD_TRUE@am__append_11 = sg_pt_dummy.c
+@OS_OPENBSD_TRUE@am__append_12 = sg_pt_dummy.c
+@OS_OTHER_TRUE@am__append_13 = sg_pt_dummy.c
+subdir = lib
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+	$(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+    $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+    *) f=$$p;; \
+  esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+  srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+  for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+  for p in $$list; do echo "$$p $$p"; done | \
+  sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+  $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+    if (++n[$$2] == $(am__install_max)) \
+      { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+    END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+  sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+  sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+  test -z "$$files" \
+    || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+    || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+         $(am__cd) "$$dir" && rm -f $$files; }; \
+  }
+am__installdirs = "$(DESTDIR)$(libdir)"
+LTLIBRARIES = $(lib_LTLIBRARIES)
+am__libsgutils2_la_SOURCES_DIST = sg_lib.c sg_pr2serr.c sg_lib_data.c \
+	sg_lib_names.c sg_cmds_basic.c sg_cmds_basic2.c \
+	sg_cmds_extra.c sg_cmds_mmc.c sg_pt_common.c sg_json_builder.c \
+	sg_pt_dummy.c sg_pt_linux.c sg_io_linux.c sg_pt_linux_nvme.c \
+	sg_pt_win32.c sg_pt_freebsd.c sg_pt_solaris.c sg_pt_osf1.c \
+	sg_pt_haiku.c
+@OS_LINUX_TRUE@@PT_DUMMY_TRUE@am__objects_1 = sg_pt_dummy.lo
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@am__objects_2 = sg_pt_linux.lo \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_io_linux.lo \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_pt_linux_nvme.lo
+@OS_WIN32_MINGW_TRUE@am__objects_3 = sg_pt_win32.lo
+@OS_WIN32_CYGWIN_TRUE@am__objects_4 = sg_pt_win32.lo
+@OS_FREEBSD_TRUE@@PT_DUMMY_TRUE@am__objects_5 = sg_pt_dummy.lo
+@OS_FREEBSD_TRUE@@PT_DUMMY_FALSE@am__objects_6 = sg_pt_freebsd.lo
+@OS_SOLARIS_TRUE@am__objects_7 = sg_pt_solaris.lo
+@OS_OSF_TRUE@am__objects_8 = sg_pt_osf1.lo
+@OS_HAIKU_TRUE@@PT_DUMMY_TRUE@am__objects_9 = sg_pt_dummy.lo
+@OS_HAIKU_TRUE@@PT_DUMMY_FALSE@am__objects_10 = sg_pt_haiku.lo
+@OS_NETBSD_TRUE@am__objects_11 = sg_pt_dummy.lo
+@OS_OPENBSD_TRUE@am__objects_12 = sg_pt_dummy.lo
+@OS_OTHER_TRUE@am__objects_13 = sg_pt_dummy.lo
+am_libsgutils2_la_OBJECTS = sg_lib.lo sg_pr2serr.lo sg_lib_data.lo \
+	sg_lib_names.lo sg_cmds_basic.lo sg_cmds_basic2.lo \
+	sg_cmds_extra.lo sg_cmds_mmc.lo sg_pt_common.lo \
+	sg_json_builder.lo $(am__objects_1) $(am__objects_2) \
+	$(am__objects_3) $(am__objects_4) $(am__objects_5) \
+	$(am__objects_6) $(am__objects_7) $(am__objects_8) \
+	$(am__objects_9) $(am__objects_10) $(am__objects_11) \
+	$(am__objects_12) $(am__objects_13)
+libsgutils2_la_OBJECTS = $(am_libsgutils2_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 = 
+libsgutils2_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
+	$(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
+	$(AM_CFLAGS) $(CFLAGS) $(libsgutils2_la_LDFLAGS) $(LDFLAGS) -o \
+	$@
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo "  GEN     " $@;
+am__v_GEN_1 = 
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 = 
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/sg_cmds_basic.Plo \
+	./$(DEPDIR)/sg_cmds_basic2.Plo ./$(DEPDIR)/sg_cmds_extra.Plo \
+	./$(DEPDIR)/sg_cmds_mmc.Plo ./$(DEPDIR)/sg_io_linux.Plo \
+	./$(DEPDIR)/sg_json_builder.Plo ./$(DEPDIR)/sg_lib.Plo \
+	./$(DEPDIR)/sg_lib_data.Plo ./$(DEPDIR)/sg_lib_names.Plo \
+	./$(DEPDIR)/sg_pr2serr.Plo ./$(DEPDIR)/sg_pt_common.Plo \
+	./$(DEPDIR)/sg_pt_dummy.Plo ./$(DEPDIR)/sg_pt_freebsd.Plo \
+	./$(DEPDIR)/sg_pt_haiku.Plo ./$(DEPDIR)/sg_pt_linux.Plo \
+	./$(DEPDIR)/sg_pt_linux_nvme.Plo ./$(DEPDIR)/sg_pt_osf1.Plo \
+	./$(DEPDIR)/sg_pt_solaris.Plo ./$(DEPDIR)/sg_pt_win32.Plo
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+	$(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+	$(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+	$(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+	$(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo "  CC      " $@;
+am__v_CC_1 = 
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+	$(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+	$(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo "  CCLD    " $@;
+am__v_CCLD_1 = 
+SOURCES = $(libsgutils2_la_SOURCES)
+DIST_SOURCES = $(am__libsgutils2_la_SOURCES_DIST)
+am__can_run_installinfo = \
+  case $$AM_UPDATE_INFO_DIR in \
+    n|no|NO) false;; \
+    *) (install-info --version) >/dev/null 2>&1;; \
+  esac
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates.  Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+  BEGIN { nonempty = 0; } \
+  { items[$$0] = 1; nonempty = 1; } \
+  END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique.  This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+  list='$(am__tagged_files)'; \
+  unique=`for i in $$list; do \
+    if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+  done | $(am__uniquify_input)`
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CSCOPE = @CSCOPE@
+CTAGS = @CTAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+ETAGS = @ETAGS@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FILECMD = @FILECMD@
+GETOPT_O_FILES = @GETOPT_O_FILES@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PTHREAD_LIB = @PTHREAD_LIB@
+RANLIB = @RANLIB@
+RT_LIB = @RT_LIB@
+SED = @SED@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+STRIP = @STRIP@
+VERSION = @VERSION@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+os_cflags = @os_cflags@
+os_libs = @os_libs@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+libsgutils2_la_SOURCES = sg_lib.c sg_pr2serr.c sg_lib_data.c \
+	sg_lib_names.c sg_cmds_basic.c sg_cmds_basic2.c \
+	sg_cmds_extra.c sg_cmds_mmc.c sg_pt_common.c sg_json_builder.c \
+	$(am__append_1) $(am__append_2) $(am__append_3) \
+	$(am__append_4) $(am__append_5) $(am__append_6) \
+	$(am__append_7) $(am__append_8) $(am__append_9) \
+	$(am__append_10) $(am__append_11) $(am__append_12) \
+	$(am__append_13)
+@DEBUG_FALSE@DBG_CFLAGS = 
+
+# This is active if --enable-debug given to ./configure
+# removed -Wduplicated-branches because needs gcc-8
+@DEBUG_TRUE@DBG_CFLAGS = -Wextra -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -Wshadow -Wjump-misses-init
+@DEBUG_FALSE@DBG_CPPFLAGS = 
+@DEBUG_TRUE@DBG_CPPFLAGS = -DDEBUG
+
+# For C++/clang testing
+
+# -std=<s> can be c99, c11, gnu11, etc. Default is gnu11 for C code
+# -Wall is no longer all warnings. Add -W (since renamed to -Wextra) for more
+AM_CPPFLAGS = -iquote ${top_srcdir}/include -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 $(DBG_CPPFLAGS)
+AM_CFLAGS = -Wall -W $(DBG_CFLAGS)
+# AM_CFLAGS = -Wall -W $(DBG_CFLAGS) -fanalyzer
+# AM_CFLAGS = -Wall -W -Wextra -Wmisleading-indentation -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wnull-dereference -Wshadow -Wjump-misses-init
+# AM_CFLAGS = -Wall -W -pedantic -std=c11
+# AM_CFLAGS = -Wall -W -pedantic -std=c11 --analyze
+# AM_CFLAGS = -Wall -W -pedantic -std=c++11
+# AM_CFLAGS = -Wall -W -pedantic -std=c++14
+# AM_CFLAGS = -Wall -W -pedantic -std=c++1z
+# AM_CFLAGS = -Wall -W -pedantic -std=c++20
+# AM_CFLAGS = -Wall -W -pedantic -std=c++23
+lib_LTLIBRARIES = libsgutils2.la
+libsgutils2_la_LDFLAGS = -version-info 2:0:0 -no-undefined -release ${PACKAGE_VERSION}
+libsgutils2_la_LIBADD = @GETOPT_O_FILES@
+libsgutils2_la_DEPENDENCIES = @GETOPT_O_FILES@
+EXTRA_DIST = \
+	sg_json_builder.h \
+	BSD_LICENSE
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am  $(am__configure_deps)
+	@for dep in $?; do \
+	  case '$(am__configure_deps)' in \
+	    *$$dep*) \
+	      ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+	        && { if test -f $@; then exit 0; else break; fi; }; \
+	      exit 1;; \
+	  esac; \
+	done; \
+	echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign lib/Makefile'; \
+	$(am__cd) $(top_srcdir) && \
+	  $(AUTOMAKE) --foreign lib/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+	@case '$?' in \
+	  *config.status*) \
+	    cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+	  *) \
+	    echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+	    cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+	esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+install-libLTLIBRARIES: $(lib_LTLIBRARIES)
+	@$(NORMAL_INSTALL)
+	@list='$(lib_LTLIBRARIES)'; test -n "$(libdir)" || list=; \
+	list2=; for p in $$list; do \
+	  if test -f $$p; then \
+	    list2="$$list2 $$p"; \
+	  else :; fi; \
+	done; \
+	test -z "$$list2" || { \
+	  echo " $(MKDIR_P) '$(DESTDIR)$(libdir)'"; \
+	  $(MKDIR_P) "$(DESTDIR)$(libdir)" || exit 1; \
+	  echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(libdir)'"; \
+	  $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(libdir)"; \
+	}
+
+uninstall-libLTLIBRARIES:
+	@$(NORMAL_UNINSTALL)
+	@list='$(lib_LTLIBRARIES)'; test -n "$(libdir)" || list=; \
+	for p in $$list; do \
+	  $(am__strip_dir) \
+	  echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(libdir)/$$f'"; \
+	  $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(libdir)/$$f"; \
+	done
+
+clean-libLTLIBRARIES:
+	-test -z "$(lib_LTLIBRARIES)" || rm -f $(lib_LTLIBRARIES)
+	@list='$(lib_LTLIBRARIES)'; \
+	locs=`for p in $$list; do echo $$p; done | \
+	      sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+	      sort -u`; \
+	test -z "$$locs" || { \
+	  echo rm -f $${locs}; \
+	  rm -f $${locs}; \
+	}
+
+libsgutils2.la: $(libsgutils2_la_OBJECTS) $(libsgutils2_la_DEPENDENCIES) $(EXTRA_libsgutils2_la_DEPENDENCIES) 
+	$(AM_V_CCLD)$(libsgutils2_la_LINK) -rpath $(libdir) $(libsgutils2_la_OBJECTS) $(libsgutils2_la_LIBADD) $(LIBS)
+
+mostlyclean-compile:
+	-rm -f *.$(OBJEXT)
+
+distclean-compile:
+	-rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_cmds_basic.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_cmds_basic2.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_cmds_extra.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_cmds_mmc.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_io_linux.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_json_builder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_lib.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_lib_data.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_lib_names.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pr2serr.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_common.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_dummy.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_freebsd.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_haiku.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_linux.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_linux_nvme.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_osf1.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_solaris.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_pt_win32.Plo@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+	@$(MKDIR_P) $(@D)
+	@echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	$(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	$(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	$(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+	-rm -f *.lo
+
+clean-libtool:
+	-rm -rf .libs _libs
+
+ID: $(am__tagged_files)
+	$(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+	set x; \
+	here=`pwd`; \
+	$(am__define_uniq_tagged_files); \
+	shift; \
+	if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+	  test -n "$$unique" || unique=$$empty_fix; \
+	  if test $$# -gt 0; then \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      "$$@" $$unique; \
+	  else \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      $$unique; \
+	  fi; \
+	fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+	$(am__define_uniq_tagged_files); \
+	test -z "$(CTAGS_ARGS)$$unique" \
+	  || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+	     $$unique
+
+GTAGS:
+	here=`$(am__cd) $(top_builddir) && pwd` \
+	  && $(am__cd) $(top_srcdir) \
+	  && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+	list='$(am__tagged_files)'; \
+	case "$(srcdir)" in \
+	  [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+	  *) sdir=$(subdir)/$(srcdir) ;; \
+	esac; \
+	for i in $$list; do \
+	  if test -f "$$i"; then \
+	    echo "$(subdir)/$$i"; \
+	  else \
+	    echo "$$sdir/$$i"; \
+	  fi; \
+	done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+	-rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+distdir: $(BUILT_SOURCES)
+	$(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+	@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	list='$(DISTFILES)'; \
+	  dist_files=`for file in $$list; do echo $$file; done | \
+	  sed -e "s|^$$srcdirstrip/||;t" \
+	      -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+	case $$dist_files in \
+	  */*) $(MKDIR_P) `echo "$$dist_files" | \
+			   sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+			   sort -u` ;; \
+	esac; \
+	for file in $$dist_files; do \
+	  if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+	  if test -d $$d/$$file; then \
+	    dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+	    if test -d "$(distdir)/$$file"; then \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+	      cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+	  else \
+	    test -f "$(distdir)/$$file" \
+	    || cp -p $$d/$$file "$(distdir)/$$file" \
+	    || exit 1; \
+	  fi; \
+	done
+check-am: all-am
+check: check-am
+all-am: Makefile $(LTLIBRARIES)
+installdirs:
+	for dir in "$(DESTDIR)$(libdir)"; do \
+	  test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+	done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+	@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+	if test -z '$(STRIP)'; then \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	      install; \
+	else \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	    "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+	fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+	-test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+	-test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+	@echo "This command is intended for maintainers to use"
+	@echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libLTLIBRARIES clean-libtool \
+	mostlyclean-am
+
+distclean: distclean-am
+		-rm -f ./$(DEPDIR)/sg_cmds_basic.Plo
+	-rm -f ./$(DEPDIR)/sg_cmds_basic2.Plo
+	-rm -f ./$(DEPDIR)/sg_cmds_extra.Plo
+	-rm -f ./$(DEPDIR)/sg_cmds_mmc.Plo
+	-rm -f ./$(DEPDIR)/sg_io_linux.Plo
+	-rm -f ./$(DEPDIR)/sg_json_builder.Plo
+	-rm -f ./$(DEPDIR)/sg_lib.Plo
+	-rm -f ./$(DEPDIR)/sg_lib_data.Plo
+	-rm -f ./$(DEPDIR)/sg_lib_names.Plo
+	-rm -f ./$(DEPDIR)/sg_pr2serr.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_common.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_dummy.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_freebsd.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_haiku.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_linux.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_linux_nvme.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_osf1.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_solaris.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_win32.Plo
+	-rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+	distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-libLTLIBRARIES
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+		-rm -f ./$(DEPDIR)/sg_cmds_basic.Plo
+	-rm -f ./$(DEPDIR)/sg_cmds_basic2.Plo
+	-rm -f ./$(DEPDIR)/sg_cmds_extra.Plo
+	-rm -f ./$(DEPDIR)/sg_cmds_mmc.Plo
+	-rm -f ./$(DEPDIR)/sg_io_linux.Plo
+	-rm -f ./$(DEPDIR)/sg_json_builder.Plo
+	-rm -f ./$(DEPDIR)/sg_lib.Plo
+	-rm -f ./$(DEPDIR)/sg_lib_data.Plo
+	-rm -f ./$(DEPDIR)/sg_lib_names.Plo
+	-rm -f ./$(DEPDIR)/sg_pr2serr.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_common.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_dummy.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_freebsd.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_haiku.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_linux.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_linux_nvme.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_osf1.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_solaris.Plo
+	-rm -f ./$(DEPDIR)/sg_pt_win32.Plo
+	-rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+	mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-libLTLIBRARIES
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+	clean-generic clean-libLTLIBRARIES clean-libtool cscopelist-am \
+	ctags ctags-am distclean distclean-compile distclean-generic \
+	distclean-libtool distclean-tags distdir dvi dvi-am html \
+	html-am info info-am install install-am install-data \
+	install-data-am install-dvi install-dvi-am install-exec \
+	install-exec-am install-html install-html-am install-info \
+	install-info-am install-libLTLIBRARIES install-man install-pdf \
+	install-pdf-am install-ps install-ps-am install-strip \
+	installcheck installcheck-am installdirs maintainer-clean \
+	maintainer-clean-generic mostlyclean mostlyclean-compile \
+	mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
+	tags tags-am uninstall uninstall-am uninstall-libLTLIBRARIES
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/lib/sg_cmds_basic.c b/lib/sg_cmds_basic.c
new file mode 100644
index 0000000..01ca55c
--- /dev/null
+++ b/lib/sg_cmds_basic.c
@@ -0,0 +1,927 @@
+/*
+ * Copyright (c) 1999-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * CONTENTS
+ *    Some SCSI commands are executed in many contexts and hence began
+ *    to appear in several sg3_utils utilities. This files centralizes
+ *    some of the low level command execution code. In most cases the
+ *    interpretation of the command response is left to the each
+ *    utility.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* Needs to be after config.h */
+#ifdef SG_LIB_LINUX
+#include <errno.h>
+#endif
+
+
+static const char * const version_str = "2.00 20220118";
+
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define EBUFF_SZ 256
+
+#define DEF_PT_TIMEOUT 60       /* 60 seconds */
+#define START_PT_TIMEOUT 120    /* 120 seconds == 2 minutes */
+#define LONG_PT_TIMEOUT 7200    /* 7,200 seconds == 120 minutes */
+
+#define INQUIRY_CMD     0x12
+#define INQUIRY_CMDLEN  6
+#define REQUEST_SENSE_CMD 0x3
+#define REQUEST_SENSE_CMDLEN 6
+#define REPORT_LUNS_CMD 0xa0
+#define REPORT_LUNS_CMDLEN 12
+#define TUR_CMD  0x0
+#define TUR_CMDLEN  6
+
+#define SAFE_STD_INQ_RESP_LEN 36 /* other lengths lock up some devices */
+
+
+const char *
+sg_cmds_version()
+{
+    return version_str;
+}
+
+/* Returns file descriptor >= 0 if successful. If error in Unix returns
+   negated errno. */
+int
+sg_cmds_open_device(const char * device_name, bool read_only, int verbose)
+{
+    return scsi_pt_open_device(device_name, read_only, verbose);
+}
+
+/* Returns file descriptor >= 0 if successful. If error in Unix returns
+   negated errno. */
+int
+sg_cmds_open_flags(const char * device_name, int flags, int verbose)
+{
+    return scsi_pt_open_flags(device_name, flags, verbose);
+}
+
+/* Returns 0 if successful. If error in Unix returns negated errno. */
+int
+sg_cmds_close_device(int device_fd)
+{
+    return scsi_pt_close_device(device_fd);
+}
+
+static const char * const pass_through_s = "pass-through";
+
+static void
+sg_cmds_resid_print(const char * leadin, bool is_din, int num_req,
+                    int num_got)
+{
+    pr2ws("    %s: %s requested %d bytes (data-%s  got %d "
+          "bytes%s\n", leadin, pass_through_s,num_req,
+          (is_din ? "in), got" : "out) but reported"), num_got,
+          (is_din ? "" : " sent"));
+}
+
+static int
+sg_cmds_process_helper(const char * leadin, int req_din_x, int act_din_x,
+                       int req_dout_x, int act_dout_x, const uint8_t * sbp,
+                       int slen, bool noisy, int verbose, int * o_sense_cat)
+{
+    int scat;
+    bool n = false;
+    bool check_data_in = false;
+
+    scat = sg_err_category_sense(sbp, slen);
+    switch (scat) {
+    case SG_LIB_CAT_NOT_READY:
+    case SG_LIB_CAT_INVALID_OP:
+    case SG_LIB_CAT_ILLEGAL_REQ:
+    case SG_LIB_LBA_OUT_OF_RANGE:
+    case SG_LIB_CAT_ABORTED_COMMAND:
+    case SG_LIB_CAT_COPY_ABORTED:
+    case SG_LIB_CAT_DATA_PROTECT:
+    case SG_LIB_CAT_PROTECTION:
+    case SG_LIB_CAT_NO_SENSE:
+    case SG_LIB_CAT_MISCOMPARE:
+    case SG_LIB_CAT_STANDBY:
+    case SG_LIB_CAT_UNAVAILABLE:
+        n = false;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+    case SG_LIB_CAT_MEDIUM_HARD:
+        check_data_in = true;
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+        __attribute__((fallthrough));
+        /* FALL THROUGH */
+#endif
+#endif
+    case SG_LIB_CAT_UNIT_ATTENTION:
+    case SG_LIB_CAT_SENSE:
+    default:
+        n = noisy;
+        break;
+    }
+    if (verbose || n) {
+        char b[512];
+
+        if (leadin && (strlen(leadin) > 0))
+            pr2ws("%s:\n", leadin);
+        sg_get_sense_str(NULL, sbp, slen, (verbose > 1),
+                         sizeof(b), b);
+        pr2ws("%s", b);
+        if (req_din_x > 0) {
+            if (act_din_x != req_din_x) {
+                if ((verbose > 2) || check_data_in || (act_din_x > 0))
+                    sg_cmds_resid_print(leadin, true, req_din_x, act_din_x);
+                if (act_din_x < 0) {
+                    if (verbose)
+                        pr2ws("    %s: %s can't get negative bytes, say it "
+                              "got none\n", leadin, pass_through_s);
+                }
+            }
+        }
+        if (req_dout_x > 0) {
+            if (act_dout_x != req_dout_x) {
+                if ((verbose > 1) && (act_dout_x > 0))
+                    sg_cmds_resid_print(leadin, false, req_dout_x, act_dout_x);
+                if (act_dout_x < 0) {
+                    if (verbose)
+                        pr2ws("    %s: %s can't send negative bytes, say it "
+                              "sent none\n", leadin, pass_through_s);
+                }
+            }
+        }
+    }
+    if (o_sense_cat)
+        *o_sense_cat = scat;
+    return -2;
+}
+
+/* This is a helper function used by sg_cmds_* implementations after the
+ * call to the pass-through. pt_res is returned from do_scsi_pt(). If valid
+ * sense data is found it is decoded and output to sg_warnings_strm (def:
+ * stderr); depending on the 'noisy' and 'verbose' settings. Returns -2 for
+ * o_sense_cat (sense category) written which may not be fatal. Returns
+ * -1 for other types of failure. Returns 0, or a positive number. If data-in
+ * type command (or bidi) then returns actual number of bytes read
+ * (din_len - resid); otherwise returns 0. Note that several sense categories
+ * also have data in bytes received; -2 is still returned. */
+int
+sg_cmds_process_resp(struct sg_pt_base * ptvp, const char * leadin,
+                     int pt_res, bool noisy, int verbose, int * o_sense_cat)
+{
+    int cat, slen, sstat, req_din_x, req_dout_x;
+    int act_din_x, act_dout_x;
+    const uint8_t * sbp;
+    char b[1024];
+
+    if (NULL == leadin)
+        leadin = "";
+    if (pt_res < 0) {
+#ifdef SG_LIB_LINUX
+        if (verbose)
+            pr2ws("%s: %s os error: %s\n", leadin, pass_through_s,
+                  safe_strerror(-pt_res));
+        if ((-ENXIO == pt_res) && o_sense_cat) {
+            if (verbose > 2)
+                pr2ws("map ENXIO to SG_LIB_CAT_NOT_READY\n");
+            *o_sense_cat = SG_LIB_CAT_NOT_READY;
+            return -2;
+        } else if (noisy && (0 == verbose))
+            pr2ws("%s: %s os error: %s\n", leadin, pass_through_s,
+                  safe_strerror(-pt_res));
+#else
+        if (noisy || verbose)
+            pr2ws("%s: %s os error: %s\n", leadin, pass_through_s,
+                  safe_strerror(-pt_res));
+#endif
+        return -1;
+    } else if (SCSI_PT_DO_BAD_PARAMS == pt_res) {
+        pr2ws("%s: bad %s setup\n", leadin, pass_through_s);
+        return -1;
+    } else if (SCSI_PT_DO_TIMEOUT == pt_res) {
+        pr2ws("%s: %s timeout\n", leadin, pass_through_s);
+        return -1;
+    }
+    if (verbose > 2) {
+        uint64_t duration = get_pt_duration_ns(ptvp);
+
+        if (duration > 0)
+            pr2ws("      duration=%" PRIu64 " ns\n", duration);
+        else {
+            int d = get_scsi_pt_duration_ms(ptvp);
+
+            if (d != -1)
+                pr2ws("      duration=%u ms\n", (uint32_t)d);
+        }
+    }
+    get_pt_req_lengths(ptvp, &req_din_x, &req_dout_x);
+    get_pt_actual_lengths(ptvp, &act_din_x, &act_dout_x);
+    slen = get_scsi_pt_sense_len(ptvp);
+    sbp = get_scsi_pt_sense_buf(ptvp);
+    switch ((cat = get_scsi_pt_result_category(ptvp))) {
+    case SCSI_PT_RESULT_GOOD:
+        if (sbp && (slen > 7)) {
+            int resp_code = sbp[0] & 0x7f;
+
+            /* SBC referrals can have status=GOOD and sense_key=COMPLETED */
+            if (resp_code >= 0x70) {
+                if (resp_code < 0x72) {
+                    if (SPC_SK_NO_SENSE != (0xf & sbp[2]))
+                        sg_err_category_sense(sbp, slen);
+                } else if (resp_code < 0x74) {
+                    if (SPC_SK_NO_SENSE != (0xf & sbp[1]))
+                        sg_err_category_sense(sbp, slen);
+                }
+            }
+        }
+        if (req_din_x > 0) {
+            if (act_din_x != req_din_x) {
+                if ((verbose > 1) && (act_din_x >= 0))
+                    sg_cmds_resid_print(leadin, true, req_din_x, act_din_x);
+                if (act_din_x < 0) {
+                    if (verbose)
+                        pr2ws("    %s: %s can't get negative bytes, say it "
+                              "got none\n", leadin, pass_through_s);
+                    act_din_x = 0;
+                }
+            }
+        }
+        if (req_dout_x > 0) {
+            if (act_dout_x != req_dout_x) {
+                if ((verbose > 1) && (act_dout_x >= 0))
+                    sg_cmds_resid_print(leadin, false, req_dout_x, act_dout_x);
+                if (act_dout_x < 0) {
+                    if (verbose)
+                        pr2ws("    %s: %s can't send negative bytes, say it "
+                              "sent none\n", leadin, pass_through_s);
+                    act_dout_x = 0;
+                }
+            }
+        }
+        return act_din_x;
+    case SCSI_PT_RESULT_STATUS: /* other than GOOD and CHECK CONDITION */
+        sstat = get_scsi_pt_status_response(ptvp);
+        if (o_sense_cat) {
+            switch (sstat) {
+            case SAM_STAT_RESERVATION_CONFLICT:
+                *o_sense_cat = SG_LIB_CAT_RES_CONFLICT;
+                return -2;
+            case SAM_STAT_CONDITION_MET:
+                *o_sense_cat = SG_LIB_CAT_CONDITION_MET;
+                return -2;
+            case SAM_STAT_BUSY:
+                *o_sense_cat = SG_LIB_CAT_BUSY;
+                return -2;
+            case SAM_STAT_TASK_SET_FULL:
+                *o_sense_cat = SG_LIB_CAT_TS_FULL;
+                return -2;
+            case SAM_STAT_ACA_ACTIVE:
+                *o_sense_cat = SG_LIB_CAT_ACA_ACTIVE;
+                return -2;
+            case SAM_STAT_TASK_ABORTED:
+                *o_sense_cat = SG_LIB_CAT_TASK_ABORTED;
+                return -2;
+            default:
+                break;
+            }
+        }
+        if (verbose || noisy) {
+            sg_get_scsi_status_str(sstat, sizeof(b), b);
+            pr2ws("%s: scsi status: %s\n", leadin, b);
+        }
+        return -1;
+    case SCSI_PT_RESULT_SENSE:
+        return sg_cmds_process_helper(leadin, req_din_x, act_din_x,
+                                      req_dout_x, act_dout_x, sbp, slen,
+                                      noisy, verbose, o_sense_cat);
+    case SCSI_PT_RESULT_TRANSPORT_ERR:
+        if (verbose || noisy) {
+            get_scsi_pt_transport_err_str(ptvp, sizeof(b), b);
+            pr2ws("%s: transport: %s\n", leadin, b);
+        }
+#ifdef SG_LIB_LINUX
+        return -1;      /* DRIVER_SENSE is not passed through */
+#else
+        /* Shall we favour sense data over a transport error (given both) */
+        {
+            bool favour_sense = ((SAM_STAT_CHECK_CONDITION ==
+                    get_scsi_pt_status_response(ptvp)) && (slen > 0));
+
+            if (favour_sense)
+                return sg_cmds_process_helper(leadin, req_din_x, act_din_x,
+                                              req_dout_x, act_dout_x, sbp,
+                                              slen, noisy, verbose,
+                                              o_sense_cat);
+            else
+                return -1;
+        }
+#endif
+    case SCSI_PT_RESULT_OS_ERR:
+        if (verbose || noisy) {
+            get_scsi_pt_os_err_str(ptvp, sizeof(b), b);
+            pr2ws("%s: os: %s\n", leadin, b);
+        }
+        return -1;
+    default:
+        pr2ws("%s: unknown %s result category (%d)\n", leadin, pass_through_s,
+               cat);
+        return -1;
+    }
+}
+
+bool
+sg_cmds_is_nvme(const struct sg_pt_base * ptvp)
+{
+    return pt_device_is_nvme(ptvp);
+}
+
+static struct sg_pt_base *
+create_pt_obj(const char * cname)
+{
+    struct sg_pt_base * ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp)
+        pr2ws("%s: out of memory\n", cname);
+    return ptvp;
+}
+
+static const char * const inquiry_s = "inquiry";
+
+
+/* Returns 0 on success, while positive values are SG_LIB_CAT_* errors
+ * (e.g. SG_LIB_CAT_MALFORMED). If OS error, returns negated errno or -1. */
+static int
+sg_ll_inquiry_com(struct sg_pt_base * ptvp, int sg_fd, bool cmddt, bool evpd,
+                  int pg_op, void * resp, int mx_resp_len, int timeout_secs,
+                  int * residp, bool noisy, int verbose)
+{
+    bool ptvp_given = false;
+    bool local_sense = true;
+    bool local_cdb = true;
+    int res, ret, sense_cat, resid;
+    uint8_t inq_cdb[INQUIRY_CMDLEN] = {INQUIRY_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    uint8_t * up;
+
+    if (resp == NULL) {
+        if (verbose)
+            pr2ws("Got NULL `resp` pointer");
+        return SG_LIB_CAT_MALFORMED;
+    }
+    if (cmddt)
+        inq_cdb[1] |= 0x2;
+    if (evpd)
+        inq_cdb[1] |= 0x1;
+    inq_cdb[2] = (uint8_t)pg_op;
+    /* 16 bit allocation length (was 8, increased in spc3r09, 200209) */
+    sg_put_unaligned_be16((uint16_t)mx_resp_len, inq_cdb + 3);
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", inquiry_s,
+              sg_get_command_str(inq_cdb, INQUIRY_CMDLEN, false, sizeof(b),
+                                 b));
+    }
+    if (mx_resp_len > 0) {
+        up = (uint8_t *)resp;
+        up[0] = 0x7f;   /* defensive prefill */
+        if (mx_resp_len > 4)
+            up[4] = 0;
+    }
+    if (timeout_secs <= 0)
+        timeout_secs = DEF_PT_TIMEOUT;
+    if (ptvp) {
+        ptvp_given = true;
+        partial_clear_scsi_pt_obj(ptvp);
+        if (get_scsi_pt_cdb_buf(ptvp))
+            local_cdb = false; /* N.B. Ignores locally built cdb */
+        else
+            set_scsi_pt_cdb(ptvp, inq_cdb, sizeof(inq_cdb));
+        if (get_scsi_pt_sense_buf(ptvp))
+            local_sense = false;
+        else
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    } else {
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, verbose);
+        if (NULL == ptvp)
+            return sg_convert_errno(ENOMEM);
+        set_scsi_pt_cdb(ptvp, inq_cdb, sizeof(inq_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    }
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, -1, timeout_secs, verbose);
+    ret = sg_cmds_process_resp(ptvp, inquiry_s, res, noisy, verbose,
+                               &sense_cat);
+    resid = get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = resid;
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else if (ret < 4) {
+        if (verbose)
+            pr2ws("%s: got too few bytes (%d)\n", __func__, ret);
+        ret = SG_LIB_CAT_MALFORMED;
+    } else
+        ret = 0;
+
+    if (resid > 0) {
+        if (resid > mx_resp_len) {
+            pr2ws("%s resid (%d) should never exceed requested "
+                    "len=%d\n", inquiry_s, resid, mx_resp_len);
+            if (0 == ret)
+                ret = SG_LIB_CAT_MALFORMED;
+            goto fini;
+        }
+        /* zero unfilled section of response buffer, based on resid */
+        memset((uint8_t *)resp + (mx_resp_len - resid), 0, resid);
+    }
+fini:
+    if (ptvp_given) {
+        if (local_sense)    /* stop caller trying to access local sense */
+            set_scsi_pt_sense(ptvp, NULL, 0);
+        if (local_cdb)
+            set_scsi_pt_cdb(ptvp, NULL, 0);
+    } else {
+        if (ptvp)
+            destruct_scsi_pt_obj(ptvp);
+    }
+    return ret;
+}
+
+/* Invokes a SCSI INQUIRY command and yields the response. Returns 0 when
+ * successful, various SG_LIB_CAT_* positive values, negated errno or
+ * -1 -> other errors. The CMDDT field is obsolete in the INQUIRY cdb. */
+int
+sg_ll_inquiry(int sg_fd, bool cmddt, bool evpd, int pg_op, void * resp,
+              int mx_resp_len, bool noisy, int verbose)
+{
+    return sg_ll_inquiry_com(NULL, sg_fd, cmddt, evpd, pg_op, resp,
+                             mx_resp_len, 0 /* timeout_sec */, NULL, noisy,
+                             verbose);
+}
+
+/* Invokes a SCSI INQUIRY command and yields the response. Returns 0 when
+ * successful, various SG_LIB_CAT_* positive values or -1 -> other errors.
+ * The CMDDT field is obsolete in the INQUIRY cdb (since spc3r16 in 2003) so
+ * an argument to set it has been removed (use the REPORT SUPPORTED OPERATION
+ * CODES command instead). Adds the ability to set the command abort timeout
+ * and the ability to report the residual count. If timeout_secs is zero
+ * or less the default command abort timeout (60 seconds) is used.
+ * If residp is non-NULL then the residual value is written where residp
+ * points. A residual value of 0 implies mx_resp_len bytes have be written
+ * where resp points. If the residual value equals mx_resp_len then no
+ * bytes have been written. */
+int
+sg_ll_inquiry_v2(int sg_fd, bool evpd, int pg_op, void * resp,
+                 int mx_resp_len, int timeout_secs, int * residp,
+                 bool noisy, int verbose)
+{
+    return sg_ll_inquiry_com(NULL, sg_fd, false, evpd, pg_op, resp,
+                             mx_resp_len, timeout_secs, residp, noisy,
+                             verbose);
+}
+
+/* Similar to _v2 but takes a pointer to an object (derived from) sg_pt_base.
+ * That object is assumed to be constructed and have a device file descriptor
+ * associated with it. Caller is responsible for lifetime of ptp. */
+int
+sg_ll_inquiry_pt(struct sg_pt_base * ptvp, bool evpd, int pg_op, void * resp,
+                 int mx_resp_len, int timeout_secs, int * residp, bool noisy,
+                 int verbose)
+{
+    return sg_ll_inquiry_com(ptvp, -1, false, evpd, pg_op, resp, mx_resp_len,
+                             timeout_secs, residp, noisy, verbose);
+}
+
+/* Yields most of first 36 bytes of a standard INQUIRY (evpd==0) response.
+ * Returns 0 when successful, various SG_LIB_CAT_* positive values, negated
+ * errno or -1 -> other errors */
+int
+sg_simple_inquiry(int sg_fd, struct sg_simple_inquiry_resp * inq_data,
+                  bool noisy, int verbose)
+{
+    int ret;
+    uint8_t * inq_resp = NULL;
+    uint8_t * free_irp = NULL;
+
+    if (inq_data) {
+        memset(inq_data, 0, sizeof(* inq_data));
+        inq_data->peripheral_qualifier = 0x3;
+        inq_data->peripheral_type = PDT_UNKNOWN;
+    }
+    inq_resp = sg_memalign(SAFE_STD_INQ_RESP_LEN, 0, &free_irp, false);
+    if (NULL == inq_resp) {
+        pr2ws("%s: out of memory\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    ret = sg_ll_inquiry_com(NULL, sg_fd, false, false, 0, inq_resp,
+                            SAFE_STD_INQ_RESP_LEN, 0, NULL, noisy, verbose);
+
+    if (inq_data && (0 == ret)) {
+        inq_data->peripheral_qualifier = (inq_resp[0] >> 5) & 0x7;
+        inq_data->peripheral_type = inq_resp[0] & PDT_MASK;
+        inq_data->byte_1 = inq_resp[1];
+        inq_data->version = inq_resp[2];
+        inq_data->byte_3 = inq_resp[3];
+        inq_data->byte_5 = inq_resp[5];
+        inq_data->byte_6 = inq_resp[6];
+        inq_data->byte_7 = inq_resp[7];
+        memcpy(inq_data->vendor, inq_resp + 8, 8);
+        memcpy(inq_data->product, inq_resp + 16, 16);
+        memcpy(inq_data->revision, inq_resp + 32, 4);
+    }
+    if (free_irp)
+        free(free_irp);
+    return ret;
+}
+
+/* Similar to sg_simple_inquiry() but takes pointer to pt object rather
+ * than device file descriptor. */
+int
+sg_simple_inquiry_pt(struct sg_pt_base * ptvp,
+                     struct sg_simple_inquiry_resp * inq_data,
+                     bool noisy, int verbose)
+{
+    int ret;
+    uint8_t * inq_resp = NULL;
+    uint8_t * free_irp = NULL;
+
+    if (inq_data) {
+        memset(inq_data, 0, sizeof(* inq_data));
+        inq_data->peripheral_qualifier = 0x3;
+        inq_data->peripheral_type = PDT_MASK;
+    }
+    inq_resp = sg_memalign(SAFE_STD_INQ_RESP_LEN, 0, &free_irp, false);
+    if (NULL == inq_resp) {
+        pr2ws("%s: out of memory\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    ret = sg_ll_inquiry_com(ptvp, -1, false, false, 0, inq_resp,
+                            SAFE_STD_INQ_RESP_LEN, 0, NULL, noisy, verbose);
+
+    if (inq_data && (0 == ret)) {
+        inq_data->peripheral_qualifier = (inq_resp[0] >> 5) & 0x7;
+        inq_data->peripheral_type = inq_resp[0] & PDT_MASK;
+        inq_data->byte_1 = inq_resp[1];
+        inq_data->version = inq_resp[2];
+        inq_data->byte_3 = inq_resp[3];
+        inq_data->byte_5 = inq_resp[5];
+        inq_data->byte_6 = inq_resp[6];
+        inq_data->byte_7 = inq_resp[7];
+        memcpy(inq_data->vendor, inq_resp + 8, 8);
+        memcpy(inq_data->product, inq_resp + 16, 16);
+        memcpy(inq_data->revision, inq_resp + 32, 4);
+    }
+    if (free_irp)
+        free(free_irp);
+    return ret;
+}
+
+/* Invokes a SCSI TEST UNIT READY command.
+ * N.B. To access the sense buffer outside this routine then one be
+ * provided by the caller.
+ * 'pack_id' is just for diagnostics, safe to set to 0.
+ * Looks for progress indicator if 'progress' non-NULL;
+ * if found writes value [0..65535] else write -1.
+ * Returns 0 when successful, various SG_LIB_CAT_* positive values or
+ * -1 -> other errors */
+static int
+sg_ll_test_unit_ready_com(struct sg_pt_base * ptvp, int sg_fd, int pack_id,
+                          int * progress, bool noisy, int verbose)
+{
+    static const char * const tur_s = "test unit ready";
+    bool ptvp_given = false;
+    bool local_sense = true;
+    bool local_cdb = true;
+    int res, ret, sense_cat;
+    uint8_t tur_cdb[TUR_CMDLEN] = {TUR_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", tur_s,
+              sg_get_command_str(tur_cdb, TUR_CMDLEN, false, sizeof(b), b));
+    }
+    if (ptvp) {
+        ptvp_given = true;
+        partial_clear_scsi_pt_obj(ptvp);
+        if (get_scsi_pt_cdb_buf(ptvp))
+            local_cdb = false; /* N.B. Ignores locally built cdb */
+        else
+            set_scsi_pt_cdb(ptvp, tur_cdb, sizeof(tur_cdb));
+        if (get_scsi_pt_sense_buf(ptvp))
+            local_sense = false;
+        else
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    } else {
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, verbose);
+        if (NULL == ptvp)
+            return sg_convert_errno(ENOMEM);
+        set_scsi_pt_cdb(ptvp, tur_cdb, sizeof(tur_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    }
+    set_scsi_pt_packet_id(ptvp, pack_id);
+    res = do_scsi_pt(ptvp, -1, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, tur_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        if (progress) {
+            int slen = get_scsi_pt_sense_len(ptvp);
+
+            if (! sg_get_sense_progress_fld(sense_b, slen, progress))
+                *progress = -1;
+        }
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    if (ptvp_given) {
+        if (local_sense)    /* stop caller trying to access local sense */
+            set_scsi_pt_sense(ptvp, NULL, 0);
+        if (local_cdb)
+            set_scsi_pt_cdb(ptvp, NULL, 0);
+    } else {
+        if (ptvp)
+            destruct_scsi_pt_obj(ptvp);
+    }
+    return ret;
+}
+
+int
+sg_ll_test_unit_ready_progress_pt(struct sg_pt_base * ptvp, int pack_id,
+                                  int * progress, bool noisy, int verbose)
+{
+    return sg_ll_test_unit_ready_com(ptvp, -1, pack_id, progress, noisy,
+                                     verbose);
+}
+
+int
+sg_ll_test_unit_ready_progress(int sg_fd, int pack_id, int * progress,
+                               bool noisy, int verbose)
+{
+    return sg_ll_test_unit_ready_com(NULL, sg_fd, pack_id, progress, noisy,
+                                     verbose);
+}
+
+/* Invokes a SCSI TEST UNIT READY command.
+ * 'pack_id' is just for diagnostics, safe to set to 0.
+ * Returns 0 when successful, various SG_LIB_CAT_* positive values or
+ * -1 -> other errors */
+int
+sg_ll_test_unit_ready(int sg_fd, int pack_id, bool noisy, int verbose)
+{
+    return sg_ll_test_unit_ready_com(NULL, sg_fd, pack_id, NULL, noisy,
+                                     verbose);
+}
+
+int
+sg_ll_test_unit_ready_pt(struct sg_pt_base * ptvp, int pack_id, bool noisy,
+                         int verbose)
+{
+    return sg_ll_test_unit_ready_com(ptvp, -1, pack_id, NULL, noisy, verbose);
+}
+
+/* Invokes a SCSI REQUEST SENSE command. Returns 0 when successful, various
+ * SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_request_sense_com(struct sg_pt_base * ptvp, int sg_fd, bool desc,
+                        void * resp, int mx_resp_len, bool noisy, int verbose)
+{
+    bool ptvp_given = false;
+    bool local_cdb = true;
+    bool local_sense = true;
+    int ret, res, sense_cat;
+    static const char * const rq_s = "request sense";
+    uint8_t rs_cdb[REQUEST_SENSE_CMDLEN] =
+        {REQUEST_SENSE_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    if (desc)
+        rs_cdb[1] |= 0x1;
+    if (mx_resp_len > 0xff) {
+        pr2ws("mx_resp_len cannot exceed 255\n");
+        return -1;
+    }
+    rs_cdb[4] = mx_resp_len & 0xff;
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", rq_s,
+              sg_get_command_str(rs_cdb, REQUEST_SENSE_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (ptvp) {
+        ptvp_given = true;
+        if (get_scsi_pt_cdb_buf(ptvp))
+            local_cdb = false;
+        else
+            set_scsi_pt_cdb(ptvp, rs_cdb, sizeof(rs_cdb));
+        if (get_scsi_pt_sense_buf(ptvp))
+            local_sense = false;
+        else
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    } else {
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, verbose);
+        if (NULL == ptvp)
+            return sg_convert_errno(ENOMEM);
+        set_scsi_pt_cdb(ptvp, rs_cdb, sizeof(rs_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    }
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, -1, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, rq_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        if ((mx_resp_len >= 8) && (ret < 8)) {
+            if (verbose)
+                pr2ws("    %s: got %d bytes in response, too short\n", rq_s,
+                      ret);
+            ret = -1;
+        } else
+            ret = 0;
+    }
+    if (ptvp_given) {
+        if (local_sense)        /* stop caller accessing local sense */
+        set_scsi_pt_sense(ptvp, NULL, 0);
+        if (local_cdb)  /* stop caller accessing local sense */
+        set_scsi_pt_cdb(ptvp, NULL, 0);
+    } else if (ptvp)
+        destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+int
+sg_ll_request_sense(int sg_fd, bool desc, void * resp, int mx_resp_len,
+                    bool noisy, int verbose)
+{
+    return sg_ll_request_sense_com(NULL, sg_fd, desc, resp, mx_resp_len,
+                                   noisy, verbose);
+}
+
+int
+sg_ll_request_sense_pt(struct sg_pt_base * ptvp, bool desc, void * resp,
+                       int mx_resp_len, bool noisy, int verbose)
+{
+    return sg_ll_request_sense_com(ptvp, -1, desc, resp, mx_resp_len,
+                                   noisy, verbose);
+}
+
+/* Invokes a SCSI REPORT LUNS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_report_luns_com(struct sg_pt_base * ptvp, int sg_fd, int select_report,
+                      void * resp, int mx_resp_len, bool noisy, int verbose)
+{
+    static const char * const report_luns_s = "report luns";
+    bool ptvp_given = false;
+    bool local_cdb = true;
+    bool local_sense = true;
+    int ret, res, sense_cat;
+    uint8_t rl_cdb[REPORT_LUNS_CMDLEN] =
+                         {REPORT_LUNS_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    rl_cdb[2] = select_report & 0xff;
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rl_cdb + 6);
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", report_luns_s,
+              sg_get_command_str(rl_cdb, REPORT_LUNS_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (ptvp) {
+        ptvp_given = true;
+        partial_clear_scsi_pt_obj(ptvp);
+        if (get_scsi_pt_cdb_buf(ptvp))
+            local_cdb = false;
+        else
+            set_scsi_pt_cdb(ptvp, rl_cdb, sizeof(rl_cdb));
+        if (get_scsi_pt_sense_buf(ptvp))
+            local_sense = false;
+        else
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    } else {
+        if (NULL == ((ptvp = create_pt_obj(report_luns_s))))
+            return sg_convert_errno(ENOMEM);
+        set_scsi_pt_cdb(ptvp, rl_cdb, sizeof(rl_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    }
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, report_luns_s, res, noisy, verbose,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    if (ptvp_given) {
+        if (local_sense)        /* stop caller accessing local sense */
+            set_scsi_pt_sense(ptvp, NULL, 0);
+        if (local_cdb)
+            set_scsi_pt_cdb(ptvp, NULL, 0);
+    } else {
+        if (ptvp)
+            destruct_scsi_pt_obj(ptvp);
+    }
+    return ret;
+}
+
+/* Invokes a SCSI REPORT LUNS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors,
+ * Expects sg_fd to be >= 0 representing an open device fd. */
+int
+sg_ll_report_luns(int sg_fd, int select_report, void * resp, int mx_resp_len,
+                  bool noisy, int verbose)
+{
+    return sg_ll_report_luns_com(NULL, sg_fd, select_report, resp,
+                                 mx_resp_len, noisy, verbose);
+}
+
+
+/* Invokes a SCSI REPORT LUNS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors.
+ * Expects a non-NULL ptvp containing an open device fd. */
+int
+sg_ll_report_luns_pt(struct sg_pt_base * ptvp, int select_report,
+                     void * resp, int mx_resp_len, bool noisy, int verbose)
+{
+    return sg_ll_report_luns_com(ptvp, -1, select_report, resp,
+                                 mx_resp_len, noisy, verbose);
+}
diff --git a/lib/sg_cmds_basic2.c b/lib/sg_cmds_basic2.c
new file mode 100644
index 0000000..cbc609a
--- /dev/null
+++ b/lib/sg_cmds_basic2.c
@@ -0,0 +1,1117 @@
+/*
+ * Copyright (c) 1999-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * CONTENTS
+ *    Some SCSI commands are executed in many contexts and hence began
+ *    to appear in several sg3_utils utilities. This files centralizes
+ *    some of the low level command execution code. In most cases the
+ *    interpretation of the command response is left to the each
+ *    utility.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define EBUFF_SZ 256
+
+#define DEF_PT_TIMEOUT 60       /* 60 seconds */
+#define START_PT_TIMEOUT 120    /* 120 seconds == 2 minutes */
+#define LONG_PT_TIMEOUT 7200    /* 7,200 seconds == 120 minutes */
+
+#define SYNCHRONIZE_CACHE_CMD     0x35
+#define SYNCHRONIZE_CACHE_CMDLEN  10
+#define SERVICE_ACTION_IN_16_CMD 0x9e
+#define SERVICE_ACTION_IN_16_CMDLEN 16
+#define READ_CAPACITY_16_SA 0x10
+#define READ_CAPACITY_10_CMD 0x25
+#define READ_CAPACITY_10_CMDLEN 10
+#define MODE_SENSE6_CMD      0x1a
+#define MODE_SENSE6_CMDLEN   6
+#define MODE_SENSE10_CMD     0x5a
+#define MODE_SENSE10_CMDLEN  10
+#define MODE_SELECT6_CMD   0x15
+#define MODE_SELECT6_CMDLEN   6
+#define MODE_SELECT10_CMD   0x55
+#define MODE_SELECT10_CMDLEN  10
+#define LOG_SENSE_CMD     0x4d
+#define LOG_SENSE_CMDLEN  10
+#define LOG_SELECT_CMD     0x4c
+#define LOG_SELECT_CMDLEN  10
+#define START_STOP_CMD          0x1b
+#define START_STOP_CMDLEN       6
+#define PREVENT_ALLOW_CMD    0x1e
+#define PREVENT_ALLOW_CMDLEN   6
+
+#define MODE6_RESP_HDR_LEN 4
+#define MODE10_RESP_HDR_LEN 8
+#define MODE_RESP_ARB_LEN 1024
+
+#define INQUIRY_RESP_INITIAL_LEN 36
+
+
+static struct sg_pt_base *
+create_pt_obj(const char * cname)
+{
+    struct sg_pt_base * ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp)
+        pr2ws("%s: out of memory\n", cname);
+    return ptvp;
+}
+
+/* Invokes a SCSI SYNCHRONIZE CACHE (10) command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_sync_cache_10(int sg_fd, bool sync_nv, bool immed, int group,
+                    unsigned int lba, unsigned int count, bool noisy,
+                    int verbose)
+{
+    static const char * const cdb_s = "synchronize cache(10)";
+    int res, ret, sense_cat;
+    uint8_t sc_cdb[SYNCHRONIZE_CACHE_CMDLEN] =
+                {SYNCHRONIZE_CACHE_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (sync_nv)
+        sc_cdb[1] |= 4;
+    if (immed)
+        sc_cdb[1] |= 2;
+    sg_put_unaligned_be32((uint32_t)lba, sc_cdb + 2);
+    sc_cdb[6] = group & GRPNUM_MASK;
+    if (count > 0xffff) {
+        pr2ws("count too big\n");
+        return -1;
+    }
+    sg_put_unaligned_be16((int16_t)count, sc_cdb + 7);
+
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(sc_cdb, SYNCHRONIZE_CACHE_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, sc_cdb, sizeof(sc_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI READ CAPACITY (16) command. Returns 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_readcap_16(int sg_fd, bool pmi, uint64_t llba, void * resp,
+                 int mx_resp_len, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "read capacity(16)";
+    int ret, res, sense_cat;
+    uint8_t rc_cdb[SERVICE_ACTION_IN_16_CMDLEN] =
+                        {SERVICE_ACTION_IN_16_CMD, READ_CAPACITY_16_SA,
+                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (pmi) { /* lbs only valid when pmi set */
+        rc_cdb[14] |= 1;
+        sg_put_unaligned_be64(llba, rc_cdb + 2);
+    }
+    /* Allocation length, no guidance in SBC-2 rev 15b */
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rc_cdb + 10);
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rc_cdb, SERVICE_ACTION_IN_16_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, rc_cdb, sizeof(rc_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI READ CAPACITY (10) command. Returns 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_readcap_10(int sg_fd, bool pmi, unsigned int lba, void * resp,
+                 int mx_resp_len, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "read capacity(10)";
+    int ret, res, sense_cat;
+    uint8_t rc_cdb[READ_CAPACITY_10_CMDLEN] =
+                         {READ_CAPACITY_10_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (pmi) { /* lbs only valid when pmi set */
+        rc_cdb[8] |= 1;
+        sg_put_unaligned_be32((uint32_t)lba, rc_cdb + 2);
+    }
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rc_cdb, READ_CAPACITY_10_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, rc_cdb, sizeof(rc_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI MODE SENSE (6) command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_mode_sense6(int sg_fd, bool dbd, int pc, int pg_code, int sub_pg_code,
+                  void * resp, int mx_resp_len, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "mode sense(6)";
+    int res, ret, sense_cat, resid;
+    uint8_t modes_cdb[MODE_SENSE6_CMDLEN] =
+        {MODE_SENSE6_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    modes_cdb[1] = (uint8_t)(dbd ? 0x8 : 0);
+    modes_cdb[2] = (uint8_t)(((pc << 6) & 0xc0) | (pg_code & 0x3f));
+    modes_cdb[3] = (uint8_t)(sub_pg_code & 0xff);
+    modes_cdb[4] = (uint8_t)(mx_resp_len & 0xff);
+    if (mx_resp_len > 0xff) {
+        pr2ws("mx_resp_len too big\n");
+        return -1;
+    }
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(modes_cdb, MODE_SENSE6_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, modes_cdb, sizeof(modes_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    resid = get_scsi_pt_resid(ptvp);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        if ((verbose > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == verbose) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+
+    if (resid > 0) {
+        if (resid > mx_resp_len) {
+            pr2ws("%s: resid (%d) should never exceed requested len=%d\n",
+                  cdb_s, resid, mx_resp_len);
+            return ret ? ret : SG_LIB_CAT_MALFORMED;
+        }
+        /* zero unfilled section of response buffer */
+        memset((uint8_t *)resp + (mx_resp_len - resid), 0, resid);
+    }
+    return ret;
+}
+
+/* Invokes a SCSI MODE SENSE (10) command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_mode_sense10(int sg_fd, bool llbaa, bool dbd, int pc, int pg_code,
+                   int sub_pg_code, void * resp, int mx_resp_len,
+                   bool noisy, int verbose)
+{
+    return sg_ll_mode_sense10_v2(sg_fd, llbaa, dbd, pc, pg_code, sub_pg_code,
+                                 resp, mx_resp_len, 0, NULL, noisy, verbose);
+}
+
+/* Invokes a SCSI MODE SENSE (10) command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors.
+ * Adds the ability to set the command abort timeout
+ * and the ability to report the residual count. If timeout_secs is zero
+ * or less the default command abort timeout (60 seconds) is used.
+ * If residp is non-NULL then the residual value is written where residp
+ * points. A residual value of 0 implies mx_resp_len bytes have be written
+ * where resp points. If the residual value equals mx_resp_len then no
+ * bytes have been written. */
+int
+sg_ll_mode_sense10_v2(int sg_fd, bool llbaa, bool dbd, int pc, int pg_code,
+                      int sub_pg_code, void * resp, int mx_resp_len,
+                      int timeout_secs, int * residp, bool noisy, int verbose)
+{
+    int res, ret, sense_cat, resid;
+    static const char * const cdb_s = "mode sense(10)";
+    struct sg_pt_base * ptvp;
+    uint8_t modes_cdb[MODE_SENSE10_CMDLEN] =
+        {MODE_SENSE10_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    modes_cdb[1] = (uint8_t)((dbd ? 0x8 : 0) | (llbaa ? 0x10 : 0));
+    modes_cdb[2] = (uint8_t)(((pc << 6) & 0xc0) | (pg_code & 0x3f));
+    modes_cdb[3] = (uint8_t)(sub_pg_code & 0xff);
+    sg_put_unaligned_be16((int16_t)mx_resp_len, modes_cdb + 7);
+    if (mx_resp_len > 0xffff) {
+        pr2ws("mx_resp_len too big\n");
+        goto gen_err;
+    }
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(modes_cdb, MODE_SENSE10_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (timeout_secs <= 0)
+        timeout_secs = DEF_PT_TIMEOUT;
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        goto gen_err;
+    set_scsi_pt_cdb(ptvp, modes_cdb, sizeof(modes_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, timeout_secs, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    resid = get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = resid;
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        if ((verbose > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == verbose) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+
+    if (resid > 0) {
+        if (resid > mx_resp_len) {
+            pr2ws("%s: resid (%d) should never exceed requested len=%d\n",
+                  cdb_s, resid, mx_resp_len);
+            return ret ? ret : SG_LIB_CAT_MALFORMED;
+        }
+        /* zero unfilled section of response buffer */
+        memset((uint8_t *)resp + (mx_resp_len - resid), 0, resid);
+    }
+    return ret;
+gen_err:
+    if (residp)
+        *residp = 0;
+    return -1;
+}
+
+/* Invokes a SCSI MODE SELECT (6) command.  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_mode_select6_v2(int sg_fd, bool pf, bool rtd, bool sp, void * paramp,
+                      int param_len, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "mode select(6)";
+    int res, ret, sense_cat;
+    uint8_t modes_cdb[MODE_SELECT6_CMDLEN] =
+        {MODE_SELECT6_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    modes_cdb[1] = (uint8_t)((pf ? 0x10 : 0x0) | (sp ? 0x1 : 0x0));
+    if (rtd)
+        modes_cdb[1] |= 0x2;
+    modes_cdb[4] = (uint8_t)(param_len & 0xff);
+    if (param_len > 0xff) {
+        pr2ws("%s: param_len too big\n", cdb_s);
+        return -1;
+    }
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(modes_cdb, MODE_SELECT6_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (verbose > 1) {
+        pr2ws("    %s parameter list\n", cdb_s);
+        hex2stderr((const uint8_t *)paramp, param_len, -1);
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, modes_cdb, sizeof(modes_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+int
+sg_ll_mode_select6(int sg_fd, bool pf, bool sp, void * paramp, int param_len,
+                   bool noisy, int verbose)
+{
+    return sg_ll_mode_select6_v2(sg_fd, pf, false, sp, paramp, param_len,
+                                 noisy, verbose);
+}
+
+/* Invokes a SCSI MODE SELECT (10) command.  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors,
+ * v2 adds rtd (revert to defaults) bit (spc5r11).  */
+int
+sg_ll_mode_select10_v2(int sg_fd, bool pf, bool rtd, bool sp, void * paramp,
+                       int param_len, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "mode select(10)";
+    int res, ret, sense_cat;
+    uint8_t modes_cdb[MODE_SELECT10_CMDLEN] =
+        {MODE_SELECT10_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    modes_cdb[1] = (uint8_t)((pf ? 0x10 : 0x0) | (sp ? 0x1 : 0x0));
+    if (rtd)
+        modes_cdb[1] |= 0x2;
+    sg_put_unaligned_be16((int16_t)param_len, modes_cdb + 7);
+    if (param_len > 0xffff) {
+        pr2ws("%s: param_len too big\n", cdb_s);
+        return -1;
+    }
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(modes_cdb, MODE_SELECT10_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (verbose > 1) {
+        pr2ws("    %s parameter list\n", cdb_s);
+        hex2stderr((const uint8_t *)paramp, param_len, -1);
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, modes_cdb, sizeof(modes_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+int
+sg_ll_mode_select10(int sg_fd, bool pf, bool sp, void * paramp,
+                       int param_len, bool noisy, int verbose)
+{
+    return sg_ll_mode_select10_v2(sg_fd, pf, false, sp, paramp, param_len,
+                                  noisy, verbose);
+}
+
+/* MODE SENSE commands yield a response that has header then zero or more
+ * block descriptors followed by mode pages. In most cases users are
+ * interested in the first mode page. This function returns the (byte)
+ * offset of the start of the first mode page. Set mode_sense_6 to true for
+ * MODE SENSE (6) and false for MODE SENSE (10). Returns >= 0 is successful
+ * or -1 if failure. If there is a failure a message is written to err_buff
+ * if it is non-NULL and err_buff_len > 0. */
+int
+sg_mode_page_offset(const uint8_t * resp, int resp_len,
+                    bool mode_sense_6, char * err_buff, int err_buff_len)
+{
+    int bd_len, calc_len, offset;
+    bool err_buff_ok = ((err_buff_len > 0) && err_buff);
+
+    if ((NULL == resp) || (resp_len < 4))
+            goto too_short;
+    if (mode_sense_6) {
+        calc_len = resp[0] + 1;
+        bd_len = resp[3];
+        offset = bd_len + MODE6_RESP_HDR_LEN;
+    } else {    /* Mode sense(10) */
+        if (resp_len < 8)
+            goto too_short;
+        calc_len = sg_get_unaligned_be16(resp) + 2;
+        bd_len = sg_get_unaligned_be16(resp + 6);
+        /* LongLBA doesn't change this calculation */
+        offset = bd_len + MODE10_RESP_HDR_LEN;
+    }
+    if ((offset + 2) > calc_len) {
+        if (err_buff_ok)
+            snprintf(err_buff, err_buff_len, "calculated response "
+                     "length too small, offset=%d calc_len=%d bd_len=%d\n",
+                     offset, calc_len, bd_len);
+        offset = -1;
+    }
+    return offset;
+too_short:
+    if (err_buff_ok)
+        snprintf(err_buff, err_buff_len, "given MS(%d) response length (%d) "
+                 "too short\n", (mode_sense_6 ? 6 : 10), resp_len);
+    return -1;
+}
+
+/* MODE SENSE commands yield a response that has header then zero or more
+ * block descriptors followed by mode pages. This functions returns the
+ * length (in bytes) of those three components. Note that the return value
+ * can exceed resp_len in which case the MODE SENSE command should be
+ * re-issued with a larger response buffer. If bd_lenp is non-NULL and if
+ * successful the block descriptor length (in bytes) is written to *bd_lenp.
+ * Set mode_sense_6 to true for MODE SENSE (6) and false for MODE SENSE (10)
+ * responses. Returns -1 if there is an error (e.g. response too short). */
+int
+sg_msense_calc_length(const uint8_t * resp, int resp_len,
+                      bool mode_sense_6, int * bd_lenp)
+{
+    int calc_len;
+
+    if (NULL == resp)
+        goto an_err;
+    if (mode_sense_6) {
+        if (resp_len < 4)
+            goto an_err;
+        calc_len = resp[0] + 1;
+    } else {
+        if (resp_len < 8)
+            goto an_err;
+        calc_len = sg_get_unaligned_be16(resp + 0) + 2;
+    }
+    if (bd_lenp)
+        *bd_lenp = mode_sense_6 ? resp[3] : sg_get_unaligned_be16(resp + 6);
+    return calc_len;
+an_err:
+    if (bd_lenp)
+        *bd_lenp = 0;
+    return -1;
+}
+
+/* Fetches current, changeable, default and/or saveable modes pages as
+ * indicated by pcontrol_arr for given pg_code and sub_pg_code. If
+ * mode6==false then use MODE SENSE (10) else use MODE SENSE (6). If
+ * flexible set and mode data length seems wrong then try and
+ * fix (compensating hack for bad device or driver). pcontrol_arr
+ * should have 4 elements for output of current, changeable, default
+ * and saved values respectively. Each element should be NULL or
+ * at least mx_mpage_len bytes long.
+ * Return of 0 -> overall success, various SG_LIB_CAT_* positive values or
+ * -1 -> other errors.
+ * If success_mask pointer is not NULL then first zeros it. Then set bits
+ * 0, 1, 2 and/or 3 if the current, changeable, default and saved values
+ * respectively have been fetched. If error on current page
+ * then stops and returns that error; otherwise continues if an error is
+ * detected but returns the first error encountered.  */
+int
+sg_get_mode_page_controls(int sg_fd, bool mode6, int pg_code, int sub_pg_code,
+                          bool dbd, bool flexible, int mx_mpage_len,
+                          int * success_mask, void * pcontrol_arr[],
+                          int * reported_lenp, int verbose)
+{
+    bool resp_mode6;
+    int k, n, res, offset, calc_len, xfer_len;
+    int resid = 0;
+    const int msense10_hlen = MODE10_RESP_HDR_LEN;
+    uint8_t buff[MODE_RESP_ARB_LEN];
+    char ebuff[EBUFF_SZ];
+    int first_err = 0;
+
+    if (success_mask)
+        *success_mask = 0;
+    if (reported_lenp)
+        *reported_lenp = 0;
+    if (mx_mpage_len < 4)
+        return 0;
+    memset(ebuff, 0, sizeof(ebuff));
+    /* first try to find length of current page response */
+    memset(buff, 0, msense10_hlen);
+    if (mode6)  /* want first 8 bytes just in case */
+        res = sg_ll_mode_sense6(sg_fd, dbd, 0 /* pc */, pg_code,
+                                sub_pg_code, buff, msense10_hlen, true,
+                                verbose);
+    else        /* MODE SENSE(10) obviously */
+        res = sg_ll_mode_sense10_v2(sg_fd, false /* llbaa */, dbd,
+                                    0 /* pc */, pg_code, sub_pg_code, buff,
+                                    msense10_hlen, 0, &resid, true, verbose);
+    if (0 != res)
+        return res;
+    n = buff[0];
+    if (reported_lenp) {
+        int m;
+
+        m = sg_msense_calc_length(buff, msense10_hlen, mode6, NULL) - resid;
+        if (m < 0)      /* Grrr, this should not happen */
+            m = 0;
+        *reported_lenp = m;
+    }
+    resp_mode6 = mode6;
+    if (flexible) {
+        if (mode6 && (n < 3)) {
+            resp_mode6 = false;
+            if (verbose)
+                pr2ws(">>> msense(6) but resp[0]=%d so try msense(10) "
+                      "response processing\n", n);
+        }
+        if ((! mode6) && (n > 5)) {
+            if ((n > 11) && (0 == (n % 2)) && (0 == buff[4]) &&
+                (0 == buff[5]) && (0 == buff[6])) {
+                buff[1] = n;
+                buff[0] = 0;
+                if (verbose)
+                    pr2ws(">>> msense(10) but resp[0]=%d and not msense(6) "
+                          "response so fix length\n", n);
+            } else
+                resp_mode6 = true;
+        }
+    }
+    if (verbose && (resp_mode6 != mode6))
+        pr2ws(">>> msense(%d) but resp[0]=%d so switch response "
+              "processing\n", (mode6 ? 6 : 10), buff[0]);
+    calc_len = sg_msense_calc_length(buff, msense10_hlen, resp_mode6, NULL);
+    if (calc_len > MODE_RESP_ARB_LEN)
+        calc_len = MODE_RESP_ARB_LEN;
+    offset = sg_mode_page_offset(buff, calc_len, resp_mode6, ebuff, EBUFF_SZ);
+    if (offset < 0) {
+        if (('\0' != ebuff[0]) && (verbose > 0))
+            pr2ws("%s: %s\n", __func__, ebuff);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    xfer_len = calc_len - offset;
+    if (xfer_len > mx_mpage_len)
+        xfer_len = mx_mpage_len;
+
+    for (k = 0; k < 4; ++k) {
+        if (NULL == pcontrol_arr[k])
+            continue;
+        memset(pcontrol_arr[k], 0, mx_mpage_len);
+        resid = 0;
+        if (mode6)
+            res = sg_ll_mode_sense6(sg_fd, dbd, k /* pc */,
+                                    pg_code, sub_pg_code, buff,
+                                    calc_len, true, verbose);
+        else
+            res = sg_ll_mode_sense10_v2(sg_fd, false /* llbaa */, dbd,
+                                        k /* pc */, pg_code, sub_pg_code,
+                                        buff, calc_len, 0, &resid, true,
+                                        verbose);
+        if (res || resid) {
+            if (0 == first_err) {
+                if (res)
+                    first_err = res;
+                else {
+                    first_err = -49;    /* unexpected resid != 0 */
+                    if (verbose)
+                        pr2ws("%s: unexpected resid=%d, page=0x%x, "
+                              "pcontrol=%d\n", __func__, resid, pg_code, k);
+                }
+            }
+            if (0 == k)
+                break;  /* if problem on current page, it won't improve */
+            else
+                continue;
+        }
+        if (xfer_len > 0)
+            memcpy(pcontrol_arr[k], buff + offset, xfer_len);
+        if (success_mask)
+            *success_mask |= (1 << k);
+    }
+    return first_err;
+}
+
+/* Invokes a SCSI LOG SENSE command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors. */
+int
+sg_ll_log_sense(int sg_fd, bool ppc, bool sp, int pc, int pg_code,
+                int subpg_code, int paramp, uint8_t * resp,
+                int mx_resp_len, bool noisy, int verbose)
+{
+    return sg_ll_log_sense_v2(sg_fd, ppc, sp, pc, pg_code, subpg_code,
+                              paramp, resp, mx_resp_len, 0, NULL, noisy,
+                              verbose);
+}
+
+/* Invokes a SCSI LOG SENSE command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors.
+ * Adds the ability to set the command abort timeout
+ * and the ability to report the residual count. If timeout_secs is zero
+ * or less the default command abort timeout (60 seconds) is used.
+ * If residp is non-NULL then the residual value is written where residp
+ * points. A residual value of 0 implies mx_resp_len bytes have be written
+ * where resp points. If the residual value equals mx_resp_len then no
+ * bytes have been written. */
+int
+sg_ll_log_sense_v2(int sg_fd, bool ppc, bool sp, int pc, int pg_code,
+                   int subpg_code, int paramp, uint8_t * resp,
+                   int mx_resp_len, int timeout_secs, int * residp,
+                   bool noisy, int verbose)
+{
+    static const char * const cdb_s = "log sense";
+    int res, ret, sense_cat, resid;
+    uint8_t logs_cdb[LOG_SENSE_CMDLEN] =
+        {LOG_SENSE_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (mx_resp_len > 0xffff) {
+        pr2ws("mx_resp_len too big\n");
+        goto gen_err;
+    }
+    logs_cdb[1] = (uint8_t)((ppc ? 2 : 0) | (sp ? 1 : 0));
+    logs_cdb[2] = (uint8_t)(((pc << 6) & 0xc0) | (pg_code & 0x3f));
+    logs_cdb[3] = (uint8_t)(subpg_code & 0xff);
+    sg_put_unaligned_be16((int16_t)paramp, logs_cdb + 5);
+    sg_put_unaligned_be16((int16_t)mx_resp_len, logs_cdb + 7);
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(logs_cdb, LOG_SENSE_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (timeout_secs <= 0)
+        timeout_secs = DEF_PT_TIMEOUT;
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        goto gen_err;
+    set_scsi_pt_cdb(ptvp, logs_cdb, sizeof(logs_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, timeout_secs, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    resid = get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = resid;
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        if ((mx_resp_len > 3) && (ret < 4)) {
+            /* resid indicates LOG SENSE response length bad, so zero it */
+            resp[2] = 0;
+            resp[3] = 0;
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+
+    if (resid > 0) {
+        if (resid > mx_resp_len) {
+            pr2ws("%s: resid (%d) should never exceed requested len=%d\n",
+                  cdb_s, resid, mx_resp_len);
+            return ret ? ret : SG_LIB_CAT_MALFORMED;
+        }
+        /* zero unfilled section of response buffer */
+        memset((uint8_t *)resp + (mx_resp_len - resid), 0, resid);
+    }
+    return ret;
+gen_err:
+    if (residp)
+        *residp = 0;
+    return -1;
+}
+
+/* Invokes a SCSI LOG SELECT command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_log_select(int sg_fd, bool pcr, bool sp, int pc, int pg_code,
+                 int subpg_code, uint8_t * paramp, int param_len,
+                 bool noisy, int verbose)
+{
+    static const char * const cdb_s = "log select";
+    int res, ret, sense_cat;
+    uint8_t logs_cdb[LOG_SELECT_CMDLEN] =
+        {LOG_SELECT_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (param_len > 0xffff) {
+        pr2ws("%s: param_len too big\n", cdb_s);
+        return -1;
+    }
+    logs_cdb[1] = (uint8_t)((pcr ? 2 : 0) | (sp ? 1 : 0));
+    logs_cdb[2] = (uint8_t)(((pc << 6) & 0xc0) | (pg_code & 0x3f));
+    logs_cdb[3] = (uint8_t)(subpg_code & 0xff);
+    sg_put_unaligned_be16((int16_t)param_len, logs_cdb + 7);
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(logs_cdb, LOG_SELECT_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if ((verbose > 1) && (param_len > 0)) {
+        pr2ws("    %s parameter list\n", cdb_s);
+        hex2stderr(paramp, param_len, -1);
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, logs_cdb, sizeof(logs_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI START STOP UNIT command (SBC + MMC).
+ * Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors.
+ * SBC-3 and MMC partially overlap on the power_condition_modifier(sbc) and
+ * format_layer_number(mmc) fields. They also overlap on the noflush(sbc)
+ * and fl(mmc) one bit field. This is the cause of the awkardly named
+ * pc_mod__fl_num and noflush__fl arguments to this function.
+ *  */
+static int
+sg_ll_start_stop_unit_com(struct sg_pt_base * ptvp, int sg_fd, bool immed,
+                          int pc_mod__fl_num, int power_cond, bool noflush__fl,
+                          bool loej, bool start, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "start stop unit";
+    bool ptvp_given = false;
+    bool local_sense = true;
+    bool local_cdb = true;
+    int res, ret, sense_cat;
+    uint8_t ssuBlk[START_STOP_CMDLEN] = {START_STOP_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    if (immed)
+        ssuBlk[1] = 0x1;
+    ssuBlk[3] = pc_mod__fl_num & 0xf;  /* bits 2 and 3 are reserved in MMC */
+    ssuBlk[4] = ((power_cond & 0xf) << 4);
+    if (noflush__fl)
+        ssuBlk[4] |= 0x4;
+    if (loej)
+        ssuBlk[4] |= 0x2;
+    if (start)
+        ssuBlk[4] |= 0x1;
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(ssuBlk, sizeof(ssuBlk), false,
+                                 sizeof(b), b));
+    }
+    if (ptvp) {
+        ptvp_given = true;
+        partial_clear_scsi_pt_obj(ptvp);
+        if (get_scsi_pt_cdb_buf(ptvp))
+            local_cdb = false; /* N.B. Ignores locally built cdb */
+        else
+            set_scsi_pt_cdb(ptvp, ssuBlk, sizeof(ssuBlk));
+        if (get_scsi_pt_sense_buf(ptvp))
+            local_sense = false;
+        else
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    } else {
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, verbose);
+        if (NULL == ptvp)
+            return sg_convert_errno(ENOMEM);
+        set_scsi_pt_cdb(ptvp, ssuBlk, sizeof(ssuBlk));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    }
+    res = do_scsi_pt(ptvp, -1, START_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+            ret = 0;
+    if (ptvp_given) {
+        if (local_sense)    /* stop caller trying to access local sense */
+            set_scsi_pt_sense(ptvp, NULL, 0);
+        if (local_cdb)
+            set_scsi_pt_cdb(ptvp, NULL, 0);
+    } else {
+        if (ptvp)
+            destruct_scsi_pt_obj(ptvp);
+    }
+    return ret;
+}
+
+int
+sg_ll_start_stop_unit(int sg_fd, bool immed, int pc_mod__fl_num,
+                      int power_cond, bool noflush__fl, bool loej, bool start,
+                      bool noisy, int verbose)
+{
+    return sg_ll_start_stop_unit_com(NULL, sg_fd, immed, pc_mod__fl_num,
+                                     power_cond, noflush__fl, loej, start,
+                                     noisy, verbose);
+}
+
+int
+sg_ll_start_stop_unit_pt(struct sg_pt_base * ptvp, bool immed,
+                         int pc_mod__fl_num, int power_cond, bool noflush__fl,
+                         bool loej, bool start, bool noisy, int verbose)
+{
+    return sg_ll_start_stop_unit_com(ptvp, -1, immed, pc_mod__fl_num,
+                                     power_cond, noflush__fl, loej, start,
+                                     noisy, verbose);
+}
+
+/* Invokes a SCSI PREVENT ALLOW MEDIUM REMOVAL command
+ * [was in SPC-3 but displaced from SPC-4 into SBC-3, MMC-5, SSC-3]
+ * prevent==0 allows removal, prevent==1 prevents removal ...
+ * Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_prevent_allow(int sg_fd, int prevent, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "prevent allow medium removal";
+    int res, ret, sense_cat;
+    uint8_t p_cdb[PREVENT_ALLOW_CMDLEN] =
+                {PREVENT_ALLOW_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if ((prevent < 0) || (prevent > 3)) {
+        pr2ws("prevent argument should be 0, 1, 2 or 3\n");
+        return -1;
+    }
+    p_cdb[4] |= (prevent & 0x3);
+    if (verbose) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(p_cdb, PREVENT_ALLOW_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, p_cdb, sizeof(p_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+            ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
diff --git a/lib/sg_cmds_extra.c b/lib/sg_cmds_extra.c
new file mode 100644
index 0000000..f0bfd16
--- /dev/null
+++ b/lib/sg_cmds_extra.c
@@ -0,0 +1,2620 @@
+/*
+ * Copyright (c) 1999-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+
+#define DEF_PT_TIMEOUT 60       /* 60 seconds */
+#define LONG_PT_TIMEOUT 7200    /* 7,200 seconds == 120 minutes */
+
+#define SERVICE_ACTION_IN_16_CMD 0x9e
+#define SERVICE_ACTION_IN_16_CMDLEN 16
+#define SERVICE_ACTION_OUT_16_CMD 0x9f
+#define SERVICE_ACTION_OUT_16_CMDLEN 16
+#define MAINTENANCE_IN_CMD 0xa3
+#define MAINTENANCE_IN_CMDLEN 12
+#define MAINTENANCE_OUT_CMD 0xa4
+#define MAINTENANCE_OUT_CMDLEN 12
+
+#define ATA_PT_12_CMD 0xa1
+#define ATA_PT_12_CMDLEN 12
+#define ATA_PT_16_CMD 0x85
+#define ATA_PT_16_CMDLEN 16
+#define ATA_PT_32_SA 0x1ff0
+#define ATA_PT_32_CMDLEN 32
+#define FORMAT_UNIT_CMD 0x4
+#define FORMAT_UNIT_CMDLEN 6
+#define PERSISTENT_RESERVE_IN_CMD 0x5e
+#define PERSISTENT_RESERVE_IN_CMDLEN 10
+#define PERSISTENT_RESERVE_OUT_CMD 0x5f
+#define PERSISTENT_RESERVE_OUT_CMDLEN 10
+#define READ_BLOCK_LIMITS_CMD 0x5
+#define READ_BLOCK_LIMITS_CMDLEN 6
+#define READ_BUFFER_CMD 0x3c
+#define READ_BUFFER_CMDLEN 10
+#define READ_DEFECT10_CMD     0x37
+#define READ_DEFECT10_CMDLEN    10
+#define REASSIGN_BLKS_CMD     0x7
+#define REASSIGN_BLKS_CMDLEN  6
+#define RECEIVE_DIAGNOSTICS_CMD   0x1c
+#define RECEIVE_DIAGNOSTICS_CMDLEN  6
+#define THIRD_PARTY_COPY_OUT_CMD 0x83   /* was EXTENDED_COPY_CMD */
+#define THIRD_PARTY_COPY_OUT_CMDLEN 16
+#define THIRD_PARTY_COPY_IN_CMD 0x84     /* was RECEIVE_COPY_RESULTS_CMD */
+#define THIRD_PARTY_COPY_IN_CMDLEN 16
+#define SEND_DIAGNOSTIC_CMD   0x1d
+#define SEND_DIAGNOSTIC_CMDLEN  6
+#define SERVICE_ACTION_IN_12_CMD 0xab
+#define SERVICE_ACTION_IN_12_CMDLEN 12
+#define READ_LONG10_CMD 0x3e
+#define READ_LONG10_CMDLEN 10
+#define UNMAP_CMD 0x42
+#define UNMAP_CMDLEN 10
+#define VERIFY10_CMD 0x2f
+#define VERIFY10_CMDLEN 10
+#define VERIFY16_CMD 0x8f
+#define VERIFY16_CMDLEN 16
+#define WRITE_LONG10_CMD 0x3f
+#define WRITE_LONG10_CMDLEN 10
+#define WRITE_BUFFER_CMD 0x3b
+#define WRITE_BUFFER_CMDLEN 10
+#define PRE_FETCH10_CMD 0x34
+#define PRE_FETCH10_CMDLEN 10
+#define PRE_FETCH16_CMD 0x90
+#define PRE_FETCH16_CMDLEN 16
+#define SEEK10_CMD 0x2b
+#define SEEK10_CMDLEN 10
+
+#define GET_LBA_STATUS16_SA 0x12
+#define GET_LBA_STATUS32_SA 0x12
+#define READ_LONG_16_SA 0x11
+#define READ_MEDIA_SERIAL_NUM_SA 0x1
+#define REPORT_IDENTIFYING_INFORMATION_SA 0x5
+#define REPORT_TGT_PRT_GRP_SA 0xa
+#define SET_IDENTIFYING_INFORMATION_SA 0x6
+#define SET_TGT_PRT_GRP_SA 0xa
+#define WRITE_LONG_16_SA 0x11
+#define REPORT_REFERRALS_SA 0x13
+#define EXTENDED_COPY_LID1_SA 0x0
+
+
+static struct sg_pt_base *
+create_pt_obj(const char * cname)
+{
+    struct sg_pt_base * ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp)
+        pr2ws("%s: out of memory\n", cname);
+    return ptvp;
+}
+
+
+/* Invokes a SCSI GET LBA STATUS(16) command (SBC). Returns 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_get_lba_status16(int sg_fd, uint64_t start_llba, uint8_t rt,
+                      void * resp, int alloc_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "Get LBA status(16)";
+    int res, s_cat, ret;
+    uint8_t getLbaStatCmd[SERVICE_ACTION_IN_16_CMDLEN];
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    memset(getLbaStatCmd, 0, sizeof(getLbaStatCmd));
+    getLbaStatCmd[0] = SERVICE_ACTION_IN_16_CMD;
+    getLbaStatCmd[1] = GET_LBA_STATUS16_SA;
+
+    sg_put_unaligned_be64(start_llba, getLbaStatCmd + 2);
+    sg_put_unaligned_be32((uint32_t)alloc_len, getLbaStatCmd + 10);
+    getLbaStatCmd[14] = rt;
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(getLbaStatCmd, SERVICE_ACTION_IN_16_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, getLbaStatCmd, sizeof(getLbaStatCmd));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, alloc_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response\n", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+int
+sg_ll_get_lba_status(int sg_fd, uint64_t start_llba, void * resp,
+                     int alloc_len, bool noisy, int vb)
+{
+    return sg_ll_get_lba_status16(sg_fd, start_llba, /* rt = */ 0x0, resp,
+                                  alloc_len, noisy, vb);
+}
+
+#define GLS32_CMD_LEN 32
+
+int
+sg_ll_get_lba_status32(int sg_fd, uint64_t start_llba, uint32_t scan_len,
+                       uint32_t element_id, uint8_t rt,
+                       void * resp, int alloc_len, bool noisy,
+                       int vb)
+{
+    static const char * const cdb_s = "Get LBA status(32)";
+    int res, s_cat, ret;
+    uint8_t gls32_cmd[GLS32_CMD_LEN];
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    memset(gls32_cmd, 0, sizeof(gls32_cmd));
+    gls32_cmd[0] = SG_VARIABLE_LENGTH_CMD;
+    gls32_cmd[7] = GLS32_CMD_LEN - 8;
+    sg_put_unaligned_be16((uint16_t)GET_LBA_STATUS32_SA, gls32_cmd + 8);
+    gls32_cmd[10] = rt;
+    sg_put_unaligned_be64(start_llba, gls32_cmd + 12);
+    sg_put_unaligned_be32(scan_len, gls32_cmd + 20);
+    sg_put_unaligned_be32(element_id, gls32_cmd + 24);
+    sg_put_unaligned_be32((uint32_t)alloc_len, gls32_cmd + 28);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(gls32_cmd, GLS32_CMD_LEN, false, sizeof(b),
+                                 b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, gls32_cmd, sizeof(gls32_cmd));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, alloc_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response\n", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+int
+sg_ll_report_tgt_prt_grp(int sg_fd, void * resp, int mx_resp_len,
+                         bool noisy, int vb)
+{
+    return sg_ll_report_tgt_prt_grp2(sg_fd, resp, mx_resp_len, false, noisy,
+                                     vb);
+}
+
+/* Invokes a SCSI REPORT TARGET PORT GROUPS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_report_tgt_prt_grp2(int sg_fd, void * resp, int mx_resp_len,
+                          bool extended, bool noisy, int vb)
+{
+    static const char * const cdb_s = "Report target port groups";
+    int res, ret, s_cat;
+    uint8_t rtpg_cdb[MAINTENANCE_IN_CMDLEN] =
+                         {MAINTENANCE_IN_CMD, REPORT_TGT_PRT_GRP_SA,
+                          0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (extended)
+        rtpg_cdb[1] |= 0x20;
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rtpg_cdb + 6);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rtpg_cdb, MAINTENANCE_IN_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, rtpg_cdb, sizeof(rtpg_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI SET TARGET PORT GROUPS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_set_tgt_prt_grp(int sg_fd, void * paramp, int param_len, bool noisy,
+                      int vb)
+{
+    static const char * const cdb_s = "Set target port groups";
+    int res, ret, s_cat;
+    uint8_t stpg_cdb[MAINTENANCE_OUT_CMDLEN] =
+                         {MAINTENANCE_OUT_CMD, SET_TGT_PRT_GRP_SA,
+                          0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be32((uint32_t)param_len, stpg_cdb + 6);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(stpg_cdb, MAINTENANCE_OUT_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 1) && paramp && param_len) {
+            pr2ws("    %s parameter list:\n", cdb_s);
+            hex2stderr((const uint8_t *)paramp, param_len, -1);
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, stpg_cdb, sizeof(stpg_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI REPORT REFERRALS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_report_referrals(int sg_fd, uint64_t start_llba, bool one_seg,
+                       void * resp, int mx_resp_len, bool noisy,
+                       int vb)
+{
+    static const char * const cdb_s = "Report referrals";
+    int res, ret, s_cat;
+    uint8_t repRef_cdb[SERVICE_ACTION_IN_16_CMDLEN] =
+                         {SERVICE_ACTION_IN_16_CMD, REPORT_REFERRALS_SA,
+                          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be64(start_llba, repRef_cdb + 2);
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, repRef_cdb + 10);
+    if (one_seg)
+        repRef_cdb[14] = 0x1;
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(repRef_cdb, SERVICE_ACTION_IN_16_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, repRef_cdb, sizeof(repRef_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI SEND DIAGNOSTIC command. Foreground, extended self tests can
+ * take a long time, if so set long_duration flag in which case the timeout
+ * is set to 7200 seconds; if the value of long_duration is > 7200 then that
+ * value is taken as the timeout value in seconds. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_send_diag_com(struct sg_pt_base * ptvp, int sg_fd, int st_code,
+                    bool pf_bit, bool st_bit, bool devofl_bit,
+                    bool unitofl_bit, int long_duration, void * paramp,
+                    int param_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "Send diagnostic";
+    bool ptvp_given = false;
+    bool local_sense = true;
+    bool local_cdb = true;
+    int res, ret, s_cat, tmout;
+    uint8_t senddiag_cdb[SEND_DIAGNOSTIC_CMDLEN] =
+        {SEND_DIAGNOSTIC_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    senddiag_cdb[1] = (uint8_t)(st_code << 5);
+    if (pf_bit)
+        senddiag_cdb[1] |= 0x10;
+    if (st_bit)
+        senddiag_cdb[1] |= 0x4;
+    if (devofl_bit)
+        senddiag_cdb[1] |= 0x2;
+    if (unitofl_bit)
+        senddiag_cdb[1] |= 0x1;
+    sg_put_unaligned_be16((uint16_t)param_len, senddiag_cdb + 3);
+    if (long_duration > LONG_PT_TIMEOUT)
+        tmout = long_duration;
+    else
+        tmout = long_duration ? LONG_PT_TIMEOUT : DEF_PT_TIMEOUT;
+
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(senddiag_cdb, SEND_DIAGNOSTIC_CMDLEN,
+                                 false, sizeof(b), b));
+        if (vb > 1) {
+            if (paramp && param_len) {
+                pr2ws("    %s parameter list:\n", cdb_s);
+                hex2stderr((const uint8_t *)paramp, param_len, -1);
+            }
+            pr2ws("    %s timeout: %d seconds\n", cdb_s, tmout);
+        }
+    }
+    if (ptvp) {
+        ptvp_given = true;
+        partial_clear_scsi_pt_obj(ptvp);
+        if (get_scsi_pt_cdb_buf(ptvp))
+            local_cdb = false; /* N.B. Ignores locally built cdb */
+        else
+            set_scsi_pt_cdb(ptvp, senddiag_cdb, sizeof(senddiag_cdb));
+        if (get_scsi_pt_sense_buf(ptvp))
+            local_sense = false;
+        else
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    } else {
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, vb);
+        if (NULL == ptvp)
+            return sg_convert_errno(ENOMEM);
+        set_scsi_pt_cdb(ptvp, senddiag_cdb, sizeof(senddiag_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    }
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, -1, tmout, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    if (ptvp_given) {
+        if (local_sense)    /* stop caller trying to access local sense */
+            set_scsi_pt_sense(ptvp, NULL, 0);
+        if (local_cdb)
+            set_scsi_pt_cdb(ptvp, NULL, 0);
+    } else {
+        if (ptvp)
+            destruct_scsi_pt_obj(ptvp);
+    }
+    return ret;
+}
+
+int
+sg_ll_send_diag_pt(struct sg_pt_base * ptvp, int st_code, bool pf_bit,
+                   bool st_bit, bool devofl_bit, bool unitofl_bit,
+                   int long_duration, void * paramp, int param_len,
+                   bool noisy, int vb)
+{
+    return sg_ll_send_diag_com(ptvp, -1, st_code, pf_bit, st_bit, devofl_bit,
+                               unitofl_bit, long_duration, paramp,
+                               param_len, noisy, vb);
+}
+
+int
+sg_ll_send_diag(int sg_fd, int st_code, bool pf_bit, bool st_bit,
+                bool devofl_bit, bool unitofl_bit, int long_duration,
+                void * paramp, int param_len, bool noisy, int vb)
+{
+    return sg_ll_send_diag_com(NULL, sg_fd, st_code, pf_bit, st_bit,
+                               devofl_bit, unitofl_bit, long_duration, paramp,
+                               param_len, noisy, vb);
+}
+
+/* Invokes a SCSI RECEIVE DIAGNOSTIC RESULTS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_receive_diag_com(struct sg_pt_base * ptvp, int sg_fd, bool pcv,
+                       int pg_code, void * resp, int mx_resp_len,
+                       int timeout_secs, int * residp, bool noisy, int vb)
+{
+    bool ptvp_given = false;
+    bool local_sense = true;
+    bool local_cdb = true;
+    int resid = 0;
+    int res, ret, s_cat;
+    static const char * const cdb_s = "Receive diagnostic results";
+    uint8_t rcvdiag_cdb[RECEIVE_DIAGNOSTICS_CMDLEN] =
+        {RECEIVE_DIAGNOSTICS_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    if (pcv)
+        rcvdiag_cdb[1] = 0x1;
+    rcvdiag_cdb[2] = (uint8_t)(pg_code);
+    sg_put_unaligned_be16((uint16_t)mx_resp_len, rcvdiag_cdb + 3);
+
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rcvdiag_cdb, RECEIVE_DIAGNOSTICS_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+    if (timeout_secs <= 0)
+        timeout_secs = DEF_PT_TIMEOUT;
+
+    if (ptvp) {
+        ptvp_given = true;
+        partial_clear_scsi_pt_obj(ptvp);
+        if (get_scsi_pt_cdb_buf(ptvp))
+            local_cdb = false; /* N.B. Ignores locally built cdb */
+        else
+            set_scsi_pt_cdb(ptvp, rcvdiag_cdb, sizeof(rcvdiag_cdb));
+        if (get_scsi_pt_sense_buf(ptvp))
+            local_sense = false;
+        else
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    } else {
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, vb);
+        if (NULL == ptvp)
+            return sg_convert_errno(ENOMEM);
+        set_scsi_pt_cdb(ptvp, rcvdiag_cdb, sizeof(rcvdiag_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    }
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, -1, timeout_secs, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    resid = get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = resid;
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                            -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+
+    if (ptvp_given) {
+        if (local_sense)    /* stop caller trying to access local sense */
+            set_scsi_pt_sense(ptvp, NULL, 0);
+        if (local_cdb)
+            set_scsi_pt_cdb(ptvp, NULL, 0);
+    } else {
+        if (ptvp)
+            destruct_scsi_pt_obj(ptvp);
+    }
+    return ret;
+}
+
+int
+sg_ll_receive_diag_pt(struct sg_pt_base * ptvp, bool pcv, int pg_code,
+                      void * resp, int mx_resp_len, int timeout_secs,
+                      int * residp, bool noisy, int vb)
+{
+    return sg_ll_receive_diag_com(ptvp, -1, pcv, pg_code, resp, mx_resp_len,
+                                  timeout_secs, residp, noisy, vb);
+}
+
+/* Invokes a SCSI RECEIVE DIAGNOSTIC RESULTS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_receive_diag(int sg_fd, bool pcv, int pg_code, void * resp,
+                   int mx_resp_len, bool noisy, int vb)
+{
+    return sg_ll_receive_diag_com(NULL, sg_fd, pcv, pg_code, resp,
+                                  mx_resp_len, 0, NULL, noisy, vb);
+}
+
+int
+sg_ll_receive_diag_v2(int sg_fd, bool pcv, int pg_code, void * resp,
+                      int mx_resp_len, int timeout_secs, int * residp,
+                      bool noisy, int vb)
+{
+    return sg_ll_receive_diag_com(NULL, sg_fd, pcv, pg_code, resp,
+                                  mx_resp_len, timeout_secs, residp, noisy,
+                                  vb);
+}
+
+/* Invokes a SCSI READ DEFECT DATA (10) command (SBC). Return of 0 -> success
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_read_defect10(int sg_fd, bool req_plist, bool req_glist, int dl_format,
+                    void * resp, int mx_resp_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "Read defect(10)";
+    int res, ret, s_cat;
+    uint8_t rdef_cdb[READ_DEFECT10_CMDLEN] =
+        {READ_DEFECT10_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    rdef_cdb[2] = (dl_format & 0x7);
+    if (req_plist)
+        rdef_cdb[2] |= 0x10;
+    if (req_glist)
+        rdef_cdb[2] |= 0x8;
+    sg_put_unaligned_be16((uint16_t)mx_resp_len, rdef_cdb + 7);
+    if (mx_resp_len > 0xffff) {
+        pr2ws("mx_resp_len too big\n");
+        return -1;
+    }
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rdef_cdb, READ_DEFECT10_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, rdef_cdb, sizeof(rdef_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response\n", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI READ MEDIA SERIAL NUMBER command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_read_media_serial_num(int sg_fd, void * resp, int mx_resp_len,
+                            bool noisy, int vb)
+{
+    static const char * const cdb_s = "Read media serial number";
+    int res, ret, s_cat;
+    uint8_t rmsn_cdb[SERVICE_ACTION_IN_12_CMDLEN] =
+                         {SERVICE_ACTION_IN_12_CMD, READ_MEDIA_SERIAL_NUM_SA,
+                          0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rmsn_cdb + 6);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rmsn_cdb, SERVICE_ACTION_IN_12_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, rmsn_cdb, sizeof(rmsn_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI REPORT IDENTIFYING INFORMATION command. This command was
+ * called REPORT DEVICE IDENTIFIER prior to spc4r07. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_report_id_info(int sg_fd, int itype, void * resp, int max_resp_len,
+                     bool noisy, int vb)
+{
+    static const char * const cdb_s = "Report identifying information";
+    int res, ret, s_cat;
+    uint8_t rii_cdb[MAINTENANCE_IN_CMDLEN] = {MAINTENANCE_IN_CMD,
+                        REPORT_IDENTIFYING_INFORMATION_SA,
+                        0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be32((uint32_t)max_resp_len, rii_cdb + 6);
+    rii_cdb[10] |= (itype << 1) & 0xfe;
+
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rii_cdb, MAINTENANCE_IN_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, rii_cdb, sizeof(rii_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, max_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI SET IDENTIFYING INFORMATION command. This command was
+ * called SET DEVICE IDENTIFIER prior to spc4r07. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_set_id_info(int sg_fd, int itype, void * paramp, int param_len,
+                  bool noisy, int vb)
+{
+    static const char * const cdb_s = "Set identifying information";
+    int res, ret, s_cat;
+    uint8_t sii_cdb[MAINTENANCE_OUT_CMDLEN] = {MAINTENANCE_OUT_CMD,
+                         SET_IDENTIFYING_INFORMATION_SA,
+                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be32((uint32_t)param_len, sii_cdb + 6);
+    sii_cdb[10] |= (itype << 1) & 0xfe;
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(sii_cdb, MAINTENANCE_OUT_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 1) && paramp && param_len) {
+            pr2ws("    %s parameter list:\n", cdb_s);
+            hex2stderr((const uint8_t *)paramp, param_len, -1);
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, sii_cdb, sizeof(sii_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a FORMAT UNIT (SBC-3) command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_format_unit(int sg_fd, int fmtpinfo, bool longlist, bool fmtdata,
+                  bool cmplst, int dlist_format, int timeout_secs,
+                  void * paramp, int param_len, bool noisy, int vb)
+{
+    return sg_ll_format_unit_v2(sg_fd, fmtpinfo, longlist, fmtdata, cmplst,
+                                dlist_format, 0, timeout_secs, paramp,
+                                param_len, noisy, vb);
+}
+
+/* Invokes a FORMAT UNIT (SBC-3) command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_format_unit2(int sg_fd, int fmtpinfo, bool longlist, bool fmtdata,
+                   bool cmplst, int dlist_format, int ffmt, int timeout_secs,
+                   void * paramp, int param_len, bool noisy, int vb)
+{
+    return sg_ll_format_unit_v2(sg_fd, fmtpinfo, longlist, fmtdata, cmplst,
+                                dlist_format, ffmt, timeout_secs, paramp,
+                                param_len, noisy, vb);
+}
+
+/* Invokes a FORMAT UNIT (SBC-4) command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors.
+ * FFMT field added in sbc4r10 [20160121] */
+int
+sg_ll_format_unit_v2(int sg_fd, int fmtpinfo, bool longlist, bool fmtdata,
+                     bool cmplst, int dlist_format, int ffmt,
+                     int timeout_secs, void * paramp, int param_len,
+                     bool noisy, int vb)
+{
+    static const char * const cdb_s = "Format unit";
+    int res, ret, s_cat, tmout;
+    uint8_t fu_cdb[FORMAT_UNIT_CMDLEN] =
+                {FORMAT_UNIT_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (fmtpinfo)
+        fu_cdb[1] |= (fmtpinfo << 6);
+    if (longlist)
+        fu_cdb[1] |= 0x20;
+    if (fmtdata)
+        fu_cdb[1] |= 0x10;
+    if (cmplst)
+        fu_cdb[1] |= 0x8;
+    if (dlist_format)
+        fu_cdb[1] |= (dlist_format & 0x7);
+    if (ffmt)
+        fu_cdb[4] |= (ffmt & 0x3);
+    tmout = (timeout_secs > 0) ? timeout_secs : DEF_PT_TIMEOUT;
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(fu_cdb, 6, false, sizeof(b), b));
+        if (vb > 1) {
+            if (param_len > 0) {
+                pr2ws("    %s parameter list:\n", cdb_s);
+                hex2stderr((const uint8_t *)paramp, param_len, -1);
+            }
+            pr2ws("    %s timeout: %d seconds\n", cdb_s, tmout);
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, fu_cdb, sizeof(fu_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, tmout, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI REASSIGN BLOCKS command.  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_reassign_blocks(int sg_fd, bool longlba, bool longlist, void * paramp,
+                      int param_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "Reassign blocks";
+    int res, ret, s_cat;
+    uint8_t reass_cdb[REASSIGN_BLKS_CMDLEN] =
+        {REASSIGN_BLKS_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (longlba)
+        reass_cdb[1] = 0x2;
+    if (longlist)
+        reass_cdb[1] |= 0x1;
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(reass_cdb, REASSIGN_BLKS_CMDLEN, false,
+                                 sizeof(b), b));
+    }
+    if (vb > 1) {
+        pr2ws("    %s parameter list\n", cdb_s);
+        hex2stderr((const uint8_t *)paramp, param_len, -1);
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, reass_cdb, sizeof(reass_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI PERSISTENT RESERVE IN command (SPC). Returns 0
+ * when successful, various SG_LIB_CAT_* positive values or
+ * -1 -> other errors */
+int
+sg_ll_persistent_reserve_in(int sg_fd, int rq_servact, void * resp,
+                            int mx_resp_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "Persistent reservation in";
+    int res, ret, s_cat;
+    uint8_t prin_cdb[PERSISTENT_RESERVE_IN_CMDLEN] =
+                 {PERSISTENT_RESERVE_IN_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (rq_servact > 0)
+        prin_cdb[1] = (uint8_t)(rq_servact & 0x1f);
+    sg_put_unaligned_be16((uint16_t)mx_resp_len, prin_cdb + 7);
+
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(prin_cdb, PERSISTENT_RESERVE_IN_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, prin_cdb, sizeof(prin_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI PERSISTENT RESERVE OUT command (SPC). Returns 0
+ * when successful, various SG_LIB_CAT_* positive values or
+ * -1 -> other errors */
+int
+sg_ll_persistent_reserve_out(int sg_fd, int rq_servact, int rq_scope,
+                             unsigned int rq_type, void * paramp,
+                             int param_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "Persistent reservation out";
+    int res, ret, s_cat;
+    uint8_t prout_cdb[PERSISTENT_RESERVE_OUT_CMDLEN] =
+                 {PERSISTENT_RESERVE_OUT_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (rq_servact > 0)
+        prout_cdb[1] = (uint8_t)(rq_servact & 0x1f);
+    prout_cdb[2] = (((rq_scope & 0xf) << 4) | (rq_type & 0xf));
+    sg_put_unaligned_be16((uint16_t)param_len, prout_cdb + 7);
+
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(prout_cdb, PERSISTENT_RESERVE_OUT_CMDLEN,
+                                 false, sizeof(b), b));
+        if (vb > 1) {
+            pr2ws("    %s parameters:\n", cdb_s);
+            hex2stderr((const uint8_t *)paramp, param_len, 0);
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, prout_cdb, sizeof(prout_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+static bool
+has_blk_ili(uint8_t * sensep, int sb_len)
+{
+    int resp_code;
+
+    if (sb_len < 8)
+        return false;
+    resp_code = (0x7f & sensep[0]);
+    if (resp_code >= 0x72) { /* descriptor format */
+        const uint8_t * cup;
+
+        /* find block command descriptor */
+        if ((cup = sg_scsi_sense_desc_find(sensep, sb_len, 0x5)))
+            return (cup[3] & 0x20);
+    } else /* fixed */
+        return (sensep[2] & 0x20);
+    return false;
+}
+
+/* Invokes a SCSI READ LONG (10) command (SBC). Note that 'xfer_len'
+ * is in bytes. Returns 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_read_long10(int sg_fd, bool pblock, bool correct, unsigned int lba,
+                  void * resp, int xfer_len, int * offsetp, bool noisy,
+                  int vb)
+{
+    static const char * const cdb_s = "read long(10)";
+    int res, s_cat, ret;
+    uint8_t readLong_cdb[READ_LONG10_CMDLEN];
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    memset(readLong_cdb, 0, READ_LONG10_CMDLEN);
+    readLong_cdb[0] = READ_LONG10_CMD;
+    if (pblock)
+        readLong_cdb[1] |= 0x4;
+    if (correct)
+        readLong_cdb[1] |= 0x2;
+
+    sg_put_unaligned_be32((uint32_t)lba, readLong_cdb + 2);
+    sg_put_unaligned_be16((uint16_t)xfer_len, readLong_cdb + 7);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(readLong_cdb, READ_LONG10_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, readLong_cdb, sizeof(readLong_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, xfer_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            {
+                bool valid, ili;
+                int slen;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                ili = has_blk_ili(sense_b, slen);
+                if (valid && ili) {
+                    if (offsetp)
+                        *offsetp = (int)(int64_t)ull;
+                    ret = SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO;
+                } else {
+                    if (vb > 1)
+                        pr2ws("  info field: 0x%" PRIx64 ",  valid: %d, "
+                              "ili: %d\n", ull, valid, ili);
+                    ret = SG_LIB_CAT_ILLEGAL_REQ;
+                }
+            }
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI READ LONG (16) command (SBC). Note that 'xfer_len'
+ * is in bytes. Returns 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_read_long16(int sg_fd, bool pblock, bool correct, uint64_t llba,
+                  void * resp, int xfer_len, int * offsetp, bool noisy,
+                  int vb)
+{
+    static const char * const cdb_s = "read long(16)";
+    int res, s_cat, ret;
+    uint8_t readLong_cdb[SERVICE_ACTION_IN_16_CMDLEN];
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    memset(readLong_cdb, 0, sizeof(readLong_cdb));
+    readLong_cdb[0] = SERVICE_ACTION_IN_16_CMD;
+    readLong_cdb[1] = READ_LONG_16_SA;
+    if (pblock)
+        readLong_cdb[14] |= 0x2;
+    if (correct)
+        readLong_cdb[14] |= 0x1;
+
+    sg_put_unaligned_be64(llba, readLong_cdb + 2);
+    sg_put_unaligned_be16((uint16_t)xfer_len, readLong_cdb + 12);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(readLong_cdb, SERVICE_ACTION_IN_16_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, readLong_cdb, sizeof(readLong_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, xfer_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            {
+                bool valid, ili;
+                int slen;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                ili = has_blk_ili(sense_b, slen);
+                if (valid && ili) {
+                    if (offsetp)
+                        *offsetp = (int)(int64_t)ull;
+                    ret = SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO;
+                } else {
+                    if (vb > 1)
+                        pr2ws("  info field: 0x%" PRIx64 ",  valid: %d, "
+                              "ili: %d\n", ull, (int)valid, (int)ili);
+                    ret = SG_LIB_CAT_ILLEGAL_REQ;
+                }
+            }
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI WRITE LONG (10) command (SBC). Note that 'xfer_len'
+ * is in bytes. Returns 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_write_long10(int sg_fd, bool cor_dis, bool wr_uncor, bool pblock,
+                   unsigned int lba, void * data_out, int xfer_len,
+                   int * offsetp, bool noisy, int vb)
+{
+    static const char * const cdb_s = "write long(10)";
+    int res, s_cat, ret;
+    uint8_t writeLong_cdb[WRITE_LONG10_CMDLEN];
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    memset(writeLong_cdb, 0, WRITE_LONG10_CMDLEN);
+    writeLong_cdb[0] = WRITE_LONG10_CMD;
+    if (cor_dis)
+        writeLong_cdb[1] |= 0x80;
+    if (wr_uncor)
+        writeLong_cdb[1] |= 0x40;
+    if (pblock)
+        writeLong_cdb[1] |= 0x20;
+
+    sg_put_unaligned_be32((uint32_t)lba, writeLong_cdb + 2);
+    sg_put_unaligned_be16((uint16_t)xfer_len, writeLong_cdb + 7);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(writeLong_cdb, (int)sizeof(writeLong_cdb),
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, writeLong_cdb, sizeof(writeLong_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)data_out, xfer_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            {
+                int valid, slen, ili;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                ili = has_blk_ili(sense_b, slen);
+                if (valid && ili) {
+                    if (offsetp)
+                        *offsetp = (int)(int64_t)ull;
+                    ret = SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO;
+                } else {
+                    if (vb > 1)
+                        pr2ws("  info field: 0x%" PRIx64 ",  valid: %d, "
+                              "ili: %d\n", ull, (int)valid, (int)ili);
+                    ret = SG_LIB_CAT_ILLEGAL_REQ;
+                }
+            }
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI WRITE LONG (16) command (SBC). Note that 'xfer_len'
+ * is in bytes. Returns 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_write_long16(int sg_fd, bool cor_dis, bool wr_uncor, bool pblock,
+                   uint64_t llba, void * data_out, int xfer_len,
+                   int * offsetp, bool noisy, int vb)
+{
+    static const char * const cdb_s = "write long(16)";
+    int res, s_cat, ret;
+    uint8_t writeLong_cdb[SERVICE_ACTION_OUT_16_CMDLEN];
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    memset(writeLong_cdb, 0, sizeof(writeLong_cdb));
+    writeLong_cdb[0] = SERVICE_ACTION_OUT_16_CMD;
+    writeLong_cdb[1] = WRITE_LONG_16_SA;
+    if (cor_dis)
+        writeLong_cdb[1] |= 0x80;
+    if (wr_uncor)
+        writeLong_cdb[1] |= 0x40;
+    if (pblock)
+        writeLong_cdb[1] |= 0x20;
+
+    sg_put_unaligned_be64(llba, writeLong_cdb + 2);
+    sg_put_unaligned_be16((uint16_t)xfer_len, writeLong_cdb + 12);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(writeLong_cdb, SERVICE_ACTION_OUT_16_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, writeLong_cdb, sizeof(writeLong_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)data_out, xfer_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            {
+                bool valid, ili;
+                int slen;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                ili = has_blk_ili(sense_b, slen);
+                if (valid && ili) {
+                    if (offsetp)
+                        *offsetp = (int)(int64_t)ull;
+                    ret = SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO;
+                } else {
+                    if (vb > 1)
+                        pr2ws("  info field: 0x%" PRIx64 ",  valid: %d, "
+                              "ili: %d\n", ull, (int)valid, (int)ili);
+                    ret = SG_LIB_CAT_ILLEGAL_REQ;
+                }
+            }
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI VERIFY (10) command (SBC and MMC).
+ * Note that 'veri_len' is in blocks while 'data_out_len' is in bytes.
+ * Returns of 0 -> success, * various SG_LIB_CAT_* positive values or
+ * -1 -> other errors */
+int
+sg_ll_verify10(int sg_fd, int vrprotect, bool dpo, int bytchk,
+               unsigned int lba, int veri_len, void * data_out,
+               int data_out_len, unsigned int * infop, bool noisy,
+               int vb)
+{
+    static const char * const cdb_s = "verify(10)";
+    int res, ret, s_cat, slen;
+    uint8_t v_cdb[VERIFY10_CMDLEN] =
+                {VERIFY10_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    /* N.B. BYTCHK field expanded to 2 bits sbc3r34 */
+    v_cdb[1] = (((vrprotect & 0x7) << 5) | ((bytchk & 0x3) << 1)) ;
+    if (dpo)
+        v_cdb[1] |= 0x10;
+    sg_put_unaligned_be32((uint32_t)lba, v_cdb + 2);
+    sg_put_unaligned_be16((uint16_t)veri_len, v_cdb + 7);
+    if (vb > 1) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(v_cdb, VERIFY10_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 3) && bytchk && data_out && (data_out_len > 0)) {
+            int k = data_out_len > 4104 ? 4104 : data_out_len;
+
+            pr2ws("    data_out buffer%s\n",
+                  (data_out_len > 4104 ? ", first 4104 bytes" : ""));
+            hex2stderr((const uint8_t *)data_out, k, vb < 5);
+        }
+    }
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, v_cdb, sizeof(v_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    if (data_out_len > 0)
+        set_scsi_pt_data_out(ptvp, (uint8_t *)data_out, data_out_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD:
+            {
+                bool valid;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                if (valid) {
+                    if (infop)
+                        *infop = (unsigned int)ull;
+                    ret = SG_LIB_CAT_MEDIUM_HARD_WITH_INFO;
+                } else
+                    ret = SG_LIB_CAT_MEDIUM_HARD;
+            }
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI VERIFY (16) command (SBC and MMC).
+ * Note that 'veri_len' is in blocks while 'data_out_len' is in bytes.
+ * Returns of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_verify16(int sg_fd, int vrprotect, bool dpo, int bytchk, uint64_t llba,
+               int veri_len, int group_num, void * data_out,
+               int data_out_len, uint64_t * infop, bool noisy, int vb)
+{
+    static const char * const cdb_s = "verify(16)";
+    int res, ret, s_cat, slen;
+    uint8_t v_cdb[VERIFY16_CMDLEN] =
+                {VERIFY16_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    /* N.B. BYTCHK field expanded to 2 bits sbc3r34 */
+    v_cdb[1] = (((vrprotect & 0x7) << 5) | ((bytchk & 0x3) << 1)) ;
+    if (dpo)
+        v_cdb[1] |= 0x10;
+    sg_put_unaligned_be64(llba, v_cdb + 2);
+    sg_put_unaligned_be32((uint32_t)veri_len, v_cdb + 10);
+    v_cdb[14] = group_num & GRPNUM_MASK;
+    if (vb > 1) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(v_cdb, VERIFY16_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 3) && bytchk && data_out && (data_out_len > 0)) {
+            int k = data_out_len > 4104 ? 4104 : data_out_len;
+
+            pr2ws("    data_out buffer%s\n",
+                  (data_out_len > 4104 ? ", first 4104 bytes" : ""));
+            hex2stderr((const uint8_t *)data_out, k, vb < 5);
+        }
+    }
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return sg_convert_errno(ENOMEM);
+    set_scsi_pt_cdb(ptvp, v_cdb, sizeof(v_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    if (data_out_len > 0)
+        set_scsi_pt_data_out(ptvp, (uint8_t *)data_out, data_out_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD:
+            {
+                bool valid;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                if (valid) {
+                    if (infop)
+                        *infop = ull;
+                    ret = SG_LIB_CAT_MEDIUM_HARD_WITH_INFO;
+                } else
+                    ret = SG_LIB_CAT_MEDIUM_HARD;
+            }
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a ATA PASS-THROUGH (12, 16 or 32) SCSI command (SAT). This is
+ * selected by the cdb_len argument that can take values of 12, 16 or 32
+ * only (else -1 is returned). The byte at offset 0 (and bytes 0 to 9
+ * inclusive for ATA PT(32)) pointed to be cdbp are ignored and apart from
+ * the control byte, the rest is copied into an internal cdb which is then
+ * sent to the device. The control byte is byte 11 for ATA PT(12), byte 15
+ * for ATA PT(16) and byte 1 for ATA PT(32). If timeout_secs <= 0 then the
+ * timeout is set to 60 seconds. For data in or out transfers set dinp or
+ * doutp, and dlen to the number of bytes to transfer. If dlen is zero then
+ * no data transfer is assumed. If sense buffer obtained then it is written
+ * to sensep, else sensep[0] is set to 0x0. If ATA return descriptor is
+ * obtained then written to ata_return_dp, else ata_return_dp[0] is set to
+ * 0x0. Either sensep or ata_return_dp (or both) may be NULL pointers.
+ * Returns SCSI status value (>= 0) or -1 if other error. Users are
+ * expected to check the sense buffer themselves. If available the data in
+ * resid is written to residp. Note in SAT-2 and later, fixed format sense
+ * data may be placed in *sensep in which case sensep[0]==0x70, prior to
+ * SAT-2 descriptor sense format was required (i.e. sensep[0]==0x72).
+ */
+int
+sg_ll_ata_pt(int sg_fd, const uint8_t * cdbp, int cdb_len,
+             int timeout_secs, void * dinp, void * doutp, int dlen,
+             uint8_t * sensep, int max_sense_len,
+             uint8_t * ata_return_dp, int max_ata_return_len,
+             int * residp, int vb)
+{
+    int k, res, slen, duration;
+    int ret = -1;
+    uint8_t apt_cdb[ATA_PT_32_CMDLEN];
+    uint8_t incoming_apt_cdb[ATA_PT_32_CMDLEN];
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    uint8_t * sp;
+    const uint8_t * bp;
+    struct sg_pt_base * ptvp;
+    const char * cnamep;
+    char b[256];
+
+    memset(apt_cdb, 0, sizeof(apt_cdb));
+    memset(incoming_apt_cdb, 0, sizeof(incoming_apt_cdb));
+    if (NULL == cdbp) {
+        if (vb)
+            pr2ws("NULL cdb pointer\n");
+        return -1;
+    }
+    memcpy(incoming_apt_cdb, cdbp, cdb_len);
+    b[0] = '\0';
+    switch (cdb_len) {
+    case 12:
+        cnamep = "ATA pass-through(12)";
+        apt_cdb[0] = ATA_PT_12_CMD;
+        memcpy(apt_cdb + 1, incoming_apt_cdb + 1,  10);
+        /* control byte at cdb[11] left at zero */
+        break;
+    case 16:
+        cnamep = "ATA pass-through(16)";
+        apt_cdb[0] = ATA_PT_16_CMD;
+        memcpy(apt_cdb + 1, incoming_apt_cdb + 1,  14);
+        /* control byte at cdb[15] left at zero */
+        break;
+    case 32:
+        cnamep = "ATA pass-through(32)";
+        apt_cdb[0] = SG_VARIABLE_LENGTH_CMD;
+        /* control byte at cdb[1] left at zero */
+        apt_cdb[7] = 0x18;    /* length starting at next byte */
+        sg_put_unaligned_be16(ATA_PT_32_SA, apt_cdb + 8);
+        memcpy(apt_cdb + 10, incoming_apt_cdb + 10,  32 - 10);
+        break;
+    default:
+        pr2ws("cdb_len must be 12, 16 or 32\n");
+        return -1;
+    }
+    if (sensep && (max_sense_len >= (int)sizeof(sense_b))) {
+        sp = sensep;
+        slen = max_sense_len;
+    } else {
+        sp = sense_b;
+        slen = sizeof(sense_b);
+    }
+    if (vb) {
+        if (cdb_len < 32) {
+            char d[128];
+
+            pr2ws("    %s cdb: %s\n", cnamep,
+                  sg_get_command_str(apt_cdb, cdb_len, false, sizeof(d), d));
+        } else {
+            pr2ws("    %s cdb:\n", cnamep);
+            hex2stderr(apt_cdb, cdb_len, -1);
+        }
+    }
+    if (NULL == ((ptvp = create_pt_obj(cnamep))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, apt_cdb, cdb_len);
+    set_scsi_pt_sense(ptvp, sp, slen);
+    if (dlen > 0) {
+        if (dinp)
+            set_scsi_pt_data_in(ptvp, (uint8_t *)dinp, dlen);
+        else if (doutp)
+            set_scsi_pt_data_out(ptvp, (uint8_t *)doutp, dlen);
+    }
+    res = do_scsi_pt(ptvp, sg_fd,
+                     ((timeout_secs > 0) ? timeout_secs : DEF_PT_TIMEOUT),
+                     vb);
+    if (SCSI_PT_DO_BAD_PARAMS == res) {
+        if (vb)
+            pr2ws("%s: bad parameters\n", cnamep);
+        goto out;
+    } else if (SCSI_PT_DO_TIMEOUT == res) {
+        if (vb)
+            pr2ws("%s: timeout\n", cnamep);
+        goto out;
+    } else if (res > 2) {
+        if (vb)
+            pr2ws("%s: do_scsi_pt: errno=%d\n", cnamep, -res);
+    }
+
+    if ((vb > 2) && ((duration = get_scsi_pt_duration_ms(ptvp)) >= 0))
+        pr2ws("      duration=%d ms\n", duration);
+
+    switch (get_scsi_pt_result_category(ptvp)) {
+    case SCSI_PT_RESULT_GOOD:
+        if ((sensep) && (max_sense_len > 0))
+            *sensep = 0;
+        if ((ata_return_dp) && (max_ata_return_len > 0))
+            *ata_return_dp = 0;
+        if (residp && (dlen > 0))
+            *residp = get_scsi_pt_resid(ptvp);
+        ret = 0;
+        break;
+    case SCSI_PT_RESULT_STATUS: /* other than GOOD + CHECK CONDITION */
+        if ((sensep) && (max_sense_len > 0))
+            *sensep = 0;
+        if ((ata_return_dp) && (max_ata_return_len > 0))
+            *ata_return_dp = 0;
+        ret = get_scsi_pt_status_response(ptvp);
+        break;
+    case SCSI_PT_RESULT_SENSE:
+        if (sensep && (sp != sensep)) {
+            k = get_scsi_pt_sense_len(ptvp);
+            k = (k > max_sense_len) ? max_sense_len : k;
+            memcpy(sensep, sp, k);
+        }
+        if (ata_return_dp && (max_ata_return_len > 0))  {
+            /* search for ATA return descriptor */
+            bp = sg_scsi_sense_desc_find(sp, slen, 0x9);
+            if (bp) {
+                k = bp[1] + 2;
+                k = (k > max_ata_return_len) ? max_ata_return_len : k;
+                memcpy(ata_return_dp, bp, k);
+            } else
+                ata_return_dp[0] = 0x0;
+        }
+        if (residp && (dlen > 0))
+            *residp = get_scsi_pt_resid(ptvp);
+        ret = get_scsi_pt_status_response(ptvp);
+        break;
+    case SCSI_PT_RESULT_TRANSPORT_ERR:
+        if (vb)
+            pr2ws("%s: transport error: %s\n", cnamep,
+                  get_scsi_pt_transport_err_str(ptvp, sizeof(b), b));
+        break;
+    case SCSI_PT_RESULT_OS_ERR:
+        if (vb)
+            pr2ws("%s: os error: %s\n", cnamep,
+                  get_scsi_pt_os_err_str(ptvp, sizeof(b) , b));
+        break;
+    default:
+        if (vb)
+            pr2ws("%s: unknown pt_result_category=%d\n", cnamep,
+                  get_scsi_pt_result_category(ptvp));
+        break;
+    }
+
+out:
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI READ BUFFER(10) command (SPC). Return of 0 -> success
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_read_buffer(int sg_fd, int mode, int buffer_id, int buffer_offset,
+                  void * resp, int mx_resp_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "read buffer(10)";
+    int res, ret, s_cat;
+    uint8_t rbuf_cdb[READ_BUFFER_CMDLEN] =
+        {READ_BUFFER_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    rbuf_cdb[1] = (uint8_t)(mode & 0x1f);
+    rbuf_cdb[2] = (uint8_t)(buffer_id & 0xff);
+    sg_put_unaligned_be24((uint32_t)buffer_offset, rbuf_cdb + 3);
+    sg_put_unaligned_be24((uint32_t)mx_resp_len, rbuf_cdb + 6);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rbuf_cdb, READ_BUFFER_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, rbuf_cdb, sizeof(rbuf_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI WRITE BUFFER command (SPC). Return of 0 -> success
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_write_buffer(int sg_fd, int mode, int buffer_id, int buffer_offset,
+                   void * paramp, int param_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "write buffer";
+    int res, ret, s_cat;
+    uint8_t wbuf_cdb[WRITE_BUFFER_CMDLEN] =
+        {WRITE_BUFFER_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    wbuf_cdb[1] = (uint8_t)(mode & 0x1f);
+    wbuf_cdb[2] = (uint8_t)(buffer_id & 0xff);
+    sg_put_unaligned_be24((uint32_t)buffer_offset, wbuf_cdb + 3);
+    sg_put_unaligned_be24((uint32_t)param_len, wbuf_cdb + 6);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(wbuf_cdb, WRITE_BUFFER_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 1) && paramp && param_len) {
+            pr2ws("    %s parameter list", cdb_s);
+            if (2 == vb) {
+                pr2ws("%s:\n", (param_len > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)paramp,
+                           (param_len > 256 ? 256 : param_len), -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)paramp, param_len, 0);
+            }
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, wbuf_cdb, sizeof(wbuf_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI WRITE BUFFER command (SPC). Return of 0 ->
+ * success, SG_LIB_CAT_INVALID_OP -> invalid opcode,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure. Adds mode specific field (spc4r32) and timeout
+ *  to command abort to override default of 60 seconds. If timeout_secs is
+ *  0 or less then the default timeout is used instead. */
+int
+sg_ll_write_buffer_v2(int sg_fd, int mode, int m_specific, int buffer_id,
+                      uint32_t buffer_offset, void * paramp,
+                      uint32_t param_len, int timeout_secs, bool noisy,
+                      int vb)
+{
+    int res, ret, s_cat;
+    uint8_t wbuf_cdb[WRITE_BUFFER_CMDLEN] =
+        {WRITE_BUFFER_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (buffer_offset > 0xffffff) {
+        pr2ws("%s: buffer_offset value too large for 24 bits\n", __func__);
+        return -1;
+    }
+    if (param_len > 0xffffff) {
+        pr2ws("%s: param_len value too large for 24 bits\n", __func__);
+        return -1;
+    }
+    wbuf_cdb[1] = (uint8_t)(mode & 0x1f);
+    wbuf_cdb[1] |= (uint8_t)((m_specific & 0x7) << 5);
+    wbuf_cdb[2] = (uint8_t)(buffer_id & 0xff);
+    sg_put_unaligned_be24(buffer_offset, wbuf_cdb + 3);
+    sg_put_unaligned_be24(param_len, wbuf_cdb + 6);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    Write buffer cdb: %s\n",
+              sg_get_command_str(wbuf_cdb, WRITE_BUFFER_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 1) && paramp && param_len) {
+            pr2ws("    Write buffer parameter list%s:\n",
+                  ((param_len > 256) ? " (first 256 bytes)" : ""));
+            hex2stderr((const uint8_t *)paramp,
+                       ((param_len > 256) ? 256 : param_len), -1);
+        }
+    }
+    if (timeout_secs <= 0)
+        timeout_secs = DEF_PT_TIMEOUT;
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2ws("%s: out of memory\n", __func__);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, wbuf_cdb, sizeof(wbuf_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, timeout_secs, vb);
+    ret = sg_cmds_process_resp(ptvp, "Write buffer", res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI UNMAP command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_unmap(int sg_fd, int group_num, int timeout_secs, void * paramp,
+            int param_len, bool noisy, int vb)
+{
+    return sg_ll_unmap_v2(sg_fd, false, group_num, timeout_secs, paramp,
+                          param_len, noisy, vb);
+}
+
+/* Invokes a SCSI UNMAP (SBC-3) command. Version 2 adds anchor field
+ * (sbc3r22). Otherwise same as sg_ll_unmap() . */
+int
+sg_ll_unmap_v2(int sg_fd, bool anchor, int group_num, int timeout_secs,
+               void * paramp, int param_len, bool noisy, int vb)
+{
+    static const char * const cdb_s = "unmap";
+    int res, ret, s_cat, tmout;
+    uint8_t u_cdb[UNMAP_CMDLEN] =
+                         {UNMAP_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (anchor)
+        u_cdb[1] |= 0x1;
+    tmout = (timeout_secs > 0) ? timeout_secs : DEF_PT_TIMEOUT;
+    u_cdb[6] = group_num & GRPNUM_MASK;
+    sg_put_unaligned_be16((uint16_t)param_len, u_cdb + 7);
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(u_cdb, UNMAP_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 1) && paramp && param_len) {
+            pr2ws("    %s parameter list:\n", cdb_s);
+            hex2stderr((const uint8_t *)paramp, param_len, -1);
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, u_cdb, sizeof(u_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, tmout, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI READ BLOCK LIMITS command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_read_block_limits_v2(int sg_fd, bool mloi, void * resp,
+                           int mx_resp_len, int * residp, bool noisy,
+                           int vb)
+{
+    static const char * const cdb_s = "read block limits";
+    int ret, res, s_cat;
+    int resid = 0;
+    uint8_t rl_cdb[READ_BLOCK_LIMITS_CMDLEN] =
+      {READ_BLOCK_LIMITS_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (mloi)
+        rl_cdb[1] |= 0x1;       /* introduced in ssc4r02 */
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(rl_cdb, READ_BLOCK_LIMITS_CMDLEN,
+                                 false, sizeof(b), b));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, rl_cdb, sizeof(rl_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    resid = get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = resid;
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else {
+        if ((vb > 2) && (ret > 0)) {
+            pr2ws("    %s: response", cdb_s);
+            if (3 == vb) {
+                pr2ws("%s:\n", (ret > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, ret, 0);
+            }
+            if (vb)
+                pr2ws("resid=%d\n", resid);
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+int
+sg_ll_read_block_limits(int sg_fd, void * resp, int mx_resp_len,
+                        bool noisy, int vb)
+{
+    return sg_ll_read_block_limits_v2(sg_fd, false, resp, mx_resp_len, NULL,
+                                      noisy, vb);
+}
+
+/* Invokes a SCSI RECEIVE COPY RESULTS command. Actually cover all current
+ * uses of opcode 0x84 (Third-party copy IN). Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_receive_copy_results(int sg_fd, int sa, int list_id, void * resp,
+                           int mx_resp_len, bool noisy, int vb)
+{
+    int res, ret, s_cat;
+    uint8_t rcvcopyres_cdb[THIRD_PARTY_COPY_IN_CMDLEN] =
+      {THIRD_PARTY_COPY_IN_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+    char b[64];
+
+    sg_get_opcode_sa_name(THIRD_PARTY_COPY_IN_CMD, sa, 0, (int)sizeof(b), b);
+    rcvcopyres_cdb[1] = (uint8_t)(sa & 0x1f);
+    if (sa <= 4)        /* LID1 variants */
+        rcvcopyres_cdb[2] = (uint8_t)(list_id);
+    else if ((sa >= 5) && (sa <= 7))    /* LID4 variants */
+        sg_put_unaligned_be32((uint32_t)list_id, rcvcopyres_cdb + 2);
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rcvcopyres_cdb + 10);
+
+    if (vb) {
+        char d[128];
+
+        pr2ws("    %s cdb: %s\n", b,
+              sg_get_command_str(rcvcopyres_cdb, THIRD_PARTY_COPY_IN_CMDLEN,
+                                 false, sizeof(d), d));
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(b))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, rcvcopyres_cdb, sizeof(rcvcopyres_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, b, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+/* SPC-4 rev 35 and later calls this opcode (0x83) "Third-party copy OUT"
+ * The original EXTENDED COPY command (now called EXTENDED COPY (LID1))
+ * is the only one supported by sg_ll_extended_copy(). See function
+ * sg_ll_3party_copy_out() for the other service actions ( > 0 ). */
+
+/* Invokes a SCSI EXTENDED COPY (LID1) command. Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_extended_copy(int sg_fd, void * paramp, int param_len, bool noisy,
+                    int vb)
+{
+    int res, ret, s_cat;
+    uint8_t xcopy_cdb[THIRD_PARTY_COPY_OUT_CMDLEN] =
+      {THIRD_PARTY_COPY_OUT_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+    const char * cdb_s = "Extended copy (LID1)";
+
+    xcopy_cdb[1] = (uint8_t)(EXTENDED_COPY_LID1_SA & 0x1f);
+    sg_put_unaligned_be32((uint32_t)param_len, xcopy_cdb + 10);
+
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(xcopy_cdb, THIRD_PARTY_COPY_OUT_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 1) && paramp && param_len) {
+            pr2ws("    %s parameter list:\n", cdb_s);
+            hex2stderr((const uint8_t *)paramp, param_len, -1);
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, xcopy_cdb, sizeof(xcopy_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Handles various service actions associated with opcode 0x83 which is
+ * called THIRD PARTY COPY OUT. These include the EXTENDED COPY(LID1 and
+ * LID4), POPULATE TOKEN and WRITE USING TOKEN commands.
+ * Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+int
+sg_ll_3party_copy_out(int sg_fd, int sa, unsigned int list_id, int group_num,
+                      int timeout_secs, void * paramp, int param_len,
+                      bool noisy, int vb)
+{
+    int res, ret, s_cat, tmout;
+    uint8_t xcopy_cdb[THIRD_PARTY_COPY_OUT_CMDLEN] =
+      {THIRD_PARTY_COPY_OUT_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+    char cname[80];
+
+    sg_get_opcode_sa_name(THIRD_PARTY_COPY_OUT_CMD, sa, 0, sizeof(cname),
+                          cname);
+    xcopy_cdb[1] = (uint8_t)(sa & 0x1f);
+    switch (sa) {
+    case 0x0:   /* XCOPY(LID1) */
+    case 0x1:   /* XCOPY(LID4) */
+        sg_put_unaligned_be32((uint32_t)param_len, xcopy_cdb + 10);
+        break;
+    case 0x10:  /* POPULATE TOKEN (SBC-3) */
+    case 0x11:  /* WRITE USING TOKEN (SBC-3) */
+        sg_put_unaligned_be32((uint32_t)list_id, xcopy_cdb + 6);
+        sg_put_unaligned_be32((uint32_t)param_len, xcopy_cdb + 10);
+        xcopy_cdb[14] = (uint8_t)(group_num & GRPNUM_MASK);
+        break;
+    case 0x1c:  /* COPY OPERATION ABORT */
+        sg_put_unaligned_be32((uint32_t)list_id, xcopy_cdb + 2);
+        break;
+    default:
+        pr2ws("%s: unknown service action 0x%x\n", __func__, sa);
+        return -1;
+    }
+    tmout = (timeout_secs > 0) ? timeout_secs : DEF_PT_TIMEOUT;
+
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cname,
+              sg_get_command_str(xcopy_cdb, THIRD_PARTY_COPY_OUT_CMDLEN,
+                                 false, sizeof(b), b));
+        if ((vb > 1) && paramp && param_len) {
+            pr2ws("    %s parameter list:\n", cname);
+            hex2stderr((const uint8_t *)paramp, param_len, -1);
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cname))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, xcopy_cdb, sizeof(xcopy_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, tmout, vb);
+    ret = sg_cmds_process_resp(ptvp, cname, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI PRE-FETCH(10), PRE-FETCH(16) or SEEK(10) command (SBC).
+ * Returns 0 -> success, 25 (SG_LIB_CAT_CONDITION_MET), various SG_LIB_CAT_*
+ * positive values or -1 -> other errors. Note that CONDITION MET status
+ * is returned when immed=true and num_blocks can fit in device's cache,
+ * somewaht strangely, GOOD status (return 0) is returned if num_blocks
+ * cannot fit in device's cache. If do_seek10==true then does a SEEK(10)
+ * command with given lba, if that LBA is < 2**32 . Unclear what SEEK(10)
+ * does, assume it is like PRE-FETCH. If timeout_secs is 0 (or less) then
+ * use DEF_PT_TIMEOUT (60 seconds) as command timeout. */
+int
+sg_ll_pre_fetch_x(int sg_fd, bool do_seek10, bool cdb16, bool immed,
+                  uint64_t lba, uint32_t num_blocks, int group_num,
+                  int timeout_secs, bool noisy, int vb)
+{
+    static const char * const cdb10_name_s = "Pre-fetch(10)";
+    static const char * const cdb16_name_s = "Pre-fetch(16)";
+    static const char * const cdb_seek_name_s = "Seek(10)";
+    int res, s_cat, ret, cdb_len, tmout;
+    const char *cdb_s;
+    uint8_t preFetchCdb[PRE_FETCH16_CMDLEN]; /* all use longest cdb */
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    memset(preFetchCdb, 0, sizeof(preFetchCdb));
+    if (do_seek10) {
+        if (lba > UINT32_MAX) {
+            if (vb)
+                pr2ws("%s: LBA exceeds 2**32 in %s\n", __func__,
+                      cdb_seek_name_s);
+            return -1;
+        }
+        preFetchCdb[0] = SEEK10_CMD;
+        cdb_len = SEEK10_CMDLEN;
+        cdb_s = cdb_seek_name_s;
+        sg_put_unaligned_be32((uint32_t)lba, preFetchCdb + 2);
+    } else {
+        if ((! cdb16) &&
+            ((lba > UINT32_MAX) || (num_blocks > UINT16_MAX))) {
+            cdb16 = true;
+            if (noisy || vb)
+                pr2ws("%s: do %s due to %s size\n", __func__, cdb16_name_s,
+                      (lba > UINT32_MAX) ? "LBA" : "NUM_BLOCKS");
+        }
+        if (cdb16) {
+            preFetchCdb[0] = PRE_FETCH16_CMD;
+            cdb_len = PRE_FETCH16_CMDLEN;
+            cdb_s = cdb16_name_s;
+            if (immed)
+                preFetchCdb[1] = 0x2;
+            sg_put_unaligned_be64(lba, preFetchCdb + 2);
+            sg_put_unaligned_be32(num_blocks, preFetchCdb + 10);
+            preFetchCdb[14] = GRPNUM_MASK & group_num;
+        } else {
+            preFetchCdb[0] = PRE_FETCH10_CMD;
+            cdb_len = PRE_FETCH10_CMDLEN;
+            cdb_s = cdb10_name_s;
+            if (immed)
+                preFetchCdb[1] = 0x2;
+            sg_put_unaligned_be32((uint32_t)lba, preFetchCdb + 2);
+            preFetchCdb[6] = GRPNUM_MASK & group_num;
+            sg_put_unaligned_be16((uint16_t)num_blocks, preFetchCdb + 7);
+        }
+    }
+    tmout = (timeout_secs > 0) ? timeout_secs : DEF_PT_TIMEOUT;
+    if (vb) {
+        char b[128];
+
+        pr2ws("    %s cdb: %s\n", cdb_s,
+              sg_get_command_str(preFetchCdb, cdb_len, false, sizeof(b), b));
+    }
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, preFetchCdb, cdb_len);
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, tmout, vb);
+    if (0 == res) {
+        int sstat = get_scsi_pt_status_response(ptvp);
+
+        if (SG_LIB_CAT_CONDITION_MET == sstat) {
+            ret = SG_LIB_CAT_CONDITION_MET;
+            if (vb > 2)
+                pr2ws("%s: returns SG_LIB_CAT_CONDITION_MET\n", __func__);
+            goto fini;
+        }
+    }
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, vb, &s_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (s_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = s_cat;
+            break;
+        }
+    } else
+        ret = 0;
+fini:
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
diff --git a/lib/sg_cmds_mmc.c b/lib/sg_cmds_mmc.c
new file mode 100644
index 0000000..e362955
--- /dev/null
+++ b/lib/sg_cmds_mmc.c
@@ -0,0 +1,370 @@
+/*
+ * Copyright (c) 2008-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <unistd.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_mmc.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+
+#define DEF_PT_TIMEOUT 60       /* 60 seconds */
+
+#define GET_CONFIG_CMD 0x46
+#define GET_CONFIG_CMD_LEN 10
+#define GET_PERFORMANCE_CMD 0xac
+#define GET_PERFORMANCE_CMD_LEN 12
+#define SET_CD_SPEED_CMD 0xbb
+#define SET_CD_SPEED_CMDLEN 12
+#define SET_STREAMING_CMD 0xb6
+#define SET_STREAMING_CMDLEN 12
+
+
+static struct sg_pt_base *
+create_pt_obj(const char * cname)
+{
+    struct sg_pt_base * ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp)
+        pr2ws("%s: out of memory\n", cname);
+    return ptvp;
+}
+
+/* Invokes a SCSI SET CD SPEED command (MMC).
+ * Return of 0 -> success, SG_LIB_CAT_INVALID_OP -> command not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_UNIT_ATTENTION,
+ * SG_LIB_CAT_NOT_READY -> device not ready, SG_LIB_CAT_ABORTED_COMMAND,
+ * -1 -> other failure */
+int
+sg_ll_set_cd_speed(int sg_fd, int rot_control, int drv_read_speed,
+                   int drv_write_speed, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "set cd speed";
+    int res, ret, sense_cat;
+    uint8_t scsCmdBlk[SET_CD_SPEED_CMDLEN] = {SET_CD_SPEED_CMD, 0,
+                                         0, 0, 0, 0, 0, 0, 0, 0, 0 ,0};
+    uint8_t sense_b[SENSE_BUFF_LEN];
+    struct sg_pt_base * ptvp;
+
+    scsCmdBlk[1] |= (rot_control & 0x3);
+    sg_put_unaligned_be16((uint16_t)drv_read_speed, scsCmdBlk + 2);
+    sg_put_unaligned_be16((uint16_t)drv_write_speed, scsCmdBlk + 4);
+
+    if (verbose) {
+        int k;
+
+        pr2ws("    %s cdb: ", cdb_s);
+        for (k = 0; k < SET_CD_SPEED_CMDLEN; ++k)
+            pr2ws("%02x ", scsCmdBlk[k]);
+        pr2ws("\n");
+    }
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, scsCmdBlk, sizeof(scsCmdBlk));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_NOT_READY:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+        case SG_LIB_CAT_INVALID_OP:
+        case SG_LIB_CAT_ILLEGAL_REQ:
+        case SG_LIB_CAT_ABORTED_COMMAND:
+            ret = sense_cat;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = -1;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI GET CONFIGURATION command (MMC-3,4,5).
+ * Returns 0 when successful, SG_LIB_CAT_INVALID_OP if command not
+ * supported, SG_LIB_CAT_ILLEGAL_REQ if field in cdb not supported,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND, else -1 */
+int
+sg_ll_get_config(int sg_fd, int rt, int starting, void * resp,
+                 int mx_resp_len, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "get configuration";
+    int res, ret, sense_cat;
+    uint8_t gcCmdBlk[GET_CONFIG_CMD_LEN] = {GET_CONFIG_CMD, 0, 0, 0,
+                                                  0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN];
+    struct sg_pt_base * ptvp;
+
+    if ((rt < 0) || (rt > 3)) {
+        pr2ws("Bad rt value: %d\n", rt);
+        return -1;
+    }
+    gcCmdBlk[1] = (rt & 0x3);
+    if ((starting < 0) || (starting > 0xffff)) {
+        pr2ws("Bad starting field number: 0x%x\n", starting);
+        return -1;
+    }
+    sg_put_unaligned_be16((uint16_t)starting, gcCmdBlk + 2);
+    if ((mx_resp_len < 0) || (mx_resp_len > 0xffff)) {
+        pr2ws("Bad mx_resp_len: 0x%x\n", starting);
+        return -1;
+    }
+    sg_put_unaligned_be16((uint16_t)mx_resp_len, gcCmdBlk + 7);
+
+    if (verbose) {
+        int k;
+
+        pr2ws("    %s cdb: ", cdb_s);
+        for (k = 0; k < GET_CONFIG_CMD_LEN; ++k)
+            pr2ws("%02x ", gcCmdBlk[k]);
+        pr2ws("\n");
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, gcCmdBlk, sizeof(gcCmdBlk));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_INVALID_OP:
+        case SG_LIB_CAT_ILLEGAL_REQ:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+        case SG_LIB_CAT_ABORTED_COMMAND:
+            ret = sense_cat;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = -1;
+            break;
+        }
+    } else {
+        if ((verbose > 2) && (ret > 3)) {
+            uint8_t * bp;
+            int len;
+
+            bp = (uint8_t *)resp;
+            len = sg_get_unaligned_be32(bp + 0);
+            if (len < 0)
+                len = 0;
+            len = (ret < len) ? ret : len;
+            pr2ws("    %s: response:\n", cdb_s);
+            if (3 == verbose) {
+                pr2ws("%s:\n", (len > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (len > 256 ? 256 : len),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, len, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI GET PERFORMANCE command (MMC-3...6).
+ * Returns 0 when successful, SG_LIB_CAT_INVALID_OP if command not
+ * supported, SG_LIB_CAT_ILLEGAL_REQ if field in cdb not supported,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_ABORTED_COMMAND, else -1 */
+int
+sg_ll_get_performance(int sg_fd, int data_type, unsigned int starting_lba,
+                      int max_num_desc, int ttype, void * resp,
+                      int mx_resp_len, bool noisy, int verbose)
+{
+    static const char * const cdb_s = "get performance";
+    int res, ret, sense_cat;
+    uint8_t gpCmdBlk[GET_PERFORMANCE_CMD_LEN] = {GET_PERFORMANCE_CMD, 0,
+                                        0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN];
+    struct sg_pt_base * ptvp;
+
+    if ((data_type < 0) || (data_type > 0x1f)) {
+        pr2ws("Bad data_type value: %d\n", data_type);
+        return -1;
+    }
+    gpCmdBlk[1] = (data_type & 0x1f);
+    sg_put_unaligned_be32((uint32_t)starting_lba, gpCmdBlk + 2);
+    if ((max_num_desc < 0) || (max_num_desc > 0xffff)) {
+        pr2ws("Bad max_num_desc: 0x%x\n", max_num_desc);
+        return -1;
+    }
+    sg_put_unaligned_be16((uint16_t)max_num_desc, gpCmdBlk + 8);
+    if ((ttype < 0) || (ttype > 0xff)) {
+        pr2ws("Bad type: 0x%x\n", ttype);
+        return -1;
+    }
+    gpCmdBlk[10] = (uint8_t)ttype;
+
+    if (verbose) {
+        int k;
+
+        pr2ws("    %s cdb: ", cdb_s);
+        for (k = 0; k < GET_PERFORMANCE_CMD_LEN; ++k)
+            pr2ws("%02x ", gpCmdBlk[k]);
+        pr2ws("\n");
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, gpCmdBlk, sizeof(gpCmdBlk));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_INVALID_OP:
+        case SG_LIB_CAT_ILLEGAL_REQ:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+        case SG_LIB_CAT_ABORTED_COMMAND:
+            ret = sense_cat;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = -1;
+            break;
+        }
+    } else {
+        if ((verbose > 2) && (ret > 3)) {
+            uint8_t * bp;
+            int len;
+
+            bp = (uint8_t *)resp;
+            len = sg_get_unaligned_be32(bp + 0);
+            if (len < 0)
+                len = 0;
+            len = (ret < len) ? ret : len;
+            pr2ws("    %s: response", cdb_s);
+            if (3 == verbose) {
+                pr2ws("%s:\n", (len > 256 ? ", first 256 bytes" : ""));
+                hex2stderr((const uint8_t *)resp, (len > 256 ? 256 : len),
+                           -1);
+            } else {
+                pr2ws(":\n");
+                hex2stderr((const uint8_t *)resp, len, 0);
+            }
+        }
+        ret = 0;
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI SET STREAMING command (MMC). Return of 0 -> success,
+ * SG_LIB_CAT_INVALID_OP -> Set Streaming not supported,
+ * SG_LIB_CAT_ILLEGAL_REQ -> bad field in cdb, SG_LIB_CAT_ABORTED_COMMAND,
+ * SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_NOT_READY -> device not ready,
+ * -1 -> other failure */
+int
+sg_ll_set_streaming(int sg_fd, int type, void * paramp, int param_len,
+                    bool noisy, int verbose)
+{
+    static const char * const cdb_s = "set streaming";
+    int res, ret, sense_cat;
+    uint8_t ssCmdBlk[SET_STREAMING_CMDLEN] =
+                 {SET_STREAMING_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN];
+    struct sg_pt_base * ptvp;
+
+    ssCmdBlk[8] = type;
+    sg_put_unaligned_be16((uint16_t)param_len, ssCmdBlk + 9);
+    if (verbose) {
+        int k;
+
+        pr2ws("    %s cdb: ", cdb_s);
+        for (k = 0; k < SET_STREAMING_CMDLEN; ++k)
+            pr2ws("%02x ", ssCmdBlk[k]);
+        pr2ws("\n");
+        if ((verbose > 1) && paramp && param_len) {
+            pr2ws("    %s parameter list:\n", cdb_s);
+            hex2stderr((const uint8_t *)paramp, param_len, -1);
+        }
+    }
+
+    if (NULL == ((ptvp = create_pt_obj(cdb_s))))
+        return -1;
+    set_scsi_pt_cdb(ptvp, ssCmdBlk, sizeof(ssCmdBlk));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cdb_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_NOT_READY:
+        case SG_LIB_CAT_INVALID_OP:
+        case SG_LIB_CAT_ILLEGAL_REQ:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+        case SG_LIB_CAT_ABORTED_COMMAND:
+            ret = sense_cat;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = -1;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
diff --git a/lib/sg_io_linux.c b/lib/sg_io_linux.c
new file mode 100644
index 0000000..968f5e7
--- /dev/null
+++ b/lib/sg_io_linux.c
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 1999-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifdef SG_LIB_LINUX
+
+#include "sg_io_linux.h"
+#include "sg_pr2serr.h"
+
+
+/* Version 1.13 20210831 */
+
+
+void
+sg_print_masked_status(int masked_status)
+{
+    int scsi_status = (masked_status << 1) & 0x7e;
+
+    sg_print_scsi_status(scsi_status);
+}
+
+/* host_bytes: DID_* are Linux SCSI result (a 32 bit variable) bits 16:23 */
+
+static const char * linux_host_bytes[] = {
+    "DID_OK", "DID_NO_CONNECT", "DID_BUS_BUSY", "DID_TIME_OUT",
+    "DID_BAD_TARGET", "DID_ABORT", "DID_PARITY", "DID_ERROR",
+    "DID_RESET", "DID_BAD_INTR", "DID_PASSTHROUGH", "DID_SOFT_ERROR",
+    "DID_IMM_RETRY", "DID_REQUEUE", "DID_TRANSPORT_DISRUPTED",
+    "DID_TRANSPORT_FAILFAST", "DID_TARGET_FAILURE", "DID_NEXUS_FAILURE",
+    "DID_ALLOC_FAILURE", "DID_MEDIUM_ERROR", "DID_TRANSPORT_MARGINAL",
+};
+
+void
+sg_print_host_status(int host_status)
+{
+    pr2ws("Host_status=0x%02x ", host_status);
+    if ((host_status < 0) ||
+        (host_status >= (int)SG_ARRAY_SIZE(linux_host_bytes)))
+        pr2ws("is invalid ");
+    else
+        pr2ws("[%s] ", linux_host_bytes[host_status]);
+}
+
+/* DRIVER_* are Linux SCSI result (a 32 bit variable) bits 24:27 .
+ * These where made obsolete around lk 5.12.0 . Only DRIVER_SENSE [0x8] is
+ * defined in scsi/sg.h for backward comaptibility */
+static const char * linux_driver_bytes[] = {
+    "DRIVER_OK", "DRIVER_BUSY", "DRIVER_SOFT", "DRIVER_MEDIA",
+    "DRIVER_ERROR", "DRIVER_INVALID", "DRIVER_TIMEOUT", "DRIVER_HARD",
+    "DRIVER_SENSE",
+};
+
+#if 0
+
+/* SUGGEST_* are Linux SCSI result (a 32 bit variable) bits 28:31 */
+
+static const char * linux_driver_suggests[] = {
+    "SUGGEST_OK", "SUGGEST_RETRY", "SUGGEST_ABORT", "SUGGEST_REMAP",
+    "SUGGEST_DIE", "UNKNOWN","UNKNOWN","UNKNOWN",
+    "SUGGEST_SENSE",
+};
+#endif
+
+
+void
+sg_print_driver_status(int driver_status)
+{
+    int driv;
+    const char * driv_cp = "invalid";
+
+    driv = driver_status & SG_LIB_DRIVER_MASK;
+    if (driv < (int)SG_ARRAY_SIZE(linux_driver_bytes))
+        driv_cp = linux_driver_bytes[driv];
+    pr2ws("Driver_status=0x%02x", driver_status);
+    pr2ws(" [%s] ", driv_cp);
+}
+
+/* Returns 1 if no errors found and thus nothing printed; otherwise
+ * prints error/warning (prefix by 'leadin') to stderr (pr2ws) and
+ * returns 0. */
+int
+sg_linux_sense_print(const char * leadin, int scsi_status, int host_status,
+                     int driver_status, const uint8_t * sense_buffer,
+                     int sb_len, bool raw_sinfo)
+{
+    bool done_leadin = false;
+    bool done_sense = false;
+
+    scsi_status &= 0x7e; /*sanity */
+    if ((0 == scsi_status) && (0 == host_status) && (0 == driver_status))
+        return 1;       /* No problems */
+    if (0 != scsi_status) {
+        if (leadin)
+            pr2ws("%s: ", leadin);
+        done_leadin = true;
+        pr2ws("SCSI status: ");
+        sg_print_scsi_status(scsi_status);
+        pr2ws("\n");
+        if (sense_buffer && ((scsi_status == SAM_STAT_CHECK_CONDITION) ||
+                             (scsi_status == SAM_STAT_COMMAND_TERMINATED))) {
+            /* SAM_STAT_COMMAND_TERMINATED is obsolete */
+            sg_print_sense(0, sense_buffer, sb_len, raw_sinfo);
+            done_sense = true;
+        }
+    }
+    if (0 != host_status) {
+        if (leadin && (! done_leadin))
+            pr2ws("%s: ", leadin);
+        if (done_leadin)
+            pr2ws("plus...: ");
+        else
+            done_leadin = true;
+        sg_print_host_status(host_status);
+        pr2ws("\n");
+    }
+    if (0 != driver_status) {
+        if (done_sense &&
+            (SG_LIB_DRIVER_SENSE == (SG_LIB_DRIVER_MASK & driver_status)))
+            return 0;
+        if (leadin && (! done_leadin))
+            pr2ws("%s: ", leadin);
+        if (done_leadin)
+            pr2ws("plus...: ");
+        sg_print_driver_status(driver_status);
+        pr2ws("\n");
+        if (sense_buffer && (! done_sense) &&
+            (SG_LIB_DRIVER_SENSE == (SG_LIB_DRIVER_MASK & driver_status)))
+            sg_print_sense(0, sense_buffer, sb_len, raw_sinfo);
+    }
+    return 0;
+}
+
+#ifdef SG_IO
+
+bool
+sg_normalize_sense(const struct sg_io_hdr * hp,
+                   struct sg_scsi_sense_hdr * sshp)
+{
+    if ((NULL == hp) || (0 == hp->sb_len_wr)) {
+        if (sshp)
+            memset(sshp, 0, sizeof(struct sg_scsi_sense_hdr));
+        return 0;
+    }
+    return sg_scsi_normalize_sense(hp->sbp, hp->sb_len_wr, sshp);
+}
+
+/* Returns 1 if no errors found and thus nothing printed; otherwise
+   returns 0. */
+int
+sg_chk_n_print3(const char * leadin, struct sg_io_hdr * hp,
+                bool raw_sinfo)
+{
+    return sg_linux_sense_print(leadin, hp->status, hp->host_status,
+                                hp->driver_status, hp->sbp, hp->sb_len_wr,
+                                raw_sinfo);
+}
+#endif
+
+/* Returns 1 if no errors found and thus nothing printed; otherwise
+   returns 0. */
+int
+sg_chk_n_print(const char * leadin, int masked_status, int host_status,
+               int driver_status, const uint8_t * sense_buffer,
+               int sb_len, bool raw_sinfo)
+{
+    int scsi_status = (masked_status << 1) & 0x7e;
+
+    return sg_linux_sense_print(leadin, scsi_status, host_status,
+                                driver_status, sense_buffer, sb_len,
+                                raw_sinfo);
+}
+
+#ifdef SG_IO
+int
+sg_err_category3(struct sg_io_hdr * hp)
+{
+    return sg_err_category_new(hp->status, hp->host_status,
+                               hp->driver_status, hp->sbp, hp->sb_len_wr);
+}
+#endif
+
+int
+sg_err_category(int masked_status, int host_status, int driver_status,
+                const uint8_t * sense_buffer, int sb_len)
+{
+    int scsi_status = (masked_status << 1) & 0x7e;
+
+    return sg_err_category_new(scsi_status, host_status, driver_status,
+                               sense_buffer, sb_len);
+}
+
+int
+sg_err_category_new(int scsi_status, int host_status, int driver_status,
+                    const uint8_t * sense_buffer, int sb_len)
+{
+    int masked_driver_status = (SG_LIB_DRIVER_MASK & driver_status);
+
+    scsi_status &= 0x7e;
+    if ((0 == scsi_status) && (0 == host_status) &&
+        (0 == masked_driver_status))
+        return SG_LIB_CAT_CLEAN;
+    if ((SAM_STAT_CHECK_CONDITION == scsi_status) ||
+        (SAM_STAT_COMMAND_TERMINATED == scsi_status) ||
+        (SG_LIB_DRIVER_SENSE == masked_driver_status))
+        return sg_err_category_sense(sense_buffer, sb_len);
+    if (0 != host_status) {
+        if ((SG_LIB_DID_NO_CONNECT == host_status) ||
+            (SG_LIB_DID_BUS_BUSY == host_status) ||
+            (SG_LIB_DID_TIME_OUT == host_status))
+            return SG_LIB_CAT_TIMEOUT;
+        if (SG_LIB_DID_NEXUS_FAILURE == host_status)
+            return SG_LIB_CAT_RES_CONFLICT;
+    }
+    if (SG_LIB_DRIVER_TIMEOUT == masked_driver_status)
+        return SG_LIB_CAT_TIMEOUT;
+    return SG_LIB_CAT_OTHER;
+}
+
+#endif  /* if SG_LIB_LINUX defined */
diff --git a/lib/sg_json_builder.c b/lib/sg_json_builder.c
new file mode 100644
index 0000000..ed2d139
--- /dev/null
+++ b/lib/sg_json_builder.c
@@ -0,0 +1,999 @@
+
+/* vim: set et ts=3 sw=3 sts=3 ft=c:
+ *
+ * Copyright (C) 2014 James McLaughlin.  All rights reserved.
+ * https://github.com/udp/json-builder
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "sg_json_builder.h"
+
+#include <string.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+/* This code was fetched from https://github.com/json-parser/json-builder
+ * and comes with the 2 clause BSD license (shown above) which is the same
+ * license that most of the rest of this package uses. */
+
+#ifdef _MSC_VER
+    #define snprintf _snprintf
+#endif
+
+static const json_serialize_opts default_opts =
+{
+   json_serialize_mode_single_line,
+   0,
+   3  /* indent_size */
+};
+
+typedef struct json_builder_value
+{
+   json_value value;
+
+   int is_builder_value;
+
+   size_t additional_length_allocated;
+   size_t length_iterated;
+
+} json_builder_value;
+
+static int builderize (json_value * value)
+{
+   if (((json_builder_value *) value)->is_builder_value)
+      return 1;
+   
+   if (value->type == json_object)
+   {
+      unsigned int i;
+
+      /* Values straight out of the parser have the names of object entries
+       * allocated in the same allocation as the values array itself.  This is
+       * not desirable when manipulating values because the names would be easy
+       * to clobber.
+       */
+      for (i = 0; i < value->u.object.length; ++ i)
+      {
+         json_char * name_copy;
+         json_object_entry * entry = &value->u.object.values [i];
+
+         if (! (name_copy = (json_char *) malloc ((entry->name_length + 1) * sizeof (json_char))))
+            return 0;
+
+         memcpy (name_copy, entry->name, entry->name_length + 1);
+         entry->name = name_copy;
+      }
+   }
+
+   ((json_builder_value *) value)->is_builder_value = 1;
+
+   return 1;
+}
+
+const size_t json_builder_extra = sizeof(json_builder_value) - sizeof(json_value);
+
+/* These flags are set up from the opts before serializing to make the
+ * serializer conditions simpler.
+ */
+const int f_spaces_around_brackets = (1 << 0);
+const int f_spaces_after_commas    = (1 << 1);
+const int f_spaces_after_colons    = (1 << 2);
+const int f_tabs                   = (1 << 3);
+
+static int get_serialize_flags (json_serialize_opts opts)
+{
+   int flags = 0;
+
+   if (opts.mode == json_serialize_mode_packed)
+      return 0;
+
+   if (opts.mode == json_serialize_mode_multiline)
+   {
+      if (opts.opts & json_serialize_opt_use_tabs)
+         flags |= f_tabs;
+   }
+   else
+   {
+      if (! (opts.opts & json_serialize_opt_pack_brackets))
+         flags |= f_spaces_around_brackets;
+
+      if (! (opts.opts & json_serialize_opt_no_space_after_comma))
+         flags |= f_spaces_after_commas;
+   }
+
+   if (! (opts.opts & json_serialize_opt_no_space_after_colon))
+      flags |= f_spaces_after_colons;
+
+   return flags;
+}
+
+json_value * json_array_new (size_t length)
+{
+    json_value * value = (json_value *) calloc (1, sizeof (json_builder_value));
+
+    if (!value)
+       return NULL;
+
+    ((json_builder_value *) value)->is_builder_value = 1;
+
+    value->type = json_array;
+
+    if (! (value->u.array.values = (json_value **) malloc (length * sizeof (json_value *))))
+    {
+       free (value);
+       return NULL;
+    }
+
+    ((json_builder_value *) value)->additional_length_allocated = length;
+
+    return value;
+}
+
+json_value * json_array_push (json_value * array, json_value * value)
+{
+   assert (array->type == json_array);
+
+   if (!builderize (array) || !builderize (value))
+      return NULL;
+
+   if (((json_builder_value *) array)->additional_length_allocated > 0)
+   {
+      -- ((json_builder_value *) array)->additional_length_allocated;
+   }
+   else
+   {
+      json_value ** values_new = (json_value **) realloc
+            (array->u.array.values, sizeof (json_value *) * (array->u.array.length + 1));
+
+      if (!values_new)
+         return NULL;
+
+      array->u.array.values = values_new;
+   }
+
+   array->u.array.values [array->u.array.length] = value;
+   ++ array->u.array.length;
+
+   value->parent = array;
+
+   return value;
+}
+
+json_value * json_object_new (size_t length)
+{
+    json_value * value = (json_value *) calloc (1, sizeof (json_builder_value));
+
+    if (!value)
+       return NULL;
+
+    ((json_builder_value *) value)->is_builder_value = 1;
+
+    value->type = json_object;
+
+    if (! (value->u.object.values = (json_object_entry *) calloc
+           (length, sizeof (*value->u.object.values))))
+    {
+       free (value);
+       return NULL;
+    }
+
+    ((json_builder_value *) value)->additional_length_allocated = length;
+
+    return value;
+}
+
+json_value * json_object_push (json_value * object,
+                               const json_char * name,
+                               json_value * value)
+{
+   return json_object_push_length (object, strlen (name), name, value);
+}
+
+json_value * json_object_push_length (json_value * object,
+                                      unsigned int name_length, const json_char * name,
+                                      json_value * value)
+{
+   json_char * name_copy;
+
+   assert (object->type == json_object);
+
+   if (! (name_copy = (json_char *) malloc ((name_length + 1) * sizeof (json_char))))
+      return NULL;
+   
+   memcpy (name_copy, name, name_length * sizeof (json_char));
+   name_copy [name_length] = 0;
+
+   if (!json_object_push_nocopy (object, name_length, name_copy, value))
+   {
+      free (name_copy);
+      return NULL;
+   }
+
+   return value;
+}
+
+json_value * json_object_push_nocopy (json_value * object,
+                                      unsigned int name_length, json_char * name,
+                                      json_value * value)
+{
+   json_object_entry * entry;
+
+   assert (object->type == json_object);
+
+   if (!builderize (object) || !builderize (value))
+      return NULL;
+
+   if (((json_builder_value *) object)->additional_length_allocated > 0)
+   {
+      -- ((json_builder_value *) object)->additional_length_allocated;
+   }
+   else
+   {
+      json_object_entry * values_new = (json_object_entry *)
+            realloc (object->u.object.values, sizeof (*object->u.object.values)
+                            * (object->u.object.length + 1));
+
+      if (!values_new)
+         return NULL;
+
+      object->u.object.values = values_new;
+   }
+
+   entry = object->u.object.values + object->u.object.length;
+
+   entry->name_length = name_length;
+   entry->name = name;
+   entry->value = value;
+
+   ++ object->u.object.length;
+
+   value->parent = object;
+
+   return value;
+}
+
+json_value * json_string_new (const json_char * buf)
+{
+   return json_string_new_length (strlen (buf), buf);
+}
+
+json_value * json_string_new_length (unsigned int length, const json_char * buf)
+{
+   json_value * value;
+   json_char * copy = (json_char *) malloc ((length + 1) * sizeof (json_char));
+
+   if (!copy)
+      return NULL;
+   
+   memcpy (copy, buf, length * sizeof (json_char));
+   copy [length] = 0;
+
+   if (! (value = json_string_new_nocopy (length, copy)))
+   {
+      free (copy);
+      return NULL;
+   }
+
+   return value;
+}
+
+json_value * json_string_new_nocopy (unsigned int length, json_char * buf)
+{
+   json_value * value = (json_value *) calloc (1, sizeof (json_builder_value));
+   
+   if (!value)
+      return NULL;
+
+   ((json_builder_value *) value)->is_builder_value = 1;
+
+   value->type = json_string;
+   value->u.string.length = length;
+   value->u.string.ptr = buf;
+
+   return value;
+}
+
+json_value * json_integer_new (json_int_t integer)
+{
+   json_value * value = (json_value *) calloc (1, sizeof (json_builder_value));
+   
+   if (!value)
+      return NULL;
+
+   ((json_builder_value *) value)->is_builder_value = 1;
+
+   value->type = json_integer;
+   value->u.integer = integer;
+
+   return value;
+}
+
+json_value * json_double_new (double dbl)
+{
+   json_value * value = (json_value *) calloc (1, sizeof (json_builder_value));
+   
+   if (!value)
+      return NULL;
+
+   ((json_builder_value *) value)->is_builder_value = 1;
+
+   value->type = json_double;
+   value->u.dbl = dbl;
+
+   return value;
+}
+
+json_value * json_boolean_new (int b)
+{
+   json_value * value = (json_value *) calloc (1, sizeof (json_builder_value));
+   
+   if (!value)
+      return NULL;
+
+   ((json_builder_value *) value)->is_builder_value = 1;
+
+   value->type = json_boolean;
+   value->u.boolean = b;
+
+   return value;
+}
+
+json_value * json_null_new (void)
+{
+   json_value * value = (json_value *) calloc (1, sizeof (json_builder_value));
+   
+   if (!value)
+      return NULL;
+
+   ((json_builder_value *) value)->is_builder_value = 1;
+
+   value->type = json_null;
+
+   return value;
+}
+
+void json_object_sort (json_value * object, json_value * proto)
+{
+   unsigned int i, out_index = 0;
+
+   if (!builderize (object))
+      return;  /* TODO error */
+
+   assert (object->type == json_object);
+   assert (proto->type == json_object);
+
+   for (i = 0; i < proto->u.object.length; ++ i)
+   {
+      unsigned int j;
+      json_object_entry proto_entry = proto->u.object.values [i];
+
+      for (j = 0; j < object->u.object.length; ++ j)
+      {
+         json_object_entry entry = object->u.object.values [j];
+
+         if (entry.name_length != proto_entry.name_length)
+            continue;
+
+         if (memcmp (entry.name, proto_entry.name, entry.name_length) != 0)
+            continue;
+
+         object->u.object.values [j] = object->u.object.values [out_index];
+         object->u.object.values [out_index] = entry;
+
+         ++ out_index;
+      }
+   }
+}
+
+json_value * json_object_merge (json_value * objectA, json_value * objectB)
+{
+   unsigned int i;
+
+   assert (objectA->type == json_object);
+   assert (objectB->type == json_object);
+   assert (objectA != objectB);
+
+   if (!builderize (objectA) || !builderize (objectB))
+      return NULL;
+
+   if (objectB->u.object.length <=
+        ((json_builder_value *) objectA)->additional_length_allocated)
+   {
+      ((json_builder_value *) objectA)->additional_length_allocated
+          -= objectB->u.object.length;
+   }
+   else
+   {
+      json_object_entry * values_new;
+
+      unsigned int alloc =
+          objectA->u.object.length
+              + ((json_builder_value *) objectA)->additional_length_allocated
+              + objectB->u.object.length;
+
+      if (! (values_new = (json_object_entry *)
+            realloc (objectA->u.object.values, sizeof (json_object_entry) * alloc)))
+      {
+          return NULL;
+      }
+
+      objectA->u.object.values = values_new;
+   }
+
+   for (i = 0; i < objectB->u.object.length; ++ i)
+   {
+      json_object_entry * entry = &objectA->u.object.values[objectA->u.object.length + i];
+
+      *entry = objectB->u.object.values[i];
+      entry->value->parent = objectA;
+   }
+
+   objectA->u.object.length += objectB->u.object.length;
+
+   free (objectB->u.object.values);
+   free (objectB);
+
+   return objectA;
+}
+
+static size_t measure_string (unsigned int length,
+                              const json_char * str)
+{
+   unsigned int i;
+   size_t measured_length = 0;
+
+   for(i = 0; i < length; ++ i)
+   {
+      json_char c = str [i];
+
+      switch (c)
+      {
+      case '"':
+      case '\\':
+      case '\b':
+      case '\f':
+      case '\n':
+      case '\r':
+      case '\t':
+
+         measured_length += 2;
+         break;
+
+      default:
+
+         ++ measured_length;
+         break;
+      };
+   };
+
+   return measured_length;
+}
+
+#define PRINT_ESCAPED(c) do {  \
+   *buf ++ = '\\';             \
+   *buf ++ = (c);              \
+} while(0);                    \
+
+static size_t serialize_string (json_char * buf,
+                                unsigned int length,
+                                const json_char * str)
+{
+   json_char * orig_buf = buf;
+   unsigned int i;
+
+   for(i = 0; i < length; ++ i)
+   {
+      json_char c = str [i];
+
+      switch (c)
+      {
+      case '"':   PRINT_ESCAPED ('\"');  continue;
+      case '\\':  PRINT_ESCAPED ('\\');  continue;
+      case '\b':  PRINT_ESCAPED ('b');   continue;
+      case '\f':  PRINT_ESCAPED ('f');   continue;
+      case '\n':  PRINT_ESCAPED ('n');   continue;
+      case '\r':  PRINT_ESCAPED ('r');   continue;
+      case '\t':  PRINT_ESCAPED ('t');   continue;
+
+      default:
+
+         *buf ++ = c;
+         break;
+      };
+   };
+
+   return buf - orig_buf;
+}
+
+size_t json_measure (json_value * value)
+{
+   return json_measure_ex (value, default_opts);
+}
+
+#define MEASURE_NEWLINE() do {                     \
+   ++ newlines;                                    \
+   indents += depth;                               \
+} while(0);                                        \
+
+size_t json_measure_ex (json_value * value, json_serialize_opts opts)
+{
+   size_t total = 1;  /* null terminator */
+   size_t newlines = 0;
+   size_t depth = 0;
+   size_t indents = 0;
+   int flags;
+   int bracket_size, comma_size, colon_size;
+
+   flags = get_serialize_flags (opts);
+
+   /* to reduce branching
+    */
+   bracket_size = flags & f_spaces_around_brackets ? 2 : 1;
+   comma_size = flags & f_spaces_after_commas ? 2 : 1;
+   colon_size = flags & f_spaces_after_colons ? 2 : 1;
+
+   while (value)
+   {
+      json_int_t integer;
+      json_object_entry * entry;
+
+      switch (value->type)
+      {
+         case json_array:
+
+            if (((json_builder_value *) value)->length_iterated == 0)
+            {
+               if (value->u.array.length == 0)
+               {
+                  total += 2;  /* `[]` */
+                  break;
+               }
+
+               total += bracket_size;  /* `[` */
+
+               ++ depth;
+               MEASURE_NEWLINE(); /* \n after [ */
+            }
+
+            if (((json_builder_value *) value)->length_iterated == value->u.array.length)
+            {
+               -- depth;
+               MEASURE_NEWLINE();
+               total += bracket_size;  /* `]` */
+
+               ((json_builder_value *) value)->length_iterated = 0;
+               break;
+            }
+
+            if (((json_builder_value *) value)->length_iterated > 0)
+            {
+               total += comma_size;  /* `, ` */
+
+               MEASURE_NEWLINE();
+            }
+
+            ((json_builder_value *) value)->length_iterated++;
+            value = value->u.array.values [((json_builder_value *) value)->length_iterated - 1];
+            continue;
+
+         case json_object:
+
+            if (((json_builder_value *) value)->length_iterated == 0)
+            {
+               if (value->u.object.length == 0)
+               {
+                  total += 2;  /* `{}` */
+                  break;
+               }
+
+               total += bracket_size;  /* `{` */
+
+               ++ depth;
+               MEASURE_NEWLINE(); /* \n after { */
+            }
+
+            if (((json_builder_value *) value)->length_iterated == value->u.object.length)
+            {
+               -- depth;
+               MEASURE_NEWLINE();
+               total += bracket_size;  /* `}` */
+
+               ((json_builder_value *) value)->length_iterated = 0;
+               break;
+            }
+
+            if (((json_builder_value *) value)->length_iterated > 0)
+            {
+               total += comma_size;  /* `, ` */
+               MEASURE_NEWLINE();
+            }
+
+            entry = value->u.object.values + (((json_builder_value *) value)->length_iterated ++);
+
+            total += 2 + colon_size;  /* `"": ` */
+            total += measure_string (entry->name_length, entry->name);
+
+            value = entry->value;
+            continue;
+
+         case json_string:
+
+            total += 2;  /* `""` */
+            total += measure_string (value->u.string.length, value->u.string.ptr);
+            break;
+
+         case json_integer:
+
+            integer = value->u.integer;
+
+            if (integer < 0)
+            {
+               total += 1;  /* `-` */
+               integer = - integer;
+            }
+
+            ++ total;  /* first digit */
+
+            while (integer >= 10)
+            {
+               ++ total;  /* another digit */
+               integer /= 10;
+            }
+
+            break;
+
+         case json_double:
+
+            total += snprintf (NULL, 0, "%g", value->u.dbl);
+
+            /* Because sometimes we need to add ".0" if sprintf does not do it
+             * for us. Downside is that we allocate more bytes than strictly
+             * needed for serialization.
+             */
+            total += 2;
+
+            break;
+
+         case json_boolean:
+
+            total += value->u.boolean ? 
+               4:  /* `true` */
+               5;  /* `false` */
+
+            break;
+
+         case json_null:
+
+            total += 4;  /* `null` */
+            break;
+
+         default:
+            break;
+      };
+
+      value = value->parent;
+   }
+
+   if (opts.mode == json_serialize_mode_multiline)
+   {
+      total += newlines * (((opts.opts & json_serialize_opt_CRLF) ? 2 : 1) + opts.indent_size);
+      total += indents * opts.indent_size;
+   }
+
+   return total;
+}
+
+void json_serialize (json_char * buf, json_value * value)
+{
+   json_serialize_ex (buf, value, default_opts);
+}
+
+#define PRINT_NEWLINE() do {                          \
+   if (opts.mode == json_serialize_mode_multiline) {  \
+      if (opts.opts & json_serialize_opt_CRLF)        \
+         *buf ++ = '\r';                              \
+      *buf ++ = '\n';                                 \
+      for(i = 0; i < indent; ++ i)                    \
+         *buf ++ = indent_char;                       \
+   }                                                  \
+} while(0);                                           \
+
+#define PRINT_OPENING_BRACKET(c) do {                 \
+   *buf ++ = (c);                                     \
+   if (flags & f_spaces_around_brackets)              \
+      *buf ++ = ' ';                                  \
+} while(0);                                           \
+
+#define PRINT_CLOSING_BRACKET(c) do {                 \
+   if (flags & f_spaces_around_brackets)              \
+      *buf ++ = ' ';                                  \
+   *buf ++ = (c);                                     \
+} while(0);                                           \
+
+void json_serialize_ex (json_char * buf, json_value * value, json_serialize_opts opts)
+{
+   json_int_t integer, orig_integer;
+   json_object_entry * entry;
+   json_char * ptr, * dot;
+   int indent = 0;
+   char indent_char;
+   int i;
+   int flags;
+
+   flags = get_serialize_flags (opts);
+
+   indent_char = flags & f_tabs ? '\t' : ' ';
+
+   while (value)
+   {
+      switch (value->type)
+      {
+         case json_array:
+
+            if (((json_builder_value *) value)->length_iterated == 0)
+            {
+               if (value->u.array.length == 0)
+               {
+                  *buf ++ = '[';
+                  *buf ++ = ']';
+
+                  break;
+               }
+
+               PRINT_OPENING_BRACKET ('[');
+
+               indent += opts.indent_size;
+               PRINT_NEWLINE();
+            }
+
+            if (((json_builder_value *) value)->length_iterated == value->u.array.length)
+            {
+               indent -= opts.indent_size;
+               PRINT_NEWLINE();
+               PRINT_CLOSING_BRACKET (']');
+
+               ((json_builder_value *) value)->length_iterated = 0;
+               break;
+            }
+
+            if (((json_builder_value *) value)->length_iterated > 0)
+            {
+               *buf ++ = ',';
+
+               if (flags & f_spaces_after_commas)
+                  *buf ++ = ' ';
+
+               PRINT_NEWLINE();
+            }
+
+            ((json_builder_value *) value)->length_iterated++;
+            value = value->u.array.values [((json_builder_value *) value)->length_iterated - 1];
+            continue;
+
+         case json_object:
+
+            if (((json_builder_value *) value)->length_iterated == 0)
+            {
+               if (value->u.object.length == 0)
+               {
+                  *buf ++ = '{';
+                  *buf ++ = '}';
+
+                  break;
+               }
+
+               PRINT_OPENING_BRACKET ('{');
+
+               indent += opts.indent_size;
+               PRINT_NEWLINE();
+            }
+
+            if (((json_builder_value *) value)->length_iterated == value->u.object.length)
+            {
+               indent -= opts.indent_size;
+               PRINT_NEWLINE();
+               PRINT_CLOSING_BRACKET ('}');
+
+               ((json_builder_value *) value)->length_iterated = 0;
+               break;
+            }
+
+            if (((json_builder_value *) value)->length_iterated > 0)
+            {
+               *buf ++ = ',';
+
+               if (flags & f_spaces_after_commas)
+                  *buf ++ = ' ';
+
+               PRINT_NEWLINE();
+            }
+
+            entry = value->u.object.values + (((json_builder_value *) value)->length_iterated ++);
+
+            *buf ++ = '\"';
+            buf += serialize_string (buf, entry->name_length, entry->name);
+            *buf ++ = '\"';
+            *buf ++ = ':';
+
+            if (flags & f_spaces_after_colons)
+               *buf ++ = ' ';
+
+            value = entry->value;
+            continue;
+
+         case json_string:
+
+            *buf ++ = '\"';
+            buf += serialize_string (buf, value->u.string.length, value->u.string.ptr);
+            *buf ++ = '\"';
+            break;
+
+         case json_integer:
+
+            integer = value->u.integer;
+
+            if (integer < 0)
+            {
+               *buf ++ = '-';
+               integer = - integer;
+            }
+
+            orig_integer = integer;
+
+            ++ buf;
+
+            while (integer >= 10)
+            {
+               ++ buf;
+               integer /= 10;
+            }
+
+            integer = orig_integer;
+            ptr = buf;
+
+            do
+            {
+               *-- ptr = "0123456789"[integer % 10];
+
+            } while ((integer /= 10) > 0);
+
+            break;
+
+         case json_double:
+
+            ptr = buf;
+
+            buf += sprintf (buf, "%g", value->u.dbl);
+
+            if ((dot = strchr (ptr, ',')))
+            {
+               *dot = '.';
+            }
+            else if (!strchr (ptr, '.') && !strchr (ptr, 'e'))
+            {
+               *buf ++ = '.';
+               *buf ++ = '0';
+            }
+
+            break;
+
+         case json_boolean:
+
+            if (value->u.boolean)
+            {
+               memcpy (buf, "true", 4);
+               buf += 4;
+            }
+            else
+            {
+               memcpy (buf, "false", 5);
+               buf += 5;
+            }
+
+            break;
+
+         case json_null:
+
+            memcpy (buf, "null", 4);
+            buf += 4;
+            break;
+
+         default:
+            break;
+      };
+
+      value = value->parent;
+   }
+
+   *buf = 0;
+}
+
+void json_builder_free (json_value * value)
+{
+   json_value * cur_value;
+
+   if (!value)
+      return;
+
+   value->parent = 0;
+
+   while (value)
+   {
+      switch (value->type)
+      {
+         case json_array:
+
+            if (!value->u.array.length)
+            {
+               free (value->u.array.values);
+               break;
+            }
+
+            value = value->u.array.values [-- value->u.array.length];
+            continue;
+
+         case json_object:
+
+            if (!value->u.object.length)
+            {
+               free (value->u.object.values);
+               break;
+            }
+
+            -- value->u.object.length;
+
+            if (((json_builder_value *) value)->is_builder_value)
+            {
+               /* Names are allocated separately for builder values.  In parser
+                * values, they are part of the same allocation as the values array
+                * itself.
+                */
+               free (value->u.object.values [value->u.object.length].name);
+            }
+
+            value = value->u.object.values [value->u.object.length].value;
+            continue;
+
+         case json_string:
+
+            free (value->u.string.ptr);
+            break;
+
+         default:
+            break;
+      };
+
+      cur_value = value;
+      value = value->parent;
+      free (cur_value);
+   }
+}
+
+
+
+
+
+
diff --git a/lib/sg_json_builder.h b/lib/sg_json_builder.h
new file mode 100644
index 0000000..9027ed5
--- /dev/null
+++ b/lib/sg_json_builder.h
@@ -0,0 +1,333 @@
+
+/* vim: set et ts=3 sw=3 sts=3 ft=c:
+ *
+ * Copyright (C) 2014 James McLaughlin.  All rights reserved.
+ * https://github.com/udp/json-builder
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SG_JSON_BUILDER_H
+#define SG_JSON_BUILDER_H
+
+/* This code was fetched from https://github.com/json-parser/json-builder
+ * and comes with the 2 clause BSD license (shown above) which is the same
+ * license that most of the rest of this package uses.
+ *
+ * This header file is in this 'lib' directory so its interface is _not_
+ * published with sg3_utils other header files found in the 'include'
+ * directory. Currently only this header's implementation (i.e.
+ * sg_json_builder.c) and sg_pr2serr.c are the only users of this header. */
+
+/*
+ * Used to require json.h from json-parser but what was needed as been
+ * included in this header.
+ * https://github.com/udp/json-parser
+ */
+/* #include "json.h" */
+
+#ifndef json_char
+   #define json_char char
+#endif
+
+#ifndef json_int_t
+   #undef JSON_INT_T_OVERRIDDEN
+   #if defined(_MSC_VER)
+      #define json_int_t __int64
+   #elif (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined(__cplusplus) && __cplusplus >= 201103L)
+      /* C99 and C++11 */
+      #include <stdint.h>
+      #define json_int_t int_fast64_t
+   #else
+      /* C89 */
+      #define json_int_t long
+   #endif
+#else
+   #define JSON_INT_T_OVERRIDDEN 1
+#endif
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+
+   #include <string.h>
+
+   extern "C"
+   {
+
+#endif
+
+typedef struct
+{
+   unsigned long max_memory;  /* should be size_t, but would modify the API */
+   int settings;
+
+   /* Custom allocator support (leave null to use malloc/free)
+    */
+
+   void * (* mem_alloc) (size_t, int zero, void * user_data);
+   void (* mem_free) (void *, void * user_data);
+
+   void * user_data;  /* will be passed to mem_alloc and mem_free */
+
+   size_t value_extra;  /* how much extra space to allocate for values? */
+
+} json_settings;
+
+#define json_enable_comments  0x01
+
+typedef enum
+{
+   json_none,
+   json_object,
+   json_array,
+   json_integer,
+   json_double,
+   json_string,
+   json_boolean,
+   json_null
+
+} json_type;
+
+extern const struct _json_value json_value_none;
+
+typedef struct _json_object_entry
+{
+    json_char * name;
+    unsigned int name_length;
+
+    struct _json_value * value;
+
+} json_object_entry;
+
+typedef struct _json_value
+{
+   struct _json_value * parent;
+
+   json_type type;
+
+   union
+   {
+      int boolean;
+      json_int_t integer;
+      double dbl;
+
+      struct
+      {
+         unsigned int length;
+         json_char * ptr; /* null terminated */
+
+      } string;
+
+      struct
+      {
+         unsigned int length;
+
+         json_object_entry * values;
+
+         #if defined(__cplusplus)
+         json_object_entry * begin () const
+         {  return values;
+         }
+         json_object_entry * end () const
+         {  return values + length;
+         }
+         #endif
+
+      } object;
+
+      struct
+      {
+         unsigned int length;
+         struct _json_value ** values;
+
+         #if defined(__cplusplus)
+         _json_value ** begin () const
+         {  return values;
+         }
+         _json_value ** end () const
+         {  return values + length;
+         }
+         #endif
+
+      } array;
+
+   } u;
+
+   union
+   {
+      struct _json_value * next_alloc;
+      void * object_mem;
+
+   } _reserved;
+
+   #ifdef JSON_TRACK_SOURCE
+
+      /* Location of the value in the source JSON
+       */
+      unsigned int line, col;
+
+   #endif
+
+
+   /* C++ operator sugar removed */
+
+} json_value;
+
+#if 0
+#define json_error_max 128
+json_value * json_parse_ex (json_settings * settings,
+                            const json_char * json,
+                            size_t length,
+                            char * error);
+
+void json_value_free (json_value *);
+
+
+/* Not usually necessary, unless you used a custom mem_alloc and now want to
+ * use a custom mem_free.
+ */
+void json_value_free_ex (json_settings * settings,
+                         json_value *);
+#endif
+
+/* <<< end of code from json-parser's json.h >>> */
+
+
+/* IMPORTANT NOTE:  If you want to use json-builder functions with values
+ * allocated by json-parser as part of the parsing process, you must pass
+ * json_builder_extra as the value_extra setting in json_settings when
+ * parsing.  Otherwise there will not be room for the extra state and
+ * json-builder WILL invoke undefined behaviour.
+ *
+ * Also note that unlike json-parser, json-builder does not currently support
+ * custom allocators (for no particular reason other than that it doesn't have
+ * any settings or global state.)
+ */
+extern const size_t json_builder_extra;
+
+
+/*** Arrays
+ ***
+ * Note that all of these length arguments are just a hint to allow for
+ * pre-allocation - passing 0 is fine.
+ */
+json_value * json_array_new (size_t length);
+json_value * json_array_push (json_value * array, json_value *);
+
+
+/*** Objects
+ ***/
+json_value * json_object_new (size_t length);
+
+json_value * json_object_push (json_value * object,
+                               const json_char * name,
+                               json_value *);
+
+/* Same as json_object_push, but doesn't call strlen() for you.
+ */
+json_value * json_object_push_length (json_value * object,
+                                      unsigned int name_length, const json_char * name,
+                                      json_value *);
+
+/* Same as json_object_push_length, but doesn't copy the name buffer before
+ * storing it in the value.  Use this micro-optimisation at your own risk.
+ */
+json_value * json_object_push_nocopy (json_value * object,
+                                      unsigned int name_length, json_char * name,
+                                      json_value *);
+
+/* Merges all entries from objectB into objectA and destroys objectB.
+ */
+json_value * json_object_merge (json_value * objectA, json_value * objectB);
+
+/* Sort the entries of an object based on the order in a prototype object.
+ * Helpful when reading JSON and writing it again to preserve user order.
+ */
+void json_object_sort (json_value * object, json_value * proto);
+
+
+
+/*** Strings
+ ***/
+json_value * json_string_new (const json_char *);
+json_value * json_string_new_length (unsigned int length, const json_char *);
+json_value * json_string_new_nocopy (unsigned int length, json_char *);
+
+
+/*** Everything else
+ ***/
+json_value * json_integer_new (json_int_t);
+json_value * json_double_new (double);
+json_value * json_boolean_new (int);
+json_value * json_null_new (void);
+
+
+/*** Serializing
+ ***/
+#define json_serialize_mode_multiline     0
+#define json_serialize_mode_single_line   1
+#define json_serialize_mode_packed        2
+
+#define json_serialize_opt_CRLF                    (1 << 1)
+#define json_serialize_opt_pack_brackets           (1 << 2)
+#define json_serialize_opt_no_space_after_comma    (1 << 3)
+#define json_serialize_opt_no_space_after_colon    (1 << 4)
+#define json_serialize_opt_use_tabs                (1 << 5)
+
+typedef struct json_serialize_opts
+{
+   int mode;
+   int opts;
+   int indent_size;
+
+} json_serialize_opts;
+
+
+/* Returns a length in characters that is at least large enough to hold the
+ * value in its serialized form, including a null terminator.
+ */
+size_t json_measure (json_value *);
+size_t json_measure_ex (json_value *, json_serialize_opts);
+
+
+/* Serializes a JSON value into the buffer given (which must already be
+ * allocated with a length of at least json_measure(value, opts))
+ */
+void json_serialize (json_char * buf, json_value *);
+void json_serialize_ex (json_char * buf, json_value *, json_serialize_opts);
+
+
+/*** Cleaning up
+ ***/
+void json_builder_free (json_value *);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif		/* SG_JSON_BUILDER_H */
+
+
+
diff --git a/lib/sg_lib.c b/lib/sg_lib.c
new file mode 100644
index 0000000..36fcf5c
--- /dev/null
+++ b/lib/sg_lib.c
@@ -0,0 +1,4088 @@
+/*
+ * Copyright (c) 1999-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/* NOTICE:
+ *    On 5th October 2004 (v1.00) this file name was changed from sg_err.c
+ *    to sg_lib.c and the previous GPL was changed to a FreeBSD license.
+ *    The intention is to maintain this file and the related sg_lib.h file
+ *    as open source and encourage their unencumbered use.
+ *
+ * CONTRIBUTIONS:
+ *    This file started out as a copy of SCSI opcodes, sense keys and
+ *    additional sense codes (ASC/ASCQ) kept in the Linux SCSI subsystem
+ *    in the kernel source file: drivers/scsi/constant.c . That file
+ *    bore this notice: "Copyright (C) 1993, 1994, 1995 Eric Youngdale"
+ *    and a GPL notice.
+ *
+ *    Much of the data in this file is derived from SCSI draft standards
+ *    found at https://www.t10.org with the "SCSI Primary Commands-4" (SPC-4)
+ *    being the central point of reference.
+ *
+ *    Contributions:
+ *      sense key specific field decoding [Trent Piepho 20031116]
+ *
+ */
+
+#define _POSIX_C_SOURCE 200809L         /* for posix_memalign() */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <ctype.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* sg_lib_version_str (and datestamp) defined in sg_lib_data.c file */
+
+#define ASCQ_ATA_PT_INFO_AVAILABLE 0x1d  /* corresponding ASC is 0 */
+
+typedef unsigned int my_uint;    /* convenience to save a few line wraps */
+
+FILE * sg_warnings_strm = NULL;        /* would like to default to stderr */
+
+
+int
+pr2ws(const char * fmt, ...)
+{
+    va_list args;
+    int n;
+
+    va_start(args, fmt);
+    n = vfprintf(sg_warnings_strm ? sg_warnings_strm : stderr, fmt, args);
+    va_end(args);
+    return n;
+}
+
+/* Want safe, 'n += snprintf(b + n, blen - n, ...)' style sequence of
+ * functions. Returns number of chars placed in cp excluding the
+ * trailing null char. So for cp_max_len > 0 the return value is always
+ * < cp_max_len; for cp_max_len <= 1 the return value is 0 and no chars are
+ * written to cp. Note this means that when cp_max_len = 1, this function
+ * assumes that cp[0] is the null character and does nothing (and returns
+ * 0). Linux kernel has a similar function called  scnprintf(). Public
+ * declaration in sg_pr2serr.h header  */
+int
+sg_scnpr(char * cp, int cp_max_len, const char * fmt, ...)
+{
+    va_list args;
+    int n;
+
+    if (cp_max_len < 2)
+        return 0;
+    va_start(args, fmt);
+    n = vsnprintf(cp, cp_max_len, fmt, args);
+    va_end(args);
+    return (n < cp_max_len) ? n : (cp_max_len - 1);
+}
+
+/* Simple ASCII printable (does not use locale), includes space and excludes
+ * DEL (0x7f). */
+static inline int
+my_isprint(int ch)
+{
+    return ((ch >= ' ') && (ch < 0x7f));
+}
+
+/* DSENSE is 'descriptor sense' as opposed to the older 'fixed sense'.
+ * Only (currently) used in SNTL. */
+bool
+sg_get_initial_dsense(void)
+{
+    int k;
+    const char * cp;
+
+    cp = getenv("SG3_UTILS_DSENSE");
+    if (cp) {
+        if (1 == sscanf(cp, "%d", &k))
+            return k ? true : false;
+    }
+    return false;
+}
+
+/* Searches 'arr' for match on 'value' then 'peri_type'. If matches
+   'value' but not 'peri_type' then yields first 'value' match entry.
+   Last element of 'arr' has NULL 'name'. If no match returns NULL. */
+static const struct sg_lib_value_name_t *
+get_value_name(const struct sg_lib_value_name_t * arr, int value,
+               int peri_type)
+{
+    const struct sg_lib_value_name_t * vp = arr;
+    const struct sg_lib_value_name_t * holdp;
+
+    if (peri_type < 0)
+        peri_type = 0;
+    for (; vp->name; ++vp) {
+        if (value == vp->value) {
+            if (sg_pdt_s_eq(peri_type, vp->peri_dev_type))
+                return vp;
+            holdp = vp;
+            while ((vp + 1)->name && (value == (vp + 1)->value)) {
+                ++vp;
+                if (sg_pdt_s_eq(peri_type, vp->peri_dev_type))
+                    return vp;
+            }
+            return holdp;
+        }
+    }
+    return NULL;
+}
+
+/* If this function is not called, sg_warnings_strm will be NULL and all users
+ * (mainly fprintf() ) need to check and substitute stderr as required */
+void
+sg_set_warnings_strm(FILE * warnings_strm)
+{
+    sg_warnings_strm = warnings_strm;
+}
+
+/* Take care to minimize printf() parsing delays when printing commands */
+static char bin2hexascii[] = {'0', '1', '2', '3', '4', '5', '6', '7',
+                              '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+
+/* Given a SCSI command pointed to by cdbp of sz bytes this function forms
+ * a SCSI command in ASCII surrounded by square brackets in 'b'. 'b' is at
+ * least blen bytes long. If cmd_name is true then the command is prefixed
+ * by its SCSI command name (e.g.  "VERIFY(10) [2f ...]". The command is
+ * shown as spaced separated pairs of hexadecimal digits (i.e. 0-9, a-f).
+ * Each pair represents byte. The leftmost pair of digits is cdbp[0] . If
+ * sz <= 0 then this function tries to guess the length of the command. */
+char *
+sg_get_command_str(const uint8_t * cdbp, int sz, bool cmd_name, int blen,
+                   char * b)
+{
+    int k, j, jj;
+
+    if ((cdbp == NULL) || (b == NULL) || (blen < 1))
+        return b;
+    if (cmd_name && (blen > 16)) {
+        sg_get_command_name(cdbp, 0, blen, b);
+        j = (int)strlen(b);
+        if (j < (blen - 1))
+            b[j++] = ' ';
+    } else
+        j = 0;
+    if (j >= blen)
+        goto fini;
+    b[j++] = '[';
+    if (j >= blen)
+        goto fini;
+    if (sz <= 0) {
+        if (SG_VARIABLE_LENGTH_CMD == cdbp[0])
+            sz = cdbp[7] + 8;
+        else
+            sz = sg_get_command_size(cdbp[0]);
+    }
+    jj = j;
+    for (k = 0; (k < sz) && (j < (blen - 3)); ++k, j += 3, ++cdbp) {
+        b[j] = bin2hexascii[(*cdbp >> 4) & 0xf];
+        b[j + 1] = bin2hexascii[*cdbp & 0xf];
+        b[j + 2] = ' ';
+    }
+    if (j > jj)
+        --j;    /* don't want trailing space before ']' */
+    if (j >= blen)
+        goto fini;
+    b[j++] = ']';
+fini:
+    if (j >= blen)
+        b[blen - 1] = '\0';     /* truncated string */
+    else
+        b[j] = '\0';
+    return b;
+}
+
+#define CMD_NAME_LEN 128
+
+void
+sg_print_command_len(const uint8_t * cdbp, int sz)
+{
+    char buff[CMD_NAME_LEN];
+
+    sg_get_command_str(cdbp, sz, true, sizeof(buff), buff);
+    pr2ws("%s\n", buff);
+}
+
+void
+sg_print_command(const uint8_t * cdbp)
+{
+    sg_print_command_len(cdbp, 0);
+}
+
+bool
+sg_scsi_status_is_good(int sstatus)
+{
+    sstatus &= 0xfe;
+    switch (sstatus) {
+    case SAM_STAT_GOOD:
+    case SAM_STAT_CONDITION_MET:
+        return true;
+    default:
+        return false;
+    }
+}
+
+bool
+sg_scsi_status_is_bad(int sstatus)
+{
+    sstatus &= 0xfe;
+    switch (sstatus) {
+    case SAM_STAT_GOOD:
+    case SAM_STAT_CONDITION_MET:
+        return false;
+    default:
+        return true;
+    }
+}
+
+void
+sg_get_scsi_status_str(int scsi_status, int buff_len, char * buff)
+{
+    const struct sg_lib_simple_value_name_t * sstatus_p;
+
+    if ((NULL == buff) || (buff_len < 1))
+        return;
+    else if (1 == buff_len) {
+        buff[0] = '\0';
+        return;
+    }
+    scsi_status &= 0x7e; /* sanitize as much as possible */
+    for (sstatus_p = sg_lib_sstatus_str_arr; sstatus_p->name; ++sstatus_p) {
+        if (scsi_status == sstatus_p->value)
+            break;
+    }
+    if (sstatus_p->name)
+        sg_scnpr(buff, buff_len, "%s", sstatus_p->name);
+    else
+        sg_scnpr(buff, buff_len, "Unknown status [0x%x]", scsi_status);
+}
+
+void
+sg_print_scsi_status(int scsi_status)
+{
+    char buff[128];
+
+    sg_get_scsi_status_str(scsi_status, sizeof(buff) - 1, buff);
+    buff[sizeof(buff) - 1] = '\0';
+    pr2ws("%s ", buff);
+}
+
+/* Get sense key from sense buffer. If successful returns a sense key value
+ * between 0 and 15. If sense buffer cannot be decode, returns -1 . */
+int
+sg_get_sense_key(const uint8_t * sbp, int sb_len)
+{
+    if ((NULL == sbp) || (sb_len < 2))
+        return -1;
+    switch (sbp[0] & 0x7f) {
+    case 0x70:
+    case 0x71:
+        return (sb_len < 3) ? -1 : (sbp[2] & 0xf);
+    case 0x72:
+    case 0x73:
+        return sbp[1] & 0xf;
+    default:
+        return -1;
+    }
+}
+
+/* Yield string associated with sense_key value. Returns 'buff'. */
+char *
+sg_get_sense_key_str(int sense_key, int buff_len, char * buff)
+{
+    if (1 == buff_len) {
+        buff[0] = '\0';
+        return buff;
+    }
+    if ((sense_key >= 0) && (sense_key < 16))
+         sg_scnpr(buff, buff_len, "%s", sg_lib_sense_key_desc[sense_key]);
+    else
+         sg_scnpr(buff, buff_len, "invalid value: 0x%x", sense_key);
+    return buff;
+}
+
+/* Yield string associated with ASC/ASCQ values. Returns 'buff'. */
+char *
+sg_get_additional_sense_str(int asc, int ascq, bool add_sense_leadin,
+                            int buff_len, char * buff)
+{
+    int k, num, rlen;
+    bool found = false;
+
+    if (1 == buff_len) {
+        buff[0] = '\0';
+        return buff;
+    }
+    for (k = 0; sg_lib_asc_ascq_range[k].text; ++k) {
+        struct sg_lib_asc_ascq_range_t * ei2p = &sg_lib_asc_ascq_range[k];
+
+        if ((ei2p->asc == asc) &&
+            (ascq >= ei2p->ascq_min)  &&
+            (ascq <= ei2p->ascq_max)) {
+            found = true;
+            if (add_sense_leadin)
+                num = sg_scnpr(buff, buff_len, "Additional sense: ");
+            else
+                num = 0;
+            rlen = buff_len - num;
+            sg_scnpr(buff + num, ((rlen > 0) ? rlen : 0), ei2p->text, ascq);
+        }
+    }
+    if (found)
+        return buff;
+
+    for (k = 0; sg_lib_asc_ascq[k].text; ++k) {
+        struct sg_lib_asc_ascq_t * eip = &sg_lib_asc_ascq[k];
+
+        if (eip->asc == asc &&
+            eip->ascq == ascq) {
+            found = true;
+            if (add_sense_leadin)
+                sg_scnpr(buff, buff_len, "Additional sense: %s", eip->text);
+            else
+                sg_scnpr(buff, buff_len, "%s", eip->text);
+        }
+    }
+    if (! found) {
+        if (asc >= 0x80)
+            sg_scnpr(buff, buff_len, "vendor specific ASC=%02x, ASCQ=%02x "
+                     "(hex)", asc, ascq);
+        else if (ascq >= 0x80)
+            sg_scnpr(buff, buff_len, "ASC=%02x, vendor specific qualification "
+                     "ASCQ=%02x (hex)", asc, ascq);
+        else
+            sg_scnpr(buff, buff_len, "ASC=%02x, ASCQ=%02x (hex)", asc, ascq);
+    }
+    return buff;
+}
+
+/* Yield string associated with ASC/ASCQ values. Returns 'buff'. */
+char *
+sg_get_asc_ascq_str(int asc, int ascq, int buff_len, char * buff)
+{
+    return sg_get_additional_sense_str(asc, ascq, true, buff_len, buff);
+}
+
+/* Attempt to find the first SCSI sense data descriptor that matches the
+ * given 'desc_type'. If found return pointer to start of sense data
+ * descriptor; otherwise (including fixed format sense data) returns NULL. */
+const uint8_t *
+sg_scsi_sense_desc_find(const uint8_t * sbp, int sb_len,
+                        int desc_type)
+{
+    int add_sb_len, desc_len, k;
+    const uint8_t * descp;
+
+    if ((sb_len < 8) || (0 == (add_sb_len = sbp[7])))
+        return NULL;
+    if ((sbp[0] < 0x72) || (sbp[0] > 0x73))
+        return NULL;
+    add_sb_len = (add_sb_len < (sb_len - 8)) ?  add_sb_len : (sb_len - 8);
+    descp = &sbp[8];
+    for (desc_len = 0, k = 0; k < add_sb_len; k += desc_len) {
+        int add_d_len;
+
+        descp += desc_len;
+        add_d_len = (k < (add_sb_len - 1)) ? descp[1]: -1;
+        desc_len = add_d_len + 2;
+        if (descp[0] == desc_type)
+            return descp;
+        if (add_d_len < 0) /* short descriptor ?? */
+            break;
+    }
+    return NULL;
+}
+
+/* Returns true if valid bit set, false if valid bit clear. Irrespective the
+ * information field is written out via 'info_outp' (except when it is
+ * NULL). Handles both fixed and descriptor sense formats. */
+bool
+sg_get_sense_info_fld(const uint8_t * sbp, int sb_len,
+                      uint64_t * info_outp)
+{
+    const uint8_t * bp;
+
+    if (info_outp)
+        *info_outp = 0;
+    if (sb_len < 7)
+        return false;
+    switch (sbp[0] & 0x7f) {
+    case 0x70:
+    case 0x71:
+        if (info_outp)
+            *info_outp = sg_get_unaligned_be32(sbp + 3);
+        return !!(sbp[0] & 0x80);
+    case 0x72:
+    case 0x73:
+        bp = sg_scsi_sense_desc_find(sbp, sb_len, 0 /* info desc */);
+        if (bp && (0xa == bp[1])) {
+            uint64_t ull = sg_get_unaligned_be64(bp + 4);
+
+            if (info_outp)
+                *info_outp = ull;
+            return !!(bp[2] & 0x80);   /* since spc3r23 should be set */
+        } else
+            return false;
+    default:
+        return false;
+    }
+}
+
+/* Returns true if fixed format or command specific information descriptor
+ * is found in the descriptor sense; else false. If available the command
+ * specific information field (4 byte integer in fixed format, 8 byte
+ * integer in descriptor format) is written out via 'cmd_spec_outp'.
+ * Handles both fixed and descriptor sense formats. */
+bool
+sg_get_sense_cmd_spec_fld(const uint8_t * sbp, int sb_len,
+                          uint64_t * cmd_spec_outp)
+{
+    const uint8_t * bp;
+
+    if (cmd_spec_outp)
+        *cmd_spec_outp = 0;
+    if (sb_len < 7)
+        return false;
+    switch (sbp[0] & 0x7f) {
+    case 0x70:
+    case 0x71:
+        if (cmd_spec_outp)
+            *cmd_spec_outp = sg_get_unaligned_be32(sbp + 8);
+        return true;
+    case 0x72:
+    case 0x73:
+        bp = sg_scsi_sense_desc_find(sbp, sb_len,
+                                     1 /* command specific info desc */);
+        if (bp && (0xa == bp[1])) {
+            if (cmd_spec_outp)
+                *cmd_spec_outp = sg_get_unaligned_be64(bp + 4);
+            return true;
+        } else
+            return false;
+    default:
+        return false;
+    }
+}
+
+/* Returns true if any of the 3 bits (i.e. FILEMARK, EOM or ILI) are set.
+ * In descriptor format if the stream commands descriptor not found
+ * then returns false. Writes true or false corresponding to these bits to
+ * the last three arguments if they are non-NULL. */
+bool
+sg_get_sense_filemark_eom_ili(const uint8_t * sbp, int sb_len,
+                              bool * filemark_p, bool * eom_p, bool * ili_p)
+{
+    const uint8_t * bp;
+
+    if (sb_len < 7)
+        return false;
+    switch (sbp[0] & 0x7f) {
+    case 0x70:
+    case 0x71:
+        if (sbp[2] & 0xe0) {
+            if (filemark_p)
+                *filemark_p = !!(sbp[2] & 0x80);
+            if (eom_p)
+                *eom_p = !!(sbp[2] & 0x40);
+            if (ili_p)
+                *ili_p = !!(sbp[2] & 0x20);
+            return true;
+        } else
+            return false;
+    case 0x72:
+    case 0x73:
+       /* Look for stream commands sense data descriptor */
+        bp = sg_scsi_sense_desc_find(sbp, sb_len, 4);
+        if (bp && (bp[1] >= 2)) {
+            if (bp[3] & 0xe0) {
+                if (filemark_p)
+                    *filemark_p = !!(bp[3] & 0x80);
+                if (eom_p)
+                    *eom_p = !!(bp[3] & 0x40);
+                if (ili_p)
+                    *ili_p = !!(bp[3] & 0x20);
+                return true;
+            }
+        }
+        return false;
+    default:
+        return false;
+    }
+}
+
+/* Returns true if SKSV is set and sense key is NO_SENSE or NOT_READY. Also
+ * returns true if progress indication sense data descriptor found. Places
+ * progress field from sense data where progress_outp points. If progress
+ * field is not available returns false and *progress_outp is unaltered.
+ * Handles both fixed and descriptor sense formats.
+ * Hint: if true is returned *progress_outp may be multiplied by 100 then
+ * divided by 65536 to get the percentage completion. */
+bool
+sg_get_sense_progress_fld(const uint8_t * sbp, int sb_len,
+                          int * progress_outp)
+{
+    const uint8_t * bp;
+    int sk, sk_pr;
+
+    if (sb_len < 7)
+        return false;
+    switch (sbp[0] & 0x7f) {
+    case 0x70:
+    case 0x71:
+        sk = (sbp[2] & 0xf);
+        if ((sb_len < 18) ||
+            ((SPC_SK_NO_SENSE != sk) && (SPC_SK_NOT_READY != sk)))
+            return false;
+        if (sbp[15] & 0x80) {        /* SKSV bit set */
+            if (progress_outp)
+                *progress_outp = sg_get_unaligned_be16(sbp + 16);
+            return true;
+        } else
+            return false;
+    case 0x72:
+    case 0x73:
+        /* sense key specific progress (0x2) or progress descriptor (0xa) */
+        sk = (sbp[1] & 0xf);
+        sk_pr = (SPC_SK_NO_SENSE == sk) || (SPC_SK_NOT_READY == sk);
+        if (sk_pr && ((bp = sg_scsi_sense_desc_find(sbp, sb_len, 2))) &&
+            (0x6 == bp[1]) && (0x80 & bp[4])) {
+            if (progress_outp)
+                *progress_outp = sg_get_unaligned_be16(bp + 5);
+            return true;
+        } else if (((bp = sg_scsi_sense_desc_find(sbp, sb_len, 0xa))) &&
+                   ((0x6 == bp[1]))) {
+            if (progress_outp)
+                *progress_outp = sg_get_unaligned_be16(bp + 6);
+            return true;
+        } else
+            return false;
+    default:
+        return false;
+    }
+}
+
+char *
+sg_get_pdt_str(int pdt, int buff_len, char * buff)
+{
+    if ((pdt < 0) || (pdt > PDT_MAX))
+        sg_scnpr(buff, buff_len, "bad pdt");
+    else
+        sg_scnpr(buff, buff_len, "%s", sg_lib_pdt_strs[pdt]);
+    return buff;
+}
+
+/* Returns true if left argument is "equal" to the right argument. l_pdt_s
+ * is a compound PDT (SCSI Peripheral Device Type) or a negative number
+ * which represents a wildcard (i.e. match anything). r_pdt_s has a similar
+ * form. PDT values are 5 bits long (0 to 31) and a compound pdt_s is
+ * formed by shifting the second (upper) PDT by eight bits to the left and
+ * OR-ing it with the first PDT. The pdt_s values must be defined so
+ * PDT_DISK (0) is _not_ the upper value in a compound pdt_s. */
+bool
+sg_pdt_s_eq(int l_pdt_s, int r_pdt_s)
+{
+    bool upper_l = !!(l_pdt_s & PDT_UPPER_MASK);
+    bool upper_r = !!(r_pdt_s & PDT_UPPER_MASK);
+
+    if ((l_pdt_s < 0) || (r_pdt_s < 0))
+        return true;
+    if (!upper_l && !upper_r)
+        return l_pdt_s == r_pdt_s;
+    else if (upper_l && upper_r)
+        return (((PDT_UPPER_MASK & l_pdt_s) == (PDT_UPPER_MASK & r_pdt_s)) ||
+                ((PDT_LOWER_MASK & l_pdt_s) == (PDT_LOWER_MASK & r_pdt_s)));
+    else if (upper_l)
+        return (((PDT_LOWER_MASK & l_pdt_s) == r_pdt_s) ||
+                ((PDT_UPPER_MASK & l_pdt_s) >> 8) == r_pdt_s);
+    return (((PDT_LOWER_MASK & r_pdt_s) == l_pdt_s) ||
+            ((PDT_UPPER_MASK & r_pdt_s) >> 8) == l_pdt_s);
+}
+
+int
+sg_lib_pdt_decay(int pdt)
+{
+    if ((pdt < 0) || (pdt > PDT_MAX))
+        return 0;
+    return sg_lib_pdt_decay_arr[pdt];
+}
+
+char *
+sg_get_trans_proto_str(int tpi, int buff_len, char * buff)
+{
+    if ((tpi < 0) || (tpi > 15))
+        sg_scnpr(buff, buff_len, "bad tpi");
+    else
+        sg_scnpr(buff, buff_len, "%s", sg_lib_transport_proto_strs[tpi]);
+    return buff;
+}
+
+#define TRANSPORT_ID_MIN_LEN 24
+
+char *
+sg_decode_transportid_str(const char * lip, uint8_t * bp, int bplen,
+                          bool only_one, int blen, char * b)
+{
+    int num, k, n;
+    uint64_t ull;
+    int bump;
+
+    if ((NULL == b) || (blen < 1))
+        return b;
+    else if (1 == blen) {
+        b[0] = '\0';
+        return b;
+    }
+    if (NULL == lip)
+        lip = "";
+    /* bump = TRANSPORT_ID_MIN_LEN; // some old compilers insisted on this */
+    for (k = 0, n = 0; bplen > 0; ++k, bp += bump, bplen -= bump) {
+        int proto_id, normal_len, tpid_format;
+
+        if ((k > 0) && only_one)
+            break;
+        if ((bplen < 24) || (0 != (bplen % 4)))
+            n += sg_scnpr(b + n, blen - n, "%sTransport Id short or not "
+                          "multiple of 4 [length=%d]:\n", lip, blen);
+        else
+            n += sg_scnpr(b + n, blen - n, "%sTransport Id of initiator:\n",
+                          lip);
+        tpid_format = ((bp[0] >> 6) & 0x3);
+        proto_id = (bp[0] & 0xf);
+        normal_len = (bplen > TRANSPORT_ID_MIN_LEN) ?
+                                TRANSPORT_ID_MIN_LEN : bplen;
+        switch (proto_id) {
+        case TPROTO_FCP: /* Fibre channel */
+            n += sg_scnpr(b + n, blen - n, "%s  FCP-2 World Wide Name:\n",
+                          lip);
+            if (0 != tpid_format)
+                n += sg_scnpr(b + n, blen - n, "%s  [Unexpected TPID format: "
+                              "%d]\n", lip, tpid_format);
+            n += hex2str(bp + 8, 8, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_SPI:        /* Scsi Parallel Interface, obsolete */
+            n += sg_scnpr(b + n, blen - n, "%s  Parallel SCSI initiator SCSI "
+                          "address: 0x%x\n", lip,
+                          sg_get_unaligned_be16(bp + 2));
+            if (0 != tpid_format)
+                n += sg_scnpr(b + n, blen - n, "%s  [Unexpected TPID format: "
+                              "%d]\n", lip, tpid_format);
+            n += sg_scnpr(b + n, blen - n, "%s  relative port number (of "
+                          "corresponding target): 0x%x\n", lip,
+                          sg_get_unaligned_be16(bp + 6));
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_SSA:
+            n += sg_scnpr(b + n, blen - n, "%s  SSA (transport id not "
+                          "defined):\n", lip);
+            n += sg_scnpr(b + n, blen - n, "%s  TPID format: %d\n", lip,
+                          tpid_format);
+            n += hex2str(bp, normal_len, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_1394: /* IEEE 1394 */
+            n += sg_scnpr(b + n, blen - n, "%s  IEEE 1394 EUI-64 name:\n",
+                          lip);
+            if (0 != tpid_format)
+                n += sg_scnpr(b + n, blen - n, "%s  [Unexpected TPID format: "
+                              "%d]\n", lip, tpid_format);
+            n += hex2str(&bp[8], 8, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_SRP:        /* SCSI over RDMA */
+            n += sg_scnpr(b + n, blen - n, "%s  RDMA initiator port "
+                          "identifier:\n", lip);
+            if (0 != tpid_format)
+                n += sg_scnpr(b + n, blen - n, "%s  [Unexpected TPID format: "
+                              "%d]\n", lip, tpid_format);
+            n += hex2str(bp + 8, 16, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_ISCSI:
+            n += sg_scnpr(b + n, blen - n, "%s  iSCSI ", lip);
+            num = sg_get_unaligned_be16(bp + 2);
+            if (0 == tpid_format)
+                n += sg_scnpr(b + n, blen - n, "name: %.*s\n", num, &bp[4]);
+            else if (1 == tpid_format)
+                n += sg_scnpr(b + n, blen - n, "world wide unique port id: "
+                              "%.*s\n", num, &bp[4]);
+            else {
+                n += sg_scnpr(b + n, blen - n, "  [Unexpected TPID format: "
+                              "%d]\n", tpid_format);
+                n += hex2str(bp, num + 4, lip, 0, blen - n, b + n);
+            }
+            bump = (((num + 4) < TRANSPORT_ID_MIN_LEN) ?
+                         TRANSPORT_ID_MIN_LEN : num + 4);
+            break;
+        case TPROTO_SAS:
+            ull = sg_get_unaligned_be64(bp + 4);
+            n += sg_scnpr(b + n, blen - n, "%s  SAS address: 0x%" PRIx64 "\n",
+                          lip, ull);
+            if (0 != tpid_format)
+                n += sg_scnpr(b + n, blen - n, "%s  [Unexpected TPID format: "
+                              "%d]\n", lip, tpid_format);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_ADT:        /* no TransportID defined by T10 yet */
+            n += sg_scnpr(b + n, blen - n, "%s  ADT:\n", lip);
+            n += sg_scnpr(b + n, blen - n, "%s  TPID format: %d\n", lip,
+                          tpid_format);
+            n += hex2str(bp, normal_len, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_ATA:        /* no TransportID defined by T10 yet */
+            n += sg_scnpr(b + n, blen - n, "%s  ATAPI:\n", lip);
+            n += sg_scnpr(b + n, blen - n, "%s  TPID format: %d\n", lip,
+                          tpid_format);
+            n += hex2str(bp, normal_len, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_UAS:        /* no TransportID defined by T10 yet */
+            n += sg_scnpr(b + n, blen - n, "%s  UAS:\n", lip);
+            n += sg_scnpr(b + n, blen - n, "%s  TPID format: %d\n", lip,
+                          tpid_format);
+            n += hex2str(bp, normal_len, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_SOP:
+            n += sg_scnpr(b + n, blen - n, "%s  SOP ", lip);
+            num = sg_get_unaligned_be16(bp + 2);
+            if (0 == tpid_format)
+                n += sg_scnpr(b + n, blen - n, "Routing ID: 0x%x\n", num);
+            else {
+                n += sg_scnpr(b + n, blen - n, "  [Unexpected TPID format: "
+                              "%d]\n", tpid_format);
+                n += hex2str(bp, normal_len, lip, 1, blen - n, b + n);
+            }
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_PCIE:       /* no TransportID defined by T10 yet */
+            n += sg_scnpr(b + n, blen - n, "%s  PCIE:\n", lip);
+            n += sg_scnpr(b + n, blen - n, "%s  TPID format: %d\n", lip,
+                          tpid_format);
+            n += hex2str(bp, normal_len, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        case TPROTO_NONE:       /* no TransportID defined by T10 */
+            n += sg_scnpr(b + n, blen - n, "%s  No specified protocol\n",
+                          lip);
+            /* n += hex2str(bp, ((bplen > 24) ? 24 : bplen),
+             *                 lip, 0, blen - n, b + n); */
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        default:
+            n += sg_scnpr(b + n, blen - n, "%s  unknown protocol id=0x%x  "
+                          "TPID format=%d\n", lip, proto_id, tpid_format);
+            n += hex2str(bp, normal_len, lip, 1, blen - n, b + n);
+            bump = TRANSPORT_ID_MIN_LEN;
+            break;
+        }
+    }
+    return b;
+}
+
+
+static const char * desig_code_set_str_arr[] =
+{
+    "Reserved [0x0]",
+    "Binary",
+    "ASCII",
+    "UTF-8",
+    "Reserved [0x4]", "Reserved [0x5]", "Reserved [0x6]", "Reserved [0x7]",
+    "Reserved [0x8]", "Reserved [0x9]", "Reserved [0xa]", "Reserved [0xb]",
+    "Reserved [0xc]", "Reserved [0xd]", "Reserved [0xe]", "Reserved [0xf]",
+};
+
+const char *
+sg_get_desig_code_set_str(int val)
+{
+    if ((val >= 0) && (val < (int)SG_ARRAY_SIZE(desig_code_set_str_arr)))
+        return desig_code_set_str_arr[val];
+    else
+        return NULL;
+}
+
+static const char * desig_assoc_str_arr[] =
+{
+    "Addressed logical unit",
+    "Target port",      /* that received request; unless SCSI ports VPD */
+    "Target device that contains addressed lu",
+    "Reserved [0x3]",
+};
+
+const char *
+sg_get_desig_assoc_str(int val)
+{
+    if ((val >= 0) && (val < (int)SG_ARRAY_SIZE(desig_assoc_str_arr)))
+        return desig_assoc_str_arr[val];
+    else
+        return NULL;
+}
+
+static const char * desig_type_str_arr[] =
+{
+    "Vendor specific [0x0]",
+    "T10 vendor identification",
+    "EUI-64 based",
+    "NAA",
+    "Relative target port",
+    "Target port group",        /* spc4r09: _primary_ target port group */
+    "Logical unit group",
+    "MD5 logical unit identifier",
+    "SCSI name string",
+    "Protocol specific port identifier",        /* spc4r36 */
+    "UUID identifier",          /* spc5r08 */
+    "Reserved [0xb]",
+    "Reserved [0xc]", "Reserved [0xd]", "Reserved [0xe]", "Reserved [0xf]",
+};
+
+const char *
+sg_get_desig_type_str(int val)
+{
+    if ((val >= 0) && (val < (int)SG_ARRAY_SIZE(desig_type_str_arr)))
+        return desig_type_str_arr[val];
+    else
+        return NULL;
+}
+
+char *
+sg_get_zone_type_str(uint8_t zt, int buff_len, char * buff)
+{
+    if ((NULL == buff) || (buff_len < 1))
+        return NULL;
+    switch (zt) {
+    case 1:
+        sg_scnpr(buff, buff_len, "conventional");
+        break;
+    case 2:
+        sg_scnpr(buff, buff_len, "sequential write required");
+        break;
+    case 3:
+        sg_scnpr(buff, buff_len, "sequential write preferred");
+        break;
+    case 4:
+        sg_scnpr(buff, buff_len, "sequential or before required");
+        break;
+    case 5:
+        sg_scnpr(buff, buff_len, "gap");
+        break;
+    default:
+        sg_scnpr(buff, buff_len, "unknown [0x%x]", zt);
+        break;
+    }
+    return buff;
+}
+
+
+/* Expects a T10 UUID designator (as found in the Device Identification VPD
+ * page) pointed to by 'dp'. To not produce an error string in 'b', c_set
+ * should be 1 (binary) and dlen should be 18. Currently T10 only supports
+ * locally assigned UUIDs. Writes output to string 'b' of no more than blen
+ * bytes and returns the number of bytes actually written to 'b' but doesn't
+ * count the trailing null character it always appends (if blen > 0). 'lip'
+ * is lead-in string (on each line) than may be NULL. skip_prefix avoids
+ * outputting: '   Locally assigned UUID: ' before the UUID. */
+int
+sg_t10_uuid_desig2str(const uint8_t *dp, int dlen, int c_set, bool do_long,
+                      bool skip_prefix, const char * lip /* lead-in */,
+                      int blen, char * b)
+{
+    int m;
+    int n = 0;
+
+    if (NULL == lip)
+        lip = "";
+    if (1 != c_set) {
+        n += sg_scnpr(b + n, blen - n, "%s      << expected binary "
+                      "code_set >>\n", lip);
+        n += hex2str(dp, dlen, lip, 0, blen - n, b + n);
+        return n;
+    }
+    if ((1 != ((dp[0] >> 4) & 0xf)) || (18 != dlen)) {
+        n += sg_scnpr(b + n, blen - n, "%s      << expected locally "
+                      "assigned UUID, 16 bytes long >>\n", lip);
+        n += hex2str(dp, dlen, lip, 0, blen - n, b + n);
+        return n;
+    }
+    if (skip_prefix) {
+        if (strlen(lip) > 0)
+            n += sg_scnpr(b + n, blen - n, "%s", lip);
+    } else
+        n += sg_scnpr(b + n, blen - n, "%s      Locally assigned UUID: ",
+                      lip);
+    for (m = 0; m < 16; ++m) {
+        if ((4 == m) || (6 == m) || (8 == m) || (10 == m))
+            n += sg_scnpr(b + n, blen - n, "-");
+        n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)dp[2 + m]);
+    }
+    n += sg_scnpr(b + n, blen - n, "\n");
+    if (do_long) {
+        n += sg_scnpr(b + n, blen - n, "%s      [0x", lip);
+        for (m = 0; m < 16; ++m)
+            n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)dp[2 + m]);
+        n += sg_scnpr(b + n, blen - n, "]\n");
+    }
+    return n;
+}
+
+int
+sg_get_designation_descriptor_str(const char * lip, const uint8_t * ddp,
+                                  int dd_len, bool print_assoc, bool do_long,
+                                  int blen, char * b)
+{
+    int m, p_id, piv, c_set, assoc, desig_type, ci_off, c_id, d_id, naa;
+    int vsi, k, n, dlen;
+    uint64_t ccc_id, vsei;
+    const uint8_t * ip;
+    char e[64];
+    const char * cp;
+
+    n = 0;
+    if (NULL == lip)
+        lip = "";
+    if (dd_len < 4) {
+        n += sg_scnpr(b + n, blen - n, "%sdesignator desc too short: got "
+                      "length of %d want 4 or more\n", lip, dd_len);
+        return n;
+    }
+    dlen = ddp[3];
+    if (dlen > (dd_len - 4)) {
+        n += sg_scnpr(b + n, blen - n, "%sdesignator too long: says it is %d "
+                      "bytes, but given %d bytes\n", lip, dlen, dd_len - 4);
+        return n;
+    }
+    ip = ddp + 4;
+    p_id = ((ddp[0] >> 4) & 0xf);
+    c_set = (ddp[0] & 0xf);
+    piv = ((ddp[1] & 0x80) ? 1 : 0);
+    assoc = ((ddp[1] >> 4) & 0x3);
+    desig_type = (ddp[1] & 0xf);
+    if (print_assoc && ((cp = sg_get_desig_assoc_str(assoc))))
+        n += sg_scnpr(b + n, blen - n, "%s  %s:\n", lip, cp);
+    n += sg_scnpr(b + n, blen - n, "%s    designator type: ", lip);
+    cp = sg_get_desig_type_str(desig_type);
+    if (cp)
+        n += sg_scnpr(b + n, blen - n, "%s", cp);
+    n += sg_scnpr(b + n, blen - n, ",  code set: ");
+    cp = sg_get_desig_code_set_str(c_set);
+    if (cp)
+        n += sg_scnpr(b + n, blen - n, "%s", cp);
+    n += sg_scnpr(b + n, blen - n, "\n");
+    if (piv && ((1 == assoc) || (2 == assoc)))
+        n += sg_scnpr(b + n, blen - n, "%s     transport: %s\n", lip,
+                      sg_get_trans_proto_str(p_id, sizeof(e), e));
+    switch (desig_type) {
+    case 0: /* vendor specific */
+        k = 0;
+        if ((1 == c_set) || (2 == c_set)) { /* ASCII or UTF-8 */
+            for (k = 0; (k < dlen) && my_isprint(ip[k]); ++k)
+                ;
+            if (k >= dlen)
+                k = 1;
+        }
+        if (k)
+            n += sg_scnpr(b + n, blen - n, "%s      vendor specific: %.*s\n",
+                          lip, dlen, ip);
+        else {
+            n += sg_scnpr(b + n, blen - n, "%s      vendor specific:\n", lip);
+            n += hex2str(ip, dlen, lip, 0, blen - n, b + n);
+        }
+        break;
+    case 1: /* T10 vendor identification */
+        n += sg_scnpr(b + n, blen - n, "%s      vendor id: %.8s\n", lip, ip);
+        if (dlen > 8) {
+            if ((2 == c_set) || (3 == c_set)) { /* ASCII or UTF-8 */
+                n += sg_scnpr(b + n, blen - n, "%s      vendor specific: "
+                              "%.*s\n", lip, dlen - 8, ip + 8);
+            } else {
+                n += sg_scnpr(b + n, blen - n, "%s      vendor specific: 0x",
+                              lip);
+                for (m = 8; m < dlen; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+                n += sg_scnpr(b + n, blen - n, "\n");
+            }
+        }
+        break;
+    case 2: /* EUI-64 based */
+        if (! do_long) {
+            if ((8 != dlen) && (12 != dlen) && (16 != dlen)) {
+                n += sg_scnpr(b + n, blen - n, "%s      << expect 8, 12 and "
+                              "16 byte EUI, got %d >>\n", lip, dlen);
+                n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+                break;
+            }
+            n += sg_scnpr(b + n, blen - n, "%s      0x", lip);
+            for (m = 0; m < dlen; ++m)
+                n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+            n += sg_scnpr(b + n, blen - n, "\n");
+            break;
+        }
+        n += sg_scnpr(b + n, blen - n, "%s      EUI-64 based %d byte "
+                      "identifier\n", lip, dlen);
+        if (1 != c_set) {
+            n += sg_scnpr(b + n, blen - n, "%s      << expected binary "
+                          "code_set (1) >>\n", lip);
+            n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+            break;
+        }
+        ci_off = 0;
+        if (16 == dlen) {       /* first 8 bytes are 'Identifier Extension' */
+            uint64_t id_ext = sg_get_unaligned_be64(ip);
+
+            ci_off = 8;
+            n += sg_scnpr(b + n, blen - n, "%s      Identifier extension: 0x%"
+                          PRIx64 "\n", lip, id_ext);
+        } else if ((8 != dlen) && (12 != dlen)) {
+            n += sg_scnpr(b + n, blen - n, "%s      << can only decode 8, 12 "
+                          "and 16 byte ids >>\n", lip);
+            n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+            break;
+        }
+        ccc_id = sg_get_unaligned_be64(ip + ci_off);
+        n += sg_scnpr(b + n, blen - n, "%s      IEEE identifier: 0x%"
+                      PRIx64 "\n", lip, ccc_id);
+        if (12 == dlen) {
+            d_id = sg_get_unaligned_be32(ip + 8);
+            n += sg_scnpr(b + n, blen - n, "%s      Directory ID: 0x%x\n",
+                          lip, d_id);
+        }
+        break;
+    case 3: /* NAA <n> */
+        if (1 != c_set) {
+            n += sg_scnpr(b + n, blen - n, "%s      << unexpected code set "
+                          "%d for NAA >>\n", lip, c_set);
+            n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+            break;
+        }
+        naa = (ip[0] >> 4) & 0xff;
+        switch (naa) {
+        case 2:         /* NAA 2: IEEE Extended */
+            if (8 != dlen) {
+                n += sg_scnpr(b + n, blen - n, "%s      << unexpected NAA 2 "
+                              "identifier length: 0x%x >>\n", lip, dlen);
+                n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+                break;
+            }
+            d_id = (((ip[0] & 0xf) << 8) | ip[1]);
+            c_id = sg_get_unaligned_be24(ip + 2);
+            vsi = sg_get_unaligned_be24(ip + 5);
+            if (do_long) {
+                n += sg_scnpr(b + n, blen - n, "%s      NAA 2, vendor "
+                              "specific identifier A: 0x%x\n", lip, d_id);
+                n += sg_scnpr(b + n, blen - n, "%s      AOI: 0x%x\n", lip,
+                              c_id);
+                n += sg_scnpr(b + n, blen - n, "%s      vendor specific "
+                              "identifier B: 0x%x\n", lip, vsi);
+                n += sg_scnpr(b + n, blen - n, "%s      [0x", lip);
+                for (m = 0; m < 8; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+                n += sg_scnpr(b + n, blen - n, "]\n");
+            }
+            n += sg_scnpr(b + n, blen - n, "%s      0x", lip);
+            for (m = 0; m < 8; ++m)
+                n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+            n += sg_scnpr(b + n, blen - n, "\n");
+            break;
+        case 3:         /* NAA 3: Locally assigned */
+            if (8 != dlen) {
+                n += sg_scnpr(b + n, blen - n, "%s      << unexpected NAA 3 "
+                              "identifier length: 0x%x >>\n", lip, dlen);
+                n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+                break;
+            }
+            if (do_long)
+                n += sg_scnpr(b + n, blen - n, "%s      NAA 3, Locally "
+                              "assigned:\n", lip);
+            n += sg_scnpr(b + n, blen - n, "%s      0x", lip);
+            for (m = 0; m < 8; ++m)
+                n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+            n += sg_scnpr(b + n, blen - n, "\n");
+            break;
+        case 5:         /* NAA 5: IEEE Registered */
+            if (8 != dlen) {
+                n += sg_scnpr(b + n, blen - n, "%s      << unexpected NAA 5 "
+                              "identifier length: 0x%x >>\n", lip, dlen);
+                n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+                break;
+            }
+            c_id = (((ip[0] & 0xf) << 20) | (ip[1] << 12) |
+                    (ip[2] << 4) | ((ip[3] & 0xf0) >> 4));
+            vsei = ip[3] & 0xf;
+            for (m = 1; m < 5; ++m) {
+                vsei <<= 8;
+                vsei |= ip[3 + m];
+            }
+            if (do_long) {
+                n += sg_scnpr(b + n, blen - n, "%s      NAA 5, AOI: 0x%x\n",
+                              lip, c_id);
+                n += sg_scnpr(b + n, blen - n, "%s      Vendor Specific "
+                              "Identifier: 0x%" PRIx64 "\n", lip, vsei);
+                n += sg_scnpr(b + n, blen - n, "%s      [0x", lip);
+                for (m = 0; m < 8; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+                n += sg_scnpr(b + n, blen - n, "]\n");
+            } else {
+                n += sg_scnpr(b + n, blen - n, "%s      0x", lip);
+                for (m = 0; m < 8; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+                n += sg_scnpr(b + n, blen - n, "\n");
+            }
+            break;
+        case 6:         /* NAA 6: IEEE Registered extended */
+            if (16 != dlen) {
+                n += sg_scnpr(b + n, blen - n, "%s      << unexpected NAA 6 "
+                              "identifier length: 0x%x >>\n", lip, dlen);
+                n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+                break;
+            }
+            c_id = (((ip[0] & 0xf) << 20) | (ip[1] << 12) |
+                    (ip[2] << 4) | ((ip[3] & 0xf0) >> 4));
+            vsei = ip[3] & 0xf;
+            for (m = 1; m < 5; ++m) {
+                vsei <<= 8;
+                vsei |= ip[3 + m];
+            }
+            if (do_long) {
+                n += sg_scnpr(b + n, blen - n, "%s      NAA 6, AOI: 0x%x\n",
+                              lip, c_id);
+                n += sg_scnpr(b + n, blen - n, "%s      Vendor Specific "
+                              "Identifier: 0x%" PRIx64 "\n", lip, vsei);
+                vsei = sg_get_unaligned_be64(ip + 8);
+                n += sg_scnpr(b + n, blen - n, "%s      Vendor Specific "
+                              "Identifier Extension: 0x%" PRIx64 "\n", lip,
+                              vsei);
+                n += sg_scnpr(b + n, blen - n, "%s      [0x", lip);
+                for (m = 0; m < 16; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+                n += sg_scnpr(b + n, blen - n, "]\n");
+            } else {
+                n += sg_scnpr(b + n, blen - n, "%s      0x", lip);
+                for (m = 0; m < 16; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", (my_uint)ip[m]);
+                n += sg_scnpr(b + n, blen - n, "\n");
+            }
+            break;
+        default:
+            n += sg_scnpr(b + n, blen - n, "%s      << unexpected NAA [0x%x] "
+                          ">>\n", lip, naa);
+            n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+            break;
+        }
+        break;
+    case 4: /* Relative target port */
+        if ((1 != c_set) || (1 != assoc) || (4 != dlen)) {
+            n += sg_scnpr(b + n, blen - n, "%s      << expected binary "
+                          "code_set, target port association, length 4 >>\n",
+                          lip);
+            n += hex2str(ip, dlen, "", 1, blen - n, b + n);
+            break;
+        }
+        d_id = sg_get_unaligned_be16(ip + 2);
+        n += sg_scnpr(b + n, blen - n, "%s      Relative target port: 0x%x\n",
+                      lip, d_id);
+        break;
+    case 5: /* (primary) Target port group */
+        if ((1 != c_set) || (1 != assoc) || (4 != dlen)) {
+            n += sg_scnpr(b + n, blen - n, "%s      << expected binary "
+                          "code_set, target port association, length 4 >>\n",
+                          lip);
+            n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+            break;
+        }
+        d_id = sg_get_unaligned_be16(ip + 2);
+        n += sg_scnpr(b + n, blen - n, "%s      Target port group: 0x%x\n",
+                      lip, d_id);
+        break;
+    case 6: /* Logical unit group */
+        if ((1 != c_set) || (0 != assoc) || (4 != dlen)) {
+            n += sg_scnpr(b + n, blen - n, "%s      << expected binary "
+                          "code_set, logical unit association, length 4 >>\n",
+                          lip);
+            n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+            break;
+        }
+        d_id = sg_get_unaligned_be16(ip + 2);
+        n += sg_scnpr(b + n, blen - n, "%s      Logical unit group: 0x%x\n",
+                      lip, d_id);
+        break;
+    case 7: /* MD5 logical unit identifier */
+        if ((1 != c_set) || (0 != assoc)) {
+            n += sg_scnpr(b + n, blen - n, "%s      << expected binary "
+                          "code_set, logical unit association >>\n", lip);
+            n += hex2str(ip, dlen, "", 1, blen - n, b + n);
+            break;
+        }
+        n += sg_scnpr(b + n, blen - n, "%s      MD5 logical unit "
+                      "identifier:\n", lip);
+        n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+        break;
+    case 8: /* SCSI name string */
+        if (3 != c_set) {       /* accept ASCII as subset of UTF-8 */
+            if (2 == c_set) {
+                if (do_long)
+                    n += sg_scnpr(b + n, blen - n, "%s      << expected "
+                                  "UTF-8, use ASCII >>\n", lip);
+            } else {
+                n += sg_scnpr(b + n, blen - n, "%s      << expected UTF-8 "
+                              "code_set >>\n", lip);
+                n += hex2str(ip, dlen, lip, 0, blen - n, b + n);
+                break;
+            }
+        }
+        n += sg_scnpr(b + n, blen - n, "%s      SCSI name string:\n", lip);
+        /* does %s print out UTF-8 ok??
+         * Seems to depend on the locale. Looks ok here with my
+         * locale setting: en_AU.UTF-8
+         */
+        n += sg_scnpr(b + n, blen - n, "%s      %.*s\n", lip, dlen,
+                      (const char *)ip);
+        break;
+    case 9: /* Protocol specific port identifier */
+        /* added in spc4r36, PIV must be set, proto_id indicates */
+        /* whether UAS (USB) or SOP (PCIe) or ... */
+        if (! piv)
+            n += sg_scnpr(b + n, blen - n, " %s      >>>> Protocol specific "
+                          "port identifier expects protocol\n%s          "
+                          "identifier to be valid and it is not\n", lip, lip);
+        if (TPROTO_UAS == p_id) {
+            n += sg_scnpr(b + n, blen - n, "%s      USB device address: "
+                          "0x%x\n", lip, 0x7f & ip[0]);
+            n += sg_scnpr(b + n, blen - n, "%s      USB interface number: "
+                          "0x%x\n", lip, ip[2]);
+        } else if (TPROTO_SOP == p_id) {
+            n += sg_scnpr(b + n, blen - n, "%s      PCIe routing ID, bus "
+                          "number: 0x%x\n", lip, ip[0]);
+            n += sg_scnpr(b + n, blen - n, "%s          function number: "
+                          "0x%x\n", lip, ip[1]);
+            n += sg_scnpr(b + n, blen - n, "%s          [or device number: "
+                          "0x%x, function number: 0x%x]\n", lip,
+                          (0x1f & (ip[1] >> 3)), 0x7 & ip[1]);
+        } else
+            n += sg_scnpr(b + n, blen - n, "%s      >>>> unexpected protocol "
+                          "identifier: %s\n%s           with Protocol "
+                          "specific port identifier\n", lip,
+                          sg_get_trans_proto_str(p_id, sizeof(e), e), lip);
+        break;
+    case 0xa: /* UUID identifier */
+        n += sg_t10_uuid_desig2str(ip, dlen, c_set, do_long, false, lip,
+                                   blen - n, b + n);
+        break;
+    default: /* reserved */
+        n += sg_scnpr(b + n, blen - n, "%s      reserved designator=0x%x\n",
+                      lip, desig_type);
+        n += hex2str(ip, dlen, lip, 1, blen - n, b + n);
+        break;
+    }
+    return n;
+}
+
+static int
+decode_sks(const char * lip, const uint8_t * descp, int add_d_len,
+           int sense_key, bool * processedp, int blen, char * b)
+{
+    int progress, pr, rem, n;
+
+    n = 0;
+    if (NULL == lip)
+        lip = "";
+    switch (sense_key) {
+    case SPC_SK_ILLEGAL_REQUEST:
+        if (add_d_len < 6) {
+            n += sg_scnpr(b + n, blen - n, "Field pointer: ");
+            goto too_short;
+        }
+        /* abbreviate to fit on one line */
+        n += sg_scnpr(b + n, blen - n, "Field pointer:\n");
+        n += sg_scnpr(b + n, blen - n, "%s        Error in %s: byte %d", lip,
+                      (descp[4] & 0x40) ? "Command" : "Data parameters",
+                      sg_get_unaligned_be16(descp + 5));
+        if (descp[4] & 0x08) {
+            n += sg_scnpr(b + n, blen - n, " bit %d\n", descp[4] & 0x07);
+        } else
+            n += sg_scnpr(b + n, blen - n, "\n");
+        break;
+    case SPC_SK_HARDWARE_ERROR:
+    case SPC_SK_MEDIUM_ERROR:
+    case SPC_SK_RECOVERED_ERROR:
+        n += sg_scnpr(b + n, blen - n, "Actual retry count: ");
+        if (add_d_len < 6)
+            goto too_short;
+        n += sg_scnpr(b + n, blen - n,"%u\n",
+                      sg_get_unaligned_be16(descp + 5));
+        break;
+    case SPC_SK_NO_SENSE:
+    case SPC_SK_NOT_READY:
+        n += sg_scnpr(b + n, blen - n, "Progress indication: ");
+        if (add_d_len < 6)
+            goto too_short;
+        progress = sg_get_unaligned_be16(descp + 5);
+        pr = (progress * 100) / 65536;
+        rem = ((progress * 100) % 65536) / 656;
+        n += sg_scnpr(b + n, blen - n, "%d.%02d%%\n", pr, rem);
+        break;
+    case SPC_SK_COPY_ABORTED:
+        n += sg_scnpr(b + n, blen - n, "Segment pointer:\n");
+        if (add_d_len < 6)
+            goto too_short;
+        n += sg_scnpr(b + n, blen - n, "%s        Relative to start of %s, "
+                      "byte %d", lip, (descp[4] & 0x20) ?
+                         "segment descriptor" : "parameter list",
+                      sg_get_unaligned_be16(descp + 5));
+        if (descp[4] & 0x08)
+            n += sg_scnpr(b + n, blen - n, " bit %d\n", descp[4] & 0x07);
+        else
+            n += sg_scnpr(b + n, blen - n, "\n");
+        break;
+    case SPC_SK_UNIT_ATTENTION:
+        n += sg_scnpr(b + n, blen - n, "Unit attention condition queue:\n");
+        n += sg_scnpr(b + n, blen - n, "%s        overflow flag is %d\n", lip,
+                      !!(descp[4] & 0x1));
+        break;
+    default:
+        n += sg_scnpr(b + n, blen - n, "Sense_key: 0x%x unexpected\n",
+                      sense_key);
+        *processedp = false;
+        break;
+    }
+    return n;
+
+too_short:
+    n += sg_scnpr(b + n, blen - n, "%s\n", "   >> descriptor too short");
+    *processedp = false;
+    return n;
+}
+
+#define TPGS_STATE_OPTIMIZED 0x0
+#define TPGS_STATE_NONOPTIMIZED 0x1
+#define TPGS_STATE_STANDBY 0x2
+#define TPGS_STATE_UNAVAILABLE 0x3
+#define TPGS_STATE_OFFLINE 0xe
+#define TPGS_STATE_TRANSITIONING 0xf
+
+static int
+decode_tpgs_state(int st, char * b, int blen)
+{
+    switch (st) {
+    case TPGS_STATE_OPTIMIZED:
+        return sg_scnpr(b, blen, "active/optimized");
+    case TPGS_STATE_NONOPTIMIZED:
+        return sg_scnpr(b, blen, "active/non optimized");
+    case TPGS_STATE_STANDBY:
+        return sg_scnpr(b, blen, "standby");
+    case TPGS_STATE_UNAVAILABLE:
+        return sg_scnpr(b, blen, "unavailable");
+    case TPGS_STATE_OFFLINE:
+        return sg_scnpr(b, blen, "offline");
+    case TPGS_STATE_TRANSITIONING:
+        return sg_scnpr(b, blen, "transitioning between states");
+    default:
+        return sg_scnpr(b, blen, "unknown: 0x%x", st);
+    }
+}
+
+static int
+uds_referral_descriptor_str(char * b, int blen, const uint8_t * dp,
+                            int alen, const char * lip)
+{
+    int n = 0;
+    int dlen = alen - 2;
+    int k, j, g, f;
+    const uint8_t * tp;
+    char c[40];
+
+    if (NULL == lip)
+        lip = "";
+    n += sg_scnpr(b + n, blen - n, "%s   Not all referrals: %d\n", lip,
+                  !!(dp[2] & 0x1));
+    dp += 4;
+    for (k = 0, f = 1; (k + 4) < dlen; k += g, dp += g, ++f) {
+        int ntpgd = dp[3];
+        uint64_t ull;
+
+        g = (ntpgd * 4) + 20;
+        n += sg_scnpr(b + n, blen - n, "%s    Descriptor %d\n", lip, f);
+        if ((k + g) > dlen) {
+            n += sg_scnpr(b + n, blen - n, "%s      truncated descriptor, "
+                          "stop\n", lip);
+            return n;
+        }
+        ull = sg_get_unaligned_be64(dp + 4);
+        n += sg_scnpr(b + n, blen - n, "%s      first uds LBA: 0x%" PRIx64
+                      "\n", lip, ull);
+        ull = sg_get_unaligned_be64(dp + 12);
+        n += sg_scnpr(b + n, blen - n, "%s      last uds LBA:  0x%" PRIx64
+                      "\n", lip, ull);
+        for (j = 0; j < ntpgd; ++j) {
+            tp = dp + 20 + (j * 4);
+            decode_tpgs_state(tp[0] & 0xf, c, sizeof(c));
+            n += sg_scnpr(b + n, blen - n, "%s        tpg: %d  state: %s\n",
+                          lip, sg_get_unaligned_be16(tp + 2), c);
+        }
+    }
+    return n;
+}
+
+static const char * dd_usage_reason_str_arr[] = {
+    "Unknown",
+    "resend this and further commands to:",
+    "resend this command to:",
+    "new subsidiary lu added to this administrative lu:",
+    "administrative lu associated with a preferred binding:",
+   };
+
+
+/* Decode descriptor format sense descriptors (assumes sense buffer is
+ * in descriptor format). 'leadin' is string prepended to each line written
+ * to 'b', NULL treated as "". Returns the number of bytes written to 'b'
+ * excluding the trailing '\0'. If problem, returns 0. */
+int
+sg_get_sense_descriptors_str(const char * lip, const uint8_t * sbp,
+                             int sb_len, int blen, char * b)
+{
+    int add_sb_len, desc_len, k, j, sense_key;
+    int n, progress, pr, rem;
+    uint16_t sct_sc;
+    bool processed;
+    const uint8_t * descp;
+    char z[64];
+    static const char * dtsp = "   >> descriptor too short";
+    static const char * eccp = "Extended copy command";
+    static const char * ddp = "destination device";
+
+    if ((NULL == b) || (blen <= 0))
+        return 0;
+    b[0] = '\0';
+    if (lip)
+        sg_scnpr(z, sizeof(z), "%.60s  ", lip);
+    else
+        sg_scnpr(z, sizeof(z), "  ");
+    if ((sb_len < 8) || (0 == (add_sb_len = sbp[7])))
+        return 0;
+    add_sb_len = (add_sb_len < (sb_len - 8)) ? add_sb_len : (sb_len - 8);
+    sense_key = (sbp[1] & 0xf);
+
+    for (descp = (sbp + 8), k = 0, n = 0;
+         (k < add_sb_len) && (n < blen);
+         k += desc_len, descp += desc_len) {
+        int add_d_len = (k < (add_sb_len - 1)) ? descp[1] : -1;
+
+        if ((k + add_d_len + 2) > add_sb_len)
+            add_d_len = add_sb_len - k - 2;
+        desc_len = add_d_len + 2;
+        n += sg_scnpr(b + n, blen - n, "%s  Descriptor type: ", lip);
+        processed = true;
+        switch (descp[0]) {
+        case 0:
+            n += sg_scnpr(b + n, blen - n, "Information: ");
+            if (add_d_len >= 10) {
+                if (! (0x80 & descp[2]))
+                    n += sg_scnpr(b + n, blen - n, "Valid=0 (-> vendor "
+                                  "specific) ");
+                n += sg_scnpr(b + n, blen - n, "0x");
+                for (j = 0; j < 8; ++j)
+                    n += sg_scnpr(b + n, blen - n, "%02x", descp[4 + j]);
+                n += sg_scnpr(b + n, blen - n, "\n");
+            } else {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+            }
+            break;
+        case 1:
+            n += sg_scnpr(b + n, blen - n, "Command specific: ");
+            if (add_d_len >= 10) {
+                n += sg_scnpr(b + n, blen - n, "0x");
+                for (j = 0; j < 8; ++j)
+                    n += sg_scnpr(b + n, blen - n, "%02x", descp[4 + j]);
+                n += sg_scnpr(b + n, blen - n, "\n");
+            } else {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+            }
+            break;
+        case 2:         /* Sense Key Specific */
+            n += sg_scnpr(b + n, blen - n, "Sense key specific: ");
+            n += decode_sks(lip, descp, add_d_len, sense_key, &processed,
+                            blen - n, b + n);
+            break;
+        case 3:
+            n += sg_scnpr(b + n, blen - n, "Field replaceable unit code: ");
+            if (add_d_len >= 2)
+                n += sg_scnpr(b + n, blen - n, "0x%x\n", descp[3]);
+            else {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+            }
+            break;
+        case 4:
+            n += sg_scnpr(b + n, blen - n, "Stream commands: ");
+            if (add_d_len >= 2) {
+                if (descp[3] & 0x80)
+                    n += sg_scnpr(b + n, blen - n, "FILEMARK");
+                if (descp[3] & 0x40)
+                    n += sg_scnpr(b + n, blen - n, "End Of Medium (EOM)");
+                if (descp[3] & 0x20)
+                    n += sg_scnpr(b + n, blen - n, "Incorrect Length "
+                                  "Indicator (ILI)");
+                n += sg_scnpr(b + n, blen - n, "\n");
+            } else {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+            }
+            break;
+        case 5:
+            n += sg_scnpr(b + n, blen - n, "Block commands: ");
+            if (add_d_len >= 2)
+                n += sg_scnpr(b + n, blen - n, "Incorrect Length Indicator "
+                              "(ILI) %s\n",
+                              (descp[3] & 0x20) ? "set" : "clear");
+            else {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+            }
+            break;
+        case 6:
+            n += sg_scnpr(b + n, blen - n, "OSD object identification\n");
+            processed = false;
+            break;
+        case 7:
+            n += sg_scnpr(b + n, blen - n, "OSD response integrity check "
+                          "value\n");
+            processed = false;
+            break;
+        case 8:
+            n += sg_scnpr(b + n, blen - n, "OSD attribute identification\n");
+            processed = false;
+            break;
+        case 9:         /* this is defined in SAT (SAT-2) */
+            n += sg_scnpr(b + n, blen - n, "ATA Status Return: ");
+            if (add_d_len >= 12) {
+                int extend, count;
+
+                extend = descp[2] & 1;
+                count = descp[5] + (extend ? (descp[4] << 8) : 0);
+                n += sg_scnpr(b + n, blen - n, "extend=%d error=0x%x \n%s"
+                              "        count=0x%x ", extend, descp[3], lip,
+                              count);
+                if (extend)
+                    n += sg_scnpr(b + n, blen - n,
+                                  "lba=0x%02x%02x%02x%02x%02x%02x ",
+                                   descp[10], descp[8], descp[6], descp[11],
+                                   descp[9], descp[7]);
+                else
+                    n += sg_scnpr(b + n, blen - n, "lba=0x%02x%02x%02x ",
+                                  descp[11], descp[9], descp[7]);
+                n += sg_scnpr(b + n, blen - n, "device=0x%x status=0x%x\n",
+                              descp[12], descp[13]);
+            } else {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+            }
+            break;
+        case 0xa:
+           /* Added in SPC-4 rev 17, became 'Another ...' in rev 34 */
+            n += sg_scnpr(b + n, blen - n, "Another progress indication: ");
+            if (add_d_len < 6) {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+                break;
+            }
+            progress = sg_get_unaligned_be16(descp + 6);
+            pr = (progress * 100) / 65536;
+            rem = ((progress * 100) % 65536) / 656;
+            n += sg_scnpr(b + n, blen - n, "%d.02%d%%\n", pr, rem);
+            n += sg_scnpr(b + n, blen - n, "%s        [sense_key=0x%x "
+                          "asc,ascq=0x%x,0x%x]\n", lip, descp[2], descp[3],
+                          descp[4]);
+            break;
+        case 0xb:       /* Added in SPC-4 rev 23, defined in SBC-3 rev 22 */
+            n += sg_scnpr(b + n, blen - n, "User data segment referral: ");
+            if (add_d_len < 2) {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+                break;
+            }
+            n += sg_scnpr(b + n, blen - n, "\n");
+            n += uds_referral_descriptor_str(b + n, blen - n, descp,
+                                             add_d_len, lip);
+            break;
+        case 0xc:       /* Added in SPC-4 rev 28 */
+            n += sg_scnpr(b + n, blen - n, "Forwarded sense data\n");
+            if (add_d_len < 2) {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+                break;
+            }
+            n += sg_scnpr(b + n, blen - n, "%s    FSDT: %s\n", lip,
+                          (descp[2] & 0x80) ? "set" : "clear");
+            j = descp[2] & 0xf;
+            n += sg_scnpr(b + n, blen - n, "%s    Sense data source: ", lip);
+            switch (j) {
+            case 0:
+                n += sg_scnpr(b + n, blen - n, "%s source device\n", eccp);
+                break;
+            case 1:
+            case 2:
+            case 3:
+            case 4:
+            case 5:
+            case 6:
+            case 7:
+                n += sg_scnpr(b + n, blen - n, "%s %s %d\n", eccp, ddp, j - 1);
+                break;
+            default:
+                n += sg_scnpr(b + n, blen - n, "unknown [%d]\n", j);
+            }
+            {
+                char c[480];
+
+                sg_get_scsi_status_str(descp[3], sizeof(c) - 1, c);
+                c[sizeof(c) - 1] = '\0';
+                n += sg_scnpr(b + n, blen - n, "%s    Forwarded status: %s\n",
+                              lip, c);
+                if (add_d_len > 2) {
+                    /* recursing; hope not to get carried away */
+                    n += sg_scnpr(b + n, blen - n, "%s vvvvvvvvvvvvvvvv\n",
+                                  lip);
+                    sg_get_sense_str(lip, descp + 4, add_d_len - 2, false,
+                                     sizeof(c), c);
+                    n += sg_scnpr(b + n, blen - n, "%s", c);
+                    n += sg_scnpr(b + n, blen - n, "%s ^^^^^^^^^^^^^^^^\n",
+                                  lip);
+                }
+            }
+            break;
+        case 0xd:       /* Added in SBC-3 rev 36d */
+            /* this descriptor combines descriptors 0, 1, 2 and 3 */
+            n += sg_scnpr(b + n, blen - n, "Direct-access block device\n");
+            if (add_d_len < 28) {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+                break;
+            }
+            if (0x20 & descp[2])
+                n += sg_scnpr(b + n, blen - n, "%s    ILI (incorrect length "
+                              "indication) set\n", lip);
+            if (0x80 & descp[4]) {
+                n += sg_scnpr(b + n, blen - n, "%s    Sense key specific: ",
+                              lip);
+                n += decode_sks(lip, descp, add_d_len, sense_key, &processed,
+                                blen - n, b + n);
+            }
+            n += sg_scnpr(b + n, blen - n, "%s    Field replaceable unit "
+                          "code: 0x%x\n", lip, descp[7]);
+            if (0x80 & descp[2]) {
+                n += sg_scnpr(b + n, blen - n, "%s    Information: 0x", lip);
+                for (j = 0; j < 8; ++j)
+                    n += sg_scnpr(b + n, blen - n, "%02x", descp[8 + j]);
+                n += sg_scnpr(b + n, blen - n, "\n");
+            }
+            n += sg_scnpr(b + n, blen - n, "%s    Command specific: 0x", lip);
+            for (j = 0; j < 8; ++j)
+                n += sg_scnpr(b + n, blen - n, "%02x", descp[16 + j]);
+            n += sg_scnpr(b + n, blen - n, "\n");
+            break;
+        case 0xe:       /* Added in SPC-5 rev 6 (for Bind/Unbind) */
+            n += sg_scnpr(b + n, blen - n, "Device designation\n");
+            j = (int)SG_ARRAY_SIZE(dd_usage_reason_str_arr);
+            if (descp[3] < j)
+                n += sg_scnpr(b + n, blen - n, "%s    Usage reason: %s\n",
+                              lip, dd_usage_reason_str_arr[descp[3]]);
+            else
+                n += sg_scnpr(b + n, blen - n, "%s    Usage reason: "
+                              "reserved[%d]\n", lip, descp[3]);
+            n += sg_get_designation_descriptor_str(z, descp + 4, descp[1] - 2,
+                                                   true, false, blen - n,
+                                                   b + n);
+            break;
+        case 0xf:       /* Added in SPC-5 rev 10 (for Write buffer) */
+            n += sg_scnpr(b + n, blen - n, "Microcode activation ");
+            if (add_d_len < 6) {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+                break;
+            }
+            progress = sg_get_unaligned_be16(descp + 6);
+            n += sg_scnpr(b + n, blen - n, "time: ");
+            if (0 == progress)
+                n += sg_scnpr(b + n, blen - n, "unknown\n");
+            else
+                n += sg_scnpr(b + n, blen - n, "%d seconds\n", progress);
+            break;
+        case 0xde:       /* NVME Status Field; vendor (sg3_utils) specific */
+            n += sg_scnpr(b + n, blen - n, "NVMe Status: ");
+            if (add_d_len < 6) {
+                n += sg_scnpr(b + n, blen - n, "%s\n", dtsp);
+                processed = false;
+                break;
+            }
+            n += sg_scnpr(b + n, blen - n, "DNR=%d, M=%d, ",
+                          (int)!!(0x80 & descp[5]), (int)!!(0x40 & descp[5]));
+            sct_sc = sg_get_unaligned_be16(descp + 6);
+            n += sg_scnpr(b + n, blen - n, "SCT_SC=0x%x\n", sct_sc);
+            if (sct_sc > 0) {
+                char d[80];
+
+                n += sg_scnpr(b + n, blen - n, "    %s\n",
+                        sg_get_nvme_cmd_status_str(sct_sc, sizeof(d), d));
+            }
+            break;
+        default:
+            if (descp[0] >= 0x80)
+                n += sg_scnpr(b + n, blen - n, "Vendor specific [0x%x]\n",
+                              descp[0]);
+            else
+                n += sg_scnpr(b + n, blen - n, "Unknown [0x%x]\n", descp[0]);
+            processed = false;
+            break;
+        }
+        if (! processed) {
+            if (add_d_len > 0) {
+                n += sg_scnpr(b + n, blen - n, "%s    ", lip);
+                for (j = 0; j < add_d_len; ++j) {
+                    if ((j > 0) && (0 == (j % 24)))
+                        n += sg_scnpr(b + n, blen - n, "\n%s    ", lip);
+                    n += sg_scnpr(b + n, blen - n, "%02x ", descp[j + 2]);
+                }
+                n += sg_scnpr(b + n, blen - n, "\n");
+            }
+        }
+        if (add_d_len < 0)
+            n += sg_scnpr(b + n, blen - n, "%s    short descriptor\n", lip);
+    }
+    return n;
+}
+
+/* Decode SAT ATA PASS-THROUGH fixed format sense. Shows "+" after 'count'
+ * and/or 'lba' values to indicate that not all data in those fields is shown.
+ * That extra field information may be available in the ATA pass-through
+ * results log page parameter with the corresponding 'log_index'. */
+static int
+sg_get_sense_sat_pt_fixed_str(const char * lip, const uint8_t * sp,
+                              int slen, int blen, char * b)
+{
+    int n = 0;
+    bool extend, count_upper_nz, lba_upper_nz;
+
+    if ((blen < 1) || (slen < 12))
+        return n;
+    if (NULL == lip)
+        lip = "";
+    if (SPC_SK_RECOVERED_ERROR != (0xf & sp[2]))
+        n += sg_scnpr(b + n, blen - n, "%s  >> expected Sense key: Recovered "
+                      "Error ??\n", lip);
+    /* Fixed sense command-specific information field starts at sp + 8 */
+    extend = !!(0x80 & sp[8]);
+    count_upper_nz = !!(0x40 & sp[8]);
+    lba_upper_nz = !!(0x20 & sp[8]);
+    /* Fixed sense information field starts at sp + 3 */
+    n += sg_scnpr(b + n, blen - n, "%s  error=0x%x, status=0x%x, "
+                  "device=0x%x, count(7:0)=0x%x%c\n", lip, sp[3], sp[4],
+                  sp[5], sp[6], (count_upper_nz ? '+' : ' '));
+    n += sg_scnpr(b + n, blen - n, "%s  extend=%d, log_index=0x%x, "
+                  "lba_high,mid,low(7:0)=0x%x,0x%x,0x%x%c\n", lip,
+                  (int)extend, (0xf & sp[8]), sp[11], sp[10], sp[9],
+                  (lba_upper_nz ? '+' : ' '));
+    return n;
+}
+
+/* Fetch sense information */
+int
+sg_get_sense_str(const char * lip, const uint8_t * sbp, int sb_len,
+                 bool raw_sinfo, int cblen, char * cbp)
+{
+    bool descriptor_format = false;
+    bool sdat_ovfl = false;
+    bool valid_info_fld;
+    int len, progress, n, r, pr, rem, blen;
+    unsigned int info;
+    uint8_t resp_code;
+    const char * ebp = NULL;
+    char ebuff[64];
+    char b[256];
+    struct sg_scsi_sense_hdr ssh;
+
+    if ((NULL == cbp) || (cblen <= 0))
+        return 0;
+    else if (1 == cblen) {
+        cbp[0] = '\0';
+        return 0;
+    }
+    blen = sizeof(b);
+    n = 0;
+    if (NULL == lip)
+        lip = "";
+    if ((NULL == sbp) || (sb_len < 1)) {
+        n += sg_scnpr(cbp, cblen, "%s >>> sense buffer empty\n", lip);
+        return n;
+    }
+    resp_code = 0x7f & sbp[0];
+    valid_info_fld = !!(sbp[0] & 0x80);
+    len = sb_len;
+    if (sg_scsi_normalize_sense(sbp, sb_len, &ssh)) {
+        switch (ssh.response_code) {
+        case 0x70:      /* fixed, current */
+            ebp = "Fixed format, current";
+            len = (sb_len > 7) ? (sbp[7] + 8) : sb_len;
+            len = (len > sb_len) ? sb_len : len;
+            sdat_ovfl = (len > 2) ? !!(sbp[2] & 0x10) : false;
+            break;
+        case 0x71:      /* fixed, deferred */
+            /* error related to a previous command */
+            ebp = "Fixed format, <<<deferred>>>";
+            len = (sb_len > 7) ? (sbp[7] + 8) : sb_len;
+            len = (len > sb_len) ? sb_len : len;
+            sdat_ovfl = (len > 2) ? !!(sbp[2] & 0x10) : false;
+            break;
+        case 0x72:      /* descriptor, current */
+            descriptor_format = true;
+            ebp = "Descriptor format, current";
+            sdat_ovfl = (sb_len > 4) ? !!(sbp[4] & 0x80) : false;
+            break;
+        case 0x73:      /* descriptor, deferred */
+            descriptor_format = true;
+            ebp = "Descriptor format, <<<deferred>>>";
+            sdat_ovfl = (sb_len > 4) ? !!(sbp[4] & 0x80) : false;
+            break;
+        case 0x0:
+            ebp = "Response code: 0x0 (?)";
+            break;
+        default:
+            sg_scnpr(ebuff, sizeof(ebuff), "Unknown response code: 0x%x",
+                     ssh.response_code);
+            ebp = ebuff;
+            break;
+        }
+        n += sg_scnpr(cbp + n, cblen - n, "%s%s; Sense key: %s\n", lip, ebp,
+                      sg_lib_sense_key_desc[ssh.sense_key]);
+        if (sdat_ovfl)
+            n += sg_scnpr(cbp + n, cblen - n, "%s<<<Sense data overflow "
+                          "(SDAT_OVFL)>>>\n", lip);
+        if (descriptor_format) {
+            n += sg_scnpr(cbp + n, cblen - n, "%s%s\n", lip,
+                          sg_get_asc_ascq_str(ssh.asc, ssh.ascq, blen, b));
+            n += sg_get_sense_descriptors_str(lip, sbp, len,
+                                              cblen - n, cbp + n);
+        } else if ((len > 12) && (0 == ssh.asc) &&
+                   (ASCQ_ATA_PT_INFO_AVAILABLE == ssh.ascq)) {
+            /* SAT ATA PASS-THROUGH fixed format */
+            n += sg_scnpr(cbp + n, cblen - n, "%s%s\n", lip,
+                          sg_get_asc_ascq_str(ssh.asc, ssh.ascq, blen, b));
+            n += sg_get_sense_sat_pt_fixed_str(lip, sbp, len,
+                                               cblen - n, cbp + n);
+        } else if (len > 2) {   /* fixed format */
+            if (len > 12)
+                n += sg_scnpr(cbp + n, cblen - n, "%s%s\n", lip,
+                         sg_get_asc_ascq_str(ssh.asc, ssh.ascq, blen, b));
+            r = 0;
+            if (strlen(lip) > 0)
+                r += sg_scnpr(b + r, blen - r, "%s", lip);
+            if (len > 6) {
+                info = sg_get_unaligned_be32(sbp + 3);
+                if (valid_info_fld)
+                    r += sg_scnpr(b + r, blen - r, "  Info fld=0x%x [%u] ",
+                                  info, info);
+                else if (info > 0)
+                    r += sg_scnpr(b + r, blen - r, "  Valid=0, Info fld=0x%x "
+                                  "[%u] ", info, info);
+            } else
+                info = 0;
+            if (sbp[2] & 0xe0) {
+                if (sbp[2] & 0x80)
+                   r += sg_scnpr(b + r, blen - r, " FMK");
+                            /* current command has read a filemark */
+                if (sbp[2] & 0x40)
+                   r += sg_scnpr(b + r, blen - r, " EOM");
+                            /* end-of-medium condition exists */
+                if (sbp[2] & 0x20)
+                   r += sg_scnpr(b + r, blen - r, " ILI");
+                            /* incorrect block length requested */
+                r += sg_scnpr(b + r, blen - r, "\n");
+            } else if (valid_info_fld || (info > 0))
+                r += sg_scnpr(b + r, blen - r, "\n");
+            if ((len >= 14) && sbp[14])
+                r += sg_scnpr(b + r, blen - r, "%s  Field replaceable unit "
+                              "code: %d\n", lip, sbp[14]);
+            if ((len >= 18) && (sbp[15] & 0x80)) {
+                /* sense key specific decoding */
+                switch (ssh.sense_key) {
+                case SPC_SK_ILLEGAL_REQUEST:
+                    r += sg_scnpr(b + r, blen - r, "%s  Sense Key Specific: "
+                                  "Error in %s: byte %d", lip,
+                                  ((sbp[15] & 0x40) ?
+                                         "Command" : "Data parameters"),
+                                  sg_get_unaligned_be16(sbp + 16));
+                    if (sbp[15] & 0x08)
+                        r += sg_scnpr(b + r, blen - r, " bit %d\n",
+                                      sbp[15] & 0x07);
+                    else
+                        r += sg_scnpr(b + r, blen - r, "\n");
+                    break;
+                case SPC_SK_NO_SENSE:
+                case SPC_SK_NOT_READY:
+                    progress = sg_get_unaligned_be16(sbp + 16);
+                    pr = (progress * 100) / 65536;
+                    rem = ((progress * 100) % 65536) / 656;
+                    r += sg_scnpr(b + r, blen - r, "%s  Progress indication: "
+                                  "%d.%02d%%\n", lip, pr, rem);
+                    break;
+                case SPC_SK_HARDWARE_ERROR:
+                case SPC_SK_MEDIUM_ERROR:
+                case SPC_SK_RECOVERED_ERROR:
+                    r += sg_scnpr(b + r, blen - r, "%s  Actual retry count: "
+                                  "0x%02x%02x\n", lip, sbp[16], sbp[17]);
+                    break;
+                case SPC_SK_COPY_ABORTED:
+                    r += sg_scnpr(b + r, blen - r, "%s  Segment pointer: ",
+                                  lip);
+                    r += sg_scnpr(b + r, blen - r, "Relative to start of %s, "
+                                  "byte %d", ((sbp[15] & 0x20) ?
+                                     "segment descriptor" : "parameter list"),
+                                  sg_get_unaligned_be16(sbp + 16));
+                    if (sbp[15] & 0x08)
+                        r += sg_scnpr(b + r, blen - r, " bit %d\n",
+                                      sbp[15] & 0x07);
+                    else
+                        r += sg_scnpr(b + r, blen - r, "\n");
+                    break;
+                case SPC_SK_UNIT_ATTENTION:
+                    r += sg_scnpr(b + r, blen - r, "%s  Unit attention "
+                                  "condition queue: ", lip);
+                    r += sg_scnpr(b + r, blen - r, "overflow flag is %d\n",
+                                  !!(sbp[15] & 0x1));
+                    break;
+                default:
+                    r += sg_scnpr(b + r, blen - r, "%s  Sense_key: 0x%x "
+                                  "unexpected\n", lip, ssh.sense_key);
+                    break;
+                }
+            }
+            if (r > 0)
+                n += sg_scnpr(cbp + n, cblen - n, "%s", b);
+        } else
+            n += sg_scnpr(cbp + n, cblen - n, "%s fixed descriptor length "
+                          "too short, len=%d\n", lip, len);
+    } else {    /* unable to normalise sense buffer, something irregular */
+        if (sb_len < 4) {       /* Too short */
+            n += sg_scnpr(cbp + n, cblen - n, "%ssense buffer too short (4 "
+                          "byte minimum)\n", lip);
+            goto check_raw;
+        }
+        if (0x7f == resp_code) {        /* Vendor specific */
+            n += sg_scnpr(cbp + n, cblen - n, "%sVendor specific sense "
+                          "buffer, in hex:\n", lip);
+            n += hex2str(sbp, sb_len, lip, -1, cblen - n, cbp + n);
+            return n;   /* no need to check raw, just output in hex */
+        }
+        /* non-extended SCSI-1 sense data ?? */
+        r = 0;
+        if (strlen(lip) > 0)
+            r += sg_scnpr(b + r, blen - r, "%s", lip);
+        r += sg_scnpr(b + r, blen - r, "Probably uninitialized data.\n%s  "
+                      "Try to view as SCSI-1 non-extended sense:\n", lip);
+        r += sg_scnpr(b + r, blen - r, "  AdValid=%d  Error class=%d  Error "
+                      "code=%d\n", valid_info_fld, ((sbp[0] >> 4) & 0x7),
+                      (sbp[0] & 0xf));
+        if (valid_info_fld)
+            sg_scnpr(b + r, blen - r, "%s  lba=0x%x\n", lip,
+                     sg_get_unaligned_be24(sbp + 1) & 0x1fffff);
+        n += sg_scnpr(cbp + n, cblen - n, "%s\n", b);
+    }
+check_raw:
+    if (raw_sinfo) {
+        int calculated_len;
+        char z[64];
+
+        n += sg_scnpr(cbp + n, cblen - n, "%s Raw sense data (in hex), "
+                      "sb_len=%d", lip, sb_len);
+        if (n >= (cblen - 1))
+            return n;
+        if ((sb_len > 7) && (sbp[0] >= 0x70) && (sbp[0] < 0x74)) {
+            calculated_len = sbp[7] + 8;
+            n += sg_scnpr(cbp + n, cblen - n, ", calculated_len=%d\n",
+                          calculated_len);
+        } else {
+            calculated_len = sb_len;
+            n += sg_scnpr(cbp + n, cblen - n, "\n");
+        }
+        if (n >= (cblen - 1))
+            return n;
+
+        sg_scnpr(z, sizeof(z), "%.50s        ", lip);
+        n += hex2str(sbp, calculated_len, z,  -1, cblen - n, cbp + n);
+    }
+    return n;
+}
+
+/* Print sense information */
+void
+sg_print_sense(const char * leadin, const uint8_t * sbp, int sb_len,
+               bool raw_sinfo)
+{
+    uint32_t pg_sz = sg_get_page_size();
+    char *cp;
+    uint8_t *free_cp;
+
+    cp = (char *)sg_memalign(pg_sz, pg_sz, &free_cp, false);
+    if (NULL == cp)
+        return;
+    sg_get_sense_str(leadin, sbp, sb_len, raw_sinfo, pg_sz, cp);
+    pr2ws("%s", cp);
+    free(free_cp);
+}
+
+/* This examines exit_status and if an error message is known it is output
+ * as a string to 'b' and true is returned. If 'longer' is true and extra
+ * information is available then it is added to the output. If no error
+ * message is available a null character is output and false is returned.
+ * If exit_status is zero (no error) and 'longer' is true then the string
+ * 'No errors' is output; if 'longer' is false then a null character is
+ * output; in both cases true is returned. If exit_status is negative then
+ * a null character is output and false is returned. All messages are a
+ * single line (less than 80 characters) with no trailing LF. The output
+ * string including the trailing null character is no longer than b_len.
+ * exit_status represents the Unix exit status available after a utility
+ * finishes executing (for whatever reason). */
+bool sg_exit2str(int exit_status, bool longer, int b_len, char *b)
+{
+    const struct sg_value_2names_t * ess = sg_exit_str_arr;
+
+    if ((b_len < 1) || (NULL == b))
+        return false;
+    /* if there is a valid buffer, initialize it to a valid empty string */
+    b[0] = '\0';
+    if (exit_status < 0)
+        return false;
+    else if ((0 == exit_status) || (SG_LIB_OK_FALSE == exit_status)) {
+        if (longer)
+            goto fini;
+        return true;
+    }
+
+    if ((exit_status > SG_LIB_OS_BASE_ERR) &&       /* 51 to 96 inclusive */
+        (exit_status < SG_LIB_CAT_MALFORMED)) {
+        snprintf(b, b_len, "%s%s", (longer ? "OS error: " : ""),
+                 safe_strerror(exit_status - SG_LIB_OS_BASE_ERR));
+        return true;
+    } else if ((exit_status > 128) && (exit_status < 255)) {
+        snprintf(b, b_len, "Utility stopped/aborted by signal number: %d",
+                 exit_status - 128);
+        return true;
+    }
+fini:
+    for ( ; ess->name; ++ess) {
+        if (exit_status == ess->value)
+            break;
+    }
+    if (ess->name) {
+        if (longer && ess->name2)
+            snprintf(b, b_len, "%s, %s", ess->name, ess->name2);
+        else
+            snprintf(b, b_len, "%s", ess->name);
+        return true;
+    }
+    return false;
+}
+
+static bool
+sg_if_can2fp(const char * leadin, int exit_status, FILE * fp)
+{
+    char b[256];
+    const char * s = leadin ? leadin : "";
+
+    if ((0 == exit_status) || (SG_LIB_OK_FALSE == exit_status))
+        return true;    /* don't print anything */
+    else if (sg_exit2str(exit_status, false, sizeof(b), b)) {
+        fprintf(fp, "%s%s\n", s, b);
+        return true;
+    } else
+        return false;
+}
+
+/* This examines exit_status and if an error message is known it is output
+ * to stdout/stderr and true is returned. If no error message is
+ * available nothing is output and false is returned. If exit_status is
+ * zero (no error) nothing is output and true is returned. If exit_status
+ * is negative then nothing is output and false is returned. If leadin is
+ * non-NULL then it is printed before the error message. All messages are
+ * a single line with a trailing LF. */
+bool
+sg_if_can2stdout(const char * leadin, int exit_status)
+{
+    return sg_if_can2fp(leadin, exit_status, stdout);
+}
+
+/* See sg_if_can2stdout() comments */
+bool
+sg_if_can2stderr(const char * leadin, int exit_status)
+{
+    return sg_if_can2fp(leadin, exit_status,
+                        sg_warnings_strm ? sg_warnings_strm : stderr);
+}
+
+/* If os_err_num is within bounds then the returned value is 'os_err_num +
+ * SG_LIB_OS_BASE_ERR' otherwise SG_LIB_OS_BASE_ERR is returned. If
+ * os_err_num is 0 then 0 is returned. */
+int
+sg_convert_errno(int os_err_num)
+{
+    if (os_err_num <= 0) {
+        if (os_err_num < 0)
+            return SG_LIB_OS_BASE_ERR;
+        return os_err_num;      /* os_err_num of 0 maps to 0 */
+    }
+    if (os_err_num < (SG_LIB_CAT_MALFORMED - SG_LIB_OS_BASE_ERR))
+        return SG_LIB_OS_BASE_ERR + os_err_num;
+    return SG_LIB_OS_BASE_ERR;
+}
+
+static const char * const bad_sense_cat = "Bad sense category";
+
+/* Yield string associated with sense category. Returns 'b' (or pointer
+ * to "Bad sense category" if 'b' is NULL). If sense_cat unknown then
+ * yield "Sense category: <sense_cat_val>" string. The original 'sense
+ * category' concept has been expanded to most detected errors and is
+ * returned by these utilities as their exit status value (an (unsigned)
+ * 8 bit value where 0 means good (i.e. no errors)).  Uses sg_exit2str()
+ * function. */
+const char *
+sg_get_category_sense_str(int sense_cat, int b_len, char * b, int verbose)
+{
+    if (NULL == b)
+        return bad_sense_cat;
+    if (b_len <= 0)
+        return b;
+    if (! sg_exit2str(sense_cat, (verbose > 0), b_len, b)) {
+        int n = sg_scnpr(b, b_len, "Sense category: %d", sense_cat);
+
+        if ((0 == verbose) && (n < (b_len - 1)))
+            sg_scnpr(b + n, b_len - n, ", try '-v' option for more "
+                     "information");
+    }
+    return b;   /* Note that a valid C string is returned in all cases */
+}
+
+/* See description in sg_lib.h header file */
+bool
+sg_scsi_normalize_sense(const uint8_t * sbp, int sb_len,
+                        struct sg_scsi_sense_hdr * sshp)
+{
+    uint8_t resp_code;
+    if (sshp)
+        memset(sshp, 0, sizeof(struct sg_scsi_sense_hdr));
+    if ((NULL == sbp) || (sb_len < 1))
+        return false;
+    resp_code = 0x7f & sbp[0];
+    if ((resp_code < 0x70) || (resp_code > 0x73))
+        return false;
+    if (sshp) {
+        sshp->response_code = resp_code;
+        if (sshp->response_code >= 0x72) {  /* descriptor format */
+            if (sb_len > 1)
+                sshp->sense_key = (0xf & sbp[1]);
+            if (sb_len > 2)
+                sshp->asc = sbp[2];
+            if (sb_len > 3)
+                sshp->ascq = sbp[3];
+            if (sb_len > 7)
+                sshp->additional_length = sbp[7];
+            sshp->byte4 = sbp[4];       /* bit 7: SDAT_OVFL bit */
+            /* sbp[5] and sbp[6] reserved for descriptor format */
+        } else {                              /* fixed format */
+            if (sb_len > 2)
+                sshp->sense_key = (0xf & sbp[2]);
+            if (sb_len > 7) {
+                sb_len = (sb_len < (sbp[7] + 8)) ? sb_len : (sbp[7] + 8);
+                if (sb_len > 12)
+                    sshp->asc = sbp[12];
+                if (sb_len > 13)
+                    sshp->ascq = sbp[13];
+            }
+            if (sb_len > 6) {   /* lower 3 bytes of INFO field */
+                sshp->byte4 = sbp[4];
+                sshp->byte5 = sbp[5];
+                sshp->byte6 = sbp[6];
+            }
+        }
+    }
+    return true;
+}
+
+/* Returns a SG_LIB_CAT_* value. If cannot decode sense buffer (sbp) or a
+ * less common sense key then return SG_LIB_CAT_SENSE .*/
+int
+sg_err_category_sense(const uint8_t * sbp, int sb_len)
+{
+    struct sg_scsi_sense_hdr ssh;
+
+    if ((sbp && (sb_len > 2)) &&
+        (sg_scsi_normalize_sense(sbp, sb_len, &ssh))) {
+        switch (ssh.sense_key) {        /* 0 to 0x1f */
+        case SPC_SK_NO_SENSE:
+            return SG_LIB_CAT_NO_SENSE;
+        case SPC_SK_RECOVERED_ERROR:
+            return SG_LIB_CAT_RECOVERED;
+        case SPC_SK_NOT_READY:
+            if ((0x04 == ssh.asc) && (0x0b == ssh.ascq))
+                return SG_LIB_CAT_STANDBY;
+            if ((0x04 == ssh.asc) && (0x0c == ssh.ascq))
+                return SG_LIB_CAT_UNAVAILABLE;
+            return SG_LIB_CAT_NOT_READY;
+        case SPC_SK_MEDIUM_ERROR:
+        case SPC_SK_HARDWARE_ERROR:
+        case SPC_SK_BLANK_CHECK:
+            return SG_LIB_CAT_MEDIUM_HARD;
+        case SPC_SK_UNIT_ATTENTION:
+            return SG_LIB_CAT_UNIT_ATTENTION;
+            /* used to return SG_LIB_CAT_MEDIA_CHANGED when ssh.asc==0x28 */
+        case SPC_SK_ILLEGAL_REQUEST:
+            if ((0x20 == ssh.asc) && (0x0 == ssh.ascq))
+                return SG_LIB_CAT_INVALID_OP;
+            else if ((0x21 == ssh.asc) && (0x0 == ssh.ascq))
+                return SG_LIB_LBA_OUT_OF_RANGE;
+            else
+                return SG_LIB_CAT_ILLEGAL_REQ;
+            break;
+        case SPC_SK_ABORTED_COMMAND:
+            if (0x10 == ssh.asc)
+                return SG_LIB_CAT_PROTECTION;
+            else
+                return SG_LIB_CAT_ABORTED_COMMAND;
+        case SPC_SK_MISCOMPARE:
+            return SG_LIB_CAT_MISCOMPARE;
+        case SPC_SK_DATA_PROTECT:
+            return SG_LIB_CAT_DATA_PROTECT;
+        case SPC_SK_COPY_ABORTED:
+            return SG_LIB_CAT_COPY_ABORTED;
+        case SPC_SK_COMPLETED:
+        case SPC_SK_VOLUME_OVERFLOW:
+            return SG_LIB_CAT_SENSE;
+        default:
+            ;   /* reserved and vendor specific sense keys fall through */
+        }
+    }
+    return SG_LIB_CAT_SENSE;
+}
+
+/* Beware: gives wrong answer for variable length command (opcode=0x7f) */
+int
+sg_get_command_size(uint8_t opcode)
+{
+    switch ((opcode >> 5) & 0x7) {
+    case 0:
+        return 6;
+    case 3: case 5:
+        return 12;
+    case 4:
+        return 16;
+    default:        /* 1, 2, 6, 7 */
+        return 10;
+    }
+}
+
+void
+sg_get_command_name(const uint8_t * cdbp, int peri_type, int buff_len,
+                    char * buff)
+{
+    int service_action;
+
+    if ((NULL == buff) || (buff_len < 1))
+        return;
+    else if (1 == buff_len) {
+        buff[0] = '\0';
+        return;
+    }
+    if (NULL == cdbp) {
+        sg_scnpr(buff, buff_len, "%s", "<null> command pointer");
+        return;
+    }
+    service_action = (SG_VARIABLE_LENGTH_CMD == cdbp[0]) ?
+                     sg_get_unaligned_be16(cdbp + 8) : (cdbp[1] & 0x1f);
+    sg_get_opcode_sa_name(cdbp[0], service_action, peri_type, buff_len, buff);
+}
+
+struct op_code2sa_t {
+    int op_code;
+    int pdt_s;
+    struct sg_lib_value_name_t * arr;
+    const char * prefix;
+};
+
+static struct op_code2sa_t op_code2sa_arr[] = {
+    {SG_VARIABLE_LENGTH_CMD, PDT_ALL, sg_lib_variable_length_arr, NULL},
+    {SG_MAINTENANCE_IN, PDT_ALL, sg_lib_maint_in_arr, NULL},
+    {SG_MAINTENANCE_OUT, PDT_ALL, sg_lib_maint_out_arr, NULL},
+    {SG_SERVICE_ACTION_IN_12, PDT_ALL, sg_lib_serv_in12_arr, NULL},
+    {SG_SERVICE_ACTION_OUT_12, PDT_ALL, sg_lib_serv_out12_arr, NULL},
+    {SG_SERVICE_ACTION_IN_16, PDT_ALL, sg_lib_serv_in16_arr, NULL},
+    {SG_SERVICE_ACTION_OUT_16, PDT_ALL, sg_lib_serv_out16_arr, NULL},
+    {SG_SERVICE_ACTION_BIDI, PDT_ALL, sg_lib_serv_bidi_arr, NULL},
+    {SG_PERSISTENT_RESERVE_IN, PDT_ALL, sg_lib_pr_in_arr,
+     "Persistent reserve in"},
+    {SG_PERSISTENT_RESERVE_OUT, PDT_ALL, sg_lib_pr_out_arr,
+     "Persistent reserve out"},
+    {SG_3PARTY_COPY_OUT, PDT_ALL, sg_lib_xcopy_sa_arr, NULL},
+    {SG_3PARTY_COPY_IN, PDT_ALL, sg_lib_rec_copy_sa_arr, NULL},
+    {SG_READ_BUFFER, PDT_ALL, sg_lib_read_buff_arr, "Read buffer(10)"},
+    {SG_READ_BUFFER_16, PDT_ALL, sg_lib_read_buff_arr, "Read buffer(16)"},
+    {SG_READ_ATTRIBUTE, PDT_ALL, sg_lib_read_attr_arr, "Read attribute"},
+    {SG_READ_POSITION, PDT_TAPE, sg_lib_read_pos_arr, "Read position"},
+    {SG_SANITIZE, PDT_DISK_ZBC, sg_lib_sanitize_sa_arr, "Sanitize"},
+    {SG_WRITE_BUFFER, PDT_ALL, sg_lib_write_buff_arr, "Write buffer"},
+    {SG_ZONING_IN, PDT_DISK_ZBC, sg_lib_zoning_in_arr, NULL},
+    {SG_ZONING_OUT, PDT_DISK_ZBC, sg_lib_zoning_out_arr, NULL},
+    {0xffff, -1, NULL, NULL},
+};
+
+void
+sg_get_opcode_sa_name(uint8_t cmd_byte0, int service_action,
+                      int peri_type, int buff_len, char * buff)
+{
+    int d_pdt;
+    const struct sg_lib_value_name_t * vnp;
+    const struct op_code2sa_t * osp;
+    char b[80];
+
+    if ((NULL == buff) || (buff_len < 1))
+        return;
+    else if (1 == buff_len) {
+        buff[0] = '\0';
+        return;
+    }
+
+    if (peri_type < 0)
+        peri_type = 0;
+    d_pdt = sg_lib_pdt_decay(peri_type);
+    for (osp = op_code2sa_arr; osp->arr; ++osp) {
+        if ((int)cmd_byte0 == osp->op_code) {
+            if (sg_pdt_s_eq(osp->pdt_s, d_pdt)) {
+                vnp = get_value_name(osp->arr, service_action, peri_type);
+                if (vnp) {
+                    if (osp->prefix)
+                        sg_scnpr(buff, buff_len, "%s, %s", osp->prefix,
+                                 vnp->name);
+                    else
+                        sg_scnpr(buff, buff_len, "%s", vnp->name);
+                } else {
+                    sg_get_opcode_name(cmd_byte0, peri_type, sizeof(b), b);
+                    sg_scnpr(buff, buff_len, "%s service action=0x%x", b,
+                             service_action);
+                }
+            } else
+                sg_get_opcode_name(cmd_byte0, peri_type, buff_len, buff);
+            return;
+        }
+    }
+    sg_get_opcode_name(cmd_byte0, peri_type, buff_len, buff);
+}
+
+void
+sg_get_opcode_name(uint8_t cmd_byte0, int peri_type, int buff_len,
+                   char * buff)
+{
+    const struct sg_lib_value_name_t * vnp;
+    int grp;
+
+    if ((NULL == buff) || (buff_len < 1))
+        return;
+    else if (1 == buff_len) {
+        buff[0] = '\0';
+        return;
+    }
+    if (SG_VARIABLE_LENGTH_CMD == cmd_byte0) {
+        sg_scnpr(buff, buff_len, "%s", "Variable length");
+        return;
+    }
+    grp = (cmd_byte0 >> 5) & 0x7;
+    switch (grp) {
+    case 0:
+    case 1:
+    case 2:
+    case 4:
+    case 5:
+        vnp = get_value_name(sg_lib_normal_opcodes, cmd_byte0, peri_type);
+        if (vnp)
+            sg_scnpr(buff, buff_len, "%s", vnp->name);
+        else
+            sg_scnpr(buff, buff_len, "Opcode=0x%x", (int)cmd_byte0);
+        break;
+    case 3:
+        sg_scnpr(buff, buff_len, "Reserved [0x%x]", (int)cmd_byte0);
+        break;
+    case 6:
+    case 7:
+        sg_scnpr(buff, buff_len, "Vendor specific [0x%x]", (int)cmd_byte0);
+        break;
+    }
+}
+
+/* Fetch NVMe command name given first byte (byte offset 0 in 64 byte
+ * command) of command. Gets Admin NVMe command name if 'admin' is true
+ * (e.g. opcode=0x6 -> Identify), otherwise gets NVM command set name
+ * (e.g. opcode=0 -> Flush). Returns 'buff'. */
+char *
+sg_get_nvme_opcode_name(uint8_t cmd_byte0, bool admin, int buff_len,
+                        char * buff)
+{
+    const struct sg_lib_simple_value_name_t * vnp = admin ?
+                        sg_lib_nvme_admin_cmd_arr : sg_lib_nvme_nvm_cmd_arr;
+
+    if ((NULL == buff) || (buff_len < 1))
+        return buff;
+    else if (1 == buff_len) {
+        buff[0] = '\0';
+        return buff;
+    }
+    for ( ; vnp->name; ++vnp) {
+        if (cmd_byte0 == (uint8_t)vnp->value) {
+            snprintf(buff, buff_len, "%s", vnp->name);
+            return buff;
+        }
+    }
+    if (admin) {
+        if (cmd_byte0 >= 0xc0)
+            snprintf(buff, buff_len, "Vendor specific opcode: 0x%x",
+                     cmd_byte0);
+        else if (cmd_byte0 >= 0x80)
+            snprintf(buff, buff_len, "Command set specific opcode: 0x%x",
+                     cmd_byte0);
+        else
+            snprintf(buff, buff_len, "Unknown opcode: 0x%x", cmd_byte0);
+    } else {    /* NVM (non-Admin) command set */
+        if (cmd_byte0 >= 0x80)
+            snprintf(buff, buff_len, "Vendor specific opcode: 0x%x",
+                     cmd_byte0);
+        else
+            snprintf(buff, buff_len, "Unknown opcode: 0x%x", cmd_byte0);
+    }
+    return buff;
+}
+
+/* Iterates to next designation descriptor in the device identification
+ * VPD page. The 'initial_desig_desc' should point to start of first
+ * descriptor with 'page_len' being the number of valid bytes in that
+ * and following descriptors. To start, 'off' should point to a negative
+ * value, thereafter it should point to the value yielded by the previous
+ * call. If 0 returned then 'initial_desig_desc + *off' should be a valid
+ * descriptor; returns -1 if normal end condition and -2 for an abnormal
+ * termination. Matches association, designator_type and/or code_set when
+ * any of those values are greater than or equal to zero. */
+int
+sg_vpd_dev_id_iter(const uint8_t * initial_desig_desc, int page_len,
+                   int * off, int m_assoc, int m_desig_type, int m_code_set)
+{
+    bool fltr = ((m_assoc >= 0) || (m_desig_type >= 0) || (m_code_set >= 0));
+    int k = *off;
+    const uint8_t * bp = initial_desig_desc;
+
+    while ((k + 3) < page_len) {
+        k = (k < 0) ? 0 : (k + bp[k + 3] + 4);
+        if ((k + 4) > page_len)
+            break;
+        if (fltr) {
+            if (m_code_set >= 0) {
+                if ((bp[k] & 0xf) != m_code_set)
+                    continue;
+            }
+            if (m_assoc >= 0) {
+                if (((bp[k + 1] >> 4) & 0x3) != m_assoc)
+                    continue;
+            }
+            if (m_desig_type >= 0) {
+                if ((bp[k + 1] & 0xf) != m_desig_type)
+                    continue;
+            }
+        }
+        *off = k;
+        return 0;
+    }
+    return (k == page_len) ? -1 : -2;
+}
+
+static const char * sg_sfs_spc_reserved = "SPC Reserved";
+static const char * sg_sfs_sbc_reserved = "SBC Reserved";
+static const char * sg_sfs_ssc_reserved = "SSC Reserved";
+static const char * sg_sfs_zbc_reserved = "ZBC Reserved";
+static const char * sg_sfs_reserved = "Reserved";
+
+/* Yield SCSI Feature Set (sfs) string. When 'peri_type' is < -1 (or > 31)
+ * returns pointer to string (same as 'buff') associated with 'sfs_code'.
+ * When 'peri_type' is between -1 (for SPC) and 31 (inclusive) then a match
+ * on both 'sfs_code' and 'peri_type' is required. If 'foundp' is not NULL
+ * then where it points is set to true if a match is found else it is set to
+ * false. If 'buff' is not NULL then in the case of a match a descriptive
+ * string is written to 'buff' while if there is not a not then a string
+ * ending in "Reserved" is written (and may be prefixed with SPC, SBC, SSC
+ * or ZBC). Returns 'buff' (i.e. a pointer value) even if it is NULL.
+ * Example:
+ *    char b[64];
+ *    ...
+ *    printf("%s\n", sg_get_sfs_str(sfs_code, -2, sizeof(b), b, NULL, 0));
+ */
+const char *
+sg_get_sfs_str(uint16_t sfs_code, int peri_type, int buff_len, char * buff,
+               bool * foundp, int verbose)
+{
+    const struct sg_lib_value_name_t * vnp = NULL;
+    int n = 0;
+    int my_pdt;
+
+    if ((NULL == buff) || (buff_len < 1)) {
+        if (foundp)
+            *foundp = false;
+        return NULL;
+    } else if (1 == buff_len) {
+        buff[0] = '\0';
+        if (foundp)
+            *foundp = false;
+        return NULL;
+    }
+    my_pdt = ((peri_type < -1) || (peri_type > PDT_MAX)) ? -2 : peri_type;
+    vnp = get_value_name(sg_lib_scsi_feature_sets, sfs_code, my_pdt);
+    if (vnp && (-2 != my_pdt)) {
+        if (! sg_pdt_s_eq(my_pdt, vnp->peri_dev_type))
+            vnp = NULL;      /* shouldn't really happen */
+    }
+    if (foundp)
+        *foundp = vnp ? true : false;
+    if (sfs_code < 0x100) {             /* SPC Feature Sets */
+        if (vnp) {
+            if (verbose)
+                n += sg_scnpr(buff, buff_len, "SPC %s", vnp->name);
+            else
+                n += sg_scnpr(buff, buff_len, "%s", vnp->name);
+        } else
+            n += sg_scnpr(buff, buff_len, "%s", sg_sfs_spc_reserved);
+    } else if (sfs_code < 0x200) {      /* SBC Feature Sets */
+        if (vnp) {
+            if (verbose)
+                n += sg_scnpr(buff, buff_len, "SBC %s", vnp->name);
+            else
+                n += sg_scnpr(buff, buff_len, "%s", vnp->name);
+        } else
+            n += sg_scnpr(buff, buff_len, "%s", sg_sfs_sbc_reserved);
+    } else if (sfs_code < 0x300) {      /* SSC Feature Sets */
+        if (vnp) {
+            if (verbose)
+                n += sg_scnpr(buff, buff_len, "SSC %s", vnp->name);
+            else
+                n += sg_scnpr(buff, buff_len, "%s", vnp->name);
+        } else
+            n += sg_scnpr(buff, buff_len, "%s", sg_sfs_ssc_reserved);
+    } else if (sfs_code < 0x400) {      /* ZBC Feature Sets */
+        if (vnp) {
+            if (verbose)
+                n += sg_scnpr(buff, buff_len, "ZBC %s", vnp->name);
+            else
+                n += sg_scnpr(buff, buff_len, "%s", vnp->name);
+        } else
+            n += sg_scnpr(buff, buff_len, "%s", sg_sfs_zbc_reserved);
+    } else {                            /* Other SCSI Feature Sets */
+        if (vnp) {
+            if (verbose)
+                n += sg_scnpr(buff, buff_len, "[unrecognized PDT] %s",
+                              vnp->name);
+            else
+                n += sg_scnpr(buff, buff_len, "%s", vnp->name);
+        } else
+            n += sg_scnpr(buff, buff_len, "%s", sg_sfs_reserved);
+
+    }
+    if (verbose > 4)
+        pr2ws("%s: length of returned string (n) %d\n", __func__, n);
+    return buff;
+}
+
+/* This is a heuristic that takes into account the command bytes and length
+ * to decide whether the presented unstructured sequence of bytes could be
+ * a SCSI command. If so it returns true otherwise false. Vendor specific
+ * SCSI commands (i.e. opcodes from 0xc0 to 0xff), if presented, are assumed
+ * to follow SCSI conventions (i.e. length of 6, 10, 12 or 16 bytes). The
+ * only SCSI commands considered above 16 bytes of length are the Variable
+ * Length Commands (opcode 0x7f) and the XCDB wrapped commands (opcode 0x7e).
+ * Both have an inbuilt length field which can be cross checked with clen.
+ * No NVMe commands (64 bytes long plus some extra added by some OSes) have
+ * opcodes 0x7e or 0x7f yet. ATA is register based but SATA has FIS
+ * structures that are sent across the wire. The FIS register structure is
+ * used to move a command from a SATA host to device, but the ATA 'command'
+ * is not the first byte. So it is harder to say what will happen if a
+ * FIS structure is presented as a SCSI command, hopefully there is a low
+ * probability this function will yield true in that case. */
+bool
+sg_is_scsi_cdb(const uint8_t * cdbp, int clen)
+{
+    uint8_t opcode;
+    uint8_t top3bits;
+
+    if (clen < 6)
+        return false;
+    opcode = cdbp[0];
+    top3bits = opcode >> 5;
+    if (0x3 == top3bits) {
+        int ilen, sa;
+
+        if ((clen < 12) || (clen % 4))
+            return false;       /* must be modulo 4 and 12 or more bytes */
+        switch (opcode) {
+        case 0x7e:      /* Extended cdb (XCDB) */
+            ilen = 4 + sg_get_unaligned_be16(cdbp + 2);
+            return (ilen == clen);
+        case 0x7f:      /* Variable Length cdb */
+            ilen = 8 + cdbp[7];
+            sa = sg_get_unaligned_be16(cdbp + 8);
+            /* service action (sa) 0x0 is reserved */
+            return ((ilen == clen) && sa);
+        default:
+            return false;
+        }
+    } else if (clen <= 16) {
+        switch (clen) {
+        case 6:
+            if (top3bits > 0x5)         /* vendor */
+                return true;
+            return (0x0 == top3bits);   /* 6 byte cdb */
+        case 10:
+            if (top3bits > 0x5)         /* vendor */
+                return true;
+            return ((0x1 == top3bits) || (0x2 == top3bits)); /* 10 byte cdb */
+        case 16:
+            if (top3bits > 0x5)         /* vendor */
+                return true;
+            return (0x4 == top3bits);   /* 16 byte cdb */
+        case 12:
+            if (top3bits > 0x5)         /* vendor */
+                return true;
+            return (0x5 == top3bits);   /* 12 byte cdb */
+        default:
+            return false;
+        }
+    }
+    /* NVMe probably falls out here, clen > 16 and (opcode < 0x60 or
+     * opcode > 0x7f). */
+    return false;
+}
+
+/* Yield string associated with NVMe command status value in sct_sc. It
+ * expects to decode DW3 bits 27:17 from the completion queue. Bits 27:25
+ * are the Status Code Type (SCT) and bits 24:17 are the Status Code (SC).
+ * Bit 17 in DW3 should be bit 0 in sct_sc. If no status string is found
+ * a string of the form "Reserved [0x<sct_sc_in_hex>]" is generated.
+ * Returns 'buff'. Does nothing if buff_len<=0 or if buff is NULL.*/
+char *
+sg_get_nvme_cmd_status_str(uint16_t sct_sc, int b_len, char * b)
+{
+    int k;
+    uint16_t s = 0x3ff & sct_sc;
+    const struct sg_lib_value_name_t * vp = sg_lib_nvme_cmd_status_arr;
+
+    if ((b_len <= 0) || (NULL == b))
+        return b;
+    else if (1 == b_len) {
+        b[0] = '\0';
+        return b;
+    }
+    for (k = 0; (vp->name && (k < 1000)); ++k, ++vp) {
+        if (s == (uint16_t)vp->value) {
+            strncpy(b, vp->name, b_len);
+            b[b_len - 1] = '\0';
+            return b;
+        }
+    }
+    if (k >= 1000)
+        pr2ws("%s: where is sentinel for sg_lib_nvme_cmd_status_arr ??\n",
+                        __func__);
+    snprintf(b, b_len, "Reserved [0x%x]", sct_sc);
+    return b;
+}
+
+/* Attempts to map NVMe status value ((SCT << 8) | SC) to SCSI status,
+ * sense_key, asc and ascq tuple. If successful returns true and writes to
+ * non-NULL pointer arguments; otherwise returns false. */
+bool
+sg_nvme_status2scsi(uint16_t sct_sc, uint8_t * status_p, uint8_t * sk_p,
+                    uint8_t * asc_p, uint8_t * ascq_p)
+{
+    int k, ind;
+    uint16_t s = 0x3ff & sct_sc;
+    struct sg_lib_value_name_t * vp = sg_lib_nvme_cmd_status_arr;
+    struct sg_lib_4tuple_u8 * mp = sg_lib_scsi_status_sense_arr;
+
+    for (k = 0; (vp->name && (k < 1000)); ++k, ++vp) {
+        if (s == (uint16_t)vp->value)
+            break;
+    }
+    if (k >= 1000) {
+        pr2ws("%s: where is sentinel for sg_lib_nvme_cmd_status_arr ??\n",
+              __func__);
+        return false;
+    }
+    if (NULL == vp->name)
+        return false;
+    ind = vp->peri_dev_type;
+
+
+    for (k = 0; (0xff != mp->t2) && k < 1000; ++k, ++mp)
+        ;       /* count entries for valid index range */
+    if (k >= 1000) {
+        pr2ws("%s: where is sentinel for sg_lib_scsi_status_sense_arr ??\n",
+              __func__);
+        return false;
+    } else if (ind >= k)
+        return false;
+
+    mp = sg_lib_scsi_status_sense_arr + ind;
+    if (status_p)
+        *status_p = mp->t1;
+    if (sk_p)
+        *sk_p = mp->t2;
+    if (asc_p)
+        *asc_p = mp->t3;
+    if (ascq_p)
+        *ascq_p = mp->t4;
+    return true;
+}
+
+/* Add vendor (sg3_utils) specific sense descriptor for the NVMe Status
+ * field. Assumes descriptor (i.e. not fixed) sense. Assumes sbp has room. */
+void
+sg_nvme_desc2sense(uint8_t * sbp, bool dnr, bool more, uint16_t sct_sc)
+{
+    int len = sbp[7] + 8;
+
+    sbp[len] = 0xde;            /* vendor specific descriptor type */
+    sbp[len + 1] = 6;           /* descriptor is 8 bytes long */
+    memset(sbp + len + 2, 0, 6);
+    if (dnr)
+        sbp[len + 5] = 0x80;
+    if (more)
+        sbp[len + 5] |= 0x40;
+    sg_put_unaligned_be16(sct_sc, sbp + len + 6);
+    sbp[7] += 8;
+}
+
+/* Build minimum sense buffer, either descriptor type (desc=true) or fixed
+ * type (desc=false). Assume sbp has enough room (8 or 14 bytes
+ * respectively). sbp should have room for 32 or 18 bytes respectively */
+void
+sg_build_sense_buffer(bool desc, uint8_t *sbp, uint8_t skey, uint8_t asc,
+                      uint8_t ascq)
+{
+    if (desc) {
+        sbp[0] = 0x72;  /* descriptor, current */
+        sbp[1] = skey;
+        sbp[2] = asc;
+        sbp[3] = ascq;
+        sbp[7] = 0;
+    } else {
+        sbp[0] = 0x70;  /* fixed, current */
+        sbp[2] = skey;
+        sbp[7] = 0xa;   /* Assumes length is 18 bytes */
+        sbp[12] = asc;
+        sbp[13] = ascq;
+    }
+}
+
+/* safe_strerror() contributed by Clayton Weaver <cgweav at email dot com>
+ * Allows for situation in which strerror() is given a wild value (or the
+ * C library is incomplete) and returns NULL. Still not thread safe.
+ */
+
+static char safe_errbuf[64] = {'u', 'n', 'k', 'n', 'o', 'w', 'n', ' ',
+                               'e', 'r', 'r', 'n', 'o', ':', ' ', 0};
+
+char *
+safe_strerror(int errnum)
+{
+    char * errstr;
+
+    if (errnum < 0)
+        errnum = -errnum;
+    errstr = strerror(errnum);
+    if (NULL == errstr) {
+        size_t len = strlen(safe_errbuf);
+
+        sg_scnpr(safe_errbuf + len, sizeof(safe_errbuf) - len, "%i", errnum);
+        return safe_errbuf;
+    }
+    return errstr;
+}
+
+static int
+trimTrailingSpaces(char * b)
+{
+    int n = strlen(b);
+
+    while ((n > 0) && (' ' == b[n - 1]))
+        b[--n] = '\0';
+    return n;
+}
+
+/* Read binary starting at 'str' for 'len' bytes and output as ASCII
+ * hexadecinal into file pointer (fp). 16 bytes per line are output with an
+ * additional space between 8th and 9th byte on each line (for readability).
+ * 'no_ascii' selects one of 3 output format types:
+ *     > 0     each line has address then up to 16 ASCII-hex bytes
+ *     = 0     in addition, the bytes are listed in ASCII to the right
+ *     < 0     only the ASCII-hex bytes are listed (i.e. without address) */
+void
+dStrHexFp(const char* str, int len, int no_ascii, FILE * fp)
+{
+    const char * p = str;
+    const char * formatstr;
+    uint8_t c;
+    char buff[82];
+    int a = 0;
+    int bpstart = 5;
+    const int cpstart = 60;
+    int cpos = cpstart;
+    int bpos = bpstart;
+    int i, k, blen;
+
+    if (len <= 0)
+        return;
+    blen = (int)sizeof(buff);
+    if (0 == no_ascii)  /* address at left and ASCII at right */
+        formatstr = "%.76s\n";
+    else                        /* previously when > 0 str was "%.58s\n" */
+        formatstr = "%s\n";     /* when < 0 str was: "%.48s\n" */
+    memset(buff, ' ', 80);
+    buff[80] = '\0';
+    if (no_ascii < 0) {
+        bpstart = 0;
+        bpos = bpstart;
+        for (k = 0; k < len; k++) {
+            c = *p++;
+            if (bpos == (bpstart + (8 * 3)))
+                bpos++;
+            sg_scnpr(&buff[bpos], blen - bpos, "%.2x", (int)(uint8_t)c);
+            buff[bpos + 2] = ' ';
+            if ((k > 0) && (0 == ((k + 1) % 16))) {
+                trimTrailingSpaces(buff);
+                fprintf(fp, formatstr, buff);
+                bpos = bpstart;
+                memset(buff, ' ', 80);
+            } else
+                bpos += 3;
+        }
+        if (bpos > bpstart) {
+            buff[bpos + 2] = '\0';
+            trimTrailingSpaces(buff);
+            fprintf(fp, "%s\n", buff);
+        }
+        return;
+    }
+    /* no_ascii>=0, start each line with address (offset) */
+    k = sg_scnpr(buff + 1, blen - 1, "%.2x", a);
+    buff[k + 1] = ' ';
+
+    for (i = 0; i < len; i++) {
+        c = *p++;
+        bpos += 3;
+        if (bpos == (bpstart + (9 * 3)))
+            bpos++;
+        sg_scnpr(&buff[bpos], blen - bpos, "%.2x", (int)(uint8_t)c);
+        buff[bpos + 2] = ' ';
+        if (no_ascii)
+            buff[cpos++] = ' ';
+        else {
+            if (! my_isprint(c))
+                c = '.';
+            buff[cpos++] = c;
+        }
+        if (cpos > (cpstart + 15)) {
+            if (no_ascii)
+                trimTrailingSpaces(buff);
+            fprintf(fp, formatstr, buff);
+            bpos = bpstart;
+            cpos = cpstart;
+            a += 16;
+            memset(buff, ' ', 80);
+            k = sg_scnpr(buff + 1, blen - 1, "%.2x", a);
+            buff[k + 1] = ' ';
+        }
+    }
+    if (cpos > cpstart) {
+        buff[cpos] = '\0';
+        if (no_ascii)
+            trimTrailingSpaces(buff);
+        fprintf(fp, "%s\n", buff);
+    }
+}
+
+void
+dStrHex(const char* str, int len, int no_ascii)
+{
+    dStrHexFp(str, len, no_ascii, stdout);
+}
+
+void
+dStrHexErr(const char* str, int len, int no_ascii)
+{
+    dStrHexFp(str, len, no_ascii,
+              (sg_warnings_strm ? sg_warnings_strm : stderr));
+}
+
+#define DSHS_LINE_BLEN 160      /* maximum characters per line */
+#define DSHS_BPL 16             /* bytes per line */
+
+/* Read 'len' bytes from 'str' and output as ASCII-Hex bytes (space separated)
+ * to 'b' not to exceed 'b_len' characters. Each line starts with 'leadin'
+ * (NULL for no leadin) and there are 16 bytes per line with an extra space
+ * between the 8th and 9th bytes. 'oformat' is 0 for repeat in printable ASCII
+ * ('.' for non printable chars) to right of each line; 1 don't (so just
+ * output ASCII hex). If 'oformat' is 2 output same as 1 but any LFs are
+ * replaced by space (and trailing spaces are trimmed). Note that an address
+ * is not printed on each line preceding the hex data. Returns number of bytes
+ * written to 'b' excluding the trailing '\0'. The only difference between
+ * dStrHexStr() and hex2str() is the type of the first argument. */
+int
+dStrHexStr(const char * str, int len, const char * leadin, int oformat,
+           int b_len, char * b)
+{
+    bool want_ascii = (0 == oformat);
+    int bpstart, bpos, k, n, prior_ascii_len;
+    char buff[DSHS_LINE_BLEN + 2];      /* allow for trailing null */
+    char a[DSHS_BPL + 1];               /* printable ASCII bytes or '.' */
+    const char * p = str;
+    const char * lf_or = (oformat > 1) ? "  " : "\n";
+
+    if (len <= 0) {
+        if (b_len > 0)
+            b[0] = '\0';
+        return 0;
+    }
+    if (b_len <= 0)
+        return 0;
+    if (want_ascii) {
+        memset(a, ' ', DSHS_BPL);
+        a[DSHS_BPL] = '\0';
+    }
+    n = 0;
+    bpstart = 0;
+    if (leadin) {
+        if (oformat > 1)
+            n += sg_scnpr(b + n, b_len - n, "%s", leadin);
+        else {
+            bpstart = strlen(leadin);
+            /* Cap leadin at (DSHS_LINE_BLEN - 70) characters */
+            if (bpstart > (DSHS_LINE_BLEN - 70))
+                bpstart = DSHS_LINE_BLEN - 70;
+        }
+    }
+    bpos = bpstart;
+    prior_ascii_len = bpstart + (DSHS_BPL * 3) + 1;
+    memset(buff, ' ', DSHS_LINE_BLEN);
+    buff[DSHS_LINE_BLEN] = '\0';
+    if (bpstart > 0)
+        memcpy(buff, leadin, bpstart);
+    for (k = 0; k < len; k++) {
+        uint8_t c = *p++;
+
+        if (bpos == (bpstart + ((DSHS_BPL / 2) * 3)))
+            bpos++;     /* for extra space in middle of each line's hex */
+        sg_scnpr(buff + bpos, (int)sizeof(buff) - bpos, "%.2x",
+                 (int)(uint8_t)c);
+        buff[bpos + 2] = ' ';
+        if (want_ascii)
+            a[k % DSHS_BPL] = my_isprint(c) ? c : '.';
+        if ((k > 0) && (0 == ((k + 1) % DSHS_BPL))) {
+            trimTrailingSpaces(buff);
+            if (want_ascii) {
+                n += sg_scnpr(b + n, b_len - n, "%-*s   %s\n",
+                              prior_ascii_len, buff, a);
+                memset(a, ' ', DSHS_BPL);
+            } else
+                n += sg_scnpr(b + n, b_len - n, "%s%s", buff, lf_or);
+            if (n >= (b_len - 1))
+                goto fini;
+            memset(buff, ' ', DSHS_LINE_BLEN);
+            bpos = bpstart;
+            if (bpstart > 0)
+                memcpy(buff, leadin, bpstart);
+        } else
+            bpos += 3;
+    }
+    if (bpos > bpstart) {
+        trimTrailingSpaces(buff);
+        if (want_ascii)
+            n += sg_scnpr(b + n, b_len - n, "%-*s   %s\n", prior_ascii_len,
+                          buff, a);
+        else
+            n += sg_scnpr(b + n, b_len - n, "%s%s", buff, lf_or);
+    }
+fini:
+    if (oformat > 1)
+        n = trimTrailingSpaces(b);
+    return n;
+}
+
+void
+hex2stdout(const uint8_t * b_str, int len, int no_ascii)
+{
+    dStrHex((const char *)b_str, len, no_ascii);
+}
+
+void
+hex2stderr(const uint8_t * b_str, int len, int no_ascii)
+{
+    dStrHexErr((const char *)b_str, len, no_ascii);
+}
+
+int
+hex2str(const uint8_t * b_str, int len, const char * leadin, int oformat,
+        int b_len, char * b)
+{
+    return dStrHexStr((const char *)b_str, len, leadin, oformat, b_len, b);
+}
+
+void
+hex2fp(const uint8_t * b_str, int len, const char * leadin, int oformat,
+       FILE * fp)
+{
+    int k, num;
+    char b[800];        /* allow for 4 lines of 16 bytes (in hex) each */
+
+    if (leadin && (strlen(leadin) > 118)) {
+        fprintf(fp, ">>> leadin parameter is too large\n");
+        return;
+    }
+    for (k = 0; k < len; k += num) {
+        num = ((k + 64) < len) ? 64 : (len - k);
+        hex2str(b_str + k, num, leadin, oformat, sizeof(b), b);
+        fprintf(fp, "%s", b);
+    }
+}
+
+/* Returns true when executed on big endian machine; else returns false.
+ * Useful for displaying ATA identify words (which need swapping on a
+ * big endian machine). */
+bool
+sg_is_big_endian()
+{
+    union u_t {
+        uint16_t s;
+        uint8_t c[sizeof(uint16_t)];
+    } u;
+
+    u.s = 0x0102;
+    return (u.c[0] == 0x01);     /* The lowest address contains
+                                    the most significant byte */
+}
+
+bool
+sg_all_zeros(const uint8_t * bp, int b_len)
+{
+    if ((NULL == bp) || (b_len <= 0))
+        return false;
+    for (--b_len; b_len >= 0; --b_len) {
+        if (0x0 != bp[b_len])
+            return false;
+    }
+    return true;
+}
+
+bool
+sg_all_ffs(const uint8_t * bp, int b_len)
+{
+    if ((NULL == bp) || (b_len <= 0))
+        return false;
+    for (--b_len; b_len >= 0; --b_len) {
+        if (0xff != bp[b_len])
+            return false;
+    }
+    return true;
+}
+
+static uint16_t
+swapb_uint16(uint16_t u)
+{
+    uint16_t r;
+
+    r = (u >> 8) & 0xff;
+    r |= ((u & 0xff) << 8);
+    return r;
+}
+
+/* Note the ASCII-hex output goes to stdout. [Most other output from functions
+ * in this file go to sg_warnings_strm (default stderr).]
+ * 'no_ascii' allows for 3 output types:
+ *     > 0     each line has address then up to 8 ASCII-hex 16 bit words
+ *     = 0     in addition, the ASCI bytes pairs are listed to the right
+ *     = -1    only the ASCII-hex words are listed (i.e. without address)
+ *     = -2    only the ASCII-hex words, formatted for "hdparm --Istdin"
+ *     < -2    same as -1
+ * If 'swapb' is true then bytes in each word swapped. Needs to be set
+ * for ATA IDENTIFY DEVICE response on big-endian machines. */
+void
+dWordHex(const uint16_t* words, int num, int no_ascii, bool swapb)
+{
+    const uint16_t * p = words;
+    uint16_t c;
+    char buff[82];
+    uint8_t upp, low;
+    int a = 0;
+    const int bpstart = 3;
+    const int cpstart = 52;
+    int cpos = cpstart;
+    int bpos = bpstart;
+    int i, k, blen;
+
+    if (num <= 0)
+        return;
+    blen = (int)sizeof(buff);
+    memset(buff, ' ', 80);
+    buff[80] = '\0';
+    if (no_ascii < 0) {
+        for (k = 0; k < num; k++) {
+            c = *p++;
+            if (swapb)
+                c = swapb_uint16(c);
+            bpos += 5;
+            sg_scnpr(buff + bpos, blen - bpos, "%.4x", (my_uint)c);
+            buff[bpos + 4] = ' ';
+            if ((k > 0) && (0 == ((k + 1) % 8))) {
+                if (-2 == no_ascii)
+                    printf("%.39s\n", buff +8);
+                else
+                    printf("%.47s\n", buff);
+                bpos = bpstart;
+                memset(buff, ' ', 80);
+            }
+        }
+        if (bpos > bpstart) {
+            if (-2 == no_ascii)
+                printf("%.39s\n", buff +8);
+            else
+                printf("%.47s\n", buff);
+        }
+        return;
+    }
+    /* no_ascii>=0, start each line with address (offset) */
+    k = sg_scnpr(buff + 1, blen - 1, "%.2x", a);
+    buff[k + 1] = ' ';
+
+    for (i = 0; i < num; i++) {
+        c = *p++;
+        if (swapb)
+            c = swapb_uint16(c);
+        bpos += 5;
+        sg_scnpr(buff + bpos, blen - bpos, "%.4x", (my_uint)c);
+        buff[bpos + 4] = ' ';
+        if (no_ascii) {
+            buff[cpos++] = ' ';
+            buff[cpos++] = ' ';
+            buff[cpos++] = ' ';
+        } else {
+            upp = (c >> 8) & 0xff;
+            low = c & 0xff;
+            if (! my_isprint(upp))
+                upp = '.';
+            buff[cpos++] = upp;
+            if (! my_isprint(low))
+                low = '.';
+            buff[cpos++] = low;
+            buff[cpos++] = ' ';
+        }
+        if (cpos > (cpstart + 23)) {
+            printf("%.76s\n", buff);
+            bpos = bpstart;
+            cpos = cpstart;
+            a += 8;
+            memset(buff, ' ', 80);
+            k = sg_scnpr(buff + 1, blen - 1, "%.2x", a);
+            buff[k + 1] = ' ';
+        }
+    }
+    if (cpos > cpstart)
+        printf("%.76s\n", buff);
+}
+
+/* If the number in 'buf' can not be decoded or the multiplier is unknown
+ * then -1 is returned. Accepts a hex prefix (0x or 0X) or a decimal
+ * multiplier suffix (as per GNU's dd (since 2002: SI and IEC 60027-2)).
+ * Main (SI) multipliers supported: K, M, G. Ignore leading spaces and
+ * tabs; accept comma, hyphen, space, tab and hash as terminator.
+ * Handles zero and positive values up to 2**31-1 .
+ * Experimental: left argument (must in with hexadecimal digit) added
+ * to, or multiplied, by right argument. No embedded spaces.
+ * Examples: '3+1k' (evaluates to 1027) and '0x34+1m'. */
+int
+sg_get_num(const char * buf)
+{
+    bool is_hex = false;
+    int res, num, n, len;
+    unsigned int unum;
+    char * cp;
+    const char * b;
+    const char * b2p;
+    char c = 'c';
+    char c2 = '\0';     /* keep static checker happy */
+    char c3 = '\0';     /* keep static checker happy */
+    char lb[16];
+
+    if ((NULL == buf) || ('\0' == buf[0]))
+        return -1;
+    len = strlen(buf);
+    n = strspn(buf, " \t");
+    if (n > 0) {
+        if (n == len)
+            return -1;
+        buf += n;
+        len -= n;
+    }
+    /* following hack to keep C++ happy */
+    cp = strpbrk((char *)buf, " \t,#-");
+    if (cp) {
+        len = cp - buf;
+        n = (int)sizeof(lb) - 1;
+        len = (len < n) ? len : n;
+        memcpy(lb, buf, len);
+        lb[len] = '\0';
+        b = lb;
+    } else
+        b = buf;
+
+    b2p = b;
+    if (('0' == b[0]) && (('x' == b[1]) || ('X' == b[1]))) {
+        res = sscanf(b + 2, "%x%c", &unum, &c);
+        num = unum;
+        is_hex = true;
+        b2p = b + 2;
+    } else if ('H' == toupper((int)b[len - 1])) {
+        res = sscanf(b, "%x", &unum);
+        num = unum;
+    } else
+        res = sscanf(b, "%d%c%c%c", &num, &c, &c2, &c3);
+
+    if (res < 1)
+        return -1;
+    else if (1 == res)
+        return num;
+    else {
+        c = toupper((int)c);
+        if (is_hex) {
+            if (! ((c == '+') || (c == 'X')))
+                return -1;
+        }
+        if (res > 2)
+            c2 = toupper((int)c2);
+        if (res > 3)
+            c3 = toupper((int)c3);
+
+        switch (c) {
+        case 'C':
+            return num;
+        case 'W':
+            return num * 2;
+        case 'B':
+            return num * 512;
+        case 'K':
+            if (2 == res)
+                return num * 1024;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1024;
+            return -1;
+        case 'M':
+            if (2 == res)
+                return num * 1048576;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1048576;
+            return -1;
+        case 'G':
+            if (2 == res)
+                return num * 1073741824;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1073741824;
+            return -1;
+        case 'X':       /* experimental: multiplication */
+            /* left argument must end with hexadecimal digit */
+            cp = (char *)strchr(b2p, 'x');
+            if (NULL == cp)
+                cp = (char *)strchr(b2p, 'X');
+            if (cp) {
+                n = sg_get_num(cp + 1);
+                if (-1 != n)
+                    return num * n;
+            }
+            return -1;
+        case '+':       /* experimental: addition */
+            /* left argument must end with hexadecimal digit */
+            cp = (char *)strchr(b2p, '+');
+            if (cp) {
+                n = sg_get_num(cp + 1);
+                if (-1 != n)
+                    return num + n;
+            }
+            return -1;
+        default:
+            pr2ws("unrecognized multiplier\n");
+            return -1;
+        }
+    }
+}
+
+/* If the number in 'buf' can not be decoded then -1 is returned. Accepts a
+ * hex prefix (0x or 0X) or a 'h' (or 'H') suffix; otherwise decimal is
+ * assumed. Does not accept multipliers. Accept a comma (","), hyphen ("-"),
+ * a whitespace or newline as terminator. */
+int
+sg_get_num_nomult(const char * buf)
+{
+    int res, len, num;
+    unsigned int unum;
+    char * commap;
+
+    if ((NULL == buf) || ('\0' == buf[0]))
+        return -1;
+    len = strlen(buf);
+    commap = (char *)strchr(buf + 1, ',');
+    if (('0' == buf[0]) && (('x' == buf[1]) || ('X' == buf[1]))) {
+        res = sscanf(buf + 2, "%x", &unum);
+        num = unum;
+    } else if (commap && ('H' == toupper((int)*(commap - 1)))) {
+        res = sscanf(buf, "%x", &unum);
+        num = unum;
+    } else if ((NULL == commap) && ('H' == toupper((int)buf[len - 1]))) {
+        res = sscanf(buf, "%x", &unum);
+        num = unum;
+    } else
+        res = sscanf(buf, "%d", &num);
+    if (1 == res)
+        return num;
+    else
+        return -1;
+}
+
+/* If the number in 'buf' can not be decoded or the multiplier is unknown
+ * then -1LL is returned. Accepts a hex prefix (0x or 0X), hex suffix
+ * (h or H), or a decimal multiplier suffix (as per GNU's dd (since 2002:
+ * SI and IEC 60027-2)).  Main (SI) multipliers supported: K, M, G, T, P
+ * and E. Ignore leading spaces and tabs; accept comma, hyphen, space, tab
+ * and hash as terminator. Handles zero and positive values up to 2**63-1 .
+ * Experimental: left argument (must in with hexadecimal digit) added
+ * to, or multiplied by right argument. No embedded spaces.
+ * Examples: '3+1k' (evaluates to 1027) and '0x34+1m'. */
+int64_t
+sg_get_llnum(const char * buf)
+{
+    bool is_hex = false;
+    int res, len, n;
+    int64_t num, ll;
+    uint64_t unum;
+    char * cp;
+    const char * b;
+    const char * b2p;
+    char c = 'c';
+    char c2 = '\0';     /* keep static checker happy */
+    char c3 = '\0';     /* keep static checker happy */
+    char lb[32];
+
+    if ((NULL == buf) || ('\0' == buf[0]))
+        return -1LL;
+    len = strlen(buf);
+    n = strspn(buf, " \t");
+    if (n > 0) {
+        if (n == len)
+            return -1LL;
+        buf += n;
+        len -= n;
+    }
+    /* following cast hack to keep C++ happy */
+    cp = strpbrk((char *)buf, " \t,#-");
+    if (cp) {
+        len = cp - buf;
+        n = (int)sizeof(lb) - 1;
+        len = (len < n) ? len : n;
+        memcpy(lb, buf, len);
+        lb[len] = '\0';
+        b = lb;
+    } else
+        b = buf;
+
+    b2p = b;
+    if (('0' == b[0]) && (('x' == b[1]) || ('X' == b[1]))) {
+        res = sscanf(b + 2, "%" SCNx64 "%c", &unum, &c);
+        num = unum;
+        is_hex = true;
+        b2p = b + 2;
+    } else if ('H' == toupper((int)b[len - 1])) {
+        res = sscanf(b, "%" SCNx64 , &unum);
+        num = unum;
+    } else
+        res = sscanf(b, "%" SCNd64 "%c%c%c", &num, &c, &c2, &c3);
+
+    if (res < 1)
+        return -1LL;
+    else if (1 == res)
+        return num;
+    else {
+        c = toupper((int)c);
+        if (is_hex) {
+            if (! ((c == '+') || (c == 'X')))
+                return -1;
+        }
+        if (res > 2)
+            c2 = toupper((int)c2);
+        if (res > 3)
+            c3 = toupper((int)c3);
+
+        switch (c) {
+        case 'C':
+            return num;
+        case 'W':
+            return num * 2;
+        case 'B':
+            return num * 512;
+        case 'K':       /* kilo or kibi */
+            if (2 == res)
+                return num * 1024;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1024;      /* KiB */
+            return -1LL;
+        case 'M':       /* mega or mebi */
+            if (2 == res)
+                return num * 1048576;   /* M */
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000;   /* MB */
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1048576;   /* MiB */
+            return -1LL;
+        case 'G':       /* giga or gibi */
+            if (2 == res)
+                return num * 1073741824;        /* G */
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000000;        /* GB */
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1073741824;        /* GiB */
+            return -1LL;
+        case 'T':       /* tera or tebi */
+            if (2 == res)
+                return num * 1099511627776LL;   /* T */
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000000000LL;   /* TB */
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1099511627776LL;   /* TiB */
+            return -1LL;
+        case 'P':       /* peta or pebi */
+            if (2 == res)
+                return num * 1099511627776LL * 1024;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000000000LL * 1000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1099511627776LL * 1024;
+            return -1LL;
+        case 'E':       /* exa or exbi */
+            if (2 == res)
+                return num * 1099511627776LL * 1024 * 1024;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000000000LL * 1000 * 1000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1099511627776LL * 1024 * 1024;
+            return -1LL;
+        case 'X':       /* experimental: decimal (left arg) multiplication */
+            cp = (char *)strchr(b2p, 'x');
+            if (NULL == cp)
+                cp = (char *)strchr(b2p, 'X');
+            if (cp) {
+                ll = sg_get_llnum(cp + 1);
+                if (-1LL != ll)
+                    return num * ll;
+            }
+            return -1LL;
+        case '+':       /* experimental: decimal (left arg) addition */
+            cp = (char *)strchr(b2p, '+');
+            if (cp) {
+                ll = sg_get_llnum(cp + 1);
+                if (-1LL != ll)
+                    return num + ll;
+            }
+            return -1LL;
+        default:
+            pr2ws("unrecognized multiplier\n");
+            return -1LL;
+        }
+    }
+}
+
+/* If the number in 'buf' can not be decoded then -1 is returned. Accepts a
+ * hex prefix (0x or 0X) or a 'h' (or 'H') suffix; otherwise decimal is
+ * assumed. Does not accept multipliers. Accept a comma (","), hyphen ("-"),
+ * a whitespace or newline as terminator. Only decimal numbers can represent
+ * negative numbers and '-1' must be treated separately. */
+int64_t
+sg_get_llnum_nomult(const char * buf)
+{
+    int res, len;
+    int64_t num;
+    uint64_t unum;
+
+    if ((NULL == buf) || ('\0' == buf[0]))
+        return -1;
+    len = strlen(buf);
+    if (('0' == buf[0]) && (('x' == buf[1]) || ('X' == buf[1]))) {
+        res = sscanf(buf + 2, "%" SCNx64 "", &unum);
+        num = unum;
+    } else if ('H' == toupper(buf[len - 1])) {
+        res = sscanf(buf, "%" SCNx64 "", &unum);
+        num = unum;
+    } else
+        res = sscanf(buf, "%" SCNd64 "", &num);
+    return (1 == res) ? num : -1;
+}
+
+#define MAX_NUM_ASCII_LINES 1048576
+
+/* Read ASCII hex bytes or binary from fname (a file named '-' taken as
+ * stdin). If reading ASCII hex then there should be either one entry per
+ * line or a comma, space, hyphen or tab separated list of bytes. If no_space
+ * is set then a string of ACSII hex digits is expected, 2 perbyte.
+ * Everything from and including a '#' on a line is ignored. Returns 0 if ok,
+ * or an error code. If the error code is SG_LIB_LBA_OUT_OF_RANGE then mp_arr
+ * would be exceeded and both mp_arr and mp_arr_len are written to.
+ * The max_arr_len_and argument may carry extra information: when it
+ * is negative its absolute value is used for the maximum number of bytes to
+ * write to mp_arr _and_ the first hexadecimal value on each line is skipped.
+ * Many hexadecimal output programs place a running address (index) as the
+ * first field on each line. When as_binary and/or no_space are true, the
+ * absolute value of max_arr_len_and is used. */
+int
+sg_f2hex_arr(const char * fname, bool as_binary, bool no_space,
+             uint8_t * mp_arr, int * mp_arr_len, int max_arr_len_and)
+{
+    bool has_stdin, split_line, skip_first, redo_first;
+    int fn_len, in_len, k, j, m, fd, err, max_arr_len;
+    int off = 0;
+    int ret = 0;
+    unsigned int h;
+    const char * lcp;
+    FILE * fp = NULL;
+    struct stat a_stat;
+    char line[512];
+    char carry_over[4];
+
+    if ((NULL == fname) || (NULL == mp_arr) || (NULL == mp_arr_len)) {
+        pr2ws("%s: bad arguments\n", __func__);
+        return SG_LIB_LOGIC_ERROR;
+    }
+    if (max_arr_len_and < 0) {
+        skip_first = true;
+        max_arr_len = -max_arr_len_and;
+    } else {
+        skip_first = false;
+        max_arr_len = max_arr_len_and;
+    }
+    fn_len = strlen(fname);
+    if (0 == fn_len)
+        return SG_LIB_SYNTAX_ERROR;
+    has_stdin = ((1 == fn_len) && ('-' == fname[0]));   /* read from stdin */
+    if (as_binary) {
+        if (has_stdin)
+            fd = STDIN_FILENO;
+        else {
+            fd = open(fname, O_RDONLY);
+            if (fd < 0) {
+                err = errno;
+                pr2ws("unable to open binary file %s: %s\n", fname,
+                         safe_strerror(err));
+                return sg_convert_errno(err);
+            }
+        }
+        k = read(fd, mp_arr, max_arr_len);
+        if (k <= 0) {
+            if (0 == k) {
+                ret = SG_LIB_FILE_ERROR;
+                pr2ws("read 0 bytes from binary file %s\n", fname);
+            } else {
+                ret = sg_convert_errno(errno);
+                pr2ws("read from binary file %s: %s\n", fname,
+                        safe_strerror(errno));
+            }
+        } else if ((k < max_arr_len) && (0 == fstat(fd, &a_stat)) &&
+                   S_ISFIFO(a_stat.st_mode)) {
+            /* pipe; keep reading till error or 0 read */
+            while (k < max_arr_len) {
+                m = read(fd, mp_arr + k, max_arr_len - k);
+                if (0 == m)
+                   break;
+                if (m < 0) {
+                    err = errno;
+                    pr2ws("read from binary pipe %s: %s\n", fname,
+                            safe_strerror(err));
+                    ret = sg_convert_errno(err);
+                    break;
+                }
+                k += m;
+            }
+        }
+        if (k >= 0)
+            *mp_arr_len = k;
+        if ((fd >= 0) && (! has_stdin))
+            close(fd);
+        return ret;
+    }
+
+    /* So read the file as ASCII hex */
+    if (has_stdin)
+        fp = stdin;
+    else {
+        fp = fopen(fname, "r");
+        if (NULL == fp) {
+            err = errno;
+            pr2ws("Unable to open %s for reading: %s\n", fname,
+                    safe_strerror(err));
+            ret = sg_convert_errno(err);
+            goto fini;
+        }
+    }
+
+    carry_over[0] = 0;
+    for (j = 0; j < MAX_NUM_ASCII_LINES; ++j) {
+        if (NULL == fgets(line, sizeof(line), fp))
+            break;
+        in_len = strlen(line);
+        if (in_len > 0) {
+            if ('\n' == line[in_len - 1]) {
+                --in_len;
+                line[in_len] = '\0';
+                split_line = false;
+            } else
+                split_line = true;
+        }
+        if (in_len < 1) {
+            carry_over[0] = 0;
+            continue;
+        }
+        if (carry_over[0]) {
+            if (isxdigit(line[0])) {
+                carry_over[1] = line[0];
+                carry_over[2] = '\0';
+                if (1 == sscanf(carry_over, "%4x", &h)) {
+                    if (off > 0) {
+                        if (off > max_arr_len) {
+                            pr2ws("%s: array length exceeded\n", __func__);
+                            ret = SG_LIB_LBA_OUT_OF_RANGE;
+                            *mp_arr_len = max_arr_len;
+                            goto fini;
+                        } else
+                            mp_arr[off - 1] = h; /* back up and overwrite */
+                    }
+                } else {
+                    pr2ws("%s: carry_over error ['%s'] around line %d\n",
+                            __func__, carry_over, j + 1);
+                    ret = SG_LIB_SYNTAX_ERROR;
+                    goto fini;
+                }
+                lcp = line + 1;
+                --in_len;
+            } else
+                lcp = line;
+            carry_over[0] = 0;
+        } else
+            lcp = line;
+
+        m = strspn(lcp, " \t");
+        if (m == in_len)
+            continue;
+        lcp += m;
+        in_len -= m;
+        if ('#' == *lcp)
+            continue;
+        k = strspn(lcp, "0123456789aAbBcCdDeEfF ,-\t");
+        if ((k < in_len) && ('#' != lcp[k]) && ('\r' != lcp[k])) {
+            pr2ws("%s: syntax error at line %d, pos %d\n", __func__,
+                    j + 1, m + k + 1);
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto fini;
+        }
+        if (no_space) {
+            for (k = 0; isxdigit(*lcp) && isxdigit(*(lcp + 1));
+                 ++k, lcp += 2) {
+                if (1 != sscanf(lcp, "%2x", &h)) {
+                    pr2ws("%s: bad hex number in line %d, pos %d\n",
+                            __func__, j + 1, (int)(lcp - line + 1));
+                    ret = SG_LIB_SYNTAX_ERROR;
+                    goto fini;
+                }
+                if ((off + k) >= max_arr_len) {
+                    pr2ws("%s: array length exceeded\n", __func__);
+                    *mp_arr_len = max_arr_len;
+                    ret = SG_LIB_LBA_OUT_OF_RANGE;
+                    goto fini;
+                } else
+                    mp_arr[off + k] = h;
+            }
+            if (isxdigit(*lcp) && (! isxdigit(*(lcp + 1))))
+                carry_over[0] = *lcp;
+            off += k;
+        } else {        /* (white)space separated ASCII hexadecimal bytes */
+            for (redo_first = false, k = 0; k < 1024;
+                 k = (redo_first ? k : k + 1)) {
+                if (1 == sscanf(lcp, "%10x", &h)) {
+                    if (h > 0xff) {
+                        pr2ws("%s: hex number larger than 0xff in line "
+                                "%d, pos %d\n", __func__, j + 1,
+                                (int)(lcp - line + 1));
+                        ret = SG_LIB_SYNTAX_ERROR;
+                        goto fini;
+                    }
+                    if (split_line && (1 == strlen(lcp))) {
+                        /* single trailing hex digit might be a split pair */
+                        carry_over[0] = *lcp;
+                    }
+                    if ((off + k) >= max_arr_len) {
+                        pr2ws("%s: array length exceeded\n", __func__);
+                        ret = SG_LIB_LBA_OUT_OF_RANGE;
+                        *mp_arr_len = max_arr_len;
+                        goto fini;
+                    } else if ((0 == k) && skip_first && (! redo_first))
+                        redo_first = true;
+                    else {
+                        redo_first = false;
+                        mp_arr[off + k] = h;
+                    }
+                    lcp = strpbrk(lcp, " ,-\t");
+                    if (NULL == lcp)
+                        break;
+                    lcp += strspn(lcp, " ,-\t");
+                    if ('\0' == *lcp)
+                        break;
+                } else {
+                    if (('#' == *lcp) || ('\r' == *lcp)) {
+                        --k;
+                        break;
+                    }
+                    pr2ws("%s: error in line %d, at pos %d\n", __func__,
+                            j + 1, (int)(lcp - line + 1));
+                    ret = SG_LIB_SYNTAX_ERROR;
+                    goto fini;
+                }
+            }
+            off += (k + 1);
+        }
+    }           /* end of per line loop */
+    if (j >= MAX_NUM_ASCII_LINES) {
+        pr2ws("%s: wow, more than %d lines of ASCII, give up\n", __func__,
+              SG_LIB_LBA_OUT_OF_RANGE);
+        return SG_LIB_LBA_OUT_OF_RANGE;
+    }
+    *mp_arr_len = off;
+    if (fp && (! has_stdin))
+        fclose(fp);
+    return 0;
+fini:
+    if (fp && (! has_stdin))
+        fclose(fp);
+    return ret;
+}
+
+/* Extract character sequence from ATA words as in the model string
+ * in a IDENTIFY DEVICE response. Returns number of characters
+ * written to 'ochars' before 0 character is found or 'num' words
+ * are processed. */
+int
+sg_ata_get_chars(const uint16_t * word_arr, int start_word,
+                 int num_words, bool is_big_endian, char * ochars)
+{
+    int k;
+    char * op = ochars;
+
+    for (k = start_word; k < (start_word + num_words); ++k) {
+        char a, b;
+        uint16_t s = word_arr[k];
+
+        if (is_big_endian) {
+            a = s & 0xff;
+            b = (s >> 8) & 0xff;
+        } else {
+            a = (s >> 8) & 0xff;
+            b = s & 0xff;
+        }
+        if (a == 0)
+            break;
+        *op++ = a;
+        if (b == 0)
+            break;
+        *op++ = b;
+    }
+    return op - ochars;
+}
+
+#ifdef SG_LIB_FREEBSD
+#include <sys/param.h>
+#elif defined(SG_LIB_WIN32)
+#include <windows.h>
+#endif
+
+uint32_t
+sg_get_page_size(void)
+{
+#if defined(HAVE_SYSCONF) && defined(_SC_PAGESIZE)
+    {
+        long res = sysconf(_SC_PAGESIZE);   /* POSIX.1 (was getpagesize()) */
+
+        return (res <= 0) ? 4096 : res;
+    }
+#elif defined(SG_LIB_WIN32)
+    static bool got_page_size = false;
+    static uint32_t win_page_size;
+
+    if (! got_page_size) {
+        SYSTEM_INFO si;
+
+        GetSystemInfo(&si);
+        win_page_size = si.dwPageSize;
+        got_page_size = true;
+    }
+    return win_page_size;
+#elif defined(SG_LIB_FREEBSD)
+    return PAGE_SIZE;
+#else
+    return 4096;     /* give up, pick likely figure */
+#endif
+}
+
+#if defined(SG_LIB_WIN32)
+#if defined(MSC_VER) || defined(__MINGW32__)
+/* windows.h already included above */
+#define sg_sleep_for(seconds)    Sleep( (seconds) * 1000)
+#else
+#define sg_sleep_for(seconds)    sleep(seconds)
+#endif
+#else
+#define sg_sleep_for(seconds)    sleep(seconds)
+#endif
+
+void
+sg_sleep_secs(int num_secs)
+{
+    sg_sleep_for(num_secs);
+}
+
+void
+sg_warn_and_wait(const char * cmd_name, const char * dev_name,
+                 bool stress_all)
+{
+    int k, j;
+    const char * stressp = stress_all ? "ALL d" : "D";
+    const char * will_mayp = stress_all ? "will" : "may";
+
+    for (k = 0, j = 15; k < 3; ++k, j -= 5) {
+        printf("\nA %s command will commence in %d seconds\n", cmd_name, j);
+        printf("    %sata on %s %s be DESTROYED%s\n", stressp, dev_name,
+               will_mayp, (stress_all ? "" : " or modified"));
+        printf("        Press control-C to abort\n");
+        sg_sleep_secs(5);
+    }
+    sg_sleep_secs(1);
+}
+
+/* Returns pointer to heap (or NULL) that is aligned to a align_to byte
+ * boundary. Sends back *buff_to_free pointer in third argument that may be
+ * different from the return value. If it is different then the *buff_to_free
+ * pointer should be freed (rather than the returned value) when the heap is
+ * no longer needed. If align_to is 0 then aligns to OS's page size. Sets all
+ * returned heap to zeros. If num_bytes is 0 then set to page size. */
+uint8_t *
+sg_memalign(uint32_t num_bytes, uint32_t align_to, uint8_t ** buff_to_free,
+            bool vb)
+{
+    size_t psz;
+
+    if (buff_to_free)   /* make sure buff_to_free is NULL if alloc fails */
+        *buff_to_free = NULL;
+    psz = (align_to > 0) ? align_to : sg_get_page_size();
+    if (0 == num_bytes)
+        num_bytes = psz;        /* ugly to handle otherwise */
+
+#ifdef HAVE_POSIX_MEMALIGN
+    {
+        int err;
+        uint8_t * res;
+        void * wp = NULL;
+
+        err = posix_memalign(&wp, psz, num_bytes);
+        if (err || (NULL == wp)) {
+            pr2ws("%s: posix_memalign: error [%d], out of memory?\n",
+                  __func__, err);
+            return NULL;
+        }
+        memset(wp, 0, num_bytes);
+        if (buff_to_free)
+            *buff_to_free = (uint8_t *)wp;
+        res = (uint8_t *)wp;
+        if (vb) {
+            pr2ws("%s: posix_ma, len=%d, ", __func__, num_bytes);
+            if (buff_to_free)
+                pr2ws("wrkBuffp=%p, ", (void *)res);
+            pr2ws("psz=%u, rp=%p\n", (unsigned int)psz, (void *)res);
+        }
+        return res;
+    }
+#else
+    {
+        void * wrkBuff;
+        uint8_t * res;
+        sg_uintptr_t align_1 = psz - 1;
+
+        wrkBuff = (uint8_t *)calloc(num_bytes + psz, 1);
+        if (NULL == wrkBuff) {
+            if (buff_to_free)
+                *buff_to_free = NULL;
+            return NULL;
+        } else if (buff_to_free)
+            *buff_to_free = (uint8_t *)wrkBuff;
+        res = (uint8_t *)(void *)
+            (((sg_uintptr_t)wrkBuff + align_1) & (~align_1));
+        if (vb) {
+            pr2ws("%s: hack, len=%d, ", __func__, num_bytes);
+            if (buff_to_free)
+                pr2ws("buff_to_free=%p, ", wrkBuff);
+            pr2ws("align_1=%" PRIuPTR "u, rp=%p\n", align_1, (void *)res);
+        }
+        return res;
+    }
+#endif
+}
+
+/* If byte_count is 0 or less then the OS page size is used as denominator.
+ * Returns true  if the remainder of ((unsigned)pointer % byte_count) is 0,
+ * else returns false. */
+bool
+sg_is_aligned(const void * pointer, int byte_count)
+{
+    return 0 == ((sg_uintptr_t)pointer %
+                 ((byte_count > 0) ? (uint32_t)byte_count :
+                                     sg_get_page_size()));
+}
+
+/* Does similar job to sg_get_unaligned_be*() but this function starts at
+ * a given start_bit (i.e. within byte, so 7 is MSbit of byte and 0 is LSbit)
+ * offset. Maximum number of num_bits is 64. For example, these two
+ * invocations are equivalent (and should yield the same result);
+ *       sg_get_big_endian(from_bp, 7, 16)
+ *       sg_get_unaligned_be16(from_bp)  */
+uint64_t
+sg_get_big_endian(const uint8_t * from_bp, int start_bit /* 0 to 7 */,
+                  int num_bits /* 1 to 64 */)
+{
+    uint64_t res;
+    int sbit_o1 = start_bit + 1;
+
+    res = (*from_bp++ & ((1 << sbit_o1) - 1));
+    num_bits -= sbit_o1;
+    while (num_bits > 0) {
+        res <<= 8;
+        res |= *from_bp++;
+        num_bits -= 8;
+    }
+    if (num_bits < 0)
+        res >>= (-num_bits);
+    return res;
+}
+
+/* Does similar job to sg_put_unaligned_be*() but this function starts at
+ * a given start_bit offset. Maximum number of num_bits is 64. Preserves
+ * residual bits in partially written bytes. start_bit 7 is MSb. */
+void
+sg_set_big_endian(uint64_t val, uint8_t * to,
+                  int start_bit /* 0 to 7 */, int num_bits /* 1 to 64 */)
+{
+    int sbit_o1 = start_bit + 1;
+    int mask, num, k, x;
+
+    if ((NULL == to) || (start_bit > 7) || (num_bits > 64)) {
+        pr2ws("%s: bad args: start_bit=%d, num_bits=%d\n", __func__,
+              start_bit, num_bits);
+        return;
+    }
+    mask = (8 != sbit_o1) ? ((1 << sbit_o1) - 1) : 0xff;
+    k = start_bit - ((num_bits - 1) % 8);
+    if (0 != k)
+        val <<= ((k > 0) ? k : (8 + k));
+    num = (num_bits + 15 - sbit_o1) / 8;
+    for (k = 0; k < num; ++k) {
+        if ((sbit_o1 - num_bits) > 0)
+            mask &= ~((1 << (sbit_o1 - num_bits)) - 1);
+        if (k < (num - 1))
+            x = (val >> ((num - k - 1) * 8)) & 0xff;
+        else
+            x = val & 0xff;
+        to[k] = (to[k] & ~mask) | (x & mask);
+        mask = 0xff;
+        num_bits -= sbit_o1;
+        sbit_o1 = 8;
+    }
+}
+
+const char *
+sg_lib_version()
+{
+    return sg_lib_version_str;
+}
+
+
+#ifdef SG_LIB_MINGW
+/* Non Unix OSes distinguish between text and binary files.
+   Set text mode on fd. Does nothing in Unix. Returns negative number on
+   failure. */
+
+#include <fcntl.h>
+
+int
+sg_set_text_mode(int fd)
+{
+    return setmode(fd, O_TEXT);
+}
+
+/* Set binary mode on fd. Does nothing in Unix. Returns negative number on
+   failure. */
+int
+sg_set_binary_mode(int fd)
+{
+    return setmode(fd, O_BINARY);
+}
+
+#else
+/* For Unix the following functions are dummies. */
+int
+sg_set_text_mode(int fd)
+{
+    return fd;  /* fd should be >= 0 */
+}
+
+int
+sg_set_binary_mode(int fd)
+{
+    return fd;
+}
+
+#endif
diff --git a/lib/sg_lib_data.c b/lib/sg_lib_data.c
new file mode 100644
index 0000000..f875fab
--- /dev/null
+++ b/lib/sg_lib_data.c
@@ -0,0 +1,2014 @@
+/*
+ * Copyright (c) 2007-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdlib.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#else
+#define SG_SCSI_STRINGS 1
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+
+
+const char * sg_lib_version_str = "2.95 20221104";
+/* spc6r06, sbc5r03, zbc2r13 */
+
+
+/* indexed by pdt; those that map to own index do not decay */
+int sg_lib_pdt_decay_arr[32] = {
+    PDT_DISK, PDT_TAPE, PDT_TAPE /* printer */, PDT_PROCESSOR,
+    PDT_DISK /* WO */, PDT_MMC, PDT_SCANNER, PDT_DISK /* optical */,
+    PDT_MCHANGER, PDT_COMMS, 0xa, 0xb,
+    PDT_SAC, PDT_SES, PDT_DISK /* rbc */, PDT_OCRW,
+    PDT_BCC, PDT_OSD, PDT_TAPE /* adc */, PDT_SMD,
+    PDT_DISK /* zbc */, 0x15, 0x16, 0x17,
+    0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, PDT_WLUN, PDT_UNKNOWN
+};
+
+/* SCSI Status values */
+struct sg_lib_simple_value_name_t sg_lib_sstatus_str_arr[] = {
+    {0x0,  "Good"},
+    {0x2,  "Check Condition"},
+    {0x4,  "Condition Met"},
+    {0x8,  "Busy"},
+    {0x10, "Intermediate (obsolete)"},
+    {0x14, "Intermediate-Condition Met (obsolete)"},
+    {0x18, "Reservation Conflict"},
+    {0x22, "Command terminated (obsolete)"},
+    {0x28, "Task Set Full"},
+    {0x30, "ACA Active"},
+    {0x40, "Task Aborted"},
+    {0xffff, NULL},
+};
+
+#ifdef SG_SCSI_STRINGS
+struct sg_lib_value_name_t sg_lib_normal_opcodes[] = {
+    {0, PDT_ALL, "Test Unit Ready"},
+    {0x1, PDT_ALL, "Rezero Unit"},
+    {0x1, PDT_TAPE, "Rewind"},
+    {0x3, PDT_ALL, "Request Sense"},
+    {0x4, PDT_DISK_ZBC, "Format Unit"},
+    {0x4, PDT_TAPE, "Format medium"},
+    {0x4, PDT_PRINTER, "Format"},
+    {0x5, PDT_TAPE, "Read Block Limits"},
+    {0x7, PDT_DISK_ZBC, "Reassign Blocks"},
+    {0x7, PDT_MCHANGER, "Initialize element status"},
+    {0x8, PDT_DISK_ZBC, "Read(6)"},        /* obsolete in sbc3r30 */
+    {0x8, PDT_PROCESSOR, "Receive"},
+    {0xa, PDT_DISK_ZBC, "Write(6)"},       /* obsolete in sbc3r30 */
+    {0xa, PDT_PRINTER, "Print"},
+    {0xa, PDT_PROCESSOR, "Send"},
+    {0xb, PDT_ALL, "Seek(6)"},
+    {0xb, PDT_TAPE, "Set capacity"},
+    {0xb, PDT_PRINTER, "Slew and print"},
+    {0xf, PDT_TAPE, "Read reverse(6)"},
+    {0x10, PDT_TAPE, "Write filemarks(6)"},
+    {0x10, PDT_PRINTER, "Synchronize buffer"},
+    {0x11, PDT_TAPE, "Space(6)"},
+    {0x12, PDT_ALL, "Inquiry"},
+    {0x13, PDT_TAPE, "Verify(6)"},  /* SSC */
+    {0x14, PDT_ALL, "Recover buffered data"},
+    {0x15, PDT_ALL, "Mode select(6)"},/* sbc3r31 recommends Mode select(10) */
+    {0x16, PDT_ALL, "Reserve(6)"},    /* obsolete in SPC-4 r11 */
+    {0x16, PDT_MCHANGER, "Reserve element(6)"},
+    {0x17, PDT_ALL, "Release(6)"},    /* obsolete in SPC-4 r11 */
+    {0x17, PDT_MCHANGER, "Release element(6)"},
+    {0x18, PDT_ALL, "Copy"},          /* obsolete in SPC-4 r11 */
+    {0x19, PDT_ALL, "Erase(6)"},
+    {0x1a, PDT_ALL, "Mode sense(6)"},/* sbc3r31 recommends Mode sense(10) */
+    {0x1b, PDT_ALL, "Start stop unit"},
+    {0x1b, PDT_TAPE, "Load unload"},
+    {0x1b, PDT_ADC, "Load unload"},
+    {0x1b, PDT_PRINTER, "Stop print"},
+    {0x1c, PDT_ALL, "Receive diagnostic results"},
+    {0x1d, PDT_ALL, "Send diagnostic"},
+    {0x1e, PDT_ALL, "Prevent allow medium removal"},
+    {0x23, PDT_MMC, "Read Format capacities"},
+    {0x24, PDT_ALL, "Set window"},
+    {0x25, PDT_ALL, "Read capacity(10)"},
+                        /* sbc3r31 recommends Read capacity(16) */
+    {0x25, PDT_OCRW, "Read card capacity"},
+    {0x28, PDT_ALL, "Read(10)"},      /* sbc3r31 recommends Read(16) */
+    {0x29, PDT_ALL, "Read generation"},
+    {0x2a, PDT_ALL, "Write(10)"},     /* sbc3r31 recommends Write(16) */
+    {0x2b, PDT_ALL, "Seek(10)"},
+    {0x2b, PDT_TAPE, "Locate(10)"},
+    {0x2b, PDT_MCHANGER, "Position to element"},
+    {0x2c, PDT_ALL, "Erase(10)"},
+    {0x2d, PDT_OPTICAL, "Read updated block"},
+    {0x2e, PDT_ALL, "Write and verify(10)"},
+                        /* sbc3r31 recommends Write and verify(16) */
+    {0x2f, PDT_ALL, "Verify(10)"},    /* sbc3r31 recommends Verify(16) */
+    {0x30, PDT_ALL, "Search data high(10)"},
+    {0x31, PDT_ALL, "Search data equal(10)"},
+    {0x32, PDT_ALL, "Search data low(10)"},
+    {0x33, PDT_ALL, "Set limits(10)"},
+    {0x34, PDT_ALL, "Pre-fetch(10)"}, /* sbc3r31 recommends Pre-fetch(16) */
+    {0x34, PDT_TAPE, "Read position"},
+    {0x35, PDT_ALL, "Synchronize cache(10)"},
+                        /* SBC-3 r31 recommends Synchronize cache(16) */
+    {0x36, PDT_ALL, "Lock unlock cache(10)"},
+    {0x37, PDT_MCHANGER, "Initialize element status with range"},
+    {0x37, PDT_ALL, "Read defect data(10)"},
+                        /* SBC-3 r31 recommends Read defect data(12) */
+    {0x38, PDT_DISK_ZBC, "Format with preset scan"},
+    {0x38, PDT_OCRW, "Medium scan"},
+    {0x39, PDT_ALL, "Compare"},               /* obsolete in SPC-4 r11 */
+    {0x3a, PDT_ALL, "Copy and verify"},       /* obsolete in SPC-4 r11 */
+    {0x3b, PDT_ALL, "Write buffer"},
+    {0x3c, PDT_ALL, "Read buffer(10)"},
+    {0x3d, PDT_OPTICAL, "Update block"},
+    {0x3e, PDT_ALL, "Read long(10)"},         /* obsolete in SBC-4 r7 */
+    {0x3f, PDT_ALL, "Write long(10)"}, /* sbc3r31 recommends Write long(16) */
+    {0x40, PDT_ALL, "Change definition"},     /* obsolete in SPC-4 r11 */
+    {0x41, PDT_ALL, "Write same(10)"}, /* sbc3r31 recommends Write same(16) */
+    {0x42, PDT_DISK_ZBC, "Unmap"},                 /* added SPC-4 rev 18 */
+    {0x42, PDT_MMC, "Read sub-channel"},
+    {0x43, PDT_MMC, "Read TOC/PMA/ATIP"},
+    {0x44, PDT_ALL, "Report density support"},
+    {0x45, PDT_MMC, "Play audio(10)"},
+    {0x46, PDT_MMC, "Get configuration"},
+    {0x47, PDT_MMC, "Play audio msf"},
+    {0x48, PDT_ALL, "Sanitize"},
+    {0x4a, PDT_MMC, "Get event status notification"},
+    {0x4b, PDT_MMC, "Pause/resume"},
+    {0x4c, PDT_ALL, "Log select"},
+    {0x4d, PDT_ALL, "Log sense"},
+    {0x4e, PDT_MMC, "Stop play/scan"},
+    {0x50, PDT_DISK, "Xdwrite(10)"},           /* obsolete in SBC-3 r31 */
+    {0x51, PDT_DISK, "Xpwrite(10)"},           /* obsolete in SBC-4 r15 */
+    {0x51, PDT_MMC, "Read disk information"},
+    {0x52, PDT_DISK, "Xdread(10)"},            /* obsolete in SBC-3 r31 */
+    {0x52, PDT_MMC, "Read track information"},
+    {0x53, PDT_DISK, "Xdwriteread(10)"},       /* obsolete in SBC-4 r15 */
+    {0x54, PDT_MMC, "Send OPC information"},
+    {0x55, PDT_ALL, "Mode select(10)"},
+    {0x56, PDT_ALL, "Reserve(10)"},           /* obsolete in SPC-4 r11 */
+    {0x56, PDT_MCHANGER, "Reserve element(10)"},
+    {0x57, PDT_ALL, "Release(10)"},           /* obsolete in SPC-4 r11 */
+    {0x57, PDT_MCHANGER, "Release element(10)"},
+    {0x58, PDT_MMC, "Repair track"},
+    {0x5a, PDT_ALL, "Mode sense(10)"},
+    {0x5b, PDT_MMC, "Close track/session"},
+    {0x5c, PDT_MMC, "Read buffer capacity"},
+    {0x5d, PDT_MMC, "Send cue sheet"},
+    {0x5e, PDT_ALL, "Persistent reserve in"},
+    {0x5f, PDT_ALL, "Persistent reserve out"},
+    {0x7e, PDT_ALL, "Extended cdb (XCBD)"},        /* added in SPC-4 r12 */
+    {0x80, PDT_DISK, "Xdwrite extended(16)"},     /* obsolete in SBC-4 r15 */
+    {0x80, PDT_TAPE, "Write filemarks(16)"},
+    {0x81, PDT_DISK, "Rebuild(16)"},
+    {0x81, PDT_TAPE, "Read reverse(16)"},
+    {0x82, PDT_DISK, "Regenerate(16)"},
+    {0x83, PDT_ALL, "Third party copy out"},/* Extended copy before spc4r34 */
+        /* Following was "Receive copy results", before spc4r34 */
+    {0x84, PDT_ALL, "Third party copy in"},
+    {0x85, PDT_ALL, "ATA pass-through(16)"},  /* was 0x98 in spc3 rev21c */
+    {0x86, PDT_ALL, "Access control in"},
+    {0x87, PDT_ALL, "Access control out"},
+    {0x88, PDT_ALL, "Read(16)"},
+    {0x89, PDT_ALL, "Compare and write"},
+    {0x8a, PDT_ALL, "Write(16)"},
+    {0x8b, PDT_DISK, "Orwrite(16)"},
+    {0x8c, PDT_ALL, "Read attribute"},
+    {0x8d, PDT_ALL, "Write attribute"},
+    {0x8e, PDT_ALL, "Write and verify(16)"},
+    {0x8f, PDT_ALL, "Verify(16)"},
+    {0x90, PDT_ALL, "Pre-fetch(16)"},
+    {0x91, PDT_ALL, "Synchronize cache(16)"},
+    {0x91, PDT_TAPE, "Space(16)"},
+    {0x92, PDT_DISK, "Lock unlock cache(16)"},
+    {0x92, PDT_TAPE, "Locate(16)"},
+    {0x93, PDT_ALL, "Write same(16)"},
+    {0x93, PDT_TAPE, "Erase(16)"},
+    {0x94, PDT_DISK_ZBC, "ZBC out"},  /* new sbc4r04, has service actions */
+    {0x95, PDT_DISK_ZBC, "ZBC in"},   /* new sbc4r04, has service actions */
+    {0x9a, PDT_ALL, "Write stream(16)"},      /* added sbc4r07 */
+    {0x9b, PDT_ALL, "Read buffer(16)"},       /* added spc5r02 */
+    {0x9c, PDT_ALL, "Write atomic(16)"},
+    {0x9d, PDT_ALL, "Service action bidirectional"},  /* added spc4r35 */
+    {0x9e, PDT_ALL, "Service action in(16)"},
+    {0x9f, PDT_ALL, "Service action out(16)"},
+    {0xa0, PDT_ALL, "Report luns"},
+    {0xa1, PDT_ALL, "ATA pass-through(12)"},
+    {0xa1, PDT_MMC, "Blank"},
+    {0xa2, PDT_ALL, "Security protocol in"},
+    {0xa3, PDT_ALL, "Maintenance in"},
+    {0xa3, PDT_MMC, "Send key"},
+    {0xa4, PDT_ALL, "Maintenance out"},
+    {0xa4, PDT_MMC, "Report key"},
+    {0xa5, PDT_ALL, "Move medium"},
+    {0xa5, PDT_MMC, "Play audio(12)"},
+    {0xa6, PDT_MCHANGER, "Exchange medium"},
+    {0xa6, PDT_MMC, "Load/unload medium"},
+    {0xa7, PDT_OPTICAL, "Move medium attached"},
+    {0xa7, PDT_MMC, "Set read ahead"},
+    {0xa8, PDT_ALL, "Read(12)"},      /* SBC-3 r31 recommends Read(16) */
+    {0xa9, PDT_ALL, "Service action out(12)"},
+    {0xaa, PDT_ALL, "Write(12)"},     /* SBC-3 r31 recommends Write(16) */
+    {0xab, PDT_ALL, "Service action in(12)"},
+    {0xac, PDT_OPTICAL, "erase(12)"},
+    {0xac, PDT_MMC, "Get performance"},
+    {0xad, PDT_MMC, "Read DVD/BD structure"},
+    {0xae, PDT_ALL, "Write and verify(12)"},
+                        /* SBC-3 r31 recommends Write and verify(16) */
+    {0xaf, PDT_ALL, "Verify(12)"},    /* SBC-3 r31 recommends Verify(16) */
+    {0xb0, PDT_DISK, "Search data high(12)"},
+    {0xb1, PDT_DISK, "Search data equal(12)"},
+    {0xb1, PDT_MCHANGER, "Open/close import/export element"},
+    {0xb2, PDT_DISK, "Search data low(12)"},
+    {0xb3, PDT_DISK, "Set limits(12)"},
+    {0xb4, PDT_ALL, "Read element status attached"},
+    {0xb5, PDT_MCHANGER, "Request volume element address"},
+    {0xb5, PDT_ALL, "Security protocol out"},
+    {0xb6, PDT_MCHANGER, "Send volume tag"},
+    {0xb6, PDT_MMC, "Set streaming"},
+    {0xb7, PDT_ALL, "Read defect data(12)"},
+    {0xb8, PDT_ALL, "Read element status"},
+    {0xb9, PDT_MMC, "Read CD msf"},
+    {0xba, PDT_MMC, "Scan"},
+    {0xba, PDT_ALL, "Redundancy group in"},
+    {0xbb, PDT_ALL, "Redundancy group out"},
+    {0xbb, PDT_MMC, "Set CD speed"},
+    {0xbc, PDT_ALL, "Spare in"},
+    {0xbd, PDT_ALL, "Spare out"},
+    {0xbd, PDT_MMC, "Mechanism status"},
+    {0xbe, PDT_MMC, "Read CD"},
+    {0xbe, PDT_ALL, "Volume set in"},
+    {0xbf, PDT_MMC, "Send DVD/BD structure"},
+    {0xbf, PDT_ALL, "Volume set out"},
+    {0xffff, 0, NULL},
+};
+
+/* Read buffer(10) [0x3c] and Read buffer(16) [0x9b] service actions (sa),
+ * need prefix */
+struct sg_lib_value_name_t sg_lib_read_buff_arr[] = {
+    {0x0, PDT_ALL, "combined header and data [or multiple modes]"},
+    {0x2, PDT_ALL, "data"},
+    {0x3, PDT_ALL, "descriptor"},
+    {0xa, PDT_ALL, "read data from echo buffer"},
+    {0xb, PDT_ALL, "echo buffer descriptor"},
+    {0xf, PDT_ALL, "read microcode status"},    /* added in spc5r20 */
+    {0x1a, PDT_ALL, "enable expander comms protocol and echo buffer"},
+    {0x1c, PDT_ALL, "error history"},
+    {0xffff, 0, NULL},
+};
+
+/* Write buffer [0x3b] service actions, need prefix */
+struct sg_lib_value_name_t sg_lib_write_buff_arr[] = {
+    {0x0, PDT_ALL, "combined header and data [or multiple modes]"},
+    {0x2, PDT_ALL, "data"},
+    {0x4, PDT_ALL, "download microcode and activate"},
+    {0x5, PDT_ALL, "download microcode, save, and activate"},
+    {0x6, PDT_ALL, "download microcode with offsets and activate"},
+    {0x7, PDT_ALL, "download microcode with offsets, save, and activate"},
+    {0xa, PDT_ALL, "write data to echo buffer"},
+    {0xd, PDT_ALL, "download microcode with offsets, select activation "
+             "events, save and defer activate"},
+    {0xe, PDT_ALL, "download microcode with offsets, save and defer activate"},
+    {0xf, PDT_ALL, "activate deferred microcode"},
+    {0x1a, PDT_ALL, "enable expander comms protocol and echo buffer"},
+    {0x1b, PDT_ALL, "disable expander comms protocol"},
+    {0x1c, PDT_ALL, "download application client error history"},
+    {0xffff, 0, NULL},
+};
+
+/* Read position (SSC) [0x34] service actions, need prefix */
+struct sg_lib_value_name_t sg_lib_read_pos_arr[] = {
+    {0x0, PDT_TAPE, "short form - block id"},
+    {0x1, PDT_TAPE, "short form - vendor specific"},
+    {0x6, PDT_TAPE, "long form"},
+    {0x8, PDT_TAPE, "extended form"},
+    {0xffff, 0, NULL},
+};
+
+/* Maintenance in [0xa3] service actions */
+struct sg_lib_value_name_t sg_lib_maint_in_arr[] = {
+    {0x0, PDT_SAC, "Report assigned/unassigned p_extent"},
+    {0x0, PDT_ADC, "Report automation device attributes"},
+    {0x1, PDT_SAC, "Report component device"},
+    {0x2, PDT_SAC, "Report component device attachments"},
+    {0x3, PDT_SAC, "Report peripheral device"},
+    {0x4, PDT_SAC, "Report peripheral device associations"},
+    {0x5, PDT_ALL, "Report identifying information"},
+                /* was "Report device identifier" prior to spc4r07 */
+    {0x6, PDT_SAC, "Report states"},
+    {0x7, PDT_SAC, "Report device identification"},
+    {0x8, PDT_SAC, "Report unconfigured capacity"},
+    {0x9, PDT_SAC, "Report supported configuration method"},
+    {0xa, PDT_ALL, "Report target port groups"},
+    {0xb, PDT_ALL, "Report aliases"},
+    {0xc, PDT_ALL, "Report supported operation codes"},
+    {0xd, PDT_ALL, "Report supported task management functions"},
+    {0xe, PDT_ALL, "Report priority"},
+    {0xf, PDT_ALL, "Report timestamp"},
+    {0x10, PDT_ALL, "Management protocol in"},
+    {0x1d, PDT_DISK, "Report provisioning initialization pattern"},
+        /* added in sbc4r07, shares sa 0x1d with ssc5r01 (tape) */
+    {0x1d, PDT_TAPE, "Receive recommended access order"},
+    {0x1e, PDT_TAPE, "Read dynamic runtime attribute"},
+    {0x1e, PDT_ADC, "Report automation device attributes"},
+    {0x1f, PDT_ALL, "Maintenance in vendor specific"},
+    {0xffff, 0, NULL},
+};
+
+/* Maintenance out [0xa4] service actions */
+struct sg_lib_value_name_t sg_lib_maint_out_arr[] = {
+    {0x0, PDT_SAC, "Add peripheral device / component device"},
+    {0x0, PDT_ADC, "Set automation device attribute"},
+    {0x1, PDT_SAC, "Attach to component device"},
+    {0x2, PDT_SAC, "Exchange p_extent"},
+    {0x3, PDT_SAC, "Exchange peripheral device / component device"},
+    {0x4, PDT_SAC, "Instruct component device"},
+    {0x5, PDT_SAC, "Remove peripheral device / component device"},
+    {0x6, PDT_ALL, "Set identifying information"},
+                /* was "Set device identifier" prior to spc4r07 */
+    {0x7, PDT_SAC, "Break peripheral device / component device"},
+    {0xa, PDT_ALL, "Set target port groups"},
+    {0xb, PDT_ALL, "Change aliases"},
+    {0xc, PDT_ALL, "Remove I_T nexus"},
+    {0xe, PDT_ALL, "Set priority"},
+    {0xf, PDT_ALL, "Set timestamp"},
+    {0x10, PDT_ALL, "Management protocol out"},
+    {0x1d, PDT_TAPE, "Generate recommended access order"},
+    {0x1e, PDT_TAPE, "write dynamic runtime attribute"},
+    {0x1e, PDT_ADC, "Set automation device attributes"},
+    {0x1f, PDT_ALL, "Maintenance out vendor specific"},
+    {0xffff, 0, NULL},
+};
+
+/* Sanitize [0x48] service actions, need prefix */
+struct sg_lib_value_name_t sg_lib_sanitize_sa_arr[] = {
+    {0x1, PDT_ALL, "overwrite"},
+    {0x2, PDT_ALL, "block erase"},
+    {0x3, PDT_ALL, "cryptographic erase"},
+    {0x1f, PDT_ALL, "exit failure mode"},
+    {0xffff, 0, NULL},
+};
+
+/* Service action in(12) [0xab] service actions */
+struct sg_lib_value_name_t sg_lib_serv_in12_arr[] = {
+    {0x1, PDT_ALL, "Read media serial number"},
+    {0xffff, 0, NULL},
+};
+
+/* Service action out(12) [0xa9] service actions */
+struct sg_lib_value_name_t sg_lib_serv_out12_arr[] = {
+    {0x1f, PDT_ADC, "Set medium attribute"},
+    {0xff, PDT_ALL, "Impossible command name"},
+    {0xffff, 0, NULL},
+};
+
+/* Service action in(16) [0x9e] service actions */
+struct sg_lib_value_name_t sg_lib_serv_in16_arr[] = {
+    {0xf, PDT_ALL, "Receive binding report"}, /* added spc5r11 */
+    {0x10, PDT_ALL, "Read capacity(16)"},
+    {0x11, PDT_ALL, "Read long(16)"},         /* obsolete in SBC-4 r7 */
+    {0x12, PDT_ALL, "Get LBA status(16)"},/* also 32 byte variant sbc4r14 */
+    {0x13, PDT_ALL, "Report referrals"},
+    {0x14, PDT_ALL, "Stream control"},
+    {0x15, PDT_ALL, "Background control"},
+    {0x16, PDT_ALL, "Get stream status"},
+    {0x17, PDT_ALL, "Get physical element status"},   /* added sbc4r13 */
+    {0x18, PDT_ALL, "Remove element and truncate"},   /* added sbc4r13 */
+    {0x19, PDT_ALL, "Restore elements and rebuild"},  /* added sbc4r19 */
+    {0x1a, PDT_ALL, "Remove element and modify zones"},   /* added zbc2r07 */
+    {0xffff, 0, NULL},
+};
+
+/* Service action out(16) [0x9f] service actions */
+struct sg_lib_value_name_t sg_lib_serv_out16_arr[] = {
+    {0x0b, PDT_ALL, "Test bind"},             /* added spc5r13 */
+    {0x0c, PDT_ALL, "Prepare bind report"},   /* added spc5r11 */
+    {0x0d, PDT_ALL, "Set affiliation"},
+    {0x0e, PDT_ALL, "Bind"},
+    {0x0f, PDT_ALL, "Unbind"},
+    {0x11, PDT_ALL, "Write long(16)"},
+    {0x12, PDT_ALL, "Write scattered(16)"},   /* added sbc4r11 */
+    {0x14, PDT_DISK_ZBC, "Reset write pointer"},
+    {0x1f, PDT_ADC, "Notify data transfer device(16)"},
+    {0xffff, 0, NULL},
+};
+
+/* Service action bidirectional [0x9d] service actions */
+struct sg_lib_value_name_t sg_lib_serv_bidi_arr[] = {
+    {0xffff, 0, NULL},
+};
+
+/* Persistent reserve in [0x5e] service actions, need prefix */
+struct sg_lib_value_name_t sg_lib_pr_in_arr[] = {
+    {0x0, PDT_ALL, "read keys"},
+    {0x1, PDT_ALL, "read reservation"},
+    {0x2, PDT_ALL, "report capabilities"},
+    {0x3, PDT_ALL, "read full status"},
+    {0xffff, 0, NULL},
+};
+
+/* Persistent reserve out [0x5f] service actions, need prefix */
+struct sg_lib_value_name_t sg_lib_pr_out_arr[] = {
+    {0x0, PDT_ALL, "register"},
+    {0x1, PDT_ALL, "reserve"},
+    {0x2, PDT_ALL, "release"},
+    {0x3, PDT_ALL, "clear"},
+    {0x4, PDT_ALL, "preempt"},
+    {0x5, PDT_ALL, "preempt and abort"},
+    {0x6, PDT_ALL, "register and ignore existing key"},
+    {0x7, PDT_ALL, "register and move"},
+    {0x8, PDT_ALL, "replace lost reservation"},
+    {0xffff, 0, NULL},
+};
+
+/* Third party copy in [0x83] service actions
+ * Opcode 'Receive copy results' was renamed 'Third party copy in' in spc4r34
+ * LID1 is an abbreviation of List Identifier length of 1 byte. In SPC-5
+ * LID1 discontinued (references back to SPC-4) and "(LID4)" suffix removed
+ * as there is no need to differentiate. */
+struct sg_lib_value_name_t sg_lib_xcopy_sa_arr[] = {    /* originating */
+    {0x0, PDT_ALL, "Extended copy(LID1)"},
+    {0x1, PDT_ALL, "Extended copy"}, /* 'Extended copy(LID4)' until spc5r01 */
+    {0x10, PDT_ALL, "Populate token"},
+    {0x11, PDT_ALL, "Write using token"},
+    {0x16, PDT_TAPE, "Set tape stream mirroring"},     /* ADC-4 and SSC-5 */
+    {0x1c, PDT_ALL, "Copy operation abort"},
+    {0xffff, 0, NULL},
+};
+
+/* Third party copy out [0x84] service actions
+ * Opcode 'Extended copy' was renamed 'Third party copy out' in spc4r34
+ * LID4 is an abbreviation of List Identifier length of 4 bytes */
+struct sg_lib_value_name_t sg_lib_rec_copy_sa_arr[] = { /* retrieve */
+    {0x0, PDT_ALL, "Receive copy status(LID1)"},
+    {0x1, PDT_ALL, "Receive copy data(LID1)"},
+    {0x3, PDT_ALL, "Receive copy operating parameters"},
+    {0x4, PDT_ALL, "Receive copy failure details(LID1)"},
+    {0x5, PDT_ALL, "Receive copy status"},/* was: Receive copy status(LID4) */
+    {0x6, PDT_ALL, "Receive copy data"},  /* was: Receive copy data(LID4) */
+    {0x7, PDT_ALL, "Receive ROD token information"},
+    {0x8, PDT_ALL, "Report all ROD tokens"},
+    {0x16, PDT_TAPE, "Report tape stream mirroring"},  /* SSC-5 */
+    {0xffff, 0, NULL},
+};
+
+/* Variable length cdb [0x7f] service actions (more than 16 bytes long) */
+struct sg_lib_value_name_t sg_lib_variable_length_arr[] = {
+    {0x1, PDT_ALL, "Rebuild(32)"},
+    {0x2, PDT_ALL, "Regenerate(32)"},
+    {0x3, PDT_ALL, "Xdread(32)"},             /* obsolete in SBC-3 r31 */
+    {0x4, PDT_ALL, "Xdwrite(32)"},            /* obsolete in SBC-3 r31 */
+    {0x5, PDT_ALL, "Xdwrite extended(32)"},   /* obsolete in SBC-4 r15 */
+    {0x6, PDT_ALL, "Xpwrite(32)"},            /* obsolete in SBC-4 r15 */
+    {0x7, PDT_ALL, "Xdwriteread(32)"},        /* obsolete in SBC-4 r15 */
+    {0x8, PDT_ALL, "Xdwrite extended(64)"},   /* obsolete in SBC-4 r15 */
+    {0x9, PDT_ALL, "Read(32)"},
+    {0xa, PDT_ALL, "Verify(32)"},
+    {0xb, PDT_ALL, "Write(32)"},
+    {0xc, PDT_ALL, "Write and verify(32)"},
+    {0xd, PDT_ALL, "Write same(32)"},
+    {0xe, PDT_ALL, "Orwrite(32)"},            /* added sbc3r25 */
+    {0xf, PDT_ALL, "Atomic write(32)"},       /* added sbc4r02 */
+    {0x10, PDT_ALL, "Write stream(32)"},      /* added sbc4r07 */
+    {0x11, PDT_ALL, "Write scattered(32)"},   /* added sbc4r11 */
+    {0x12, PDT_ALL, "Get LBA status(32)"},    /* added sbc4r14 */
+    {0x1800, PDT_ALL, "Receive credential"},
+    {0x1ff0, PDT_ALL, "ATA pass-through(32)"},/* added sat4r05 */
+    {0x8801, PDT_ALL, "Format OSD (osd)"},
+    {0x8802, PDT_ALL, "Create (osd)"},
+    {0x8803, PDT_ALL, "List (osd)"},
+    {0x8805, PDT_ALL, "Read (osd)"},
+    {0x8806, PDT_ALL, "Write (osd)"},
+    {0x8807, PDT_ALL, "Append (osd)"},
+    {0x8808, PDT_ALL, "Flush (osd)"},
+    {0x880a, PDT_ALL, "Remove (osd)"},
+    {0x880b, PDT_ALL, "Create partition (osd)"},
+    {0x880c, PDT_ALL, "Remove partition (osd)"},
+    {0x880e, PDT_ALL, "Get attributes (osd)"},
+    {0x880f, PDT_ALL, "Set attributes (osd)"},
+    {0x8812, PDT_ALL, "Create and write (osd)"},
+    {0x8815, PDT_ALL, "Create collection (osd)"},
+    {0x8816, PDT_ALL, "Remove collection (osd)"},
+    {0x8817, PDT_ALL, "List collection (osd)"},
+    {0x8818, PDT_ALL, "Set key (osd)"},
+    {0x8819, PDT_ALL, "Set master key (osd)"},
+    {0x881a, PDT_ALL, "Flush collection (osd)"},
+    {0x881b, PDT_ALL, "Flush partition (osd)"},
+    {0x881c, PDT_ALL, "Flush OSD (osd)"},
+    {0x8880, PDT_ALL, "Object structure check (osd-2)"},
+    {0x8881, PDT_ALL, "Format OSD (osd-2)"},
+    {0x8882, PDT_ALL, "Create (osd-2)"},
+    {0x8883, PDT_ALL, "List (osd-2)"},
+    {0x8884, PDT_ALL, "Punch (osd-2)"},
+    {0x8885, PDT_ALL, "Read (osd-2)"},
+    {0x8886, PDT_ALL, "Write (osd-2)"},
+    {0x8887, PDT_ALL, "Append (osd-2)"},
+    {0x8888, PDT_ALL, "Flush (osd-2)"},
+    {0x8889, PDT_ALL, "Clear (osd-2)"},
+    {0x888a, PDT_ALL, "Remove (osd-2)"},
+    {0x888b, PDT_ALL, "Create partition (osd-2)"},
+    {0x888c, PDT_ALL, "Remove partition (osd-2)"},
+    {0x888e, PDT_ALL, "Get attributes (osd-2)"},
+    {0x888f, PDT_ALL, "Set attributes (osd-2)"},
+    {0x8892, PDT_ALL, "Create and write (osd-2)"},
+    {0x8895, PDT_ALL, "Create collection (osd-2)"},
+    {0x8896, PDT_ALL, "Remove collection (osd-2)"},
+    {0x8897, PDT_ALL, "List collection (osd-2)"},
+    {0x8898, PDT_ALL, "Set key (osd-2)"},
+    {0x8899, PDT_ALL, "Set master key (osd-2)"},
+    {0x889a, PDT_ALL, "Flush collection (osd-2)"},
+    {0x889b, PDT_ALL, "Flush partition (osd-2)"},
+    {0x889c, PDT_ALL, "Flush OSD (osd-2)"},
+    {0x88a0, PDT_ALL, "Query (osd-2)"},
+    {0x88a1, PDT_ALL, "Remove member objects (osd-2)"},
+    {0x88a2, PDT_ALL, "Get member attributes (osd-2)"},
+    {0x88a3, PDT_ALL, "Set member attributes (osd-2)"},
+    {0x88b1, PDT_ALL, "Read map (osd-2)"},
+    {0x8f7c, PDT_ALL, "Perform SCSI command (osd-2)"},
+    {0x8f7d, PDT_ALL, "Perform task management function (osd-2)"},
+    {0x8f7e, PDT_ALL, "Perform SCSI command (osd)"},
+    {0x8f7f, PDT_ALL, "Perform task management function (osd)"},
+    {0xffff, 0, NULL},
+};
+
+/* Zoning out [0x94] service actions */
+struct sg_lib_value_name_t sg_lib_zoning_out_arr[] = {
+    {0x1, PDT_DISK_ZBC, "Close zone"},
+    {0x2, PDT_DISK_ZBC, "Finish zone"},
+    {0x3, PDT_DISK_ZBC, "Open zone"},
+    {0x4, PDT_DISK_ZBC, "Reset write pointer"},
+    {0x10, PDT_DISK_ZBC, "Sequentialize zone"},      /* zbc2r01b */
+    {0xffff, 0, NULL},
+};
+
+/* Zoning in [0x95] service actions */
+struct sg_lib_value_name_t sg_lib_zoning_in_arr[] = {
+    {0x0, PDT_DISK_ZBC, "Report zones"},
+    {0x6, PDT_DISK_ZBC, "Report realms"},            /* zbc2r04 */
+    {0x7, PDT_DISK_ZBC, "Report zone domains"},      /* zbc2r04 */
+    {0x8, PDT_DISK_ZBC, "Zone activate"},            /* zbc2r04 */
+    {0x9, PDT_DISK_ZBC, "Zone query"},               /* zbc2r04 */
+    {0xffff, 0, NULL},
+};
+
+const char * sg_lib_tapealert_strs[] = {
+    "<parameter code 0, unknown>",              /* 0x0 */
+    "Read warning",
+    "Write warning",
+    "Hard error",
+    "Media",
+    "Read failure",
+    "Write failure",
+    "Media life",
+    "Not data grade",                           /* 0x8 */
+    "Write protect",
+    "No removal",
+    "Cleaning media",
+    "Unsupported format",
+    "Recoverable mechanical cartridge failure",
+    "Unrecoverable mechanical cartridge failure",
+    "Memory chip in cartridge failure",
+    "Forced eject",                             /* 0x10 */
+    "Read only format",
+    "Tape directory corrupted on load",
+    "Nearing media life",
+    "Cleaning required",
+    "Cleaning requested",
+    "Expired cleaning media",
+    "Invalid cleaning tape",
+    "Retension requested",                      /* 0x18 */
+    "Dual port interface error",
+    "Cooling fan failing",
+    "Power supply failure",
+    "Power consumption",
+    "Drive maintenance",
+    "Hardware A",
+    "Hardware B",
+    "Interface",                                /* 0x20 */
+    "Eject media",
+    "Microcode update fail",
+    "Drive humidity",
+    "Drive temperature",
+    "Drive voltage",
+    "Predictive failure",
+    "Diagnostics required",
+    "Reserved (28h)",                           /* 0x28 */
+    "Reserved (29h)",
+    "Reserved (2Ah)",
+    "Reserved (2Bh)",
+    "Reserved (2Ch)",
+    "Reserved (2Dh)",
+    "Reserved (2Eh)",
+    "External data encryption control - communications failure",
+    "External data encryption control - key manager returned error",/* 0x30 */
+    "Diminished native capacity",
+    "Lost statistics",
+    "Tape directory invalid at unload",
+    "Tape system area write failure",
+    "Tape system area read failure",
+    "No start of data",
+    "Loading failure",
+    "Unrecoverable unload failure",             /* 0x38 */
+    "Automation interface failure",
+    "Firmware failure",
+    "WORM medium - integrity check failed",
+    "WORM medium - overwrite attempted",
+    "Encryption policy violation",
+    "Reserved (3Eh)",
+    "Reserved (3Fh)",
+    "Reserved (40h)",                           /* 0x40 */
+    NULL,
+};
+
+/* Read attribute [0x8c] service actions */
+struct sg_lib_value_name_t sg_lib_read_attr_arr[] = {
+    {0x0, PDT_ALL, "attribute values"},
+    {0x1, PDT_ALL, "attribute list"},
+    {0x2, PDT_ALL, "logical volume list"},
+    {0x3, PDT_ALL, "partition list"},
+    {0x5, PDT_ALL, "supported attributes"},
+    {0xffff, 0, NULL},
+};
+
+#else   /* SG_SCSI_STRINGS */
+
+struct sg_lib_value_name_t sg_lib_normal_opcodes[] = {
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_read_buff_arr[] = {  /* opcode 0x3c */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_write_buff_arr[] = {  /* opcode 0x3b */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_read_pos_arr[] = {  /* opcode 0x34 (SSC) */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_maint_in_arr[] = {  /* opcode 0xa3 */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_maint_out_arr[] = {  /* opcode 0xa4 */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_sanitize_sa_arr[] = {  /* opcode 0x94 */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_serv_in12_arr[] = { /* opcode 0xab */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_serv_out12_arr[] = { /* opcode 0xa9 */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_serv_in16_arr[] = { /* opcode 0x9e */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_serv_out16_arr[] = { /* opcode 0x9f */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_serv_bidi_arr[] = { /* opcode 0x9d */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_pr_in_arr[] = { /* opcode 0x5e */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_pr_out_arr[] = { /* opcode 0x5f */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_xcopy_sa_arr[] = { /* opcode 0x83 */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_rec_copy_sa_arr[] = { /* opcode 0x84 */
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_variable_length_arr[] = {
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_zoning_out_arr[] = {
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_zoning_in_arr[] = {
+    {0xffff, 0, NULL},
+};
+
+struct sg_lib_value_name_t sg_lib_read_attr_arr[] = {
+    {0xffff, 0, NULL},
+};
+
+const char * sg_lib_tapealert_strs[] = {
+    NULL,
+};
+
+#endif  /* SG_SCSI_STRINGS */
+
+/* A conveniently formatted list of SCSI ASC/ASCQ codes and their
+ * corresponding text can be found at: www.t10.org/lists/asc-num.txt
+ * The following should match asc-num.txt dated 20200817 */
+
+#ifdef SG_SCSI_STRINGS
+struct sg_lib_asc_ascq_range_t sg_lib_asc_ascq_range[] =
+{
+    {0x40,0x01,0x7f,"Ram failure [0x%x]"},
+    {0x40,0x80,0xff,"Diagnostic failure on component [0x%x]"},
+    {0x41,0x01,0xff,"Data path failure [0x%x]"},
+    {0x42,0x01,0xff,"Power-on or self-test failure [0x%x]"},
+    {0x4d,0x00,0xff,"Tagged overlapped commands [0x%x]"},
+    {0x70,0x00,0xff,"Decompression exception short algorithm id of 0x%x"},
+    {0, 0, 0, NULL}
+};
+
+struct sg_lib_asc_ascq_t sg_lib_asc_ascq[] =
+{
+    {0x00,0x00,"No additional sense information"},
+    {0x00,0x01,"Filemark detected"},
+    {0x00,0x02,"End-of-partition/medium detected"},
+    {0x00,0x03,"Setmark detected"},
+    {0x00,0x04,"Beginning-of-partition/medium detected"},
+    {0x00,0x05,"End-of-data detected"},
+    {0x00,0x06,"I/O process terminated"},
+    {0x00,0x07,"Programmable early warning detected"},
+    {0x00,0x11,"Audio play operation in progress"},
+    {0x00,0x12,"Audio play operation paused"},
+    {0x00,0x13,"Audio play operation successfully completed"},
+    {0x00,0x14,"Audio play operation stopped due to error"},
+    {0x00,0x15,"No current audio status to return"},
+    {0x00,0x16,"operation in progress"},
+    {0x00,0x17,"Cleaning requested"},
+    {0x00,0x18,"Erase operation in progress"},
+    {0x00,0x19,"Locate operation in progress"},
+    {0x00,0x1a,"Rewind operation in progress"},
+    {0x00,0x1b,"Set capacity operation in progress"},
+    {0x00,0x1c,"Verify operation in progress"},
+    {0x00,0x1d,"ATA pass through information available"},
+    {0x00,0x1e,"Conflicting SA creation request"},
+    {0x00,0x1f,"Logical unit transitioning to another power condition"},
+    {0x00,0x20,"Extended copy information available"},
+    {0x00,0x21,"Atomic command aborted due to ACA"},
+    {0x00,0x22,"Deferred microcode is pending"},
+    {0x01,0x00,"No index/sector signal"},
+    {0x02,0x00,"No seek complete"},
+    {0x03,0x00,"Peripheral device write fault"},
+    {0x03,0x01,"No write current"},
+    {0x03,0x02,"Excessive write errors"},
+    {0x04,0x00,"Logical unit not ready, cause not reportable"},
+    {0x04,0x01,"Logical unit is in process of becoming ready"},
+    {0x04,0x02,"Logical unit not ready, "
+                "initializing command required"},
+    {0x04,0x03,"Logical unit not ready, "
+                "manual intervention required"},
+    {0x04,0x04,"Logical unit not ready, format in progress"},
+    {0x04,0x05,"Logical unit not ready, rebuild in progress"},
+    {0x04,0x06,"Logical unit not ready, recalculation in progress"},
+    {0x04,0x07,"Logical unit not ready, operation in progress"},
+    {0x04,0x08,"Logical unit not ready, long write in progress"},
+    {0x04,0x09,"Logical unit not ready, self-test in progress"},
+    {0x04,0x0a,"Logical unit "
+                "not accessible, asymmetric access state transition"},
+    {0x04,0x0b,"Logical unit "
+                "not accessible, target port in standby state"},
+    {0x04,0x0c,"Logical unit "
+                "not accessible, target port in unavailable state"},
+    {0x04,0x0d,"Logical unit not ready, structure check required"},
+    {0x04,0x0e,"Logical unit not ready, security session in progress"},
+    {0x04,0x10,"Logical unit not ready, "
+                "auxiliary memory not accessible"},
+    {0x04,0x11,"Logical unit not ready, "
+                "notify (enable spinup) required"},
+    {0x04,0x12,"Logical unit not ready, offline"},
+    {0x04,0x13,"Logical unit not ready, SA creation in progress"},
+    {0x04,0x14,"Logical unit not ready, space allocation in progress"},
+    {0x04,0x15,"Logical unit not ready, robotics disabled"},
+    {0x04,0x16,"Logical unit not ready, configuration required"},
+    {0x04,0x17,"Logical unit not ready, calibration required"},
+    {0x04,0x18,"Logical unit not ready, a door is open"},
+    {0x04,0x19,"Logical unit not ready, operating in sequential mode"},
+    {0x04,0x1a,"Logical unit not ready, start stop unit command in progress"},
+    {0x04,0x1b,"Logical unit not ready, sanitize in progress"},
+    {0x04,0x1c,"Logical unit not ready, additional power use not yet "
+                "granted"},
+    {0x04,0x1d,"Logical unit not ready, configuration in progress"},
+    {0x04,0x1e,"Logical unit not ready, microcode activation required"},
+    {0x04,0x1f,"Logical unit not ready, microcode download required"},
+    {0x04,0x20,"Logical unit not ready, logical unit reset required"},
+    {0x04,0x21,"Logical unit not ready, hard reset required"},
+    {0x04,0x22,"Logical unit not ready, power cycle required"},
+    {0x04,0x23,"Logical unit not ready, affiliation required"},
+    {0x04,0x24,"Depopulation in progress"},             /* spc5r15 */
+    {0x04,0x25,"Depopulation restoration in progress"}, /* spc6r02 */
+    {0x05,0x00,"Logical unit does not respond to selection"},
+    {0x06,0x00,"No reference position found"},
+    {0x07,0x00,"Multiple peripheral devices selected"},
+    {0x08,0x00,"Logical unit communication failure"},
+    {0x08,0x01,"Logical unit communication time-out"},
+    {0x08,0x02,"Logical unit communication parity error"},
+    {0x08,0x03,"Logical unit communication CRC error (Ultra-DMA/32)"},
+    {0x08,0x04,"Unreachable copy target"},
+    {0x09,0x00,"Track following error"},
+    {0x09,0x01,"Tracking servo failure"},
+    {0x09,0x02,"Focus servo failure"},
+    {0x09,0x03,"Spindle servo failure"},
+    {0x09,0x04,"Head select fault"},
+    {0x09,0x05,"Vibration induced tracking error"},
+    {0x0A,0x00,"Error log overflow"},
+    {0x0B,0x00,"Warning"},
+    {0x0B,0x01,"Warning - specified temperature exceeded"},
+    {0x0B,0x02,"Warning - enclosure degraded"},
+    {0x0B,0x03,"Warning - background self-test failed"},
+    {0x0B,0x04,"Warning - background pre-scan detected medium error"},
+    {0x0B,0x05,"Warning - background medium scan detected medium error"},
+    {0x0B,0x06,"Warning - non-volatile cache now volatile"},
+    {0x0B,0x07,"Warning - degraded power to non-volatile cache"},
+    {0x0B,0x08,"Warning - power loss expected"},
+    {0x0B,0x09,"Warning - device statistics notification active"},
+    {0x0B,0x0A,"Warning - high critical temperature limit exceeded"},
+    {0x0B,0x0B,"Warning - low critical temperature limit exceeded"},
+    {0x0B,0x0C,"Warning - high operating temperature limit exceeded"},
+    {0x0B,0x0D,"Warning - low operating temperature limit exceeded"},
+    {0x0B,0x0E,"Warning - high critical humidity limit exceeded"},
+    {0x0B,0x0F,"Warning - low critical humidity limit exceeded"},
+    {0x0B,0x10,"Warning - high operating humidity limit exceeded"},
+    {0x0B,0x11,"Warning - low operating humidity limit exceeded"},
+    {0x0B,0x12,"Warning - microcode security at risk"},
+    {0x0B,0x13,"Warning - microcode digital signature validation failure"},
+    {0x0B,0x14,"Warning - physical element status change"},     /* spc5r15 */
+    {0x0C,0x00,"Write error"},
+    {0x0C,0x01,"Write error - recovered with auto reallocation"},
+    {0x0C,0x02,"Write error - auto reallocation failed"},
+    {0x0C,0x03,"Write error - recommend reassignment"},
+    {0x0C,0x04,"Compression check miscompare error"},
+    {0x0C,0x05,"Data expansion occurred during compression"},
+    {0x0C,0x06,"Block not compressible"},
+    {0x0C,0x07,"Write error - recovery needed"},
+    {0x0C,0x08,"Write error - recovery failed"},
+    {0x0C,0x09,"Write error - loss of streaming"},
+    {0x0C,0x0A,"Write error - padding blocks added"},
+    {0x0C,0x0B,"Auxiliary memory write error"},
+    {0x0C,0x0C,"Write error - unexpected unsolicited data"},
+    {0x0C,0x0D,"Write error - not enough unsolicited data"},
+    {0x0C,0x0E,"Multiple write errors"},
+    {0x0C,0x0F,"Defects in error window"},
+    {0x0C,0x10,"Incomplete multiple atomic write operations"},
+    {0x0C,0x11,"Write error - recovery scan needed"},
+    {0x0C,0x12,"Write error - insufficient zone resources"},
+    {0x0D,0x00,"Error detected by third party temporary initiator"},
+    {0x0D,0x01,"Third party device failure"},
+    {0x0D,0x02,"Copy target device not reachable"},
+    {0x0D,0x03,"Incorrect copy target device type"},
+    {0x0D,0x04,"Copy target device data underrun"},
+    {0x0D,0x05,"Copy target device data overrun"},
+    {0x0E,0x00,"Invalid information unit"},
+    {0x0E,0x01,"Information unit too short"},
+    {0x0E,0x02,"Information unit too long"},
+    {0x0E,0x03,"Invalid field in command information unit"},
+    {0x10,0x00,"Id CRC or ECC error"},
+    {0x10,0x01,"Logical block guard check failed"},
+    {0x10,0x02,"Logical block application tag check failed"},
+    {0x10,0x03,"Logical block reference tag check failed"},
+    {0x10,0x04,"Logical block protection error on recover buffered data"},
+    {0x10,0x05,"Logical block protection method error"},
+    {0x11,0x00,"Unrecovered read error"},
+    {0x11,0x01,"Read retries exhausted"},
+    {0x11,0x02,"Error too long to correct"},
+    {0x11,0x03,"Multiple read errors"},
+    {0x11,0x04,"Unrecovered read error - auto reallocate failed"},
+    {0x11,0x05,"L-EC uncorrectable error"},
+    {0x11,0x06,"CIRC unrecovered error"},
+    {0x11,0x07,"Data re-synchronization error"},
+    {0x11,0x08,"Incomplete block read"},
+    {0x11,0x09,"No gap found"},
+    {0x11,0x0A,"Miscorrected error"},
+    {0x11,0x0B,"Unrecovered read error - recommend reassignment"},
+    {0x11,0x0C,"Unrecovered read error - recommend rewrite the data"},
+    {0x11,0x0D,"De-compression CRC error"},
+    {0x11,0x0E,"Cannot decompress using declared algorithm"},
+    {0x11,0x0F,"Error reading UPC/EAN number"},
+    {0x11,0x10,"Error reading ISRC number"},
+    {0x11,0x11,"Read error - loss of streaming"},
+    {0x11,0x12,"Auxiliary memory read error"},
+    {0x11,0x13,"Read error - failed retransmission request"},
+    {0x11,0x14,"Read error - LBA marked bad by application client"},
+    {0x11,0x15,"Write after sanitize required"},
+    {0x12,0x00,"Address mark not found for id field"},
+    {0x13,0x00,"Address mark not found for data field"},
+    {0x14,0x00,"Recorded entity not found"},
+    {0x14,0x01,"Record not found"},
+    {0x14,0x02,"Filemark or setmark not found"},
+    {0x14,0x03,"End-of-data not found"},
+    {0x14,0x04,"Block sequence error"},
+    {0x14,0x05,"Record not found - recommend reassignment"},
+    {0x14,0x06,"Record not found - data auto-reallocated"},
+    {0x14,0x07,"Locate operation failure"},
+    {0x15,0x00,"Random positioning error"},
+    {0x15,0x01,"Mechanical positioning error"},
+    {0x15,0x02,"Positioning error detected by read of medium"},
+    {0x16,0x00,"Data synchronization mark error"},
+    {0x16,0x01,"Data sync error - data rewritten"},
+    {0x16,0x02,"Data sync error - recommend rewrite"},
+    {0x16,0x03,"Data sync error - data auto-reallocated"},
+    {0x16,0x04,"Data sync error - recommend reassignment"},
+    {0x17,0x00,"Recovered data with no error correction applied"},
+    {0x17,0x01,"Recovered data with retries"},
+    {0x17,0x02,"Recovered data with positive head offset"},
+    {0x17,0x03,"Recovered data with negative head offset"},
+    {0x17,0x04,"Recovered data with retries and/or circ applied"},
+    {0x17,0x05,"Recovered data using previous sector id"},
+    {0x17,0x06,"Recovered data without ECC - data auto-reallocated"},
+    {0x17,0x07,"Recovered data without ECC - recommend reassignment"},
+    {0x17,0x08,"Recovered data without ECC - recommend rewrite"},
+    {0x17,0x09,"Recovered data without ECC - data rewritten"},
+    {0x18,0x00,"Recovered data with error correction applied"},
+    {0x18,0x01,"Recovered data with error corr. & retries applied"},
+    {0x18,0x02,"Recovered data - data auto-reallocated"},
+    {0x18,0x03,"Recovered data with CIRC"},
+    {0x18,0x04,"Recovered data with L-EC"},
+    {0x18,0x05,"Recovered data - recommend reassignment"},
+    {0x18,0x06,"Recovered data - recommend rewrite"},
+    {0x18,0x07,"Recovered data with ECC - data rewritten"},
+    {0x18,0x08,"Recovered data with linking"},
+    {0x19,0x00,"Defect list error"},
+    {0x19,0x01,"Defect list not available"},
+    {0x19,0x02,"Defect list error in primary list"},
+    {0x19,0x03,"Defect list error in grown list"},
+    {0x1A,0x00,"Parameter list length error"},
+    {0x1B,0x00,"Synchronous data transfer error"},
+    {0x1C,0x00,"Defect list not found"},
+    {0x1C,0x01,"Primary defect list not found"},
+    {0x1C,0x02,"Grown defect list not found"},
+    {0x1D,0x00,"Miscompare during verify operation"},
+    {0x1D,0x01,"Miscompare verify of unmapped lba"},
+    {0x1E,0x00,"Recovered id with ECC correction"},
+    {0x1F,0x00,"Partial defect list transfer"},
+    {0x20,0x00,"Invalid command operation code"},
+    {0x20,0x01,"Access denied - initiator pending-enrolled"},
+    {0x20,0x02,"Access denied - no access rights"},
+    {0x20,0x03,"Access denied - invalid mgmt id key"},
+    {0x20,0x04,"Illegal command while in write capable state"},
+    {0x20,0x05,"Write type operation while in read capable state (obs)"},
+    {0x20,0x06,"Illegal command while in explicit address mode"},
+    {0x20,0x07,"Illegal command while in implicit address mode"},
+    {0x20,0x08,"Access denied - enrollment conflict"},
+    {0x20,0x09,"Access denied - invalid LU identifier"},
+    {0x20,0x0A,"Access denied - invalid proxy token"},
+    {0x20,0x0B,"Access denied - ACL LUN conflict"},
+    {0x20,0x0C,"Illegal command when not in append-only mode"},
+    {0x20,0x0D,"Not an administrative logical unit"},
+    {0x20,0x0E,"Not a subsidiary logical unit"},
+    {0x20,0x0F,"Not a conglomerate logical unit"},
+    {0x21,0x00,"Logical block address out of range"},
+    {0x21,0x01,"Invalid element address"},
+    {0x21,0x02,"Invalid address for write"},
+    {0x21,0x03,"Invalid write crossing layer jump"},
+    {0x21,0x04,"Unaligned write command"},
+    {0x21,0x05,"Write boundary violation"},
+    {0x21,0x06,"Attempt to read invalid data"},
+    {0x21,0x07,"Read boundary violation"},
+    {0x21,0x08,"Misaligned write command"},
+    {0x21,0x09,"Attempt to access gap zone"},
+    {0x22,0x00,"Illegal function (use 20 00, 24 00, or 26 00)"},
+    {0x23,0x00,"Invalid token operation, cause not reportable"},
+    {0x23,0x01,"Invalid token operation, unsupported token type"},
+    {0x23,0x02,"Invalid token operation, remote token usage not supported"},
+    {0x23,0x03,"invalid token operation, remote rod token creation not "
+               "supported"},
+    {0x23,0x04,"Invalid token operation, token unknown"},
+    {0x23,0x05,"Invalid token operation, token corrupt"},
+    {0x23,0x06,"Invalid token operation, token revoked"},
+    {0x23,0x07,"Invalid token operation, token expired"},
+    {0x23,0x08,"Invalid token operation, token cancelled"},
+    {0x23,0x09,"Invalid token operation, token deleted"},
+    {0x23,0x0a,"Invalid token operation, invalid token length"},
+    {0x24,0x00,"Invalid field in cdb"},
+    {0x24,0x01,"CDB decryption error"},
+    {0x24,0x02,"Invalid cdb field while in explicit block model (obs)"},
+    {0x24,0x03,"Invalid cdb field while in implicit block model (obs)"},
+    {0x24,0x04,"Security audit value frozen"},
+    {0x24,0x05,"Security working key frozen"},
+    {0x24,0x06,"Nonce not unique"},
+    {0x24,0x07,"Nonce timestamp out of range"},
+    {0x24,0x08,"Invalid xcdb"},
+    {0x24,0x09,"Invalid fast format"},
+    {0x25,0x00,"Logical unit not supported"},
+    {0x26,0x00,"Invalid field in parameter list"},
+    {0x26,0x01,"Parameter not supported"},
+    {0x26,0x02,"Parameter value invalid"},
+    {0x26,0x03,"Threshold parameters not supported"},
+    {0x26,0x04,"Invalid release of persistent reservation"},
+    {0x26,0x05,"Data decryption error"},
+    {0x26,0x06,"Too many target descriptors"},
+    {0x26,0x07,"Unsupported target descriptor type code"},
+    {0x26,0x08,"Too many segment descriptors"},
+    {0x26,0x09,"Unsupported segment descriptor type code"},
+    {0x26,0x0A,"Unexpected inexact segment"},
+    {0x26,0x0B,"Inline data length exceeded"},
+    {0x26,0x0C,"Invalid operation for copy source or destination"},
+    {0x26,0x0D,"Copy segment granularity violation"},
+    {0x26,0x0E,"Invalid parameter while port is enabled"},
+    {0x26,0x0F,"Invalid data-out buffer integrity check value"},
+    {0x26,0x10,"Data decryption key fail limit reached"},
+    {0x26,0x11,"Incomplete key-associated data set"},
+    {0x26,0x12,"Vendor specific key reference not found"},
+    {0x26,0x13,"Application tag mode page is invalid"},
+    {0x26,0x14,"Tape stream mirroring prevented"},
+    {0x26,0x15,"Copy source or copy destination not authorized"},
+    {0x26,0x16,"Fast copy not possible"},
+    {0x27,0x00,"Write protected"},
+    {0x27,0x01,"Hardware write protected"},
+    {0x27,0x02,"Logical unit software write protected"},
+    {0x27,0x03,"Associated write protect"},
+    {0x27,0x04,"Persistent write protect"},
+    {0x27,0x05,"Permanent write protect"},
+    {0x27,0x06,"Conditional write protect"},
+    {0x27,0x07,"Space allocation failed write protect"},
+    {0x27,0x08,"Zone is read only"},
+    {0x28,0x00,"Not ready to ready change, medium may have changed"},
+    {0x28,0x01,"Import or export element accessed"},
+    {0x28,0x02,"Format-layer may have changed"},
+    {0x28,0x03,"Import/export element accessed, medium changed"},
+    {0x29,0x00,"Power on, reset, or bus device reset occurred"},
+    {0x29,0x01,"Power on occurred"},
+    {0x29,0x02,"SCSI bus reset occurred"},
+    {0x29,0x03,"Bus device reset function occurred"},
+    {0x29,0x04,"Device internal reset"},
+    {0x29,0x05,"Transceiver mode changed to single-ended"},
+    {0x29,0x06,"Transceiver mode changed to lvd"},
+    {0x29,0x07,"I_T nexus loss occurred"},
+    {0x2A,0x00,"Parameters changed"},
+    {0x2A,0x01,"Mode parameters changed"},
+    {0x2A,0x02,"Log parameters changed"},
+    {0x2A,0x03,"Reservations preempted"},
+    {0x2A,0x04,"Reservations released"},
+    {0x2A,0x05,"Registrations preempted"},
+    {0x2A,0x06,"Asymmetric access state changed"},
+    {0x2A,0x07,"Implicit asymmetric access state transition failed"},
+    {0x2A,0x08,"Priority changed"},
+    {0x2A,0x09,"Capacity data has changed"},
+    {0x2A,0x0c, "Error recovery attributes have changed"},
+    {0x2A,0x0d, "Data encryption capabilities changed"},
+    {0x2A,0x10,"Timestamp changed"},
+    {0x2A,0x11,"Data encryption parameters changed by another i_t nexus"},
+    {0x2A,0x12,"Data encryption parameters changed by vendor specific event"},
+    {0x2A,0x13,"Data encryption key instance counter has changed"},
+    {0x2A,0x0a,"Error history i_t nexus cleared"},
+    {0x2A,0x0b,"Error history snapshot released"},
+    {0x2A,0x14,"SA creation capabilities data has changed"},
+    {0x2A,0x15,"Medium removal prevention preempted"},
+    {0x2A,0x16,"Zone reset write pointer recommended"},
+    {0x2B,0x00,"Copy cannot execute since host cannot disconnect"},
+    {0x2C,0x00,"Command sequence error"},
+    {0x2C,0x01,"Too many windows specified"},
+    {0x2C,0x02,"Invalid combination of windows specified"},
+    {0x2C,0x03,"Current program area is not empty"},
+    {0x2C,0x04,"Current program area is empty"},
+    {0x2C,0x05,"Illegal power condition request"},
+    {0x2C,0x06,"Persistent prevent conflict"},
+    {0x2C,0x07,"Previous busy status"},
+    {0x2C,0x08,"Previous task set full status"},
+    {0x2C,0x09,"Previous reservation conflict status"},
+    {0x2C,0x0A,"Partition or collection contains user objects"},
+    {0x2C,0x0B,"Not reserved"},
+    {0x2C,0x0C,"ORWRITE generation does not match"},
+    {0x2C,0x0D,"Reset write pointer not allowed"},
+    {0x2C,0x0E,"Zone is offline"},
+    {0x2C,0x0F,"Stream not open"},
+    {0x2C,0x10,"Unwritten data in zone"},
+    {0x2C,0x11,"Descriptor format sense data required"},
+    {0x2C,0x12,"Zone is inactive"},
+    {0x2C,0x13,"Well known logical unit access required"},      /* spc6r02 */
+    {0x2D,0x00,"Overwrite error on update in place"},
+    {0x2E,0x00,"Insufficient time for operation"},
+    {0x2E,0x01,"Command timeout before processing"},
+    {0x2E,0x02,"Command timeout during processing"},
+    {0x2E,0x03,"Command timeout during processing due to error recovery"},
+    {0x2F,0x00,"Commands cleared by another initiator"},
+    {0x2F,0x01,"Commands cleared by power loss notification"},
+    {0x2F,0x02,"Commands cleared by device server"},
+    {0x2F,0x03,"Some commands cleared by queuing layer event"},
+    {0x30,0x00,"Incompatible medium installed"},
+    {0x30,0x01,"Cannot read medium - unknown format"},
+    {0x30,0x02,"Cannot read medium - incompatible format"},
+    {0x30,0x03,"Cleaning cartridge installed"},
+    {0x30,0x04,"Cannot write medium - unknown format"},
+    {0x30,0x05,"Cannot write medium - incompatible format"},
+    {0x30,0x06,"Cannot format medium - incompatible medium"},
+    {0x30,0x07,"Cleaning failure"},
+    {0x30,0x08,"Cannot write - application code mismatch"},
+    {0x30,0x09,"Current session not fixated for append"},
+    {0x30,0x0A,"Cleaning request rejected"},
+    {0x30,0x0B,"Cleaning tape expired"},
+    {0x30,0x0C,"WORM medium - overwrite attempted"},
+    {0x30,0x0D,"WORM medium - integrity check"},
+    {0x30,0x10,"Medium not formatted"},
+    {0x30,0x11,"Incompatible volume type"},
+    {0x30,0x12,"Incompatible volume qualifier"},
+    {0x30,0x13,"Cleaning volume expired"},
+    {0x31,0x00,"Medium format corrupted"},
+    {0x31,0x01,"Format command failed"},
+    {0x31,0x02,"Zoned formatting failed due to spare linking"},
+    {0x31,0x03,"Sanitize command failed"},
+    {0x31,0x04,"Depopulation failed"},               /* spc5r15 */
+    {0x31,0x05,"Depopulation restoration failed"},   /* spc6r02 */
+    {0x32,0x00,"No defect spare location available"},
+    {0x32,0x01,"Defect list update failure"},
+    {0x33,0x00,"Tape length error"},
+    {0x34,0x00,"Enclosure failure"},
+    {0x35,0x00,"Enclosure services failure"},
+    {0x35,0x01,"Unsupported enclosure function"},
+    {0x35,0x02,"Enclosure services unavailable"},
+    {0x35,0x03,"Enclosure services transfer failure"},
+    {0x35,0x04,"Enclosure services transfer refused"},
+    {0x35,0x05,"Enclosure services checksum error"},
+    {0x36,0x00,"Ribbon, ink, or toner failure"},
+    {0x37,0x00,"Rounded parameter"},
+    {0x38,0x00,"Event status notification"},
+    {0x38,0x02,"Esn - power management class event"},
+    {0x38,0x04,"Esn - media class event"},
+    {0x38,0x06,"Esn - device busy class event"},
+    {0x38,0x07,"Thin provisioning soft threshold reached"},
+    {0x38,0x08,"Depopulation interrupted"},     /* spc6r03 */
+    {0x39,0x00,"Saving parameters not supported"},
+    {0x3A,0x00,"Medium not present"},
+    {0x3A,0x01,"Medium not present - tray closed"},
+    {0x3A,0x02,"Medium not present - tray open"},
+    {0x3A,0x03,"Medium not present - loadable"},
+    {0x3A,0x04,"Medium not present - medium auxiliary memory accessible"},
+    {0x3B,0x00,"Sequential positioning error"},
+    {0x3B,0x01,"Tape position error at beginning-of-medium"},
+    {0x3B,0x02,"Tape position error at end-of-medium"},
+    {0x3B,0x03,"Tape or electronic vertical forms unit not ready"},
+    {0x3B,0x04,"Slew failure"},
+    {0x3B,0x05,"Paper jam"},
+    {0x3B,0x06,"Failed to sense top-of-form"},
+    {0x3B,0x07,"Failed to sense bottom-of-form"},
+    {0x3B,0x08,"Reposition error"},
+    {0x3B,0x09,"Read past end of medium"},
+    {0x3B,0x0A,"Read past beginning of medium"},
+    {0x3B,0x0B,"Position past end of medium"},
+    {0x3B,0x0C,"Position past beginning of medium"},
+    {0x3B,0x0D,"Medium destination element full"},
+    {0x3B,0x0E,"Medium source element empty"},
+    {0x3B,0x0F,"End of medium reached"},
+    {0x3B,0x11,"Medium magazine not accessible"},
+    {0x3B,0x12,"Medium magazine removed"},
+    {0x3B,0x13,"Medium magazine inserted"},
+    {0x3B,0x14,"Medium magazine locked"},
+    {0x3B,0x15,"Medium magazine unlocked"},
+    {0x3B,0x16,"Mechanical positioning or changer error"},
+    {0x3B,0x17,"Read past end of user object"},
+    {0x3B,0x18,"Element disabled"},
+    {0x3B,0x19,"Element enabled"},
+    {0x3B,0x1a,"Data transfer device removed"},
+    {0x3B,0x1b,"Data transfer device inserted"},
+    {0x3B,0x1c,"Too many logical objects on partition to support operation"},
+    {0x3B,0x20,"Element static information changed"},
+    {0x3D,0x00,"Invalid bits in identify message"},
+    {0x3E,0x00,"Logical unit has not self-configured yet"},
+    {0x3E,0x01,"Logical unit failure"},
+    {0x3E,0x02,"Timeout on logical unit"},
+    {0x3E,0x03,"Logical unit failed self-test"},
+    {0x3E,0x04,"Logical unit unable to update self-test log"},
+    {0x3F,0x00,"Target operating conditions have changed"},
+    {0x3F,0x01,"Microcode has been changed"},
+    {0x3F,0x02,"Changed operating definition"},
+    {0x3F,0x03,"Inquiry data has changed"},
+    {0x3F,0x04,"Component device attached"},
+    {0x3F,0x05,"Device identifier changed"},
+    {0x3F,0x06,"Redundancy group created or modified"},
+    {0x3F,0x07,"Redundancy group deleted"},
+    {0x3F,0x08,"Spare created or modified"},
+    {0x3F,0x09,"Spare deleted"},
+    {0x3F,0x0A,"Volume set created or modified"},
+    {0x3F,0x0B,"Volume set deleted"},
+    {0x3F,0x0C,"Volume set deassigned"},
+    {0x3F,0x0D,"Volume set reassigned"},
+    {0x3F,0x0E,"Reported luns data has changed"},
+    {0x3F,0x0F,"Echo buffer overwritten"},
+    {0x3F,0x10,"Medium loadable"},
+    {0x3F,0x11,"Medium auxiliary memory accessible"},
+    {0x3F,0x12,"iSCSI IP address added"},
+    {0x3F,0x13,"iSCSI IP address removed"},
+    {0x3F,0x14,"iSCSI IP address changed"},
+    {0x3F,0x15,"Inspect referrals sense descriptors"},
+    {0x3F,0x16,"Microcode has been changed without reset"},
+    {0x3F,0x17,"Zone transition to full"},
+    {0x3F,0x18,"Bind completed"},
+    {0x3F,0x19,"Bind redirected"},
+    {0x3F,0x1A,"Subsidiary binding changed"},
+
+    /*
+     * ASC 0x40, 0x41 and 0x42 overridden by "additional2" array entries
+     * for ascq > 1. Preferred error message for this group is
+     * "Diagnostic failure on component nn (80h-ffh)".
+     */
+    {0x40,0x00,"Ram failure (should use 40 nn)"},
+    {0x41,0x00,"Data path failure (should use 40 nn)"},
+    {0x42,0x00,"Power-on or self-test failure (should use 40 nn)"},
+
+    {0x43,0x00,"Message error"},
+    {0x44,0x00,"Internal target failure"},
+    {0x44,0x01,"Persistent reservation information lost"},
+    {0x44,0x71,"ATA device failed Set Features"},
+    {0x45,0x00,"Select or reselect failure"},
+    {0x46,0x00,"Unsuccessful soft reset"},
+    {0x47,0x00,"SCSI parity error"},
+    {0x47,0x01,"Data phase CRC error detected"},
+    {0x47,0x02,"SCSI parity error detected during st data phase"},
+    {0x47,0x03,"Information unit iuCRC error detected"},
+    {0x47,0x04,"Asynchronous information protection error detected"},
+    {0x47,0x05,"Protocol service CRC error"},
+    {0x47,0x06,"Phy test function in progress"},
+    {0x47,0x7F,"Some commands cleared by iSCSI protocol event"},
+    {0x48,0x00,"Initiator detected error message received"},
+    {0x49,0x00,"Invalid message error"},
+    {0x4A,0x00,"Command phase error"},
+    {0x4B,0x00,"Data phase error"},
+    {0x4B,0x01,"Invalid target port transfer tag received"},
+    {0x4B,0x02,"Too much write data"},
+    {0x4B,0x03,"Ack/nak timeout"},
+    {0x4B,0x04,"Nak received"},
+    {0x4B,0x05,"Data offset error"},
+    {0x4B,0x06,"Initiator response timeout"},
+    {0x4B,0x07,"Connection lost"},
+    {0x4B,0x08,"Data-in buffer overflow - data buffer size"},
+    {0x4B,0x09,"Data-in buffer overflow - data buffer descriptor area"},
+    {0x4B,0x0A,"Data-in buffer error"},
+    {0x4B,0x0B,"Data-out buffer overflow - data buffer size"},
+    {0x4B,0x0C,"Data-out buffer overflow - data buffer descriptor area"},
+    {0x4B,0x0D,"Data-out buffer error"},
+    {0x4B,0x0E,"PCIe fabric error"},
+    {0x4B,0x0f,"PCIe completion timeout"},
+    {0x4B,0x10,"PCIe completer abort"},
+    {0x4B,0x11,"PCIe poisoned tlp received"},
+    {0x4B,0x12,"PCIe ecrc check failed"},
+    {0x4B,0x13,"PCIe unsupported request"},
+    {0x4B,0x14,"PCIe acs violation"},
+    {0x4B,0x15,"PCIe tlp prefix blocked"},
+    {0x4C,0x00,"Logical unit failed self-configuration"},
+    /*
+     * ASC 0x4D overridden by an "additional2" array entry
+     * so there is no need to have them here.
+     */
+    /* {0x4D,0x00,"Tagged overlapped commands (nn = queue tag)"}, */
+
+    {0x4E,0x00,"Overlapped commands attempted"},
+    {0x50,0x00,"Write append error"},
+    {0x50,0x01,"Write append position error"},
+    {0x50,0x02,"Position error related to timing"},
+    {0x51,0x00,"Erase failure"},
+    {0x51,0x01,"Erase failure - incomplete erase operation detected"},
+    {0x52,0x00,"Cartridge fault"},
+    {0x53,0x00,"Media load or eject failed"},
+    {0x53,0x01,"Unload tape failure"},
+    {0x53,0x02,"Medium removal prevented"},
+    {0x53,0x03,"Medium removal prevented by data transfer element"},
+    {0x53,0x04,"Medium thread or unthread failure"},
+    {0x53,0x05,"Volume identifier invalid"},
+    {0x53,0x06,"Volume identifier missing"},
+    {0x53,0x07,"Duplicate volume identifier"},
+    {0x53,0x08,"Element status unknown"},
+    {0x53,0x09,"Data transfer device error - load failed"},
+    {0x53,0x0A,"Data transfer device error - unload failed"},
+    {0x53,0x0B,"Data transfer device error - unload missing"},
+    {0x53,0x0C,"Data transfer device error - eject failed"},
+    {0x53,0x0D,"Data transfer device error - library communication failed"},
+    {0x54,0x00,"SCSI to host system interface failure"},
+    {0x55,0x00,"System resource failure"},
+    {0x55,0x01,"System buffer full"},
+    {0x55,0x02,"Insufficient reservation resources"},
+    {0x55,0x03,"Insufficient resources"},
+    {0x55,0x04,"Insufficient registration resources"},
+    {0x55,0x05,"Insufficient access control resources"},
+    {0x55,0x06,"Auxiliary memory out of space"},
+    {0x55,0x07,"Quota error"},
+    {0x55,0x08,"Maximum number of supplemental decryption keys exceeded"},
+    {0x55,0x09,"Medium auxiliary memory not accessible"},
+    {0x55,0x0a,"Data currently unavailable"},
+    {0x55,0x0b,"Insufficient power for operation"},
+    {0x55,0x0c,"Insufficient resources to create rod"},
+    {0x55,0x0d,"Insufficient resources to create rod token"},
+    {0x55,0x0e,"Insufficient zone resources"},
+    {0x55,0x0f,"Insufficient zone resources to complete write"},
+    {0x55,0x10,"Maximum number of streams open"},
+    {0x55,0x11,"Insufficient resources to bind"},
+    {0x57,0x00,"Unable to recover table-of-contents"},
+    {0x58,0x00,"Generation does not exist"},
+    {0x59,0x00,"Updated block read"},
+    {0x5A,0x00,"Operator request or state change input"},
+    {0x5A,0x01,"Operator medium removal request"},
+    {0x5A,0x02,"Operator selected write protect"},
+    {0x5A,0x03,"Operator selected write permit"},
+    {0x5B,0x00,"Log exception"},
+    {0x5B,0x01,"Threshold condition met"},
+    {0x5B,0x02,"Log counter at maximum"},
+    {0x5B,0x03,"Log list codes exhausted"},
+    {0x5C,0x00,"Rpl status change"},
+    {0x5C,0x01,"Spindles synchronized"},
+    {0x5C,0x02,"Spindles not synchronized"},
+    {0x5D,0x00,"Failure prediction threshold exceeded"},
+    {0x5D,0x01,"Media failure prediction threshold exceeded"},
+    {0x5D,0x02,"Logical unit failure prediction threshold exceeded"},
+    {0x5D,0x03,"spare area exhaustion prediction threshold exceeded"},
+    {0x5D,0x10,"Hardware impending failure general hard drive failure"},
+    {0x5D,0x11,"Hardware impending failure drive error rate too high" },
+    {0x5D,0x12,"Hardware impending failure data error rate too high" },
+    {0x5D,0x13,"Hardware impending failure seek error rate too high" },
+    {0x5D,0x14,"Hardware impending failure too many block reassigns"},
+    {0x5D,0x15,"Hardware impending failure access times too high" },
+    {0x5D,0x16,"Hardware impending failure start unit times too high" },
+    {0x5D,0x17,"Hardware impending failure channel parametrics"},
+    {0x5D,0x18,"Hardware impending failure controller detected"},
+    {0x5D,0x19,"Hardware impending failure throughput performance"},
+    {0x5D,0x1A,"Hardware impending failure seek time performance"},
+    {0x5D,0x1B,"Hardware impending failure spin-up retry count"},
+    {0x5D,0x1C,"Hardware impending failure drive calibration retry count"},
+    {0x5D,0x1D,"Hardware impending failure power loss protection circuit"},
+    {0x5D,0x20,"Controller impending failure general hard drive failure"},
+    {0x5D,0x21,"Controller impending failure drive error rate too high" },
+    {0x5D,0x22,"Controller impending failure data error rate too high" },
+    {0x5D,0x23,"Controller impending failure seek error rate too high" },
+    {0x5D,0x24,"Controller impending failure too many block reassigns"},
+    {0x5D,0x25,"Controller impending failure access times too high" },
+    {0x5D,0x26,"Controller impending failure start unit times too high" },
+    {0x5D,0x27,"Controller impending failure channel parametrics"},
+    {0x5D,0x28,"Controller impending failure controller detected"},
+    {0x5D,0x29,"Controller impending failure throughput performance"},
+    {0x5D,0x2A,"Controller impending failure seek time performance"},
+    {0x5D,0x2B,"Controller impending failure spin-up retry count"},
+    {0x5D,0x2C,"Controller impending failure drive calibration retry count"},
+    {0x5D,0x30,"Data channel impending failure general hard drive failure"},
+    {0x5D,0x31,"Data channel impending failure drive error rate too high" },
+    {0x5D,0x32,"Data channel impending failure data error rate too high" },
+    {0x5D,0x33,"Data channel impending failure seek error rate too high" },
+    {0x5D,0x34,"Data channel impending failure too many block reassigns"},
+    {0x5D,0x35,"Data channel impending failure access times too high" },
+    {0x5D,0x36,"Data channel impending failure start unit times too high" },
+    {0x5D,0x37,"Data channel impending failure channel parametrics"},
+    {0x5D,0x38,"Data channel impending failure controller detected"},
+    {0x5D,0x39,"Data channel impending failure throughput performance"},
+    {0x5D,0x3A,"Data channel impending failure seek time performance"},
+    {0x5D,0x3B,"Data channel impending failure spin-up retry count"},
+    {0x5D,0x3C,"Data channel impending failure drive calibration retry count"},
+    {0x5D,0x40,"Servo impending failure general hard drive failure"},
+    {0x5D,0x41,"Servo impending failure drive error rate too high" },
+    {0x5D,0x42,"Servo impending failure data error rate too high" },
+    {0x5D,0x43,"Servo impending failure seek error rate too high" },
+    {0x5D,0x44,"Servo impending failure too many block reassigns"},
+    {0x5D,0x45,"Servo impending failure access times too high" },
+    {0x5D,0x46,"Servo impending failure start unit times too high" },
+    {0x5D,0x47,"Servo impending failure channel parametrics"},
+    {0x5D,0x48,"Servo impending failure controller detected"},
+    {0x5D,0x49,"Servo impending failure throughput performance"},
+    {0x5D,0x4A,"Servo impending failure seek time performance"},
+    {0x5D,0x4B,"Servo impending failure spin-up retry count"},
+    {0x5D,0x4C,"Servo impending failure drive calibration retry count"},
+    {0x5D,0x50,"Spindle impending failure general hard drive failure"},
+    {0x5D,0x51,"Spindle impending failure drive error rate too high" },
+    {0x5D,0x52,"Spindle impending failure data error rate too high" },
+    {0x5D,0x53,"Spindle impending failure seek error rate too high" },
+    {0x5D,0x54,"Spindle impending failure too many block reassigns"},
+    {0x5D,0x55,"Spindle impending failure access times too high" },
+    {0x5D,0x56,"Spindle impending failure start unit times too high" },
+    {0x5D,0x57,"Spindle impending failure channel parametrics"},
+    {0x5D,0x58,"Spindle impending failure controller detected"},
+    {0x5D,0x59,"Spindle impending failure throughput performance"},
+    {0x5D,0x5A,"Spindle impending failure seek time performance"},
+    {0x5D,0x5B,"Spindle impending failure spin-up retry count"},
+    {0x5D,0x5C,"Spindle impending failure drive calibration retry count"},
+    {0x5D,0x60,"Firmware impending failure general hard drive failure"},
+    {0x5D,0x61,"Firmware impending failure drive error rate too high" },
+    {0x5D,0x62,"Firmware impending failure data error rate too high" },
+    {0x5D,0x63,"Firmware impending failure seek error rate too high" },
+    {0x5D,0x64,"Firmware impending failure too many block reassigns"},
+    {0x5D,0x65,"Firmware impending failure access times too high" },
+    {0x5D,0x66,"Firmware impending failure start unit times too high" },
+    {0x5D,0x67,"Firmware impending failure channel parametrics"},
+    {0x5D,0x68,"Firmware impending failure controller detected"},
+    {0x5D,0x69,"Firmware impending failure throughput performance"},
+    {0x5D,0x6A,"Firmware impending failure seek time performance"},
+    {0x5D,0x6B,"Firmware impending failure spin-up retry count"},
+    {0x5D,0x6C,"Firmware impending failure drive calibration retry count"},
+    {0x5D,0x73,"Media impending failure endurance limit met"},
+    {0x5D,0xFF,"Failure prediction threshold exceeded (false)"},
+    {0x5E,0x00,"Low power condition on"},
+    {0x5E,0x01,"Idle condition activated by timer"},
+    {0x5E,0x02,"Standby condition activated by timer"},
+    {0x5E,0x03,"Idle condition activated by command"},
+    {0x5E,0x04,"Standby condition activated by command"},
+    {0x5E,0x05,"Idle_b condition activated by timer"},
+    {0x5E,0x06,"Idle_b condition activated by command"},
+    {0x5E,0x07,"Idle_c condition activated by timer"},
+    {0x5E,0x08,"Idle_c condition activated by command"},
+    {0x5E,0x09,"Standby_y condition activated by timer"},
+    {0x5E,0x0a,"Standby_y condition activated by command"},
+    {0x5E,0x41,"Power state change to active"},
+    {0x5E,0x42,"Power state change to idle"},
+    {0x5E,0x43,"Power state change to standby"},
+    {0x5E,0x45,"Power state change to sleep"},
+    {0x5E,0x47,"Power state change to device control"},
+    {0x60,0x00,"Lamp failure"},
+    {0x61,0x00,"Video acquisition error"},
+    {0x61,0x01,"Unable to acquire video"},
+    {0x61,0x02,"Out of focus"},
+    {0x62,0x00,"Scan head positioning error"},
+    {0x63,0x00,"End of user area encountered on this track"},
+    {0x63,0x01,"Packet does not fit in available space"},
+    {0x64,0x00,"Illegal mode for this track"},
+    {0x64,0x01,"Invalid packet size"},
+    {0x65,0x00,"Voltage fault"},
+    {0x66,0x00,"Automatic document feeder cover up"},
+    {0x66,0x01,"Automatic document feeder lift up"},
+    {0x66,0x02,"Document jam in automatic document feeder"},
+    {0x66,0x03,"Document miss feed automatic in document feeder"},
+    {0x67,0x00,"Configuration failure"},
+    {0x67,0x01,"Configuration of incapable logical units failed"},
+    {0x67,0x02,"Add logical unit failed"},
+    {0x67,0x03,"Modification of logical unit failed"},
+    {0x67,0x04,"Exchange of logical unit failed"},
+    {0x67,0x05,"Remove of logical unit failed"},
+    {0x67,0x06,"Attachment of logical unit failed"},
+    {0x67,0x07,"Creation of logical unit failed"},
+    {0x67,0x08,"Assign failure occurred"},
+    {0x67,0x09,"Multiply assigned logical unit"},
+    {0x67,0x0A,"Set target port groups command failed"},
+    {0x67,0x0B,"ATA device feature not enabled"},
+    {0x67,0x0C,"Command rejected"},
+    {0x67,0x0D,"Explicit bind not allowed"},
+    {0x68,0x00,"Logical unit not configured"},
+    {0x68,0x01,"Subsidiary logical unit not configured"},
+    {0x69,0x00,"Data loss on logical unit"},
+    {0x69,0x01,"Multiple logical unit failures"},
+    {0x69,0x02,"Parity/data mismatch"},
+    {0x6A,0x00,"Informational, refer to log"},
+    {0x6B,0x00,"State change has occurred"},
+    {0x6B,0x01,"Redundancy level got better"},
+    {0x6B,0x02,"Redundancy level got worse"},
+    {0x6C,0x00,"Rebuild failure occurred"},
+    {0x6D,0x00,"Recalculate failure occurred"},
+    {0x6E,0x00,"Command to logical unit failed"},
+    {0x6F,0x00,"Copy protection key exchange failure - authentication "
+               "failure"},
+    {0x6F,0x01,"Copy protection key exchange failure - key not present"},
+    {0x6F,0x02,"Copy protection key exchange failure - key not established"},
+    {0x6F,0x03,"Read of scrambled sector without authentication"},
+    {0x6F,0x04,"Media region code is mismatched to logical unit region"},
+    {0x6F,0x05,"Drive region must be permanent/region reset count error"},
+    {0x6F,0x06,"Insufficient block count for binding nonce recording"},
+    {0x6F,0x07,"Conflict in binding nonce recording"},
+    {0x6F,0x08,"Insufficient permission"},
+    {0x6F,0x09,"Invalid drive-host pairing server"},
+    {0x6F,0x0A,"Drive-host pairing suspended"},
+    /*
+     * ASC 0x70 overridden by an "additional2" array entry
+     * so there is no need to have them here.
+     */
+    /* {0x70,0x00,"Decompression exception short algorithm id of nn"}, */
+
+    {0x71,0x00,"Decompression exception long algorithm id"},
+    {0x72,0x00,"Session fixation error"},
+    {0x72,0x01,"Session fixation error writing lead-in"},
+    {0x72,0x02,"Session fixation error writing lead-out"},
+    {0x72,0x03,"Session fixation error - incomplete track in session"},
+    {0x72,0x04,"Empty or partially written reserved track"},
+    {0x72,0x05,"No more track reservations allowed"},
+    {0x72,0x06,"RMZ extension is not allowed"},
+    {0x72,0x07,"No more test zone extensions are allowed"},
+    {0x73,0x00,"CD control error"},
+    {0x73,0x01,"Power calibration area almost full"},
+    {0x73,0x02,"Power calibration area is full"},
+    {0x73,0x03,"Power calibration area error"},
+    {0x73,0x04,"Program memory area update failure"},
+    {0x73,0x05,"Program memory area is full"},
+    {0x73,0x06,"RMA/PMA is almost full"},
+    {0x73,0x10,"Current power calibration area almost full"},
+    {0x73,0x11,"Current power calibration area is full"},
+    {0x73,0x17,"RDZ is full"},
+    {0x74,0x00,"Security error"},
+    {0x74,0x01,"Unable to decrypt data"},
+    {0x74,0x02,"Unencrypted data encountered while decrypting"},
+    {0x74,0x03,"Incorrect data encryption key"},
+    {0x74,0x04,"Cryptographic integrity validation failed"},
+    {0x74,0x05,"Error decrypting data"},
+    {0x74,0x06,"Unknown signature verification key"},
+    {0x74,0x07,"Encryption parameters not usable"},
+    {0x74,0x08,"Digital signature validation failure"},
+    {0x74,0x09,"Encryption mode mismatch on read"},
+    {0x74,0x0a,"Encrypted block not raw read enabled"},
+    {0x74,0x0b,"Incorrect Encryption parameters"},
+    {0x74,0x0c,"Unable to decrypt parameter list"},
+    {0x74,0x0d,"Encryption algorithm disabled"},
+    {0x74,0x10,"SA creation parameter value invalid"},
+    {0x74,0x11,"SA creation parameter value rejected"},
+    {0x74,0x12,"Invalid SA usage"},
+    {0x74,0x21,"Data encryption configuration prevented"},
+    {0x74,0x30,"SA creation parameter not supported"},
+    {0x74,0x40,"Authentication failed"},
+    {0x74,0x61,"External data encryption key manager access error"},
+    {0x74,0x62,"External data encryption key manager error"},
+    {0x74,0x63,"External data encryption key not found"},
+    {0x74,0x64,"External data encryption request not authorized"},
+    {0x74,0x6e,"External data encryption control timeout"},
+    {0x74,0x6f,"External data encryption control error"},
+    {0x74,0x71,"Logical unit access not authorized"},
+    {0x74,0x79,"Security conflict in translated device"},
+    {0, 0, NULL}
+};
+
+#else   /* SG_SCSI_STRINGS */
+
+struct sg_lib_asc_ascq_range_t sg_lib_asc_ascq_range[] =
+{
+    {0, 0, 0, NULL}
+};
+
+struct sg_lib_asc_ascq_t sg_lib_asc_ascq[] =
+{
+    {0, 0, NULL}
+};
+#endif /* SG_SCSI_STRINGS */
+
+const char * sg_lib_sense_key_desc[] = {
+    "No Sense",                 /* Filemark, ILI and/or EOM; progress
+                                   indication (during FORMAT); power
+                                   condition sensing (REQUEST SENSE) */
+    "Recovered Error",          /* The last command completed successfully
+                                   but used error correction */
+    "Not Ready",                /* The addressed target is not ready */
+    "Medium Error",             /* Data error detected on the medium */
+    "Hardware Error",           /* Controller or device failure */
+    "Illegal Request",
+    "Unit Attention",           /* Removable medium was changed, or
+                                   the target has been reset */
+    "Data Protect",             /* Access to the data is blocked */
+    "Blank Check",              /* Reached unexpected written or unwritten
+                                   region of the medium */
+    "Vendor specific(9)",       /* Vendor specific */
+    "Copy Aborted",             /* COPY or COMPARE was aborted */
+    "Aborted Command",          /* The target aborted the command */
+    "Equal",                    /* SEARCH DATA found data equal (obsolete) */
+    "Volume Overflow",          /* Medium full with data to be written */
+    "Miscompare",               /* Source data and data on the medium
+                                   do not agree */
+    "Completed"                 /* may occur for successful cmd (spc4r23) */
+};
+
+const char * sg_lib_pdt_strs[32] = {    /* should have 2**5 elements */
+    /* 0 */ "disk",
+    "tape",
+    "printer",                  /* obsolete, spc5r01 */
+    "processor",        /* often SAF-TE device, copy manager */
+    "write once optical disk",  /* obsolete, spc5r01 */
+    /* 5 */ "cd/dvd",
+    "scanner",                  /* obsolete */
+    "optical memory device",
+    "medium changer",
+    "communications",           /* obsolete */
+    /* 0xa */ "graphics [0xa]", /* obsolete */
+    "graphics [0xb]",           /* obsolete */
+    "storage array controller",
+    "enclosure services device",
+    "simplified direct access device",
+    "optical card reader/writer device",
+    /* 0x10 */ "bridge controller commands",
+    "object based storage",
+    "automation/driver interface",
+    "security manager device",  /* obsolete, spc5r01 */
+    "host managed zoned block",
+    "0x15", "0x16", "0x17", "0x18",
+    "0x19", "0x1a", "0x1b", "0x1c", "0x1d",
+    "well known logical unit",
+    "unknown or no device type", /* coupled with PQ=3 for not accessible
+                                    via this lu's port (try the other) */
+};
+
+const char * sg_lib_transport_proto_strs[] =
+{
+    "Fibre Channel Protocol for SCSI (FCP-5)",  /* now at fcp5r01 */
+    "SCSI Parallel Interface (SPI-5)",  /* obsolete in spc5r01 */
+    "Serial Storage Architecture SCSI-3 Protocol (SSA-S3P)",
+    "Serial Bus Protocol for IEEE 1394 (SBP-3)",
+    "SCSI RDMA Protocol (SRP)",
+    "Internet SCSI (iSCSI)",
+    "Serial Attached SCSI Protocol (SPL-4)",
+    "Automation/Drive Interface Transport (ADT-2)",
+    "AT Attachment Interface (ACS-2)",          /* 0x8 */
+    "USB Attached SCSI (UAS-2)",
+    "SCSI over PCI Express (SOP)",
+    "PCIe",                             /* added in spc5r02 */
+    "Oxc", "Oxd", "Oxe",
+    "No specific protocol"
+};
+
+/* SCSI Feature Sets array. code->value, pdt->peri_dev_type (-1 for SPC) */
+struct sg_lib_value_name_t sg_lib_scsi_feature_sets[] =
+{
+    {SCSI_FS_SPC_DISCOVERY_2016, -1, "Discovery 2016"},
+    {SCSI_FS_SBC_BASE_2010, PDT_DISK, "SBC Base 2010"},
+    {SCSI_FS_SBC_BASE_2016, PDT_DISK, "SBC Base 2016"},
+    {SCSI_FS_SBC_BASIC_PROV_2016, PDT_DISK, "Basic provisioning 2016"},
+    {SCSI_FS_SBC_DRIVE_MAINT_2016, PDT_DISK, "Drive maintenance 2016"},
+    {SCSI_FS_ZBC_HOST_AWARE_2020, PDT_DISK_ZBC, "Host Aware 2020"},
+    {SCSI_FS_ZBC_HOST_MANAGED_2020, PDT_DISK_ZBC, "Host Managed 2020"},
+    {SCSI_FS_ZBC_DOMAINS_REALMS_2020, PDT_DISK_ZBC, "Domains and Realms 2020"},
+    {0x0, 0, NULL},     /* 0x0 is reserved sfs; trailing sentinel */
+};
+
+#if (SG_SCSI_STRINGS && HAVE_NVME && (! IGNORE_NVME))
+
+/* Commands sent to the NVMe Admin Queue (queue id 0) have the following
+ * names in the NVM Express 1.3a document dated 20171024 */
+struct sg_lib_simple_value_name_t sg_lib_nvme_admin_cmd_arr[] =
+{
+    {0x0,  "Delete I/O Submission Queue"},      /* first mandatory command */
+    {0x1,  "Create I/O Submission Queue"},
+    {0x2,  "Get Log Page"},
+    {0x4,  "Delete I/O Completion Queue"},
+    {0x5,  "Create I/O Completion Queue"},
+    {0x6,  "Identify"},
+    {0x8,  "Abort"},
+    {0x9,  "Set Features"},
+    {0xa,  "Get Features"},
+    {0xc,  "Asynchronous Event Request"},       /* last mandatory command */
+    {0xd,  "Namespace Management"},             /* first optional command */
+    {0x10, "Firmware commit"},
+    {0x11, "Firmware image download"},
+    {0x14, "Device Self-test"},
+    {0x15, "Namespace Attachment"},
+    {0x18, "Keep Alive"},
+    {0x19, "Directive Send"},
+    {0x1a, "Directive Receive"},
+    {0x1c, "Virtualization Management"},
+    {0x1d, "NVMe-MI Send"},    /* SES SEND DIAGNOSTIC cmd passes thru here */
+    {0x1e, "NVMe-MI Receive"}, /* RECEIVE DIAGNOSTIC RESULTS thru here */
+    {0x7c, "Doorbell Buffer Config"},
+    {0x7f, "NVMe over Fabrics"},
+
+    /* I/O command set specific 0x80 to 0xbf */
+    {0x80, "Format NVM"},               /* first NVM specific */
+    {0x81, "Security Send"},
+    {0x82, "Security Receive"},
+    {0x84, "Sanitize"},                 /* last NVM specific in 1.3a */
+    {0x86, "Get LBA status"},           /* NVM specific, new in 1.4 */
+    /* Vendor specific 0xc0 to 0xff */
+    {0xffff, NULL},                     /* Sentinel */
+};
+
+/* Commands sent any NVMe non-Admin Queue (queue id >0) for the NVM command
+ * set have the following names in the NVM Express 1.3a document dated
+ * 20171024 */
+struct sg_lib_simple_value_name_t sg_lib_nvme_nvm_cmd_arr[] =
+{
+    {0x0,  "Flush"},                    /* first mandatory command */
+    {0x1,  "Write"},
+    {0x2,  "Read"},                     /* last mandatory command */
+    {0x4,  "Write Uncorrectable"},      /* first optional command */
+    {0x5,  "Compare"},
+    {0x8,  "Write Zeroes"},
+    {0x9,  "Dataset Management"},
+    {0xd,  "Reservation Register"},
+    {0xe,  "Reservation Report"},
+    {0x11, "Reservation Acquire"},
+    {0x15, "Reservation Release"},      /* last optional command in 1.3a */
+
+    /* Vendor specific 0x80 to 0xff */
+    {0xffff, NULL},                     /* Sentinel */
+};
+
+
+/* .value is completion queue's DW3 as follows: ((DW3 >> 17) & 0x3ff)
+ * .peri_dev_type is an index for the sg_lib_scsi_status_sense_arr[]
+ * .name is taken from NVMe 1.3a document, section 4.6.1.2.1 with less
+ *       capitalization.
+ * NVMe term bits 31:17 of DW3 in the completion field as the "Status
+ * Field" (SF). Bit 31 is "Do not retry" (DNR) and bit 30 is "More" (M).
+ * Bits 29:28 are reserved, bit 27:25 are the "Status Code Type" (SCT)
+ * and bits 24:17 are the Status Code (SC). This table is in ascending
+ * order of its .value field so a binary search could be done on it.  */
+struct sg_lib_value_name_t sg_lib_nvme_cmd_status_arr[] =
+{
+    /* Generic command status values, Status Code Type (SCT): 0h
+     * Lowest 8 bits are the Status Code (SC), in this case:
+     *   00h - 7Fh: Applicable to Admin Command Set, or across multiple
+     *              command sets
+     *   80h - BFh: I/O Command Set Specific status codes
+     *   c0h - FFh: I/O Vendor Specific status codes            */
+    {0x0,   0, "Successful completion"},
+    {0x1,   1, "Invalid command opcode"},
+    {0x2,   2, "Invalid field in command"},
+    {0x3,   2, "Command id conflict"},
+    {0x4,   3, "Data transfer error"},
+    {0x5,   4, "Command aborted due to power loss notification"},
+    {0x6,   5, "Internal error"},
+    {0x7,   6, "Command abort requested"},
+    {0x8,   6, "Command aborted due to SQ deletion"},
+    {0x9,   6, "Command aborted due to failed fused command"},
+    {0xa,   6, "Command aborted due to missing fused command"},
+    {0xb,   7, "Invalid namespace or format"},
+    {0xc,   5, "Command sequence error"},
+    {0xd,   5, "Invalid SGL segment descriptor"},
+    {0xe,   5, "Invalid number of SGL descriptors"},
+    {0xf,   5, "Data SGL length invalid"},
+    {0x10,  5, "Metadata SGL length invalid"},
+    {0x11,  5, "SGL descriptor type invalid"},
+    {0x12,  5, "Invalid use of controller memory buffer"},
+    {0x13,  5, "PRP offset invalid"},
+    {0x14,  2, "Atomic write unit exceeded"},
+    {0x15,  8, "Operation denied"},
+    {0x16,  5, "SGL offset invalid"},
+    {0x17,  5, "Reserved [0x17]"},
+    {0x18,  5, "Host identifier inconsistent format"},
+    {0x19,  5, "Keep alive timeout expired"},
+    {0x1a,  5, "Keep alive timeout invalid"},
+    {0x1b,  6, "Command aborted due to Preempt and Abort"},
+    {0x1c, 10, "Sanitize failed"},
+    {0x1d, 11, "Sanitize in progress"},
+    {0x1e,  5, "SGL data block granularity invalid"},
+    {0x1f,  5, "Command not supported for queue in CMB"},
+    {0x20,  18, "Namespace is write protected"},        /* NVMe 1.4 */
+    {0x21,  6, "Command interrupted"},                  /* NVMe 1.4 */
+    {0x22,  5, "Transient transport error"},            /* NVMe 1.4 */
+    {0x23,  5, "Prohibited by lockdown"},               /* NVMe 2.0 */
+    {0x24,  5, "Admin command: media not ready"},       /* NVMe 2.0 */
+
+    /* 0x80 - 0xbf: I/O command set specific */
+    /* Command specific status values, NVM (I/O) Command Set */
+    {0x80, 12, "LBA out of range"},
+    {0x81,  3, "Capacity exceeded"},
+    {0x82, 13, "Namespace not ready"},
+    {0x83, 14, "Reservation conflict"},
+    {0x84, 15, "Format in progress"},
+    {0x85, 2, "Invalid value size"},
+    {0x86, 2, "Invalid key size"},
+    {0x87, 2, "KV key does not exist"},
+    {0x88, 15, "Unrecovered error"},
+    {0x89, 2, "Key exists"},
+
+    /* Command specific status values, ZNS (NVM) Command Set */
+    {0xb8, 0x1f, "Zone boundary error"},
+    {0xb9, 0x2, "Zone is full"},
+    {0xba, 0x1b, "Zone is read only"},
+    {0xbb, 0x1c, "Zone is offline"},
+    {0xbc, 2, "Zone invalid write"},
+    {0xbd, 0x20, "Too many active zones"},
+    {0xbe, 0x20, "Too many open zones"},
+    {0xbf, 2, "Invalid zone state transition"},
+    /* 0xc0 - 0xff: vendor specific */
+
+    /* Command specific status values, Status Code Type (SCT): 1h */
+    {0x100, 5, "Completion queue invalid"},
+    {0x101, 5, "Invalid queue identifier"},
+    {0x102, 5, "Invalid queue size"},
+    {0x103, 5, "Abort command limit exceeded"},
+    {0x104, 5, "Reserved [0x104]"},
+    {0x105, 5, "Asynchronous event request limit exceeded"},
+    {0x106, 5, "Invalid firmware slot"},
+    {0x107, 5, "Invalid firmware image"},
+    {0x108, 5, "Invalid interrupt vector"},
+    {0x109, 5, "Invalid log page"},
+    {0x10a,16, "Invalid format"},
+    {0x10b, 5, "Firmware activation requires conventional reset"},
+    {0x10c, 5, "Invalid queue deletion"},
+    {0x10d, 5, "Feature identifier not saveable"},
+    {0x10e, 5, "Feature not changeable"},
+    {0x10f, 5, "Feature not namespace specific"},
+    {0x110, 5, "Firmware activation requires NVM subsystem reset"},
+    {0x111, 5, "Firmware activation requires reset"},
+    {0x112, 5, "Firmware activation requires maximum time violation"},
+    {0x113, 5, "Firmware activation prohibited"},
+    {0x114, 5, "Overlapping range"},
+    {0x115, 5, "Namespace insufficient capacity"},
+    {0x116, 5, "Namespace identifier unavailable"},
+    {0x117, 5, "Reserved [0x107]"},
+    {0x118, 5, "Namespace already attached"},
+    {0x119, 5, "Namespace is private"},
+    {0x11a, 5, "Namespace not attached"},
+    {0x11b, 3, "Thin provisioning not supported"},
+    {0x11c, 3, "Controller list invalid"},
+    {0x11d,17, "Device self-test in progress"},
+    {0x11e,18, "Boot partition write prohibited"},
+    {0x11f, 5, "Invalid controller identifier"},
+    {0x120, 5, "Invalid secondary controller state"},
+    {0x121, 5, "Invalid number of controller resources"},
+    {0x122, 5, "Invalid resource identifier"},
+    {0x123, 5, "Sanitize prohibited while PM enabled"},         /* NVMe 1.4 */
+    {0x124, 5, "ANA group identifier invalid"},                 /* NVMe 1.4 */
+    {0x125, 5, "ANA attach failed"},                            /* NVMe 1.4 */
+
+    /* Command specific status values, Status Code Type (SCT): 1h
+     * for NVM (I/O) Command Set */
+    {0x180, 2, "Conflicting attributes"},
+    {0x181,19, "Invalid protection information"},
+    {0x182,18, "Attempted write to read only range"},
+    /* 0x1c0 - 0x1ff: vendor specific */
+
+    /* Media and Data Integrity error values, Status Code Type (SCT): 2h */
+    {0x280,20, "Write fault"},
+    {0x281,21, "Unrecovered read error"},
+    {0x282,22, "End-to-end guard check error"},
+    {0x283,23, "End-to-end application tag check error"},
+    {0x284,24, "End-to-end reference tag check error"},
+    {0x285,25, "Compare failure"},
+    {0x286, 8, "Access denied"},
+    {0x287,26, "Deallocated or unwritten logical block"},
+    /* 0x2c0 - 0x2ff: vendor specific */
+
+    /* Leave this Sentinel value at end of this array */
+    {0x3ff, 0, NULL},
+};
+
+/* The sg_lib_nvme_cmd_status_arr[n].peri_dev_type field is an index
+ * to this array. It allows an NVMe status (error) value to be mapped
+ * to this SCSI tuple: status, sense_key, additional sense code (asc) and
+ * asc qualifier (ascq). For brevity SAM_STAT_CHECK_CONDITION is written
+ * as 0x2. */
+struct sg_lib_4tuple_u8 sg_lib_scsi_status_sense_arr[] =
+{
+    /* SCSI Status, SCSI sense key, ASC, ASCQ */
+/* index: 0 */
+    {SAM_STAT_GOOD, SPC_SK_NO_SENSE, 0, 0},     /* it's all good */
+    {SAM_STAT_CHECK_CONDITION, SPC_SK_ILLEGAL_REQUEST, 0x20, 0x0},/* opcode */
+    {0x2, SPC_SK_ILLEGAL_REQUEST, 0x24, 0x0},   /* field in cdb */
+    {0x2, SPC_SK_MEDIUM_ERROR, 0x0, 0x0},
+    {SAM_STAT_TASK_ABORTED, SPC_SK_ABORTED_COMMAND, 0xb, 0x8},
+    {0x2, SPC_SK_HARDWARE_ERROR, 0x44, 0x0},   /* internal error */
+    {SAM_STAT_TASK_ABORTED, SPC_SK_ABORTED_COMMAND, 0x0, 0x0},
+    {0x2, SPC_SK_ILLEGAL_REQUEST, 0x20, 0x9},   /* invalid LU */
+
+/* index: 8 */
+    {0x2, SPC_SK_ILLEGAL_REQUEST, 0x20, 0x2},   /* access denied */
+    {0x2, SPC_SK_ILLEGAL_REQUEST, 0x2c, 0x0},   /* cmd sequence error */
+    {0x2, SPC_SK_MEDIUM_ERROR, 0x31, 0x3},   /* sanitize failed */ /* 10 */
+    {0x2, SPC_SK_NOT_READY, 0x4, 0x1b}, /* sanitize in progress */
+    {0x2, SPC_SK_ILLEGAL_REQUEST, 0x21, 0x0},   /* LBA out of range */
+    {0x2, SPC_SK_NOT_READY, 0x4, 0x0},  /* not reportable; 0x1: becoming */
+    {SAM_STAT_RESERVATION_CONFLICT, 0x0, 0x0, 0x0},
+    {0x2, SPC_SK_NOT_READY, 0x4, 0x4},  /* format in progress */
+
+/* index: 0x10 */
+    {0x2, SPC_SK_ILLEGAL_REQUEST, 0x31, 0x1},  /* format failed */
+    {0x2, SPC_SK_NOT_READY, 0x4, 0x9},  /* self-test in progress */
+    {0x2, SPC_SK_DATA_PROTECT, 0x27, 0x0},      /* write prohibited */
+    {0x2, SPC_SK_ILLEGAL_REQUEST, 0x10, 0x5},  /* protection info */
+    {0x2, SPC_SK_MEDIUM_ERROR, 0x3, 0x0}, /* periph dev w fault */
+    {0x2, SPC_SK_MEDIUM_ERROR, 0x11, 0x0},      /* unrecoc rd */
+    {0x2, SPC_SK_MEDIUM_ERROR, 0x10, 0x1},      /* PI guard */
+    {0x2, SPC_SK_MEDIUM_ERROR, 0x10, 0x2},      /* PI app tag */
+
+/* index: 0x18 */
+    {0x2, SPC_SK_MEDIUM_ERROR, 0x10, 0x3},      /* PI reference tag */
+    {0x2, SPC_SK_MISCOMPARE, 0x1d, 0x0},        /* during verify */
+    {0x2, SPC_SK_MEDIUM_ERROR, 0x21, 0x6},      /* read invalid data */
+    {0x2, SPC_SK_DATA_PROTECT, 0x27, 0x8},      /* zone is read only */
+    {0x2, SPC_SK_DATA_PROTECT, 0x2c, 0xe},      /* zone is offline */
+    {0x2, SPC_SK_DATA_PROTECT, 0x2c, 0x12},     /* zone is inactive */
+    {0x2, SPC_SK_DATA_PROTECT, 0x3f, 0x17},     /* zone is full */
+    {0x2, SPC_SK_ILLEGAL_REQUEST, 0x21, 0x5},   /* Write boundary violation */
+
+/* index: 0x20 */
+    {0x2, SPC_SK_DATA_PROTECT, 0x55, 0xe},   /* Insufficient zone resources */
+
+    /* Leave this Sentinel value at end of this array */
+    {0xff, 0xff, 0xff, 0xff},
+};
+
+/* These are the error (or warning) exit status values and their associated
+ * strings. They combine utility input syntax errors, SCSI status and sense
+ * key categories, OS errors (e.g. ENODEV for device not found), one that
+ * indicates NVMe non-zero status plus listing those that a Unix OS generates
+ * for any executable (that fails). The convention is 0 means no error and
+ * that in Unix the exit status is an (unsigned) 8 bit value. */
+struct sg_value_2names_t sg_exit_str_arr[] = {
+    {0,  "No errors", "may also convey true"},
+    {1,  "Syntax error", "command line options (usually)"},
+    {2,  "Device not ready", "type: sense key"},
+    {3,  "Medium or hardware error", "type: sense key (plus blank check for "
+         "tape)"},
+    {5,  "Illegal request", "type: sense key, apart from Invalid opcode"},
+    {6,  "Unit attention", "type: sense key"},
+    {7,  "Data protect", "type: sense key; write protected media?"},
+    {9,  "Illegal request, Invalid opcode", "type: sense key + asc,ascq"},
+    {10, "Copy aborted", "type: sense key"},
+    {11, "Aborted command",
+         "type: sense key, other than protection related (asc=0x10)"},
+    {12, "Device not ready, standby", "type: sense key"},
+    {13, "Device not ready, unavailable", "type: sense key"},
+    {14, "Miscompare", "type: sense key"},
+    {15, "File error", NULL},
+    {17, "Illegal request with Info field", NULL},
+    {18, "Medium or hardware error with Info", NULL},
+    {20, "No sense key", "type: probably additional sense code"},
+    {21, "Recovered error (warning)", "type: sense key"},
+         /* N.B. this is a warning not error */
+    {22, "LBA out of range", NULL},
+    {24, "Reservation conflict", "type: SCSI status"},
+    {25, "Condition met", "type: SCSI status"}, /* from PRE-FETCH command */
+    {26, "Busy", "type: SCSI status"},   /* could be transport issue */
+    {27, "Task set full", "type: SCSI status"},
+    {28, "ACA aactive", "type: SCSI status"},
+    {29, "Task aborted", "type: SCSI status"},
+    {31, "Contradict", "command line options contradict or select bad mode"},
+    {32, "Logic error", "unexpected situation, contact author"},
+    {33, "SCSI command timeout", NULL},         /* OS timed out command */
+    {34, "Windows error number", "doesn't fit in 7 bits"},
+    {35, "Transport error", "driver or interconnect error"},
+    {36, "No errors (false)", NULL},
+    {40, "Aborted command, protection error", NULL},
+    {41, "Aborted command, protection error with Info field", NULL},
+    {47, "flock (Unix system call) error", NULL},       /* ddpt */
+    {48, "NVMe command with non-zero status", NULL},
+    {50, "An OS error occurred", "(errno > 46 or negative)"},
+    /* OS errors (errno in Unix) from 1 to 46 mapped into this range */
+    {97, "Malformed SCSI command", "trouble building command"},
+    {98, "Some other sense error", "try '-v' option for more information"},
+    {99, "Some other error", "possible transport of driver issue"},
+    {100, "Parameter list length error", NULL}, /* these for ddpt, xcopy */
+    {101, "Invalid field in parameter", NULL},
+    {102, "Too many segments in parameters", NULL},
+    {103, "Target underrun", NULL},
+    {104, "Target overrun", NULL},
+    {105, "Operation in progress", NULL},
+    {106, "Insufficient resources to create ROD", NULL},
+    {107, "Insufficient resources to create ROD token", NULL},
+    {108, "Commands cleared by device server", NULL},
+    {109, "See leave_reason for error", NULL},        /* internal error */
+    /* DDPT_CAT_TOKOP_BASE: asc=0x23, ascq=110 follow */
+    {110, "Invalid token operation, cause not reportable", NULL},
+    {111, "Invalid token operation, unsupported token type", NULL},
+    {112, "Invalid token operation, remote token usage not supported", NULL},
+    {113, "Invalid token operation, remote token creation not supported",
+          NULL},
+    {114, "Invalid token operation, token unknown", NULL},
+    {115, "Invalid token operation, token corrupt", NULL},
+    {116, "Invalid token operation, token revoked", NULL},
+    {117, "Invalid token operation, token expired", NULL},
+    {118, "Invalid token operation, token cancelled", NULL},
+    {119, "Invalid token operation, token deleted", NULL},
+    {120, "Invalid token operation, invalid token length", NULL},
+
+    /* The following error codes are generated by a Unix OS */
+    {126, "Utility found but did not have execute permissions", NULL},
+    {127, "Utility to be executed was not found", NULL},
+    {128, "Utility stopped/aborted by signal number: 0", "signal # 0 ??"},
+    /* 128 + <signal_number>: signal number that aborted the utility.
+                              real time signals start at offset SIGRTMIN */
+    /* OS signals from 1 to 126 mapped into this range (129 to 254) */
+    {255, "Utility returned 255 or higher", "Windows error number?"},
+    {0xffff, NULL, NULL},       /* end marking sentinel */
+};
+
+#else           /* (SG_SCSI_STRINGS && HAVE_NVME && (! IGNORE_NVME)) */
+
+struct sg_lib_simple_value_name_t sg_lib_nvme_admin_cmd_arr[] =
+{
+
+    /* Vendor specific 0x80 to 0xff */
+    {0xffff, NULL},                     /* Sentinel */
+};
+
+struct sg_lib_simple_value_name_t sg_lib_nvme_nvm_cmd_arr[] =
+{
+
+    /* Vendor specific 0x80 to 0xff */
+    {0xffff, NULL},                     /* Sentinel */
+};
+
+struct sg_lib_value_name_t sg_lib_nvme_cmd_status_arr[] =
+{
+
+    /* Leave this Sentinel value at end of this array */
+    {0x3ff, 0, NULL},
+};
+
+struct sg_lib_4tuple_u8 sg_lib_scsi_status_sense_arr[] =
+{
+
+    /* Leave this Sentinel value at end of this array */
+    {0xff, 0xff, 0xff, 0xff},
+};
+
+struct sg_value_2names_t sg_exit_str_arr[] = {
+    {0xffff, NULL, NULL},       /* end marking sentinel */
+};
+
+#endif           /* (SG_SCSI_STRINGS && HAVE_NVME && (! IGNORE_NVME)) */
diff --git a/lib/sg_lib_names.c b/lib/sg_lib_names.c
new file mode 100644
index 0000000..19ffa44
--- /dev/null
+++ b/lib/sg_lib_names.c
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdlib.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#else
+#define SG_SCSI_STRINGS 1
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_lib_names.h"
+
+/* List of SPC, then SBC, the ZBC mode page names. Tape and other mode pages
+ * are squeezed into this list as long as they don't conflict.
+ * The value is: (mode_page << 8) | mode_subpage
+ * Maintain the list in numerical order to allow binary search. */
+struct sg_lib_simple_value_name_t sg_lib_names_mode_arr[] = {
+    {0x0000, "Unit Attention condition"},  /* common vendor specific page */
+    {0x0100, "Read-Write error recovery"},      /* SBC */
+    {0x0200, "Disconnect-Reconnect"},           /* SPC */
+    {0x0300, "Format (obsolete)"},              /* SBC */
+    {0x0400, "Rigid disk geometry (obsolete)"}, /* SBC */
+    {0x0500, "Flexible disk (obsolete)"},       /* SBC */
+    {0x0700, "Verify error recovery"},          /* SBC */
+    {0x0800, "Caching"},                        /* SBC */
+    {0x0900, "Peripheral device (obsolete)"},   /* SPC */
+    {0x0a00, "Control"},                        /* SPC */
+    {0x0a01, "Control extension"},              /* SPC */
+    {0x0a02, "Application tag"},                /* SBC */
+    {0x0a03, "Command duration limit A"},       /* SPC */
+    {0x0a04, "Command duration limit B"},       /* SPC */
+    {0x0a05, "IO Advice Hints Grouping"},       /* SBC */
+    {0x0a06, "Background operation control"},   /* SBC */
+    {0x0af0, "Control data protection"},        /* SSC */
+    {0x0af1, "PATA control"},                   /* SAT */
+    {0x0b00, "Medium Types Supported (obsolete)"},   /* SSC */
+    {0x0c00, "Notch and partition (obsolete)"}, /* SBC */
+    {0x0d00, "Power condition (obsolete), CD device parameters"},
+    {0x0e00, "CD audio control"},               /* MMC */
+    {0x0e01, "Target device"},                  /* ADC */
+    {0x0e02, "DT device primary port"},         /* ADC */
+    {0x0e03, "Logical unit"},                   /* ADC */
+    {0x0e04, "Target device serial number"},    /* ADC */
+    {0x0f00, "Data compression"},               /* SSC */
+    {0x1000, "XOR control (obsolete, Device configuration"}, /* SBC,SSC */
+    {0x1001, "Device configuration extension"}, /* SSC */
+    {0x1100, "Medium partition (1)"},           /* SSC */
+    {0x1400, "Enclosure services management"},  /* SES */
+    {0x1800, "Protocol specific logical unit"}, /* transport */
+    {0x1900, "Protocol specific port"},         /* transport */
+    {0x1901, "Phy control and discovery"},      /* SPL */
+    {0x1902, "Shared port control"},            /* SPL */
+    {0x1903, "Enhanced phy control"},           /* SPL */
+    {0x1904, "Out of band  management control"}, /* SPL */
+    {0x1A00, "Power condition"},                /* SPC */
+    {0x1A01, "Power consumption"},              /* SPC */
+    {0x1Af1, "ATA Power condition"},            /* SPC */
+    {0x1b00, "LUN mapping"},                    /* ADC */
+    {0x1c00, "Information exceptions control"}, /* SPC */
+    {0x1c01, "Background control"},             /* SBC */
+    {0x1c02, "Logical block provisioning"},     /* SBC */
+    {0x1c02, "Logical block provisioning"},     /* SBC */
+    {0x1d00, "Medium configuration, CD/DVD timeout, "
+             "element address assignments"},    /* SSC,MMC,SMC */
+    {0x1e00, "Transport geometry assignments"}, /* SMC */
+    {0x1f00, "Device capabilities"},            /* SMC */
+
+    {-1, NULL},                                 /* sentinel */
+};
+
+/* Don't count sentinel when doing binary searches, etc */
+const size_t sg_lib_names_mode_len =
+                SG_ARRAY_SIZE(sg_lib_names_mode_arr) - 1;
+
+/* List of SPC, then SBC, the ZBC VPD page names. Tape and other VPD pages
+ * are squeezed into this list as long as they don't conflict.
+ * For VPDs > 0 the value is: (vpd << 8) | vpd_number
+ * Maintain the list in numerical order to allow binary search. */
+struct sg_lib_simple_value_name_t sg_lib_names_vpd_arr[] = {
+    {0x00, "Supported VPD pages"},              /* SPC */
+    {0x80, "Unit serial number"},               /* SPC */
+    {0x81, "Implemented operating definition (obsolete)"}, /* SPC */
+    {0x82, "ASCII implemented operating definition (obsolete)"}, /* SPC */
+    {0x83, "Device identification"},            /* SPC */
+    {0x84, "Software interface identification"}, /* SPC */
+    {0x85, "Management network addresses"},     /* SPC */
+    {0x86, "Extended INQUIRY data"},            /* SPC */
+    {0x87, "Mode page policy"},                 /* SPC */
+    {0x88, "SCSI ports"},                       /* SPC */
+    {0x89, "ATA information"},                  /* SAT */
+    {0x8a, "Power condition"},                  /* SPC */
+    {0x8b, "Device constituents"},              /* SSC */
+    {0x8c, "CFA profile information"},          /* SPC */
+    {0x8d, "Power consumption"},                /* SPC */
+    {0x8f, "Third party copy"},                 /* SPC */
+    {0x90, "Protocol specific logical unit information"}, /* transport */
+    {0x91, "Protocol specific port information"}, /* transport */
+    {0x92, "SCSI feature sets"},                /* SPC,SBC */
+    {0xb0, "Block limits"},                     /* SBC */
+    {0xb1, "Block device characteristics"},     /* SBC */
+    {0xb2, "Logical block provisioning"},       /* SBC */
+    {0xb3, "Referrals"},                        /* SBC */
+    {0xb4, "Supported Block Lengths and Protection Types"}, /* SBC */
+    {0xb5, "Block device characteristics extension"}, /* SBC */
+    {0xb6, "Zoned block device characteristics"}, /* ZBC */
+    {0xb7, "Block limits extension"},           /* SBC */
+    {0xb8, "Format presets"},                   /* SBC */
+    {0xb9, "Concurrent positioning ranges"},    /* SBC */
+    {0x01b0, "Sequential access Device Capabilities"}, /* SSC */
+    {0x01b1, "Manufacturer-assigned serial number"}, /* SSC */
+    {0x01b2, "TapeAlert supported flags"},      /* SSC */
+    {0x01b3, "Automation device serial number"}, /* SSC */
+    {0x01b4, "Data transfer device element address"}, /* SSC */
+    {0x01b5, "Data transfer device element address"}, /* SSC */
+    {0x11b0, "OSD information"},                /* OSD */
+    {0x11b1, "Security token"},                 /* OSD */
+
+    {-1, NULL},                                 /* sentinel */
+};
+
+/* Don't count sentinel when doing binary searches, etc */
+const size_t sg_lib_names_vpd_len =
+                SG_ARRAY_SIZE(sg_lib_names_vpd_arr) - 1;
diff --git a/lib/sg_pr2serr.c b/lib/sg_pr2serr.c
new file mode 100644
index 0000000..ef53396
--- /dev/null
+++ b/lib/sg_pr2serr.c
@@ -0,0 +1,2026 @@
+/*
+ * Copyright (c) 2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "sg_pr2serr.h"
+#include "sg_json_builder.h"
+
+/*
+ * Some users of sg_pr2serr may not need fixed and descriptor sense decoded
+ * for JSON output. If the following define is commented out the effective
+ * compile size of this file is reduced by 800 lines plus dependencies on
+ * other large components of the sg3_utils library.
+ * Comment out the next line to remove dependency on sg_lib.h and its code.
+ */
+#define SG_PRSE_SENSE_DECODE 1
+
+#ifdef SG_PRSE_SENSE_DECODE
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_unaligned.h"
+#endif
+
+
+#define sgj_opts_ev "SG3_UTILS_JSON_OPTS"
+
+/*
+ * #define json_serialize_mode_multiline     0
+ * #define json_serialize_mode_single_line   1
+ * #define json_serialize_mode_packed        2
+ *
+ * #define json_serialize_opt_CRLF                    (1 << 1)
+ * #define json_serialize_opt_pack_brackets           (1 << 2)
+ * #define json_serialize_opt_no_space_after_comma    (1 << 3)
+ * #define json_serialize_opt_no_space_after_colon    (1 << 4)
+ * #define json_serialize_opt_use_tabs                (1 << 5)
+ */
+
+
+static const json_serialize_opts def_out_settings = {
+    json_serialize_mode_multiline,      /* one of serialize_mode_* */
+    0,                                  /* serialize_opt_* OR-ed together */
+    4                                   /* indent size */
+};
+
+static int sgj_name_to_snake(const char * in, char * out, int maxlen_out);
+
+
+/* Users of the sg_pr2serr.h header need this function definition */
+int
+pr2serr(const char * fmt, ...)
+{
+    va_list args;
+    int n;
+
+    va_start(args, fmt);
+    n = vfprintf(stderr, fmt, args);
+    va_end(args);
+    return n;
+}
+
+#ifndef SG_PRSE_SENSE_DECODE
+
+/* Want safe, 'n += snprintf(b + n, blen - n, ...)' style sequence of
+ * functions. Returns number of chars placed in cp excluding the
+ * trailing null char. So for cp_max_len > 0 the return value is always
+ * < cp_max_len; for cp_max_len <= 1 the return value is 0 and no chars are
+ * written to cp. Note this means that when cp_max_len = 1, this function
+ * assumes that cp[0] is the null character and does nothing (and returns
+ * 0). Linux kernel has a similar function called  scnprintf(). Public
+ * declaration in sg_pr2serr.h header  */
+int
+sg_scnpr(char * cp, int cp_max_len, const char * fmt, ...)
+{
+    va_list args;
+    int n;
+
+    if (cp_max_len < 2)
+        return 0;
+    va_start(args, fmt);
+    n = vsnprintf(cp, cp_max_len, fmt, args);
+    va_end(args);
+    return (n < cp_max_len) ? n : (cp_max_len - 1);
+}
+
+int
+pr2ws(const char * fmt, ...)
+{
+    va_list args;
+    int n;
+
+    va_start(args, fmt);
+    n = vfprintf(stderr, fmt, args);
+    va_end(args);
+    return n;
+}
+
+#endif
+
+static bool
+sgj_parse_opts(sgj_state * jsp, const char * j_optarg)
+{
+    bool bad_arg = false;
+    bool prev_negate = false;
+    bool negate;
+    int k, c;
+
+    for (k = 0; j_optarg[k]; ++k) {     /* step over leading whitespace */
+        if (! isspace(j_optarg[k]))
+            break;
+    }
+    for ( ; j_optarg[k]; ++k) {
+        c = j_optarg[k];
+        negate = false;
+        switch (c) {
+        case '=':
+            if (0 == k)
+                break;  /* allow and ignore leading '=' */
+            bad_arg = true;
+            if (0 == jsp->first_bad_char)
+                jsp->first_bad_char = c;
+            break;
+        case '!':
+        case '~':
+        case '-':       /* '-' is probably most practical negation symbol */
+            negate = true;
+            break;
+        case '0':
+        case '2':
+            jsp->pr_indent_size = 2;
+            break;
+        case '3':
+            jsp->pr_indent_size = 3;
+            break;
+        case '4':
+            jsp->pr_indent_size = 4;
+            break;
+        case '8':
+            jsp->pr_indent_size = 8;
+            break;
+        case 'e':
+            jsp->pr_exit_status = ! prev_negate;
+            break;
+        case 'g':
+            jsp->pr_format = 'g';
+            break;
+        case 'h':
+            jsp->pr_hex = ! prev_negate;
+            break;
+        case 'k':
+            jsp->pr_packed = ! prev_negate;
+            break;
+        case 'l':
+            jsp->pr_leadin = ! prev_negate;
+            break;
+        case 'n':
+            jsp->pr_name_ex = ! prev_negate;
+            break;
+        case 'o':
+            jsp->pr_out_hr = ! prev_negate;
+            break;
+        case 'p':
+            jsp->pr_pretty = ! prev_negate;
+            break;
+        case 's':
+            jsp->pr_string = ! prev_negate;
+            break;
+        case 'v':
+            ++jsp->verbose;
+            break;
+        case 'y':
+            jsp->pr_format = 'g';
+            break;
+        case '?':
+            bad_arg = true;
+            jsp->first_bad_char = '\0';
+            break;
+        default:
+            bad_arg = true;
+            if (0 == jsp->first_bad_char)
+                jsp->first_bad_char = c;
+            break;
+        }
+        prev_negate = negate ? ! prev_negate : false;
+    }
+    return ! bad_arg;
+}
+
+char *
+sg_json_usage(int char_if_not_j, char * b, int blen)
+{
+    int n = 0;
+    char short_opt = char_if_not_j ? char_if_not_j : 'j';
+
+    if ((NULL == b) || (blen < 1))
+        goto fini;
+    n +=  sg_scnpr(b + n, blen - n, "JSON option usage:\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "     --json[-JO] | -%c[JO]\n\n", short_opt);
+    n +=  sg_scnpr(b + n, blen - n, "  where JO is a string of one or more "
+                   "of:\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      0 | 2    tab pretty output to 2 spaces\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      4    tab pretty output to 4 spaces\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      8    tab pretty output to 8 spaces\n");
+    if (n >= (blen - 1))
+        goto fini;
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      e    show 'exit_status' field\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      h    show 'hex' fields\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      k    packed, only non-pretty printed output\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      l    show lead-in fields (invocation "
+                   "information)\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      n    show 'name_extra' information fields\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      o    non-JSON output placed in 'output' array in "
+                   "lead-in\n");
+    if (n >= (blen - 1))
+        goto fini;
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      p    pretty print the JSON output\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      s    show string output (usually fields named "
+                   "'meaning')\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      v    make JSON output more verbose\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      =    ignored if first character, else it's an "
+                   "error\n");
+    n +=  sg_scnpr(b + n, blen - n,
+                   "      - | ~ | !    toggle next letter setting\n");
+
+    n +=  sg_scnpr(b + n, blen - n, "\nIn the absence of the optional JO "
+                   "argument, the following are set\non: 'elps' while the "
+                   "others are set off, and tabs are set to 4.\nBefore "
+                   "command line JO options are applied, the environment\n"
+                   "variable: %s is applied (if present). Note that\nno "
+                   "space is permitted between the short option ('-%c') "
+                   "and its\nargument ('JO').\n", sgj_opts_ev, short_opt);
+fini:
+    return b;
+}
+
+char *
+sg_json_settings(sgj_state * jsp, char * b, int blen)
+{
+    snprintf(b, blen, "%d%se%sh%sk%sl%sn%so%sp%ss%sv", jsp->pr_indent_size,
+             jsp->pr_exit_status ? "" : "-", jsp->pr_hex ? "" : "-",
+             jsp->pr_packed ? "" : "-", jsp->pr_leadin ? "" : "-",
+             jsp->pr_name_ex ? "" : "-", jsp->pr_out_hr ? "" : "-",
+             jsp->pr_pretty ? "" : "-", jsp->pr_string ? "" : "-",
+             jsp->verbose ? "" : "-");
+    return b;
+}
+
+static void
+sgj_def_opts(sgj_state * jsp)
+{
+    jsp->pr_as_json = true;
+    jsp->pr_exit_status = true;
+    jsp->pr_hex = false;
+    jsp->pr_leadin = true;
+    jsp->pr_out_hr = false;
+    jsp->pr_name_ex = false;
+    jsp->pr_packed = false;     /* 'k' control character, needs '-p' */
+    jsp->pr_pretty = true;
+    jsp->pr_string = true;
+    jsp->pr_format = 0;
+    jsp->first_bad_char = 0;
+    jsp->verbose = 0;
+    jsp->pr_indent_size = 4;
+}
+
+bool
+sgj_init_state(sgj_state * jsp, const char * j_optarg)
+{
+    const char * cp;
+
+    sgj_def_opts(jsp);
+    jsp->basep = NULL;
+    jsp->out_hrp = NULL;
+    jsp->userp = NULL;
+
+    cp = getenv(sgj_opts_ev);
+    if (cp) {
+        if (! sgj_parse_opts(jsp, cp)) {
+            pr2ws("error parsing %s environment variable, ignore\n",
+                  sgj_opts_ev);
+            sgj_def_opts(jsp);
+        }
+    }
+    return j_optarg ? sgj_parse_opts(jsp, j_optarg) : true;
+}
+
+sgj_opaque_p
+sgj_start_r(const char * util_name, const char * ver_str, int argc,
+            char *argv[], sgj_state * jsp)
+{
+    int k;
+    json_value * jvp = json_object_new(0);
+    json_value * jv2p = NULL;
+    json_value * jap = NULL;
+
+    if (NULL == jvp)
+        return NULL;
+    if (NULL == jsp)
+        return jvp;
+
+    jsp->basep = jvp;
+    if (jsp->pr_leadin) {
+        jap = json_array_new(0);
+        if  (NULL == jap) {
+            json_builder_free((json_value *)jvp);
+            return NULL;
+        }
+        /* assume rest of json_*_new() calls succeed */
+        json_array_push((json_value *)jap, json_integer_new(1));
+        json_array_push((json_value *)jap, json_integer_new(0));
+        json_object_push((json_value *)jvp, "json_format_version",
+                         (json_value *)jap);
+        if (util_name) {
+            jap = json_array_new(0);
+            if (argv) {
+                for (k = 0; k < argc; ++k)
+                    json_array_push((json_value *)jap,
+                                    json_string_new(argv[k]));
+            }
+            jv2p = json_object_push((json_value *)jvp, "utility_invoked",
+                                    json_object_new(0));
+            json_object_push((json_value *)jv2p, "name",
+                             json_string_new(util_name));
+            if (ver_str)
+                json_object_push((json_value *)jv2p, "version_date",
+                                 json_string_new(ver_str));
+            else
+                json_object_push((json_value *)jv2p, "version_date",
+                                 json_string_new("0.0"));
+            json_object_push((json_value *)jv2p, "argv", jap);
+        }
+        if (jsp->verbose) {
+            const char * cp = getenv(sgj_opts_ev);
+            char b[32];
+
+            json_object_push((json_value *)jv2p, "environment_variable_name",
+                             json_string_new(sgj_opts_ev));
+            json_object_push((json_value *)jv2p, "environment_variable_value",
+                             json_string_new(cp ? cp : "no available"));
+            sg_json_settings(jsp, b, sizeof(b));
+            json_object_push((json_value *)jv2p, "json_options",
+                             json_string_new(b));
+        }
+    } else {
+        if (jsp->pr_out_hr && util_name)
+            jv2p = json_object_push((json_value *)jvp, "utility_invoked",
+                                    json_object_new(0));
+    }
+    if (jsp->pr_out_hr && jv2p) {
+        jsp->out_hrp = json_object_push((json_value *)jv2p, "output",
+                                        json_array_new(0));
+        if (jsp->pr_leadin && (jsp->verbose > 3)) {
+            char * bp = (char *)calloc(4096, 1);
+
+            if (bp) {
+                sg_json_usage(0, bp, 4096);
+                sgj_js_str_out(jsp, bp, strlen(bp));
+                free(bp);
+            }
+        }
+    }
+    return jvp;
+}
+
+void
+sgj_js2file(sgj_state * jsp, sgj_opaque_p jop, int exit_status, FILE * fp)
+{
+    size_t len;
+    char * b;
+    json_value * jvp = (json_value *)(jop ? jop : jsp->basep);
+    json_serialize_opts out_settings;
+
+    if (NULL == jvp) {
+        fprintf(fp, "%s: all NULL pointers ??\n", __func__);
+        return;
+    }
+    if ((NULL == jop) && jsp->pr_exit_status) {
+        char d[80];
+
+#ifdef SG_PRSE_SENSE_DECODE
+        if (sg_exit2str(exit_status, jsp->verbose, sizeof(d), d)) {
+            if (0 == strlen(d))
+                strncpy(d, "no errors", sizeof(d) - 1);
+        } else
+            strncpy(d, "not available", sizeof(d) - 1);
+#else
+        if (0 == exit_status)
+            strncpy(d, "no errors", sizeof(d) - 1);
+        else
+            snprintf(d, sizeof(d), "exit_status=%d", exit_status);
+#endif
+        sgj_js_nv_istr(jsp, jop, "exit_status", exit_status, NULL, d);
+    }
+    memcpy(&out_settings, &def_out_settings, sizeof(out_settings));
+    if (jsp->pr_indent_size != def_out_settings.indent_size)
+        out_settings.indent_size = jsp->pr_indent_size;
+    if (! jsp->pr_pretty)
+        out_settings.mode = jsp->pr_packed ? json_serialize_mode_packed :
+                                json_serialize_mode_single_line;
+
+    len = json_measure_ex(jvp, out_settings);
+    if (len < 1)
+        return;
+    if (jsp->verbose > 3)
+        fprintf(fp, "%s: serialization length: %zu bytes\n", __func__, len);
+    b = (char *)calloc(len, 1);
+    if (NULL == b) {
+        if (jsp->verbose > 3)
+            pr2serr("%s: unable to get %zu bytes on heap\n", __func__, len);
+        return;
+    }
+
+    json_serialize_ex(b, jvp, out_settings);
+    if (jsp->verbose > 3)
+        fprintf(fp, "json serialized:\n");
+    fprintf(fp, "%s\n", b);
+}
+
+void
+sgj_finish(sgj_state * jsp)
+{
+    if (jsp && jsp->basep) {
+        json_builder_free((json_value *)jsp->basep);
+        jsp->basep = NULL;
+        jsp->out_hrp = NULL;
+        jsp->userp = NULL;
+    }
+}
+
+void
+sgj_free_unattached(sgj_opaque_p jop)
+{
+    if (jop)
+        json_builder_free((json_value *)jop);
+}
+
+void
+sgj_pr_hr(sgj_state * jsp, const char * fmt, ...)
+{
+    va_list args;
+
+    if (jsp->pr_as_json && jsp->pr_out_hr) {
+        size_t len;
+        char b[256];
+
+        va_start(args, fmt);
+        len = vsnprintf(b, sizeof(b), fmt, args);
+        if ((len > 0) && (len < sizeof(b))) {
+            const char * cp = b;
+
+            /* remove up to two trailing linefeeds */
+            if (b[len - 1] == '\n') {
+                --len;
+                if (b[len - 1] == '\n')
+                    --len;
+                b[len] = '\0';
+            }
+            /* remove leading linefeed, if present */
+            if ((len > 0) && ('\n' == b[0]))
+                ++cp;
+            json_array_push((json_value *)jsp->out_hrp, json_string_new(cp));
+        }
+        va_end(args);
+    } else if (jsp->pr_as_json) {
+        va_start(args, fmt);
+        va_end(args);
+    } else {
+        va_start(args, fmt);
+        vfprintf(stdout, fmt, args);
+        va_end(args);
+    }
+}
+
+/* jop will 'own' returned value (if non-NULL) */
+sgj_opaque_p
+sgj_named_subobject_r(sgj_state * jsp, sgj_opaque_p jop, const char * name)
+{
+    sgj_opaque_p resp = NULL;
+
+    if (jsp && jsp->pr_as_json && name)
+        resp = json_object_push((json_value *)(jop ? jop : jsp->basep), name,
+                                json_object_new(0));
+    return resp;
+}
+
+sgj_opaque_p
+sgj_snake_named_subobject_r(sgj_state * jsp, sgj_opaque_p jop,
+                            const char * conv2sname)
+{
+    if (jsp && jsp->pr_as_json && conv2sname) {
+        int olen = strlen(conv2sname);
+        char * sname = (char *)malloc(olen + 8);
+        int nlen = sgj_name_to_snake(conv2sname, sname, olen + 8);
+
+        if (nlen > 0)
+            return json_object_push((json_value *)(jop ? jop : jsp->basep),
+                                    sname, json_object_new(0));
+    }
+    return NULL;
+}
+
+/* jop will 'own' returned value (if non-NULL) */
+sgj_opaque_p
+sgj_named_subarray_r(sgj_state * jsp, sgj_opaque_p jop, const char * name)
+{
+    sgj_opaque_p resp = NULL;
+
+    if (jsp && jsp->pr_as_json && name)
+        resp = json_object_push((json_value *)(jop ? jop : jsp->basep), name,
+                                json_array_new(0));
+    return resp;
+}
+
+sgj_opaque_p
+sgj_snake_named_subarray_r(sgj_state * jsp, sgj_opaque_p jop,
+                           const char * conv2sname)
+{
+    if (jsp && jsp->pr_as_json && conv2sname) {
+        int olen = strlen(conv2sname);
+        char * sname = (char *)malloc(olen + 8);
+        int nlen = sgj_name_to_snake(conv2sname, sname, olen + 8);
+
+        if (nlen > 0)
+            return json_object_push((json_value *)(jop ? jop : jsp->basep),
+                                    sname, json_array_new(0));
+    }
+    return NULL;
+}
+
+/* Newly created object is un-attached to jsp->basep tree */
+sgj_opaque_p
+sgj_new_unattached_object_r(sgj_state * jsp)
+{
+    return (jsp && jsp->pr_as_json) ? json_object_new(0) : NULL;
+}
+
+/* Newly created array is un-attached to jsp->basep tree */
+sgj_opaque_p
+sgj_new_unattached_array_r(sgj_state * jsp)
+{
+    return (jsp && jsp->pr_as_json) ? json_array_new(0) : NULL;
+}
+
+sgj_opaque_p
+sgj_js_nv_s(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+            const char * value)
+{
+    if (jsp && jsp->pr_as_json && value) {
+        if (name)
+            return json_object_push((json_value *)(jop ? jop : jsp->basep),
+                                    name, json_string_new(value));
+        else
+            return json_array_push((json_value *)(jop ? jop : jsp->basep),
+                                   json_string_new(value));
+    } else
+        return NULL;
+}
+
+sgj_opaque_p
+sgj_js_nv_s_len(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+                const char * value, int slen)
+{
+    int k;
+
+    if (jsp && jsp->pr_as_json && value && (slen >= 0)) {
+        for (k = 0; k < slen; ++k) {    /* don't want '\0' in value string */
+            if (0 == value[k])
+                break;
+        }
+        if (name)
+            return json_object_push((json_value *)(jop ? jop : jsp->basep),
+                                    name, json_string_new_length(k, value));
+        else
+            return json_array_push((json_value *)(jop ? jop : jsp->basep),
+                                   json_string_new_length(k, value));
+    } else
+        return NULL;
+}
+
+sgj_opaque_p
+sgj_js_nv_i(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+            int64_t value)
+{
+    if (jsp && jsp->pr_as_json) {
+        if (name)
+            return json_object_push((json_value *)(jop ? jop : jsp->basep),
+                                    name, json_integer_new(value));
+        else
+            return json_array_push((json_value *)(jop ? jop : jsp->basep),
+                                   json_integer_new(value));
+    }
+    else
+        return NULL;
+}
+
+sgj_opaque_p
+sgj_js_nv_b(sgj_state * jsp, sgj_opaque_p jop, const char * name, bool value)
+{
+    if (jsp && jsp->pr_as_json) {
+        if (name)
+            return json_object_push((json_value *)(jop ? jop : jsp->basep),
+                                    name, json_boolean_new(value));
+        else
+            return json_array_push((json_value *)(jop ? jop : jsp->basep),
+                                   json_boolean_new(value));
+    } else
+        return NULL;
+}
+
+/* jop will 'own' ua_jop (if returned value is non-NULL) */
+sgj_opaque_p
+sgj_js_nv_o(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+            sgj_opaque_p ua_jop)
+{
+    if (jsp && jsp->pr_as_json && ua_jop) {
+        if (name)
+            return json_object_push((json_value *)(jop ? jop : jsp->basep),
+                                    name, (json_value *)ua_jop);
+        else
+            return json_array_push((json_value *)(jop ? jop : jsp->basep),
+                                   (json_value *)ua_jop);
+    } else
+        return NULL;
+}
+
+void
+sgj_js_nv_ihex(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+               uint64_t value)
+{
+    if ((NULL == jsp) || (NULL == name) || (! jsp->pr_as_json))
+        return;
+    else if (jsp->pr_hex)  {
+        sgj_opaque_p jo2p = sgj_named_subobject_r(jsp, jop, name);
+        char b[64];
+
+        if (NULL == jo2p)
+            return;
+        sgj_js_nv_i(jsp, jo2p, "i", (int64_t)value);
+        snprintf(b, sizeof(b), "%" PRIx64, value);
+        sgj_js_nv_s(jsp, jo2p, "hex", b);
+    } else
+        sgj_js_nv_i(jsp, jop, name, (int64_t)value);
+}
+
+static const char * sc_mn_s = "meaning";
+
+void
+sgj_js_nv_istr(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+               int64_t val_i, const char * str_name, const char * val_s)
+{
+    if ((NULL == jsp) || (! jsp->pr_as_json))
+        return;
+    else if (val_s && jsp->pr_string) {
+        sgj_opaque_p jo2p = sgj_named_subobject_r(jsp, jop, name);
+
+        if (NULL == jo2p)
+            return;
+        sgj_js_nv_i(jsp, jo2p, "i", (int64_t)val_i);
+        sgj_js_nv_s(jsp, jo2p, str_name ? str_name : sc_mn_s, val_s);
+    } else
+        sgj_js_nv_i(jsp, jop, name, val_i);
+}
+
+void
+sgj_js_nv_ihexstr(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+                  int64_t val_i, const char * str_name, const char * val_s)
+{
+    bool as_str;
+
+    if ((NULL == jsp) || (! jsp->pr_as_json))
+        return;
+    as_str = jsp->pr_string && val_s;
+    if ((! jsp->pr_hex) && (! as_str))
+        sgj_js_nv_i(jsp, jop, name, val_i);
+    else {
+        char b[64];
+        sgj_opaque_p jo2p = sgj_named_subobject_r(jsp, jop, name);
+
+        if (NULL == jo2p)
+            return;
+        sgj_js_nv_i(jsp, jo2p, "i", (int64_t)val_i);
+        if (jsp->pr_hex) {
+            snprintf(b, sizeof(b), "%" PRIx64, val_i);
+            sgj_js_nv_s(jsp, jo2p, "hex", b);
+        }
+        if (as_str)
+            sgj_js_nv_s(jsp, jo2p, str_name ? str_name : sc_mn_s, val_s);
+    }
+}
+
+static const char * sc_nex_s = "name_extra";
+
+void
+sgj_js_nv_ihex_nex(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+                   int64_t val_i, bool hex_as_well, const char * nex_s)
+{
+    bool as_hex, as_nex;
+
+    if ((NULL == jsp) || (! jsp->pr_as_json))
+        return;
+    as_hex = jsp->pr_hex && hex_as_well;
+    as_nex = jsp->pr_name_ex && nex_s;
+    if (! (as_hex || as_nex))
+        sgj_js_nv_i(jsp, jop, name, val_i);
+    else {
+        char b[64];
+        sgj_opaque_p jo2p =
+                 sgj_named_subobject_r(jsp, jop, name);
+
+        if (NULL == jo2p)
+            return;
+        sgj_js_nv_i(jsp, jo2p, "i", (int64_t)val_i);
+        if (as_hex) {
+            snprintf(b, sizeof(b), "%" PRIx64, val_i);
+            sgj_js_nv_s(jsp, jo2p, "hex", b);
+        }
+        if (as_nex)
+            sgj_js_nv_s(jsp, jo2p, sc_nex_s, nex_s);
+    }
+}
+
+#ifndef SG_PRSE_SENSE_DECODE
+static void
+h2str(const uint8_t * byte_arr, int num_bytes, char * bp, int blen)
+{
+    int j, k, n;
+
+    for (k = 0, n = 0; (k < num_bytes) && (n < blen); ) {
+        j = sg_scnpr(bp + n, blen - n, "%02x ", byte_arr[k]);
+        if (j < 2)
+            break;
+        n += j;
+        ++k;
+        if ((0 == (k % 8)) && (k < num_bytes) && (n < blen)) {
+            bp[n++] = ' ';
+        }
+    }
+    j = strlen(bp);
+    if ((j > 0) && (' ' == bp[j - 1]))
+        bp[j - 1] = '\0';    /* chop off trailing space */
+}
+#endif
+
+/* Add hex byte strings irrespective of jsp->pr_hex setting. */
+void
+sgj_js_nv_hex_bytes(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+                    const uint8_t * byte_arr, int num_bytes)
+{
+    int blen = num_bytes * 4;
+    char * bp;
+
+    if ((NULL == jsp) || (! jsp->pr_as_json))
+        return;
+    bp = (char *)calloc(blen + 4, 1);
+    if (bp) {
+#ifdef SG_PRSE_SENSE_DECODE
+        hex2str(byte_arr, num_bytes, NULL, 2, blen, bp);
+#else
+        h2str(byte_arr, num_bytes, bp, blen);
+#endif
+        sgj_js_nv_s(jsp, jop, name, bp);
+        free(bp);
+    }
+}
+
+void
+sgj_js_nv_ihexstr_nex(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+                      int64_t val_i, bool hex_as_well, const char * str_name,
+                      const char * val_s, const char * nex_s)
+{
+    bool as_hex = jsp->pr_hex && hex_as_well;
+    bool as_str = jsp->pr_string && val_s;
+    bool as_nex = jsp->pr_name_ex && nex_s;
+    const char * sname =  str_name ? str_name : sc_mn_s;
+
+    if ((NULL == jsp) || (! jsp->pr_as_json))
+        return;
+    if (! (as_hex || as_nex || as_str))
+        sgj_js_nv_i(jsp, jop, name, val_i);
+    else {
+        char b[64];
+        sgj_opaque_p jo2p =
+                 sgj_named_subobject_r(jsp, jop, name);
+
+        if (NULL == jo2p)
+            return;
+        sgj_js_nv_i(jsp, jo2p, "i", (int64_t)val_i);
+        if (as_nex) {
+            if (as_hex) {
+                snprintf(b, sizeof(b), "%" PRIx64, val_i);
+                sgj_js_nv_s(jsp, jo2p, "hex", b);
+            }
+            if (as_str) {
+                sgj_js_nv_s(jsp, jo2p, sname, val_s);
+            }
+            sgj_js_nv_s(jsp, jo2p, sc_nex_s, nex_s);
+        } else if (as_hex) {
+            snprintf(b, sizeof(b), "%" PRIx64, val_i);
+            sgj_js_nv_s(jsp, jo2p, "hex", b);
+            if (as_str)
+                sgj_js_nv_s(jsp, jo2p, sname, val_s);
+        } else if (as_str)
+            sgj_js_nv_s(jsp, jo2p, sname, val_s);
+    }
+}
+
+/* Treat '\n' in sp as line breaks. Consumes characters from sp until either
+ * a '\0' is found or slen is exhausted. Add each line to jsp->out_hrp JSON
+ * array (if conditions met). */
+void
+sgj_js_str_out(sgj_state * jsp, const char * sp, int slen)
+{
+    char c;
+    int k, n;
+    const char * prev_sp = sp;
+    const char * cur_sp = sp;
+
+    if ((NULL == jsp) || (NULL == jsp->out_hrp) || (! jsp->pr_as_json) ||
+        (! jsp->pr_out_hr))
+        return;
+    for (k = 0; k < slen; ++k, ++cur_sp) {
+        c = *cur_sp;
+        if ('\0' == c)
+            break;
+        else if ('\n' == c) {
+            n = cur_sp - prev_sp;
+            /* when name is NULL, add to array (jsp->out_hrp) */
+            sgj_js_nv_s_len(jsp, jsp->out_hrp, NULL, prev_sp, n);
+            prev_sp = cur_sp + 1;
+        }
+    }
+    if (prev_sp < cur_sp) {
+        n = cur_sp - prev_sp;
+        sgj_js_nv_s_len(jsp, jsp->out_hrp, NULL, prev_sp, n);
+    }
+}
+
+char *
+sgj_convert_to_snake_name(const char * in_name, char * sname,
+                          int max_sname_len)
+{
+    sgj_name_to_snake(in_name, sname, max_sname_len);
+    return sname;
+}
+
+bool
+sgj_is_snake_name(const char * in_name)
+{
+    size_t k;
+    size_t ln = strlen(in_name);
+    char c;
+
+    for (k = 0; k < ln; ++k) {
+        c = in_name[k];
+        if (((c >= '0') && (c <= '9')) ||
+            ((c >= 'a') && (c <= 'z')) ||
+            (c == '_'))
+            continue;
+        else
+            return false;
+    }
+    return true;
+}
+
+/* This function tries to convert the 'in' C string to "snake_case"
+ * convention so the output 'out' only contains lower case ASCII letters,
+ * numerals and "_" as a separator. Any leading or trailing underscores
+ * are removed as are repeated underscores (e.g. "_Snake __ case" becomes
+ * "snake_case"). Parentheses and the characters between them are removed.
+ * Returns number of characters placed in 'out' excluding the trailing
+ * NULL */
+static int
+sgj_name_to_snake(const char * in, char * out, int maxlen_out)
+{
+    bool prev_underscore = false;
+    bool within_paren = false;
+    int c, k, j, inlen;
+
+    if (maxlen_out < 2) {
+        if (maxlen_out == 1)
+            out[0] = '\0';
+        return 0;
+    }
+    inlen = strlen(in);
+    for (k = 0, j = 0; (k < inlen) && (j < maxlen_out); ++k) {
+        c = in[k];
+        if (within_paren) {
+            if (')' == c)
+                within_paren = false;
+            continue;
+        }
+        if (isalnum(c)) {
+            out[j++] = isupper(c) ? tolower(c) : c;
+            prev_underscore = false;
+        } else if ('(' == c)
+            within_paren = true;
+        else if ((j > 0) && (! prev_underscore)) {
+            out[j++] = '_';
+            prev_underscore = true;
+        }
+        /* else we are skipping character 'c' */
+    }
+    if (j == maxlen_out)
+        out[--j] = '\0';
+    /* trim of trailing underscores (might have been spaces) */
+    for (k = j - 1; k >= 0; --k) {
+        if (out[k] != '_')
+            break;
+    }
+    if (k < 0)
+        k = 0;
+    else
+        ++k;
+    out[k] = '\0';
+    return k;
+}
+
+static int
+sgj_jtype_to_s(char * b, int blen_max, json_value * jvp)
+{
+    json_type jtype = jvp ? jvp->type : json_none;
+
+    switch (jtype) {
+    case json_string:
+        return sg_scnpr(b, blen_max, "%s", jvp->u.string.ptr);
+    case json_integer:
+        return sg_scnpr(b, blen_max, "%" PRIi64, jvp->u.integer);
+    case json_boolean:
+        return sg_scnpr(b, blen_max, "%s", jvp->u.boolean ? "true" : "false");
+    case json_none:
+    default:
+        if ((blen_max > 0) && ('\0' != b[0]))
+            b[0] = '\0';
+        break;
+    }
+    return 0;
+}
+
+static int
+sgj_haj_helper(char * b, int blen_max, const char * name,
+               enum sgj_separator_t sep, bool use_jvp,
+               json_value * jvp, int64_t val_instead)
+{
+    int n = 0;
+
+    if (name) {
+        n += sg_scnpr(b + n, blen_max - n, "%s", name);
+        switch (sep) {
+        case SGJ_SEP_NONE:
+            break;
+        case SGJ_SEP_SPACE_1:
+            n += sg_scnpr(b + n, blen_max - n, " ");
+            break;
+        case SGJ_SEP_SPACE_2:
+            n += sg_scnpr(b + n, blen_max - n, "  ");
+            break;
+        case SGJ_SEP_SPACE_3:
+            n += sg_scnpr(b + n, blen_max - n, "   ");
+            break;
+        case SGJ_SEP_SPACE_4:
+            n += sg_scnpr(b + n, blen_max - n, "    ");
+            break;
+        case SGJ_SEP_EQUAL_NO_SPACE:
+            n += sg_scnpr(b + n, blen_max - n, "=");
+            break;
+        case SGJ_SEP_EQUAL_1_SPACE:
+            n += sg_scnpr(b + n, blen_max - n, "= ");
+            break;
+        case SGJ_SEP_COLON_NO_SPACE:
+            n += sg_scnpr(b + n, blen_max - n, ":");
+            break;
+        case SGJ_SEP_COLON_1_SPACE:
+            n += sg_scnpr(b + n, blen_max - n, ": ");
+            break;
+        default:
+            break;
+        }
+    }
+    if (use_jvp)
+        n += sgj_jtype_to_s(b + n, blen_max - n, jvp);
+    else
+        n += sg_scnpr(b + n, blen_max - n, "%" PRIi64, val_instead);
+    return n;
+}
+
+static void
+sgj_haj_xx(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+           const char * name, enum sgj_separator_t sep, json_value * jvp,
+           bool hex_as_well, const char * val_s, const char * nex_s)
+{
+    bool eaten = false;
+    bool as_json = (jsp && jsp->pr_as_json);
+    bool done;
+    int n;
+    json_type jtype = jvp ? jvp->type : json_none;
+    char b[256];
+    char jname[96];
+    static const int blen = sizeof(b);
+
+    if (leadin_sp > 128)
+        leadin_sp = 128;
+    for (n = 0; n < leadin_sp; ++n)
+        b[n] = ' ';
+    b[n] = '\0';
+    if (NULL == name) {
+        if ((! as_json) || (jsp && jsp->pr_out_hr)) {
+            n += sgj_jtype_to_s(b + n, blen - n, jvp);
+            printf("%s\n", b);
+        }
+        if (NULL == jop) {
+            if (as_json && jsp->pr_out_hr) {
+                eaten = true;
+                json_array_push((json_value *)jsp->out_hrp,
+                                jvp ? jvp : json_null_new());
+            }
+        } else {        /* assume jop points to named array */
+            if (as_json) {
+                eaten = true;
+                json_array_push((json_value *)jop,
+                                jvp ? jvp : json_null_new());
+            }
+        }
+        goto fini;
+    }
+    if (as_json) {
+        int k;
+
+        if (NULL == jop)
+            jop = jsp->basep;
+        k = sgj_name_to_snake(name, jname, sizeof(jname));
+        if (k > 0) {
+            done = false;
+            if (nex_s && (strlen(nex_s) > 0)) {
+                switch (jtype) {
+                case json_string:
+                    break;
+                case json_integer:
+                    sgj_js_nv_ihexstr_nex(jsp, jop, jname, jvp->u.integer,
+                                          hex_as_well, sc_mn_s, val_s, nex_s);
+                    done = true;
+                    break;
+                case json_boolean:
+                    sgj_js_nv_ihexstr_nex(jsp, jop, jname, jvp->u.boolean,
+                                          false, sc_mn_s, val_s, nex_s);
+                    done = true;
+                    break;
+                case json_none:
+                default:
+                    break;
+                }
+            } else {
+                switch (jtype) {
+                case json_string:
+                    break;
+                case json_integer:
+                    if (hex_as_well) {
+                        sgj_js_nv_ihexstr(jsp, jop, jname, jvp->u.integer,
+                                          sc_mn_s, val_s);
+                        done = true;
+                    }
+                    break;
+                case json_none:
+                default:
+                    break;
+                }
+            }
+            if (! done) {
+                eaten = true;
+                json_object_push((json_value *)jop, jname,
+                                 jvp ? jvp : json_null_new());
+            }
+        }
+    }
+    if (jvp && ((as_json && jsp->pr_out_hr) || (! as_json)))
+        n += sgj_haj_helper(b + n, blen - n, name, sep, true, jvp, 0);
+
+    if (as_json && jsp->pr_out_hr)
+        json_array_push((json_value *)jsp->out_hrp, json_string_new(b));
+    if (! as_json)
+        printf("%s\n", b);
+fini:
+    if (jvp && (! eaten))
+        json_builder_free((json_value *)jvp);
+}
+
+void
+sgj_haj_vs(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+           const char * name, enum sgj_separator_t sep, const char * value)
+{
+    json_value * jvp;
+
+    /* make json_value even if jsp->pr_as_json is false */
+    jvp = value ? json_string_new(value) : NULL;
+    sgj_haj_xx(jsp, jop, leadin_sp, name, sep, jvp, false, NULL, NULL);
+}
+
+void
+sgj_haj_vi(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+          const char * name, enum sgj_separator_t sep, int64_t value,
+           bool hex_as_well)
+{
+    json_value * jvp;
+
+    jvp = json_integer_new(value);
+    sgj_haj_xx(jsp, jop, leadin_sp, name, sep, jvp, hex_as_well, NULL, NULL);
+}
+
+void
+sgj_haj_vistr(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+              const char * name, enum sgj_separator_t sep, int64_t value,
+              bool hex_as_well, const char * val_s)
+{
+    json_value * jvp;
+
+    jvp = json_integer_new(value);
+    sgj_haj_xx(jsp, jop, leadin_sp, name, sep, jvp, hex_as_well, val_s,
+                 NULL);
+}
+
+void
+sgj_haj_vi_nex(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+               const char * name, enum sgj_separator_t sep,
+               int64_t value, bool hex_as_well, const char * nex_s)
+{
+    json_value * jvp;
+
+    jvp = json_integer_new(value);
+    sgj_haj_xx(jsp, jop, leadin_sp, name, sep, jvp, hex_as_well, NULL, nex_s);
+}
+
+void
+sgj_haj_vistr_nex(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+                  const char * name, enum sgj_separator_t sep,
+                  int64_t value, bool hex_as_well,
+                  const char * val_s, const char * nex_s)
+{
+    json_value * jvp;
+
+    jvp = json_integer_new(value);
+    sgj_haj_xx(jsp, jop, leadin_sp, name, sep, jvp, hex_as_well, val_s,
+               nex_s);
+}
+
+void
+sgj_haj_vb(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+           const char * name, enum sgj_separator_t sep, bool value)
+{
+    json_value * jvp;
+
+    jvp = json_boolean_new(value);
+    sgj_haj_xx(jsp, jop, leadin_sp, name, sep, jvp, false, NULL, NULL);
+}
+
+sgj_opaque_p
+sgj_haj_subo_r(sgj_state * jsp, sgj_opaque_p jop, int leadin_sp,
+               const char * name, enum sgj_separator_t sep, int64_t value,
+               bool hex_as_well)
+{
+    bool as_json = (jsp && jsp->pr_as_json);
+    int n = 0;
+    sgj_opaque_p jo2p;
+    char b[256];
+    static const int blen = sizeof(b);
+
+    if (NULL == name)
+        return NULL;
+    for (n = 0; n < leadin_sp; ++n)
+        b[n] = ' ';
+    b[n] = '\0';
+    if ((! as_json) || (jsp && jsp->pr_out_hr))
+        n += sgj_haj_helper(b + n, blen - n, name, sep, false, NULL, value);
+
+    if (as_json && jsp->pr_out_hr)
+        json_array_push((json_value *)jsp->out_hrp, json_string_new(b));
+    if (! as_json)
+        printf("%s\n", b);
+
+    if (as_json) {
+        sgj_name_to_snake(name, b, blen);
+        jo2p = sgj_named_subobject_r(jsp, jop, b);
+        if (jo2p) {
+            sgj_js_nv_i(jsp, jo2p, "i", value);
+            if (hex_as_well && jsp->pr_hex) {
+                snprintf(b, blen, "%" PRIx64, value);
+                sgj_js_nv_s(jsp, jo2p, "hex", b);
+            }
+        }
+        return jo2p;
+    }
+    return NULL;
+}
+
+#ifdef SG_PRSE_SENSE_DECODE
+
+static const char * dtsp = "descriptor too short";
+static const char * sksvp = "sense-key specific valid";
+static const char * ddep = "designation_descriptor_error";
+static const char * naa_exp = "Network Address Authority";
+static const char * aoi_exp = "IEEE-Administered Organizational Identifier";
+
+bool
+sgj_js_designation_descriptor(sgj_state * jsp, sgj_opaque_p jop,
+                              const uint8_t * ddp, int dd_len)
+{
+    int p_id, piv, c_set, assoc, desig_type, d_id, naa;
+    int n, aoi, vsi, dlen;
+    uint64_t ull;
+    const uint8_t * ip;
+    char e[80];
+    char b[256];
+    const char * cp;
+    const char * naa_sp;
+    sgj_opaque_p jo2p;
+    static const int blen = sizeof(b);
+    static const int elen = sizeof(e);
+
+    if (dd_len < 4) {
+        sgj_js_nv_s(jsp, jop, ddep, "too short");
+        return false;
+    }
+    dlen = ddp[3];
+    if (dlen > (dd_len - 4)) {
+        snprintf(e, elen, "too long: says it is %d bytes, but given %d "
+                 "bytes\n", dlen, dd_len - 4);
+        sgj_js_nv_s(jsp, jop, ddep, e);
+        return false;
+    }
+    ip = ddp + 4;
+    p_id = ((ddp[0] >> 4) & 0xf);
+    c_set = (ddp[0] & 0xf);
+    piv = ((ddp[1] & 0x80) ? 1 : 0);
+    assoc = ((ddp[1] >> 4) & 0x3);
+    desig_type = (ddp[1] & 0xf);
+    cp = sg_get_desig_assoc_str(assoc);
+    if (assoc == 3)
+        cp = "Reserved [0x3]";    /* should not happen */
+    sgj_js_nv_ihexstr(jsp, jop, "association", assoc, NULL, cp);
+    cp = sg_get_desig_type_str(desig_type);
+    if (NULL == cp)
+        cp = "unknown";
+    sgj_js_nv_ihexstr(jsp, jop, "designator_type", desig_type,
+                       NULL, cp);
+    cp = sg_get_desig_code_set_str(c_set);
+    if (NULL == cp)
+        cp = "unknown";
+    sgj_js_nv_ihexstr(jsp, jop, "code_set", desig_type,
+                      NULL, cp);
+    sgj_js_nv_ihex_nex(jsp, jop, "piv", piv, false,
+                       "Protocol Identifier Valid");
+    sg_get_trans_proto_str(p_id, elen, e);
+    sgj_js_nv_ihexstr(jsp, jop, "protocol_identifier", p_id, NULL, e);
+    switch (desig_type) {
+    case 0: /* vendor specific */
+        sgj_js_nv_hex_bytes(jsp, jop, "vendor_specific_hexbytes", ip, dlen);
+        break;
+    case 1: /* T10 vendor identification */
+        n = (dlen < 8) ? dlen : 8;
+        snprintf(b, blen, "%.*s", n, ip);
+        sgj_js_nv_s(jsp, jop, "t10_vendor_identification", b);
+        b[0] = '\0';
+        if (dlen > 8)
+            snprintf(b, blen, "%.*s", dlen - 8, ip + 8);
+        sgj_js_nv_s(jsp, jop, "vendor_specific_identifier", b);
+        break;
+    case 2: /* EUI-64 based */
+        sgj_js_nv_i(jsp, jop, "eui_64_based_designator_length", dlen);
+        ull = sg_get_unaligned_be64(ip);
+        switch (dlen) {
+        case 8:
+            sgj_js_nv_ihex(jsp, jop, "ieee_identifier", ull);
+            break;
+        case 12:
+            sgj_js_nv_ihex(jsp, jop, "ieee_identifier", ull);
+            sgj_js_nv_ihex(jsp, jop, "directory_id",
+                            sg_get_unaligned_be32(ip + 8));
+            break;
+        case 16:
+            sgj_js_nv_ihex(jsp, jop, "identifier_extension", ull);
+            sgj_js_nv_ihex(jsp, jop, "ieee_identifier",
+                            sg_get_unaligned_be64(ip + 8));
+            break;
+        default:
+            sgj_js_nv_s(jsp, jop, "eui_64", "decoding failed");
+            break;
+        }
+        break;
+    case 3: /* NAA <n> */
+        if (jsp->pr_hex)
+            sgj_js_nv_hex_bytes(jsp, jop, "full_naa_hexbytes", ip, dlen);
+        naa = (ip[0] >> 4) & 0xff;
+        switch (naa) {
+        case 2:
+            naa_sp = "IEEE Extended";
+            sgj_js_nv_ihexstr_nex(jsp, jop, "naa", naa, false, NULL, naa_sp,
+                                  naa_exp);
+            d_id = (((ip[0] & 0xf) << 8) | ip[1]);
+            sgj_js_nv_ihex(jsp, jop, "vendor_specific_identifier_a", d_id);
+            aoi = sg_get_unaligned_be24(ip + 2);
+            sgj_js_nv_ihex_nex(jsp, jop, "aoi", aoi, true, aoi_exp);
+            vsi = sg_get_unaligned_be24(ip + 5);
+            sgj_js_nv_ihex(jsp, jop, "vendor_specific_identifier_b", vsi);
+            break;
+        case 3:
+            naa_sp = "Locally Assigned";
+            sgj_js_nv_ihexstr_nex(jsp, jop, "naa", naa, false, NULL, naa_sp,
+                                  naa_exp);
+            ull = sg_get_unaligned_be64(ip + 0) & 0xfffffffffffffffULL;
+            sgj_js_nv_ihex(jsp, jop, "locally_administered_value", ull);
+            break;
+        case 5:
+            naa_sp = "IEEE Registered";
+            sgj_js_nv_ihexstr_nex(jsp, jop, "naa", naa, false, NULL, naa_sp,
+                                  naa_exp);
+            aoi = (sg_get_unaligned_be32(ip + 0) >> 4) & 0xffffff;
+            sgj_js_nv_ihex_nex(jsp, jop, "aoi", aoi, true, aoi_exp);
+            ull = sg_get_unaligned_be48(ip + 2) & 0xfffffffffULL;
+            sgj_js_nv_ihex(jsp, jop, "vendor_specific_identifier", ull);
+            break;
+        case 6:
+            naa_sp = "IEEE Registered Extended";
+            sgj_js_nv_ihexstr_nex(jsp, jop, "naa", naa, false, NULL, naa_sp,
+                                  naa_exp);
+            aoi = (sg_get_unaligned_be32(ip + 0) >> 4) & 0xffffff;
+            sgj_js_nv_ihex_nex(jsp, jop, "aoi", aoi, true, aoi_exp);
+            ull = sg_get_unaligned_be48(ip + 2) & 0xfffffffffULL;
+            sgj_js_nv_ihex(jsp, jop, "vendor_specific_identifier", ull);
+            ull = sg_get_unaligned_be64(ip + 8);
+            sgj_js_nv_ihex(jsp, jop, "vendor_specific_identifier_extension",
+                           ull);
+            break;
+        default:
+            snprintf(b, blen, "unknown NAA value=0x%x", naa);
+            sgj_js_nv_ihexstr_nex(jsp, jop, "naa", naa, true, NULL, b,
+                                  naa_exp);
+            sgj_js_nv_hex_bytes(jsp, jop, "full_naa_hexbytes", ip, dlen);
+            break;
+        }
+        break;
+    case 4: /* Relative target port */
+        if (jsp->pr_hex)
+            sgj_js_nv_hex_bytes(jsp, jop, "relative_target_port_hexbytes",
+                                ip, dlen);
+        sgj_js_nv_ihex(jsp, jop, "relative_target_port_identifier",
+                       sg_get_unaligned_be16(ip + 2));
+        break;
+    case 5: /* (primary) Target port group */
+        if (jsp->pr_hex)
+            sgj_js_nv_hex_bytes(jsp, jop, "target_port_group_hexbytes",
+                                ip, dlen);
+        sgj_js_nv_ihex(jsp, jop, "target_port_group",
+                       sg_get_unaligned_be16(ip + 2));
+        break;
+    case 6: /* Logical unit group */
+        if (jsp->pr_hex)
+            sgj_js_nv_hex_bytes(jsp, jop, "logical_unit_group_hexbytes",
+                                ip, dlen);
+        sgj_js_nv_ihex(jsp, jop, "logical_unit_group",
+                       sg_get_unaligned_be16(ip + 2));
+        break;
+    case 7: /* MD5 logical unit identifier */
+        sgj_js_nv_hex_bytes(jsp, jop, "md5_logical_unit_hexbytes",
+                            ip, dlen);
+        break;
+    case 8: /* SCSI name string */
+        if (jsp->pr_hex)
+            sgj_js_nv_hex_bytes(jsp, jop, "scsi_name_string_hexbytes",
+                                ip, dlen);
+        snprintf(b, blen, "%.*s", dlen, ip);
+        sgj_js_nv_s(jsp, jop, "scsi_name_string", b);
+        break;
+    case 9: /* Protocol specific port identifier */
+        if (jsp->pr_hex)
+            sgj_js_nv_hex_bytes(jsp, jop,
+                                "protocol_specific_port_identifier_hexbytes",
+                                ip, dlen);
+        if (TPROTO_UAS == p_id) {
+            jo2p = sgj_named_subobject_r(jsp, jop,
+                                        "usb_target_port_identifier");
+            sgj_js_nv_ihex(jsp, jo2p, "device_address", 0x7f & ip[0]);
+            sgj_js_nv_ihex(jsp, jo2p, "interface_number", ip[2]);
+        } else if (TPROTO_SOP == p_id) {
+            jo2p = sgj_named_subobject_r(jsp, jop, "pci_express_routing_id");
+            sgj_js_nv_ihex(jsp, jo2p, "routing_id",
+                           sg_get_unaligned_be16(ip + 0));
+        } else
+            sgj_js_nv_s(jsp, jop, "protocol_specific_port_identifier",
+                        "decoding failure");
+
+        break;
+    case 0xa: /* UUID identifier */
+        if (jsp->pr_hex)
+            sgj_js_nv_hex_bytes(jsp, jop, "uuid_hexbytes", ip, dlen);
+        sg_t10_uuid_desig2str(ip, dlen, c_set, false, true, NULL, blen, b);
+        n = strlen(b);
+        if ((n > 0) && ('\n' == b[n - 1]))
+            b[n - 1] = '\0';
+        sgj_js_nv_s(jsp, jop, "uuid", b);
+        break;
+    default: /* reserved */
+        sgj_js_nv_hex_bytes(jsp, jop, "reserved_designator_hexbytes",
+                            ip, dlen);
+        break;
+    }
+    return true;
+}
+
+static void
+sgj_progress_indication(sgj_state * jsp, sgj_opaque_p jop,
+                        uint16_t prog_indic, bool is_another)
+{
+    uint32_t progress, pr, rem;
+    sgj_opaque_p jo2p;
+    char b[64];
+
+    if (is_another)
+        jo2p = sgj_named_subobject_r(jsp, jop, "another_progress_indication");
+    else
+        jo2p = sgj_named_subobject_r(jsp, jop, "progress_indication");
+    if (NULL == jo2p)
+        return;
+    progress = prog_indic;
+    sgj_js_nv_i(jsp, jo2p, "i", progress);
+    snprintf(b, sizeof(b), "%x", progress);
+    sgj_js_nv_s(jsp, jo2p, "hex", b);
+    progress *= 100;
+    pr = progress / 65536;
+    rem = (progress % 65536) / 656;
+    snprintf(b, sizeof(b), "%d.02%d%%\n", pr, rem);
+    sgj_js_nv_s(jsp, jo2p, "percentage", b);
+}
+
+static bool
+sgj_decode_sks(sgj_state * jsp, sgj_opaque_p jop, const uint8_t * dp, int dlen,
+               int sense_key)
+{
+    switch (sense_key) {
+    case SPC_SK_ILLEGAL_REQUEST:
+        if (dlen < 3) {
+            sgj_js_nv_s(jsp, jop, "illegal_request_sks", dtsp);
+            return false;
+        }
+        sgj_js_nv_ihex_nex(jsp, jop, "sksv", !! (dp[0] & 0x80), false,
+                           sksvp);
+        sgj_js_nv_ihex_nex(jsp, jop, "c_d", !! (dp[0] & 0x40), false,
+                           "c: cdb; d: data-out");
+        sgj_js_nv_ihex_nex(jsp, jop, "bpv", !! (dp[0] & 0x8), false,
+                           "bit pointer (index) valid");
+        sgj_js_nv_i(jsp, jop, "bit_pointer", dp[0] & 0x7);
+        sgj_js_nv_ihex(jsp, jop, "field_pointer",
+                       sg_get_unaligned_be16(dp + 1));
+        break;
+    case SPC_SK_HARDWARE_ERROR:
+    case SPC_SK_MEDIUM_ERROR:
+    case SPC_SK_RECOVERED_ERROR:
+        if (dlen < 3) {
+            sgj_js_nv_s(jsp, jop, "actual_retry_count_sks", dtsp);
+            return false;
+        }
+        sgj_js_nv_ihex_nex(jsp, jop, "sksv", !! (dp[0] & 0x80), false,
+                           sksvp);
+        sgj_js_nv_ihex(jsp, jop, "actual_retry_count",
+                       sg_get_unaligned_be16(dp + 1));
+        break;
+    case SPC_SK_NO_SENSE:
+    case SPC_SK_NOT_READY:
+        if (dlen < 7) {
+            sgj_js_nv_s(jsp, jop, "progress_indication_sks", dtsp);
+            return false;
+        }
+        sgj_js_nv_ihex_nex(jsp, jop, "sksv", !! (dp[0] & 0x80), false,
+                           sksvp);
+        sgj_progress_indication(jsp, jop, sg_get_unaligned_be16(dp + 1),
+                                false);
+        break;
+    case SPC_SK_COPY_ABORTED:
+        if (dlen < 7) {
+            sgj_js_nv_s(jsp, jop, "segment_indication_sks", dtsp);
+            return false;
+        }
+        sgj_js_nv_ihex_nex(jsp, jop, "sksv", !! (dp[0] & 0x80), false,
+                           sksvp);
+        sgj_js_nv_ihex_nex(jsp, jop, "sd", !! (dp[0] & 0x20), false,
+                           "field pointer relative to: 1->segment "
+                           "descriptor, 0->parameter list");
+        sgj_js_nv_ihex_nex(jsp, jop, "bpv", !! (dp[0] & 0x8), false,
+                           "bit pointer (index) valid");
+        sgj_js_nv_i(jsp, jop, "bit_pointer", dp[0] & 0x7);
+        sgj_js_nv_ihex(jsp, jop, "field_pointer",
+                       sg_get_unaligned_be16(dp + 1));
+        break;
+    case SPC_SK_UNIT_ATTENTION:
+        if (dlen < 7) {
+            sgj_js_nv_s(jsp, jop, "segment_indication_sks", dtsp);
+            return false;
+        }
+        sgj_js_nv_ihex_nex(jsp, jop, "sksv", !! (dp[0] & 0x80), false,
+                           sksvp);
+        sgj_js_nv_i(jsp, jop, "overflow", !! (dp[0] & 0x80));
+        break;
+    default:
+        sgj_js_nv_ihex(jsp, jop, "unexpected_sense_key", sense_key);
+        return false;
+    }
+    return true;
+}
+
+#define TPGS_STATE_OPTIMIZED 0x0
+#define TPGS_STATE_NONOPTIMIZED 0x1
+#define TPGS_STATE_STANDBY 0x2
+#define TPGS_STATE_UNAVAILABLE 0x3
+#define TPGS_STATE_OFFLINE 0xe
+#define TPGS_STATE_TRANSITIONING 0xf
+
+static int
+decode_tpgs_state(int st, char * b, int blen)
+{
+    switch (st) {
+    case TPGS_STATE_OPTIMIZED:
+        return sg_scnpr(b, blen, "active/optimized");
+    case TPGS_STATE_NONOPTIMIZED:
+        return sg_scnpr(b, blen, "active/non optimized");
+    case TPGS_STATE_STANDBY:
+        return sg_scnpr(b, blen, "standby");
+    case TPGS_STATE_UNAVAILABLE:
+        return sg_scnpr(b, blen, "unavailable");
+    case TPGS_STATE_OFFLINE:
+        return sg_scnpr(b, blen, "offline");
+    case TPGS_STATE_TRANSITIONING:
+        return sg_scnpr(b, blen, "transitioning between states");
+    default:
+        return sg_scnpr(b, blen, "unknown: 0x%x", st);
+    }
+}
+
+static bool
+sgj_uds_referral_descriptor(sgj_state * jsp, sgj_opaque_p jop,
+                            const uint8_t * dp, int alen)
+{
+    int dlen = alen - 2;
+    int k, j, g, f, aas;
+    uint64_t ull;
+    const uint8_t * tp;
+    sgj_opaque_p jap, jo2p, ja2p, jo3p;
+    char c[40];
+
+    sgj_js_nv_ihex_nex(jsp, jop, "not_all_r", (dp[2] & 0x1), false,
+                       "Not all referrals");
+    dp += 4;
+    jap = sgj_named_subarray_r(jsp, jop,
+                               "user_data_segment_referral_descriptor_list");
+    for (k = 0, f = 1; (k + 4) < dlen; k += g, dp += g, ++f) {
+        int ntpgd = dp[3];
+
+        jo2p = sgj_new_unattached_object_r(jsp);
+        g = (ntpgd * 4) + 20;
+        sgj_js_nv_ihex(jsp, jo2p, "number_of_target_port_group_descriptors",
+                       ntpgd);
+        if ((k + g) > dlen) {
+            sgj_js_nv_i(jsp, jo2p, "truncated_descriptor_dlen", dlen);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            return false;
+        }
+        ull = sg_get_unaligned_be64(dp + 4);
+        sgj_js_nv_ihex(jsp, jo2p, "first_user_date_sgment_lba", ull);
+        ull = sg_get_unaligned_be64(dp + 12);
+        sgj_js_nv_ihex(jsp, jo2p, "last_user_date_sgment_lba", ull);
+        ja2p = sgj_named_subarray_r(jsp, jo2p,
+                                    "target_port_group_descriptor_list");
+        for (j = 0; j < ntpgd; ++j) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            tp = dp + 20 + (j * 4);
+            aas = tp[0] & 0xf;
+            decode_tpgs_state(aas, c, sizeof(c));
+            sgj_js_nv_ihexstr(jsp, jo3p, "asymmetric_access_state", aas,
+                              NULL, c);
+            sgj_js_nv_ihex(jsp, jo3p, "target_port_group",
+                           sg_get_unaligned_be16(tp + 2));
+            sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+        }
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+    return true;
+}
+
+/* Copy of static array in sg_lib.c */
+static const char * dd_usage_reason_str_arr[] = {
+    "Unknown",
+    "resend this and further commands to:",
+    "resend this command to:",
+    "new subsidiary lu added to this administrative lu:",
+    "administrative lu associated with a preferred binding:",
+   };
+
+static bool
+sgj_js_sense_descriptors(sgj_state * jsp, sgj_opaque_p jop,
+                         const struct sg_scsi_sense_hdr * sshp,
+                         const uint8_t * sbp, int sb_len)
+{
+    bool processed = true;
+    int add_sb_len, desc_len, k, dt, sense_key, n, sds;
+    uint16_t sct_sc;
+    uint64_t ull;
+    const uint8_t * descp;
+    const char * cp;
+    sgj_opaque_p jap, jo2p, jo3p;
+    char b[80];
+    static const int blen = sizeof(b);
+    static const char * parsing = "parsing_error";
+#if 0
+    static const char * eccp = "Extended copy command";
+    static const char * ddp = "destination device";
+#endif
+
+    add_sb_len = sshp->additional_length;
+    add_sb_len = (add_sb_len < sb_len) ? add_sb_len : sb_len;
+    sense_key = sshp->sense_key;
+    jap = sgj_named_subarray_r(jsp, jop, "sense_data_descriptor_list");
+
+    for (descp = sbp, k = 0; (k < add_sb_len);
+         k += desc_len, descp += desc_len) {
+        int add_d_len = (k < (add_sb_len - 1)) ? descp[1] : -1;
+
+        jo2p = sgj_new_unattached_object_r(jsp);
+        if ((k + add_d_len + 2) > add_sb_len)
+            add_d_len = add_sb_len - k - 2;
+        desc_len = add_d_len + 2;
+        processed = true;
+        dt = descp[0];
+        switch (dt) {
+        case 0:
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt,
+                              NULL, "Information");
+            if (add_d_len >= 10) {
+                int valid = !! (0x80 & descp[2]);
+                sgj_js_nv_ihexstr(jsp, jo2p, "valid", valid, NULL,
+                                  valid ? "as per T10" : "Vendor specific");
+                sgj_js_nv_ihex(jsp, jo2p, "information",
+                               sg_get_unaligned_be64(descp + 4));
+            } else {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+            }
+            break;
+        case 1:
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt,
+                              NULL, "Command specific");
+            if (add_d_len >= 10) {
+                sgj_js_nv_ihex(jsp, jo2p, "command_specific_information",
+                               sg_get_unaligned_be64(descp + 4));
+            } else {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+            }
+            break;
+        case 2:         /* Sense Key Specific */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Sense key specific");
+            processed = sgj_decode_sks(jsp, jo2p, descp + 4, desc_len - 4,
+                                       sense_key);
+            break;
+        case 3:
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Field replaceable unit code");
+            if (add_d_len >= 2)
+                sgj_js_nv_ihex(jsp, jo2p, "field_replaceable_unit_code",
+                               descp[3]);
+            else {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+            }
+            break;
+        case 4:
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Stream commands");
+            if (add_d_len >= 2) {
+                sgj_js_nv_i(jsp, jo2p, "filemark", !! (descp[3] & 0x80));
+                sgj_js_nv_ihex_nex(jsp, jo2p, "eom", !! (descp[3] & 0x40),
+                                   false, "End Of Medium");
+                sgj_js_nv_ihex_nex(jsp, jo2p, "ili", !! (descp[3] & 0x20),
+                                   false, "Incorrect Length Indicator");
+            } else {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+            }
+            break;
+        case 5:
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Block commands");
+            if (add_d_len >= 2)
+                sgj_js_nv_ihex_nex(jsp, jo2p, "ili", !! (descp[3] & 0x20),
+                                   false, "Incorrect Length Indicator");
+            else {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+            }
+            break;
+        case 6:
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "OSD object identification");
+            sgj_js_nv_s(jsp, jo2p, parsing, "Unsupported");
+            processed = false;
+            break;
+        case 7:
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "OSD response integrity check value");
+            sgj_js_nv_s(jsp, jo2p, parsing, "Unsupported");
+            break;
+        case 8:
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "OSD attribute identification");
+            sgj_js_nv_s(jsp, jo2p, parsing, "Unsupported");
+            processed = false;
+            break;
+        case 9:         /* this is defined in SAT (SAT-2) */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "ATA status return");
+            if (add_d_len >= 12) {
+                sgj_js_nv_i(jsp, jo2p, "extend", !! (descp[2] & 1));
+                sgj_js_nv_ihex(jsp, jo2p, "error", descp[3]);
+                sgj_js_nv_ihex(jsp, jo2p, "count",
+                               sg_get_unaligned_be16(descp + 4));
+                ull = ((uint64_t)descp[10] << 40) |
+                       ((uint64_t)descp[8] << 32) |
+                       (descp[6] << 24) |
+                       (descp[11] << 16) |
+                       (descp[9] << 8) |
+                       descp[7];
+                sgj_js_nv_ihex(jsp, jo2p, "lba", ull);
+                sgj_js_nv_ihex(jsp, jo2p, "device", descp[12]);
+                sgj_js_nv_ihex(jsp, jo2p, "status", descp[13]);
+            } else {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+            }
+            break;
+        case 0xa:
+           /* Added in SPC-4 rev 17, became 'Another ...' in rev 34 */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Another progress indication");
+            if (add_d_len < 6) {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+                break;
+            }
+            sgj_js_nv_ihex(jsp, jo2p, "another_sense_key", descp[2]);
+            sgj_js_nv_ihex(jsp, jo2p, "another_additional_sense_code",
+                           descp[3]);
+            sgj_js_nv_ihex(jsp, jo2p,
+                           "another_additional_sense_code_qualifier",
+                           descp[4]);
+            sgj_progress_indication(jsp, jo2p,
+                                    sg_get_unaligned_be16(descp + 6), true);
+            break;
+        case 0xb:       /* Added in SPC-4 rev 23, defined in SBC-3 rev 22 */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "User data segment referral");
+            if (add_d_len < 2) {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+                break;
+            }
+            if (! sgj_uds_referral_descriptor(jsp, jo2p, descp, add_d_len)) {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+            }
+            break;
+        case 0xc:       /* Added in SPC-4 rev 28 */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Forwarded sense data");
+            if (add_d_len < 2) {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+                break;
+            }
+            sgj_js_nv_ihex_nex(jsp, jo2p, "fsdt", !! (0x80 & descp[2]),
+                               false, "Forwarded Sense Data Truncated");
+            sds = (0x7 & descp[2]);
+            if (sds < 1)
+                snprintf(b, blen, "%s [%d]", "Unknown", sds);
+            else if (sds > 9)
+                snprintf(b, blen, "%s [%d]", "Reserved", sds);
+            else {
+                n = 0;
+                n += sg_scnpr(b + n, blen - n, "EXTENDED COPY command copy %s",
+                              (sds == 1) ? "source" : "destination");
+                if (sds > 1)
+                    n += sg_scnpr(b + n, blen - n, " %d", sds - 1);
+            }
+            sgj_js_nv_ihexstr(jsp, jo2p, "sense_data_source",
+                              (0x7 & descp[2]), NULL, b);
+            jo3p = sgj_named_subobject_r(jsp, jo2p, "forwarded_sense_data");
+            sgj_js_sense(jsp, jo3p, descp + 4, desc_len - 4);
+            break;
+        case 0xd:       /* Added in SBC-3 rev 36d */
+            /* this descriptor combines descriptors 0, 1, 2 and 3 */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Direct-access block device");
+            if (add_d_len < 28) {
+                sgj_js_nv_s(jsp, jo2p, parsing, dtsp);
+                processed = false;
+                break;
+            }
+            sgj_js_nv_i(jsp, jo2p, "valid", (0x80 & descp[2]));
+            sgj_js_nv_ihex_nex(jsp, jo2p, "ili", !! (0x20 & descp[2]),
+                               false, "Incorrect Length Indicator");
+            processed = sgj_decode_sks(jsp, jo2p, descp + 4, desc_len - 4,
+                                       sense_key);
+            sgj_js_nv_ihex(jsp, jo2p, "field_replaceable_unit_code",
+                           descp[7]);
+            sgj_js_nv_ihex(jsp, jo2p, "information",
+                           sg_get_unaligned_be64(descp + 8));
+            sgj_js_nv_ihex(jsp, jo2p, "command_specific_information",
+                           sg_get_unaligned_be64(descp + 16));
+            break;
+        case 0xe:       /* Added in SPC-5 rev 6 (for Bind/Unbind) */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Device designation");
+            n = descp[3];
+            cp = (n < (int)SG_ARRAY_SIZE(dd_usage_reason_str_arr)) ?
+                  dd_usage_reason_str_arr[n] : "Unknown (reserved)";
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_usage_reason",
+                              n, NULL, cp);
+            jo3p = sgj_named_subobject_r(jsp, jo2p,
+                                         "device_designation_descriptor");
+            sgj_js_designation_descriptor(jsp, jo3p, descp + 4, desc_len - 4);
+            break;
+        case 0xf:       /* Added in SPC-5 rev 10 (for Write buffer) */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "Microcode activation");
+            if (add_d_len < 6) {
+                sgj_js_nv_s(jsp, jop, parsing, dtsp);
+                processed = false;
+                break;
+            }
+            sgj_js_nv_ihex(jsp, jo2p, "microcode_activation_time",
+                           sg_get_unaligned_be16(descp + 6));
+            break;
+        case 0xde:       /* NVME Status Field; vendor (sg3_utils) specific */
+            sgj_js_nv_ihexstr(jsp, jo2p, "descriptor_type", dt, NULL,
+                              "NVME status (sg3_utils)");
+            if (add_d_len < 6) {
+                sgj_js_nv_s(jsp, jop, parsing, dtsp);
+                processed = false;
+                break;
+            }
+            sgj_js_nv_ihex_nex(jsp, jo2p, "dnr", !! (0x80 & descp[5]),
+                               false, "Do not retry");
+            sgj_js_nv_ihex_nex(jsp, jo2p, "m", !! (0x40 & descp[5]),
+                               false, "More");
+            sct_sc = sg_get_unaligned_be16(descp + 6);
+            sgj_js_nv_ihexstr_nex
+                (jsp, jo2p, "sct_sc", sct_sc, true, NULL,
+                 sg_get_nvme_cmd_status_str(sct_sc, blen, b),
+                 "Status Code Type (upper 8 bits) and Status Code");
+            break;
+        default:
+            if (dt >= 0x80)
+                sgj_js_nv_ihex(jsp, jo2p, "vendor_specific_descriptor_type",
+                               dt);
+            else
+                sgj_js_nv_ihex(jsp, jo2p, "unknown_descriptor_type", dt);
+            sgj_js_nv_hex_bytes(jsp, jo2p, "descriptor_hexbytes",
+                                descp, desc_len);
+            processed = false;
+            break;
+        }
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+    return processed;
+}
+
+#define ASCQ_ATA_PT_INFO_AVAILABLE 0x1d  /* corresponding ASC is 0 */
+
+/* Fetch sense information */
+bool
+sgj_js_sense(sgj_state * jsp, sgj_opaque_p jop, const uint8_t * sbp,
+             int sb_len)
+{
+    bool descriptor_format = false;
+    bool sdat_ovfl = false;
+    bool ret = true;
+    bool valid_info_fld;
+    int len, n;
+    uint32_t info;
+    uint8_t resp_code;
+    const char * ebp = NULL;
+    char ebuff[64];
+    char b[256];
+    struct sg_scsi_sense_hdr ssh;
+    static int blen = sizeof(b);
+    static int elen = sizeof(ebuff);
+
+    if ((NULL == sbp) || (sb_len < 1)) {
+        snprintf(ebuff, elen, "sense buffer empty\n");
+        ebp = ebuff;
+        ret = false;
+        goto fini;
+    }
+    resp_code = 0x7f & sbp[0];
+    valid_info_fld = !!(sbp[0] & 0x80);
+    len = sb_len;
+    if (! sg_scsi_normalize_sense(sbp, sb_len, &ssh)) {
+        ebp = "unable to normalize sense buffer";
+        ret = false;
+        goto fini;
+    }
+    /* We have been able to normalize the sense buffer */
+    switch (resp_code) {
+    case 0x70:      /* fixed, current */
+        ebp = "Fixed format, current";
+        len = (sb_len > 7) ? (sbp[7] + 8) : sb_len;
+        len = (len > sb_len) ? sb_len : len;
+        sdat_ovfl = (len > 2) ? !!(sbp[2] & 0x10) : false;
+        break;
+    case 0x71:      /* fixed, deferred */
+        /* error related to a previous command */
+        ebp = "Fixed format, <<<deferred>>>";
+        len = (sb_len > 7) ? (sbp[7] + 8) : sb_len;
+        len = (len > sb_len) ? sb_len : len;
+        sdat_ovfl = (len > 2) ? !!(sbp[2] & 0x10) : false;
+        break;
+    case 0x72:      /* descriptor, current */
+        descriptor_format = true;
+        ebp = "Descriptor format, current";
+        sdat_ovfl = (sb_len > 4) ? !!(sbp[4] & 0x80) : false;
+        break;
+    case 0x73:      /* descriptor, deferred */
+        descriptor_format = true;
+        ebp = "Descriptor format, <<<deferred>>>";
+        sdat_ovfl = (sb_len > 4) ? !!(sbp[4] & 0x80) : false;
+        break;
+    default:
+        sg_scnpr(ebuff, elen, "Unknown code: 0x%x", resp_code);
+        ebp = ebuff;
+        break;
+    }
+    sgj_js_nv_ihexstr(jsp, jop, "response_code", resp_code, NULL, ebp);
+    sgj_js_nv_b(jsp, jop, "descriptor_format", descriptor_format);
+    sgj_js_nv_ihex_nex(jsp, jop, "sdat_ovfl", sdat_ovfl, false,
+                       "Sense data overflow");
+    sgj_js_nv_ihexstr(jsp, jop, "sense_key", ssh.sense_key, NULL,
+                      sg_lib_sense_key_desc[ssh.sense_key]);
+    sgj_js_nv_ihex(jsp, jop, "additional_sense_code", ssh.asc);
+    sgj_js_nv_ihex(jsp, jop, "additional_sense_code_qualifier", ssh.ascq);
+    sgj_js_nv_s(jsp, jop, "additional_sense_str",
+                sg_get_additional_sense_str(ssh.asc, ssh.ascq, false,
+                                             blen, b));
+    if (descriptor_format) {
+        if (len > 8) {
+            ret = sgj_js_sense_descriptors(jsp, jop, &ssh, sbp + 8, len - 8);
+            if (ret == false) {
+                ebp = "unable to decode sense descriptor";
+                goto fini;
+            }
+        }
+    } else if ((len > 12) && (0 == ssh.asc) &&
+               (ASCQ_ATA_PT_INFO_AVAILABLE == ssh.ascq)) {
+        /* SAT ATA PASS-THROUGH fixed format */
+        sgj_js_nv_ihex(jsp, jop, "error", sbp[3]);
+        sgj_js_nv_ihex(jsp, jop, "status", sbp[4]);
+        sgj_js_nv_ihex(jsp, jop, "device", sbp[5]);
+        sgj_js_nv_i(jsp, jop, "extend", !! (0x80 & sbp[8]));
+        sgj_js_nv_i(jsp, jop, "count_upper_nonzero", !! (0x40 & sbp[8]));
+        sgj_js_nv_i(jsp, jop, "lba_upper_nonzero", !! (0x20 & sbp[8]));
+        sgj_js_nv_i(jsp, jop, "log_index", (0xf & sbp[8]));
+        sgj_js_nv_i(jsp, jop, "lba", sg_get_unaligned_le24(sbp + 9));
+    } else if (len > 2) {   /* fixed format */
+        sgj_js_nv_i(jsp, jop, "valid", valid_info_fld);
+        sgj_js_nv_i(jsp, jop, "filemark", !! (sbp[2] & 0x80));
+        sgj_js_nv_ihex_nex(jsp, jop, "eom", !! (sbp[2] & 0x40),
+                           false, "End Of Medium");
+        sgj_js_nv_ihex_nex(jsp, jop, "ili", !! (sbp[2] & 0x20),
+                           false, "Incorrect Length Indicator");
+        info = sg_get_unaligned_be32(sbp + 3);
+        sgj_js_nv_ihex(jsp, jop, "information", info);
+        sgj_js_nv_ihex(jsp, jop, "additional_sense_length", sbp[7]);
+        if (sb_len > 11) {
+            info = sg_get_unaligned_be32(sbp + 8);
+            sgj_js_nv_ihex(jsp, jop, "command_specific_information", info);
+        }
+        if (sb_len > 14)
+            sgj_js_nv_ihex(jsp, jop, "field_replaceable_unit_code", sbp[14]);
+        if (sb_len > 17)
+            sgj_decode_sks(jsp, jop, sbp + 15, sb_len - 15, ssh.sense_key);
+        n =  sbp[7];
+        n = (sb_len > n) ? n : sb_len;
+        sgj_js_nv_ihex(jsp, jop, "number_of_bytes_beyond_18",
+                       (n > 18) ? n - 18 : 0);
+    } else {
+        snprintf(ebuff, sizeof(ebuff), "sb_len=%d too short", sb_len);
+        ebp = ebuff;
+        ret = false;
+    }
+fini:
+    if ((! ret) && ebp)
+        sgj_js_nv_s(jsp, jop, "sense_decode_error", ebp);
+    return ret;
+}
+
+#endif
diff --git a/lib/sg_pt_common.c b/lib/sg_pt_common.c
new file mode 100644
index 0000000..9af5020
--- /dev/null
+++ b/lib/sg_pt_common.c
@@ -0,0 +1,651 @@
+/*
+ * Copyright (c) 2009-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+#include "sg_pr2serr.h"
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+#include "sg_pt_nvme.h"
+#endif
+
+static const char * scsi_pt_version_str = "3.19 20220127";
+
+/* List of external functions that need to be defined for each OS are
+ * listed at the top of sg_pt_dummy.c   */
+
+const char *
+scsi_pt_version()
+{
+    return scsi_pt_version_str;
+}
+
+const char *
+sg_pt_version()
+{
+    return scsi_pt_version_str;
+}
+
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
+
+#define SAVING_PARAMS_UNSUP 0x39
+#define INVALID_FIELD_IN_CDB 0x24
+#define INVALID_FIELD_IN_PARAM_LIST 0x26
+#define PARAMETER_LIST_LENGTH_ERR 0x1a
+
+static const char * nvme_scsi_vendor_str = "NVMe    ";
+
+
+#define F_SA_LOW                0x80    /* cdb byte 1, bits 4 to 0 */
+#define F_SA_HIGH               0x100   /* as used by variable length cdbs */
+#define FF_SA (F_SA_HIGH | F_SA_LOW)
+#define F_INV_OP                0x200
+
+/* Table of SCSI operation code (opcodes) supported by SNTL */
+static struct sg_opcode_info_t sg_opcode_info_arr[] =
+{
+    {0x0, 0, 0, {6,              /* TEST UNIT READY */
+      0, 0, 0, 0, 0xc7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} },
+    {0x3, 0, 0, {6,             /* REQUEST SENSE */
+      0xe1, 0, 0, 0xff, 0xc7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} },
+    {0x12, 0, 0, {6,            /* INQUIRY */
+      0xe3, 0xff, 0xff, 0xff, 0xc7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} },
+    {0x1b, 0, 0, {6,            /* START STOP UNIT */
+      0x1, 0, 0xf, 0xf7, 0xc7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} },
+    {0x1c, 0, 0, {6,            /* RECEIVE DIAGNOSTIC RESULTS */
+      0x1, 0xff, 0xff, 0xff, 0xc7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} },
+    {0x1d, 0, 0, {6,            /* SEND DIAGNOSTIC */
+      0xf7, 0x0, 0xff, 0xff, 0xc7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} },
+    {0x25, 0, 0, {10,            /* READ CAPACITY(10) */
+      0x1, 0xff, 0xff, 0xff, 0xff, 0, 0, 0x1, 0xc7, 0, 0, 0, 0, 0, 0} },
+    {0x28, 0, 0, {10,            /* READ(10) */
+      0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xc7, 0, 0, 0, 0,
+      0, 0} },
+    {0x2a, 0, 0, {10,            /* WRITE(10) */
+      0xfb, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xc7, 0, 0, 0, 0,
+      0, 0} },
+    {0x2f, 0, 0, {10,            /* VERIFY(10) */
+      0xf6, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xc7, 0, 0, 0, 0,
+      0, 0} },
+    {0x35, 0, 0, {10,            /* SYNCHRONIZE CACHE(10) */
+      0x7, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xc7, 0, 0, 0, 0,
+      0, 0} },
+    {0x41, 0, 0, {10,            /* WRITE SAME(10) */
+      0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xc7, 0, 0, 0, 0,
+      0, 0} },
+    {0x55, 0, 0, {10,           /* MODE SELECT(10) */
+      0x13, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xc7, 0, 0, 0, 0, 0, 0} },
+    {0x5a, 0, 0, {10,           /* MODE SENSE(10) */
+      0x18, 0xff, 0xff, 0x0, 0x0, 0x0, 0xff, 0xff, 0xc7, 0, 0, 0, 0, 0, 0} },
+    {0x88, 0, 0, {16,            /* READ(16) */
+      0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+      0xff, 0xff, 0xff, 0xc7} },
+    {0x8a, 0, 0, {16,            /* WRITE(16) */
+      0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+      0xff, 0xff, 0xff, 0xc7} },
+    {0x8f, 0, 0, {16,            /* VERIFY(16) */
+      0xf6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+      0xff, 0xff, 0x3f, 0xc7} },
+    {0x91, 0, 0, {16,            /* SYNCHRONIZE CACHE(16) */
+      0x7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+      0xff, 0xff, 0x3f, 0xc7} },
+    {0x93, 0, 0, {16,            /* WRITE SAME(16) */
+      0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+      0xff, 0xff, 0x3f, 0xc7} },
+    {0x9e, 0x10, F_SA_LOW, {16,  /* READ CAPACITY(16) [service action in] */
+      0x10, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+      0xff, 0xff, 0x1, 0xc7} },
+    {0xa0, 0, 0, {12,           /* REPORT LUNS */
+      0xe3, 0xff, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0, 0xc7, 0, 0, 0, 0} },
+    {0xa3, 0xc, F_SA_LOW, {12,  /* REPORT SUPPORTED OPERATION CODES */
+      0xc, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0xc7, 0, 0, 0,
+      0} },
+    {0xa3, 0xd, F_SA_LOW, {12,  /* REPORT SUPPORTED TASK MAN. FUNCTIONS */
+      0xd, 0x80, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0, 0xc7, 0, 0, 0, 0} },
+
+    {0xff, 0xffff, 0xffff, {0,  /* Sentinel, keep as last element */
+      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} },
+};
+
+/* Returns pointer to array of struct sg_opcode_info_t of SCSI commands
+ * translated to NVMe. */
+const struct sg_opcode_info_t *
+sg_get_opcode_translation(void)
+{
+    return sg_opcode_info_arr;
+}
+
+/* Given the NVMe Identify controller response and optionally the NVMe
+ * Identify namespace response (NULL otherwise), generate the SCSI VPD
+ * page 0x83 (device identification) descriptor(s) in dop. Return the
+ * number of bytes written which will not exceed max_do_len. Probably use
+ * Peripheral Device Type (pdt) of 0 (disk) for don't know. Transport
+ * protocol (tproto) should be -1 if not known, else SCSI value.
+ * N.B. Does not write total VPD page length into dop[2:3] . */
+int
+sg_make_vpd_devid_for_nvme(const uint8_t * nvme_id_ctl_p,
+                           const uint8_t * nvme_id_ns_p, int pdt,
+                           int tproto, uint8_t * dop, int max_do_len)
+{
+    bool have_nguid, have_eui64;
+    int k, n;
+    char b[4];
+
+    if ((NULL == nvme_id_ctl_p) || (NULL == dop) || (max_do_len < 56))
+        return 0;
+
+    memset(dop, 0, max_do_len);
+    dop[0] = 0x1f & pdt;  /* (PQ=0)<<5 | (PDT=pdt); 0 or 0xd (SES) */
+    dop[1] = 0x83;      /* Device Identification VPD page number */
+    /* Build a T10 Vendor ID based designator (desig_id=1) for controller */
+    if (tproto >= 0) {
+        dop[4] = ((0xf & tproto) << 4) | 0x2;
+        dop[5] = 0xa1; /* PIV=1, ASSOC=2 (target device), desig_id=1 */
+    } else {
+        dop[4] = 0x2;  /* Prococol id=0, code_set=2 (ASCII) */
+        dop[5] = 0x21; /* PIV=0, ASSOC=2 (target device), desig_id=1 */
+    }
+    memcpy(dop + 8, nvme_scsi_vendor_str, 8); /* N.B. this is "NVMe    " */
+    memcpy(dop + 16, nvme_id_ctl_p + 24, 40);  /* MN */
+    for (k = 40; k > 0; --k) {
+        if (' ' == dop[15 + k])
+            dop[15 + k] = '_'; /* convert trailing spaces */
+        else
+            break;
+    }
+    if (40 == k)
+        --k;
+    n = 16 + 1 + k;
+    if (max_do_len < (n + 20))
+        return 0;
+    memcpy(dop + n, nvme_id_ctl_p + 4, 20); /* SN */
+    for (k = 20; k > 0; --k) {  /* trim trailing spaces */
+        if (' ' == dop[n + k - 1])
+            dop[n + k - 1] = '\0';
+        else
+            break;
+    }
+    n += k;
+    if (0 != (n % 4))
+        n = ((n / 4) + 1) * 4;  /* round up to next modulo 4 */
+    dop[7] = n - 8;
+    if (NULL == nvme_id_ns_p)
+        return n;
+
+    /* Look for NGUID (16 byte identifier) or EUI64 (8 byte) fields in
+     * NVME Identify for namespace. If found form a EUI and a SCSI string
+     * descriptor for non-zero NGUID or EUI64 (prefer NGUID if both). */
+    have_nguid = ! sg_all_zeros(nvme_id_ns_p + 104, 16);
+    have_eui64 = ! sg_all_zeros(nvme_id_ns_p + 120, 8);
+    if ((! have_nguid) && (! have_eui64))
+        return n;
+    if (have_nguid) {
+        if (max_do_len < (n + 20))
+            return n;
+        dop[n + 0] = 0x1;  /* Prococol id=0, code_set=1 (binary) */
+        dop[n + 1] = 0x02; /* PIV=0, ASSOC=0 (lu), desig_id=2 (eui) */
+        dop[n + 3] = 16;
+        memcpy(dop + n + 4, nvme_id_ns_p + 104, 16);
+        n += 20;
+        if (max_do_len < (n + 40))
+            return n;
+        dop[n + 0] = 0x3;  /* Prococol id=0, code_set=3 (utf8) */
+        dop[n + 1] = 0x08; /* PIV=0, ASSOC=0 (lu), desig_id=8 (scsi string) */
+        dop[n + 3] = 36;
+        memcpy(dop + n + 4, "eui.", 4);
+        for (k = 0; k < 16; ++k) {
+            snprintf(b, sizeof(b), "%02X", nvme_id_ns_p[104 + k]);
+            memcpy(dop + n + 8 + (2 * k), b, 2);
+        }
+        return n + 40;
+    } else {    /* have_eui64 is true, 8 byte identifier */
+        if (max_do_len < (n + 12))
+            return n;
+        dop[n + 0] = 0x1;  /* Prococol id=0, code_set=1 (binary) */
+        dop[n + 1] = 0x02; /* PIV=0, ASSOC=0 (lu), desig_id=2 (eui) */
+        dop[n + 3] = 8;
+        memcpy(dop + n + 4, nvme_id_ns_p + 120, 8);
+        n += 12;
+        if (max_do_len < (n + 24))
+            return n;
+        dop[n + 0] = 0x3;  /* Prococol id=0, code_set=3 (utf8) */
+        dop[n + 1] = 0x08; /* PIV=0, ASSOC=0 (lu), desig_id=8 (scsi string) */
+        dop[n + 3] = 20;
+        memcpy(dop + n + 4, "eui.", 4);
+        for (k = 0; k < 8; ++k) {
+            snprintf(b, sizeof(b), "%02X", nvme_id_ns_p[120 + k]);
+            memcpy(dop + n + 8 + (2 * k), b, 2);
+        }
+        return n + 24;
+    }
+}
+
+/* Disconnect-Reconnect page for mode_sense */
+static int
+resp_disconnect_pg(uint8_t * p, int pcontrol)
+{
+    uint8_t disconnect_pg[] = {0x2, 0xe, 128, 128, 0, 10, 0, 0,
+                               0, 0, 0, 0, 0, 0, 0, 0};
+
+    memcpy(p, disconnect_pg, sizeof(disconnect_pg));
+    if (1 == pcontrol)
+        memset(p + 2, 0, sizeof(disconnect_pg) - 2);
+    return sizeof(disconnect_pg);
+}
+
+static uint8_t caching_m_pg[] = {0x8, 18, 0x14, 0, 0xff, 0xff, 0, 0,
+                                 0xff, 0xff, 0xff, 0xff, 0x80, 0x14, 0, 0,
+                                 0, 0, 0, 0};
+
+/* Control mode page (SBC) for mode_sense */
+static int
+resp_caching_m_pg(unsigned char *p, int pcontrol, bool wce)
+{       /* Caching page for mode_sense */
+        uint8_t ch_caching_m_pg[] = {/* 0x8, 18, */ 0x4, 0, 0, 0, 0, 0,
+                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        uint8_t d_caching_m_pg[] = {0x8, 18, 0x14, 0, 0xff, 0xff, 0, 0,
+                0xff, 0xff, 0xff, 0xff, 0x80, 0x14, 0, 0,     0, 0, 0, 0};
+
+        // if (SDEBUG_OPT_N_WCE & sdebug_opts)
+                caching_m_pg[2] &= ~0x4;  /* set WCE=0 (default WCE=1) */
+        if ((0 == pcontrol) || (3 == pcontrol)) {
+            if (wce)
+                caching_m_pg[2] |= 0x4;
+            else
+                caching_m_pg[2] &= ~0x4;
+        }
+        memcpy(p, caching_m_pg, sizeof(caching_m_pg));
+        if (1 == pcontrol) {
+            if (wce)
+                ch_caching_m_pg[2] |= 0x4;
+            else
+                ch_caching_m_pg[2] &= ~0x4;
+            memcpy(p + 2, ch_caching_m_pg, sizeof(ch_caching_m_pg));
+        }
+        else if (2 == pcontrol) {
+            if (wce)
+                d_caching_m_pg[2] |= 0x4;
+            else
+                d_caching_m_pg[2] &= ~0x4;
+            memcpy(p, d_caching_m_pg, sizeof(d_caching_m_pg));
+        }
+        return sizeof(caching_m_pg);
+}
+
+static uint8_t ctrl_m_pg[] = {0xa, 10, 2, 0, 0, 0, 0, 0,
+                              0, 0, 0x2, 0x4b};
+
+/* Control mode page for mode_sense */
+static int
+resp_ctrl_m_pg(uint8_t *p, int pcontrol)
+{
+    uint8_t ch_ctrl_m_pg[] = {/* 0xa, 10, */ 0x6, 0, 0, 0, 0, 0,
+                              0, 0, 0, 0};
+    uint8_t d_ctrl_m_pg[] = {0xa, 10, 2, 0, 0, 0, 0, 0,
+                             0, 0, 0x2, 0x4b};
+
+    memcpy(p, ctrl_m_pg, sizeof(ctrl_m_pg));
+    if (1 == pcontrol)
+        memcpy(p + 2, ch_ctrl_m_pg, sizeof(ch_ctrl_m_pg));
+    else if (2 == pcontrol)
+        memcpy(p, d_ctrl_m_pg, sizeof(d_ctrl_m_pg));
+    return sizeof(ctrl_m_pg);
+}
+
+static uint8_t ctrl_ext_m_pg[] = {0x4a, 0x1, 0, 0x1c,  0, 0, 0x40, 0,
+                                  0, 0, 0, 0,  0, 0, 0, 0,
+                                  0, 0, 0, 0,  0, 0, 0, 0,
+                                  0, 0, 0, 0,  0, 0, 0, 0, };
+
+/* Control Extension mode page [0xa,0x1] for mode_sense */
+static int
+resp_ctrl_ext_m_pg(uint8_t *p, int pcontrol)
+{
+    uint8_t ch_ctrl_ext_m_pg[] = {/* 0x4a, 0x1, 0, 0x1c, */ 0, 0, 0, 0,
+                         0, 0, 0, 0,  0, 0, 0, 0,
+                         0, 0, 0, 0,  0, 0, 0, 0,
+                         0, 0, 0, 0,  0, 0, 0, 0, };
+    uint8_t d_ctrl_ext_m_pg[] = {0x4a, 0x1, 0, 0x1c,  0, 0, 0x40, 0,
+                         0, 0, 0, 0,  0, 0, 0, 0,
+                         0, 0, 0, 0,  0, 0, 0, 0,
+                         0, 0, 0, 0,  0, 0, 0, 0, };
+
+    memcpy(p, ctrl_ext_m_pg, sizeof(ctrl_ext_m_pg));
+    if (1 == pcontrol)
+        memcpy(p + 4, ch_ctrl_ext_m_pg, sizeof(ch_ctrl_ext_m_pg));
+    else if (2 == pcontrol)
+        memcpy(p, d_ctrl_ext_m_pg, sizeof(d_ctrl_ext_m_pg));
+    return sizeof(ctrl_ext_m_pg);
+}
+
+static uint8_t iec_m_pg[] = {0x1c, 0xa, 0x08, 0, 0, 0, 0, 0, 0, 0, 0x0, 0x0};
+
+/* Informational Exceptions control mode page for mode_sense */
+static int
+resp_iec_m_pg(uint8_t *p, int pcontrol)
+{
+    uint8_t ch_iec_m_pg[] = {/* 0x1c, 0xa, */ 0x4, 0xf, 0, 0, 0, 0, 0, 0,
+                             0x0, 0x0};
+    uint8_t d_iec_m_pg[] = {0x1c, 0xa, 0x08, 0, 0, 0, 0, 0, 0, 0, 0x0, 0x0};
+
+    memcpy(p, iec_m_pg, sizeof(iec_m_pg));
+    if (1 == pcontrol)
+        memcpy(p + 2, ch_iec_m_pg, sizeof(ch_iec_m_pg));
+    else if (2 == pcontrol)
+        memcpy(p, d_iec_m_pg, sizeof(d_iec_m_pg));
+    return sizeof(iec_m_pg);
+}
+
+static uint8_t vs_ua_m_pg[] = {0x0, 0xe, 0, 0, 0, 0, 0, 0,
+                               0, 0, 0, 0, 0, 0, 0};
+
+/* Vendor specific Unit Attention mode page for mode_sense */
+static int
+resp_vs_ua_m_pg(uint8_t *p, int pcontrol)
+{
+    uint8_t ch_vs_ua_m_pg[] = {/* 0x0, 0xe, */ 0xff, 0, 0, 0, 0, 0,
+                               0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t d_vs_ua_m_pg[] = {0x0, 0xe, 0, 0, 0, 0, 0, 0,
+                              0, 0, 0, 0, 0, 0, 0, 0};
+
+    memcpy(p, vs_ua_m_pg, sizeof(vs_ua_m_pg));
+    if (1 == pcontrol)
+        memcpy(p + 2, ch_vs_ua_m_pg, sizeof(ch_vs_ua_m_pg));
+    else if (2 == pcontrol)
+        memcpy(p, d_vs_ua_m_pg, sizeof(d_vs_ua_m_pg));
+    return sizeof(vs_ua_m_pg);
+}
+
+void
+sntl_init_dev_stat(struct sg_sntl_dev_state_t * dsp)
+{
+    if (dsp) {
+        dsp->scsi_dsense = !! (0x4 & ctrl_m_pg[2]);
+        dsp->enclosure_override = vs_ua_m_pg[2];
+    }
+}
+
+
+#define SDEBUG_MAX_MSENSE_SZ 256
+
+/* Only support MODE SENSE(10). Returns the number of bytes written to dip,
+ * or -1 if error info placed in resp. */
+int
+sntl_resp_mode_sense10(const struct sg_sntl_dev_state_t * dsp,
+                       const uint8_t * cdbp, uint8_t * dip, int mx_di_len,
+                       struct sg_sntl_result_t * resp)
+{
+    bool dbd, llbaa, is_disk, bad_pcode;
+    int pcontrol, pcode, subpcode, bd_len, alloc_len, offset, len;
+    const uint32_t num_blocks = 0x100000;       /* made up */
+    const uint32_t lb_size = 512;               /* guess */
+    uint8_t dev_spec;
+    uint8_t * ap;
+    uint8_t arr[SDEBUG_MAX_MSENSE_SZ];
+
+    memset(resp, 0, sizeof(*resp));
+    dbd = !! (cdbp[1] & 0x8);         /* disable block descriptors */
+    pcontrol = (cdbp[2] & 0xc0) >> 6;
+    pcode = cdbp[2] & 0x3f;
+    subpcode = cdbp[3];
+    llbaa = !!(cdbp[1] & 0x10);
+    is_disk = sg_pdt_s_eq(sg_lib_pdt_decay(dsp->pdt), PDT_DISK_ZBC);
+    if (is_disk && !dbd)
+        bd_len = llbaa ? 16 : 8;
+    else
+        bd_len = 0;
+    alloc_len = sg_get_unaligned_be16(cdbp + 7);
+    memset(arr, 0, SDEBUG_MAX_MSENSE_SZ);
+    if (0x3 == pcontrol) {  /* Saving values not supported */
+        resp->asc = SAVING_PARAMS_UNSUP;
+        goto err_out;
+    }
+    /* for disks set DPOFUA bit and clear write protect (WP) bit */
+    if (is_disk)
+        dev_spec = 0x10;        /* =0x90 if WP=1 implies read-only */
+    else
+        dev_spec = 0x0;
+    arr[3] = dev_spec;
+    if (16 == bd_len)
+        arr[4] = 0x1;   /* set LONGLBA bit */
+    arr[7] = bd_len;        /* assume 255 or less */
+    offset = 8;
+    ap = arr + offset;
+
+    if (8 == bd_len) {
+        sg_put_unaligned_be32(num_blocks, ap + 0);
+        sg_put_unaligned_be16((uint16_t)lb_size, ap + 6);
+        offset += bd_len;
+        ap = arr + offset;
+    } else if (16 == bd_len) {
+        sg_put_unaligned_be64(num_blocks, ap + 0);
+        sg_put_unaligned_be32(lb_size, ap + 12);
+        offset += bd_len;
+        ap = arr + offset;
+    }
+    bad_pcode = false;
+
+    switch (pcode) {
+    case 0x2:       /* Disconnect-Reconnect page, all devices */
+        if (0x0 == subpcode)
+            len = resp_disconnect_pg(ap, pcontrol);
+        else {
+            len = 0;
+            bad_pcode = true;
+        }
+        offset += len;
+        break;
+    case 0x8:       /* Caching Mode page, disk (like) devices */
+        if (! is_disk) {
+            len = 0;
+            bad_pcode = true;
+        } else if (0x0 == subpcode)
+            len = resp_caching_m_pg(ap, pcontrol, dsp->wce);
+        else {
+            len = 0;
+            bad_pcode = true;
+        }
+        offset += len;
+        break;
+    case 0xa:       /* Control Mode page, all devices */
+        if (0x0 == subpcode)
+            len = resp_ctrl_m_pg(ap, pcontrol);
+        else if (0x1 == subpcode)
+            len = resp_ctrl_ext_m_pg(ap, pcontrol);
+        else {
+            len = 0;
+            bad_pcode = true;
+        }
+        offset += len;
+        break;
+    case 0x1c:      /* Informational Exceptions Mode page, all devices */
+        if (0x0 == subpcode)
+            len = resp_iec_m_pg(ap, pcontrol);
+        else {
+            len = 0;
+            bad_pcode = true;
+        }
+        offset += len;
+        break;
+    case 0x3f:      /* Read all Mode pages */
+        if ((0 == subpcode) || (0xff == subpcode)) {
+            len = 0;
+            len = resp_disconnect_pg(ap + len, pcontrol);
+            if (is_disk)
+                len += resp_caching_m_pg(ap + len, pcontrol, dsp->wce);
+            len += resp_ctrl_m_pg(ap + len, pcontrol);
+            if (0xff == subpcode)
+                len += resp_ctrl_ext_m_pg(ap + len, pcontrol);
+            len += resp_iec_m_pg(ap + len, pcontrol);
+            len += resp_vs_ua_m_pg(ap + len, pcontrol);
+            offset += len;
+        } else {
+            resp->asc = INVALID_FIELD_IN_CDB;
+            resp->in_byte = 3;
+            resp->in_bit = 255;
+            goto err_out;
+        }
+        break;
+    case 0x0:       /* Vendor specific "Unit Attention" mode page */
+        /* all sub-page codes ?? */
+        len = resp_vs_ua_m_pg(ap, pcontrol);
+        offset += len;
+        break;      /* vendor is "NVMe    " (from INQUIRY field) */
+    default:
+        bad_pcode = true;
+        break;
+    }
+    if (bad_pcode) {
+        resp->asc = INVALID_FIELD_IN_CDB;
+        resp->in_byte = 2;
+        resp->in_bit = 5;
+        goto err_out;
+    }
+    sg_put_unaligned_be16(offset - 2, arr + 0);
+    len = (alloc_len < offset) ? alloc_len : offset;
+    len = (len < mx_di_len) ? len : mx_di_len;
+    memcpy(dip, arr, len);
+    return len;
+
+err_out:
+    resp->sstatus = SAM_STAT_CHECK_CONDITION;
+    resp->sk = SPC_SK_ILLEGAL_REQUEST;
+    return -1;
+}
+
+#define SDEBUG_MAX_MSELECT_SZ 512
+
+/* Only support MODE SELECT(10). Returns number of bytes used from dop,
+ * else -1 on error with sense code placed in resp. */
+int
+sntl_resp_mode_select10(struct sg_sntl_dev_state_t * dsp,
+                        const uint8_t * cdbp, const uint8_t * dop, int do_len,
+                        struct sg_sntl_result_t * resp)
+{
+    int pf, sp, ps, md_len, bd_len, off, spf, pg_len, rlen, param_len, mpage;
+    int sub_mpage;
+    uint8_t arr[SDEBUG_MAX_MSELECT_SZ];
+
+    memset(resp, 0, sizeof(*resp));
+    memset(arr, 0, sizeof(arr));
+    pf = cdbp[1] & 0x10;
+    sp = cdbp[1] & 0x1;
+    param_len = sg_get_unaligned_be16(cdbp + 7);
+    if ((0 == pf) || sp || (param_len > SDEBUG_MAX_MSELECT_SZ)) {
+        resp->asc = INVALID_FIELD_IN_CDB;
+        resp->in_byte = 1;
+        if (sp)
+            resp->in_bit = 0;
+        else if (0 == pf)
+            resp->in_bit = 4;
+        else {
+            resp->in_byte = 7;
+            resp->in_bit = 255;
+        }
+        goto err_out;
+    }
+    rlen = (do_len < param_len) ? do_len : param_len;
+    memcpy(arr, dop, rlen);
+    md_len = sg_get_unaligned_be16(arr + 0) + 2;
+    bd_len = sg_get_unaligned_be16(arr + 6);
+    if (md_len > 2) {
+        resp->asc = INVALID_FIELD_IN_PARAM_LIST;
+        resp->in_byte = 0;
+        resp->in_bit = 255;
+        goto err_out;
+    }
+    off = bd_len + 8;
+    mpage = arr[off] & 0x3f;
+    ps = !!(arr[off] & 0x80);
+    if (ps) {
+        resp->asc = INVALID_FIELD_IN_PARAM_LIST;
+        resp->in_byte = off;
+        resp->in_bit = 7;
+        goto err_out;
+    }
+    spf = !!(arr[off] & 0x40);
+    pg_len = spf ? (sg_get_unaligned_be16(arr + off + 2) + 4) :
+                   (arr[off + 1] + 2);
+    sub_mpage = spf ? arr[off + 1] : 0;
+    if ((pg_len + off) > param_len) {
+        resp->asc = PARAMETER_LIST_LENGTH_ERR;
+        goto err_out;
+    }
+    switch (mpage) {
+    case 0x8:      /* Caching Mode page */
+        if (0x0 == sub_mpage) {
+            if (caching_m_pg[1] == arr[off + 1]) {
+                memcpy(caching_m_pg + 2, arr + off + 2,
+                       sizeof(caching_m_pg) - 2);
+                dsp->wce = !!(caching_m_pg[2] & 0x4);
+                dsp->wce_changed = true;
+                break;
+            }
+        }
+        goto def_case;
+    case 0xa:      /* Control Mode page */
+        if (0x0 == sub_mpage) {
+            if (ctrl_m_pg[1] == arr[off + 1]) {
+                memcpy(ctrl_m_pg + 2, arr + off + 2,
+                       sizeof(ctrl_m_pg) - 2);
+                dsp->scsi_dsense = !!(ctrl_m_pg[2] & 0x4);
+                break;
+            }
+        }
+        goto def_case;
+    case 0x1c:      /* Informational Exceptions Mode page (SBC) */
+        if (0x0 == sub_mpage) {
+            if (iec_m_pg[1] == arr[off + 1]) {
+                memcpy(iec_m_pg + 2, arr + off + 2,
+                       sizeof(iec_m_pg) - 2);
+                break;
+            }
+        }
+        goto def_case;
+    case 0x0:       /* Vendor specific "Unit Attention" mode page */
+        if (vs_ua_m_pg[1] == arr[off + 1]) {
+            memcpy(vs_ua_m_pg + 2, arr + off + 2,
+                   sizeof(vs_ua_m_pg) - 2);
+            dsp->enclosure_override = vs_ua_m_pg[2];
+        }
+        break;
+    default:
+def_case:
+        resp->asc = INVALID_FIELD_IN_PARAM_LIST;
+        resp->in_byte = off;
+        resp->in_bit = 5;
+        goto err_out;
+    }
+    return rlen;
+
+err_out:
+    resp->sk = SPC_SK_ILLEGAL_REQUEST;
+    resp->sstatus = SAM_STAT_CHECK_CONDITION;
+    return -1;
+}
+
+#endif          /* (HAVE_NVME && (! IGNORE_NVME)) [near line 140] */
diff --git a/lib/sg_pt_dummy.c b/lib/sg_pt_dummy.c
new file mode 100644
index 0000000..e080ff8
--- /dev/null
+++ b/lib/sg_pt_dummy.c
@@ -0,0 +1,442 @@
+/*
+ * Copyright (c) 2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+
+#include "sg_pt.h"
+#include "sg_lib.h"
+#include "sg_pr2serr.h"
+
+/* Version 1.02 20210618 */
+
+/* List of function names with external linkage that need to be defined
+ *
+ *   check_pt_file_handle
+ *   clear_scsi_pt_obj
+ *   construct_scsi_pt_obj
+ *   construct_scsi_pt_obj_with_fd
+ *   destruct_scsi_pt_obj
+ *   do_scsi_pt
+ *   do_nvm_pt
+ *   get_pt_actual_lengths
+ *   get_pt_duration_ns
+ *   get_pt_file_handle
+ *   get_pt_nvme_nsid
+ *   get_pt_req_lengths
+ *   get_pt_result
+ *   get_scsi_pt_cdb_buf
+ *   get_scsi_pt_cdb_len
+ *   get_scsi_pt_duration_ms
+ *   get_scsi_pt_os_err
+ *   get_scsi_pt_os_err_str
+ *   get_scsi_pt_resid
+ *   get_scsi_pt_result_category
+ *   get_scsi_pt_sense_buf
+ *   get_scsi_pt_sense_len
+ *   get_scsi_pt_status_response
+ *   get_scsi_pt_transport_err
+ *   get_scsi_pt_transport_err_str
+ *   partial_clear_scsi_pt_obj
+ *   pt_device_is_nvme
+ *   scsi_pt_close_device
+ *   scsi_pt_open_device
+ *   scsi_pt_open_flags
+ *   set_pt_file_handle
+ *   set_pt_metadata_xfer
+ *   set_scsi_pt_cdb
+ *   set_scsi_pt_data_in
+ *   set_scsi_pt_data_out
+ *   set_scsi_pt_flags
+ *   set_scsi_pt_packet_id
+ *   set_scsi_pt_sense
+ *   set_scsi_pt_tag
+ *   set_scsi_pt_task_attr
+ *   set_scsi_pt_task_management
+ *   set_scsi_pt_transport_err
+ */
+
+/* Simply defines all the functions needed by the pt interface (see sg_pt.h).
+ * They do nothing. This allows decoding of hex files (e.g. with the --in=
+ * or --inhex= option) with utilities like sg_vpd and sg_logs. */
+
+struct sg_pt_dummy {
+    int dummy;
+};
+
+struct sg_pt_base {
+    struct sg_pt_dummy impl;
+};
+
+
+/* Returns >= 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_open_device(const char * device_name, bool read_only, int verbose)
+{
+    int oflags = 0 /* O_NONBLOCK*/ ;
+
+    oflags |= (read_only ? O_RDONLY : O_RDWR);
+    return scsi_pt_open_flags(device_name, oflags, verbose);
+}
+
+/* Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed
+ * together. The 'flags' argument is ignored in OSF-1.
+ * Returns >= 0 if successful, otherwise returns negated errno. */
+int
+scsi_pt_open_flags(const char * device_name, int flags, int verbose)
+{
+    if (device_name) {}
+    if (flags) {}
+    if (verbose) {}
+    errno = EINVAL;
+    return -1;
+}
+
+/* Returns 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_close_device(int device_fd)
+{
+    if (device_fd) {}
+    return 0;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj_with_fd(int device_fd, int verbose)
+{
+    struct sg_pt_dummy * ptp;
+
+    if (device_fd) {}
+    ptp = (struct sg_pt_dummy *)malloc(sizeof(struct sg_pt_dummy));
+    if (ptp) {
+        memset(ptp, 0, sizeof(struct sg_pt_dummy));
+    } else if (verbose)
+        pr2ws("%s: malloc() out of memory\n", __func__);
+    return (struct sg_pt_base *)ptp;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj(void)
+{
+    return construct_scsi_pt_obj_with_fd(-1, 0);
+}
+
+void
+destruct_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_dummy * ptp = &vp->impl;
+
+    if (ptp)
+        free(ptp);
+}
+
+void
+clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_dummy * ptp = &vp->impl;
+
+    if (ptp) {
+        ptp->dummy = 0;
+    }
+}
+
+void
+partial_clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_dummy * ptp = &vp->impl;
+
+    if (NULL == ptp)
+        return;
+    ptp->dummy = 0;
+}
+
+void
+set_scsi_pt_cdb(struct sg_pt_base * vp, const uint8_t * cdb,
+                int cdb_len)
+{
+    if (vp) {}
+    if (cdb) {}
+    if (cdb_len) {}
+}
+
+int
+get_scsi_pt_cdb_len(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return 6;
+}
+
+uint8_t *
+get_scsi_pt_cdb_buf(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return NULL;
+}
+
+void
+set_scsi_pt_sense(struct sg_pt_base * vp, uint8_t * sense,
+                  int max_sense_len)
+{
+    if (vp) {}
+    if (sense) {}
+    if (max_sense_len) {}
+}
+
+/* from device */
+void
+set_scsi_pt_data_in(struct sg_pt_base * vp, uint8_t * dxferp,
+                    int dxfer_len)
+{
+    if (vp) {}
+    if (dxferp) {}
+    if (dxfer_len) {}
+}
+
+/* to device */
+void
+set_scsi_pt_data_out(struct sg_pt_base * vp, const uint8_t * dxferp,
+                     int dxfer_len)
+{
+    if (vp) {}
+    if (dxferp) {}
+    if (dxfer_len) {}
+}
+
+void
+set_scsi_pt_packet_id(struct sg_pt_base * vp, int pack_id)
+{
+    if (vp) {}
+    if (pack_id) {}
+}
+
+void
+set_scsi_pt_tag(struct sg_pt_base * vp, uint64_t tag)
+{
+    if (vp) {}
+    if (tag) {}
+}
+
+void
+set_scsi_pt_task_management(struct sg_pt_base * vp, int tmf_code)
+{
+    if (vp) {}
+    if (tmf_code) {}
+}
+
+void
+set_scsi_pt_task_attr(struct sg_pt_base * vp, int attrib, int priority)
+{
+    if (vp) {}
+    if (attrib) {}
+    if (priority) {}
+}
+
+void
+set_scsi_pt_flags(struct sg_pt_base * vp, int flags)
+{
+    if (vp) {}
+    if (flags) {}
+}
+
+int
+do_scsi_pt(struct sg_pt_base * vp, int device_fd, int time_secs, int verbose)
+{
+    if (vp) {}
+    if (device_fd) {}
+    if (time_secs) {}
+    if (verbose) {}
+    return 0;
+}
+
+int
+get_scsi_pt_result_category(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return 0;
+}
+
+int
+get_scsi_pt_resid(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return 0;
+}
+
+void
+get_pt_req_lengths(const struct sg_pt_base * vp, int * req_dinp,
+                   int * req_doutp)
+{
+    if (vp) {}
+    if (req_dinp) {}
+    if (req_doutp) {}
+}
+
+void
+get_pt_actual_lengths(const struct sg_pt_base * vp, int * act_dinp,
+                      int * act_doutp)
+{
+    if (vp) {}
+    if (act_dinp) {}
+    if (act_doutp) {}
+}
+
+
+int
+get_scsi_pt_status_response(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return 0;
+}
+
+int
+get_scsi_pt_sense_len(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return 0;
+}
+
+uint8_t *
+get_scsi_pt_sense_buf(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return NULL;
+}
+
+int
+get_scsi_pt_duration_ms(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return 0;
+}
+
+/* If not available return 0 otherwise return number of nanoseconds that the
+ * lower layers (and hardware) took to execute the command just completed. */
+uint64_t
+get_pt_duration_ns(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    return 0;
+}
+
+int
+get_scsi_pt_transport_err(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return 0;
+}
+
+int
+get_scsi_pt_os_err(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return 0;
+}
+
+bool
+pt_device_is_nvme(const struct sg_pt_base * vp)
+{
+    if (vp) {}
+    return false;
+}
+
+char *
+get_scsi_pt_transport_err_str(const struct sg_pt_base * vp, int max_b_len,
+                              char * b)
+{
+    if (vp) {}
+    if (max_b_len) {}
+    if (b) {}
+    return NULL;
+}
+
+char *
+get_scsi_pt_os_err_str(const struct sg_pt_base * vp, int max_b_len, char * b)
+{
+    if (vp) {}
+    if (max_b_len) {}
+    if (b) {}
+    return NULL;
+}
+
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int verbose)
+{
+    if (vp) { }
+    if (submq) { }
+    if (timeout_secs) { }
+    if (verbose) { }
+    return SCSI_PT_DO_NOT_SUPPORTED;
+}
+
+int
+check_pt_file_handle(int device_fd, const char * device_name, int vb)
+{
+    if (device_fd) {}
+    if (device_name) {}
+    if (vb) {}
+    return 0;
+}
+
+/* Valid file handles (which is the return value) are >= 0 . Returns -1
+ * if there is no valid file handle. */
+int
+get_pt_file_handle(const struct sg_pt_base * vp)
+{
+    if (vp) { }
+    return -1;
+}
+
+/* If a NVMe block device (which includes the NSID) handle is associated
+ * with 'vp', then its NSID is returned (values range from 0x1 to
+ * 0xffffffe). Otherwise 0 is returned. */
+uint32_t
+get_pt_nvme_nsid(const struct sg_pt_base * vp)
+{
+    if (vp) { }
+    return 0;
+}
+
+uint32_t
+get_pt_result(const struct sg_pt_base * vp)
+{
+    if (vp) { }
+    return 0;
+}
+
+int
+set_pt_file_handle(struct sg_pt_base * vp, int dev_han, int vb)
+{
+    if (vp) { }
+    if (dev_han) { }
+    if (vb) { }
+    return 0;
+}
+
+void
+set_pt_metadata_xfer(struct sg_pt_base * vp, uint8_t * mdxferp,
+                     uint32_t mdxfer_len, bool out_true)
+{
+    if (vp) { }
+    if (mdxferp) { }
+    if (mdxfer_len) { }
+    if (out_true) { }
+}
+
+void
+set_scsi_pt_transport_err(struct sg_pt_base * vp, int err)
+{
+    if (vp) { }
+    if (err) { }
+}
diff --git a/lib/sg_pt_freebsd.c b/lib/sg_pt_freebsd.c
new file mode 100644
index 0000000..e0cd957
--- /dev/null
+++ b/lib/sg_pt_freebsd.c
@@ -0,0 +1,3168 @@
+/*
+ * Copyright (c) 2005-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/* sg_pt_freebsd version 1.48 20220811 */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <limits.h>
+#include <libgen.h>     /* for basename */
+#include <fcntl.h>
+#include <errno.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>   /* from PRIx macros */
+#include <err.h>
+#include <camlib.h>
+#include <cam/scsi/scsi_message.h>
+// #include <sys/ata.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <fcntl.h>
+#include <stddef.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_pt.h"
+#include "sg_lib.h"
+#include "sg_unaligned.h"
+#include "sg_pt_nvme.h"
+#include "sg_pr2serr.h"
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+#include "freebsd_nvme_ioctl.h"
+#else
+#define NVME_CTRLR_PREFIX   "/dev/nvme"
+#define NVME_NS_PREFIX      "ns"
+#endif
+
+#define SG_NVME_NVD_PREFIX  "/dev/nvd"  /* >= FreeBSD 9.2 */
+#define SG_NVME_NDA_PREFIX  "/dev/nda"  /* >= FreeBSD 12.0, CAM compatible */
+
+#define FREEBSD_MAXDEV 64
+#define FREEBSD_FDOFFSET 16;
+
+#if __FreeBSD_version > 500000
+#define CAM_ERROR_PRINT(a, b, c, d, e) cam_error_print(a, b, c, d, e);
+#else
+#define CAM_ERROR_PRINT(a, b, c, d, e)
+#endif
+
+
+struct freebsd_dev_channel {    /* one instance per open file descriptor */
+    bool is_nvme_dev;   /* true if NVMe device attached, else SCSI */
+    bool is_cam_nvme;   /* NVMe via /dev/nda<n> or /dev/pass<n> devices */
+    bool is_pass;       /* CAM passthrough device (i.e. 'pass<n>') */
+    int unitnum;        /* the SCSI unit number, NVMe controller id? */
+    uint32_t nsid;
+    // uint32_t nv_ctrlid;      /* unitnum seems to have this role */
+    int nvme_fd_ns;     // for non-CAM NVMe, use -1 to indicate not provided
+    int nvme_fd_ctrl;   // open("/dev/nvme<n>") if needed */
+    char* devname;      // from cam_get_device() or ioctl(NVME_GET_NSID)
+    struct cam_device* cam_dev;
+    uint8_t * nvme_id_ctlp;
+    uint8_t * free_nvme_id_ctlp;
+    struct sg_sntl_dev_state_t dev_stat;    // owner
+};
+
+// Private table of open devices: guaranteed zero on startup since
+// part of static data.
+static struct freebsd_dev_channel *devicetable[FREEBSD_MAXDEV];
+
+#define DEF_TIMEOUT 60000       /* 60,000 milliseconds (60 seconds) */
+
+struct sg_pt_freebsd_scsi { /* context of one SCSI/NVME command (pt object) */
+    union ccb *ccb;
+    uint8_t * cdb;
+    int cdb_len;
+    uint8_t * sense;
+    int sense_len;
+    uint8_t * dxferp;
+    int dxfer_len;
+    int dxfer_dir;      /* CAM_DIR_NONE, _IN, _OUT and _BOTH */
+    uint8_t * dxferip;
+    uint8_t * dxferop;
+    uint8_t * mdxferp;
+    uint32_t dxfer_ilen;
+    uint32_t dxfer_olen;
+    uint32_t mdxfer_len;
+    uint32_t nvme_result;         // cdw0 from completion
+    uint16_t nvme_status;         // from completion: ((sct << 8) | sc)
+    uint8_t cq_dw0_3[16];
+    int timeout_ms;
+    int scsi_status;
+    int resid;
+    int sense_resid;
+    int in_err;
+    int os_err;
+    int transport_err;
+    int dev_han;                // should be >= FREEBSD_FDOFFSET then
+                                // (dev_han - FREEBSD_FDOFFSET) is the
+                                // index into devicetable[]
+    bool mdxfer_out;
+    bool is_nvme_dev;   /* copied from owning mchanp */
+    bool nvme_our_sntl; /* true: our SNTL; false: received NVMe command */
+    struct freebsd_dev_channel * mchanp;    /* associated device info */
+};
+
+struct sg_pt_base {
+    struct sg_pt_freebsd_scsi impl;
+};
+
+// static const uint32_t broadcast_nsid = SG_NVME_BROADCAST_NSID;
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+static int sg_do_nvme_pt(struct sg_pt_freebsd_scsi * ptp, int fd,
+                         bool is_admin, int timeout_secs, int vb);
+#endif
+
+
+
+static struct freebsd_dev_channel *
+get_fdc_p(struct sg_pt_freebsd_scsi * ptp)
+{
+    int han = ptp->dev_han - FREEBSD_FDOFFSET;
+
+    if ((han < 0) || (han >= FREEBSD_MAXDEV))
+        return NULL;
+    return devicetable[han];
+}
+
+static const struct freebsd_dev_channel *
+get_fdc_cp(const struct sg_pt_freebsd_scsi * ptp)
+{
+    int han = ptp->dev_han - FREEBSD_FDOFFSET;
+
+    if ((han < 0) || (han >= FREEBSD_MAXDEV))
+        return NULL;
+    return devicetable[han];
+}
+
+#if __FreeBSD_version >= 1100000
+/* This works with /dev/nvme*, /dev/nvd* and /dev/nda* but not /dev/pass* */
+static int
+nvme_get_nsid(int fd, uint32_t *nsid, char *b, int blen, int vb)
+{
+    struct nvme_get_nsid gnsid;
+    int n_cdev = sizeof(gnsid.cdev);
+
+    if (ioctl(fd, NVME_GET_NSID, &gnsid) < 0) {
+        int err = errno;
+
+        if (vb > 2)
+            pr2ws("%s: ioctl(NVME_GET_NSID) failed, errno=%d\n", __func__,
+                  err);
+        return -err;
+    }
+    if (n_cdev < blen) {
+        strncpy(b, gnsid.cdev, n_cdev);
+        b[n_cdev] = '\0';
+    } else {
+        strncpy(b, gnsid.cdev, blen);
+        b[blen - 1] = '\0';
+    }
+    if (nsid != NULL)
+        *nsid = gnsid.nsid;
+    return 0;
+}
+#endif
+
+/* Returns >= 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_open_device(const char * device_name, bool read_only, int vb)
+{
+    int oflags = 0 /* O_NONBLOCK*/ ;
+
+    oflags |= (read_only ? O_RDONLY : O_RDWR);
+    return scsi_pt_open_flags(device_name, oflags, vb);
+}
+
+#if __FreeBSD_version >= 1100000
+/* Get a get device CCB for the specified device, borrowed from camdd.c */
+int
+sg_cam_get_cgd(struct cam_device *device, struct ccb_getdev *cgd, int vb)
+{
+    union ccb *ccb;
+    FILE * ferrp = sg_warnings_strm ? sg_warnings_strm : stderr;
+    int retval = 0;
+
+    ccb = cam_getccb(device);
+    if (ccb == NULL) {
+        if (vb)
+            pr2ws("%s: couldn't allocate CCB\n", __func__);
+        return -ENOMEM;
+    }
+    CCB_CLEAR_ALL_EXCEPT_HDR(&ccb->cgd);
+    ccb->ccb_h.func_code = XPT_GDEV_TYPE;
+
+    if (cam_send_ccb(device, ccb) < 0) {
+        if (vb > 1) {
+            pr2ws("%s: error sending Get Device Information CCB\n", __func__);
+            CAM_ERROR_PRINT(device, ccb, CAM_ESF_ALL, CAM_EPF_ALL, ferrp);
+        }
+        retval = -ENODEV;
+        goto bailout;
+    }
+    if ((ccb->ccb_h.status & CAM_STATUS_MASK) != CAM_REQ_CMP) {
+        if (vb > 1)
+            CAM_ERROR_PRINT(device, ccb, CAM_ESF_ALL, CAM_EPF_ALL, ferrp);
+        retval = -ENODEV;
+        goto bailout;
+    }
+    bcopy(&ccb->cgd, cgd, sizeof(struct ccb_getdev));
+bailout:
+    cam_freeccb(ccb);
+    return retval;
+}
+#endif
+
+/* Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed
+ * together. The 'oflags' is only used on NVMe devices. It is ignored on
+ * SCSI and ATA devices in FreeBSD.
+ * Returns >= 0 if successful, otherwise returns negated errno. */
+int
+scsi_pt_open_flags(const char * device_name, int oflags, int vb)
+{
+    bool maybe_non_cam_nvme = false;
+    bool basnam0_n = false;
+    char first_ch;
+    int k, err, dev_fd, ret, handle_idx;
+    ssize_t s;
+    struct freebsd_dev_channel *fdc_p = NULL;
+    struct cam_device* cam_dev;
+    struct stat a_stat;
+    char dev_nm[PATH_MAX];
+
+    if (vb > 6)
+        pr2ws("%s: device_name=%s, oflags=0x%x\n", __func__, device_name,
+              oflags);
+    // Search table for a free entry
+    for (k = 0; k < FREEBSD_MAXDEV; k++)
+        if (! devicetable[k])
+            break;
+
+    // If no free entry found, return error.  We have max allowed number
+    // of "file descriptors" already allocated.
+    if (k == FREEBSD_MAXDEV) {
+        if (vb)
+            pr2ws("too many open file descriptors (%d)\n", FREEBSD_MAXDEV);
+        ret = -EMFILE;
+        goto err_out;
+    }
+    handle_idx = k;
+    fdc_p = (struct freebsd_dev_channel *)
+                calloc(1,sizeof(struct freebsd_dev_channel));
+    if (fdc_p == NULL) {
+        // errno already set by call to calloc()
+        ret = -ENOMEM;
+        goto err_out;
+    }
+    fdc_p->nvme_fd_ns = -1;
+    fdc_p->nvme_fd_ctrl = -1;
+    if (! (fdc_p->devname = (char *)calloc(1, DEV_IDLEN+1))) {
+         ret = -ENOMEM;
+         goto err_out;
+    }
+    /* Don't know yet whether device_name is a SCSI, NVME(non-CAM) or
+     * NVME(CAM) device. Start by assuming it is CAM. */
+    if (cam_get_device(device_name, fdc_p->devname, DEV_IDLEN,
+                       &(fdc_p->unitnum)) == -1) {
+        if (vb > 3)
+            pr2ws("%s: cam_get_device(%s) fails, should work for SCSI and "
+                  "NVMe devices\n", __func__, device_name, errno);
+        ret = -EINVAL;
+        goto err_out;
+    } else if (vb > 6)
+        pr2ws("%s: cam_get_device() works, devname=%s unit=%u\n", __func__,
+             fdc_p->devname, fdc_p->unitnum);
+
+    if (! (cam_dev = cam_open_spec_device(fdc_p->devname,
+                                          fdc_p->unitnum, O_RDWR, NULL))) {
+        if (vb > 6) {
+            pr2ws("cam_open_spec_device: %s\n", cam_errbuf);
+            pr2ws("%s: so not CAM, but still maybe NVME\n", __func__);
+        }
+        maybe_non_cam_nvme = true;
+    } else {    /* found CAM, could be SCSI or NVME(CAM) [nda driver] */
+#if __FreeBSD_version >= 1100000
+        struct ccb_getdev cgd;
+
+        fdc_p->cam_dev = cam_dev;
+        ret = sg_cam_get_cgd(cam_dev, &cgd, vb);
+        if (ret)
+            goto err_out;
+        switch (cgd.protocol) {
+        case PROTO_SCSI:
+            fdc_p->is_nvme_dev = false;
+            break;
+        case PROTO_NVME:
+            fdc_p->is_nvme_dev = true;
+            fdc_p->is_cam_nvme = true;
+            fdc_p->nsid = cam_dev->target_lun & UINT32_MAX;
+            break;
+        case PROTO_ATA:
+        case PROTO_ATAPI:
+        case PROTO_SATAPM:
+        case PROTO_SEMB:  /* SATA Enclosure Management bridge */
+            if (vb) {
+                pr2ws("%s: ATA and derivative devices not supported\n",
+                      __func__);
+                if (vb > 2)
+                    pr2ws("  ... FreeBSD doesn't have a SAT in its kernel\n");
+            }
+            ret = -EINVAL;
+            break;
+#if __FreeBSD_version > 1200058
+        case PROTO_MMCSD:
+            if (vb)
+                pr2ws("%s: MMC and SD devices not supported\n",
+                      __func__);
+            ret = -EINVAL;
+            break;
+#endif
+        default:
+            if (vb)
+                pr2ws("%s: unexpected device protocol\n", __func__);
+            ret = -EINVAL;
+            break;
+        }
+        if (ret)
+            goto err_out;
+        if (0 == memcpy(fdc_p->devname, "pass", 4))
+            fdc_p->is_pass = true;
+#else
+        ret = 0;
+        fdc_p->is_nvme_dev = false;
+#endif
+    }
+    if (maybe_non_cam_nvme) {
+        first_ch = device_name[0];
+        if (('/' != first_ch) && ('.' != first_ch)) {
+            char b[PATH_MAX];
+
+            /* Step 1: if device_name is symlink, follow it */
+            s = readlink(device_name,  b, sizeof(b));
+            if (s <= 0) {
+                strncpy(b, device_name, PATH_MAX - 1);
+                b[PATH_MAX - 1] = '\0';
+            }
+            /* Step 2: if no leading '/' nor '.' given, prepend '/dev/' */
+            first_ch = b[0];
+            basnam0_n = ('n' == first_ch);
+            if (('/' != first_ch) && ('.' != first_ch))
+                snprintf(dev_nm, PATH_MAX, "%s%s", "/dev/", b);
+            else
+                strcpy(dev_nm, b);
+        } else {
+            const char * cp;
+
+            strcpy(dev_nm, device_name);
+            cp = basename(dev_nm);
+            basnam0_n = ('n' == *cp);
+            strcpy(dev_nm, device_name);
+        }
+        if (stat(dev_nm, &a_stat) < 0) {
+            err = errno;
+            if (vb)
+                pr2ws("%s: unable to stat(%s): %s; basnam0_n=%d\n",
+                      __func__, dev_nm, strerror(err), basnam0_n);
+            ret = -err;
+            goto err_out;
+        }
+        if (! (S_ISCHR(a_stat.st_mode))) {
+            if (vb > 1)
+                pr2ws("%s: %s is not a char device ??\n", __func__, dev_nm);
+            ret = -ENODEV;
+            goto err_out;
+        }
+        dev_fd = open(dev_nm, oflags);
+        if (dev_fd < 0) {
+            err = errno;
+            if (vb > 1)
+                pr2ws("%s: open(%s) failed: %s (errno=%d), try SCSI/ATA\n",
+                      __func__, dev_nm, strerror(err), err);
+            ret = -err;
+            goto err_out;
+        }
+#if __FreeBSD_version >= 1100000
+        ret = nvme_get_nsid(dev_fd, &fdc_p->nsid, fdc_p->devname, DEV_IDLEN,
+                            vb);
+        if (ret)
+            goto err_out;
+#else
+        {
+            unsigned int u;
+
+            /* only support /dev/nvme<n> and /dev/nvme<n>ns<m> */
+            k = sscanf(dev_nm, "nvme%uns%u", &u, &fdc_p->nsid);
+            if (2 == k) {
+                char * cp = strchr(dev_nm, 's');
+
+                *(cp - 2) = '\0';
+                strcpy(fdc_p->devname, dev_nm);
+            } else if (1 == k) {
+                strncpy(fdc_p->devname, dev_nm, DEV_IDLEN);
+                fdc_p->nsid = 0;
+            } else if (vb > 1) {
+                pr2ws("%s: only support '[/dev/]nvme<n>[ns<m>]'\n", __func__);
+                goto err_out;
+            }
+        }
+#endif
+        if (vb > 6)
+            pr2ws("%s: nvme_dev_nm: %s, nsid=%u\n", __func__, fdc_p->devname,
+                  fdc_p->nsid);
+        fdc_p->is_nvme_dev = true;
+        fdc_p->is_cam_nvme = false;
+        if (fdc_p->nsid > 0)
+            fdc_p->nvme_fd_ns = dev_fd;
+        else
+            fdc_p->nvme_fd_ctrl = dev_fd;
+    }
+    // return pointer to "file descriptor" table entry, properly offset.
+    devicetable[handle_idx] = fdc_p;
+    return handle_idx + FREEBSD_FDOFFSET;
+
+err_out:                /* ret should be negative value (negated errno) */
+    if (fdc_p) {
+        if (fdc_p->devname)
+            free(fdc_p->devname);
+        if (fdc_p->nvme_fd_ns >= 0)
+            close(fdc_p->nvme_fd_ns);
+        if (fdc_p->nvme_fd_ctrl >= 0)
+            close(fdc_p->nvme_fd_ctrl);
+        free(fdc_p);
+        fdc_p = NULL;
+    }
+    return ret;
+}
+
+/* Returns 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_close_device(int device_han)
+{
+    struct freebsd_dev_channel *fdc_p;
+    int han = device_han - FREEBSD_FDOFFSET;
+
+    if ((han < 0) || (han >= FREEBSD_MAXDEV)) {
+        errno = ENODEV;
+        return -errno;
+    }
+    fdc_p = devicetable[han];
+    if (NULL == fdc_p) {
+        errno = ENODEV;
+        return -errno;
+    }
+    if (fdc_p->devname)
+        free(fdc_p->devname);
+    if (fdc_p->cam_dev)         /* N.B. can be cam_nvme devices */
+        cam_close_device(fdc_p->cam_dev);
+    else if (fdc_p->is_nvme_dev) {
+        if (fdc_p->nvme_fd_ns >= 0)
+            close(fdc_p->nvme_fd_ns);
+        if (fdc_p->nvme_fd_ctrl >= 0)
+            close(fdc_p->nvme_fd_ctrl);
+        if (fdc_p->free_nvme_id_ctlp) {
+            free(fdc_p->free_nvme_id_ctlp);
+            fdc_p->nvme_id_ctlp = NULL;
+            fdc_p->free_nvme_id_ctlp = NULL;
+        }
+    }
+    free(fdc_p);
+    devicetable[han] = NULL;
+    errno = 0;
+    return 0;
+}
+
+/* Assumes device_han is an "open" file handle associated with some device.
+ * Returns 1 if SCSI generic pass-though device [SCSI CAM primary: nda0],
+ * returns 2 if secondary * SCSI pass-through device [SCSI CAM: pass<n>];
+ * returns 3 if non-CAM NVMe with no nsid [nvme0]; returns 4 if non-CAM
+ * NVMe device with nsid (> 0) [nvme0ns1, nvd0]; returns 5 if CAM NVMe
+ * (with or without nsid) [nda0]; or returns 0 if something else (e.g. ATA
+ * block device) or device_han < 0.
+ * If error, returns negated errno (operating system) value. */
+int
+check_pt_file_handle(int device_han, const char * device_name, int vb)
+{
+    struct freebsd_dev_channel *fdc_p;
+    int han = device_han - FREEBSD_FDOFFSET;
+
+    if (vb > 6)
+        pr2ws("%s: device_handle=%d, device_name: %s\n", __func__,
+              device_han, device_name);
+    if ((han < 0) || (han >= FREEBSD_MAXDEV))
+        return -ENODEV;
+    fdc_p = devicetable[han];
+    if (NULL == fdc_p)
+        return -ENODEV;
+    if (fdc_p->is_nvme_dev) {
+        if (fdc_p->is_cam_nvme)
+            return 5;
+        else if (fdc_p->nsid == 0)
+            return 3;
+        else
+            return 4;       /* Something like nvme0ns1 or nvd0 */
+    } else if (fdc_p->cam_dev)
+        return fdc_p->is_pass ? 2 : 1;
+    else {
+        if (vb > 1)
+            pr2ws("%s: neither SCSI nor NVMe ... hmm, device name: %s\n",
+                  __func__, device_name);
+        return 0;
+    }
+}
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+static bool checked_ev_dsense = false;
+static bool ev_dsense = false;
+#endif
+
+struct sg_pt_base *
+construct_scsi_pt_obj_with_fd(int dev_han, int vb)
+{
+    struct sg_pt_freebsd_scsi * ptp;
+
+    ptp = (struct sg_pt_freebsd_scsi *)
+                calloc(1, sizeof(struct sg_pt_freebsd_scsi));
+    if (ptp) {
+        ptp->dxfer_dir = CAM_DIR_NONE;
+        ptp->dev_han = (dev_han < 0) ? -1 : dev_han;
+        if (ptp->dev_han >= 0) {
+            struct freebsd_dev_channel *fdc_p;
+
+            fdc_p = get_fdc_p(ptp);
+            if (fdc_p) {
+                ptp->mchanp = fdc_p;
+#if (HAVE_NVME && (! IGNORE_NVME))
+                sntl_init_dev_stat(&fdc_p->dev_stat);
+                if (! checked_ev_dsense) {
+                    ev_dsense = sg_get_initial_dsense();
+                    checked_ev_dsense = true;
+                }
+                fdc_p->dev_stat.scsi_dsense = ev_dsense;
+#endif
+            } else if (vb)
+                pr2ws("%s: bad dev_han=%d\n", __func__, dev_han);
+        }
+    } else if (vb)
+        pr2ws("%s: calloc() out of memory\n", __func__);
+    return (struct sg_pt_base *)ptp;
+}
+
+
+struct sg_pt_base *
+construct_scsi_pt_obj()
+{
+    return construct_scsi_pt_obj_with_fd(-1, 0);
+}
+
+void
+destruct_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_freebsd_scsi * ptp;
+
+    if (NULL == vp) {
+        pr2ws(">>>> %s: given NULL pointer\n", __func__);
+        return;
+    }
+    if ((ptp = &vp->impl)) {
+        if (ptp->ccb)
+            cam_freeccb(ptp->ccb);
+        free(vp);
+    }
+}
+
+void
+clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_freebsd_scsi * ptp;
+
+    if (NULL == vp) {
+        pr2ws(">>>>> %s: NULL pointer given\n", __func__);
+        return;
+    }
+    if ((ptp = &vp->impl)) {
+        int dev_han = ptp->dev_han;
+        struct freebsd_dev_channel *fdc_p = ptp->mchanp;
+
+        if (ptp->ccb)
+            cam_freeccb(ptp->ccb);
+        memset(ptp, 0, sizeof(struct sg_pt_freebsd_scsi));
+        ptp->dxfer_dir = CAM_DIR_NONE;
+        ptp->dev_han = dev_han;
+        ptp->mchanp = fdc_p;
+    }
+}
+
+void
+partial_clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (NULL == ptp)
+        return;
+    ptp->in_err = 0;
+    ptp->os_err = 0;
+    ptp->transport_err = 0;
+    ptp->scsi_status = 0;
+    ptp->dxfer_dir = CAM_DIR_NONE;
+    ptp->dxferip = NULL;
+    ptp->dxfer_ilen = 0;
+    ptp->dxferop = NULL;
+    ptp->dxfer_olen = 0;
+    ptp->nvme_result = 0;
+}
+
+/* Forget any previous dev_han and install the one given. May attempt to
+ * find file type (e.g. if pass-though) from OS so there could be an error.
+ * Returns 0 for success or the same value as get_scsi_pt_os_err()
+ * will return. dev_han should be >= 0 for a valid file handle or -1 . */
+int
+set_pt_file_handle(struct sg_pt_base * vp, int dev_han, int vb)
+{
+    struct sg_pt_freebsd_scsi * ptp;
+
+    if (NULL == vp) {
+        if (vb)
+            pr2ws(">>>> %s: pointer to object is NULL\n", __func__);
+        return EINVAL;
+    }
+    if ((ptp = &vp->impl)) {
+        struct freebsd_dev_channel *fdc_p;
+
+        if (dev_han < 0) {
+            ptp->dev_han = -1;
+            ptp->dxfer_dir = CAM_DIR_NONE;
+            return 0;
+        }
+        fdc_p = get_fdc_p(ptp);
+        if (NULL == fdc_p) {
+            if (vb)
+                pr2ws("%s: dev_han (%d) is invalid\n", __func__, dev_han);
+            ptp->os_err = EINVAL;
+            return ptp->os_err;
+        }
+        ptp->os_err = 0;
+        ptp->transport_err = 0;
+        ptp->in_err = 0;
+        ptp->scsi_status = 0;
+        ptp->dev_han = dev_han;
+        ptp->dxfer_dir = CAM_DIR_NONE;
+        ptp->mchanp = fdc_p;
+    }
+    return 0;
+}
+
+/* Valid file handles (which is the return value) are >= 0 . Returns -1
+ * if there is no valid file handle. */
+int
+get_pt_file_handle(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    return ptp ? ptp->dev_han : -1;
+}
+
+void
+set_scsi_pt_cdb(struct sg_pt_base * vp, const uint8_t * cdb, int cdb_len)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    ptp->cdb = (uint8_t *)cdb;
+    ptp->cdb_len = cdb_len;
+}
+
+int
+get_scsi_pt_cdb_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    return ptp->cdb_len;
+}
+
+uint8_t *
+get_scsi_pt_cdb_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    return ptp->cdb;
+}
+
+void
+set_scsi_pt_sense(struct sg_pt_base * vp, uint8_t * sense,
+                  int max_sense_len)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (sense) {
+        if (max_sense_len > 0)
+            memset(sense, 0, max_sense_len);
+    }
+    ptp->sense = sense;
+    ptp->sense_len = max_sense_len;
+}
+
+/* Setup for data transfer from device */
+void
+set_scsi_pt_data_in(struct sg_pt_base * vp, uint8_t * dxferp,
+                    int dxfer_len)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp->dxferip)
+        ++ptp->in_err;
+    ptp->dxferip = dxferp;
+    ptp->dxfer_ilen = dxfer_len;
+    if (dxfer_len > 0) {
+        ptp->dxferp = dxferp;
+        ptp->dxfer_len = dxfer_len;
+        if (ptp->dxfer_dir == CAM_DIR_OUT)
+            ptp->dxfer_dir = CAM_DIR_BOTH;
+        else
+            ptp->dxfer_dir = CAM_DIR_IN;
+    }
+}
+
+/* Setup for data transfer toward device */
+void
+set_scsi_pt_data_out(struct sg_pt_base * vp, const uint8_t * dxferp,
+                     int dxfer_len)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp->dxferop)
+        ++ptp->in_err;
+    ptp->dxferop = (uint8_t *)dxferp;
+    ptp->dxfer_olen = dxfer_len;
+    if (dxfer_len > 0) {
+        ptp->dxferp = (uint8_t *)dxferp;
+        ptp->dxfer_len = dxfer_len;
+        if (ptp->dxfer_dir == CAM_DIR_IN)
+            ptp->dxfer_dir = CAM_DIR_BOTH;
+        else
+            ptp->dxfer_dir = CAM_DIR_OUT;
+    }
+}
+
+void
+set_pt_metadata_xfer(struct sg_pt_base * vp, uint8_t * mdxferp,
+                     uint32_t mdxfer_len, bool out_true)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp->mdxferp)
+        ++ptp->in_err;
+    ptp->mdxferp = mdxferp;
+    ptp->mdxfer_len = mdxfer_len;
+    if (mdxfer_len > 0)
+        ptp->mdxfer_out = out_true;
+}
+
+void
+set_scsi_pt_packet_id(struct sg_pt_base * vp __attribute__ ((unused)),
+                      int pack_id __attribute__ ((unused)))
+{
+}
+
+void
+set_scsi_pt_tag(struct sg_pt_base * vp, uint64_t tag __attribute__ ((unused)))
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_task_management(struct sg_pt_base * vp,
+                            int tmf_code __attribute__ ((unused)))
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_task_attr(struct sg_pt_base * vp,
+                      int attrib __attribute__ ((unused)),
+                      int priority __attribute__ ((unused)))
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_flags(struct sg_pt_base * objp, int flags)
+{
+    if (objp) { ; }      /* unused, suppress warning */
+    if (flags) { ; }     /* unused, suppress warning */
+}
+
+/* Executes SCSI command (or at least forwards it to lower layers).
+ * Clears os_err field prior to active call (whose result may set it
+ * again). */
+int
+do_scsi_pt(struct sg_pt_base * vp, int dev_han, int time_secs, int vb)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+    struct freebsd_dev_channel *fdc_p;
+    FILE * ferrp = sg_warnings_strm ? sg_warnings_strm : stderr;
+    union ccb *ccb;
+
+    if (vb > 6)
+        pr2ws("%s: dev_han=%d, time_secs=%d\n", __func__, dev_han, time_secs);
+    ptp->os_err = 0;
+    if (ptp->in_err) {
+        if (vb)
+            pr2ws("Replicated or unused set_scsi_pt...\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (dev_han < 0) {
+        if (ptp->dev_han < 0) {
+            if (vb)
+                pr2ws("%s: No device file handle given\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        dev_han = ptp->dev_han;
+    } else {
+        if (ptp->dev_han >= 0) {
+            if (dev_han != ptp->dev_han) {
+                if (vb)
+                    pr2ws("%s: file handle given to create and this "
+                          "differ\n", __func__);
+                return SCSI_PT_DO_BAD_PARAMS;
+            }
+        } else
+            ptp->dev_han = dev_han;
+    }
+
+    if (NULL == ptp->cdb) {
+        if (vb)
+            pr2ws("No command (cdb) given\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+
+    fdc_p = ptp->mchanp;
+    if (NULL == fdc_p) {
+        fdc_p = get_fdc_p(ptp);
+        if (NULL == fdc_p) {
+            if (vb)
+                pr2ws("File descriptor bad or closed??\n");
+            ptp->os_err = ENODEV;
+            return -ptp->os_err;
+        }
+        ptp->mchanp = fdc_p;
+    }
+#if (HAVE_NVME && (! IGNORE_NVME))
+    if (fdc_p->is_nvme_dev)
+        return sg_do_nvme_pt(ptp, -1, true /* assume Admin */, time_secs, vb);
+#endif
+
+    /* SCSI CAM pass-through follows */
+    ptp->is_nvme_dev = fdc_p->is_nvme_dev;
+    if (NULL == fdc_p->cam_dev) {
+        if (vb)
+            pr2ws("No open CAM device\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+
+    if (NULL == ptp->ccb) {     /* re-use if we have one already */
+        if (! (ccb = cam_getccb(fdc_p->cam_dev))) {
+            if (vb)
+                pr2ws("cam_getccb: failed\n");
+            ptp->os_err = ENOMEM;
+            return -ptp->os_err;
+        }
+        ptp->ccb = ccb;
+    } else
+        ccb = ptp->ccb;
+
+    // clear out structure, except for header that was filled in for us
+    bzero(&(&ccb->ccb_h)[1],
+            sizeof(struct ccb_scsiio) - sizeof(struct ccb_hdr));
+
+    ptp->timeout_ms = (time_secs > 0) ? (time_secs * 1000) : DEF_TIMEOUT;
+    cam_fill_csio(&ccb->csio,
+                  /* retries */ 1,
+                  /* cbfcnp */ NULL,
+                  /* flags */ ptp->dxfer_dir,
+                  /* tagaction */ MSG_SIMPLE_Q_TAG,
+                  /* dataptr */ ptp->dxferp,
+                  /* datalen */ ptp->dxfer_len,
+                  /* senselen */ ptp->sense_len,
+                  /* cdblen */ ptp->cdb_len,
+                  /* timeout (millisecs) */ ptp->timeout_ms);
+    memcpy(ccb->csio.cdb_io.cdb_bytes, ptp->cdb, ptp->cdb_len);
+
+    if (cam_send_ccb(fdc_p->cam_dev, ccb) < 0) {
+        if (vb) {
+            pr2serr("%s: cam_send_ccb() error\n", __func__);
+            CAM_ERROR_PRINT(fdc_p->cam_dev, ccb, CAM_ESF_ALL,
+                            CAM_EPF_ALL, ferrp);
+        }
+        cam_freeccb(ptp->ccb);
+        ptp->ccb = NULL;
+        ptp->os_err = EIO;
+        return -ptp->os_err;
+    }
+
+    if (((ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) ||
+        ((ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_SCSI_STATUS_ERROR)) {
+        ptp->scsi_status = ccb->csio.scsi_status;
+        ptp->resid = ccb->csio.resid;
+        ptp->sense_resid = ccb->csio.sense_resid;
+
+        if ((SAM_STAT_CHECK_CONDITION == ptp->scsi_status) ||
+            (SAM_STAT_COMMAND_TERMINATED == ptp->scsi_status)) {
+            int len;
+
+            if (ptp->sense_resid > ptp->sense_len)
+                len = ptp->sense_len;   /* crazy; ignore sense_resid */
+            else
+                len = ptp->sense_len - ptp->sense_resid;
+            if (len > 0)
+                memcpy(ptp->sense, &(ccb->csio.sense_data), len);
+        }
+    } else
+        ptp->transport_err = 1;
+
+    return 0;
+}
+
+int
+get_scsi_pt_result_category(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp->os_err)
+        return SCSI_PT_RESULT_OS_ERR;
+    else if (ptp->transport_err)
+        return SCSI_PT_RESULT_TRANSPORT_ERR;
+    else if ((SAM_STAT_CHECK_CONDITION == ptp->scsi_status) ||
+             (SAM_STAT_COMMAND_TERMINATED == ptp->scsi_status))
+        return SCSI_PT_RESULT_SENSE;
+    else if (ptp->scsi_status)
+        return SCSI_PT_RESULT_STATUS;
+    else
+        return SCSI_PT_RESULT_GOOD;
+}
+
+int
+get_scsi_pt_resid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if ((NULL == ptp) || (NULL == ptp->mchanp))
+        return 0;
+    return ((ptp->is_nvme_dev && ! ptp->nvme_our_sntl)) ?  0 : ptp->resid;
+}
+
+void
+get_pt_req_lengths(const struct sg_pt_base * vp, int * req_dinp,
+                   int * req_doutp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+    bool bidi = (ptp->dxfer_dir == CAM_DIR_BOTH);
+
+    if (req_dinp) {
+        if (ptp->dxfer_ilen > 0)
+            *req_dinp = ptp->dxfer_ilen;
+        else
+            *req_dinp = 0;
+    }
+    if (req_doutp) {
+        if ((!bidi) && (ptp->dxfer_olen > 0))
+            *req_doutp = ptp->dxfer_olen;
+        else
+            *req_doutp = 0;
+    }
+}
+
+void
+get_pt_actual_lengths(const struct sg_pt_base * vp, int * act_dinp,
+                      int * act_doutp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+    bool bidi = (ptp->dxfer_dir == CAM_DIR_BOTH);
+
+    if (act_dinp) {
+        if (ptp->dxfer_ilen > 0)
+            *act_dinp = ptp->dxfer_ilen - ptp->resid;
+        else
+            *act_dinp = 0;
+    }
+    if (act_doutp) {
+        if ((!bidi) && (ptp->dxfer_olen > 0))
+            *act_doutp = ptp->dxfer_olen - ptp->resid;
+        else
+            *act_doutp = 0;
+    }
+}
+
+/* Returns SCSI status value (from device that received the command). If an
+ * NVMe command was issued directly (i.e. through do_scsi_pt() then return
+ * NVMe status (i.e. ((SCT << 8) | SC)). If problem returns -1. */
+int
+get_scsi_pt_status_response(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp) {
+        const struct freebsd_dev_channel * fdc_p = ptp->mchanp;
+
+        if (NULL == fdc_p)
+            return -1;
+        if (ptp->is_nvme_dev && ! ptp->nvme_our_sntl)
+            return (int)ptp->nvme_status;
+        else
+            return ptp->scsi_status;
+    }
+    return -1;
+}
+
+/* For NVMe command: CDW0 from completion (32 bits); for SCSI: the status */
+uint32_t
+get_pt_result(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp) {
+        const struct freebsd_dev_channel * fdc_p = ptp->mchanp;
+
+        if (NULL == fdc_p)
+            return -1;
+        if (ptp->is_nvme_dev && ! ptp->nvme_our_sntl)
+            return ptp->nvme_result;
+        else
+            return (uint32_t)ptp->scsi_status;
+    }
+    return 0xffffffff;
+}
+
+int
+get_scsi_pt_sense_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp->sense_resid > ptp->sense_len)
+        return ptp->sense_len;  /* strange; ignore ptp->sense_resid */
+    else
+        return ptp->sense_len - ptp->sense_resid;
+}
+
+uint8_t *
+get_scsi_pt_sense_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    return ptp->sense;
+}
+
+/* Not implemented so return -1 . */
+int
+get_scsi_pt_duration_ms(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    // const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    return -1;
+}
+
+/* If not available return 0 otherwise return number of nanoseconds that the
+ * lower layers (and hardware) took to execute the command just completed. */
+uint64_t
+get_pt_duration_ns(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    return 0;
+}
+
+int
+get_scsi_pt_transport_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    return ptp->transport_err;
+}
+
+void
+set_scsi_pt_transport_err(struct sg_pt_base * vp, int err)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    ptp->transport_err = err;
+}
+
+int
+get_scsi_pt_os_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    return ptp->os_err;
+}
+
+char *
+get_scsi_pt_transport_err_str(const struct sg_pt_base * vp, int max_b_len,
+                              char * b)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (0 == ptp->transport_err) {
+        strncpy(b, "no transport error available", max_b_len);
+        b[max_b_len - 1] = '\0';
+        return b;
+    }
+    if (ptp->mchanp && ptp->mchanp->is_nvme_dev) {
+        snprintf(b, max_b_len, "NVMe has no transport errors at present "
+                 "but tranport_err=%d ??\n", ptp->transport_err);
+        return b;
+    }
+#if __FreeBSD_version > 500000
+    if (ptp->mchanp && ptp->mchanp->cam_dev)
+        cam_error_string(ptp->mchanp->cam_dev, ptp->ccb, b, max_b_len,
+                         CAM_ESF_ALL, CAM_EPF_ALL);
+    else {
+        strncpy(b, "no transport error available", max_b_len);
+        b[max_b_len - 1] = '\0';
+   }
+#else
+    strncpy(b, "no transport error available", max_b_len);
+    b[max_b_len - 1] = '\0';
+#endif
+    return b;
+}
+
+bool
+pt_device_is_nvme(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp && (ptp->dev_han >= 0)) {
+        const struct freebsd_dev_channel *fdc_p;
+
+        fdc_p = get_fdc_cp(ptp);
+        if (NULL == fdc_p) {
+            pr2ws("%s: unable to find fdc_p\n", __func__);
+            errno = ENODEV;
+            return false;
+        }
+        return fdc_p->is_nvme_dev;
+    }
+    return false;
+}
+
+/* If a NVMe block device (which includes the NSID) handle is associated
+ * with 'objp', then its NSID is returned (values range from 0x1 to
+ * 0xffffffe). Otherwise 0 is returned. */
+uint32_t
+get_pt_nvme_nsid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+
+    if (ptp && (ptp->dev_han >= 0)) {
+        const struct freebsd_dev_channel *fdc_p;
+
+        fdc_p = get_fdc_cp(ptp);
+        if (NULL == fdc_p)
+            return 0;
+        return fdc_p->nsid;
+    }
+    return 0;
+}
+
+char *
+get_scsi_pt_os_err_str(const struct sg_pt_base * vp, int max_b_len, char * b)
+{
+    const struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+    const char * cp;
+
+    cp = safe_strerror(ptp->os_err);
+    strncpy(b, cp, max_b_len);
+    if ((int)strlen(cp) >= max_b_len)
+        b[max_b_len - 1] = '\0';
+    return b;
+}
+
+
+#define SCSI_INQUIRY_OPC     0x12
+#define SCSI_MAINT_IN_OPC  0xa3
+#define SCSI_MODE_SENSE10_OPC  0x5a
+#define SCSI_MODE_SELECT10_OPC  0x55
+#define SCSI_READ10_OPC 0x28
+#define SCSI_READ16_OPC 0x88
+#define SCSI_READ_CAPACITY10_OPC  0x25
+#define SCSI_START_STOP_OPC 0x1b
+#define SCSI_SYNC_CACHE10_OPC  0x35
+#define SCSI_SYNC_CACHE16_OPC  0x91
+#define SCSI_VERIFY10_OPC 0x2f
+#define SCSI_VERIFY16_OPC 0x8f
+#define SCSI_WRITE10_OPC 0x2a
+#define SCSI_WRITE16_OPC 0x8a
+#define SCSI_WRITE_SAME10_OPC 0x41
+#define SCSI_WRITE_SAME16_OPC 0x93
+#define SCSI_RECEIVE_DIAGNOSTIC_OPC  0x1c
+#define SCSI_REP_SUP_OPCS_OPC  0xc
+#define SCSI_REP_SUP_TMFS_OPC  0xd
+#define SCSI_REPORT_LUNS_OPC 0xa0
+#define SCSI_REQUEST_SENSE_OPC  0x3
+#define SCSI_SEND_DIAGNOSTIC_OPC  0x1d
+#define SCSI_TEST_UNIT_READY_OPC  0x0
+#define SCSI_SERVICE_ACT_IN_OPC  0x9e
+#define SCSI_READ_CAPACITY16_SA  0x10
+#define SCSI_SA_MSK  0x1f
+
+/* Additional Sense Code (ASC) */
+#define NO_ADDITIONAL_SENSE 0x0
+#define LOGICAL_UNIT_NOT_READY 0x4
+#define LOGICAL_UNIT_COMMUNICATION_FAILURE 0x8
+#define UNRECOVERED_READ_ERR 0x11
+#define PARAMETER_LIST_LENGTH_ERR 0x1a
+#define INVALID_OPCODE 0x20
+#define LBA_OUT_OF_RANGE 0x21
+#define INVALID_FIELD_IN_CDB 0x24
+#define INVALID_FIELD_IN_PARAM_LIST 0x26
+#define UA_RESET_ASC 0x29
+#define UA_CHANGED_ASC 0x2a
+#define TARGET_CHANGED_ASC 0x3f
+#define LUNS_CHANGED_ASCQ 0x0e
+#define INSUFF_RES_ASC 0x55
+#define INSUFF_RES_ASCQ 0x3
+#define LOW_POWER_COND_ON_ASC  0x5e     /* ASCQ=0 */
+#define POWER_ON_RESET_ASCQ 0x0
+#define BUS_RESET_ASCQ 0x2      /* scsi bus reset occurred */
+#define MODE_CHANGED_ASCQ 0x1   /* mode parameters changed */
+#define CAPACITY_CHANGED_ASCQ 0x9
+#define SAVING_PARAMS_UNSUP 0x39
+#define TRANSPORT_PROBLEM 0x4b
+#define THRESHOLD_EXCEEDED 0x5d
+#define LOW_POWER_COND_ON 0x5e
+#define MISCOMPARE_VERIFY_ASC 0x1d
+#define MICROCODE_CHANGED_ASCQ 0x1      /* with TARGET_CHANGED_ASC */
+#define MICROCODE_CHANGED_WO_RESET_ASCQ 0x16
+#define PCIE_ERR_ASC 0x4b
+#define PCIE_UNSUPP_REQ_ASCQ 0x13
+
+/* NVMe Admin commands */
+#define SG_NVME_AD_GET_FEATURE 0xa
+#define SG_NVME_AD_SET_FEATURE 0x9
+#define SG_NVME_AD_IDENTIFY 0x6         /* similar to SCSI INQUIRY */
+#define SG_NVME_AD_DEV_SELT_TEST 0x14
+#define SG_NVME_AD_MI_RECEIVE 0x1e      /* MI: Management Interface */
+#define SG_NVME_AD_MI_SEND 0x1d         /* hmmm, same opcode as SEND DIAG */
+
+/* NVMe NVM (Non-Volatile Memory) commands */
+#define SG_NVME_NVM_FLUSH 0x0           /* SCSI SYNCHRONIZE CACHE */
+#define SG_NVME_NVM_COMPARE 0x5         /* SCSI VERIFY(BYTCHK=1) */
+#define SG_NVME_NVM_READ 0x2
+#define SG_NVME_NVM_VERIFY 0xc          /* SCSI VERIFY(BYTCHK=0) */
+#define SG_NVME_NVM_WRITE 0x1
+#define SG_NVME_NVM_WRITE_ZEROES 0x8    /* SCSI WRITE SAME */
+
+#define SG_NVME_RW_CDW12_FUA (1 << 30) /* Force Unit Access bit */
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+
+static void
+mk_sense_asc_ascq(struct sg_pt_freebsd_scsi * ptp, int sk, int asc, int ascq,
+                  int vb)
+{
+    bool dsense = ptp->mchanp ? ptp->mchanp->dev_stat.scsi_dsense : false;
+    int n;
+    uint8_t * sbp = ptp->sense;
+
+    ptp->scsi_status = SAM_STAT_CHECK_CONDITION;
+    n = ptp->sense_len;
+    if ((n < 8) || ((! dsense) && (n < 14))) {
+        if (vb)
+            pr2ws("%s: sense_len=%d too short, want 14 or more\n", __func__,
+                  n);
+        return;
+    } else
+        ptp->sense_resid = ptp->sense_len -
+                           (dsense ? 8 : ((n < 18) ? n : 18));
+    memset(sbp, 0, n);
+    sg_build_sense_buffer(dsense, sbp, sk, asc, ascq);
+    if (vb > 3)
+        pr2ws("%s:  [sense_key,asc,ascq]: [0x%x,0x%x,0x%x]\n", __func__,
+              sk, asc, ascq);
+}
+
+static void
+mk_sense_from_nvme_status(struct sg_pt_freebsd_scsi * ptp, uint16_t sct_sc,
+                          int vb)
+{
+    bool ok;
+    bool dsense = ptp->mchanp ? ptp->mchanp->dev_stat.scsi_dsense : false;
+    int n;
+    uint8_t sstatus, sk, asc, ascq;
+    uint8_t * sbp = ptp->sense;
+
+    ok = sg_nvme_status2scsi(sct_sc, &sstatus, &sk, &asc, &ascq);
+    if (! ok) { /* can't find a mapping to a SCSI error, so ... */
+        sstatus = SAM_STAT_CHECK_CONDITION;
+        sk = SPC_SK_ILLEGAL_REQUEST;
+        asc = 0xb;
+        ascq = 0x0;     /* asc: "WARNING" purposely vague */
+    }
+
+    ptp->scsi_status = sstatus;
+    n = ptp->sense_len;
+    if ((n < 8) || ((! dsense) && (n < 14))) {
+        if (vb)
+            pr2ws("%s: sense_len=%d too short, want 14 or more\n", __func__,
+                  n);
+        return;
+    } else
+        ptp->sense_resid = ptp->sense_len -
+                           (dsense ? 8 : ((n < 18) ? n : 18));
+    memset(sbp, 0, n);
+    sg_build_sense_buffer(dsense, sbp, sk, asc, ascq);
+    if (vb > 3)
+        pr2ws("%s:  [sense_key,asc,ascq]: [0x%x,0x%x,0x%x]\n", __func__,
+              sk, asc, ascq);
+    if (dsense && (sct_sc > 0) && (ptp->sense_resid > 7)) {
+        sg_nvme_desc2sense(sbp, 0x4000 & sct_sc /* dnr */,
+                           0x2000 & sct_sc /* more */, 0x7ff & sct_sc);
+        ptp->sense_resid -= 8;
+    }
+}
+
+/* Set in_bit to -1 to indicate no bit position of invalid field */
+static void
+mk_sense_invalid_fld(struct sg_pt_freebsd_scsi * ptp, bool in_cdb,
+                     int in_byte, int in_bit, int vb)
+{
+    bool ds = ptp->mchanp ? ptp->mchanp->dev_stat.scsi_dsense : false;
+    int asc, n;
+    uint8_t * sbp = (uint8_t *)ptp->sense;
+    uint8_t sks[4];
+
+    ptp->scsi_status = SAM_STAT_CHECK_CONDITION;
+    asc = in_cdb ? INVALID_FIELD_IN_CDB : INVALID_FIELD_IN_PARAM_LIST;
+    n = ptp->sense_len;
+    if ((n < 8) || ((! ds) && (n < 14))) {
+        if (vb)
+            pr2ws("%s: max_response_len=%d too short, want 14 or more\n",
+                  __func__, n);
+        return;
+    } else
+        ptp->sense_resid = ptp->sense_len - (ds ? 8 : ((n < 18) ? n : 18));
+    memset(sbp, 0, n);
+    sg_build_sense_buffer(ds, sbp, SPC_SK_ILLEGAL_REQUEST, asc, 0);
+    memset(sks, 0, sizeof(sks));
+    sks[0] = 0x80;
+    if (in_cdb)
+        sks[0] |= 0x40;
+    if (in_bit >= 0) {
+        sks[0] |= 0x8;
+        sks[0] |= (0x7 & in_bit);
+    }
+    sg_put_unaligned_be16(in_byte, sks + 1);
+    if (ds) {
+        int sl = sbp[7] + 8;
+
+        sbp[7] = sl;
+        sbp[sl] = 0x2;
+        sbp[sl + 1] = 0x6;
+        memcpy(sbp + sl + 4, sks, 3);
+    } else
+        memcpy(sbp + 15, sks, 3);
+    if (vb > 3)
+        pr2ws("%s:  [sense_key,asc,ascq]: [0x5,0x%x,0x0] %c byte=%d, bit=%d\n",
+              __func__, asc, in_cdb ? 'C' : 'D', in_byte,
+              ((in_bit > 0) ? (0x7 & in_bit) : 0));
+}
+
+#if 0
+static void
+nvme_cbfcn(struct cam_periph * camperp, union ccb * ccb)
+{
+    pr2ws("%s: >>>> called, camperp=%p, ccb=%p\n", __func__, camperp, ccb);
+}
+#endif
+
+/* Does actual ioctl(NVME_PASSTHROUGH_CMD) or uses NVME(CAM) interface.
+ * Returns 0 on success; negative values are Unix negated errno values;
+ * positive values are NVMe status (i.e. ((SCT << 8) | SC) ). */
+static int
+nvme_pt_low(struct sg_pt_freebsd_scsi * ptp, void * dxferp, uint32_t len,
+            bool is_admin, bool is_read, struct nvme_pt_command * npcp,
+            int time_secs, int vb)
+{
+    int err, dev_fd;
+    uint16_t sct_sc;
+    uint8_t opcode;
+    struct freebsd_dev_channel *fdc_p = ptp->mchanp;
+
+    if (vb > 6)
+        pr2ws("%s: is_read=%d, time_secs=%d, is_cam_nvme=%d, is_admin=%d\n",
+              __func__, (int)is_read, time_secs, (int)fdc_p->is_cam_nvme,
+             (int)is_admin);
+    ptp->is_nvme_dev = fdc_p->is_nvme_dev;
+    npcp->buf = dxferp;
+    npcp->len = len;
+    npcp->is_read = (uint32_t)is_read;
+    opcode = npcp->cmd.opc;
+#if __FreeBSD_version >= 1100000
+    if (fdc_p->is_cam_nvme)
+        goto cam_nvme;
+#endif
+
+    /* non-CAM NVMe processing follows */
+    if (is_admin) {
+        if (fdc_p->nvme_fd_ctrl < 0) {
+            if (vb > 4)
+                pr2ws("%s: not CAM but nvme_fd_ctrl<0, try to open "
+                      "controller\n", __func__);
+            if ((fdc_p->nsid > 0) && fdc_p->devname && *fdc_p->devname) {
+                int fd;
+                char dev_nm[PATH_MAX];
+
+                if ((fdc_p->devname[0] == '/') || (fdc_p->devname[0] == '.'))
+                    strncpy(dev_nm, fdc_p->devname, PATH_MAX);
+                else
+                    snprintf(dev_nm, PATH_MAX, "/dev/%s", fdc_p->devname);
+                fd = open(dev_nm, O_RDWR);
+                if (fd < 0) {
+                    if (vb > 1)
+                        pr2ws("%s: Unable to open %s of NVMe controller: "
+                              "%s\n", __func__, dev_nm, strerror(errno));
+                } else
+                    fdc_p->nvme_fd_ctrl = fd;
+            }
+            if (fdc_p->nvme_fd_ctrl < 0)
+                return -EINVAL;
+        }
+        dev_fd = fdc_p->nvme_fd_ctrl;
+    } else {
+        if (fdc_p->nvme_fd_ns < 0) {
+            if (vb > 1)
+                pr2ws("%s: not CAM but nvme_fd_ns<0, inconsistent\n",
+                      __func__);
+            return -EINVAL;
+        }
+        dev_fd = fdc_p->nvme_fd_ns;
+    }
+    err = ioctl(dev_fd, NVME_PASSTHROUGH_CMD, npcp);
+    if (err < 0) {
+        err = errno;
+        if (vb)
+            pr2ws("%s: ioctl(NVME_PASSTHROUGH_CMD) errno: %s\n", __func__,
+                  strerror(err));
+        /* when that ioctl returns an error npcp->cpl is not populated */
+        return -err;
+    }
+
+#if __FreeBSD_version <= 1200058
+    sct_sc = ((npcp->cpl.status.sct << 8) | npcp->cpl.status.sc);
+#else
+    sct_sc = (NVME_STATUS_GET_SCT(npcp->cpl.status) << 8) |
+             NVME_STATUS_GET_SC(npcp->cpl.status);
+#endif
+    ptp->nvme_result = npcp->cpl.cdw0;
+    sg_put_unaligned_le32(npcp->cpl.cdw0,
+                          ptp->cq_dw0_3 + SG_NVME_PT_CQ_RESULT);
+    sg_put_unaligned_le32(npcp->cpl.rsvd1, ptp->cq_dw0_3 + 4);
+    sg_put_unaligned_le16(npcp->cpl.sqhd, ptp->cq_dw0_3 + 8);
+    sg_put_unaligned_le16(npcp->cpl.sqid, ptp->cq_dw0_3 + 10);
+    sg_put_unaligned_le16(npcp->cpl.cid, ptp->cq_dw0_3 + 12);
+    sg_put_unaligned_le16(*((const uint16_t *)&(npcp->cpl.status)),
+                          ptp->cq_dw0_3 + SG_NVME_PT_CQ_STATUS_P);
+    if (sct_sc && (vb > 1)) {
+        char nam[64];
+        char b[80];
+
+        sg_get_nvme_opcode_name(opcode, is_admin, sizeof(nam), nam);
+        pr2ws("%s: %s [0x%x], status: %s\n", __func__, nam, opcode,
+              sg_get_nvme_cmd_status_str(sct_sc, sizeof(b), b));
+    }
+    return sct_sc;
+
+#if __FreeBSD_version >= 1100000
+cam_nvme:
+    {
+        cam_status ccb_status;
+        union ccb *ccb;
+        struct ccb_nvmeio *nviop;
+        FILE * ferrp = sg_warnings_strm ? sg_warnings_strm : stderr;
+
+        if (NULL == ptp->ccb) {     /* re-use if we have one already */
+            if (! (ccb = cam_getccb(fdc_p->cam_dev))) {
+                if (vb)
+                    pr2ws("%s: cam_getccb: failed\n", __func__);
+                ptp->os_err = ENOMEM;
+                return -ptp->os_err;
+            }
+            ptp->ccb = ccb;
+        } else
+            ccb = ptp->ccb;
+        nviop = &ccb->nvmeio;
+        CCB_CLEAR_ALL_EXCEPT_HDR(nviop);
+
+        memcpy(&nviop->cmd, &npcp->cmd, sizeof(nviop->cmd));
+        ptp->timeout_ms = (time_secs > 0) ? (time_secs * 1000) : DEF_TIMEOUT;
+        if (is_admin)
+            cam_fill_nvmeadmin(nviop,
+                               1 /* retries */,
+                               NULL,
+                               is_read ? CAM_DIR_IN : CAM_DIR_OUT,
+                               dxferp,
+                               len,
+                               ptp->timeout_ms);
+
+        else {   /* NVM command set, rather than Admin */
+            if (fdc_p->nsid != npcp->cmd.nsid) {
+                if (vb)
+                    pr2ws("%s: device node nsid [%u] not equal to cmd nsid "
+                          "[%u]\n", __func__, fdc_p->nsid, npcp->cmd.nsid);
+                return -EINVAL;
+            }
+            cam_fill_nvmeio(nviop,
+                            1 /* retries */,
+                            NULL,
+                            is_read ? CAM_DIR_IN : CAM_DIR_OUT,
+                            dxferp,
+                            len,
+                            ptp->timeout_ms);
+        }
+
+        if (cam_send_ccb(fdc_p->cam_dev, ccb) < 0) {
+            if (vb) {
+                pr2ws("%s: cam_send_ccb(NVME) %s ccb error\n", __func__,
+                      (is_admin ? "Admin" : "NVM"));
+                CAM_ERROR_PRINT(fdc_p->cam_dev, ccb, CAM_ESF_ALL,
+                                CAM_EPF_ALL, ferrp);
+            }
+            cam_freeccb(ptp->ccb);
+            ptp->ccb = NULL;
+            ptp->os_err = EIO;
+            return -ptp->os_err;
+        }
+        ccb_status = ccb->ccb_h.status & CAM_STATUS_MASK;
+        if (ccb_status == CAM_REQ_CMP) {
+            ptp->nvme_result = 0;
+            ptp->os_err = 0;
+            return 0;
+        }
+        /* error processing follows ... */
+        ptp->os_err = EIO;
+        if (vb) {
+            pr2ws("%s: ccb_status != CAM_REQ_CMP\n", __func__);
+            CAM_ERROR_PRINT(fdc_p->cam_dev, ccb, CAM_ESF_ALL,
+                            CAM_EPF_ALL, ferrp);
+        }
+#if __FreeBSD_version <= 1200058
+        sct_sc = ((nviop->cpl.status.sct << 8) | nviop->cpl.status.sc);
+#else
+        sct_sc = (NVME_STATUS_GET_SCT(nviop->cpl.status) << 8) |
+             NVME_STATUS_GET_SC(nviop->cpl.status);
+#endif
+        ptp->nvme_result = nviop->cpl.cdw0;
+        sg_put_unaligned_le32(nviop->cpl.cdw0,
+                              ptp->cq_dw0_3 + SG_NVME_PT_CQ_RESULT);
+        sg_put_unaligned_le32(nviop->cpl.rsvd1, ptp->cq_dw0_3 + 4);
+        sg_put_unaligned_le16(nviop->cpl.sqhd, ptp->cq_dw0_3 + 8);
+        sg_put_unaligned_le16(nviop->cpl.sqid, ptp->cq_dw0_3 + 10);
+        sg_put_unaligned_le16(nviop->cpl.cid, ptp->cq_dw0_3 + 12);
+        sg_put_unaligned_le16(*((const uint16_t *)&(nviop->cpl.status)),
+                              ptp->cq_dw0_3 + SG_NVME_PT_CQ_STATUS_P);
+        if (sct_sc && (vb > 1)) {
+            char nam[64];
+            char b[80];
+
+            sg_get_nvme_opcode_name(opcode, is_admin, sizeof(nam),
+                                    nam);
+            pr2ws("%s: %s [0x%x], status: %s\n", __func__, nam, opcode,
+                  sg_get_nvme_cmd_status_str(sct_sc, sizeof(b), b));
+        }
+        return sct_sc ? sct_sc : ptp->os_err;
+    }
+#endif
+    return 0;
+}
+
+static void
+sntl_check_enclosure_override(struct freebsd_dev_channel * fdc_p, int vb)
+{
+    uint8_t * up = fdc_p->nvme_id_ctlp;
+    uint8_t nvmsr;
+
+    if (NULL == up)
+        return;
+    nvmsr = up[253];
+    if (vb > 5)
+        pr2ws("%s: enter, nvmsr=%u\n", __func__, nvmsr);
+    fdc_p->dev_stat.id_ctl253 = nvmsr;
+    switch (fdc_p->dev_stat.enclosure_override) {
+    case 0x0:       /* no override */
+        if (0x3 == (0x3 & nvmsr)) {
+            fdc_p->dev_stat.pdt = PDT_DISK;
+            fdc_p->dev_stat.enc_serv = 1;
+        } else if (0x2 & nvmsr) {
+            fdc_p->dev_stat.pdt = PDT_SES;
+            fdc_p->dev_stat.enc_serv = 1;
+        } else if (0x1 & nvmsr) {
+            fdc_p->dev_stat.pdt = PDT_DISK;
+            fdc_p->dev_stat.enc_serv = 0;
+        } else {
+            uint32_t nn = sg_get_unaligned_le32(up + 516);
+
+            fdc_p->dev_stat.pdt = nn ? PDT_DISK : PDT_UNKNOWN;
+            fdc_p->dev_stat.enc_serv = 0;
+        }
+        break;
+    case 0x1:       /* override to SES device */
+        fdc_p->dev_stat.pdt = PDT_SES;
+        fdc_p->dev_stat.enc_serv = 1;
+        break;
+    case 0x2:       /* override to disk with attached SES device */
+        fdc_p->dev_stat.pdt = PDT_DISK;
+        fdc_p->dev_stat.enc_serv = 1;
+        break;
+    case 0x3:       /* override to SAFTE device (PDT_PROCESSOR) */
+        fdc_p->dev_stat.pdt = PDT_PROCESSOR;
+        fdc_p->dev_stat.enc_serv = 1;
+        break;
+    case 0xff:      /* override to normal disk */
+        fdc_p->dev_stat.pdt = PDT_DISK;
+        fdc_p->dev_stat.enc_serv = 0;
+        break;
+    default:
+        pr2ws("%s: unknown enclosure_override value: %d\n", __func__,
+              fdc_p->dev_stat.enclosure_override);
+        break;
+    }
+}
+
+static int
+sntl_do_identify(struct sg_pt_freebsd_scsi * ptp, int cns, int nsid,
+                 int u_len, uint8_t * up, int time_secs, int vb)
+{
+    int err;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+
+    if (vb > 5)
+        pr2ws("%s: nsid=%d\n", __func__, nsid);
+    memset(npc_up, 0, sizeof(npc));
+    npc_up[SG_NVME_PT_OPCODE] = SG_NVME_AD_IDENTIFY;
+    sg_put_unaligned_le32(nsid, npc_up + SG_NVME_PT_NSID);
+    /* CNS=0x1 Identify: controller */
+    sg_put_unaligned_le32(cns, npc_up + SG_NVME_PT_CDW10);
+    sg_put_unaligned_le64((sg_uintptr_t)up, npc_up + SG_NVME_PT_ADDR);
+    sg_put_unaligned_le32(u_len, npc_up + SG_NVME_PT_DATA_LEN);
+    err = nvme_pt_low(ptp, up, u_len, true, true, &npc, time_secs, vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n", __func__,
+                      strerror(-err), -err);
+            return err;
+        } else {        /* non-zero NVMe command status */
+            ptp->nvme_status = err;
+            return SG_LIB_NVME_STATUS;
+        }
+    }
+    return 0;
+}
+
+/* Currently only caches associated controller response (4096 bytes) */
+static int
+sntl_cache_identity(struct sg_pt_freebsd_scsi * ptp, int time_secs, int vb)
+{
+    int ret;
+    uint32_t pg_sz = sg_get_page_size();
+    struct freebsd_dev_channel * fdc_p = ptp->mchanp;
+
+    fdc_p->nvme_id_ctlp = sg_memalign(pg_sz, pg_sz,
+                                      &fdc_p->free_nvme_id_ctlp, vb > 3);
+    if (NULL == fdc_p->nvme_id_ctlp) {
+        if (vb)
+            pr2ws("%s: sg_memalign() failed to get memory\n", __func__);
+        return -ENOMEM;
+    }
+    ret = sntl_do_identify(ptp, 0x1 /* CNS */, 0 /* nsid */, pg_sz,
+                           fdc_p->nvme_id_ctlp, time_secs, vb);
+    if (0 == ret)
+        sntl_check_enclosure_override(fdc_p, vb);
+    return (ret < 0) ? sg_convert_errno(-ret) : ret;
+}
+
+static const char * nvme_scsi_vendor_str = "NVMe    ";
+static const uint16_t inq_resp_len = 36;
+
+static int
+sntl_inq(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp, int time_secs,
+         int vb)
+{
+    bool evpd;
+    int res;
+    uint16_t n, alloc_len, pg_cd;
+    uint32_t pg_sz = sg_get_page_size();
+    struct freebsd_dev_channel * fdc_p;
+    uint8_t * nvme_id_ns = NULL;
+    uint8_t * free_nvme_id_ns = NULL;
+    uint8_t inq_dout[256];
+
+    if (vb > 5)
+        pr2ws("%s: starting\n", __func__);
+
+    if (0x2 & cdbp[1]) {        /* Reject CmdDt=1 */
+        mk_sense_invalid_fld(ptp, true, 1, 1, vb);
+        return 0;
+    }
+    fdc_p = get_fdc_p(ptp);
+    if (NULL == fdc_p) {
+        if (vb)
+            pr2ws("%s: get_fdc_p() failed, no file descriptor ?\n", __func__);
+        return -EINVAL;
+    }
+    if (NULL == fdc_p->nvme_id_ctlp) {
+        res = sntl_cache_identity(ptp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, ptp->nvme_status, vb);
+            return 0;
+        } else if (res)         /* should be negative errno */
+            return res;
+    }
+    memset(inq_dout, 0, sizeof(inq_dout));
+    alloc_len = sg_get_unaligned_be16(cdbp + 3);
+    evpd = !!(0x1 & cdbp[1]);
+    pg_cd = cdbp[2];
+    if (evpd) {         /* VPD page responses */
+        bool cp_id_ctl = false;
+
+        switch (pg_cd) {
+        case 0:         /* Supported VPD pages VPD page */
+            /* inq_dout[0] = (PQ=0)<<5 | (PDT=0); prefer pdt=0xd --> SES */
+            inq_dout[1] = pg_cd;
+            n = 11;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[4] = 0x0;
+            inq_dout[5] = 0x80;
+            inq_dout[6] = 0x83;
+            inq_dout[7] = 0x86;
+            inq_dout[8] = 0x87;
+            inq_dout[9] = 0x92;
+            inq_dout[n - 1] = SG_NVME_VPD_NICR;     /* last VPD number */
+            break;
+        case 0x80:      /* Serial number VPD page */
+            /* inq_dout[0] = (PQ=0)<<5 | (PDT=0); prefer pdt=0xd --> SES */
+            inq_dout[1] = pg_cd;
+            n = 24;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            memcpy(inq_dout + 4, fdc_p->nvme_id_ctlp + 4, 20);    /* SN */
+            break;
+        case 0x83:      /* Device identification VPD page */
+            if ((fdc_p->nsid > 0) && (fdc_p->nsid < SG_NVME_BROADCAST_NSID)) {
+                nvme_id_ns = sg_memalign(pg_sz, pg_sz, &free_nvme_id_ns,
+                                         vb > 3);
+                if (nvme_id_ns) {
+                    struct nvme_pt_command npc;
+                    uint8_t * npc_up = (uint8_t *)&npc;
+
+                    memset(npc_up, 0, sizeof(npc));
+                    npc_up[SG_NVME_PT_OPCODE] = SG_NVME_AD_IDENTIFY;
+                    sg_put_unaligned_le32(fdc_p->nsid,
+                                          npc_up + SG_NVME_PT_NSID);
+                    /* CNS=0x0 Identify: namespace */
+                    sg_put_unaligned_le32(0x0, npc_up + SG_NVME_PT_CDW10);
+                    sg_put_unaligned_le64((sg_uintptr_t)nvme_id_ns,
+                                          npc_up + SG_NVME_PT_ADDR);
+                    sg_put_unaligned_le32(pg_sz,
+                                          npc_up + SG_NVME_PT_DATA_LEN);
+                    res = nvme_pt_low(ptp, nvme_id_ns, pg_sz, true, true,
+                                      &npc, time_secs, vb > 3);
+                    if (res) {
+                        free(free_nvme_id_ns);
+                        free_nvme_id_ns = NULL;
+                        nvme_id_ns = NULL;
+                    }
+                }
+            }
+            n = sg_make_vpd_devid_for_nvme(fdc_p->nvme_id_ctlp, nvme_id_ns, 0,
+                                           -1, inq_dout, sizeof(inq_dout));
+            if (n > 3)
+                sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            if (free_nvme_id_ns) {
+                free(free_nvme_id_ns);
+                free_nvme_id_ns = NULL;
+                nvme_id_ns = NULL;
+            }
+            break;
+        case 0x86:      /* Extended INQUIRY (per SFS SPC Discovery 2016) */
+            inq_dout[1] = pg_cd;
+            n = 64;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[5] = 0x1;          /* SIMPSUP=1 */
+            inq_dout[7] = 0x1;          /* LUICLR=1 */
+            inq_dout[13] = 0x40;        /* max supported sense data length */
+            break;
+        case 0x87:      /* Mode page policy (per SFS SPC Discovery 2016) */
+            inq_dout[1] = pg_cd;
+            n = 8;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[4] = 0x3f;         /* all mode pages */
+            inq_dout[5] = 0xff;         /*     and their sub-pages */
+            inq_dout[6] = 0x80;         /* MLUS=1, policy=shared */
+            break;
+        case 0x92:      /* SCSI Feature set: only SPC Discovery 2016 */
+            inq_dout[1] = pg_cd;
+            n = 10;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[9] = 0x1;  /* SFS SPC Discovery 2016 */
+            break;
+        case SG_NVME_VPD_NICR:  /* 0xde */
+            inq_dout[1] = pg_cd;
+            sg_put_unaligned_be16((16 + 4096) - 4, inq_dout + 2);
+            n = 16 + 4096;
+            cp_id_ctl = true;
+            break;
+        default:        /* Point to page_code field in cdb */
+            mk_sense_invalid_fld(ptp, true, 2, 7, vb);
+            return 0;
+        }
+        if (alloc_len > 0) {
+            n = (alloc_len < n) ? alloc_len : n;
+            n = (n < ptp->dxfer_len) ? n : ptp->dxfer_len;
+            ptp->resid = ptp->dxfer_len - n;
+            if (n > 0) {
+                if (cp_id_ctl) {
+                    memcpy((uint8_t *)ptp->dxferp, inq_dout,
+                           (n < 16 ? n : 16));
+                    if (n > 16)
+                        memcpy((uint8_t *)ptp->dxferp + 16,
+                               fdc_p->nvme_id_ctlp, n - 16);
+                } else
+                    memcpy((uint8_t *)ptp->dxferp, inq_dout, n);
+            }
+        }
+    } else {            /* Standard INQUIRY response */
+        /* pdt=0 --> disk; pdt=0xd --> SES; pdt=3 --> processor (safte) */
+        inq_dout[0] = (PDT_MASK & fdc_p->dev_stat.pdt);  /* (PQ=0)<<5 */
+        /* inq_dout[1] = (RMD=0)<<7 | (LU_CONG=0)<<6; rest reserved */
+        inq_dout[2] = 6;   /* version: SPC-4 */
+        inq_dout[3] = 2;   /* NORMACA=0, HISUP=0, response data format: 2 */
+        inq_dout[4] = 31;  /* so response length is (or could be) 36 bytes */
+        inq_dout[6] = fdc_p->dev_stat.enc_serv ? 0x40 : 0;
+        inq_dout[7] = 0x2;    /* CMDQUE=1 */
+        memcpy(inq_dout + 8, nvme_scsi_vendor_str, 8);  /* NVMe not Intel */
+        memcpy(inq_dout + 16, fdc_p->nvme_id_ctlp + 24, 16);/* Prod <-- MN */
+        memcpy(inq_dout + 32, fdc_p->nvme_id_ctlp + 64, 4); /* Rev <-- FR */
+        if (alloc_len > 0) {
+            n = (alloc_len < inq_resp_len) ? alloc_len : inq_resp_len;
+            n = (n < ptp->dxfer_len) ? n : ptp->dxfer_len;
+            if (n > 0)
+                memcpy((uint8_t *)ptp->dxferp, inq_dout, n);
+        }
+    }
+    return 0;
+}
+
+static int
+sntl_rluns(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+           int time_secs, int vb)
+{
+    int res;
+    uint16_t sel_report;
+    uint32_t alloc_len, k, n, num, max_nsid;
+    struct freebsd_dev_channel * fdc_p;
+    uint8_t * rl_doutp;
+    uint8_t * up;
+
+    if (vb > 5)
+        pr2ws("%s: starting\n", __func__);
+    fdc_p = get_fdc_p(ptp);
+    if (NULL == fdc_p) {
+        if (vb)
+            pr2ws("%s: get_fdc_p() failed, no file descriptor ?\n", __func__);
+        return -EINVAL;
+    }
+    sel_report = cdbp[2];
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (NULL == fdc_p->nvme_id_ctlp) {
+        res = sntl_cache_identity(ptp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, ptp->nvme_status, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    max_nsid = sg_get_unaligned_le32(fdc_p->nvme_id_ctlp + 516);
+    switch (sel_report) {
+    case 0:
+    case 2:
+        num = max_nsid;
+        break;
+    case 1:
+    case 0x10:
+    case 0x12:
+        num = 0;
+        break;
+    case 0x11:
+        num = (1 == fdc_p->nsid) ? max_nsid :  0;
+        break;
+    default:
+        if (vb > 1)
+            pr2ws("%s: bad select_report value: 0x%x\n", __func__,
+                  sel_report);
+        mk_sense_invalid_fld(ptp, true, 2, 7, vb);
+        return 0;
+    }
+    rl_doutp = (uint8_t *)calloc(num + 1, 8);
+    if (NULL == rl_doutp) {
+        if (vb)
+            pr2ws("%s: calloc() failed to get memory\n", __func__);
+        return -ENOMEM;
+    }
+    for (k = 0, up = rl_doutp + 8; k < num; ++k, up += 8)
+        sg_put_unaligned_be16(k, up);
+    n = num * 8;
+    sg_put_unaligned_be32(n, rl_doutp);
+    n+= 8;
+    if (alloc_len > 0) {
+        n = (alloc_len < n) ? alloc_len : n;
+        n = (n < (uint32_t)ptp->dxfer_len) ? n : (uint32_t)ptp->dxfer_len;
+        ptp->resid = ptp->dxfer_len - (int)n;
+        if (n > 0)
+            memcpy((uint8_t *)ptp->dxferp, rl_doutp, n);
+    }
+    res = 0;
+    free(rl_doutp);
+    return res;
+}
+
+static int
+sntl_tur(struct sg_pt_freebsd_scsi * ptp, int time_secs, int vb)
+{
+    int err;
+    uint32_t pow_state;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+
+    if (vb > 5)
+        pr2ws("%s: starting\n", __func__);
+    fdc_p = get_fdc_p(ptp);
+    if (NULL == fdc_p) {
+        if (vb)
+            pr2ws("%s: get_fdc_p() failed, no file descriptor ?\n", __func__);
+        return -EINVAL;
+    }
+    if (NULL == fdc_p->nvme_id_ctlp) {
+        int res = sntl_cache_identity(ptp, time_secs, vb);
+
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, ptp->nvme_status, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    memset(npc_up, 0, sizeof(npc));
+    npc_up[SG_NVME_PT_OPCODE] = SG_NVME_AD_GET_FEATURE;
+    sg_put_unaligned_le32(SG_NVME_BROADCAST_NSID, npc_up + SG_NVME_PT_NSID);
+    /* SEL=0 (current), Feature=2 Power Management */
+    sg_put_unaligned_le32(0x2, npc_up + SG_NVME_PT_CDW10);
+    err = nvme_pt_low(ptp, NULL, 0, true, false, &npc, time_secs, vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n", __func__,
+                      strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    pow_state = (0x1f & ptp->nvme_result);
+    if (vb > 3)
+        pr2ws("%s: pow_state=%u\n", __func__, pow_state);
+#if 0   /* pow_state bounces around too much on laptop */
+    if (pow_state)
+        mk_sense_asc_ascq(ptp, SPC_SK_NOT_READY, LOW_POWER_COND_ON_ASC, 0,
+                          vb);
+#endif
+    return 0;
+}
+
+static int
+sntl_req_sense(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+               int time_secs, int vb)
+{
+    bool desc;
+    int err;
+    uint32_t pow_state, alloc_len, n;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+    uint8_t rs_dout[64];
+
+    if (vb > 5)
+        pr2ws("%s: starting\n", __func__);
+    fdc_p = get_fdc_p(ptp);
+    if (NULL == fdc_p) {
+        if (vb)
+            pr2ws("%s: get_fdc_p() failed, no file descriptor ?\n", __func__);
+        return -EINVAL;
+    }
+    if (NULL == fdc_p->nvme_id_ctlp) {
+        int res = sntl_cache_identity(ptp, time_secs, vb);
+
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, ptp->nvme_status, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    desc = !!(0x1 & cdbp[1]);
+    alloc_len = cdbp[4];
+    memset(npc_up, 0, sizeof(npc));
+    npc_up[SG_NVME_PT_OPCODE] = SG_NVME_AD_GET_FEATURE;
+    sg_put_unaligned_le32(SG_NVME_BROADCAST_NSID, npc_up + SG_NVME_PT_NSID);
+    /* SEL=0 (current), Feature=2 Power Management */
+    sg_put_unaligned_le32(0x2, npc_up + SG_NVME_PT_CDW10);
+    err = nvme_pt_low(ptp, NULL, 0, true, false, &npc, time_secs, vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n", __func__,
+                      strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    pow_state = (0x1f & ptp->nvme_result);
+    if (vb > 3)
+        pr2ws("%s: pow_state=%u\n", __func__, pow_state);
+    memset(rs_dout, 0, sizeof(rs_dout));
+    if (pow_state)
+            sg_build_sense_buffer(desc, rs_dout, SPC_SK_NO_SENSE,
+                                  LOW_POWER_COND_ON_ASC, 0);
+    else
+            sg_build_sense_buffer(desc, rs_dout, SPC_SK_NO_SENSE,
+                                  NO_ADDITIONAL_SENSE, 0);
+    n = desc ? 8 : 18;
+    n = (n < alloc_len) ? n : alloc_len;
+        n = (n < (uint32_t)ptp->dxfer_len) ? n : (uint32_t)ptp->dxfer_len;
+    ptp->resid = ptp->dxfer_len - (int)n;
+    if (n > 0)
+        memcpy((uint8_t *)ptp->dxferp, rs_dout, n);
+    return 0;
+}
+
+static int
+sntl_mode_ss(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+             int time_secs, int vb)
+{
+    bool is_msense = (SCSI_MODE_SENSE10_OPC == cdbp[0]);
+    int n, len;
+    uint8_t * bp;
+    struct freebsd_dev_channel * fdc_p;
+    struct sg_sntl_result_t sntl_result;
+
+    if (vb > 5)
+        pr2ws("%s: mse%s\n", __func__, (is_msense ? "nse" : "lect"));
+    fdc_p = get_fdc_p(ptp);
+    if (NULL == fdc_p) {
+        if (vb)
+            pr2ws("%s: get_fdc_p() failed, no file descriptor ?\n", __func__);
+        return -EINVAL;
+    }
+    if (NULL == fdc_p->nvme_id_ctlp) {
+        int res = sntl_cache_identity(ptp, time_secs, vb);
+
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, ptp->nvme_status, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    if (is_msense) {    /* MODE SENSE(10) */
+        len = ptp->dxfer_len;
+        bp = ptp->dxferp;
+        n = sntl_resp_mode_sense10(&fdc_p->dev_stat, cdbp, bp, len,
+                                   &sntl_result);
+        ptp->resid = (n >= 0) ? len - n : len;
+    } else {            /* MODE SELECT(10) */
+        uint8_t pre_enc_ov = fdc_p->dev_stat.enclosure_override;
+
+        len = ptp->dxfer_len;
+        bp = ptp->dxferp;
+        n = sntl_resp_mode_select10(&fdc_p->dev_stat, cdbp, bp, len,
+                                    &sntl_result);
+        if (pre_enc_ov != fdc_p->dev_stat.enclosure_override)
+            sntl_check_enclosure_override(fdc_p, vb);  /* ENC_OV has changed */
+    }
+    if (n < 0) {
+        int in_bit = (255 == sntl_result.in_bit) ? (int)sntl_result.in_bit :
+                                                   -1;
+        if ((SAM_STAT_CHECK_CONDITION == sntl_result.sstatus) &&
+            (SPC_SK_ILLEGAL_REQUEST == sntl_result.sk)) {
+            if (INVALID_FIELD_IN_CDB == sntl_result.asc)
+                mk_sense_invalid_fld(ptp, true, sntl_result.in_byte, in_bit,
+                                     vb);
+            else if (INVALID_FIELD_IN_PARAM_LIST == sntl_result.asc)
+                mk_sense_invalid_fld(ptp, false, sntl_result.in_byte, in_bit,
+                                     vb);
+            else
+                mk_sense_asc_ascq(ptp, sntl_result.sk, sntl_result.asc,
+                                  sntl_result.ascq, vb);
+        } else if (vb)
+            pr2ws("%s: error but no sense?? n=%d\n", __func__, n);
+    }
+    return 0;
+}
+
+/* This is not really a SNTL. For SCSI SEND DIAGNOSTIC(PF=1) NVMe-MI
+ * has a special command (SES Send) to tunnel through pages to an
+ * enclosure. The NVMe enclosure is meant to understand the SES
+ * (SCSI Enclosure Services) use of diagnostics pages that are
+ * related to SES. */
+static int
+sntl_senddiag(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+              int time_secs, int vb)
+{
+    bool pf, self_test;
+    int err;
+    uint8_t st_cd, dpg_cd;
+    uint32_t alloc_len, n, dout_len, dpg_len, nvme_dst;
+    const uint8_t * dop;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+
+    st_cd = 0x7 & (cdbp[1] >> 5);
+    pf = !! (0x4 & cdbp[1]);
+    self_test = !! (0x10 & cdbp[1]);
+    if (vb > 5)
+        pr2ws("%s: pf=%d, self_test=%d, st_code=%d\n", __func__, (int)pf,
+              (int)self_test, (int)st_cd);
+    fdc_p = get_fdc_p(ptp);
+    if (NULL == fdc_p) {
+        if (vb)
+            pr2ws("%s: get_fdc_p() failed, no file descriptor ?\n", __func__);
+        return -EINVAL;
+    }
+    if (self_test || st_cd) {
+        memset(npc_up, 0, sizeof(npc));
+        npc_up[SG_NVME_PT_OPCODE] = SG_NVME_AD_DEV_SELT_TEST;
+        /* just this namespace (if there is one) and controller */
+        sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+        switch (st_cd) {
+        case 0: /* Here if self_test is set, do short self-test */
+        case 1: /* Background short */
+        case 5: /* Foreground short */
+            nvme_dst = 1;
+            break;
+        case 2: /* Background extended */
+        case 6: /* Foreground extended */
+            nvme_dst = 2;
+            break;
+        case 4: /* Abort self-test */
+            nvme_dst = 0xf;
+            break;
+        default:
+            pr2ws("%s: bad self-test code [0x%x]\n", __func__, st_cd);
+            mk_sense_invalid_fld(ptp, true, 1, 7, vb);
+            return 0;
+        }
+        sg_put_unaligned_le32(nvme_dst, npc_up + SG_NVME_PT_CDW10);
+        err = nvme_pt_low(ptp, NULL, 0x0, true, false, &npc, time_secs, vb);
+        goto do_low;
+    }
+    alloc_len = sg_get_unaligned_be16(cdbp + 3); /* parameter list length */
+    dout_len = ptp->dxfer_len;
+    if (pf) {
+        if (0 == alloc_len) {
+            mk_sense_invalid_fld(ptp, true, 3, 7, vb);
+            if (vb)
+                pr2ws("%s: PF bit set bit param_list_len=0\n", __func__);
+            return 0;
+        }
+    } else {    /* PF bit clear */
+        if (alloc_len) {
+            mk_sense_invalid_fld(ptp, true, 3, 7, vb);
+            if (vb)
+                pr2ws("%s: param_list_len>0 but PF clear\n", __func__);
+            return 0;
+        } else
+            return 0;     /* nothing to do */
+        if (dout_len > 0) {
+            if (vb)
+                pr2ws("%s: dout given but PF clear\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+    }
+    if (dout_len < 4) {
+        if (vb)
+            pr2ws("%s: dout length (%u bytes) too short\n", __func__,
+                  dout_len);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    n = dout_len;
+    n = (n < alloc_len) ? n : alloc_len;
+    dop = (const uint8_t *)ptp->dxferp;
+    if (! sg_is_aligned(dop, 0)) {
+        if (vb)
+            pr2ws("%s: dout [0x%" PRIx64 "] not page aligned\n", __func__,
+                  (uint64_t)ptp->dxferp);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    dpg_cd = dop[0];
+    dpg_len = sg_get_unaligned_be16(dop + 2) + 4;
+    /* should we allow for more than one D_PG is dout ?? */
+    n = (n < dpg_len) ? n : dpg_len;    /* not yet ... */
+
+    if (vb)
+        pr2ws("%s: passing through d_pg=0x%x, len=%u to NVME_MI SES send\n",
+              __func__, dpg_cd, dpg_len);
+    memset(npc_up, 0, sizeof(npc));
+    npc_up[SG_NVME_PT_OPCODE] = SG_NVME_AD_MI_SEND;
+    sg_put_unaligned_le64((sg_uintptr_t)ptp->dxferp,
+                          npc_up + SG_NVME_PT_ADDR);
+    /* NVMe 4k page size. Maybe determine this? */
+    /* dout_len > 0x1000, is this a problem?? */
+    sg_put_unaligned_le32(0x1000, npc_up + SG_NVME_PT_DATA_LEN);
+    /* NVMe Message Header */
+    sg_put_unaligned_le32(0x0804, npc_up + SG_NVME_PT_CDW10);
+    /* nvme_mi_ses_send; (0x8 -> mi_ses_recv) */
+    sg_put_unaligned_le32(0x9, npc_up + SG_NVME_PT_CDW11);
+    /* data-out length I hope */
+    sg_put_unaligned_le32(n, npc_up + SG_NVME_PT_CDW13);
+    err = nvme_pt_low(ptp, ptp->dxferp, 0x1000, true, false, &npc, time_secs,
+                      vb);
+do_low:
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n",
+                      __func__, strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    return 0;
+}
+
+/* This is not really a SNTL. For SCSI RECEIVE DIAGNOSTIC RESULTS(PCV=1)
+ * NVMe-MI has a special command (SES Receive) to read pages through a
+ * tunnel from an enclosure. The NVMe enclosure is meant to understand the
+ * SES (SCSI Enclosure Services) use of diagnostics pages that are
+ * related to SES. */
+static int
+sntl_recvdiag(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+              int time_secs, int vb)
+{
+    bool pcv;
+    int err;
+    uint8_t dpg_cd;
+    uint32_t alloc_len, n, din_len;
+    const uint8_t * dip;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+
+    pcv = !! (0x1 & cdbp[1]);
+    dpg_cd = cdbp[2];
+    alloc_len = sg_get_unaligned_be16(cdbp + 3); /* parameter list length */
+    if (vb > 5)
+        pr2ws("%s: dpg_cd=0x%x, pcv=%d, alloc_len=0x%x\n", __func__,
+              dpg_cd, (int)pcv, alloc_len);
+    fdc_p = get_fdc_p(ptp);
+    if (NULL == fdc_p) {
+        if (vb)
+            pr2ws("%s: get_fdc_p() failed, no file descriptor ?\n", __func__);
+        return -EINVAL;
+    }
+    din_len = ptp->dxfer_len;
+    if (pcv) {
+        if (0 == alloc_len) {
+            /* T10 says not an error, hmmm */
+            mk_sense_invalid_fld(ptp, true, 3, 7, vb);
+            if (vb)
+                pr2ws("%s: PCV bit set bit but alloc_len=0\n", __func__);
+            return 0;
+        }
+    } else {    /* PCV bit clear */
+        if (alloc_len) {
+            mk_sense_invalid_fld(ptp, true, 3, 7, vb);
+            if (vb)
+                pr2ws("%s: alloc_len>0 but PCV clear\n", __func__);
+            return 0;
+        } else
+            return 0;     /* nothing to do */
+        if (din_len > 0) {
+            if (vb)
+                pr2ws("%s: din given but PCV clear\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+    }
+    n = din_len;
+    n = (n < alloc_len) ? n : alloc_len;
+    dip = (const uint8_t *)ptp->dxferp;
+    if (! sg_is_aligned(dip, 0)) {
+        if (vb)
+            pr2ws("%s: din [0x%" PRIx64 "] not page aligned\n", __func__,
+                  (uint64_t)ptp->dxferp);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+
+    if (vb)
+        pr2ws("%s: expecting d_pg=0x%x from NVME_MI SES receive\n", __func__,
+              dpg_cd);
+    memset(npc_up, 0, sizeof(npc));
+    npc_up[SG_NVME_PT_OPCODE] = SG_NVME_AD_MI_RECEIVE;
+    sg_put_unaligned_le64((sg_uintptr_t)ptp->dxferp,
+                          npc_up + SG_NVME_PT_ADDR);
+    /* NVMe 4k page size. Maybe determine this? */
+    /* dout_len > 0x1000, is this a problem?? */
+    sg_put_unaligned_le32(0x1000, npc_up + SG_NVME_PT_DATA_LEN);
+    /* NVMe Message Header */
+    sg_put_unaligned_le32(0x0804, npc_up + SG_NVME_PT_CDW10);
+    /* nvme_mi_ses_receive */
+    sg_put_unaligned_le32(0x8, npc_up + SG_NVME_PT_CDW11);
+    sg_put_unaligned_le32(dpg_cd, npc_up + SG_NVME_PT_CDW12);
+    /* data-in length I hope */
+    sg_put_unaligned_le32(n, npc_up + SG_NVME_PT_CDW13);
+    err = nvme_pt_low(ptp, ptp->dxferp, 0x1000, true, true, &npc, time_secs,
+                      vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n",
+                      __func__, strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    ptp->resid = din_len - n;
+    return 0;
+}
+
+#define F_SA_LOW                0x80    /* cdb byte 1, bits 4 to 0 */
+#define F_SA_HIGH               0x100   /* as used by variable length cdbs */
+#define FF_SA (F_SA_HIGH | F_SA_LOW)
+#define F_INV_OP                0x200
+
+static int
+sntl_rep_opcodes(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+                 int time_secs, int vb)
+{
+    bool rctd;
+    uint8_t reporting_opts, req_opcode, supp;
+    uint16_t req_sa;
+    uint32_t alloc_len, offset, a_len;
+    uint32_t pg_sz = sg_get_page_size();
+    int len, count, bump;
+    const struct sg_opcode_info_t *oip;
+    uint8_t *arr;
+    uint8_t *free_arr;
+
+    if (vb > 5)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    rctd = !!(cdbp[2] & 0x80);      /* report command timeout desc. */
+    reporting_opts = cdbp[2] & 0x7;
+    req_opcode = cdbp[3];
+    req_sa = sg_get_unaligned_be16(cdbp + 4);
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (alloc_len < 4 || alloc_len > 0xffff) {
+        mk_sense_invalid_fld(ptp, true, 6, -1, vb);
+        return 0;
+    }
+    a_len = pg_sz - 72;
+    arr = sg_memalign(pg_sz, pg_sz, &free_arr, vb > 3);
+    if (NULL == arr) {
+        if (vb)
+            pr2ws("%s: calloc() failed to get memory\n", __func__);
+        return -ENOMEM;
+    }
+    switch (reporting_opts) {
+    case 0: /* all commands */
+        count = 0;
+        bump = rctd ? 20 : 8;
+        for (offset = 4, oip = sg_get_opcode_translation();
+             (oip->flags != 0xffff) && (offset < a_len); ++oip) {
+            if (F_INV_OP & oip->flags)
+                continue;
+            ++count;
+            arr[offset] = oip->opcode;
+            sg_put_unaligned_be16(oip->sa, arr + offset + 2);
+            if (rctd)
+                arr[offset + 5] |= 0x2;
+            if (FF_SA & oip->flags)
+                arr[offset + 5] |= 0x1;
+            sg_put_unaligned_be16(oip->len_mask[0], arr + offset + 6);
+            if (rctd)
+                sg_put_unaligned_be16(0xa, arr + offset + 8);
+            offset += bump;
+        }
+        sg_put_unaligned_be32(count * bump, arr + 0);
+        break;
+    case 1: /* one command: opcode only */
+    case 2: /* one command: opcode plus service action */
+    case 3: /* one command: if sa==0 then opcode only else opcode+sa */
+        for (oip = sg_get_opcode_translation(); oip->flags != 0xffff; ++oip) {
+            if ((req_opcode == oip->opcode) && (req_sa == oip->sa))
+                break;
+        }
+        if ((0xffff == oip->flags) || (F_INV_OP & oip->flags)) {
+            supp = 1;
+            offset = 4;
+        } else {
+            if (1 == reporting_opts) {
+                if (FF_SA & oip->flags) {
+                    mk_sense_invalid_fld(ptp, true, 2, 2, vb);
+                    free(free_arr);
+                    return 0;
+                }
+                req_sa = 0;
+            } else if ((2 == reporting_opts) && 0 == (FF_SA & oip->flags)) {
+                mk_sense_invalid_fld(ptp, true, 4, -1, vb);
+                free(free_arr);
+                return 0;
+            }
+            if ((0 == (FF_SA & oip->flags)) && (req_opcode == oip->opcode))
+                supp = 3;
+            else if (0 == (FF_SA & oip->flags))
+                supp = 1;
+            else if (req_sa != oip->sa)
+                supp = 1;
+            else
+                supp = 3;
+            if (3 == supp) {
+                uint16_t u = oip->len_mask[0];
+                int k;
+
+                sg_put_unaligned_be16(u, arr + 2);
+                arr[4] = oip->opcode;
+                for (k = 1; k < u; ++k)
+                    arr[4 + k] = (k < 16) ?
+                oip->len_mask[k] : 0xff;
+                offset = 4 + u;
+            } else
+                offset = 4;
+        }
+        arr[1] = (rctd ? 0x80 : 0) | supp;
+        if (rctd) {
+            sg_put_unaligned_be16(0xa, arr + offset);
+            offset += 12;
+        }
+        break;
+    default:
+        mk_sense_invalid_fld(ptp, true, 2, 2, vb);
+        free(free_arr);
+        return 0;
+    }
+    offset = (offset < a_len) ? offset : a_len;
+    len = (offset < alloc_len) ? offset : alloc_len;
+    ptp->resid = ptp->dxfer_len - (int)len;
+    if (len > 0)
+        memcpy((uint8_t *)ptp->dxferp, arr, len);
+    free(free_arr);
+    return 0;
+}
+
+static int
+sntl_rep_tmfs(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+              int time_secs, int vb)
+{
+    bool repd;
+    uint32_t alloc_len, len;
+    uint8_t arr[16];
+
+    if (vb > 5)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    memset(arr, 0, sizeof(arr));
+    repd = !!(cdbp[2] & 0x80);
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (alloc_len < 4) {
+        mk_sense_invalid_fld(ptp, true, 6, -1, vb);
+        return 0;
+    }
+    arr[0] = 0xc8;          /* ATS | ATSS | LURS */
+    arr[1] = 0x1;           /* ITNRS */
+    if (repd) {
+        arr[3] = 0xc;
+        len = 16;
+    } else
+        len = 4;
+
+    len = (len < alloc_len) ? len : alloc_len;
+    ptp->resid = ptp->dxfer_len - (int)len;
+    if (len > 0)
+        memcpy((uint8_t *)ptp->dxferp, arr, len);
+    return 0;
+}
+
+static int
+sntl_rread(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+           int time_secs, int vb)
+{
+    bool is_read10 = (SCSI_READ10_OPC == cdbp[0]);
+    bool have_fua = !!(cdbp[1] & 0x8);
+    int err;
+    uint32_t nblks_t10 = 0;     /* 'control' in upper 16 bits */
+    uint64_t lba;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+
+    if (vb > 5)
+        pr2ws("%s: fua=%d\n", __func__, (int)have_fua);
+    fdc_p = get_fdc_p(ptp);
+    memset(&npc, 0, sizeof(npc));
+    npc.cmd.opc = SG_NVME_NVM_READ;
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+    if (is_read10) {
+        lba = sg_get_unaligned_be32(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be16(cdbp + 7);
+    } else {
+        lba = sg_get_unaligned_be64(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be32(cdbp + 10);
+        if (nblks_t10 > (UINT16_MAX + 1)) {
+            mk_sense_invalid_fld(ptp, true, 11, -1, vb);
+            return 0;
+        }
+    }
+    if (0 == nblks_t10) {         /* NOP in SCSI */
+        if (vb > 4)
+            pr2ws("%s: nblks_t10 is 0, a NOP in SCSI, can't map to NVMe\n",
+                  __func__);
+        return 0;
+    }
+    --nblks_t10;       /* crazy "0's based" counts */
+    sg_put_unaligned_le64(lba, npc_up + SG_NVME_PT_CDW10); /* fills W11 too */
+    if (have_fua)
+        nblks_t10 |= SG_NVME_RW_CDW12_FUA;
+    sg_put_unaligned_le32(nblks_t10, npc_up + SG_NVME_PT_CDW12);
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+
+    err = nvme_pt_low(ptp, ptp->dxferp, ptp->dxfer_len, false, true, &npc,
+                      time_secs, vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n",
+                      __func__, strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    ptp->resid = 0;     /* hoping */
+    return 0;
+}
+
+static int
+sntl_write(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+           int time_secs, int vb)
+{
+    bool is_write10 = (SCSI_WRITE10_OPC == cdbp[0]);
+    bool have_fua = !!(cdbp[1] & 0x8);
+    int err;
+    uint32_t nblks_t10 = 0;
+    uint64_t lba;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+
+    if (vb > 5)
+        pr2ws("%s: fua=%d, time_secs=%d\n", __func__, (int)have_fua,
+              time_secs);
+    fdc_p = get_fdc_p(ptp);
+    memset(&npc, 0, sizeof(npc));
+    npc.cmd.opc = SG_NVME_NVM_WRITE;
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+    if (is_write10) {
+        lba = sg_get_unaligned_be32(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be16(cdbp + 7);
+    } else {
+        lba = sg_get_unaligned_be64(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be32(cdbp + 10);
+        if (nblks_t10 > (UINT16_MAX + 1)) {
+            mk_sense_invalid_fld(ptp, true, 11, -1, vb);
+            return 0;
+        }
+    }
+    if (0 == nblks_t10) { /* NOP in SCSI */
+        if (vb > 4)
+            pr2ws("%s: nblks_t10 is 0, a NOP in SCSI, can't map to NVMe\n",
+                  __func__);
+        return 0;
+    }
+    --nblks_t10;
+    sg_put_unaligned_le64(lba, npc_up + SG_NVME_PT_CDW10); /* fills W11 too */
+    if (have_fua)
+        nblks_t10 |= SG_NVME_RW_CDW12_FUA;
+    sg_put_unaligned_le32(nblks_t10, npc_up + SG_NVME_PT_CDW12);
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+
+    err = nvme_pt_low(ptp, ptp->dxferp, ptp->dxfer_len, false, false, &npc,
+                      time_secs, vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n",
+                      __func__, strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    ptp->resid = 0;
+    return 0;
+}
+
+static int
+sntl_verify(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+           int time_secs, int vb)
+{
+    bool is_verify10 = (SCSI_VERIFY10_OPC == cdbp[0]);
+    uint8_t bytchk = (cdbp[1] >> 1) & 0x3;
+    int err;
+    uint32_t nblks_t10 = 0;
+    uint64_t lba;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+
+    if (vb > 5)
+        pr2ws("%s: bytchk=%d, time_secs=%d\n", __func__, bytchk, time_secs);
+    if (bytchk > 1) {
+        mk_sense_invalid_fld(ptp, true, 1, 2, vb);
+        return 0;
+    }
+    fdc_p = get_fdc_p(ptp);
+    memset(&npc, 0, sizeof(npc));
+    npc.cmd.opc = bytchk ? SG_NVME_NVM_COMPARE : SG_NVME_NVM_VERIFY;
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+    if (is_verify10) {
+        lba = sg_get_unaligned_be32(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be16(cdbp + 7);
+    } else {
+        lba = sg_get_unaligned_be64(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be32(cdbp + 10);
+        if (nblks_t10 > (UINT16_MAX + 1)) {
+            mk_sense_invalid_fld(ptp, true, 11, -1, vb);
+            return 0;
+        }
+    }
+    if (0 == nblks_t10) { /* NOP in SCSI */
+        if (vb > 4)
+            pr2ws("%s: nblks_t10 is 0, a NOP in SCSI, can't map to NVMe\n",
+                  __func__);
+        return 0;
+    }
+    --nblks_t10;
+    sg_put_unaligned_le64(lba, npc_up + SG_NVME_PT_CDW10); /* fills W11 too */
+    sg_put_unaligned_le32(nblks_t10, npc_up + SG_NVME_PT_CDW12);
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+
+    err = nvme_pt_low(ptp, ptp->dxferp, ptp->dxfer_len, false, false, &npc,
+                      time_secs, vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n",
+                      __func__, strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    ptp->resid = 0;
+    return 0;
+}
+
+static int
+sntl_write_same(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+                int time_secs, int vb)
+{
+    bool is_ws10 = (SCSI_WRITE_SAME10_OPC == cdbp[0]);
+    bool ndob = is_ws10 ? false : !!(0x1 & cdbp[1]);
+    int err;
+    int nblks_t10 = 0;
+    uint64_t lba;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+
+    if (vb > 5)
+        pr2ws("%s: ndob=%d, time_secs=%d\n", __func__, (int)ndob, time_secs);
+    if (! ndob) {
+        int flbas, index, lbafx, lbads, lbsize;
+        uint8_t * up;
+        uint8_t * dp;
+
+        dp = ptp->dxferp;
+        up = ptp->mchanp->nvme_id_ctlp;
+        if ((dp == NULL) || (up == NULL))
+            return sg_convert_errno(ENOMEM);
+        flbas = up[26];     /* NVME FLBAS field from Identify */
+        index = 128 + (4 * (flbas & 0xf));
+        lbafx = sg_get_unaligned_le32(up + index);
+        lbads = (lbafx >> 16) & 0xff;  /* bits 16 to 23 inclusive, pow2 */
+        lbsize = 1 << lbads;
+        if (! sg_all_zeros(dp, lbsize)) {
+            mk_sense_asc_ascq(ptp, SPC_SK_ILLEGAL_REQUEST, PCIE_ERR_ASC,
+                              PCIE_UNSUPP_REQ_ASCQ, vb);
+            return 0;
+        }
+        /* so given single LB full of zeros, can translate .... */
+    }
+    fdc_p = ptp->mchanp;
+    memset(&npc, 0, sizeof(npc));
+    npc.cmd.opc = SG_NVME_NVM_WRITE_ZEROES;
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+    if (is_ws10) {
+        lba = sg_get_unaligned_be32(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be16(cdbp + 7);
+    } else {
+        uint32_t num = sg_get_unaligned_be32(cdbp + 10);
+
+        lba = sg_get_unaligned_be64(cdbp + 2);
+        if (num > (UINT16_MAX + 1)) {
+            mk_sense_invalid_fld(ptp, true, 11, -1, vb);
+            return 0;
+        } else
+            nblks_t10 = num;
+    }
+    if (0 == nblks_t10) { /* NOP in SCSI */
+        if (vb > 4)
+            pr2ws("%s: nblks_t10 is 0, a NOP in SCSI, can't map to NVMe\n",
+                  __func__);
+        return 0;
+    }
+    --nblks_t10;
+    sg_put_unaligned_le64(lba, npc_up + SG_NVME_PT_CDW10); /* fills W11 too */
+    sg_put_unaligned_le32(nblks_t10, npc_up + SG_NVME_PT_CDW12);
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+
+    err = nvme_pt_low(ptp, ptp->dxferp, ptp->dxfer_len, false, false, &npc,
+                      time_secs, vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n",
+                      __func__, strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    ptp->resid = 0;
+    return 0;
+}
+
+static int
+sntl_sync_cache(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+                int time_secs, int vb)
+{
+    bool immed = !!(0x2 & cdbp[1]);
+    int err;
+    struct nvme_pt_command npc;
+    uint8_t * npc_up = (uint8_t *)&npc;
+    struct freebsd_dev_channel * fdc_p;
+
+    if (vb > 5)
+        pr2ws("%s: immed=%d, time_secs=%d\n", __func__, (int)immed,
+              time_secs);
+    fdc_p = ptp->mchanp;
+    memset(&npc, 0, sizeof(npc));
+    npc.cmd.opc = SG_NVME_NVM_FLUSH;
+    sg_put_unaligned_le32(fdc_p->nsid, npc_up + SG_NVME_PT_NSID);
+    if (vb > 4)
+        pr2ws("%s: immed bit, lba and num_lbs fields ignored\n", __func__);
+    err = nvme_pt_low(ptp, ptp->dxferp, ptp->dxfer_len, false, false, &npc,
+                      time_secs, vb);
+    if (err) {
+        if (err < 0) {
+            if (vb > 1)
+                pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n",
+                      __func__, strerror(-err), -err);
+            return err;
+        } else {
+            ptp->nvme_status = err;
+            mk_sense_from_nvme_status(ptp, err, vb);
+            return 0;
+        }
+    }
+    ptp->resid = 0;
+    return 0;
+}
+
+static int
+sntl_start_stop(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+                int time_secs, int vb)
+{
+    bool immed = !!(0x1 & cdbp[1]);
+
+    if (vb > 5)
+        pr2ws("%s: immed=%d, time_secs=%d, ignore\n", __func__, (int)immed,
+              time_secs);
+    if (ptp) { }        /* suppress warning */
+    return 0;
+}
+
+/* Note that the "Returned logical block address" (RLBA) field in the SCSI
+ * READ CAPACITY (10+16) command's response provides the address of the _last_
+ * LBA (counting origin 0) which will be one less that the "size" in the
+ * NVMe Identify command response's NSZE field. One problem is that in
+ * some situations NSZE can be zero: temporarily set RLBA field to 0
+ * (implying a 1 LB logical units size) pending further research. The LBLIB
+ * is the "Logical Block Length In Bytes" field in the RCAP response. */
+static int
+sntl_readcap(struct sg_pt_freebsd_scsi * ptp, const uint8_t * cdbp,
+             int time_secs, int vb)
+{
+    bool is_rcap10 = (SCSI_READ_CAPACITY10_OPC == cdbp[0]);
+    int res, n, len, alloc_len, dps;
+    uint8_t flbas, index, lbads;  /* NVMe: 2**LBADS --> Logical Block size */
+    uint32_t lbafx;     /* NVME: LBAF0...LBAF15, each 16 bytes */
+    uint32_t pg_sz = sg_get_page_size();
+    uint64_t nsze;
+    uint8_t * bp;
+    uint8_t * up;
+    uint8_t * free_up = NULL;
+    struct freebsd_dev_channel * fdc_p;
+    uint8_t resp[32];
+
+    if (vb > 5)
+        pr2ws("%s: RCAP%d\n", __func__, (is_rcap10 ? 10 : 16));
+    fdc_p = ptp->mchanp;
+    if (NULL == fdc_p) {
+        if (vb)
+            pr2ws("%s: get_fdc_p() failed, no file descriptor ?\n", __func__);
+        return -EINVAL;
+    }
+    up = sg_memalign(pg_sz, pg_sz, &free_up, false);
+    if (NULL == up) {
+        if (vb)
+            pr2ws("%s: sg_memalign() failed to get memory\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    res = sntl_do_identify(ptp, 0x0 /* CNS */, fdc_p->nsid, pg_sz, up,
+                           time_secs, vb);
+    if (res < 0) {
+        res = sg_convert_errno(-res);
+        goto fini;
+    }
+    memset(resp, 0, sizeof(resp));
+    nsze = sg_get_unaligned_le64(up + 0);
+    flbas = up[26];     /* NVME FLBAS field from Identify, want LBAF[flbas] */
+    index = 128 + (4 * (flbas & 0xf));
+    lbafx = sg_get_unaligned_le32(up + index);
+    lbads = (lbafx >> 16) & 0xff;       /* bits 16 to 23 inclusive, pow2 */
+    if (is_rcap10) {
+        alloc_len = 8;  /* implicit, not in cdb */
+        if (nsze > 0xffffffff)
+            sg_put_unaligned_be32(0xffffffff, resp + 0);
+        else if (0 == nsze)     /* no good answer here */
+            sg_put_unaligned_be32(0, resp + 0);         /* SCSI RLBA field */
+        else
+            sg_put_unaligned_be32((uint32_t)(nsze - 1), resp + 0);
+        sg_put_unaligned_be32(1 << lbads, resp + 4);    /* SCSI LBLIB field */
+    } else {
+        alloc_len = sg_get_unaligned_be32(cdbp + 10);
+        dps = up[29];
+        if (0x7 & dps) {
+            resp[12] = 0x1;
+            n = (0x7 & dps) - 1;
+            if (n > 0)
+                resp[12] |= (n + n);
+        }
+        if (0 == nsze)  /* no good answer here */
+            sg_put_unaligned_be64(0, resp + 0);
+        else
+            sg_put_unaligned_be64(nsze - 1, resp + 0);
+        sg_put_unaligned_be32(1 << lbads, resp + 8);    /* SCSI LBLIB field */
+    }
+    len = ptp->dxfer_len;
+    bp = ptp->dxferp;
+    n = 32;
+    n = (n < alloc_len) ? n : alloc_len;
+    n = (n < len) ? n : len;
+    ptp->resid = len - n;
+    if (n > 0)
+        memcpy(bp, resp, n);
+fini:
+    if (free_up)
+        free(free_up);
+    return res;
+}
+
+/* Executes NVMe Admin command (or at least forwards it to lower layers).
+ * Depending on the device, this could be NVME(via CAM) or NVME(non-CAM).
+ * is_admin will be overridden if the SNTL functions are called.
+ * Returns 0 for success, negative numbers are negated 'errno' values from
+ * OS system calls. Positive return values are errors from this package. */
+static int
+sg_do_nvme_pt(struct sg_pt_freebsd_scsi * ptp, int fd, bool is_admin,
+              int time_secs, int vb)
+{
+    bool scsi_cdb, in_xfer;
+    int n, err, len, io_len;
+    uint16_t sct_sc, sa;
+    uint8_t * dxferp;
+    uint8_t * npc_up;
+    struct freebsd_dev_channel * fdc_p;
+    const uint8_t * cdbp;
+    struct nvme_pt_command npc;
+
+    npc_up = (uint8_t *)&npc;
+    if (vb > 6)
+        pr2ws("%s: fd=%d, is_admin=%d\n", __func__, fd, (int)is_admin);
+    if (! ptp->cdb) {
+        if (vb)
+            pr2ws("%s: No NVMe command given (set_scsi_pt_cdb())\n",
+                  __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    fdc_p = ptp->mchanp;
+    if (fd < 0) {
+        if (NULL == fdc_p) {
+            if (vb)
+                pr2ws("%s: no device handle in object or fd ?\n", __func__);
+            return -EINVAL;
+        }
+        /* no fd, but have fdc_p so that is okay */
+    } else {
+        int han = fd - FREEBSD_FDOFFSET;
+
+        if ((han < 0) || (han >= FREEBSD_MAXDEV)) {
+            if (vb)
+                pr2ws("%s: argument 'fd' is bad\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        if (NULL == devicetable[han]) {
+            if (vb)
+                pr2ws("%s: argument 'fd' is bad (2)\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        if (fdc_p && (fdc_p != devicetable[han])) {
+            if (vb)
+                pr2ws("%s: different device handle in object and fd ?\n",
+                      __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        if (NULL == fdc_p) {
+            ptp->dev_han = fd;
+            fdc_p = devicetable[han];
+        }
+    }
+
+    ptp->is_nvme_dev = fdc_p->is_nvme_dev;
+    n = ptp->cdb_len;
+    cdbp = (const uint8_t *)ptp->cdb;
+    if (vb > 3)
+        pr2ws("%s: opcode=0x%x, fd=%d\n", __func__, cdbp[0], fd);
+    scsi_cdb = sg_is_scsi_cdb(cdbp, n);
+    /* nvme_our_sntl is false when NVMe command (64 byte) has been given */
+    ptp->nvme_our_sntl = scsi_cdb;
+    if (scsi_cdb) {
+        switch (cdbp[0]) {
+        case SCSI_INQUIRY_OPC:
+            return sntl_inq(ptp, cdbp, time_secs, vb);
+        case SCSI_REPORT_LUNS_OPC:
+            return sntl_rluns(ptp, cdbp, time_secs, vb);
+        case SCSI_TEST_UNIT_READY_OPC:
+            return sntl_tur(ptp, time_secs, vb);
+        case SCSI_REQUEST_SENSE_OPC:
+            return sntl_req_sense(ptp, cdbp, time_secs, vb);
+        case SCSI_READ10_OPC:
+        case SCSI_READ16_OPC:
+            return sntl_rread(ptp, cdbp, time_secs, vb);
+        case SCSI_WRITE10_OPC:
+        case SCSI_WRITE16_OPC:
+            return sntl_write(ptp, cdbp, time_secs, vb);
+        case SCSI_START_STOP_OPC:
+            return sntl_start_stop(ptp, cdbp, time_secs, vb);
+        case SCSI_SEND_DIAGNOSTIC_OPC:
+            return sntl_senddiag(ptp, cdbp, time_secs, vb);
+        case SCSI_RECEIVE_DIAGNOSTIC_OPC:
+            return sntl_recvdiag(ptp, cdbp, time_secs, vb);
+        case SCSI_MODE_SENSE10_OPC:
+        case SCSI_MODE_SELECT10_OPC:
+            return sntl_mode_ss(ptp, cdbp, time_secs, vb);
+        case SCSI_READ_CAPACITY10_OPC:
+            return sntl_readcap(ptp, cdbp, time_secs, vb);
+        case SCSI_VERIFY10_OPC:
+        case SCSI_VERIFY16_OPC:
+            return sntl_verify(ptp, cdbp, time_secs, vb);
+        case SCSI_WRITE_SAME10_OPC:
+        case SCSI_WRITE_SAME16_OPC:
+            return sntl_write_same(ptp, cdbp, time_secs, vb);
+        case SCSI_SYNC_CACHE10_OPC:
+        case SCSI_SYNC_CACHE16_OPC:
+            return sntl_sync_cache(ptp, cdbp, time_secs, vb);
+        case SCSI_SERVICE_ACT_IN_OPC:
+            if (SCSI_READ_CAPACITY16_SA == (cdbp[1] & SCSI_SA_MSK))
+                return sntl_readcap(ptp, cdbp, time_secs, vb);
+            goto fini;
+        case SCSI_MAINT_IN_OPC:
+            sa = SCSI_SA_MSK & cdbp[1];        /* service action */
+            if (SCSI_REP_SUP_OPCS_OPC == sa)
+                return sntl_rep_opcodes(ptp, cdbp, time_secs, vb);
+            else if (SCSI_REP_SUP_TMFS_OPC == sa)
+                return sntl_rep_tmfs(ptp, cdbp, time_secs, vb);
+            /* fall through */
+        default:
+fini:
+            if (vb > 2) {
+                char b[64];
+
+                sg_get_command_name(cdbp, -1, sizeof(b), b);
+                pr2ws("%s: no translation to NVMe for SCSI %s command\n",
+                      __func__, b);
+            }
+            mk_sense_asc_ascq(ptp, SPC_SK_ILLEGAL_REQUEST, INVALID_OPCODE,
+                              0, vb);
+            return 0;
+        }
+    }
+
+    /* NVMe command given to pass-through */
+    if (vb > 4)
+        pr2ws("%s: NVMe pass-through command, admin=%d\n", __func__,
+              is_admin);
+    len = (int)sizeof(npc.cmd);
+    n = (n < len) ? n : len;
+    if (n < 64) {
+        if (vb)
+            pr2ws("%s: command length of %d bytes is too short\n", __func__,
+                  n);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    memcpy(npc_up, (const uint8_t *)ptp->cdb, n);
+    if (n < len)        /* zero out rest of 'npc' */
+        memset(npc_up + n, 0, len - n);
+    in_xfer = false;
+    io_len = 0;
+    dxferp = NULL;
+    if (ptp->dxfer_ilen > 0) {
+        in_xfer = true;
+        io_len = ptp->dxfer_ilen;
+        dxferp = ptp->dxferip;
+        sg_put_unaligned_le32(ptp->dxfer_ilen, npc_up + SG_NVME_PT_DATA_LEN);
+        sg_put_unaligned_le64((sg_uintptr_t)ptp->dxferip,
+                              npc_up + SG_NVME_PT_ADDR);
+    } else if (ptp->dxfer_olen > 0) {
+        in_xfer = false;
+        io_len = ptp->dxfer_olen;
+        dxferp = ptp->dxferop;
+        sg_put_unaligned_le32(ptp->dxfer_olen, npc_up + SG_NVME_PT_DATA_LEN);
+        sg_put_unaligned_le64((sg_uintptr_t)ptp->dxferop,
+                              npc_up + SG_NVME_PT_ADDR);
+    }
+    err = nvme_pt_low(ptp, dxferp, io_len, is_admin, in_xfer, &npc, time_secs,
+                      vb);
+    if (err < 0) {
+        if (vb > 1)
+            pr2ws("%s: nvme_pt_low() failed: %s (errno=%d)\n",
+                  __func__, strerror(-err), -err);
+        return err;
+    }
+    sct_sc = err;       /* ((SCT << 8) | SC) which may be 0 */
+    ptp->nvme_status = sct_sc;
+    if (ptp->sense && (ptp->sense_len > 0)) {
+        uint32_t k = sizeof(ptp->cq_dw0_3);
+
+        if ((int)k < ptp->sense_len)
+            ptp->sense_resid = ptp->sense_len - (int)k;
+        else {
+            k = ptp->sense_len;
+            ptp->sense_resid = 0;
+        }
+        memcpy(ptp->sense, ptp->cq_dw0_3, k);
+    }
+    if (in_xfer)
+        ptp->resid = 0; /* Just hoping ... */
+    return sct_sc ? SG_LIB_NVME_STATUS : 0;
+}
+
+#endif          /* (HAVE_NVME && (! IGNORE_NVME)) */
+
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+
+/* Requires pass-through file to be open and associated with vp */
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int vb)
+{
+    struct sg_pt_freebsd_scsi * ptp = &vp->impl;
+    struct freebsd_dev_channel *fdc_p;
+
+    if (vb && (submq != 0))
+        pr2ws("%s: warning, uses submit queue 0\n", __func__);
+    fdc_p = ptp->mchanp;
+    if (NULL == fdc_p) {
+        fdc_p = get_fdc_p(ptp);
+        if (NULL == fdc_p) {
+            if (vb > 2)
+                pr2ws("%s: no open file associated with pt object\n",
+                      __func__);
+            return -EINVAL;
+        }
+        ptp->mchanp = fdc_p;
+    }
+    return sg_do_nvme_pt(ptp, -1, false, timeout_secs, vb);
+}
+
+#else           /* (HAVE_NVME && (! IGNORE_NVME)) */
+
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int vb)
+{
+    if (vb) {
+        pr2ws("%s: not supported, ", __func__);
+#ifdef HAVE_NVME
+        pr2ws("HAVE_NVME, ");
+#else
+        pr2ws("don't HAVE_NVME, ");
+#endif
+
+#ifdef IGNORE_NVME
+        pr2ws("IGNORE_NVME");
+#else
+        pr2ws("don't IGNORE_NVME");
+#endif
+    }
+    if (vp) { }
+    if (submq) { }
+    if (timeout_secs) { }
+    return SCSI_PT_DO_NOT_SUPPORTED;
+}
+
+#endif          /* (HAVE_NVME && (! IGNORE_NVME)) */
diff --git a/lib/sg_pt_haiku.c b/lib/sg_pt_haiku.c
new file mode 100644
index 0000000..6b1ed22
--- /dev/null
+++ b/lib/sg_pt_haiku.c
@@ -0,0 +1,572 @@
+/*
+ * Copyright (c) 2017 Leorize.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <device/CAM.h>
+#include <scsi.h>
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+
+#if defined(__GNUC__) || defined(__clang__)
+static int pr2ws(const char * fmt, ...)
+        __attribute__ ((format (printf, 1, 2)));
+#else
+static int pr2ws(const char * fmt, ...);
+#endif
+
+static int
+pr2ws(const char * fmt, ...)
+{
+    va_list args;
+    int n;
+
+    va_start(args, fmt);
+    n = vfprintf(sg_warnings_strm ? sg_warnings_strm : stderr, fmt, args);
+    va_end(args);
+    return n;
+}
+
+struct sg_pt_haiku_scsi {
+    raw_device_command raw_command;
+    size_t data_len;
+    int in_err;
+    int os_err;
+    int dev_fd;
+};
+
+struct sg_pt_base {
+    struct sg_pt_haiku_scsi impl;
+};
+
+/* Returns >= 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_open_device(const char * device_name, bool read_only, int verbose)
+{
+    int oflags = O_NONBLOCK;
+
+    oflags |= (read_only ? O_RDONLY : O_RDWR);
+    return scsi_pt_open_flags(device_name, oflags, verbose);
+}
+
+/* Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed */
+/* together. The 'flags' argument is advisory and may be ignored. */
+/* Returns >= 0 if successful, otherwise returns negated errno. */
+int
+scsi_pt_open_flags(const char * device_name, int flags, int verbose)
+{
+    int fd;
+
+    if (verbose > 1) {
+        pr2ws("open %s with flags=0x%x\n", device_name, flags);
+    }
+    fd = open(device_name, flags);
+    if (fd < 0)
+        fd = -errno;
+    return fd;
+}
+
+/* Returns 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_close_device(int device_fd)
+{
+    int res;
+
+    res = close(device_fd);
+    if (res < 0)
+        res = -errno;
+    return res;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj_with_fd(int device_fd, int verbose)
+{
+    struct sg_pt_haiku_scsi * ptp;
+
+    /* The following 2 lines are temporary. It is to avoid a NULL pointer
+     * crash when an old utility is used with a newer library built after
+     * the sg_warnings_strm cleanup */
+    if (NULL == sg_warnings_strm)
+        sg_warnings_strm = stderr;
+
+    ptp = (struct sg_pt_haiku_scsi *)
+           calloc(1, sizeof(struct sg_pt_haiku_scsi));
+    if (ptp) {
+        ptp->raw_command.flags = B_RAW_DEVICE_REPORT_RESIDUAL;
+        ptp->dev_fd = device_fd;
+    } else if (verbose)
+        pr2ws("%s: malloc() out of memory\n", __func__);
+    return (struct sg_pt_base *)ptp;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj()
+{
+    return construct_scsi_pt_obj_with_fd(-1, 0);
+}
+
+void
+destruct_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (ptp)
+        free(ptp);
+}
+
+void
+clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (ptp) {
+
+        int fd = ptp->dev_fd;
+        memset(ptp, 0, sizeof(struct sg_pt_haiku_scsi));
+        ptp->dev_fd = fd;
+        ptp->raw_command.flags = B_RAW_DEVICE_REPORT_RESIDUAL;
+    }
+}
+
+void
+set_scsi_pt_cdb(struct sg_pt_base * vp, const unsigned char * cdb,
+                int cdb_len)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    for (int i = 0; i < 16; ++i)
+        if (ptp->raw_command.command[i])
+            ++ptp->in_err;
+    memcpy(ptp->raw_command.command, cdb, cdb_len);
+    ptp->raw_command.command_length = (uint8_t)cdb_len;
+}
+
+void
+set_scsi_pt_sense(struct sg_pt_base * vp, unsigned char * sense,
+                  int max_sense_len)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (ptp->raw_command.sense_data)
+        ++ptp->in_err;
+    memset(sense, 0, max_sense_len);
+    ptp->raw_command.sense_data = sense;
+    ptp->raw_command.sense_data_length = max_sense_len;
+}
+
+/* Setup for data transfer from device */
+void
+set_scsi_pt_data_in(struct sg_pt_base * vp, unsigned char * dxferp,
+                    int dxfer_len)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (ptp->raw_command.data)
+        ++ptp->in_err;
+    if (dxfer_len > 0) {
+        ptp->raw_command.data = dxferp;
+        ptp->raw_command.data_length = dxfer_len;
+        ptp->data_len = dxfer_len;
+        ptp->raw_command.flags |= B_RAW_DEVICE_DATA_IN;
+    }
+}
+
+/* Setup for data transfer toward device */
+void
+set_scsi_pt_data_out(struct sg_pt_base * vp, const unsigned char * dxferp,
+                     int dxfer_len)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (ptp->raw_command.data)
+        ++ptp->in_err;
+    if (dxfer_len > 0) {
+        ptp->raw_command.data = (unsigned char *)dxferp;
+        ptp->raw_command.data_length = dxfer_len;
+        ptp->data_len = dxfer_len;
+        ptp->raw_command.flags &= ~B_RAW_DEVICE_DATA_IN;
+    }
+}
+
+void
+set_scsi_pt_packet_id(struct sg_pt_base * vp __attribute__ ((unused)),
+                      int pack_id __attribute__ ((unused)))
+{
+}
+
+void
+set_scsi_pt_tag(struct sg_pt_base * vp, uint64_t tag __attribute__ ((unused)))
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_task_management(struct sg_pt_base * vp,
+                            int tmf_code __attribute__ ((unused)))
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_task_attr(struct sg_pt_base * vp,
+                      int attrib __attribute__ ((unused)),
+                      int priority __attribute__ ((unused)))
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_flags(struct sg_pt_base * vp __attribute__ ((unused)),
+                  int flags __attribute__ ((unused)))
+{
+}
+
+/* Executes SCSI command (or at least forwards it to lower layers).
+ * Clears os_err field prior to active call (whose result may set it
+ * again). */
+int
+do_scsi_pt(struct sg_pt_base * vp, int fd, int timeout_secs, int verbose)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    ptp->os_err = 0;
+    if (ptp->in_err) {
+        if (verbose)
+            pr2ws("Replicated or unused set_scsi_pt...\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (fd >= 0) {
+        if ((ptp->dev_fd >= 0) && (fd != ptp->dev_fd)) {
+            if (verbose)
+                pr2ws("%s: file descriptor given to create() and here "
+                      "differ\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        ptp->dev_fd = fd;
+    } else if (ptp->dev_fd < 0) {
+        if (verbose)
+            pr2ws("%s: invalid file descriptors\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    } else
+        fd = ptp->dev_fd;
+
+    if (NULL == ptp->raw_command.command) {
+        if (verbose)
+            pr2ws("No SCSI command (cdb) given\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    /* raw_command.timeout is in microseconds */
+    ptp->raw_command.timeout = ((timeout_secs > 0) ? (timeout_secs * 1000000) :
+                                                   CAM_TIME_DEFAULT);
+
+    if (ioctl(fd, B_RAW_DEVICE_COMMAND, &ptp->raw_command) < 0) {
+        ptp->os_err = errno;
+        if (verbose > 1)
+            pr2ws("ioctl(B_RAW_DEVICE_COMMAND) failed: %s (errno=%d)\n",
+                  safe_strerror(ptp->os_err), ptp->os_err);
+        return -ptp->os_err;
+    }
+
+    return SCSI_PT_DO_START_OK;
+}
+
+int
+get_scsi_pt_result_category(const struct sg_pt_base * vp)
+{
+    int cam_status_masked;
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (ptp->os_err)
+        return SCSI_PT_RESULT_OS_ERR;
+    cam_status_masked = ptp->raw_command.cam_status & CAM_STATUS_MASK;
+    if (cam_status_masked != CAM_REQ_CMP &&
+        cam_status_masked != CAM_REQ_CMP_ERR)
+        return SCSI_PT_RESULT_TRANSPORT_ERR;
+    else if ((SAM_STAT_CHECK_CONDITION == ptp->raw_command.scsi_status) ||
+             (SAM_STAT_COMMAND_TERMINATED == ptp->raw_command.scsi_status))
+        return SCSI_PT_RESULT_SENSE;
+    else if (ptp->raw_command.scsi_status)
+        return SCSI_PT_RESULT_STATUS;
+    else
+        return SCSI_PT_RESULT_GOOD;
+}
+
+int
+get_scsi_pt_resid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    /* For various reasons Haiku return data_len - data_resid */
+    return ptp->data_len - ptp->raw_command.data_length;
+}
+
+int
+get_scsi_pt_status_response(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    return ptp->raw_command.scsi_status;
+}
+
+uint8_t *
+get_scsi_pt_sense_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    return (uint8_t *)ptp->raw_command.sense_data;
+}
+
+int
+get_scsi_pt_sense_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    return ptp->raw_command.sense_data_length;
+}
+
+int
+get_scsi_pt_os_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    return ptp->os_err;
+}
+
+char *
+get_scsi_pt_os_err_str(const struct sg_pt_base * vp __attribute__ ((unused)),
+                       int max_b_len, char * b)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    const char *cp;
+
+    cp = safe_strerror(ptp->os_err);
+    strncpy(b, cp, max_b_len);
+    if ((int)strlen(cp) >= max_b_len)
+        b[max_b_len - 1] = '\0';
+    return b;
+}
+
+int
+get_scsi_pt_transport_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if ((ptp->raw_command.cam_status & CAM_STATUS_MASK) != CAM_REQ_CMP ||
+        (ptp->raw_command.cam_status & CAM_STATUS_MASK) != CAM_REQ_CMP_ERR)
+        return ptp->raw_command.cam_status & CAM_STATUS_MASK;
+
+    return 0;
+}
+
+char *
+get_scsi_pt_transport_err_str(
+                const struct sg_pt_base * vp __attribute__ ((unused)),
+                              int max_b_len, char * b)
+{
+    strncpy(b, "no transport error available", max_b_len);
+    b[max_b_len - 1] = '\0';
+    return b;
+}
+
+int
+get_scsi_pt_duration_ms(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    return -1;
+}
+
+void
+partial_clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (NULL == ptp)
+        return;
+    ptp->in_err = 0;
+    ptp->os_err = 0;
+    ptp->data_len = 0;
+    ptp->raw_command.cam_status = 0;
+    ptp->raw_command.data_length = 0;
+}
+
+bool pt_device_is_nvme(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    return 0;
+}
+
+int
+check_pt_file_handle(int device_fd, const char * device_name, int vb)
+{
+    if (device_fd) {}
+    if (device_name) {}
+    if (vb) {}
+    return 1;   /* guess it is a SCSI generic pass-though device */
+}
+
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int verbose)
+{
+    if (vp) { }
+    if (submq) { }
+    if (timeout_secs) { }
+    if (verbose) { }
+    return SCSI_PT_DO_NOT_SUPPORTED;
+}
+
+void
+get_pt_actual_lengths(const struct sg_pt_base * vp, int * act_dinp,
+                      int * act_doutp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (ptp->data_len == 0) {
+        if (act_dinp)
+            *act_dinp = 0;
+        if (act_doutp)
+            *act_doutp = 0;
+    } else if (act_dinp && (ptp->raw_command.flags & B_RAW_DEVICE_DATA_IN)) {
+        /* "For various reasons Haiku return data_len - data_resid" */
+        *act_dinp = ptp->raw_command.data_length;
+        if (act_doutp)
+            *act_doutp = 0;
+    } else if (act_doutp &&
+               !(ptp->raw_command.flags & B_RAW_DEVICE_DATA_IN)) {
+        *act_doutp = ptp->raw_command.data_length;
+        if (act_dinp)
+            *act_dinp = 0;
+    } else {
+        if (act_dinp)
+            *act_dinp = 0;
+        if (act_doutp)
+            *act_doutp = 0;
+    }
+}
+
+/* If not available return 0 otherwise return number of nanoseconds that the
+ * lower layers (and hardware) took to execute the command just completed. */
+uint64_t
+get_pt_duration_ns(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    return 0;
+}
+
+/* Valid file handles (which is the return value) are >= 0 . Returns -1
+ * if there is no valid file handle. */
+int
+get_pt_file_handle(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    return ptp->dev_fd;
+}
+
+
+/* If a NVMe block device (which includes the NSID) handle is associated
+ * with 'vp', then its NSID is returned (values range from 0x1 to
+ * 0xffffffe). Otherwise 0 is returned. */
+uint32_t
+get_pt_nvme_nsid(const struct sg_pt_base * vp)
+{
+    if (vp) { }
+    return 0;
+}
+
+void
+get_pt_req_lengths(const struct sg_pt_base * vp, int * req_dinp,
+                   int * req_doutp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (ptp->data_len == 0) {
+        if (req_dinp)
+            *req_dinp = 0;
+        if (req_doutp)
+            *req_doutp = 0;
+    } else if (req_dinp && (ptp->raw_command.flags & B_RAW_DEVICE_DATA_IN)) {
+        /* "For various reasons Haiku return data_len - data_resid" */
+        *req_dinp = ptp->data_len;
+        if (req_doutp)
+            *req_doutp = 0;
+    } else if (req_doutp &&
+               !(ptp->raw_command.flags & B_RAW_DEVICE_DATA_IN)) {
+        *req_doutp = ptp->data_len;
+        if (req_dinp)
+            *req_dinp = 0;
+    } else {
+        if (req_dinp)
+            *req_dinp = 0;
+        if (req_doutp)
+            *req_doutp = 0;
+    }
+}
+
+uint32_t
+get_pt_result(const struct sg_pt_base * vp)
+{
+    return (uint32_t)get_scsi_pt_status_response(vp);
+}
+
+uint8_t *
+get_scsi_pt_cdb_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    return (uint8_t *)ptp->raw_command.command;
+}
+
+int
+get_scsi_pt_cdb_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    return (int)ptp->raw_command.command_length;
+}
+
+/* Forget any previous dev_han and install the one given. May attempt to
+ * find file type (e.g. if pass-though) from OS so there could be an error.
+ * Returns 0 for success or the same value as get_scsi_pt_os_err()
+ * will return. dev_han should be >= 0 for a valid file handle or -1 . */
+int
+set_pt_file_handle(struct sg_pt_base * vp, int dev_han, int vb)
+{
+    struct sg_pt_haiku_scsi * ptp = &vp->impl;
+
+    if (vb) {}
+    ptp->dev_fd = (dev_han < 0) ? -1 : dev_han;
+    ptp->in_err = 0;
+    ptp->os_err = 0;
+    return 0;
+}
+
+void
+set_scsi_pt_transport_err(struct sg_pt_base * vp, int err)
+{
+    if (vp) { }
+    if (err) { }
+}
+
+void
+set_pt_metadata_xfer(struct sg_pt_base * vp, uint8_t * mdxferp,
+                     uint32_t mdxfer_len, bool out_true)
+{
+    if (vp) { }
+    if (mdxferp) { }
+    if (mdxfer_len) { }
+    if (out_true) { }
+}
diff --git a/lib/sg_pt_linux.c b/lib/sg_pt_linux.c
new file mode 100644
index 0000000..6d282f5
--- /dev/null
+++ b/lib/sg_pt_linux.c
@@ -0,0 +1,1181 @@
+/*
+ * Copyright (c) 2005-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/* sg_pt_linux version 1.54 20210923 */
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>      /* to define 'major' */
+#ifndef major
+#include <sys/types.h>
+#endif
+
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <linux/major.h>
+
+#include "sg_pt.h"
+#include "sg_lib.h"
+#include "sg_linux_inc.h"
+#include "sg_pt_linux.h"
+#include "sg_pr2serr.h"
+
+
+#ifdef major
+#define SG_DEV_MAJOR major
+#else
+#ifdef HAVE_LINUX_KDEV_T_H
+#include <linux/kdev_t.h>
+#endif
+#define SG_DEV_MAJOR MAJOR  /* MAJOR() macro faulty if > 255 minors */
+#endif
+
+#ifndef BLOCK_EXT_MAJOR
+#define BLOCK_EXT_MAJOR 259
+#endif
+
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs (60 seconds) */
+
+/* sg driver displayed format: [x]xyyzz --> [x]x.[y]y.zz */
+#define SG_LINUX_SG_VER_V4_BASE 40000   /* lowest sg driver version with
+                                         * v4 interface */
+#define SG_LINUX_SG_VER_V4_FULL 40030   /* lowest version with full v4
+                                         * interface */
+
+static const char * linux_host_bytes[] = {
+    "DID_OK", "DID_NO_CONNECT", "DID_BUS_BUSY", "DID_TIME_OUT",
+    "DID_BAD_TARGET", "DID_ABORT", "DID_PARITY", "DID_ERROR",
+    "DID_RESET", "DID_BAD_INTR", "DID_PASSTHROUGH", "DID_SOFT_ERROR",
+    "DID_IMM_RETRY", "DID_REQUEUE" /* 0xd */,
+    "DID_TRANSPORT_DISRUPTED", "DID_TRANSPORT_FAILFAST",
+    "DID_TARGET_FAILURE" /* 0x10 */,
+    "DID_NEXUS_FAILURE (reservation conflict)",
+    "DID_ALLOC_FAILURE",
+    "DID_MEDIUM_ERROR",
+    "DID_TRANSPORT_MARGINAL",   /*0x14 */
+};
+
+/* These where made obsolete around lk 5.12.0 . Only DRIVER_SENSE [0x8] is
+ * defined in scsi/sg.h for backward comaptibility */
+static const char * linux_driver_bytes[] = {
+    "DRIVER_OK", "DRIVER_BUSY", "DRIVER_SOFT", "DRIVER_MEDIA",
+    "DRIVER_ERROR", "DRIVER_INVALID", "DRIVER_TIMEOUT", "DRIVER_HARD",
+    "DRIVER_SENSE"
+};
+
+#if 0
+
+static const char * linux_driver_suggests[] = {
+    "SUGGEST_OK", "SUGGEST_RETRY", "SUGGEST_ABORT", "SUGGEST_REMAP",
+    "SUGGEST_DIE", "UNKNOWN","UNKNOWN","UNKNOWN",
+    "SUGGEST_SENSE"
+};
+
+#endif
+
+/*
+ * These defines are for constants that should be visible in the
+ * /usr/include/scsi directory (brought in by sg_linux_inc.h).
+ * Redefined and aliased here to decouple this code from
+ * sg_io_linux.h  N.B. the SUGGEST_* constants are no longer used.
+ */
+#ifndef DRIVER_MASK
+#define DRIVER_MASK 0x0f
+#endif
+#ifndef SUGGEST_MASK
+#define SUGGEST_MASK 0xf0       /* Suggest mask is obsolete */
+#endif
+#ifndef DRIVER_SENSE
+#define DRIVER_SENSE 0x08
+#endif
+#define SG_LIB_DRIVER_MASK      DRIVER_MASK
+#define SG_LIB_SUGGEST_MASK     SUGGEST_MASK
+#define SG_LIB_DRIVER_SENSE    DRIVER_SENSE
+
+bool sg_bsg_nvme_char_major_checked = false;
+int sg_bsg_major = 0;
+volatile int sg_nvme_char_major = 0;
+
+bool sg_checked_version_num = false;
+int sg_driver_version_num = 0;
+bool sg_duration_set_nano = false;
+
+long sg_lin_page_size = 4096;   /* default, overridden with correct value */
+
+
+/* This function only needs to be called once (unless a NVMe controller
+ * can be hot-plugged into system in which case it should be called
+ * (again) after that event). */
+void
+sg_find_bsg_nvme_char_major(int verbose)
+{
+    bool got_one = false;
+    int n;
+    const char * proc_devices = "/proc/devices";
+    char * cp;
+    FILE *fp;
+    char a[128];
+    char b[128];
+
+    sg_lin_page_size = sysconf(_SC_PAGESIZE);
+    if (NULL == (fp = fopen(proc_devices, "r"))) {
+        if (verbose)
+            pr2ws("fopen %s failed: %s\n", proc_devices, strerror(errno));
+        return;
+    }
+    while ((cp = fgets(b, sizeof(b), fp))) {
+        if ((1 == sscanf(b, "%126s", a)) &&
+            (0 == memcmp(a, "Character", 9)))
+            break;
+    }
+    while (cp && (cp = fgets(b, sizeof(b), fp))) {
+        if (2 == sscanf(b, "%d %126s", &n, a)) {
+            if (0 == strcmp("bsg", a)) {
+                sg_bsg_major = n;
+                if (got_one)
+                    break;
+                got_one = true;
+            } else if (0 == strcmp("nvme", a)) {
+                sg_nvme_char_major = n;
+                if (got_one)
+                    break;
+                got_one = true;
+            }
+        } else
+            break;
+    }
+    if (verbose > 3) {
+        if (cp) {
+            if (sg_bsg_major > 0)
+                pr2ws("found sg_bsg_major=%d\n", sg_bsg_major);
+            if (sg_nvme_char_major > 0)
+                pr2ws("found sg_nvme_char_major=%d\n", sg_nvme_char_major);
+        } else
+            pr2ws("found no bsg not nvme char device in %s\n", proc_devices);
+    }
+    fclose(fp);
+}
+
+/* Assumes that sg_find_bsg_nvme_char_major() has already been called. Returns
+ * true if dev_fd is a scsi generic pass-through device. If yields
+ * *is_nvme_p = true with *nsid_p = 0 then dev_fd is a NVMe char device.
+ * If yields *nsid_p > 0 then dev_fd is a NVMe block device. */
+static bool
+check_file_type(int dev_fd, struct stat * dev_statp, bool * is_bsg_p,
+                bool * is_nvme_p, uint32_t * nsid_p, int * os_err_p,
+                int verbose)
+{
+    bool is_nvme = false;
+    bool is_sg = false;
+    bool is_bsg = false;
+    bool is_block = false;
+    int os_err = 0;
+    int major_num;
+    uint32_t nsid = 0;          /* invalid NSID */
+
+    if (dev_fd >= 0) {
+        if (fstat(dev_fd, dev_statp) < 0) {
+            os_err = errno;
+            if (verbose)
+                pr2ws("%s: fstat() failed: %s (errno=%d)\n", __func__,
+                      safe_strerror(os_err), os_err);
+            goto skip_out;
+        }
+        major_num = (int)SG_DEV_MAJOR(dev_statp->st_rdev);
+        if (S_ISCHR(dev_statp->st_mode)) {
+            if (SCSI_GENERIC_MAJOR == major_num)
+                is_sg = true;
+            else if (sg_bsg_major == major_num)
+                is_bsg = true;
+            else if (sg_nvme_char_major == major_num)
+                is_nvme = true;
+        } else if (S_ISBLK(dev_statp->st_mode)) {
+            is_block = true;
+            if (BLOCK_EXT_MAJOR == major_num) {
+                is_nvme = true;
+                nsid = ioctl(dev_fd, NVME_IOCTL_ID, NULL);
+                if (SG_NVME_BROADCAST_NSID == nsid) {  /* means ioctl error */
+                    os_err = errno;
+                    if (verbose)
+                        pr2ws("%s: ioctl(NVME_IOCTL_ID) failed: %s "
+                              "(errno=%d)\n", __func__, safe_strerror(os_err),
+                              os_err);
+                } else
+                    os_err = 0;
+            }
+        }
+    } else {
+        os_err = EBADF;
+        if (verbose)
+            pr2ws("%s: invalid file descriptor (%d)\n", __func__, dev_fd);
+    }
+skip_out:
+    if (verbose > 3) {
+        pr2ws("%s: file descriptor is ", __func__);
+        if (is_sg)
+            pr2ws("sg device\n");
+        else if (is_bsg)
+            pr2ws("bsg device\n");
+        else if (is_nvme && (0 == nsid))
+            pr2ws("NVMe char device\n");
+        else if (is_nvme)
+            pr2ws("NVMe block device, nsid=%lld\n",
+                  ((uint32_t)-1 == nsid) ? -1LL : (long long)nsid);
+        else if (is_block)
+            pr2ws("block device\n");
+        else
+            pr2ws("undetermined device, could be regular file\n");
+    }
+    if (is_bsg_p)
+        *is_bsg_p = is_bsg;
+    if (is_nvme_p)
+        *is_nvme_p = is_nvme;
+    if (nsid_p)
+        *nsid_p = nsid;
+    if (os_err_p)
+        *os_err_p = os_err;
+    return is_sg;
+}
+
+/* Assumes dev_fd is an "open" file handle associated with device_name. If
+ * the implementation (possibly for one OS) cannot determine from dev_fd if
+ * a SCSI or NVMe pass-through is referenced, then it might guess based on
+ * device_name. Returns 1 if SCSI generic pass-though device, returns 2 if
+ * secondary SCSI pass-through device (in Linux a bsg device); returns 3 is
+ * char NVMe device (i.e. no NSID); returns 4 if block NVMe device (includes
+ * NSID), or 0 if something else (e.g. ATA block device) or dev_fd < 0.
+ * If error, returns negated errno (operating system) value. */
+int
+check_pt_file_handle(int dev_fd, const char * device_name, int verbose)
+{
+    if (verbose > 4)
+        pr2ws("%s: dev_fd=%d, device_name: %s\n", __func__, dev_fd,
+              device_name);
+    /* Linux doesn't need device_name to determine which pass-through */
+    if (! sg_bsg_nvme_char_major_checked) {
+        sg_bsg_nvme_char_major_checked = true;
+        sg_find_bsg_nvme_char_major(verbose);
+    }
+    if (dev_fd >= 0) {
+        bool is_sg, is_bsg, is_nvme;
+        int err;
+        uint32_t nsid;
+        struct stat a_stat;
+
+        is_sg = check_file_type(dev_fd, &a_stat, &is_bsg, &is_nvme, &nsid,
+                                &err, verbose);
+        if (err)
+            return -err;
+        else if (is_sg)
+            return 1;
+        else if (is_bsg)
+            return 2;
+        else if (is_nvme && (0 == nsid))
+            return 3;
+        else if (is_nvme)
+            return 4;
+        else
+            return 0;
+    } else
+        return 0;
+}
+
+/*
+ * We make a runtime decision whether to use the sg v3 interface or the sg
+ * v4 interface (currently exclusively used by the bsg driver). If all the
+ * following are true we use sg v4 which is only currently supported on bsg
+ * device nodes:
+ *   a) there is a bsg entry in the /proc/devices file
+ *   b) the device node given to scsi_pt_open() is a char device
+ *   c) the char major number of the device node given to scsi_pt_open()
+ *      matches the char major number of the bsg entry in /proc/devices
+ * Otherwise the sg v3 interface is used.
+ *
+ * Note that in either case we prepare the data in a sg v4 structure. If
+ * the runtime tests indicate that the v3 interface is needed then
+ * do_scsi_pt_v3() transfers the input data into a v3 structure and
+ * then the output data is transferred back into a sg v4 structure.
+ * That implementation detail could change in the future.
+ *
+ * [20120806] Only use MAJOR() macro in kdev_t.h if that header file is
+ * available and major() macro [N.B. lower case] is not available.
+ */
+
+
+#ifdef major
+#define SG_DEV_MAJOR major
+#else
+#ifdef HAVE_LINUX_KDEV_T_H
+#include <linux/kdev_t.h>
+#endif
+#define SG_DEV_MAJOR MAJOR  /* MAJOR() macro faulty if > 255 minors */
+#endif
+
+
+/* Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed */
+/* together. The 'flags' argument is advisory and may be ignored. */
+/* Returns >= 0 if successful, otherwise returns negated errno. */
+int
+scsi_pt_open_flags(const char * device_name, int flags, int verbose)
+{
+    int fd;
+
+    if (! sg_bsg_nvme_char_major_checked) {
+        sg_bsg_nvme_char_major_checked = true;
+        sg_find_bsg_nvme_char_major(verbose);
+    }
+    if (verbose > 1) {
+        pr2ws("open %s with flags=0x%x\n", device_name, flags);
+    }
+    fd = open(device_name, flags);
+    if (fd < 0) {
+        fd = -errno;
+        if (verbose > 1)
+            pr2ws("%s: open(%s, 0x%x) failed: %s\n", __func__, device_name,
+                  flags, safe_strerror(-fd));
+    }
+    return fd;
+}
+
+/* Returns >= 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_open_device(const char * device_name, bool read_only, int verbose)
+{
+    int oflags = O_NONBLOCK;
+
+    oflags |= (read_only ? O_RDONLY : O_RDWR);
+    return scsi_pt_open_flags(device_name, oflags, verbose);
+}
+
+/* Returns 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_close_device(int device_fd)
+{
+    int res;
+
+    res = close(device_fd);
+    if (res < 0)
+        res = -errno;
+    return res;
+}
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+static bool checked_ev_dsense = false;
+static bool ev_dsense = false;
+#endif
+
+
+/* Caller should additionally call get_scsi_pt_os_err() after this call */
+struct sg_pt_base *
+construct_scsi_pt_obj_with_fd(int dev_fd, int verbose)
+{
+    struct sg_pt_linux_scsi * ptp;
+
+    ptp = (struct sg_pt_linux_scsi *)
+          calloc(1, sizeof(struct sg_pt_linux_scsi));
+    if (ptp) {
+        int err;
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+        sntl_init_dev_stat(&ptp->dev_stat);
+        if (! checked_ev_dsense) {
+            ev_dsense = sg_get_initial_dsense();
+            checked_ev_dsense = true;
+        }
+        ptp->dev_stat.scsi_dsense = ev_dsense;
+#endif
+        err = set_pt_file_handle((struct sg_pt_base *)ptp, dev_fd, verbose);
+        if ((0 == err) && (! ptp->is_nvme)) {
+            ptp->io_hdr.guard = 'Q';
+#ifdef BSG_PROTOCOL_SCSI
+            ptp->io_hdr.protocol = BSG_PROTOCOL_SCSI;
+#endif
+#ifdef BSG_SUB_PROTOCOL_SCSI_CMD
+            ptp->io_hdr.subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD;
+#endif
+        }
+    } else if (verbose)
+        pr2ws("%s: calloc() failed, out of memory?\n", __func__);
+
+    return (struct sg_pt_base *)ptp;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj()
+{
+    return construct_scsi_pt_obj_with_fd(-1 /* dev_fd */, 0 /* verbose */);
+}
+
+void
+destruct_scsi_pt_obj(struct sg_pt_base * vp)
+{
+
+    if (NULL == vp)
+        pr2ws(">>>>>>> Warning: %s called with NULL pointer\n", __func__);
+    else {
+        struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+        if (ptp->free_nvme_id_ctlp) {
+            free(ptp->free_nvme_id_ctlp);
+            ptp->free_nvme_id_ctlp = NULL;
+            ptp->nvme_id_ctlp = NULL;
+        }
+        if (vp)
+            free(vp);
+    }
+}
+
+/* Remembers previous device file descriptor */
+void
+clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (ptp) {
+        bool is_sg, is_bsg, is_nvme;
+        int fd;
+        uint32_t nvme_nsid;
+        struct sg_sntl_dev_state_t dev_stat;
+
+        fd = ptp->dev_fd;
+        is_sg = ptp->is_sg;
+        is_bsg = ptp->is_bsg;
+        is_nvme = ptp->is_nvme;
+        nvme_nsid = ptp->nvme_nsid;
+        dev_stat = ptp->dev_stat;
+        if (ptp->free_nvme_id_ctlp)
+            free(ptp->free_nvme_id_ctlp);
+        memset(ptp, 0, sizeof(struct sg_pt_linux_scsi));
+        ptp->io_hdr.guard = 'Q';
+#ifdef BSG_PROTOCOL_SCSI
+        ptp->io_hdr.protocol = BSG_PROTOCOL_SCSI;
+#endif
+#ifdef BSG_SUB_PROTOCOL_SCSI_CMD
+        ptp->io_hdr.subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD;
+#endif
+        ptp->dev_fd = fd;
+        ptp->is_sg = is_sg;
+        ptp->is_bsg = is_bsg;
+        ptp->is_nvme = is_nvme;
+        ptp->nvme_our_sntl = false;
+        ptp->nvme_nsid = nvme_nsid;
+        ptp->dev_stat = dev_stat;
+    }
+}
+
+void
+partial_clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (NULL == ptp)
+        return;
+    ptp->in_err = 0;
+    ptp->os_err = 0;
+    ptp->io_hdr.device_status = 0;
+    ptp->io_hdr.transport_status = 0;
+    ptp->io_hdr.driver_status = 0;
+    ptp->io_hdr.din_xferp = 0;
+    ptp->io_hdr.din_xfer_len = 0;
+    ptp->io_hdr.dout_xferp = 0;
+    ptp->io_hdr.dout_xfer_len = 0;
+    ptp->nvme_result = 0;
+}
+
+#ifndef SG_SET_GET_EXTENDED
+
+/* If both sei_wr_mask and sei_rd_mask are 0, this ioctl does nothing */
+struct sg_extended_info {
+    uint32_t   sei_wr_mask;    /* OR-ed SG_SEIM_* user->driver values */
+    uint32_t   sei_rd_mask;    /* OR-ed SG_SEIM_* driver->user values */
+    uint32_t   ctl_flags_wr_mask;      /* OR-ed SG_CTL_FLAGM_* values */
+    uint32_t   ctl_flags_rd_mask;      /* OR-ed SG_CTL_FLAGM_* values */
+    uint32_t   ctl_flags;      /* bit values OR-ed, see SG_CTL_FLAGM_* */
+    uint32_t   read_value;     /* write SG_SEIRV_*, read back related */
+
+    uint32_t   reserved_sz;    /* data/sgl size of pre-allocated request */
+    uint32_t   tot_fd_thresh;  /* total data/sgat for this fd, 0: no limit */
+    uint32_t   minor_index;    /* rd: kernel's sg device minor number */
+    uint32_t   share_fd;       /* SHARE_FD and CHG_SHARE_FD use this */
+    uint32_t   sgat_elem_sz;   /* sgat element size (must be power of 2) */
+    uint8_t    pad_to_96[52];  /* pad so struct is 96 bytes long */
+};
+
+#define SG_IOCTL_MAGIC_NUM 0x22
+
+#define SG_SET_GET_EXTENDED _IOWR(SG_IOCTL_MAGIC_NUM, 0x51,     \
+                                  struct sg_extended_info)
+
+#define SG_SEIM_CTL_FLAGS       0x1
+
+#define SG_CTL_FLAGM_TIME_IN_NS 0x1
+
+#endif
+
+/* Forget any previous dev_fd and install the one given. May attempt to
+ * find file type (e.g. if pass-though) from OS so there could be an error.
+ * Returns 0 for success or the same value as get_scsi_pt_os_err()
+ * will return. dev_fd should be >= 0 for a valid file handle or -1 . */
+int
+set_pt_file_handle(struct sg_pt_base * vp, int dev_fd, int verbose)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+    struct stat a_stat;
+
+    if (! sg_bsg_nvme_char_major_checked) {
+        sg_bsg_nvme_char_major_checked = true;
+        sg_find_bsg_nvme_char_major(verbose);
+    }
+    ptp->dev_fd = dev_fd;
+    if (dev_fd >= 0) {
+        ptp->is_sg = check_file_type(dev_fd, &a_stat, &ptp->is_bsg,
+                                     &ptp->is_nvme, &ptp->nvme_nsid,
+                                     &ptp->os_err, verbose);
+        if (ptp->is_sg && (! sg_checked_version_num)) {
+            if (ioctl(dev_fd, SG_GET_VERSION_NUM, &ptp->sg_version) < 0) {
+                ptp->sg_version = 0;
+                if (verbose > 3)
+                    pr2ws("%s: ioctl(SG_GET_VERSION_NUM) failed: errno: %d "
+                          "[%s]\n", __func__, errno, safe_strerror(errno));
+            } else {    /* got version number */
+                sg_driver_version_num = ptp->sg_version;
+                sg_checked_version_num = true;
+            }
+            if (verbose > 4) {
+                int ver = ptp->sg_version;
+
+                if (ptp->sg_version >= SG_LINUX_SG_VER_V4_BASE) {
+#ifdef IGNORE_LINUX_SGV4
+                    pr2ws("%s: sg driver version %d.%02d.%02d but config "
+                          "override back to v3\n", __func__, ver / 10000,
+                          (ver / 100) % 100, ver % 100);
+#else
+                    pr2ws("%s: sg driver version %d.%02d.%02d so choose v4\n",
+                          __func__, ver / 10000, (ver / 100) % 100,
+                          ver % 100);
+#endif
+                } else if (verbose > 5)
+                    pr2ws("%s: sg driver version %d.%02d.%02d so choose v3\n",
+                          __func__, ver / 10000, (ver / 100) % 100,
+                          ver % 100);
+            }
+        } else if (ptp->is_sg)
+            ptp->sg_version = sg_driver_version_num;
+
+        if (ptp->is_sg && (ptp->sg_version >= SG_LINUX_SG_VER_V4_FULL) &&
+            getenv("SG3_UTILS_LINUX_NANO")) {
+            struct sg_extended_info sei;
+            struct sg_extended_info * seip = &sei;
+
+            memset(seip, 0, sizeof(*seip));
+            /* try to override default of milliseconds */
+            seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+            seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+            if (ioctl(dev_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+                if (verbose > 2)
+                    pr2ws("%s: unable to override milli --> nanoseconds: "
+                          "%s\n", __func__, safe_strerror(errno));
+            } else {
+                if (! sg_duration_set_nano)
+                    sg_duration_set_nano = true;
+                if (verbose > 5)
+                    pr2ws("%s: dev_fd=%d, succeeding in setting durations "
+                          "to nanoseconds\n", __func__, dev_fd);
+            }
+        } else if (ptp->is_sg && (ptp->sg_version >= SG_LINUX_SG_VER_V4_BASE)
+                   && getenv("SG3_UTILS_LINUX_NANO")) {
+            if (verbose > 2)
+                pr2ws("%s: dev_fd=%d, ignored SG3_UTILS_LINUX_NANO\nbecause "
+                      "base version sg version 4 driver\n", __func__, dev_fd);
+        }
+    } else {
+        ptp->is_sg = false;
+        ptp->is_bsg = false;
+        ptp->is_nvme = false;
+        ptp->nvme_our_sntl = false;
+        ptp->nvme_nsid = 0;
+        ptp->os_err = 0;
+    }
+    return ptp->os_err;
+}
+
+int
+sg_linux_get_sg_version(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return ptp->sg_version;
+}
+
+/* Valid file handles (which is the return value) are >= 0 . Returns -1
+ * if there is no valid file handle. */
+int
+get_pt_file_handle(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return ptp->dev_fd;
+}
+
+void
+set_scsi_pt_cdb(struct sg_pt_base * vp, const uint8_t * cdb,
+                int cdb_len)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    ptp->io_hdr.request = (__u64)(sg_uintptr_t)cdb;
+    ptp->io_hdr.request_len = cdb_len;
+}
+
+int
+get_scsi_pt_cdb_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return ptp->io_hdr.request_len;
+}
+
+uint8_t *
+get_scsi_pt_cdb_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return (uint8_t *)(sg_uintptr_t)ptp->io_hdr.request;
+}
+
+void
+set_scsi_pt_sense(struct sg_pt_base * vp, uint8_t * sense,
+                  int max_sense_len)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (sense) {
+        if (max_sense_len > 0)
+            memset(sense, 0, max_sense_len);
+    }
+    ptp->io_hdr.response = (__u64)(sg_uintptr_t)sense;
+    ptp->io_hdr.max_response_len = max_sense_len;
+}
+
+/* Setup for data transfer from device */
+void
+set_scsi_pt_data_in(struct sg_pt_base * vp, uint8_t * dxferp,
+                    int dxfer_ilen)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (ptp->io_hdr.din_xferp)
+        ++ptp->in_err;
+    if (dxfer_ilen > 0) {
+        ptp->io_hdr.din_xferp = (__u64)(sg_uintptr_t)dxferp;
+        ptp->io_hdr.din_xfer_len = dxfer_ilen;
+    }
+}
+
+/* Setup for data transfer toward device */
+void
+set_scsi_pt_data_out(struct sg_pt_base * vp, const uint8_t * dxferp,
+                     int dxfer_olen)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (ptp->io_hdr.dout_xferp)
+        ++ptp->in_err;
+    if (dxfer_olen > 0) {
+        ptp->io_hdr.dout_xferp = (__u64)(sg_uintptr_t)dxferp;
+        ptp->io_hdr.dout_xfer_len = dxfer_olen;
+    }
+}
+
+void
+set_pt_metadata_xfer(struct sg_pt_base * vp, uint8_t * dxferp,
+                     uint32_t dxfer_len, bool out_true)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (dxfer_len > 0) {
+        ptp->mdxferp = dxferp;
+        ptp->mdxfer_len = dxfer_len;
+        ptp->mdxfer_out = out_true;
+    }
+}
+
+void
+set_scsi_pt_packet_id(struct sg_pt_base * vp, int pack_id)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    ptp->io_hdr.request_extra = pack_id;        /* was placed in spare_in */
+}
+
+void
+set_scsi_pt_tag(struct sg_pt_base * vp, uint64_t tag)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    ptp->io_hdr.request_tag = tag;
+}
+
+/* Note that task management function codes are transport specific */
+void
+set_scsi_pt_task_management(struct sg_pt_base * vp, int tmf_code)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    ptp->io_hdr.subprotocol = 1;        /* SCSI task management function */
+    ptp->tmf_request[0] = (uint8_t)tmf_code;      /* assume it fits */
+    ptp->io_hdr.request = (__u64)(sg_uintptr_t)(&(ptp->tmf_request[0]));
+    ptp->io_hdr.request_len = 1;
+}
+
+void
+set_scsi_pt_task_attr(struct sg_pt_base * vp, int attribute, int priority)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    ptp->io_hdr.request_attr = attribute;
+    ptp->io_hdr.request_priority = priority;
+}
+
+#ifndef BSG_FLAG_Q_AT_TAIL
+#define BSG_FLAG_Q_AT_TAIL 0x10
+#endif
+#ifndef BSG_FLAG_Q_AT_HEAD
+#define BSG_FLAG_Q_AT_HEAD 0x20
+#endif
+
+/* Need this later if translated to v3 interface */
+#ifndef SG_FLAG_Q_AT_TAIL
+#define SG_FLAG_Q_AT_TAIL 0x10
+#endif
+#ifndef SG_FLAG_Q_AT_HEAD
+#define SG_FLAG_Q_AT_HEAD 0x20
+#endif
+
+void
+set_scsi_pt_flags(struct sg_pt_base * vp, int flags)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    /* default action of bsg driver (sg v4) is QUEUE_AT_HEAD */
+    /* default action of block layer SG_IO ioctl is QUEUE_AT_TAIL */
+    if (SCSI_PT_FLAGS_QUEUE_AT_HEAD & flags) {  /* favour AT_HEAD */
+        ptp->io_hdr.flags |= BSG_FLAG_Q_AT_HEAD;
+        ptp->io_hdr.flags &= ~BSG_FLAG_Q_AT_TAIL;
+    } else if (SCSI_PT_FLAGS_QUEUE_AT_TAIL & flags) {
+        ptp->io_hdr.flags |= BSG_FLAG_Q_AT_TAIL;
+        ptp->io_hdr.flags &= ~BSG_FLAG_Q_AT_HEAD;
+    }
+}
+
+/* If supported it is the number of bytes requested to transfer less the
+ * number actually transferred. This it typically important for data-in
+ * transfers. For data-out (only) transfers, the 'dout_req_len -
+ * dout_act_len' is returned. For bidi transfer the "din" residual is
+ * returned. */
+/* N.B. Returns din_resid and ignores dout_resid */
+int
+get_scsi_pt_resid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if ((NULL == ptp) || (ptp->is_nvme && ! ptp->nvme_our_sntl))
+        return 0;
+    else if ((ptp->io_hdr.din_xfer_len > 0) &&
+             (ptp->io_hdr.dout_xfer_len > 0))
+        return ptp->io_hdr.din_resid;
+    else if (ptp->io_hdr.dout_xfer_len > 0)
+        return ptp->io_hdr.dout_resid;
+    return ptp->io_hdr.din_resid;
+}
+
+void
+get_pt_req_lengths(const struct sg_pt_base * vp, int * req_dinp,
+                   int * req_doutp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (req_dinp) {
+        if (ptp->io_hdr.din_xfer_len > 0)
+            *req_dinp = ptp->io_hdr.din_xfer_len;
+        else
+            *req_dinp = 0;
+    }
+    if (req_doutp) {
+        if (ptp->io_hdr.dout_xfer_len > 0)
+            *req_doutp = ptp->io_hdr.dout_xfer_len;
+        else
+            *req_doutp = 0;
+    }
+}
+
+void
+get_pt_actual_lengths(const struct sg_pt_base * vp, int * act_dinp,
+                      int * act_doutp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (act_dinp) {
+        if (ptp->io_hdr.din_xfer_len > 0) {
+            int res = ptp->io_hdr.din_xfer_len - ptp->io_hdr.din_resid;
+
+            *act_dinp = (res > 0) ? res : 0;
+        } else
+            *act_dinp = 0;
+    }
+    if (act_doutp) {
+        if (ptp->io_hdr.dout_xfer_len > 0)
+            *act_doutp = ptp->io_hdr.dout_xfer_len - ptp->io_hdr.dout_resid;
+        else
+            *act_doutp = 0;
+    }
+}
+
+int
+get_scsi_pt_status_response(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (NULL == ptp)
+        return 0;
+    return (int)((ptp->is_nvme && ! ptp->nvme_our_sntl) ?
+                         ptp->nvme_status : ptp->io_hdr.device_status);
+}
+
+uint32_t
+get_pt_result(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    if (NULL == ptp)
+        return 0;
+    return (ptp->is_nvme && ! ptp->nvme_our_sntl) ?
+                        ptp->nvme_result : ptp->io_hdr.device_status;
+}
+
+int
+get_scsi_pt_sense_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return ptp->io_hdr.response_len;
+}
+
+uint8_t *
+get_scsi_pt_sense_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return (uint8_t *)(sg_uintptr_t)ptp->io_hdr.response;
+}
+
+int
+get_scsi_pt_duration_ms(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return sg_duration_set_nano ? (ptp->io_hdr.duration / 1000) :
+                                  ptp->io_hdr.duration;
+}
+
+/* If not available return 0 otherwise return number of nanoseconds that the
+ * lower layers (and hardware) took to execute the command just completed. */
+uint64_t
+get_pt_duration_ns(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return sg_duration_set_nano ? (uint32_t)ptp->io_hdr.duration : 0;
+}
+
+int
+get_scsi_pt_transport_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return ptp->io_hdr.transport_status;
+}
+
+void
+set_scsi_pt_transport_err(struct sg_pt_base * vp, int err)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    ptp->io_hdr.transport_status = err;
+}
+
+/* Returns b which will contain a null char terminated string (if
+ * max_b_len > 0). Combined driver and transport (called "host" in Linux
+ * kernel) statuses */
+char *
+get_scsi_pt_transport_err_str(const struct sg_pt_base * vp, int max_b_len,
+                              char * b)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+    int ds = ptp->io_hdr.driver_status;
+    int hs = ptp->io_hdr.transport_status;
+    int n, m;
+    char * cp = b;
+    int driv;
+    const char * driv_cp = "invalid";
+
+    if (max_b_len < 1)
+        return b;
+    m = max_b_len;
+    n = 0;
+    if (hs) {
+        if ((hs < 0) || (hs >= (int)SG_ARRAY_SIZE(linux_host_bytes)))
+            n = snprintf(cp, m, "Host_status=0x%02x is invalid\n", hs);
+        else
+            n = snprintf(cp, m, "Host_status=0x%02x [%s]\n", hs,
+                         linux_host_bytes[hs]);
+    }
+    m -= n;
+    if (m < 1) {
+        b[max_b_len - 1] = '\0';
+        return b;
+    }
+    cp += n;
+    if (ds) {
+        driv = ds & SG_LIB_DRIVER_MASK;
+        if (driv < (int)SG_ARRAY_SIZE(linux_driver_bytes))
+            driv_cp = linux_driver_bytes[driv];
+        n = snprintf(cp, m, "Driver_status=0x%02x [%s]\n", ds, driv_cp);
+        m -= n;
+    }
+    if (m < 1)
+        b[max_b_len - 1] = '\0';
+    return b;
+}
+
+int
+get_scsi_pt_result_category(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+    int dr_st = ptp->io_hdr.driver_status & SG_LIB_DRIVER_MASK;
+    int scsi_st = ptp->io_hdr.device_status & 0x7e;
+
+    if (ptp->os_err)
+        return SCSI_PT_RESULT_OS_ERR;
+    else if (ptp->io_hdr.transport_status)
+        return SCSI_PT_RESULT_TRANSPORT_ERR;
+    else if (dr_st && (SG_LIB_DRIVER_SENSE != dr_st))
+        return SCSI_PT_RESULT_TRANSPORT_ERR;
+    else if ((SG_LIB_DRIVER_SENSE == dr_st) ||
+             (SAM_STAT_CHECK_CONDITION == scsi_st) ||
+             (SAM_STAT_COMMAND_TERMINATED == scsi_st))
+        return SCSI_PT_RESULT_SENSE;
+    else if (scsi_st)
+        return SCSI_PT_RESULT_STATUS;
+    else
+        return SCSI_PT_RESULT_GOOD;
+}
+
+int
+get_scsi_pt_os_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return ptp->os_err;
+}
+
+char *
+get_scsi_pt_os_err_str(const struct sg_pt_base * vp, int max_b_len, char * b)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+    const char * cp;
+
+    cp = safe_strerror(ptp->os_err);
+    strncpy(b, cp, max_b_len);
+    if ((int)strlen(cp) >= max_b_len)
+        b[max_b_len - 1] = '\0';
+    return b;
+}
+
+bool
+pt_device_is_nvme(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return ptp->is_nvme;
+}
+
+/* If a NVMe block device (which includes the NSID) handle is associated
+ * with 'vp', then its NSID is returned (values range from 0x1 to
+ * 0xffffffe). Otherwise 0 is returned. */
+uint32_t
+get_pt_nvme_nsid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_linux_scsi * ptp = &vp->impl;
+
+    return ptp->nvme_nsid;
+}
+
+/* Executes SCSI command using sg v3 interface */
+static int
+do_scsi_pt_v3(struct sg_pt_linux_scsi * ptp, int fd, int time_secs,
+              int verbose)
+{
+    struct sg_io_hdr v3_hdr;
+
+    memset(&v3_hdr, 0, sizeof(v3_hdr));
+    /* convert v4 to v3 header */
+    v3_hdr.interface_id = 'S';
+    v3_hdr.dxfer_direction = SG_DXFER_NONE;
+    v3_hdr.cmdp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.request;
+    v3_hdr.cmd_len = (uint8_t)ptp->io_hdr.request_len;
+    if (ptp->io_hdr.din_xfer_len > 0) {
+        if (ptp->io_hdr.dout_xfer_len > 0) {
+            if (verbose)
+                pr2ws("sgv3 doesn't support bidi\n");
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        v3_hdr.dxferp = (void *)(long)ptp->io_hdr.din_xferp;
+        v3_hdr.dxfer_len = (unsigned int)ptp->io_hdr.din_xfer_len;
+        v3_hdr.dxfer_direction =  SG_DXFER_FROM_DEV;
+    } else if (ptp->io_hdr.dout_xfer_len > 0) {
+        v3_hdr.dxferp = (void *)(long)ptp->io_hdr.dout_xferp;
+        v3_hdr.dxfer_len = (unsigned int)ptp->io_hdr.dout_xfer_len;
+        v3_hdr.dxfer_direction =  SG_DXFER_TO_DEV;
+    }
+    if (ptp->io_hdr.response && (ptp->io_hdr.max_response_len > 0)) {
+        v3_hdr.sbp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.response;
+        v3_hdr.mx_sb_len = (uint8_t)ptp->io_hdr.max_response_len;
+    }
+    v3_hdr.pack_id = (int)ptp->io_hdr.request_extra;
+    if (BSG_FLAG_Q_AT_HEAD & ptp->io_hdr.flags)
+        v3_hdr.flags |= SG_FLAG_Q_AT_HEAD;      /* favour AT_HEAD */
+    else if (BSG_FLAG_Q_AT_TAIL & ptp->io_hdr.flags)
+        v3_hdr.flags |= SG_FLAG_Q_AT_TAIL;
+
+    if (NULL == v3_hdr.cmdp) {
+        if (verbose)
+            pr2ws("No SCSI command (cdb) given [v3]\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    /* io_hdr.timeout is in milliseconds, if greater than zero */
+    v3_hdr.timeout = ((time_secs > 0) ? (time_secs * 1000) : DEF_TIMEOUT);
+    /* Finally do the v3 SG_IO ioctl */
+    if (ioctl(fd, SG_IO, &v3_hdr) < 0) {
+        ptp->os_err = errno;
+        if (verbose > 1)
+            pr2ws("ioctl(SG_IO v3) failed: %s (errno=%d)\n",
+                  safe_strerror(ptp->os_err), ptp->os_err);
+        return -ptp->os_err;
+    }
+    ptp->io_hdr.device_status = (__u32)v3_hdr.status;
+    ptp->io_hdr.driver_status = (__u32)v3_hdr.driver_status;
+    ptp->io_hdr.transport_status = (__u32)v3_hdr.host_status;
+    ptp->io_hdr.response_len = (__u32)v3_hdr.sb_len_wr;
+    ptp->io_hdr.duration = (__u32)v3_hdr.duration;
+    ptp->io_hdr.din_resid = (__s32)v3_hdr.resid;
+    /* v3_hdr.info not passed back since no mapping defined (yet) */
+    return 0;
+}
+
+/* Executes SCSI command using sg v4 interface */
+static int
+do_scsi_pt_v4(struct sg_pt_linux_scsi * ptp, int fd, int time_secs,
+              int verbose)
+{
+    if (0 == ptp->io_hdr.request) {
+        if (verbose)
+            pr2ws("No SCSI command (cdb) given [v4]\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    /* io_hdr.timeout is in milliseconds, if greater than zero */
+    ptp->io_hdr.timeout = ((time_secs > 0) ? (time_secs * 1000) : DEF_TIMEOUT);
+    if (ioctl(fd, SG_IO, &ptp->io_hdr) < 0) {
+        ptp->os_err = errno;
+        if (verbose > 1)
+            pr2ws("ioctl(SG_IO v4) failed: %s (errno=%d)\n",
+                  safe_strerror(ptp->os_err), ptp->os_err);
+        return -ptp->os_err;
+    }
+    return 0;
+}
+
+/* Executes SCSI command (or at least forwards it to lower layers).
+ * Returns 0 for success, negative numbers are negated 'errno' values from
+ * OS system calls. Positive return values are errors from this package. */
+int
+do_scsi_pt(struct sg_pt_base * vp, int fd, int time_secs, int verbose)
+{
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+    bool have_checked_for_type = (ptp->dev_fd >= 0);
+
+    if (! sg_bsg_nvme_char_major_checked) {
+        sg_bsg_nvme_char_major_checked = true;
+        sg_find_bsg_nvme_char_major(verbose);
+    }
+    if (ptp->in_err) {
+        if (verbose)
+            pr2ws("Replicated or unused set_scsi_pt... functions\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (fd >= 0) {
+        if ((ptp->dev_fd >= 0) && (fd != ptp->dev_fd)) {
+            if (verbose)
+                pr2ws("%s: file descriptor given to create() and here "
+                      "differ\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        ptp->dev_fd = fd;
+    } else if (ptp->dev_fd < 0) {
+        if (verbose)
+            pr2ws("%s: invalid file descriptors\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    } else
+        fd = ptp->dev_fd;
+    if (! have_checked_for_type) {
+        int err = set_pt_file_handle(vp, ptp->dev_fd, verbose);
+
+        if (err)
+            return -ptp->os_err;
+    }
+    if (ptp->os_err)
+        return -ptp->os_err;
+    if (verbose > 5)
+        pr2ws("%s:  is_nvme=%d, is_sg=%d, is_bsg=%d\n", __func__,
+              (int)ptp->is_nvme, (int)ptp->is_sg, (int)ptp->is_bsg);
+    if (ptp->is_nvme)
+        return sg_do_nvme_pt(vp, -1, time_secs, verbose);
+    else if (ptp->is_sg) {
+#ifdef IGNORE_LINUX_SGV4
+        return do_scsi_pt_v3(ptp, fd, time_secs, verbose);
+#else
+        if (ptp->sg_version >= SG_LINUX_SG_VER_V4_BASE)
+            return do_scsi_pt_v4(ptp, fd, time_secs, verbose);
+        else
+            return do_scsi_pt_v3(ptp, fd, time_secs, verbose);
+#endif
+    } else if (sg_bsg_major <= 0)
+        return do_scsi_pt_v3(ptp, fd, time_secs, verbose);
+    else if (ptp->is_bsg)
+        return do_scsi_pt_v4(ptp, fd, time_secs, verbose);
+    else
+        return do_scsi_pt_v3(ptp, fd, time_secs, verbose);
+
+    pr2ws("%s: Should never reach this point\n", __func__);
+    return 0;
+}
diff --git a/lib/sg_pt_linux_nvme.c b/lib/sg_pt_linux_nvme.c
new file mode 100644
index 0000000..5a710f9
--- /dev/null
+++ b/lib/sg_pt_linux_nvme.c
@@ -0,0 +1,1935 @@
+/*
+ * Copyright (c) 2017-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * The code to use the NVMe Management Interface (MI) SES pass-through
+ * was provided by WDC in November 2017.
+ */
+
+/*
+ * Copyright 2017, Western Digital Corporation
+ *
+ * Written by Berck Nash
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * Based on the NVM-Express command line utility, which bore the following
+ * notice:
+ *
+ * Copyright (c) 2014-2015, Intel Corporation.
+ *
+ * Written by Keith Busch <keith.busch@intel.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *                   MA 02110-1301, USA.
+ */
+
+/* sg_pt_linux_nvme version 1.18 20210601 */
+
+/* This file contains a small "SPC-only" SNTL to support the SES pass-through
+ * of SEND DIAGNOSTIC and RECEIVE DIAGNOSTIC RESULTS through NVME-MI
+ * SES Send and SES Receive. */
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>      /* to define 'major' */
+#ifndef major
+#include <sys/types.h>
+#endif
+
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <linux/major.h>
+
+#include "sg_pt.h"
+#include "sg_lib.h"
+#include "sg_linux_inc.h"
+#include "sg_pt_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+#define SCSI_INQUIRY_OPC     0x12
+#define SCSI_REPORT_LUNS_OPC 0xa0
+#define SCSI_TEST_UNIT_READY_OPC  0x0
+#define SCSI_REQUEST_SENSE_OPC  0x3
+#define SCSI_SEND_DIAGNOSTIC_OPC  0x1d
+#define SCSI_RECEIVE_DIAGNOSTIC_OPC  0x1c
+#define SCSI_MAINT_IN_OPC  0xa3
+#define SCSI_READ10_OPC 0x28
+#define SCSI_READ16_OPC 0x88
+#define SCSI_REP_SUP_OPCS_OPC  0xc
+#define SCSI_REP_SUP_TMFS_OPC  0xd
+#define SCSI_MODE_SENSE10_OPC  0x5a
+#define SCSI_MODE_SELECT10_OPC  0x55
+#define SCSI_READ_CAPACITY10_OPC  0x25
+#define SCSI_START_STOP_OPC 0x1b
+#define SCSI_SYNC_CACHE10_OPC  0x35
+#define SCSI_SYNC_CACHE16_OPC  0x91
+#define SCSI_VERIFY10_OPC 0x2f
+#define SCSI_VERIFY16_OPC 0x8f
+#define SCSI_WRITE10_OPC 0x2a
+#define SCSI_WRITE16_OPC 0x8a
+#define SCSI_WRITE_SAME10_OPC 0x41
+#define SCSI_WRITE_SAME16_OPC 0x93
+#define SCSI_SERVICE_ACT_IN_OPC  0x9e
+#define SCSI_READ_CAPACITY16_SA  0x10
+#define SCSI_SA_MSK  0x1f
+
+/* Additional Sense Code (ASC) */
+#define NO_ADDITIONAL_SENSE 0x0
+#define LOGICAL_UNIT_NOT_READY 0x4
+#define LOGICAL_UNIT_COMMUNICATION_FAILURE 0x8
+#define UNRECOVERED_READ_ERR 0x11
+#define PARAMETER_LIST_LENGTH_ERR 0x1a
+#define INVALID_OPCODE 0x20
+#define LBA_OUT_OF_RANGE 0x21
+#define INVALID_FIELD_IN_CDB 0x24
+#define INVALID_FIELD_IN_PARAM_LIST 0x26
+#define UA_RESET_ASC 0x29
+#define UA_CHANGED_ASC 0x2a
+#define TARGET_CHANGED_ASC 0x3f
+#define LUNS_CHANGED_ASCQ 0x0e
+#define INSUFF_RES_ASC 0x55
+#define INSUFF_RES_ASCQ 0x3
+#define LOW_POWER_COND_ON_ASC  0x5e     /* ASCQ=0 */
+#define POWER_ON_RESET_ASCQ 0x0
+#define BUS_RESET_ASCQ 0x2      /* scsi bus reset occurred */
+#define MODE_CHANGED_ASCQ 0x1   /* mode parameters changed */
+#define CAPACITY_CHANGED_ASCQ 0x9
+#define SAVING_PARAMS_UNSUP 0x39
+#define TRANSPORT_PROBLEM 0x4b
+#define THRESHOLD_EXCEEDED 0x5d
+#define LOW_POWER_COND_ON 0x5e
+#define MISCOMPARE_VERIFY_ASC 0x1d
+#define MICROCODE_CHANGED_ASCQ 0x1      /* with TARGET_CHANGED_ASC */
+#define MICROCODE_CHANGED_WO_RESET_ASCQ 0x16
+#define PCIE_ERR_ASC 0x4b
+#define PCIE_UNSUPP_REQ_ASCQ 0x13
+
+/* NVMe Admin commands */
+#define SG_NVME_AD_GET_FEATURE 0xa
+#define SG_NVME_AD_SET_FEATURE 0x9
+#define SG_NVME_AD_IDENTIFY 0x6         /* similar to SCSI INQUIRY */
+#define SG_NVME_AD_DEV_SELT_TEST 0x14
+#define SG_NVME_AD_MI_RECEIVE 0x1e      /* MI: Management Interface */
+#define SG_NVME_AD_MI_SEND 0x1d         /* hmmm, same opcode as SEND DIAG */
+
+/* NVMe NVM (Non-Volatile Memory) commands */
+#define SG_NVME_NVM_FLUSH 0x0           /* SCSI SYNCHRONIZE CACHE */
+#define SG_NVME_NVM_COMPARE 0x5         /* SCSI VERIFY(BYTCHK=1) */
+#define SG_NVME_NVM_READ 0x2
+#define SG_NVME_NVM_VERIFY 0xc          /* SCSI VERIFY(BYTCHK=0) */
+#define SG_NVME_NVM_WRITE 0x1
+#define SG_NVME_NVM_WRITE_ZEROES 0x8    /* SCSI WRITE SAME */
+
+#define SG_NVME_RW_CONTROL_FUA (1 << 14) /* Force Unit Access bit */
+
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+
+/* This trims given NVMe block device name in Linux (e.g. /dev/nvme0n1p5)
+ * to the name of its associated char device (e.g. /dev/nvme0). If this
+ * occurs true is returned and the char device name is placed in 'b' (as
+ * long as b_len is sufficient). Otherwise false is returned. */
+bool
+sg_get_nvme_char_devname(const char * nvme_block_devname, uint32_t b_len,
+                         char * b)
+{
+    uint32_t n, tlen;
+    const char * cp;
+    char buff[8];
+
+    if ((NULL == b) || (b_len < 5))
+        return false;   /* degenerate cases */
+    cp = strstr(nvme_block_devname, "nvme");
+    if (NULL == cp)
+        return false;   /* expected to find "nvme" in given name */
+    if (1 != sscanf(cp, "nvme%u", &n))
+        return false;   /* didn't find valid "nvme<number>" */
+    snprintf(buff, sizeof(buff), "%u", n);
+    tlen = (cp - nvme_block_devname) + 4 + strlen(buff);
+    if ((tlen + 1) > b_len)
+        return false;           /* b isn't long enough to fit output */
+    memcpy(b, nvme_block_devname, tlen);
+    b[tlen] = '\0';
+    return true;
+}
+
+static void
+mk_sense_asc_ascq(struct sg_pt_linux_scsi * ptp, int sk, int asc, int ascq,
+                  int vb)
+{
+    bool dsense = !! ptp->dev_stat.scsi_dsense;
+    int n;
+    uint8_t * sbp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.response;
+
+    ptp->io_hdr.device_status = SAM_STAT_CHECK_CONDITION;
+    n = ptp->io_hdr.max_response_len;
+    if ((n < 8) || ((! dsense) && (n < 14))) {
+        if (vb)
+            pr2ws("%s: max_response_len=%d too short, want 14 or more\n",
+                  __func__, n);
+        return;
+    } else
+        ptp->io_hdr.response_len = dsense ? n : ((n < 18) ? n : 18);
+    memset(sbp, 0, n);
+    sg_build_sense_buffer(dsense, sbp, sk, asc, ascq);
+    if (vb > 3)
+        pr2ws("%s:  [sense_key,asc,ascq]: [0x%x,0x%x,0x%x]\n", __func__, sk,
+              asc, ascq);
+}
+
+static void
+mk_sense_from_nvme_status(struct sg_pt_linux_scsi * ptp, int vb)
+{
+    bool ok;
+    bool dsense = !! ptp->dev_stat.scsi_dsense;
+    int n;
+    uint8_t sstatus, sk, asc, ascq;
+    uint8_t * sbp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.response;
+
+    ok = sg_nvme_status2scsi(ptp->nvme_status, &sstatus, &sk, &asc, &ascq);
+    if (! ok) { /* can't find a mapping to a SCSI error, so ... */
+        sstatus = SAM_STAT_CHECK_CONDITION;
+        sk = SPC_SK_ILLEGAL_REQUEST;
+        asc = 0xb;
+        ascq = 0x0;     /* asc: "WARNING" purposely vague */
+    }
+
+    ptp->io_hdr.device_status = sstatus;
+    n = ptp->io_hdr.max_response_len;
+    if ((n < 8) || ((! dsense) && (n < 14))) {
+        pr2ws("%s: sense_len=%d too short, want 14 or more\n", __func__, n);
+        return;
+    } else
+        ptp->io_hdr.response_len = dsense ? n : ((n < 18) ? n : 18);
+    memset(sbp, 0, n);
+    sg_build_sense_buffer(dsense, sbp, sk, asc, ascq);
+    if (dsense && (ptp->nvme_status > 0))
+        sg_nvme_desc2sense(sbp, ptp->nvme_stat_dnr, ptp->nvme_stat_more,
+                           ptp->nvme_status);
+    if (vb > 3)
+        pr2ws("%s: [status, sense_key,asc,ascq]: [0x%x, 0x%x,0x%x,0x%x]\n",
+              __func__, sstatus, sk, asc, ascq);
+}
+
+/* Set in_bit to -1 to indicate no bit position of invalid field */
+static void
+mk_sense_invalid_fld(struct sg_pt_linux_scsi * ptp, bool in_cdb, int in_byte,
+                     int in_bit, int vb)
+{
+    bool dsense = !! ptp->dev_stat.scsi_dsense;
+    int asc, n;
+    uint8_t * sbp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.response;
+    uint8_t sks[4];
+
+    ptp->io_hdr.device_status = SAM_STAT_CHECK_CONDITION;
+    asc = in_cdb ? INVALID_FIELD_IN_CDB : INVALID_FIELD_IN_PARAM_LIST;
+    n = ptp->io_hdr.max_response_len;
+    if ((n < 8) || ((! dsense) && (n < 14))) {
+        if (vb)
+            pr2ws("%s: max_response_len=%d too short, want 14 or more\n",
+                  __func__, n);
+        return;
+    } else
+        ptp->io_hdr.response_len = dsense ? n : ((n < 18) ? n : 18);
+
+    memset(sbp, 0, n);
+    sg_build_sense_buffer(dsense, sbp, SPC_SK_ILLEGAL_REQUEST, asc, 0);
+    memset(sks, 0, sizeof(sks));
+    sks[0] = 0x80;
+    if (in_cdb)
+        sks[0] |= 0x40;
+    if (in_bit >= 0) {
+        sks[0] |= 0x8;
+        sks[0] |= (0x7 & in_bit);
+    }
+    sg_put_unaligned_be16(in_byte, sks + 1);
+    if (dsense) {
+        int sl = sbp[7] + 8;
+
+        sbp[7] = sl;
+        sbp[sl] = 0x2;
+        sbp[sl + 1] = 0x6;
+        memcpy(sbp + sl + 4, sks, 3);
+    } else
+        memcpy(sbp + 15, sks, 3);
+    if (vb > 3)
+        pr2ws("%s:  [sense_key,asc,ascq]: [0x5,0x%x,0x0] %c byte=%d, bit=%d\n",
+              __func__, asc, in_cdb ? 'C' : 'D', in_byte,
+              ((in_bit > 0) ? (0x7 & in_bit) : 0));
+}
+
+/* Returns 0 for success. Returns SG_LIB_NVME_STATUS if there is non-zero
+ * NVMe status (from the completion queue) with the value placed in
+ * ptp->nvme_status. If Unix error from ioctl then return negated value
+ * (equivalent -errno from basic Unix system functions like open()).
+ * CDW0 from the completion queue is placed in ptp->nvme_result in the
+ * absence of a Unix error. If time_secs is negative it is treated as
+ * a timeout in milliseconds (of abs(time_secs) ). */
+static int
+sg_nvme_admin_cmd(struct sg_pt_linux_scsi * ptp,
+                  struct sg_nvme_passthru_cmd *cmdp, void * dp, bool is_read,
+                  int time_secs, int vb)
+{
+    const uint32_t cmd_len = sizeof(struct sg_nvme_passthru_cmd);
+    int res;
+    uint32_t n;
+    uint16_t sct_sc;
+    const uint8_t * up = ((const uint8_t *)cmdp) + SG_NVME_PT_OPCODE;
+    char nam[64];
+
+    if (vb)
+        sg_get_nvme_opcode_name(*up, true /* ADMIN */, sizeof(nam), nam);
+    else
+        nam[0] = '\0';
+    cmdp->timeout_ms = (time_secs < 0) ? (-time_secs) : (1000 * time_secs);
+    ptp->os_err = 0;
+    if (vb > 2) {
+        pr2ws("NVMe Admin command: %s\n", nam);
+        hex2stderr((const uint8_t *)cmdp, cmd_len, 1);
+        if ((vb > 4) && (! is_read) && dp) {
+            uint32_t len = sg_get_unaligned_le32(up + SG_NVME_PT_DATA_LEN);
+
+            if (len > 0) {
+                n = len;
+                if ((len < 512) || (vb > 5))
+                    pr2ws("\nData-out buffer (%u bytes):\n", n);
+                else {
+                    pr2ws("\nData-out buffer (first 512 of %u bytes):\n", n);
+                    n = 512;
+                }
+                hex2stderr((const uint8_t *)dp, n, 0);
+            }
+        }
+    }
+    res = ioctl(ptp->dev_fd, NVME_IOCTL_ADMIN_CMD, cmdp);
+    if (res < 0) {  /* OS error (errno negated) */
+        ptp->os_err = -res;
+        if (vb > 1) {
+            pr2ws("%s: ioctl for %s [0x%x] failed: %s "
+                  "(errno=%d)\n", __func__, nam, *up, strerror(-res), -res);
+        }
+        return res;
+    }
+
+    /* Now res contains NVMe completion queue CDW3 31:17 (15 bits) */
+    ptp->nvme_result = cmdp->result;
+    if ((! ptp->nvme_our_sntl) && ptp->io_hdr.response &&
+        (ptp->io_hdr.max_response_len > 3)) {
+        /* build 32 byte "sense" buffer */
+        uint8_t * sbp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.response;
+        uint16_t st = (uint16_t)res;
+
+        n = ptp->io_hdr.max_response_len;
+        n = (n < 32) ? n : 32;
+        memset(sbp, 0 , n);
+        ptp->io_hdr.response_len = n;
+        sg_put_unaligned_le32(cmdp->result,
+                              sbp + SG_NVME_PT_CQ_RESULT);
+        if (n > 15) /* LSBit will be 0 (Phase bit) after (st << 1) */
+            sg_put_unaligned_le16(st << 1, sbp + SG_NVME_PT_CQ_STATUS_P);
+    }
+    /* clear upper bits (DNR and More) leaving ((SCT << 8) | SC) */
+    sct_sc = 0x7ff & res;       /* 11 bits */
+    ptp->nvme_status = sct_sc;
+    ptp->nvme_stat_dnr = !!(0x4000 & res);
+    ptp->nvme_stat_more = !!(0x2000 & res);
+    if (sct_sc) {  /* when non-zero, treat as command error */
+        if (vb > 1) {
+            char b[80];
+
+            pr2ws("%s: ioctl for %s [0x%x] failed, status: %s [0x%x]\n",
+                   __func__, nam, *up,
+                  sg_get_nvme_cmd_status_str(sct_sc, sizeof(b), b), sct_sc);
+        }
+        return SG_LIB_NVME_STATUS;      /* == SCSI_PT_DO_NVME_STATUS */
+    }
+    if ((vb > 4) && is_read && dp) {
+        uint32_t len = sg_get_unaligned_le32(up + SG_NVME_PT_DATA_LEN);
+
+        if (len > 0) {
+            n = len;
+            if ((len < 1024) || (vb > 5))
+                pr2ws("\nData-in buffer (%u bytes):\n", n);
+            else {
+                pr2ws("\nData-in buffer (first 1024 of %u bytes):\n", n);
+                n = 1024;
+            }
+            hex2stderr((const uint8_t *)dp, n, 0);
+        }
+    }
+    return 0;
+}
+
+/* see NVME MI document, NVMSR is NVM Subsystem Report */
+static void
+sntl_check_enclosure_override(struct sg_pt_linux_scsi * ptp, int vb)
+{
+    uint8_t * up = ptp->nvme_id_ctlp;
+    uint8_t nvmsr;
+
+    if (NULL == up)
+        return;
+    nvmsr = up[253];
+    if (vb > 5)
+        pr2ws("%s: enter, nvmsr=%u\n", __func__, nvmsr);
+    ptp->dev_stat.id_ctl253 = nvmsr;
+    switch (ptp->dev_stat.enclosure_override) {
+    case 0x0:       /* no override */
+        if (0x3 == (0x3 & nvmsr)) {
+            ptp->dev_stat.pdt = PDT_DISK;
+            ptp->dev_stat.enc_serv = 1;
+        } else if (0x2 & nvmsr) {
+            ptp->dev_stat.pdt = PDT_SES;
+            ptp->dev_stat.enc_serv = 1;
+        } else if (0x1 & nvmsr) {
+            ptp->dev_stat.pdt = PDT_DISK;
+            ptp->dev_stat.enc_serv = 0;
+        } else {
+            uint32_t nn = sg_get_unaligned_le32(up + 516);
+
+            ptp->dev_stat.pdt = nn ? PDT_DISK : PDT_UNKNOWN;
+            ptp->dev_stat.enc_serv = 0;
+        }
+        break;
+    case 0x1:       /* override to SES device */
+        ptp->dev_stat.pdt = PDT_SES;
+        ptp->dev_stat.enc_serv = 1;
+        break;
+    case 0x2:       /* override to disk with attached SES device */
+        ptp->dev_stat.pdt = PDT_DISK;
+        ptp->dev_stat.enc_serv = 1;
+        break;
+    case 0x3:       /* override to SAFTE device (PDT_PROCESSOR) */
+        ptp->dev_stat.pdt = PDT_PROCESSOR;
+        ptp->dev_stat.enc_serv = 1;
+        break;
+    case 0xff:      /* override to normal disk */
+        ptp->dev_stat.pdt = PDT_DISK;
+        ptp->dev_stat.enc_serv = 0;
+        break;
+    default:
+        pr2ws("%s: unknown enclosure_override value: %d\n", __func__,
+              ptp->dev_stat.enclosure_override);
+        break;
+    }
+}
+
+static int
+sntl_do_identify(struct sg_pt_linux_scsi * ptp, int cns, int nsid,
+                 int time_secs, int u_len, uint8_t * up, int vb)
+{
+    struct sg_nvme_passthru_cmd cmd;
+
+    memset(&cmd, 0, sizeof(cmd));
+    cmd.opcode = SG_NVME_AD_IDENTIFY;
+    cmd.nsid = nsid;
+    cmd.cdw10 = cns;
+    cmd.addr = (uint64_t)(sg_uintptr_t)up;
+    cmd.data_len = u_len;
+    return sg_nvme_admin_cmd(ptp, &cmd, up, true, time_secs, vb);
+}
+
+/* Currently only caches associated identify controller response (4096 bytes).
+ * Returns 0 on success; otherwise a positive value is returned */
+static int
+sntl_cache_identify(struct sg_pt_linux_scsi * ptp, int time_secs, int vb)
+{
+    int ret;
+    uint32_t pg_sz = sg_get_page_size();
+    uint8_t * up;
+
+    up = sg_memalign(pg_sz, pg_sz, &ptp->free_nvme_id_ctlp, false);
+    ptp->nvme_id_ctlp = up;
+    if (NULL == up) {
+        pr2ws("%s: sg_memalign() failed to get memory\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    ret = sntl_do_identify(ptp, 0x1 /* CNS */, 0 /* nsid */, time_secs,
+                           pg_sz, up, vb);
+    if (0 == ret)
+        sntl_check_enclosure_override(ptp, vb);
+    return (ret < 0) ? sg_convert_errno(-ret) : ret;
+}
+
+/* If nsid==0 then set cmdp->nsid to SG_NVME_BROADCAST_NSID. */
+static int
+sntl_get_features(struct sg_pt_linux_scsi * ptp, int feature_id, int select,
+                  uint32_t nsid, uint64_t din_addr, int time_secs, int vb)
+{
+    int res;
+    struct sg_nvme_passthru_cmd cmd;
+    struct sg_nvme_passthru_cmd * cmdp = &cmd;
+
+    if (vb > 4)
+        pr2ws("%s: feature_id=0x%x, select=%d\n", __func__, feature_id,
+              select);
+    memset(cmdp, 0, sizeof(*cmdp));
+    cmdp->opcode = SG_NVME_AD_GET_FEATURE;
+    cmdp->nsid = nsid ? nsid : SG_NVME_BROADCAST_NSID;
+    select &= 0x7;
+    feature_id &= 0xff;
+    cmdp->cdw10 = (select << 8) | feature_id;
+    if (din_addr)
+        cmdp->addr = din_addr;
+    cmdp->timeout_ms = (time_secs < 0) ? 0 : (1000 * time_secs);
+    res = sg_nvme_admin_cmd(ptp, cmdp, NULL, false, time_secs, vb);
+    if (res)
+        return res;
+    ptp->os_err = 0;
+    ptp->nvme_status = 0;
+    return 0;
+}
+
+static const char * nvme_scsi_vendor_str = "NVMe    ";
+static const uint16_t inq_resp_len = 36;
+
+static int
+sntl_inq(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp, int time_secs,
+         int vb)
+{
+    bool evpd;
+    int res;
+    uint16_t n, alloc_len, pg_cd;
+    uint32_t pg_sz = sg_get_page_size();
+    uint8_t * nvme_id_ns = NULL;
+    uint8_t * free_nvme_id_ns = NULL;
+    uint8_t inq_dout[256];
+
+    if (vb > 5)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+
+    if (0x2 & cdbp[1]) {        /* Reject CmdDt=1 */
+        mk_sense_invalid_fld(ptp, true, 1, 1, vb);
+        return 0;
+    }
+    if (NULL == ptp->nvme_id_ctlp) {
+        res = sntl_cache_identify(ptp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        } else if (res) /* should be negative errno */
+            return res;
+    }
+    memset(inq_dout, 0, sizeof(inq_dout));
+    alloc_len = sg_get_unaligned_be16(cdbp + 3);
+    evpd = !!(0x1 & cdbp[1]);
+    pg_cd = cdbp[2];
+    if (evpd) {         /* VPD page responses */
+        bool cp_id_ctl = false;
+
+        switch (pg_cd) {
+        case 0:
+            /* inq_dout[0] = (PQ=0)<<5 | (PDT=0); prefer pdt=0xd --> SES */
+            inq_dout[1] = pg_cd;
+            n = 11;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[4] = 0x0;
+            inq_dout[5] = 0x80;
+            inq_dout[6] = 0x83;
+            inq_dout[7] = 0x86;
+            inq_dout[8] = 0x87;
+            inq_dout[9] = 0x92;
+            inq_dout[n - 1] = SG_NVME_VPD_NICR;     /* last VPD number */
+            break;
+        case 0x80:
+            /* inq_dout[0] = (PQ=0)<<5 | (PDT=0); prefer pdt=0xd --> SES */
+            inq_dout[1] = pg_cd;
+            n = 24;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            memcpy(inq_dout + 4, ptp->nvme_id_ctlp + 4, 20);    /* SN */
+            break;
+        case 0x83:
+            if ((ptp->nvme_nsid > 0) &&
+                (ptp->nvme_nsid < SG_NVME_BROADCAST_NSID)) {
+                nvme_id_ns = sg_memalign(pg_sz, pg_sz, &free_nvme_id_ns,
+                                         false);
+                if (nvme_id_ns) {
+                    /* CNS=0x0 Identify namespace */
+                    res = sntl_do_identify(ptp, 0x0, ptp->nvme_nsid,
+                                           time_secs, pg_sz, nvme_id_ns, vb);
+                    if (res) {
+                        free(free_nvme_id_ns);
+                        free_nvme_id_ns = NULL;
+                        nvme_id_ns = NULL;
+                    }
+                }
+            }
+            n = sg_make_vpd_devid_for_nvme(ptp->nvme_id_ctlp, nvme_id_ns,
+                                           0 /* pdt */, -1 /*tproto */,
+                                           inq_dout, sizeof(inq_dout));
+            if (n > 3)
+                sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            if (free_nvme_id_ns) {
+                free(free_nvme_id_ns);
+                free_nvme_id_ns = NULL;
+                nvme_id_ns = NULL;
+            }
+            break;
+        case 0x86:      /* Extended INQUIRY (per SFS SPC Discovery 2016) */
+            inq_dout[1] = pg_cd;
+            n = 64;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[5] = 0x1;          /* SIMPSUP=1 */
+            inq_dout[7] = 0x1;          /* LUICLR=1 */
+            inq_dout[13] = 0x40;        /* max supported sense data length */
+            break;
+        case 0x87:      /* Mode page policy (per SFS SPC Discovery 2016) */
+            inq_dout[1] = pg_cd;
+            n = 8;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[4] = 0x3f;         /* all mode pages */
+            inq_dout[5] = 0xff;         /*     and their sub-pages */
+            inq_dout[6] = 0x80;         /* MLUS=1, policy=shared */
+            break;
+        case 0x92:      /* SCSI Feature set: only SPC Discovery 2016 */
+            inq_dout[1] = pg_cd;
+            n = 10;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[9] = 0x1;  /* SFS SPC Discovery 2016 */
+            break;
+        case SG_NVME_VPD_NICR:  /* 0xde (vendor (sg3_utils) specific) */
+            inq_dout[1] = pg_cd;
+            sg_put_unaligned_be16((16 + 4096) - 4, inq_dout + 2);
+            n = 16 + 4096;
+            cp_id_ctl = true;
+            break;
+        default:        /* Point to page_code field in cdb */
+            mk_sense_invalid_fld(ptp, true, 2, 7, vb);
+            return 0;
+        }
+        if (alloc_len > 0) {
+            n = (alloc_len < n) ? alloc_len : n;
+            n = (n < ptp->io_hdr.din_xfer_len) ? n : ptp->io_hdr.din_xfer_len;
+            ptp->io_hdr.din_resid = ptp->io_hdr.din_xfer_len - n;
+            if (n > 0) {
+                uint8_t * dp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp;
+
+                if (cp_id_ctl) {
+                    memcpy(dp, inq_dout, (n < 16 ? n : 16));
+                    if (n > 16)
+                        memcpy(dp + 16, ptp->nvme_id_ctlp, n - 16);
+                } else
+                    memcpy(dp, inq_dout, n);
+            }
+        }
+    } else {            /* Standard INQUIRY response */
+        /* pdt=0 --> disk; pdt=0xd --> SES; pdt=3 --> processor (safte) */
+        inq_dout[0] = (0x1f & ptp->dev_stat.pdt);  /* (PQ=0)<<5 */
+        /* inq_dout[1] = (RMD=0)<<7 | (LU_CONG=0)<<6 | (HOT_PLUG=0)<<4; */
+        inq_dout[2] = 6;   /* version: SPC-4 */
+        inq_dout[3] = 2;   /* NORMACA=0, HISUP=0, response data format: 2 */
+        inq_dout[4] = 31;  /* so response length is (or could be) 36 bytes */
+        inq_dout[6] = ptp->dev_stat.enc_serv ? 0x40 : 0;
+        inq_dout[7] = 0x2;    /* CMDQUE=1 */
+        memcpy(inq_dout + 8, nvme_scsi_vendor_str, 8);  /* NVMe not Intel */
+        memcpy(inq_dout + 16, ptp->nvme_id_ctlp + 24, 16); /* Prod <-- MN */
+        memcpy(inq_dout + 32, ptp->nvme_id_ctlp + 64, 4);  /* Rev <-- FR */
+        if (alloc_len > 0) {
+            n = (alloc_len < inq_resp_len) ? alloc_len : inq_resp_len;
+            n = (n < ptp->io_hdr.din_xfer_len) ? n : ptp->io_hdr.din_xfer_len;
+            ptp->io_hdr.din_resid = ptp->io_hdr.din_xfer_len - n;
+            if (n > 0)
+                memcpy((uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp,
+                       inq_dout, n);
+        }
+    }
+    return 0;
+}
+
+static int
+sntl_rluns(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp, int time_secs,
+           int vb)
+{
+    int res;
+    uint16_t sel_report;
+    uint32_t alloc_len, k, n, num, max_nsid;
+    uint8_t * rl_doutp;
+    uint8_t * up;
+
+    if (vb > 5)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+
+    sel_report = cdbp[2];
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (NULL == ptp->nvme_id_ctlp) {
+        res = sntl_cache_identify(ptp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    max_nsid = sg_get_unaligned_le32(ptp->nvme_id_ctlp + 516);
+    switch (sel_report) {
+    case 0:
+    case 2:
+        num = max_nsid;
+        break;
+    case 1:
+    case 0x10:
+    case 0x12:
+        num = 0;
+        break;
+    case 0x11:
+        num = (1 == ptp->nvme_nsid) ? max_nsid :  0;
+        break;
+    default:
+        if (vb > 1)
+            pr2ws("%s: bad select_report value: 0x%x\n", __func__,
+                  sel_report);
+        mk_sense_invalid_fld(ptp, true, 2, 7, vb);
+        return 0;
+    }
+    rl_doutp = (uint8_t *)calloc(num + 1, 8);
+    if (NULL == rl_doutp) {
+        pr2ws("%s: calloc() failed to get memory\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    for (k = 0, up = rl_doutp + 8; k < num; ++k, up += 8)
+        sg_put_unaligned_be16(k, up);
+    n = num * 8;
+    sg_put_unaligned_be32(n, rl_doutp);
+    n+= 8;
+    if (alloc_len > 0) {
+        n = (alloc_len < n) ? alloc_len : n;
+        n = (n < ptp->io_hdr.din_xfer_len) ? n : ptp->io_hdr.din_xfer_len;
+        ptp->io_hdr.din_resid = ptp->io_hdr.din_xfer_len - n;
+        if (n > 0)
+            memcpy((uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp, rl_doutp,
+                   n);
+    }
+    res = 0;
+    free(rl_doutp);
+    return res;
+}
+
+static int
+sntl_tur(struct sg_pt_linux_scsi * ptp, int time_secs, int vb)
+{
+    int res;
+    uint32_t pow_state;
+
+    if (vb > 5)
+        pr2ws("%s: start\n", __func__);
+    if (NULL == ptp->nvme_id_ctlp) {
+        res = sntl_cache_identify(ptp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    res = sntl_get_features(ptp, 2 /* Power Management */, 0 /* current */,
+                            0, 0, time_secs, vb);
+    if (0 != res) {
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        } else
+            return res;
+    }
+    pow_state = (0x1f & ptp->nvme_result);
+    if (vb > 5)
+        pr2ws("%s: pow_state=%u\n", __func__, pow_state);
+#if 0   /* pow_state bounces around too much on laptop */
+    if (pow_state)
+        mk_sense_asc_ascq(ptp, SPC_SK_NOT_READY, LOW_POWER_COND_ON_ASC, 0,
+                          vb);
+#endif
+    return 0;
+}
+
+static int
+sntl_req_sense(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+               int time_secs, int vb)
+{
+    bool desc;
+    int res;
+    uint32_t pow_state, alloc_len, n;
+    uint8_t rs_dout[64];
+
+    if (vb > 5)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    if (NULL == ptp->nvme_id_ctlp) {
+        res = sntl_cache_identify(ptp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    desc = !!(0x1 & cdbp[1]);
+    alloc_len = cdbp[4];
+    res = sntl_get_features(ptp, 0x2 /* Power Management */, 0 /* current */,
+                            0, 0, time_secs, vb);
+    if (0 != res) {
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        } else
+            return res;
+    }
+    ptp->io_hdr.response_len = 0;
+    pow_state = (0x1f & ptp->nvme_result);
+    if (vb > 5)
+        pr2ws("%s: pow_state=%u\n", __func__, pow_state);
+    memset(rs_dout, 0, sizeof(rs_dout));
+    if (pow_state)
+        sg_build_sense_buffer(desc, rs_dout, SPC_SK_NO_SENSE,
+                              LOW_POWER_COND_ON_ASC, 0);
+    else
+        sg_build_sense_buffer(desc, rs_dout, SPC_SK_NO_SENSE,
+                              NO_ADDITIONAL_SENSE, 0);
+    n = desc ? 8 : 18;
+    n = (n < alloc_len) ? n : alloc_len;
+    n = (n < ptp->io_hdr.din_xfer_len) ? n : ptp->io_hdr.din_xfer_len;
+    ptp->io_hdr.din_resid = ptp->io_hdr.din_xfer_len - n;
+    if (n > 0)
+        memcpy((uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp, rs_dout, n);
+    return 0;
+}
+
+static uint8_t pc_t10_2_select[] = {0, 3, 1, 2};
+
+/* For MODE SENSE(10) and MODE SELECT(10). 6 byte variants not supported */
+static int
+sntl_mode_ss(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+             int time_secs, int vb)
+{
+    bool is_msense = (SCSI_MODE_SENSE10_OPC == cdbp[0]);
+    int res, n, len;
+    uint8_t * bp;
+    struct sg_sntl_result_t sntl_result;
+
+    if (vb > 5)
+        pr2ws("%s: mode se%s\n", __func__, (is_msense ? "nse" : "lect"));
+    if (NULL == ptp->nvme_id_ctlp) {
+        res = sntl_cache_identify(ptp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    if (is_msense) {    /* MODE SENSE(10) */
+        uint8_t pc_t10 = (cdbp[2] >> 6) & 0x3;
+        int mp_t10 = (cdbp[2] & 0x3f);
+
+        if ((0x3f == mp_t10) || (0x8 /* caching mpage */ == mp_t10)) {
+            /* 0x6 is "Volatile write cache" feature id */
+            res = sntl_get_features(ptp, 0x6, pc_t10_2_select[pc_t10], 0,
+                                    0, time_secs, vb);
+            if (0 != res) {
+                if (SG_LIB_NVME_STATUS == res) {
+                    mk_sense_from_nvme_status(ptp, vb);
+                    return 0;
+                } else
+                    return res;
+            }
+            ptp->dev_stat.wce = !!(0x1 & ptp->nvme_result);
+        }
+        len = ptp->io_hdr.din_xfer_len;
+        bp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp;
+        n = sntl_resp_mode_sense10(&ptp->dev_stat, cdbp, bp, len,
+                                   &sntl_result);
+        ptp->io_hdr.din_resid = (n >= 0) ? len - n : len;
+    } else {            /* MODE SELECT(10) */
+        bool sp = !!(0x1 & cdbp[1]);    /* Save Page indication */
+        uint8_t pre_enc_ov = ptp->dev_stat.enclosure_override;
+
+        len = ptp->io_hdr.dout_xfer_len;
+        bp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.dout_xferp;
+        ptp->dev_stat.wce_changed = false;
+        n = sntl_resp_mode_select10(&ptp->dev_stat, cdbp, bp, len,
+                                    &sntl_result);
+        if (ptp->dev_stat.wce_changed) {
+            uint32_t nsid = ptp->nvme_nsid;
+            struct sg_nvme_passthru_cmd cmd;
+            struct sg_nvme_passthru_cmd * cmdp = &cmd;
+
+            ptp->dev_stat.wce_changed = false;
+            memset(cmdp, 0, sizeof(*cmdp));
+            cmdp->opcode = SG_NVME_AD_SET_FEATURE;
+            cmdp->nsid = nsid ? nsid : SG_NVME_BROADCAST_NSID;
+            cmdp->cdw10 = 0x6; /* "Volatile write cache" feature id */
+            if (sp)
+                cmdp->cdw10 |= (1U << 31);
+            cmdp->cdw11 = (uint32_t)ptp->dev_stat.wce;
+            cmdp->timeout_ms = (time_secs < 0) ? 0 : (1000 * time_secs);
+            res = sg_nvme_admin_cmd(ptp, cmdp, NULL, false, time_secs, vb);
+            if (0 != res) {
+                if (SG_LIB_NVME_STATUS == res) {
+                    mk_sense_from_nvme_status(ptp, vb);
+                    return 0;
+                } else
+                    return res;
+            }
+            ptp->os_err = 0;
+            ptp->nvme_status = 0;
+        }
+        if (pre_enc_ov != ptp->dev_stat.enclosure_override)
+            sntl_check_enclosure_override(ptp, vb);  /* ENC_OV has changed */
+    }
+    if (n < 0) {
+        int in_bit = (255 == sntl_result.in_bit) ? (int)sntl_result.in_bit :
+                                                   -1;
+        if ((SAM_STAT_CHECK_CONDITION == sntl_result.sstatus) &&
+            (SPC_SK_ILLEGAL_REQUEST == sntl_result.sk)) {
+            if (INVALID_FIELD_IN_CDB == sntl_result.asc)
+                mk_sense_invalid_fld(ptp, true, sntl_result.in_byte, in_bit,
+                                     vb);
+            else if (INVALID_FIELD_IN_PARAM_LIST == sntl_result.asc)
+                mk_sense_invalid_fld(ptp, false, sntl_result.in_byte, in_bit,
+                                     vb);
+            else
+                mk_sense_asc_ascq(ptp, sntl_result.sk, sntl_result.asc,
+                                  sntl_result.ascq, vb);
+        } else
+            pr2ws("%s: error but no sense?? n=%d\n", __func__, n);
+    }
+    return 0;
+}
+
+/* This is not really a SNTL. For SCSI SEND DIAGNOSTIC(PF=1) NVMe-MI
+ * has a special command (SES Send) to tunnel through pages to an
+ * enclosure. The NVMe enclosure is meant to understand the SES
+ * (SCSI Enclosure Services) use of diagnostics pages that are
+ * related to SES. */
+static int
+sntl_senddiag(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+              int time_secs, int vb)
+{
+    bool pf, self_test;
+    int res;
+    uint8_t st_cd, dpg_cd;
+    uint32_t alloc_len, n, dout_len, dpg_len;
+    const uint32_t pg_sz = sg_get_page_size();
+    uint8_t * dop;
+    struct sg_nvme_passthru_cmd cmd;
+    uint8_t * cmd_up = (uint8_t *)&cmd;
+
+    st_cd = 0x7 & (cdbp[1] >> 5);
+    self_test = !! (0x4 & cdbp[1]);
+    pf = !! (0x10 & cdbp[1]);
+    if (vb > 5)
+        pr2ws("%s: pf=%d, self_test=%d (st_code=%d)\n", __func__, (int)pf,
+              (int)self_test, (int)st_cd);
+    if (self_test || st_cd) {
+        uint32_t nvme_dst;
+
+        memset(cmd_up, 0, sizeof(cmd));
+        cmd_up[SG_NVME_PT_OPCODE] = SG_NVME_AD_DEV_SELT_TEST;
+        /* just this namespace (if there is one) and controller */
+        sg_put_unaligned_le32(ptp->nvme_nsid, cmd_up + SG_NVME_PT_NSID);
+        switch (st_cd) {
+        case 0: /* Here if self_test is set, do short self-test */
+        case 1: /* Background short */
+        case 5: /* Foreground short */
+            nvme_dst = 1;
+            break;
+        case 2: /* Background extended */
+        case 6: /* Foreground extended */
+            nvme_dst = 2;
+            break;
+        case 4: /* Abort self-test */
+            nvme_dst = 0xf;
+            break;
+        default:
+            pr2ws("%s: bad self-test code [0x%x]\n", __func__, st_cd);
+            mk_sense_invalid_fld(ptp, true, 1, 7, vb);
+            return 0;
+        }
+        sg_put_unaligned_le32(nvme_dst, cmd_up + SG_NVME_PT_CDW10);
+        res = sg_nvme_admin_cmd(ptp, &cmd, NULL, false, time_secs, vb);
+        if (0 != res) {
+            if (SG_LIB_NVME_STATUS == res) {
+                mk_sense_from_nvme_status(ptp, vb);
+                return 0;
+            } else
+                return res;
+        }
+    }
+    alloc_len = sg_get_unaligned_be16(cdbp + 3); /* parameter list length */
+    dout_len = ptp->io_hdr.dout_xfer_len;
+    if (pf) {
+        if (0 == alloc_len) {
+            mk_sense_invalid_fld(ptp, true, 3, 7, vb);
+            if (vb)
+                pr2ws("%s: PF bit set bit param_list_len=0\n", __func__);
+            return 0;
+        }
+    } else {    /* PF bit clear */
+        if (alloc_len) {
+            mk_sense_invalid_fld(ptp, true, 3, 7, vb);
+            if (vb)
+                pr2ws("%s: param_list_len>0 but PF clear\n", __func__);
+            return 0;
+        } else
+            return 0;     /* nothing to do */
+    }
+    if (dout_len < 4) {
+        if (vb)
+            pr2ws("%s: dout length (%u bytes) too short\n", __func__,
+                  dout_len);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    n = dout_len;
+    n = (n < alloc_len) ? n : alloc_len;
+    dop = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.dout_xferp;
+    if (! sg_is_aligned(dop, pg_sz)) {  /* is dop page aligned ? */
+        if (vb)
+            pr2ws("%s: dout [0x%" PRIx64 "] not page aligned\n", __func__,
+                  (uint64_t)ptp->io_hdr.dout_xferp);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    dpg_cd = dop[0];
+    dpg_len = sg_get_unaligned_be16(dop + 2) + 4;
+    /* should we allow for more than one D_PG is dout ?? */
+    n = (n < dpg_len) ? n : dpg_len;    /* not yet ... */
+
+    if (vb)
+        pr2ws("%s: passing through d_pg=0x%x, len=%u to NVME_MI SES send\n",
+              __func__, dpg_cd, dpg_len);
+    memset(&cmd, 0, sizeof(cmd));
+    cmd.opcode = SG_NVME_AD_MI_SEND;
+    cmd.addr = (uint64_t)(sg_uintptr_t)dop;
+    cmd.data_len = 0x1000;   /* NVMe 4k page size. Maybe determine this? */
+                             /* dout_len > 0x1000, is this a problem?? */
+    cmd.cdw10 = 0x0804;      /* NVMe Message Header */
+    cmd.cdw11 = 0x9;         /* nvme_mi_ses_send; (0x8 -> mi_ses_recv) */
+    cmd.cdw13 = n;
+    res = sg_nvme_admin_cmd(ptp, &cmd, dop, false, time_secs, vb);
+    if (0 != res) {
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        }
+    }
+    return res;
+}
+
+/* This is not really a SNTL. For SCSI RECEIVE DIAGNOSTIC RESULTS(PCV=1)
+ * NVMe-MI has a special command (SES Receive) to read pages through a
+ * tunnel from an enclosure. The NVMe enclosure is meant to understand the
+ * SES (SCSI Enclosure Services) use of diagnostics pages that are
+ * related to SES. */
+static int
+sntl_recvdiag(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+              int time_secs, int vb)
+{
+    bool pcv;
+    int res;
+    uint8_t dpg_cd;
+    uint32_t alloc_len, n, din_len;
+    uint32_t pg_sz = sg_get_page_size();
+    uint8_t * dip;
+    struct sg_nvme_passthru_cmd cmd;
+
+    pcv = !! (0x1 & cdbp[1]);
+    dpg_cd = cdbp[2];
+    alloc_len = sg_get_unaligned_be16(cdbp + 3); /* parameter list length */
+    if (vb > 5)
+        pr2ws("%s: dpg_cd=0x%x, pcv=%d, alloc_len=0x%x\n", __func__,
+              dpg_cd, (int)pcv, alloc_len);
+    din_len = ptp->io_hdr.din_xfer_len;
+    n = din_len;
+    n = (n < alloc_len) ? n : alloc_len;
+    dip = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp;
+    if (! sg_is_aligned(dip, pg_sz)) {
+        if (vb)
+            pr2ws("%s: din [0x%" PRIx64 "] not page aligned\n", __func__,
+                  (uint64_t)ptp->io_hdr.din_xferp);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+
+    if (vb)
+        pr2ws("%s: expecting d_pg=0x%x from NVME_MI SES receive\n", __func__,
+              dpg_cd);
+    memset(&cmd, 0, sizeof(cmd));
+    cmd.opcode = SG_NVME_AD_MI_RECEIVE;
+    cmd.addr = (uint64_t)(sg_uintptr_t)dip;
+    cmd.data_len = 0x1000;   /* NVMe 4k page size. Maybe determine this? */
+                             /* din_len > 0x1000, is this a problem?? */
+    cmd.cdw10 = 0x0804;      /* NVMe Message Header */
+    cmd.cdw11 = 0x8;         /* nvme_mi_ses_receive */
+    cmd.cdw12 = dpg_cd;
+    cmd.cdw13 = n;
+    res = sg_nvme_admin_cmd(ptp, &cmd, dip, true, time_secs, vb);
+    if (0 != res) {
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(ptp, vb);
+            return 0;
+        } else
+            return res;
+    }
+    ptp->io_hdr.din_resid = din_len - n;
+    return res;
+}
+
+#define F_SA_LOW                0x80    /* cdb byte 1, bits 4 to 0 */
+#define F_SA_HIGH               0x100   /* as used by variable length cdbs */
+#define FF_SA (F_SA_HIGH | F_SA_LOW)
+#define F_INV_OP                0x200
+
+static int
+sntl_rep_opcodes(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+                 int time_secs, int vb)
+{
+    bool rctd;
+    uint8_t reporting_opts, req_opcode, supp;
+    uint16_t req_sa;
+    uint32_t alloc_len, offset, a_len;
+    uint32_t pg_sz = sg_get_page_size();
+    int len, count, bump;
+    const struct sg_opcode_info_t *oip;
+    uint8_t *arr;
+    uint8_t *free_arr;
+
+    if (vb > 5)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    rctd = !!(cdbp[2] & 0x80);      /* report command timeout desc. */
+    reporting_opts = cdbp[2] & 0x7;
+    req_opcode = cdbp[3];
+    req_sa = sg_get_unaligned_be16(cdbp + 4);
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (alloc_len < 4 || alloc_len > 0xffff) {
+        mk_sense_invalid_fld(ptp, true, 6, -1, vb);
+        return 0;
+    }
+    a_len = pg_sz - 72;
+    arr = sg_memalign(pg_sz, pg_sz, &free_arr, false);
+    if (NULL == arr) {
+        pr2ws("%s: calloc() failed to get memory\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    switch (reporting_opts) {
+    case 0: /* all commands */
+        count = 0;
+        bump = rctd ? 20 : 8;
+        for (offset = 4, oip = sg_get_opcode_translation();
+             (oip->flags != 0xffff) && (offset < a_len); ++oip) {
+            if (F_INV_OP & oip->flags)
+                continue;
+            ++count;
+            arr[offset] = oip->opcode;
+            sg_put_unaligned_be16(oip->sa, arr + offset + 2);
+            if (rctd)
+                arr[offset + 5] |= 0x2;
+            if (FF_SA & oip->flags)
+                arr[offset + 5] |= 0x1;
+            sg_put_unaligned_be16(oip->len_mask[0], arr + offset + 6);
+            if (rctd)
+                sg_put_unaligned_be16(0xa, arr + offset + 8);
+            offset += bump;
+        }
+        sg_put_unaligned_be32(count * bump, arr + 0);
+        break;
+    case 1: /* one command: opcode only */
+    case 2: /* one command: opcode plus service action */
+    case 3: /* one command: if sa==0 then opcode only else opcode+sa */
+        for (oip = sg_get_opcode_translation(); oip->flags != 0xffff; ++oip) {
+            if ((req_opcode == oip->opcode) && (req_sa == oip->sa))
+                break;
+        }
+        if ((0xffff == oip->flags) || (F_INV_OP & oip->flags)) {
+            supp = 1;
+            offset = 4;
+        } else {
+            if (1 == reporting_opts) {
+                if (FF_SA & oip->flags) {
+                    mk_sense_invalid_fld(ptp, true, 2, 2, vb);
+                    free(free_arr);
+                    return 0;
+                }
+                req_sa = 0;
+            } else if ((2 == reporting_opts) && 0 == (FF_SA & oip->flags)) {
+                mk_sense_invalid_fld(ptp, true, 4, -1, vb);
+                free(free_arr);
+                return 0;
+            }
+            if ((0 == (FF_SA & oip->flags)) && (req_opcode == oip->opcode))
+                supp = 3;
+            else if (0 == (FF_SA & oip->flags))
+                supp = 1;
+            else if (req_sa != oip->sa)
+                supp = 1;
+            else
+                supp = 3;
+            if (3 == supp) {
+                uint16_t u;
+                int k;
+
+                u = oip->len_mask[0];
+                sg_put_unaligned_be16(u, arr + 2);
+                arr[4] = oip->opcode;
+                for (k = 1; k < u; ++k)
+                    arr[4 + k] = (k < 16) ?
+                oip->len_mask[k] : 0xff;
+                offset = 4 + u;
+            } else
+                offset = 4;
+        }
+        arr[1] = (rctd ? 0x80 : 0) | supp;
+        if (rctd) {
+            sg_put_unaligned_be16(0xa, arr + offset);
+            offset += 12;
+        }
+        break;
+    default:
+        mk_sense_invalid_fld(ptp, true, 2, 2, vb);
+        free(free_arr);
+        return 0;
+    }
+    offset = (offset < a_len) ? offset : a_len;
+    len = (offset < alloc_len) ? offset : alloc_len;
+    ptp->io_hdr.din_resid = ptp->io_hdr.din_xfer_len - len;
+    if (len > 0)
+        memcpy((uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp, arr, len);
+    free(free_arr);
+    return 0;
+}
+
+static int
+sntl_rep_tmfs(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+              int time_secs, int vb)
+{
+    bool repd;
+    uint32_t alloc_len, len;
+    uint8_t arr[16];
+
+    if (vb > 5)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    memset(arr, 0, sizeof(arr));
+    repd = !!(cdbp[2] & 0x80);
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (alloc_len < 4) {
+        mk_sense_invalid_fld(ptp, true, 6, -1, vb);
+        return 0;
+    }
+    arr[0] = 0xc8;          /* ATS | ATSS | LURS */
+    arr[1] = 0x1;           /* ITNRS */
+    if (repd) {
+        arr[3] = 0xc;
+        len = 16;
+    } else
+        len = 4;
+
+    len = (len < alloc_len) ? len : alloc_len;
+    ptp->io_hdr.din_resid = ptp->io_hdr.din_xfer_len - len;
+    if (len > 0)
+        memcpy((uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp, arr, len);
+    return 0;
+}
+
+/* Note that the "Returned logical block address" (RLBA) field in the SCSI
+ * READ CAPACITY (10+16) command's response provides the address of the _last_
+ * LBA (counting origin 0) which will be one less that the "size" in the
+ * NVMe Identify command response's NSZE field. One problem is that in
+ * some situations NSZE can be zero: temporarily set RLBA field to 0
+ * (implying a 1 LB logical units size) pending further research. The LBLIB
+ * is the "Logical Block Length In Bytes" field in the RCAP response. */
+static int
+sntl_readcap(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+             int time_secs, int vb)
+{
+    bool is_rcap10 = (SCSI_READ_CAPACITY10_OPC == cdbp[0]);
+    int res, n, len, alloc_len, dps;
+    uint8_t flbas, index, lbads;  /* NVMe: 2**LBADS --> Logical Block size */
+    uint32_t lbafx;     /* NVME: LBAF0...LBAF15, each 16 bytes */
+    uint32_t pg_sz = sg_get_page_size();
+    uint64_t nsze;
+    uint8_t * bp;
+    uint8_t * up;
+    uint8_t * free_up = NULL;
+    uint8_t resp[32];
+
+    if (vb > 5)
+        pr2ws("%s: RCAP%d, time_secs=%d\n", __func__,
+              (is_rcap10 ? 10 : 16), time_secs);
+    up = sg_memalign(pg_sz, pg_sz, &free_up, false);
+    if (NULL == up) {
+        pr2ws("%s: sg_memalign() failed to get memory\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    res = sntl_do_identify(ptp, 0x0 /* CNS */, ptp->nvme_nsid, time_secs,
+                           pg_sz, up, vb);
+    if (res < 0) {
+        res = sg_convert_errno(-res);
+        goto fini;
+    }
+    memset(resp, 0, sizeof(resp));
+    nsze = sg_get_unaligned_le64(up + 0);
+    flbas = up[26];     /* NVME FLBAS field from Identify, want LBAF[flbas] */
+    index = 128 + (4 * (flbas & 0xf));
+    lbafx = sg_get_unaligned_le32(up + index);
+    lbads = (lbafx >> 16) & 0xff;       /* bits 16 to 23 inclusive, pow2 */
+    if (is_rcap10) {
+        alloc_len = 8;  /* implicit, not in cdb */
+        if (nsze > 0xffffffff)
+            sg_put_unaligned_be32(0xffffffff, resp + 0);
+        else if (0 == nsze)     /* no good answer here */
+            sg_put_unaligned_be32(0, resp + 0);         /* SCSI RLBA field */
+        else
+            sg_put_unaligned_be32((uint32_t)(nsze - 1), resp + 0);
+        sg_put_unaligned_be32(1 << lbads, resp + 4);    /* SCSI LBLIB field */
+    } else {
+        alloc_len = sg_get_unaligned_be32(cdbp + 10);
+        dps = up[29];
+        if (0x7 & dps) {
+            resp[12] = 0x1;
+            n = (0x7 & dps) - 1;
+            if (n > 0)
+                resp[12] |= (n + n);
+        }
+        if (0 == nsze)  /* no good answer here */
+            sg_put_unaligned_be64(0, resp + 0);
+        else
+            sg_put_unaligned_be64(nsze - 1, resp + 0);
+        sg_put_unaligned_be32(1 << lbads, resp + 8);    /* SCSI LBLIB field */
+    }
+    len = ptp->io_hdr.din_xfer_len;
+    bp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.din_xferp;
+    n = 32;
+    n = (n < alloc_len) ? n : alloc_len;
+    n = (n < len) ? n : len;
+    ptp->io_hdr.din_resid = len - n;
+    if (n > 0)
+        memcpy(bp, resp, n);
+fini:
+    if (free_up)
+        free(free_up);
+    return res;
+}
+
+static int
+do_nvm_pt_low(struct sg_pt_linux_scsi * ptp,
+              struct sg_nvme_passthru_cmd *cmdp, void * dp, int dlen,
+              bool is_read, int time_secs, int vb)
+{
+    const uint32_t cmd_len = sizeof(struct sg_nvme_passthru_cmd);
+    int res;
+    uint32_t n;
+    uint16_t sct_sc;
+    const uint8_t * up = ((const uint8_t *)cmdp) + SG_NVME_PT_OPCODE;
+    char nam[64];
+
+    if (vb)
+        sg_get_nvme_opcode_name(*up, false /* NVM */ , sizeof(nam), nam);
+    else
+        nam[0] = '\0';
+    cmdp->timeout_ms = (time_secs < 0) ? (-time_secs) : (1000 * time_secs);
+    ptp->os_err = 0;
+    if (vb > 2) {
+        pr2ws("NVMe NVM command: %s\n", nam);
+        hex2stderr((const uint8_t *)cmdp, cmd_len, 1);
+        if ((vb > 4) && (! is_read) && dp) {
+            if (dlen > 0) {
+                n = dlen;
+                if ((dlen < 512) || (vb > 5))
+                    pr2ws("\nData-out buffer (%u bytes):\n", n);
+                else {
+                    pr2ws("\nData-out buffer (first 512 of %u bytes):\n", n);
+                    n = 512;
+                }
+                hex2stderr((const uint8_t *)dp, n, 0);
+            }
+        }
+    }
+    res = ioctl(ptp->dev_fd, NVME_IOCTL_IO_CMD, cmdp);
+    if (res < 0) {  /* OS error (errno negated) */
+        ptp->os_err = -res;
+        if (vb > 1) {
+            pr2ws("%s: ioctl for %s [0x%x] failed: %s "
+                  "(errno=%d)\n", __func__, nam, *up, strerror(-res), -res);
+        }
+        return res;
+    }
+
+    /* Now res contains NVMe completion queue CDW3 31:17 (15 bits) */
+    ptp->nvme_result = cmdp->result;
+    if ((! ptp->nvme_our_sntl) && ptp->io_hdr.response &&
+        (ptp->io_hdr.max_response_len > 3)) {
+        /* build 32 byte "sense" buffer */
+        uint8_t * sbp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.response;
+        uint16_t st = (uint16_t)res;
+
+        n = ptp->io_hdr.max_response_len;
+        n = (n < 32) ? n : 32;
+        memset(sbp, 0 , n);
+        ptp->io_hdr.response_len = n;
+        sg_put_unaligned_le32(cmdp->result,
+                              sbp + SG_NVME_PT_CQ_RESULT);
+        if (n > 15) /* LSBit will be 0 (Phase bit) after (st << 1) */
+            sg_put_unaligned_le16(st << 1, sbp + SG_NVME_PT_CQ_STATUS_P);
+    }
+    /* clear upper bits (DNR and More) leaving ((SCT << 8) | SC) */
+    sct_sc = 0x7ff & res;       /* 11 bits */
+    ptp->nvme_status = sct_sc;
+    ptp->nvme_stat_dnr = !!(0x4000 & res);
+    ptp->nvme_stat_more = !!(0x2000 & res);
+    if (sct_sc) {  /* when non-zero, treat as command error */
+        if (vb > 1) {
+            char b[80];
+
+            pr2ws("%s: ioctl for %s [0x%x] failed, status: %s [0x%x]\n",
+                   __func__, nam, *up,
+                  sg_get_nvme_cmd_status_str(sct_sc, sizeof(b), b), sct_sc);
+        }
+        return SG_LIB_NVME_STATUS;      /* == SCSI_PT_DO_NVME_STATUS */
+    }
+    if ((vb > 4) && is_read && dp) {
+        if (dlen > 0) {
+            n = dlen;
+            if ((dlen < 1024) || (vb > 5))
+                pr2ws("\nData-in buffer (%u bytes):\n", n);
+            else {
+                pr2ws("\nData-in buffer (first 1024 of %u bytes):\n", n);
+                n = 1024;
+            }
+            hex2stderr((const uint8_t *)dp, n, 0);
+        }
+    }
+    return 0;
+}
+
+/* Since ptp can be a char device (e.g. /dev/nvme0) or a blocks device
+ * (e.g. /dev/nvme0n1 or /dev/nvme0n1p3) use NVME_IOCTL_IO_CMD which is
+ * common to both (and takes a timeout). The difficult is that
+ * NVME_IOCTL_IO_CMD takes a nvme_passthru_cmd object point. */
+static int
+sntl_do_nvm_cmd(struct sg_pt_linux_scsi * ptp, struct sg_nvme_user_io * iop,
+                uint32_t dlen, bool is_read, int time_secs, int vb)
+{
+
+    struct sg_nvme_passthru_cmd nvme_pt_cmd;
+    struct sg_nvme_passthru_cmd *cmdp = &nvme_pt_cmd;
+    void * dp = (void *)(sg_uintptr_t)iop->addr;
+
+    memset(cmdp, 0, sizeof(*cmdp));
+    cmdp->opcode = iop->opcode;
+    cmdp->flags = iop->flags;
+    cmdp->nsid = ptp->nvme_nsid;
+    cmdp->addr = iop->addr;
+    cmdp->data_len = dlen;
+    cmdp->cdw10 = iop->slba & 0xffffffff;
+    cmdp->cdw11 = (iop->slba >> 32) & 0xffffffff;
+    cmdp->cdw12 = iop->nblocks; /* lower 16 bits already "0's based" count */
+
+    return do_nvm_pt_low(ptp, cmdp, dp, dlen, is_read, time_secs, vb);
+}
+
+static int
+sntl_rread(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+           int time_secs, int vb)
+{
+    bool is_read10 = (SCSI_READ10_OPC == cdbp[0]);
+    bool have_fua = !!(cdbp[1] & 0x8);
+    int res;
+    uint32_t nblks_t10 = 0;
+    struct sg_nvme_user_io io;
+    struct sg_nvme_user_io * iop = &io;
+
+    if (vb > 5)
+        pr2ws("%s: fua=%d, time_secs=%d\n", __func__, (int)have_fua,
+              time_secs);
+    memset(iop, 0, sizeof(*iop));
+    iop->opcode = SG_NVME_NVM_READ;
+    if (is_read10) {
+        iop->slba = sg_get_unaligned_be32(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be16(cdbp + 7);
+    } else {
+        iop->slba = sg_get_unaligned_be64(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be32(cdbp + 10);
+        if (nblks_t10 > (UINT16_MAX + 1)) {
+            mk_sense_invalid_fld(ptp, true, 11, -1, vb);
+            return 0;
+        }
+    }
+    if (0 == nblks_t10) {         /* NOP in SCSI */
+        if (vb > 4)
+            pr2ws("%s: nblks_t10 is 0, a NOP in SCSI, can't map to NVMe\n",
+                  __func__);
+        return 0;
+    }
+    iop->nblocks = nblks_t10 - 1;       /* crazy "0's based" */
+    if (have_fua)
+        iop->control |= SG_NVME_RW_CONTROL_FUA;
+    iop->addr = (uint64_t)ptp->io_hdr.din_xferp;
+    res = sntl_do_nvm_cmd(ptp, iop, ptp->io_hdr.din_xfer_len,
+                          true /* is_read */, time_secs, vb);
+    if (SG_LIB_NVME_STATUS == res) {
+        mk_sense_from_nvme_status(ptp, vb);
+        return 0;
+    }
+    return res;
+}
+
+static int
+sntl_write(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+           int time_secs, int vb)
+{
+    bool is_write10 = (SCSI_WRITE10_OPC == cdbp[0]);
+    bool have_fua = !!(cdbp[1] & 0x8);
+    int res;
+    uint32_t nblks_t10 = 0;
+    struct sg_nvme_user_io io;
+    struct sg_nvme_user_io * iop = &io;
+
+    if (vb > 5)
+        pr2ws("%s: fua=%d, time_secs=%d\n", __func__, (int)have_fua,
+              time_secs);
+    memset(iop, 0, sizeof(*iop));
+    iop->opcode = SG_NVME_NVM_WRITE;
+    if (is_write10) {
+        iop->slba = sg_get_unaligned_be32(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be16(cdbp + 7);
+    } else {
+        iop->slba = sg_get_unaligned_be64(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be32(cdbp + 10);
+        if (nblks_t10 > (UINT16_MAX + 1)) {
+            mk_sense_invalid_fld(ptp, true, 11, -1, vb);
+            return 0;
+        }
+    }
+    if (0 == nblks_t10) { /* NOP in SCSI */
+        if (vb > 4)
+            pr2ws("%s: nblks_t10 is 0, a NOP in SCSI, can't map to NVMe\n",
+                  __func__);
+        return 0;
+    }
+    iop->nblocks = nblks_t10 - 1;
+    if (have_fua)
+        iop->control |= SG_NVME_RW_CONTROL_FUA;
+    iop->addr = (uint64_t)ptp->io_hdr.dout_xferp;
+    res = sntl_do_nvm_cmd(ptp, iop, ptp->io_hdr.dout_xfer_len, false,
+                          time_secs, vb);
+    if (SG_LIB_NVME_STATUS == res) {
+        mk_sense_from_nvme_status(ptp, vb);
+        return 0;
+    }
+    return res;
+}
+
+static int
+sntl_verify(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+           int time_secs, int vb)
+{
+    bool is_verify10 = (SCSI_VERIFY10_OPC == cdbp[0]);
+    uint8_t bytchk = (cdbp[1] >> 1) & 0x3;
+    uint32_t dlen = 0;
+    int res;
+    uint32_t nblks_t10 = 0;
+    struct sg_nvme_user_io io;
+    struct sg_nvme_user_io * iop = &io;
+
+    if (vb > 5)
+        pr2ws("%s: bytchk=%d, time_secs=%d\n", __func__, bytchk, time_secs);
+    if (bytchk > 1) {
+        mk_sense_invalid_fld(ptp, true, 1, 2, vb);
+        return 0;
+    }
+    memset(iop, 0, sizeof(*iop));
+    iop->opcode = bytchk ? SG_NVME_NVM_COMPARE : SG_NVME_NVM_VERIFY;
+    if (is_verify10) {
+        iop->slba = sg_get_unaligned_be32(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be16(cdbp + 7);
+    } else {
+        iop->slba = sg_get_unaligned_be64(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be32(cdbp + 10);
+        if (nblks_t10 > (UINT16_MAX + 1)) {
+            mk_sense_invalid_fld(ptp, true, 11, -1, vb);
+            return 0;
+        }
+    }
+    if (0 == nblks_t10) { /* NOP in SCSI */
+        if (vb > 4)
+            pr2ws("%s: nblks_t10 is 0, a NOP in SCSI, can't map to NVMe\n",
+                  __func__);
+        return 0;
+    }
+    iop->nblocks = nblks_t10 - 1;
+    if (bytchk) {
+        iop->addr = (uint64_t)ptp->io_hdr.dout_xferp;
+        dlen = ptp->io_hdr.dout_xfer_len;
+    }
+    res = sntl_do_nvm_cmd(ptp, iop, dlen, false, time_secs, vb);
+    if (SG_LIB_NVME_STATUS == res) {
+        mk_sense_from_nvme_status(ptp, vb);
+        return 0;
+    }
+    return res;
+}
+
+static int
+sntl_write_same(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+                int time_secs, int vb)
+{
+    bool is_ws10 = (SCSI_WRITE_SAME10_OPC == cdbp[0]);
+    bool ndob = is_ws10 ? false : !!(0x1 & cdbp[1]);
+    int res;
+    int nblks_t10 = 0;
+    struct sg_nvme_user_io io;
+    struct sg_nvme_user_io * iop = &io;
+
+    if (vb > 5)
+        pr2ws("%s: ndob=%d, time_secs=%d\n", __func__, (int)ndob, time_secs);
+    if (! ndob) {
+        int flbas, index, lbafx, lbads, lbsize;
+        uint8_t * up;
+        uint8_t * dp;
+
+        dp = (uint8_t *)(sg_uintptr_t)ptp->io_hdr.dout_xferp;
+        if (dp == NULL)
+            return sg_convert_errno(ENOMEM);
+        if (NULL == ptp->nvme_id_ctlp) {
+            res = sntl_cache_identify(ptp, time_secs, vb);
+            if (SG_LIB_NVME_STATUS == res) {
+                mk_sense_from_nvme_status(ptp, vb);
+                return 0;
+            } else if (res)
+                return res;
+        }
+        up = ptp->nvme_id_ctlp;
+        flbas = up[26];     /* NVME FLBAS field from Identify */
+        index = 128 + (4 * (flbas & 0xf));
+        lbafx = sg_get_unaligned_le32(up + index);
+        lbads = (lbafx >> 16) & 0xff;  /* bits 16 to 23 inclusive, pow2 */
+        lbsize = 1 << lbads;
+        if (! sg_all_zeros(dp, lbsize)) {
+            mk_sense_asc_ascq(ptp, SPC_SK_ILLEGAL_REQUEST, PCIE_ERR_ASC,
+                              PCIE_UNSUPP_REQ_ASCQ, vb);
+            return 0;
+        }
+        /* so given single LB full of zeros, can translate .... */
+    }
+    memset(iop, 0, sizeof(*iop));
+    iop->opcode =  SG_NVME_NVM_WRITE_ZEROES;
+    if (is_ws10) {
+        iop->slba = sg_get_unaligned_be32(cdbp + 2);
+        nblks_t10 = sg_get_unaligned_be16(cdbp + 7);
+    } else {
+        uint32_t num = sg_get_unaligned_be32(cdbp + 10);
+
+        iop->slba = sg_get_unaligned_be64(cdbp + 2);
+        if (num > (UINT16_MAX + 1)) {
+            mk_sense_invalid_fld(ptp, true, 11, -1, vb);
+            return 0;
+        } else
+            nblks_t10 = num;
+    }
+    if (0 == nblks_t10) { /* NOP in SCSI */
+        if (vb > 4)
+            pr2ws("%s: nblks_t10 is 0, a NOP in SCSI, can't map to NVMe\n",
+                  __func__);
+        return 0;
+    }
+    iop->nblocks = nblks_t10 - 1;
+    res = sntl_do_nvm_cmd(ptp, iop, 0, false, time_secs, vb);
+    if (SG_LIB_NVME_STATUS == res) {
+        mk_sense_from_nvme_status(ptp, vb);
+        return 0;
+    }
+    return res;
+}
+
+static int
+sntl_sync_cache(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+                int time_secs, int vb)
+{
+    bool immed = !!(0x2 & cdbp[1]);
+    struct sg_nvme_user_io io;
+    struct sg_nvme_user_io * iop = &io;
+    int res;
+
+    if (vb > 5)
+        pr2ws("%s: immed=%d, time_secs=%d\n", __func__, (int)immed,
+              time_secs);
+    memset(iop, 0, sizeof(*iop));
+    iop->opcode =  SG_NVME_NVM_FLUSH;
+    if (vb > 4)
+        pr2ws("%s: immed bit, lba and num_lbs fields ignored\n", __func__);
+    res = sntl_do_nvm_cmd(ptp, iop, 0, false, time_secs, vb);
+    if (SG_LIB_NVME_STATUS == res) {
+        mk_sense_from_nvme_status(ptp, vb);
+        return 0;
+    }
+    return res;
+}
+
+static int
+sntl_start_stop(struct sg_pt_linux_scsi * ptp, const uint8_t * cdbp,
+                int time_secs, int vb)
+{
+    bool immed = !!(0x1 & cdbp[1]);
+
+    if (vb > 5)
+        pr2ws("%s: immed=%d, time_secs=%d, ignore\n", __func__, (int)immed,
+              time_secs);
+    if (ptp) { }        /* suppress warning */
+    return 0;
+}
+
+/* Executes NVMe Admin command (or at least forwards it to lower layers).
+ * Returns 0 for success, negative numbers are negated 'errno' values from
+ * OS system calls. Positive return values are errors from this package.
+ * When time_secs is 0 the Linux NVMe Admin command default of 60 seconds
+ * is used. */
+int
+sg_do_nvme_pt(struct sg_pt_base * vp, int fd, int time_secs, int vb)
+{
+    bool scsi_cdb;
+    bool is_read = false;
+    int n, len, hold_dev_fd;
+    uint16_t sa;
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+    struct sg_nvme_passthru_cmd cmd;
+    const uint8_t * cdbp;
+    void * dp = NULL;
+
+    if (! ptp->io_hdr.request) {
+        if (vb)
+            pr2ws("No NVMe command given (set_scsi_pt_cdb())\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    hold_dev_fd = ptp->dev_fd;
+    if (fd >= 0) {
+        if ((ptp->dev_fd >= 0) && (fd != ptp->dev_fd)) {
+            if (vb)
+                pr2ws("%s: file descriptor given to create() and here "
+                      "differ\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        ptp->dev_fd = fd;
+    } else if (ptp->dev_fd < 0) {
+        if (vb)
+            pr2ws("%s: invalid file descriptors\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    n = ptp->io_hdr.request_len;
+    cdbp = (const uint8_t *)(sg_uintptr_t)ptp->io_hdr.request;
+    if (vb > 4)
+        pr2ws("%s: opcode=0x%x, fd=%d (dev_fd=%d), time_secs=%d\n", __func__,
+              cdbp[0], fd, hold_dev_fd, time_secs);
+    scsi_cdb = sg_is_scsi_cdb(cdbp, n);
+    /* direct NVMe command (i.e. 64 bytes long) or SNTL */
+    ptp->nvme_our_sntl = scsi_cdb;
+    if (scsi_cdb) {
+        switch (cdbp[0]) {
+        case SCSI_INQUIRY_OPC:
+            return sntl_inq(ptp, cdbp, time_secs, vb);
+        case SCSI_REPORT_LUNS_OPC:
+            return sntl_rluns(ptp, cdbp, time_secs, vb);
+        case SCSI_TEST_UNIT_READY_OPC:
+            return sntl_tur(ptp, time_secs, vb);
+        case SCSI_REQUEST_SENSE_OPC:
+            return sntl_req_sense(ptp, cdbp, time_secs, vb);
+        case SCSI_READ10_OPC:
+        case SCSI_READ16_OPC:
+            return sntl_rread(ptp, cdbp, time_secs, vb);
+        case SCSI_WRITE10_OPC:
+        case SCSI_WRITE16_OPC:
+            return sntl_write(ptp, cdbp, time_secs, vb);
+        case SCSI_START_STOP_OPC:
+            return sntl_start_stop(ptp, cdbp, time_secs, vb);
+        case SCSI_SEND_DIAGNOSTIC_OPC:
+            return sntl_senddiag(ptp, cdbp, time_secs, vb);
+        case SCSI_RECEIVE_DIAGNOSTIC_OPC:
+            return sntl_recvdiag(ptp, cdbp, time_secs, vb);
+        case SCSI_MODE_SENSE10_OPC:
+        case SCSI_MODE_SELECT10_OPC:
+            return sntl_mode_ss(ptp, cdbp, time_secs, vb);
+        case SCSI_READ_CAPACITY10_OPC:
+            return sntl_readcap(ptp, cdbp, time_secs, vb);
+        case SCSI_VERIFY10_OPC:
+        case SCSI_VERIFY16_OPC:
+            return sntl_verify(ptp, cdbp, time_secs, vb);
+        case SCSI_WRITE_SAME10_OPC:
+        case SCSI_WRITE_SAME16_OPC:
+            return sntl_write_same(ptp, cdbp, time_secs, vb);
+        case SCSI_SYNC_CACHE10_OPC:
+        case SCSI_SYNC_CACHE16_OPC:
+            return sntl_sync_cache(ptp, cdbp, time_secs, vb);
+        case SCSI_SERVICE_ACT_IN_OPC:
+            if (SCSI_READ_CAPACITY16_SA == (cdbp[1] & SCSI_SA_MSK))
+                return sntl_readcap(ptp, cdbp, time_secs, vb);
+            goto fini;
+        case SCSI_MAINT_IN_OPC:
+            sa = SCSI_SA_MSK & cdbp[1];        /* service action */
+            if (SCSI_REP_SUP_OPCS_OPC == sa)
+                return sntl_rep_opcodes(ptp, cdbp, time_secs, vb);
+            else if (SCSI_REP_SUP_TMFS_OPC == sa)
+                return sntl_rep_tmfs(ptp, cdbp, time_secs, vb);
+            /* fall through */
+        default:
+fini:
+            if (vb > 2) {
+                char b[64];
+
+                sg_get_command_name(cdbp, -1, sizeof(b), b);
+                pr2ws("%s: no translation to NVMe for SCSI %s command\n",
+                      __func__, b);
+            }
+            mk_sense_asc_ascq(ptp, SPC_SK_ILLEGAL_REQUEST, INVALID_OPCODE,
+                              0, vb);
+            return 0;
+        }
+    }
+    len = (int)sizeof(cmd);
+    n = (n < len) ? n : len;
+    if (n < 64) {
+        if (vb)
+            pr2ws("%s: command length of %d bytes is too short\n", __func__,
+                  n);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    memcpy(&cmd, (const uint8_t *)(sg_uintptr_t)ptp->io_hdr.request, n);
+    if (n < len)        /* zero out rest of 'cmd' */
+        memset((uint8_t *)&cmd + n, 0, len - n);
+    if (ptp->io_hdr.din_xfer_len > 0) {
+        cmd.data_len = ptp->io_hdr.din_xfer_len;
+        dp = (void *)(sg_uintptr_t)ptp->io_hdr.din_xferp;
+        cmd.addr = (uint64_t)(sg_uintptr_t)ptp->io_hdr.din_xferp;
+        is_read = true;
+    } else if (ptp->io_hdr.dout_xfer_len > 0) {
+        cmd.data_len = ptp->io_hdr.dout_xfer_len;
+        dp = (void *)(sg_uintptr_t)ptp->io_hdr.dout_xferp;
+        cmd.addr = (uint64_t)(sg_uintptr_t)ptp->io_hdr.dout_xferp;
+        is_read = false;
+    }
+    return sg_nvme_admin_cmd(ptp, &cmd, dp, is_read, time_secs, vb);
+}
+
+#else           /* (HAVE_NVME && (! IGNORE_NVME)) [around line 140] */
+
+int
+sg_do_nvme_pt(struct sg_pt_base * vp, int fd, int time_secs, int vb)
+{
+    if (vb) {
+        pr2ws("%s: not supported, ", __func__);
+#ifdef HAVE_NVME
+        pr2ws("HAVE_NVME, ");
+#else
+        pr2ws("don't HAVE_NVME, ");
+#endif
+
+#ifdef IGNORE_NVME
+        pr2ws("IGNORE_NVME");
+#else
+        pr2ws("don't IGNORE_NVME");
+#endif
+        pr2ws("\n");
+     }
+    if (vp) { ; }               /* suppress warning */
+    if (fd) { ; }               /* suppress warning */
+    if (time_secs) { ; }        /* suppress warning */
+    return -ENOTTY;             /* inappropriate ioctl error */
+}
+
+#endif          /* (HAVE_NVME && (! IGNORE_NVME)) */
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int vb)
+{
+    bool is_read = false;
+    int dlen;
+    struct sg_pt_linux_scsi * ptp = &vp->impl;
+    struct sg_nvme_passthru_cmd cmd;
+    uint8_t * cmdp = (uint8_t *)&cmd;
+    void * dp = NULL;
+
+    if (vb && (submq != 0))
+        pr2ws("%s: warning, uses submit queue 0\n", __func__);
+    if (ptp->dev_fd < 0) {
+        if (vb > 1)
+            pr2ws("%s: no NVMe file descriptor given\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (! ptp->is_nvme) {
+        if (vb > 1)
+            pr2ws("%s: file descriptor is not NVMe device\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if ((! ptp->io_hdr.request) || (64 != ptp->io_hdr.request_len)) {
+        if (vb > 1)
+            pr2ws("%s: no NVMe 64 byte command present\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (sizeof(cmd) > 64)
+        memset(cmdp + 64, 0, sizeof(cmd) - 64);
+    memcpy(cmdp, (uint8_t *)(sg_uintptr_t)ptp->io_hdr.request, 64);
+    ptp->nvme_our_sntl = false;
+
+    dlen = ptp->io_hdr.din_xfer_len;
+    if (dlen > 0) {
+        is_read = true;
+        dp = (void *)(sg_uintptr_t)ptp->io_hdr.din_xferp;
+    } else {
+        dlen = ptp->io_hdr.dout_xfer_len;
+        if (dlen > 0)
+            dp = (void *)(sg_uintptr_t)ptp->io_hdr.dout_xferp;
+    }
+    return do_nvm_pt_low(ptp, &cmd, dp, dlen, is_read, timeout_secs, vb);
+}
+
+#else           /* (HAVE_NVME && (! IGNORE_NVME)) */
+
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int vb)
+{
+    if (vb) {
+        pr2ws("%s: not supported, ", __func__);
+#ifdef HAVE_NVME
+        pr2ws("HAVE_NVME, ");
+#else
+        pr2ws("don't HAVE_NVME, ");
+#endif
+
+#ifdef IGNORE_NVME
+        pr2ws("IGNORE_NVME");
+#else
+        pr2ws("don't IGNORE_NVME");
+#endif
+    }
+    if (vp) { }
+    if (submq) { }
+    if (timeout_secs) { }
+    return SCSI_PT_DO_NOT_SUPPORTED;
+}
+
+#endif          /* (HAVE_NVME && (! IGNORE_NVME)) */
diff --git a/lib/sg_pt_osf1.c b/lib/sg_pt_osf1.c
new file mode 100644
index 0000000..38e32cf
--- /dev/null
+++ b/lib/sg_pt_osf1.c
@@ -0,0 +1,722 @@
+/*
+ * Copyright (c) 2005-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <io/common/devgetinfo.h>
+#include <io/common/iotypes.h>
+#include <io/cam/cam.h>
+#include <io/cam/uagt.h>
+#include <io/cam/rzdisk.h>
+#include <io/cam/scsi_opcodes.h>
+#include <io/cam/scsi_all.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+
+#include "sg_pt.h"
+#include "sg_lib.h"
+#include "sg_pr2serr.h"
+
+/* Version 2.04 20210617 */
+
+#define OSF1_MAXDEV 64
+
+#ifndef CAM_DIR_BOTH
+#define CAM_DIR_BOTH 0x0        /* copy value from FreeBSD */
+#endif
+
+struct osf1_dev_channel {
+    int bus;
+    int tgt;
+    int lun;
+};
+
+// Private table of open devices: guaranteed zero on startup since
+// part of static data.
+static struct osf1_dev_channel *devicetable[OSF1_MAXDEV] SG_C_CPP_ZERO_INIT;
+static char *cam_dev = "/dev/cam";
+static int camfd;
+static int camopened = 0;
+
+struct sg_pt_osf1_scsi {
+    uint8_t * cdb;
+    int cdb_len;
+    uint8_t * sense;
+    int sense_len;
+    uint8_t * dxferp;
+    int dxfer_len;
+    int dxfer_dir;
+    int scsi_status;
+    int resid;
+    int sense_resid;
+    int in_err;
+    int os_err;
+    int transport_err;
+    bool is_nvme;
+    int dev_fd;
+};
+
+struct sg_pt_base {
+    struct sg_pt_osf1_scsi impl;
+};
+
+
+/* Returns >= 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_open_device(const char * device_name, bool read_only, int verbose)
+{
+    int oflags = 0 /* O_NONBLOCK*/ ;
+
+    oflags |= (read_only ? O_RDONLY : O_RDWR);
+    return scsi_pt_open_flags(device_name, oflags, verbose);
+}
+
+/* Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed
+ * together. The 'flags' argument is ignored in OSF-1.
+ * Returns >= 0 if successful, otherwise returns negated errno. */
+int
+scsi_pt_open_flags(const char * device_name, int flags, int verbose)
+{
+    struct osf1_dev_channel *fdchan;
+    int fd, k;
+
+    if (!camopened) {
+        camfd = open(cam_dev, O_RDWR, 0);
+        if (camfd < 0)
+            return -1;
+        camopened++;
+    }
+
+    // Search table for a free entry
+    for (k = 0; k < OSF1_MAXDEV; k++)
+        if (! devicetable[k])
+            break;
+
+    if (k == OSF1_MAXDEV) {
+        if (verbose)
+            pr2ws("too many open devices (%d)\n", OSF1_MAXDEV);
+        errno=EMFILE;
+        return -1;
+    }
+
+    fdchan = (struct osf1_dev_channel *)calloc(1,
+                                sizeof(struct osf1_dev_channel));
+    if (fdchan == NULL) {
+        // errno already set by call to malloc()
+        return -1;
+    }
+
+    fd = open(device_name, O_RDONLY|O_NONBLOCK);
+    if (fd > 0) {
+        device_info_t devinfo;
+        bzero(&devinfo, sizeof(devinfo));
+        if (ioctl(fd, DEVGETINFO, &devinfo) == 0) {
+            fdchan->bus = devinfo.v1.businfo.bus.scsi.bus_num;
+            fdchan->tgt = devinfo.v1.businfo.bus.scsi.tgt_id;
+            fdchan->lun = devinfo.v1.businfo.bus.scsi.lun;
+        }
+        close (fd);
+    } else {
+        free(fdchan);
+        return -1;
+    }
+
+    devicetable[k] = fdchan;
+    return k;
+}
+
+/* Returns 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_close_device(int device_fd)
+{
+    struct osf1_dev_channel *fdchan;
+    int i;
+
+    if ((device_fd < 0) || (device_fd >= OSF1_MAXDEV)) {
+        errno = ENODEV;
+        return -1;
+    }
+
+    fdchan = devicetable[device_fd];
+    if (NULL == fdchan) {
+        errno = ENODEV;
+        return -1;
+    }
+
+    free(fdchan);
+    devicetable[device_fd] = NULL;
+
+    for (i = 0; i < OSF1_MAXDEV; i++) {
+        if (devicetable[i])
+            break;
+    }
+    if (i == OSF1_MAXDEV) {
+        close(camfd);
+        camopened = 0;
+    }
+    return 0;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj_with_fd(int device_fd, int verbose)
+{
+    struct sg_pt_osf1_scsi * ptp;
+
+    ptp = (struct sg_pt_osf1_scsi *)malloc(sizeof(struct sg_pt_osf1_scsi));
+    if (ptp) {
+        bzero(ptp, sizeof(struct sg_pt_osf1_scsi));
+        ptp->dev_fd = (device_fd < 0) ? -1 : device_fd;
+        ptp->is_nvme = false;
+        ptp->dxfer_dir = CAM_DIR_NONE;
+    } else if (verbose)
+        pr2ws("%s: malloc() out of memory\n", __func__);
+    return (struct sg_pt_base *)ptp;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj(void)
+{
+    return construct_scsi_pt_obj_with_fd(-1, 0);
+}
+
+void
+destruct_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (ptp)
+        free(ptp);
+}
+
+void
+clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    bool is_nvme;
+    int dev_fd;
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (ptp) {
+        is_nvme = ptp->is_nvme;
+        dev_fd = ptp->dev_fd;
+        bzero(ptp, sizeof(struct sg_pt_osf1_scsi));
+        ptp->dev_fd = dev_fd;
+        ptp->is_nvme = is_nvme;
+        ptp->dxfer_dir = CAM_DIR_NONE;
+    }
+}
+
+void
+partial_clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (NULL == ptp)
+        return;
+    ptp->in_err = 0;
+    ptp->os_err = 0;
+    ptp->transport_err = 0;
+    ptp->scsi_status = 0;
+    ptp->dxfer_dir = CAM_DIR_NONE;
+    ptp->dxferp = NULL;
+    ptp->dxfer_len = 0;
+}
+
+void
+set_scsi_pt_cdb(struct sg_pt_base * vp, const uint8_t * cdb,
+                int cdb_len)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    ptp->cdb = (uint8_t *)cdb;
+    ptp->cdb_len = cdb_len;
+}
+
+int
+get_scsi_pt_cdb_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp->cdb_len;
+}
+
+uint8_t *
+get_scsi_pt_cdb_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp->cdb;
+}
+
+void
+set_scsi_pt_sense(struct sg_pt_base * vp, uint8_t * sense,
+                  int max_sense_len)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (sense) {
+        if (max_sense_len > 0)
+            bzero(sense, max_sense_len);
+    }
+    ptp->sense = sense;
+    ptp->sense_len = max_sense_len;
+}
+
+/* from device */
+void
+set_scsi_pt_data_in(struct sg_pt_base * vp, uint8_t * dxferp,
+                    int dxfer_len)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (ptp->dxferp)
+        ++ptp->in_err;
+    if (dxfer_len > 0) {
+        ptp->dxferp = dxferp;
+        ptp->dxfer_len = dxfer_len;
+        ptp->dxfer_dir = CAM_DIR_IN;
+    }
+}
+
+/* to device */
+void
+set_scsi_pt_data_out(struct sg_pt_base * vp, const uint8_t * dxferp,
+                     int dxfer_len)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (ptp->dxferp)
+        ++ptp->in_err;
+    if (dxfer_len > 0) {
+        ptp->dxferp = (uint8_t *)dxferp;
+        ptp->dxfer_len = dxfer_len;
+        ptp->dxfer_dir = CAM_DIR_OUT;
+    }
+}
+
+void
+set_scsi_pt_packet_id(struct sg_pt_base * vp, int pack_id)
+{
+}
+
+void
+set_scsi_pt_tag(struct sg_pt_base * vp, uint64_t tag)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_task_management(struct sg_pt_base * vp, int tmf_code)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_task_attr(struct sg_pt_base * vp, int attrib, int priority)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+}
+
+void
+set_scsi_pt_flags(struct sg_pt_base * objp, int flags)
+{
+    /* do nothing, suppress warnings */
+    objp = objp;
+    flags = flags;
+}
+
+static int
+release_sim(struct sg_pt_base *vp, int device_fd, int verbose) {
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+    struct osf1_dev_channel *fdchan = devicetable[device_fd];
+    UAGT_CAM_CCB uagt;
+    CCB_RELSIM relsim;
+    int retval;
+
+    bzero(&uagt, sizeof(uagt));
+    bzero(&relsim, sizeof(relsim));
+
+    uagt.uagt_ccb = (CCB_HEADER *) &relsim;
+    uagt.uagt_ccblen = sizeof(relsim);
+
+    relsim.cam_ch.cam_ccb_len = sizeof(relsim);
+    relsim.cam_ch.cam_func_code = XPT_REL_SIMQ;
+    relsim.cam_ch.cam_flags = CAM_DIR_IN | CAM_DIS_CALLBACK;
+    relsim.cam_ch.cam_path_id = fdchan->bus;
+    relsim.cam_ch.cam_target_id = fdchan->tgt;
+    relsim.cam_ch.cam_target_lun = fdchan->lun;
+
+    retval = ioctl(camfd, UAGT_CAM_IO, &uagt);
+    if (retval < 0) {
+        if (verbose)
+            pr2ws("CAM ioctl error (Release SIM Queue)\n");
+    }
+    return retval;
+}
+
+int
+do_scsi_pt(struct sg_pt_base * vp, int device_fd, int time_secs, int verbose)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+    struct osf1_dev_channel *fdchan;
+    int len, retval;
+    CCB_SCSIIO ccb;
+    UAGT_CAM_CCB uagt;
+    uint8_t sensep[ADDL_SENSE_LENGTH];
+
+
+    ptp->os_err = 0;
+    if (ptp->in_err) {
+        if (verbose)
+            pr2ws("Replicated or unused set_scsi_pt...\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (device_fd < 0) {
+        if (ptp->dev_fd < 0) {
+            if (verbose)
+                pr2ws("%s: No device file descriptor given\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+    } else {
+        if (ptp->dev_fd >= 0) {
+            if (device_fd != ptp->dev_fd) {
+                if (verbose)
+                    pr2ws("%s: file descriptor given to create and this "
+                          "differ\n", __func__);
+                return SCSI_PT_DO_BAD_PARAMS;
+            }
+        } else
+            ptp->dev_fd = device_fd;
+    }
+    if (NULL == ptp->cdb) {
+        if (verbose)
+            pr2ws("No command (cdb) given\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+
+    if ((ptp->dev_fd < 0) || (ptp->dev_fd >= OSF1_MAXDEV)) {
+        if (verbose)
+            pr2ws("Bad file descriptor\n");
+        ptp->os_err = ENODEV;
+        return -ptp->os_err;
+    }
+    fdchan = devicetable[ptp->dev_fd];
+    if (NULL == fdchan) {
+        if (verbose)
+            pr2ws("File descriptor closed??\n");
+        ptp->os_err = ENODEV;
+        return -ptp->os_err;
+    }
+    if (0 == camopened) {
+        if (verbose)
+            pr2ws("No open CAM device\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+
+    bzero(&uagt, sizeof(uagt));
+    bzero(&ccb, sizeof(ccb));
+
+    uagt.uagt_ccb = (CCB_HEADER *) &ccb;
+    uagt.uagt_ccblen = sizeof(ccb);
+    uagt.uagt_snsbuf = ccb.cam_sense_ptr = ptp->sense ? ptp->sense : sensep;
+    uagt.uagt_snslen = ccb.cam_sense_len = ptp->sense ? ptp->sense_len :
+                                                        sizeof sensep;
+    uagt.uagt_buffer = ccb.cam_data_ptr =  ptp->dxferp;
+    uagt.uagt_buflen = ccb.cam_dxfer_len = ptp->dxfer_len;
+
+    ccb.cam_timeout = time_secs;
+    ccb.cam_ch.my_addr = (CCB_HEADER *) &ccb;
+    ccb.cam_ch.cam_ccb_len = sizeof(ccb);
+    ccb.cam_ch.cam_func_code = XPT_SCSI_IO;
+    ccb.cam_ch.cam_flags = ptp->dxfer_dir;
+    ccb.cam_cdb_len = ptp->cdb_len;
+    memcpy(ccb.cam_cdb_io.cam_cdb_bytes, ptp->cdb, ptp->cdb_len);
+    ccb.cam_ch.cam_path_id = fdchan->bus;
+    ccb.cam_ch.cam_target_id = fdchan->tgt;
+    ccb.cam_ch.cam_target_lun = fdchan->lun;
+
+    if (ioctl(camfd, UAGT_CAM_IO, &uagt) < 0) {
+        if (verbose)
+            pr2ws("CAN I/O Error\n");
+        ptp->os_err = EIO;
+        return -ptp->os_err;
+    }
+
+    if (((ccb.cam_ch.cam_status & CAM_STATUS_MASK) == CAM_REQ_CMP) ||
+            ((ccb.cam_ch.cam_status & CAM_STATUS_MASK) == CAM_REQ_CMP_ERR)) {
+        ptp->scsi_status = ccb.cam_scsi_status;
+        ptp->resid = ccb.cam_resid;
+        if (ptp->sense)
+            ptp->sense_resid = ccb.cam_sense_resid;
+    } else {
+        ptp->transport_err = 1;
+    }
+
+    /* If the SIM queue is frozen, release SIM queue. */
+    if (ccb.cam_ch.cam_status & CAM_SIM_QFRZN)
+        release_sim(vp, ptp->dev_fd, verbose);
+
+    return 0;
+}
+
+int
+get_scsi_pt_result_category(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (ptp->os_err)
+        return SCSI_PT_RESULT_OS_ERR;
+    else if (ptp->transport_err)
+        return SCSI_PT_RESULT_TRANSPORT_ERR;
+    else if ((SAM_STAT_CHECK_CONDITION == ptp->scsi_status) ||
+             (SAM_STAT_COMMAND_TERMINATED == ptp->scsi_status))
+        return SCSI_PT_RESULT_SENSE;
+    else if (ptp->scsi_status)
+        return SCSI_PT_RESULT_STATUS;
+    else
+        return SCSI_PT_RESULT_GOOD;
+}
+
+int
+get_scsi_pt_resid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp->resid;
+}
+
+void
+get_pt_req_lengths(const struct sg_pt_base * vp, int * req_dinp,
+                   int * req_doutp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+    bool bidi = (ptp->dxfer_dir == CAM_DIR_BOTH);
+
+    if (req_dinp) {
+        if (ptp->dxfer_len > 0)
+            *req_dinp = ptp->dxfer_len;
+        else
+            *req_dinp = 0;
+    }
+    if (req_doutp) {
+        if ((!bidi) && (ptp->dxfer_len > 0))
+            *req_doutp = ptp->dxfer_len;
+        else
+            *req_doutp = 0;
+    }
+}
+
+void
+get_pt_actual_lengths(const struct sg_pt_base * vp, int * act_dinp,
+                      int * act_doutp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+    bool bidi = (ptp->dxfer_dir == CAM_DIR_BOTH);
+
+    if (act_dinp) {
+        if (ptp->dxfer_len > 0)
+            *act_dinp = ptp->dxfer_len - ptp->resid;
+        else
+            *act_dinp = 0;
+    }
+    if (act_doutp) {
+        if ((!bidi) && (ptp->dxfer_len > 0))
+            *act_doutp = ptp->dxfer_len - ptp->resid;
+        else
+            *act_doutp = 0;
+    }
+}
+
+
+int
+get_scsi_pt_status_response(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp->scsi_status;
+}
+
+int
+get_scsi_pt_sense_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+    int len;
+
+    len = ptp->sense_len - ptp->sense_resid;
+    return (len > 0) ? len : 0;
+}
+
+uint8_t *
+get_scsi_pt_sense_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp->sense;
+}
+
+int
+get_scsi_pt_duration_ms(const struct sg_pt_base * vp)
+{
+    // const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return -1;
+}
+
+/* If not available return 0 otherwise return number of nanoseconds that the
+ * lower layers (and hardware) took to execute the command just completed. */
+uint64_t
+get_pt_duration_ns(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    return 0;
+}
+
+int
+get_scsi_pt_transport_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp->transport_err;
+}
+
+int
+get_scsi_pt_os_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp->os_err;
+}
+
+bool
+pt_device_is_nvme(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp ? ptp->is_nvme : false;
+}
+
+char *
+get_scsi_pt_transport_err_str(const struct sg_pt_base * vp, int max_b_len,
+                              char * b)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (0 == ptp->transport_err) {
+        strncpy(b, "no transport error available", max_b_len);
+        b[max_b_len - 1] = '\0';
+        return b;
+    }
+    strncpy(b, "no transport error available", max_b_len);
+    b[max_b_len - 1] = '\0';
+    return b;
+}
+
+char *
+get_scsi_pt_os_err_str(const struct sg_pt_base * vp, int max_b_len, char * b)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+    const char * cp;
+
+    cp = safe_strerror(ptp->os_err);
+    strncpy(b, cp, max_b_len);
+    if ((int)strlen(cp) >= max_b_len)
+        b[max_b_len - 1] = '\0';
+    return b;
+}
+
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int verbose)
+{
+    if (vp) { }
+    if (submq) { }
+    if (timeout_secs) { }
+    if (verbose) { }
+    return SCSI_PT_DO_NOT_SUPPORTED;
+}
+
+int
+check_pt_file_handle(int device_fd, const char * device_name, int vb)
+{
+    if (device_fd) {}
+    if (device_name) {}
+    if (vb) {}
+    return 0;
+}
+
+/* Valid file handles (which is the return value) are >= 0 . Returns -1
+ * if there is no valid file handle. */
+int
+get_pt_file_handle(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    return ptp->dev_fd;
+}
+
+/* If a NVMe block device (which includes the NSID) handle is associated
+ * with 'vp', then its NSID is returned (values range from 0x1 to
+ * 0xffffffe). Otherwise 0 is returned. */
+uint32_t
+get_pt_nvme_nsid(const struct sg_pt_base * vp)
+{
+    if (vp) { }
+    return 0;
+}
+
+uint32_t
+get_pt_result(const struct sg_pt_base * vp)
+{
+    if (vp) { }
+    return 0;
+}
+
+/* Forget any previous dev_han and install the one given. May attempt to
+ * find file type (e.g. if pass-though) from OS so there could be an error.
+ * Returns 0 for success or the same value as get_scsi_pt_os_err()
+ * will return. dev_han should be >= 0 for a valid file handle or -1 . */
+int
+set_pt_file_handle(struct sg_pt_base * vp, int dev_han, int vb)
+{
+    struct sg_pt_osf1_scsi * ptp = &vp->impl;
+
+    if (vb) {}
+    ptp->dev_fd = (dev_han < 0) ? -1 : dev_han;
+    ptp->in_err = 0;
+    ptp->os_err = 0;
+    ptp->is_nvme = false;
+    return 0;
+}
+
+void
+set_scsi_pt_transport_err(struct sg_pt_base * vp, int err)
+{
+    if (vp) { }
+    if (err) { }
+}
+
+void
+set_pt_metadata_xfer(struct sg_pt_base * vp, uint8_t * mdxferp,
+                     uint32_t mdxfer_len, bool out_true)
+{
+    if (vp) { }
+    if (mdxferp) { }
+    if (mdxfer_len) { }
+    if (out_true) { }
+}
diff --git a/lib/sg_pt_solaris.c b/lib/sg_pt_solaris.c
new file mode 100644
index 0000000..e6cfa57
--- /dev/null
+++ b/lib/sg_pt_solaris.c
@@ -0,0 +1,578 @@
+/*
+ * Copyright (c) 2007-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/* sg_pt_solaris version 1.15 20210617 */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/param.h>
+
+/* Solaris headers */
+#include <sys/scsi/generic/commands.h>
+#include <sys/scsi/generic/status.h>
+#include <sys/scsi/impl/types.h>
+#include <sys/scsi/impl/uscsi.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_pt.h"
+#include "sg_lib.h"
+
+
+#define DEF_TIMEOUT 60       /* 60 seconds */
+
+struct sg_pt_solaris_scsi {
+    struct uscsi_cmd uscsi;
+    int max_sense_len;
+    int in_err;
+    int os_err;
+    bool is_nvme;
+    int dev_fd;
+};
+
+struct sg_pt_base {
+    struct sg_pt_solaris_scsi impl;
+};
+
+
+/* Returns >= 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_open_device(const char * device_name, bool read_only, int verbose)
+{
+    int oflags = 0 /* O_NONBLOCK*/ ;
+
+    oflags |= (read_only ? O_RDONLY : O_RDWR);
+    return scsi_pt_open_flags(device_name, oflags, verbose);
+}
+
+/* Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed
+ * together. The 'flags' argument is ignored in Solaris.
+ * Returns >= 0 if successful, otherwise returns negated errno. */
+int
+scsi_pt_open_flags(const char * device_name, int flags_arg, int verbose)
+{
+    int oflags = O_NONBLOCK | O_RDWR;
+    int fd;
+
+    flags_arg = flags_arg;  /* ignore flags argument, suppress warning */
+    if (verbose > 1) {
+        fprintf(sg_warnings_strm ? sg_warnings_strm : stderr,
+                "open %s with flags=0x%x\n", device_name, oflags);
+    }
+    fd = open(device_name, oflags);
+    if (fd < 0)
+        fd = -errno;
+    return fd;
+}
+
+/* Returns 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_close_device(int device_fd)
+{
+    int res;
+
+    res = close(device_fd);
+    if (res < 0)
+        res = -errno;
+    return res;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj_with_fd(int dev_fd, int verbose)
+{
+    struct sg_pt_solaris_scsi * ptp;
+
+    ptp = (struct sg_pt_solaris_scsi *)
+          calloc(1, sizeof(struct sg_pt_solaris_scsi));
+    if (ptp) {
+        ptp->dev_fd = (dev_fd < 0) ? -1 : dev_fd;
+        ptp->is_nvme = false;
+        ptp->uscsi.uscsi_timeout = DEF_TIMEOUT;
+        /* Comment in Illumos suggest USCSI_ISOLATE and USCSI_DIAGNOSE (both)
+         * seem to mean "don't retry" which is what we want. */
+        ptp->uscsi.uscsi_flags = USCSI_ISOLATE | USCSI_DIAGNOSE |
+                                 USCSI_RQENABLE;
+    } else if (verbose)
+        fprintf(sg_warnings_strm ? sg_warnings_strm : stderr,
+                "%s: calloc() out of memory\n", __func__);
+    return (struct sg_pt_base *)ptp;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj()
+{
+    return construct_scsi_pt_obj_with_fd(-1, 0);
+}
+
+void
+destruct_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (ptp)
+        free(ptp);
+}
+
+void
+clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    bool is_nvme;
+    int dev_fd;
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (ptp) {
+        is_nvme = ptp->is_nvme;
+        dev_fd = ptp->dev_fd;
+        memset(ptp, 0, sizeof(struct sg_pt_solaris_scsi));
+        ptp->dev_fd = dev_fd;
+        ptp->is_nvme = is_nvme;
+        ptp->uscsi.uscsi_timeout = DEF_TIMEOUT;
+        ptp->uscsi.uscsi_flags = USCSI_ISOLATE | USCSI_DIAGNOSE |
+                                 USCSI_RQENABLE;
+    }
+}
+
+void
+partial_clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (ptp) {
+        ptp->in_err = 0;
+        ptp->os_err = 0;
+        ptp->uscsi.uscsi_status = 0;
+        ptp->uscsi.uscsi_bufaddr = NULL;
+        ptp->uscsi.uscsi_buflen = 0;
+        ptp->uscsi.uscsi_flags = USCSI_ISOLATE | USCSI_DIAGNOSE |
+                                 USCSI_RQENABLE;
+    }
+}
+
+void
+set_scsi_pt_cdb(struct sg_pt_base * vp, const uint8_t * cdb,
+                int cdb_len)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    ptp->uscsi.uscsi_cdb = (char *)cdb;
+    ptp->uscsi.uscsi_cdblen = cdb_len;
+}
+
+int
+get_scsi_pt_cdb_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return ptp->uscsi.uscsi_cdblen;
+}
+
+uint8_t *
+get_scsi_pt_cdb_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return (uint8_t *)ptp->uscsi.uscsi_cdb;
+}
+
+void
+set_scsi_pt_sense(struct sg_pt_base * vp, uint8_t * sense,
+                  int max_sense_len)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (sense && (max_sense_len > 0))
+        memset(sense, 0, max_sense_len);
+    ptp->uscsi.uscsi_rqbuf = (char *)sense;
+    ptp->uscsi.uscsi_rqlen = max_sense_len;
+    ptp->max_sense_len = max_sense_len;
+}
+
+/* from device */
+void
+set_scsi_pt_data_in(struct sg_pt_base * vp, uint8_t * dxferp,
+                    int dxfer_len)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (ptp->uscsi.uscsi_bufaddr)
+        ++ptp->in_err;
+    if (dxfer_len > 0) {
+        ptp->uscsi.uscsi_bufaddr = (char *)dxferp;
+        ptp->uscsi.uscsi_buflen = dxfer_len;
+        ptp->uscsi.uscsi_flags = USCSI_READ | USCSI_ISOLATE | USCSI_DIAGNOSE |
+                                 USCSI_RQENABLE;
+    }
+}
+
+/* to device */
+void
+set_scsi_pt_data_out(struct sg_pt_base * vp, const uint8_t * dxferp,
+                     int dxfer_len)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (ptp->uscsi.uscsi_bufaddr)
+        ++ptp->in_err;
+    if (dxfer_len > 0) {
+        ptp->uscsi.uscsi_bufaddr = (char *)dxferp;
+        ptp->uscsi.uscsi_buflen = dxfer_len;
+        ptp->uscsi.uscsi_flags = USCSI_WRITE | USCSI_ISOLATE | USCSI_DIAGNOSE |
+                                 USCSI_RQENABLE;
+    }
+}
+
+void
+set_scsi_pt_packet_id(struct sg_pt_base * vp, int pack_id)
+{
+    // struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    vp = vp;                    /* ignore and suppress warning */
+    pack_id = pack_id;          /* ignore and suppress warning */
+}
+
+void
+set_scsi_pt_tag(struct sg_pt_base * vp, uint64_t tag)
+{
+    // struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    vp = vp;                    /* ignore and suppress warning */
+    tag = tag;                  /* ignore and suppress warning */
+}
+
+/* Note that task management function codes are transport specific */
+void
+set_scsi_pt_task_management(struct sg_pt_base * vp, int tmf_code)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+    tmf_code = tmf_code;        /* dummy to silence compiler */
+}
+
+void
+set_scsi_pt_task_attr(struct sg_pt_base * vp, int attribute, int priority)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    ++ptp->in_err;
+    attribute = attribute;      /* dummy to silence compiler */
+    priority = priority;        /* dummy to silence compiler */
+}
+
+void
+set_scsi_pt_flags(struct sg_pt_base * objp, int flags)
+{
+    /* do nothing, suppress warnings */
+    objp = objp;
+    flags = flags;
+}
+
+/* Executes SCSI command (or at least forwards it to lower layers).
+ * Clears os_err field prior to active call (whose result may set it
+ * again). */
+int
+do_scsi_pt(struct sg_pt_base * vp, int fd, int time_secs, int verbose)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+    FILE * ferr = sg_warnings_strm ? sg_warnings_strm : stderr;
+
+    ptp->os_err = 0;
+    if (ptp->in_err) {
+        if (verbose)
+            fprintf(ferr, "Replicated or unused set_scsi_pt... functions\n");
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (fd < 0) {
+        if (ptp->dev_fd < 0) {
+            if (verbose)
+                fprintf(ferr, "%s: No device file descriptor given\n",
+                        __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+    } else {
+        if (ptp->dev_fd >= 0) {
+            if (fd != ptp->dev_fd) {
+                if (verbose)
+                    fprintf(ferr, "%s: file descriptor given to create and "
+                            "this differ\n", __func__);
+                return SCSI_PT_DO_BAD_PARAMS;
+            }
+        } else
+            ptp->dev_fd = fd;
+    }
+    if (NULL == ptp->uscsi.uscsi_cdb) {
+        if (verbose)
+            fprintf(ferr, "%s: No SCSI command (cdb) given\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (time_secs > 0)
+        ptp->uscsi.uscsi_timeout = time_secs;
+
+    if (ioctl(ptp->dev_fd, USCSICMD, &ptp->uscsi)) {
+        ptp->os_err = errno;
+        if ((EIO == ptp->os_err) && ptp->uscsi.uscsi_status) {
+            ptp->os_err = 0;
+            return 0;
+        }
+        if (verbose)
+            fprintf(ferr, "%s: ioctl(USCSICMD) failed with os_err (errno) "
+                    "= %d\n", __func__, ptp->os_err);
+        return -ptp->os_err;
+    }
+    return 0;
+}
+
+int
+get_scsi_pt_result_category(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+    int scsi_st = ptp->uscsi.uscsi_status;
+
+    if (ptp->os_err)
+        return SCSI_PT_RESULT_OS_ERR;
+    else if ((SAM_STAT_CHECK_CONDITION == scsi_st) ||
+             (SAM_STAT_COMMAND_TERMINATED == scsi_st))
+        return SCSI_PT_RESULT_SENSE;
+    else if (scsi_st)
+        return SCSI_PT_RESULT_STATUS;
+    else
+        return SCSI_PT_RESULT_GOOD;
+}
+
+uint32_t
+get_pt_result(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return (uint32_t)ptp->uscsi.uscsi_status;
+}
+
+int
+get_scsi_pt_resid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return ptp->uscsi.uscsi_resid;
+}
+
+void
+get_pt_req_lengths(const struct sg_pt_base * vp, int * req_dinp,
+                   int * req_doutp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+    int dxfer_len = ptp->uscsi.uscsi_buflen;
+    int flags = ptp->uscsi.uscsi_flags;
+
+    if (req_dinp) {
+        if ((dxfer_len > 0) && (USCSI_READ & flags))
+            *req_dinp = dxfer_len;
+        else
+            *req_dinp = 0;
+    }
+    if (req_doutp) {
+        if ((dxfer_len > 0) && (USCSI_WRITE & flags))
+            *req_doutp = dxfer_len;
+        else
+            *req_doutp = 0;
+    }
+}
+
+void
+get_pt_actual_lengths(const struct sg_pt_base * vp, int * act_dinp,
+                      int * act_doutp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+    int dxfer_len = ptp->uscsi.uscsi_buflen;
+    int flags = ptp->uscsi.uscsi_flags;
+
+    if (act_dinp) {
+        if ((dxfer_len > 0) && (USCSI_READ & flags))
+            *act_dinp = dxfer_len - ptp->uscsi.uscsi_resid;
+        else
+            *act_dinp = 0;
+    }
+    if (act_doutp) {
+        if ((dxfer_len > 0) && (USCSI_WRITE & flags))
+            *act_doutp = dxfer_len - ptp->uscsi.uscsi_resid;
+        else
+            *act_doutp = 0;
+    }
+}
+
+int
+get_scsi_pt_status_response(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return ptp->uscsi.uscsi_status;
+}
+
+int
+get_scsi_pt_sense_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+    int res;
+
+    if (ptp->max_sense_len > 0) {
+        res = ptp->max_sense_len - ptp->uscsi.uscsi_rqresid;
+        return (res > 0) ? res : 0;
+    }
+    return 0;
+}
+
+uint8_t *
+get_scsi_pt_sense_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return (uint8_t *)ptp->uscsi.uscsi_rqbuf;
+}
+
+int
+get_scsi_pt_duration_ms(const struct sg_pt_base * vp)
+{
+    // const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    vp = vp;            /* ignore and suppress warning */
+    return -1;          /* not available */
+}
+
+/* If not available return 0 otherwise return number of nanoseconds that the
+ * lower layers (and hardware) took to execute the command just completed. */
+uint64_t
+get_pt_duration_ns(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    return 0;
+}
+
+int
+get_scsi_pt_transport_err(const struct sg_pt_base * vp)
+{
+    // const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (vp) { ; }            /* ignore and suppress warning */
+    return 0;
+}
+
+void
+set_scsi_pt_transport_err(struct sg_pt_base * vp, int err)
+{
+    // const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (vp) { ; }            /* ignore and suppress warning */
+    if (err) { ; }           /* ignore and suppress warning */
+}
+
+int
+get_scsi_pt_os_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return ptp->os_err;
+}
+
+bool
+pt_device_is_nvme(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return ptp ? ptp->is_nvme : false;
+}
+
+char *
+get_scsi_pt_transport_err_str(const struct sg_pt_base * vp, int max_b_len,
+                              char * b)
+{
+    // const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    vp = vp;            /* ignore and suppress warning */
+    if (max_b_len > 0)
+        b[0] = '\0';
+
+    return b;
+}
+
+char *
+get_scsi_pt_os_err_str(const struct sg_pt_base * vp, int max_b_len, char * b)
+{
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+    const char * cp;
+
+    cp = safe_strerror(ptp->os_err);
+    strncpy(b, cp, max_b_len);
+    if ((int)strlen(cp) >= max_b_len)
+        b[max_b_len - 1] = '\0';
+    return b;
+}
+
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int verbose)
+{
+    if (vp) { }
+    if (submq) { }
+    if (timeout_secs) { }
+    if (verbose) { }
+    return SCSI_PT_DO_NOT_SUPPORTED;
+}
+
+int
+check_pt_file_handle(int device_fd, const char * device_name, int vb)
+{
+    if (device_fd) {}
+    if (device_name) {}
+    if (vb) {}
+    return 0;
+}
+
+/* Valid file handles (which is the return value) are >= 0 . Returns -1
+ * if there is no valid file handle. */
+int
+get_pt_file_handle(const struct sg_pt_base * vp)
+{   
+    const struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    return ptp->dev_fd;
+}
+
+
+/* If a NVMe block device (which includes the NSID) handle is associated
+ * with 'vp', then its NSID is returned (values range from 0x1 to
+ * 0xffffffe). Otherwise 0 is returned. */
+uint32_t
+get_pt_nvme_nsid(const struct sg_pt_base * vp)
+{
+    if (vp) { }
+    return 0;
+}
+
+/* Forget any previous dev_han and install the one given. May attempt to
+ * find file type (e.g. if pass-though) from OS so there could be an error.
+ * Returns 0 for success or the same value as get_scsi_pt_os_err()
+ * will return. dev_han should be >= 0 for a valid file handle or -1 . */
+int
+set_pt_file_handle(struct sg_pt_base * vp, int dev_han, int vb)
+{
+    struct sg_pt_solaris_scsi * ptp = &vp->impl;
+
+    if (vb) {}
+    ptp->dev_fd = (dev_han < 0) ? -1 : dev_han;
+    ptp->in_err = 0;
+    ptp->os_err = 0;
+    ptp->is_nvme = false;
+    return 0;
+}
diff --git a/lib/sg_pt_win32.c b/lib/sg_pt_win32.c
new file mode 100644
index 0000000..170e33c
--- /dev/null
+++ b/lib/sg_pt_win32.c
@@ -0,0 +1,3155 @@
+/*
+ * Copyright (c) 2006-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/* sg_pt_win32 version 1.34 20210503 */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <fcntl.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_unaligned.h"
+#include "sg_pt.h"
+#include "sg_pt_win32.h"
+#include "sg_pt_nvme.h"
+#include "sg_pr2serr.h"
+
+
+/* Comment the following line out to use the pre-W10 NVMe pass-through */
+#define W10_NVME_NON_PASSTHRU 1
+
+#ifndef O_EXCL
+// #define O_EXCL 0x80  // cygwin ??
+// #define O_EXCL 0x80  // Linux
+#define O_EXCL 0x400    // mingw
+#warning "O_EXCL not defined"
+#endif
+
+#define SCSI_INQUIRY_OPC     0x12
+#define SCSI_REPORT_LUNS_OPC 0xa0
+#define SCSI_TEST_UNIT_READY_OPC  0x0
+#define SCSI_REQUEST_SENSE_OPC  0x3
+#define SCSI_SEND_DIAGNOSTIC_OPC  0x1d
+#define SCSI_RECEIVE_DIAGNOSTIC_OPC  0x1c
+#define SCSI_MAINT_IN_OPC  0xa3
+#define SCSI_REP_SUP_OPCS_OPC  0xc
+#define SCSI_REP_SUP_TMFS_OPC  0xd
+#define SCSI_MODE_SENSE10_OPC  0x5a
+#define SCSI_MODE_SELECT10_OPC  0x55
+
+/* Additional Sense Code (ASC) */
+#define NO_ADDITIONAL_SENSE 0x0
+#define LOGICAL_UNIT_NOT_READY 0x4
+#define LOGICAL_UNIT_COMMUNICATION_FAILURE 0x8
+#define UNRECOVERED_READ_ERR 0x11
+#define PARAMETER_LIST_LENGTH_ERR 0x1a
+#define INVALID_OPCODE 0x20
+#define LBA_OUT_OF_RANGE 0x21
+#define INVALID_FIELD_IN_CDB 0x24
+#define INVALID_FIELD_IN_PARAM_LIST 0x26
+#define UA_RESET_ASC 0x29
+#define UA_CHANGED_ASC 0x2a
+#define TARGET_CHANGED_ASC 0x3f
+#define LUNS_CHANGED_ASCQ 0x0e
+#define INSUFF_RES_ASC 0x55
+#define INSUFF_RES_ASCQ 0x3
+#define LOW_POWER_COND_ON_ASC  0x5e     /* ASCQ=0 */
+#define POWER_ON_RESET_ASCQ 0x0
+#define BUS_RESET_ASCQ 0x2      /* scsi bus reset occurred */
+#define MODE_CHANGED_ASCQ 0x1   /* mode parameters changed */
+#define CAPACITY_CHANGED_ASCQ 0x9
+#define SAVING_PARAMS_UNSUP 0x39
+#define TRANSPORT_PROBLEM 0x4b
+#define THRESHOLD_EXCEEDED 0x5d
+#define LOW_POWER_COND_ON 0x5e
+#define MISCOMPARE_VERIFY_ASC 0x1d
+#define MICROCODE_CHANGED_ASCQ 0x1      /* with TARGET_CHANGED_ASC */
+#define MICROCODE_CHANGED_WO_RESET_ASCQ 0x16
+
+/* Use the Microsoft SCSI Pass Through (SPT) interface. It has two
+ * variants: "SPT" where data is double buffered; and "SPTD" where data
+ * pointers to the user space are passed to the OS. Only Windows
+ * 2000 and later (i.e. not 95,98 or ME).
+ * There is no ASPI interface which relies on a dll from adaptec.
+ * This code uses cygwin facilities and is built in a cygwin
+ * shell. It can be run in a normal DOS shell if the cygwin1.dll
+ * file is put in an appropriate place.
+ * This code can build in a MinGW environment.
+ *
+ * N.B. MSDN says that the "SPT" interface (i.e. double buffered)
+ * should be used for small amounts of data (it says "< 16 KB").
+ * The direct variant (i.e. IOCTL_SCSI_PASS_THROUGH_DIRECT) should
+ * be used for larger amounts of data but the buffer needs to be
+ * "cache aligned". Is that 16 byte alignment or greater?
+ *
+ * This code will default to indirect (i.e. double buffered) access
+ * unless the WIN32_SPT_DIRECT preprocessor constant is defined in
+ * config.h . In version 1.12 runtime selection of direct and indirect
+ * access was added; the default is still determined by the
+ * WIN32_SPT_DIRECT preprocessor constant.
+ */
+
+#define DEF_TIMEOUT 60       /* 60 seconds */
+#define MAX_OPEN_SIMULT 8
+#define WIN32_FDOFFSET 32
+
+union STORAGE_DEVICE_DESCRIPTOR_DATA {
+    STORAGE_DEVICE_DESCRIPTOR desc;
+    char raw[256];
+};
+
+union STORAGE_DEVICE_UID_DATA {
+    STORAGE_DEVICE_UNIQUE_IDENTIFIER desc;
+    char raw[1060];
+};
+
+
+struct sg_pt_handle {
+    bool in_use;
+    bool not_claimed;
+    bool checked_handle;
+    bool bus_type_failed;
+    bool is_nvme;
+    bool got_physical_drive;
+    HANDLE fh;
+    char adapter[32];   /* for example: '\\.\scsi3' */
+    int bus;            /* a.k.a. PathId in MS docs */
+    int target;
+    int lun;
+    int scsi_pdt;       /* Peripheral Device Type, PDT_ALL if not known */
+    // uint32_t nvme_nsid;      /* how do we find this given file handle ?? */
+    int verbose;        /* tunnel verbose through to scsi_pt_close_device */
+    char dname[20];
+    struct sg_sntl_dev_state_t dev_stat;        // owner
+};
+
+/* Start zeroed but need to zeroed before use because could be re-use */
+static struct sg_pt_handle handle_arr[MAX_OPEN_SIMULT];
+
+struct sg_pt_win32_scsi {
+    bool is_nvme;
+    bool nvme_direct;   /* false: our SNTL; true: received NVMe command */
+    bool mdxfer_out;    /* direction of metadata xfer, true->data-out */
+    bool have_nvme_cmd;
+    bool is_read;
+    int sense_len;
+    int scsi_status;
+    int resid;
+    int sense_resid;
+    int in_err;
+    int os_err;                 /* pseudo unix error */
+    int transport_err;          /* windows error number */
+    int dev_fd;                 /* -1 for no "file descriptor" given */
+    uint32_t nvme_nsid;         /* 1 to 0xfffffffe are possibly valid, 0
+                                 * implies dev_fd is not a NVMe device
+                                 * (is_nvme=false) or has no storage (e.g.
+                                 * enclosure rather than disk) */
+    uint32_t nvme_result;       /* DW0 from completion queue */
+    uint32_t nvme_status;       /* SCT|SC: DW3 27:17 from completion queue,
+                                 * note: the DNR+More bit are not there.
+                                 * The whole 16 byte completion q entry is
+                                 * sent back as sense data */
+    uint32_t dxfer_len;
+    uint32_t mdxfer_len;
+    uint8_t * dxferp;
+    uint8_t * mdxferp;          /* NVMe has metadata buffer */
+    uint8_t * sensep;
+    uint8_t * nvme_id_ctlp;
+    uint8_t * free_nvme_id_ctlp;
+    struct sg_sntl_dev_state_t * dev_statp; /* points to handle's dev_stat */
+    uint8_t nvme_cmd[64];
+    union {
+        SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER swb_d;
+        /* Last entry in structure so data buffer can be extended */
+        SCSI_PASS_THROUGH_WITH_BUFFERS swb_i;
+    };
+};
+
+/* embed pointer so can change on fly if (non-direct) data buffer
+ * is not big enough */
+struct sg_pt_base {
+    struct sg_pt_win32_scsi * implp;
+};
+
+#ifdef WIN32_SPT_DIRECT
+static int spt_direct = 1;
+#else
+static int spt_direct = 0;
+#endif
+
+static int nvme_pt(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+                   int time_secs, int vb);
+
+
+/* Request SPT direct interface when state_direct is 1, state_direct set
+ * to 0 for the SPT indirect interface. */
+void
+scsi_pt_win32_direct(int state_direct)
+{
+    spt_direct = state_direct;
+}
+
+/* Returns current SPT interface state, 1 for direct, 0 for indirect */
+int
+scsi_pt_win32_spt_state(void)
+{
+    return spt_direct;
+}
+
+static const char *
+bus_type_str(int bt)
+{
+    switch (bt)
+    {
+    case BusTypeUnknown:
+        return "Unknown";
+    case BusTypeScsi:
+        return "Scsi";
+    case BusTypeAtapi:
+        return "Atapi";
+    case BusTypeAta:
+        return "Ata";
+    case BusType1394:
+        return "1394";
+    case BusTypeSsa:
+        return "Ssa";
+    case BusTypeFibre:
+        return "Fibre";
+    case BusTypeUsb:
+        return "Usb";
+    case BusTypeRAID:
+        return "RAID";
+    case BusTypeiScsi:
+        return "iScsi";
+    case BusTypeSas:
+        return "Sas";
+    case BusTypeSata:
+        return "Sata";
+    case BusTypeSd:
+        return "Sd";
+    case BusTypeMmc:
+        return "Mmc";
+    case BusTypeVirtual:
+        return "Virt";
+    case BusTypeFileBackedVirtual:
+        return "FBVir";
+#ifdef BusTypeSpaces
+    case BusTypeSpaces:
+#else
+    case 0x10:
+#endif
+        return "Spaces";
+#ifdef BusTypeNvme
+    case BusTypeNvme:
+#else
+    case 0x11:
+#endif
+        return "NVMe";
+#ifdef BusTypeSCM
+    case BusTypeSCM:
+#else
+    case 0x12:
+#endif
+        return "SCM";
+#ifdef BusTypeUfs
+    case BusTypeUfs:
+#else
+    case 0x13:
+#endif
+        return "Ufs";
+    case 0x14:
+        return "Max";
+    case 0x7f:
+        return "Max Reserved";
+    default:
+        return "_unknown";
+    }
+}
+
+static char *
+get_err_str(DWORD err, int max_b_len, char * b)
+{
+    LPVOID lpMsgBuf;
+    int k, num, ch;
+
+    memset(b, 0, max_b_len);
+    FormatMessage(
+        FORMAT_MESSAGE_ALLOCATE_BUFFER |
+        FORMAT_MESSAGE_FROM_SYSTEM,
+        NULL,
+        err,
+        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+        (LPTSTR) &lpMsgBuf,
+        0, NULL );
+    num = lstrlen((LPCTSTR)lpMsgBuf);
+    if (num < 1)
+        return b;
+    num = (num < max_b_len) ? num : (max_b_len - 1);
+    for (k = 0; k < num; ++k) {
+        ch = *((LPCTSTR)lpMsgBuf + k);
+        if ((ch >= 0x0) && (ch < 0x7f))
+            b[k] = ch & 0x7f;
+        else
+            b[k] = '?';
+    }
+    return b;
+}
+
+/* Returns pointer to sg_pt_handle object given Unix like device_fd. If
+ * device_fd is invalid or not open returns NULL. If psp is non-NULL and
+ * NULL is returned then ENODEV is placed in psp->os_err. */
+static struct sg_pt_handle *
+get_open_pt_handle(struct sg_pt_win32_scsi * psp, int device_fd, bool vbb)
+{
+    int index = device_fd - WIN32_FDOFFSET;
+    struct sg_pt_handle * shp;
+
+    if ((index < 0) || (index >= WIN32_FDOFFSET)) {
+        if (vbb)
+            pr2ws("Bad file descriptor\n");
+        if (psp)
+            psp->os_err = EBADF;
+        return NULL;
+    }
+    shp = handle_arr + index;
+    if (! shp->in_use) {
+        if (vbb)
+            pr2ws("File descriptor closed??\n");
+        if (psp)
+            psp->os_err = ENODEV;
+        return NULL;
+    }
+    return shp;
+}
+
+
+/* Returns >= 0 if successful. If error in Unix returns negated errno. */
+int
+scsi_pt_open_device(const char * device_name, bool read_only, int vb)
+{
+    int oflags = 0 /* O_NONBLOCK*/ ;
+
+    oflags |= (read_only ? 0 : 0);      /* was ... ? O_RDONLY : O_RDWR) */
+    return scsi_pt_open_flags(device_name, oflags, vb);
+}
+
+/*
+ * Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed
+ * together. The 'flags' argument is ignored in Windows.
+ * Returns >= 0 if successful, otherwise returns negated errno.
+ * Optionally accept leading "\\.\". If given something of the form
+ * "SCSI<num>:<bus>,<target>,<lun>" where the values in angle brackets
+ * are integers, then will attempt to open "\\.\SCSI<num>:" and save the
+ * other three values for the DeviceIoControl call. The trailing ".<lun>"
+ * is optionally and if not given 0 is assumed. Since "PhysicalDrive"
+ * is a lot of keystrokes, "PD" is accepted and converted to the longer
+ * form.
+ */
+int
+scsi_pt_open_flags(const char * device_name, int flags, int vb)
+{
+    bool got_scsi_name = false;
+    int len, k, adapter_num, bus, target, lun, off, index, num, pd_num;
+    int share_mode;
+    struct sg_pt_handle * shp;
+    char buff[8];
+
+    share_mode = (O_EXCL & flags) ? 0 : (FILE_SHARE_READ | FILE_SHARE_WRITE);
+    /* lock */
+    for (k = 0; k < MAX_OPEN_SIMULT; k++)
+        if (! handle_arr[k].in_use)
+            break;
+    if (k == MAX_OPEN_SIMULT) {
+        if (vb)
+            pr2ws("too many open handles (%d)\n", MAX_OPEN_SIMULT);
+        return -EMFILE;
+    } else {
+        /* clear any previous contents */
+        memset(handle_arr + k, 0, sizeof(struct sg_pt_handle));
+        handle_arr[k].in_use = true;
+    }
+    /* unlock */
+    index = k;
+    shp = handle_arr + index;
+#if (HAVE_NVME && (! IGNORE_NVME))
+    sntl_init_dev_stat(&shp->dev_stat);
+#endif
+    adapter_num = 0;
+    bus = 0;    /* also known as 'PathId' in MS docs */
+    target = 0;
+    lun = 0;
+    len = (int)strlen(device_name);
+    k = (int)sizeof(shp->dname);
+    if (len < k)
+        strcpy(shp->dname, device_name);
+    else if (len == k)
+        memcpy(shp->dname, device_name, k - 1);
+    else        /* trim on left */
+        memcpy(shp->dname, device_name + (len - k), k - 1);
+    shp->dname[k - 1] = '\0';
+    if ((len > 4) && (0 == strncmp("\\\\.\\", device_name, 4)))
+        off = 4;
+    else
+        off = 0;
+    if (len > (off + 2)) {
+        buff[0] = toupper((int)device_name[off + 0]);
+        buff[1] = toupper((int)device_name[off + 1]);
+        if (0 == strncmp("PD", buff, 2)) {
+            num = sscanf(device_name + off + 2, "%d", &pd_num);
+            if (1 == num)
+                shp->got_physical_drive = true;
+        }
+        if (! shp->got_physical_drive) {
+            buff[2] = toupper((int)device_name[off + 2]);
+            buff[3] = toupper((int)device_name[off + 3]);
+            if (0 == strncmp("SCSI", buff, 4)) {
+                num = sscanf(device_name + off + 4, "%d:%d,%d,%d",
+                             &adapter_num, &bus, &target, &lun);
+                if (num < 3) {
+                    if (vb)
+                        pr2ws("expected format like: "
+                              "'SCSI<port>:<bus>,<target>[,<lun>]'\n");
+                    shp->in_use = false;
+                    return -EINVAL;
+                }
+                got_scsi_name = true;
+            }
+        }
+    }
+    shp->bus = bus;
+    shp->target = target;
+    shp->lun = lun;
+    shp->scsi_pdt = PDT_ALL;
+    shp->verbose = vb;
+    memset(shp->adapter, 0, sizeof(shp->adapter));
+    memcpy(shp->adapter, "\\\\.\\", 4);
+    if (shp->got_physical_drive)
+        snprintf(shp->adapter + 4, sizeof(shp->adapter) - 5,
+                 "PhysicalDrive%d", pd_num);
+    else if (got_scsi_name)
+        snprintf(shp->adapter + 4, sizeof(shp->adapter) - 5, "SCSI%d:",
+                 adapter_num);
+    else
+        snprintf(shp->adapter + 4, sizeof(shp->adapter) - 5, "%s",
+                 device_name + off);
+    if (vb > 4)
+        pr2ws("%s: CreateFile('%s'), bus=%d, target=%d, lun=%d\n", __func__,
+              shp->adapter, bus, target, lun);
+#if 1
+    shp->fh = CreateFile(shp->adapter, GENERIC_READ | GENERIC_WRITE,
+                         share_mode, NULL, OPEN_EXISTING, 0, NULL);
+#endif
+
+#if 0
+    shp->fh = CreateFileA(shp->adapter, GENERIC_READ|GENERIC_WRITE,
+    FILE_SHARE_READ|FILE_SHARE_WRITE,
+    (SECURITY_ATTRIBUTES *)0, OPEN_EXISTING, 0, 0);
+  // No GENERIC_READ/WRITE access required, works without admin rights (W10)
+    shp->fh = CreateFileA(shp->adapter, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
+                      (SECURITY_ATTRIBUTES *)0, OPEN_EXISTING, 0, (HANDLE)0);
+#endif
+    if (shp->fh == INVALID_HANDLE_VALUE) {
+        if (vb) {
+            uint32_t err = (uint32_t)GetLastError();
+            char b[128];
+
+            pr2ws("%s: CreateFile error: %s [%u]\n", __func__,
+                  get_err_str(err, sizeof(b), b), err);
+        }
+        shp->in_use = false;
+        return -ENODEV;
+    }
+    return index + WIN32_FDOFFSET;
+}
+
+/* Returns 0 if successful. If device_id seems wild returns -ENODEV,
+ * other errors return 0. If CloseHandle() fails and verbose > 0 then
+ * outputs warning with value from GetLastError(). The verbose value
+ * defaults to zero and is potentially set from the most recent call
+ * to scsi_pt_open_device() or do_scsi_pt(). */
+int
+scsi_pt_close_device(int device_fd)
+{
+    struct sg_pt_handle * shp = get_open_pt_handle(NULL, device_fd, false);
+
+    if (NULL == shp)
+        return -ENODEV;
+    if ((! CloseHandle(shp->fh)) && shp->verbose)
+        pr2ws("Windows CloseHandle error=%u\n", (unsigned int)GetLastError());
+    shp->bus = 0;
+    shp->target = 0;
+    shp->lun = 0;
+    memset(shp->adapter, 0, sizeof(shp->adapter));
+    shp->in_use = false;
+    shp->verbose = 0;
+    shp->dname[0] = '\0';
+    return 0;
+}
+
+/* Attempt to return device's SCSI peripheral device type (pdt), a number
+ * between 0 (disks) and 31 (not given) by calling IOCTL_SCSI_GET_INQUIRY_DATA
+ * on the adapter. Returns -EIO on error and -999 if not found. */
+static int
+get_scsi_pdt(struct sg_pt_handle *shp, int vb)
+{
+    const int alloc_sz = 8192;
+    int j;
+    int ret = -999;
+    BOOL ok;
+    ULONG dummy;
+    DWORD err;
+    BYTE wbus;
+    uint8_t * inqBuf;
+    uint8_t * free_inqBuf;
+    char b[128];
+
+    if (vb > 2)
+        pr2ws("%s: enter, adapter: %s\n", __func__, shp->adapter);
+    inqBuf = sg_memalign(alloc_sz, 0 /* page size */, &free_inqBuf, false);
+    if (NULL == inqBuf) {
+        pr2ws("%s: unable to allocate %d bytes\n", __func__, alloc_sz);
+        return -ENOMEM;
+    }
+    ok = DeviceIoControl(shp->fh, IOCTL_SCSI_GET_INQUIRY_DATA,
+                         NULL, 0, inqBuf, alloc_sz, &dummy, NULL);
+    if (ok) {
+        PSCSI_ADAPTER_BUS_INFO  ai;
+        PSCSI_BUS_DATA pbd;
+        PSCSI_INQUIRY_DATA pid;
+        int num_lus, off;
+
+        ai = (PSCSI_ADAPTER_BUS_INFO)inqBuf;
+        for (wbus = 0; wbus < ai->NumberOfBusses; ++wbus) {
+            pbd = ai->BusData + wbus;
+            num_lus = pbd->NumberOfLogicalUnits;
+            off = pbd->InquiryDataOffset;
+            for (j = 0; j < num_lus; ++j) {
+                if ((off < (int)sizeof(SCSI_ADAPTER_BUS_INFO)) ||
+                    (off > (alloc_sz - (int)sizeof(SCSI_INQUIRY_DATA))))
+                    break;
+                pid = (PSCSI_INQUIRY_DATA)(inqBuf + off);
+                if ((shp->bus == pid->PathId) &&
+                    (shp->target == pid->TargetId) &&
+                    (shp->lun == pid->Lun)) {   /* got match */
+                    shp->scsi_pdt = pid->InquiryData[0] & PDT_MASK;
+                    shp->not_claimed = ! pid->DeviceClaimed;
+                    shp->checked_handle = true;
+                    shp->bus_type_failed = false;
+                    if (vb > 3)
+                        pr2ws("%s: found, scsi_pdt=%d, claimed=%d, "
+                             "target=%d, lun=%d\n", __func__, shp->scsi_pdt,
+                             pid->DeviceClaimed, shp->target, shp->lun);
+                    ret = shp->scsi_pdt;
+                    goto fini;
+                }
+                off = pid->NextInquiryDataOffset;
+            }
+        }
+    } else {
+        err = GetLastError();
+        if (vb > 1)
+            pr2ws("%s: IOCTL_SCSI_GET_INQUIRY_DATA failed err=%u\n\t%s",
+                  shp->adapter, (unsigned int)err,
+                  get_err_str(err, sizeof(b), b));
+        ret = -EIO;
+    }
+fini:
+    if (free_inqBuf)
+        free(free_inqBuf);
+    return ret; /* no match after checking all PathIds, Targets and LUs */
+}
+
+/* Returns 0 on success, negated errno if error */
+static int
+get_bus_type(struct sg_pt_handle *shp, const char *dname,
+             STORAGE_BUS_TYPE * btp, int vb)
+{
+    DWORD num_out, err;
+    STORAGE_BUS_TYPE bt;
+    union STORAGE_DEVICE_DESCRIPTOR_DATA sddd;
+    STORAGE_PROPERTY_QUERY query = {StorageDeviceProperty,
+                                    PropertyStandardQuery, {0} };
+    char b[256];
+
+    memset(&sddd, 0, sizeof(sddd));
+    if (! DeviceIoControl(shp->fh, IOCTL_STORAGE_QUERY_PROPERTY,
+                          &query, sizeof(query), &sddd, sizeof(sddd),
+                          &num_out, NULL)) {
+        if (vb > 2) {
+            err = GetLastError();
+            pr2ws("%s  IOCTL_STORAGE_QUERY_PROPERTY(Devprop) failed, "
+                  "Error: %s [%u]\n", dname, get_err_str(err, sizeof(b), b),
+                  (uint32_t)err);
+        }
+        shp->bus_type_failed = true;
+        return -EIO;
+    }
+    bt = sddd.desc.BusType;
+    if (vb > 2) {
+        pr2ws("%s: Bus type: %s\n", __func__, bus_type_str((int)bt));
+        if (vb > 3) {
+            pr2ws("Storage Device Descriptor Data:\n");
+            hex2stderr((const uint8_t *)&sddd, num_out, 0);
+        }
+    }
+    if (shp) {
+        shp->checked_handle = true;
+        shp->bus_type_failed = false;
+        shp->is_nvme = (BusTypeNvme == bt);
+    }
+    if (btp)
+        *btp = bt;
+    return 0;
+}
+
+/* Assumes dev_fd is an "open" file handle associated with device_name. If
+ * the implementation (possibly for one OS) cannot determine from dev_fd if
+ * a SCSI or NVMe pass-through is referenced, then it might guess based on
+ * device_name. Returns 1 if SCSI generic pass-though device, returns 2 if
+ * secondary SCSI pass-through device (in Linux a bsg device); returns 3 is
+ * char NVMe device (i.e. no NSID); returns 4 if block NVMe device (includes
+ * NSID), or 0 if something else (e.g. ATA block device) or dev_fd < 0.
+ * If error, returns negated errno (operating system) value. */
+int
+check_pt_file_handle(int device_fd, const char * device_name, int vb)
+{
+    int res;
+    STORAGE_BUS_TYPE bt;
+    const char * dnp = device_name;
+    struct sg_pt_handle * shp;
+
+    if (vb > 3)
+        pr2ws("%s: device_name: %s\n", __func__, dnp);
+    shp = get_open_pt_handle(NULL, device_fd, vb > 1);
+    if (NULL == shp) {
+        pr2ws("%s: device_fd (%s) bad or not in_use ??\n", __func__,
+              dnp ? dnp : "");
+        return -ENODEV;
+    }
+    if (shp->bus_type_failed) {
+        if (vb > 2)
+            pr2ws("%s: skip because get_bus_type() has failed\n", __func__);
+        return 0;
+    }
+    dnp = dnp ? dnp : shp->dname;
+    res = get_bus_type(shp, dnp, &bt, vb);
+    if (res < 0) {
+        if (! shp->got_physical_drive) {
+            res = get_scsi_pdt(shp, vb);
+            if (res >= 0)
+                return 1;
+        }
+        return res;
+    }
+    return (BusTypeNvme == bt) ? 3 : 1;
+    /* NVMe "char" ?? device, could be enclosure: 3 */
+    /* SCSI generic pass-though device: 1 */
+}
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+static bool checked_ev_dsense = false;
+static bool ev_dsense = false;
+#endif
+
+struct sg_pt_base *
+construct_scsi_pt_obj_with_fd(int dev_fd, int vb)
+{
+    int res;
+    struct sg_pt_win32_scsi * psp;
+    struct sg_pt_base * vp = NULL;
+    struct sg_pt_handle * shp = NULL;
+
+    if (dev_fd >= 0) {
+        shp = get_open_pt_handle(NULL, dev_fd, vb > 1);
+        if (NULL == shp) {
+            if (vb)
+                pr2ws("%s: dev_fd is not open\n", __func__);
+            return NULL;
+        }
+        if (! (shp->bus_type_failed || shp->checked_handle)) {
+            res = get_bus_type(shp, shp->dname, NULL, vb);
+            if (res < 0) {
+                if (! shp->got_physical_drive)
+                    res = get_scsi_pdt(shp, vb);
+                if ((res < 0) && (vb > 1))
+                    pr2ws("%s: get_bus_type() errno=%d, continue\n", __func__,
+                          -res);
+            }
+        }
+    }
+    psp = (struct sg_pt_win32_scsi *)calloc(sizeof(struct sg_pt_win32_scsi),
+                                            1);
+    if (psp) {
+        psp->dev_fd = (dev_fd < 0) ? -1 : dev_fd;
+        if (shp) {
+            psp->is_nvme = shp->is_nvme;
+            psp->dev_statp = &shp->dev_stat;
+#if (HAVE_NVME && (! IGNORE_NVME))
+            sntl_init_dev_stat(psp->dev_statp);
+            if (! checked_ev_dsense) {
+                ev_dsense = sg_get_initial_dsense();
+                checked_ev_dsense = true;
+            }
+            shp->dev_stat.scsi_dsense = ev_dsense;
+#endif
+        }
+        if (psp->is_nvme) {
+            ; /* should be 'psp->nvme_nsid = shp->nvme_nsid' */
+        } else if (spt_direct) {
+            psp->swb_d.spt.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
+            psp->swb_d.spt.SenseInfoLength = SCSI_MAX_SENSE_LEN;
+            psp->swb_d.spt.SenseInfoOffset =
+                offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucSenseBuf);
+            psp->swb_d.spt.TimeOutValue = DEF_TIMEOUT;
+        } else {
+            psp->swb_i.spt.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
+            psp->swb_i.spt.SenseInfoLength = SCSI_MAX_SENSE_LEN;
+            psp->swb_i.spt.SenseInfoOffset =
+                offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucSenseBuf);
+            psp->swb_i.spt.TimeOutValue = DEF_TIMEOUT;
+        }
+        vp = (struct sg_pt_base *)malloc(sizeof(struct sg_pt_win32_scsi *));
+        /* yes, allocating the size of a pointer (4 or 8 bytes) */
+        if (vp)
+            vp->implp = psp;
+        else
+            free(psp);
+    }
+    if ((NULL == vp) && vb)
+        pr2ws("%s: about to return NULL, space problem\n", __func__);
+    return vp;
+}
+
+struct sg_pt_base *
+construct_scsi_pt_obj(void)
+{
+    return construct_scsi_pt_obj_with_fd(-1, 0);
+}
+
+void
+destruct_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    if (vp) {
+        struct sg_pt_win32_scsi * psp = vp->implp;
+
+        if (psp) {
+            free(psp);
+        }
+        free(vp);
+    }
+}
+
+/* Forget any previous dev_han and install the one given. May attempt to
+ * find file type (e.g. if pass-though) from OS so there could be an error.
+ * Returns 0 for success or the same value as get_scsi_pt_os_err()
+ * will return. dev_han should be >= 0 for a valid file handle or -1 . */
+int
+set_pt_file_handle(struct sg_pt_base * vp, int dev_han, int vb)
+{
+    int res;
+    struct sg_pt_win32_scsi * psp;
+
+    if (NULL == vp) {
+        if (vb)
+            pr2ws(">>>> %s: pointer to object is NULL\n", __func__);
+        return EINVAL;
+    }
+    if ((psp = vp->implp)) {
+        struct sg_pt_handle * shp;
+
+        if (dev_han < 0) {
+            psp->dev_fd = -1;
+            psp->is_nvme = false;
+            psp->nvme_nsid = 0;
+            return 0;
+        }
+        shp = get_open_pt_handle(psp, dev_han, vb > 1);
+        if (NULL == shp) {
+            if (vb)
+                pr2ws("%s: dev_han (%d) is invalid\n", __func__, dev_han);
+            psp->os_err = EINVAL;
+            return psp->os_err;
+        }
+        psp->os_err = 0;
+        psp->transport_err = 0;
+        psp->in_err = 0;
+        psp->scsi_status = 0;
+        psp->dev_fd = dev_han;
+        if (! (shp->bus_type_failed || shp->checked_handle)) {
+            res = get_bus_type(shp, shp->dname, NULL, vb);
+            if (res < 0) {
+                res = get_scsi_pdt(shp, vb);
+                if (res >= 0)   /* clears shp->bus_type_failed on success */
+                    psp->os_err = 0;
+            }
+            if ((res < 0) && (vb > 2))
+                pr2ws("%s: get_bus_type() errno=%d\n", __func__, -res);
+        }
+        if (shp->bus_type_failed)
+            psp->os_err = EIO;
+        if (psp->os_err)
+            return psp->os_err;
+        psp->is_nvme = shp->is_nvme;
+        psp->nvme_nsid = 0;  /* should be 'psp->nvme_nsid = shp->nvme_nsid' */
+        psp->dev_statp = &shp->dev_stat;
+    }
+    return 0;
+}
+
+/* Valid file handles (which is the return value) are >= 0 . Returns -1
+ * if there is no valid file handle. */
+int
+get_pt_file_handle(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp;
+
+    if (vp) {
+        psp = vp->implp;
+        return psp ? psp->dev_fd : -1;
+    }
+    return -1;
+}
+
+/* Keep state information such as dev_fd and nvme_nsid */
+void
+clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    bool is_nvme;
+    int dev_fd;
+    uint32_t nvme_nsid;
+    struct sg_pt_win32_scsi * psp = vp->implp;
+    struct sg_sntl_dev_state_t * dsp;
+
+    if (psp) {
+        dev_fd = psp->dev_fd;
+        is_nvme = psp->is_nvme;
+        nvme_nsid = psp->nvme_nsid;
+        dsp = psp->dev_statp;
+        memset(psp, 0, sizeof(struct sg_pt_win32_scsi));
+        if (spt_direct) {
+            psp->swb_d.spt.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
+            psp->swb_d.spt.SenseInfoLength = SCSI_MAX_SENSE_LEN;
+            psp->swb_d.spt.SenseInfoOffset =
+                offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucSenseBuf);
+            psp->swb_d.spt.TimeOutValue = DEF_TIMEOUT;
+        } else {
+            psp->swb_i.spt.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
+            psp->swb_i.spt.SenseInfoLength = SCSI_MAX_SENSE_LEN;
+            psp->swb_i.spt.SenseInfoOffset =
+                offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucSenseBuf);
+            psp->swb_i.spt.TimeOutValue = DEF_TIMEOUT;
+        }
+        psp->dev_fd = dev_fd;
+        psp->is_nvme = is_nvme;
+        psp->nvme_nsid = nvme_nsid;
+        psp->dev_statp = dsp;
+    }
+}
+
+void
+partial_clear_scsi_pt_obj(struct sg_pt_base * vp)
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (NULL == psp)
+        return;
+    psp->in_err = 0;
+    psp->os_err = 0;
+    psp->transport_err = 0;
+    psp->scsi_status = 0;
+    if (spt_direct) {
+        psp->swb_d.spt.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
+        psp->swb_d.spt.SenseInfoLength = SCSI_MAX_SENSE_LEN;
+        psp->swb_d.spt.SenseInfoOffset =
+            offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucSenseBuf);
+        psp->swb_d.spt.TimeOutValue = DEF_TIMEOUT;
+    } else {
+        psp->swb_i.spt.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
+        psp->swb_i.spt.SenseInfoLength = SCSI_MAX_SENSE_LEN;
+        psp->swb_i.spt.SenseInfoOffset =
+            offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucSenseBuf);
+        psp->swb_i.spt.TimeOutValue = DEF_TIMEOUT;
+    }
+}
+
+void
+set_scsi_pt_cdb(struct sg_pt_base * vp, const uint8_t * cdb,
+                int cdb_len)
+{
+    bool scsi_cdb = sg_is_scsi_cdb(cdb, cdb_len);
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (! scsi_cdb) {
+        psp->have_nvme_cmd = true;
+        memcpy(psp->nvme_cmd, cdb, cdb_len);
+    } else if (spt_direct) {
+        if (cdb_len > (int)sizeof(psp->swb_d.spt.Cdb)) {
+            ++psp->in_err;
+            return;
+        }
+        memcpy(psp->swb_d.spt.Cdb, cdb, cdb_len);
+        psp->swb_d.spt.CdbLength = cdb_len;
+    } else {
+        if (cdb_len > (int)sizeof(psp->swb_i.spt.Cdb)) {
+            ++psp->in_err;
+            return;
+        }
+        memcpy(psp->swb_i.spt.Cdb, cdb, cdb_len);
+        psp->swb_i.spt.CdbLength = cdb_len;
+    }
+}
+
+int
+get_scsi_pt_cdb_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    return spt_direct ? psp->swb_d.spt.CdbLength : psp->swb_i.spt.CdbLength;
+}
+
+uint8_t *
+get_scsi_pt_cdb_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (spt_direct) {
+        if (psp->swb_d.spt.CdbLength > 0)
+            return (uint8_t *)(psp->swb_d.spt.Cdb);
+        else
+            return NULL;
+    } else {
+        if (psp->swb_i.spt.CdbLength > 0)
+            return (uint8_t *)(psp->swb_i.spt.Cdb);
+        else
+            return NULL;
+    }
+}
+
+void
+set_scsi_pt_sense(struct sg_pt_base * vp, uint8_t * sense, int sense_len)
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (sense && (sense_len > 0))
+        memset(sense, 0, sense_len);
+    psp->sensep = sense;
+    psp->sense_len = sense_len;
+}
+
+/* from device */
+void
+set_scsi_pt_data_in(struct sg_pt_base * vp, uint8_t * dxferp,
+                    int dxfer_len)
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (psp->dxferp)
+        ++psp->in_err;
+    if (dxfer_len > 0) {
+        psp->dxferp = dxferp;
+        psp->dxfer_len = (uint32_t)dxfer_len;
+        psp->is_read = true;
+        if (spt_direct)
+            psp->swb_d.spt.DataIn = SCSI_IOCTL_DATA_IN;
+        else
+            psp->swb_i.spt.DataIn = SCSI_IOCTL_DATA_IN;
+    }
+}
+
+/* to device */
+void
+set_scsi_pt_data_out(struct sg_pt_base * vp, const uint8_t * dxferp,
+                     int dxfer_len)
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (psp->dxferp)
+        ++psp->in_err;
+    if (dxfer_len > 0) {
+        psp->dxferp = (uint8_t *)dxferp;
+        psp->dxfer_len = (uint32_t)dxfer_len;
+        if (spt_direct)
+            psp->swb_d.spt.DataIn = SCSI_IOCTL_DATA_OUT;
+        else
+            psp->swb_i.spt.DataIn = SCSI_IOCTL_DATA_OUT;
+    }
+}
+
+void
+set_pt_metadata_xfer(struct sg_pt_base * vp, uint8_t * mdxferp,
+                     uint32_t mdxfer_len, bool out_true)
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (psp->mdxferp)
+        ++psp->in_err;
+    if (mdxfer_len > 0) {
+        psp->mdxferp = mdxferp;
+        psp->mdxfer_len = mdxfer_len;
+        psp->mdxfer_out = out_true;
+    }
+}
+
+void
+set_scsi_pt_packet_id(struct sg_pt_base * vp __attribute__ ((unused)),
+                      int pack_id __attribute__ ((unused)))
+{
+}
+
+void
+set_scsi_pt_tag(struct sg_pt_base * vp, uint64_t tag __attribute__ ((unused)))
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    ++psp->in_err;
+}
+
+void
+set_scsi_pt_task_management(struct sg_pt_base * vp,
+                            int tmf_code __attribute__ ((unused)))
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    ++psp->in_err;
+}
+
+void
+set_scsi_pt_task_attr(struct sg_pt_base * vp,
+                      int attrib __attribute__ ((unused)),
+                      int priority __attribute__ ((unused)))
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    ++psp->in_err;
+}
+
+void
+set_scsi_pt_flags(struct sg_pt_base * objp, int flags)
+{
+    /* do nothing, suppress warnings */
+    objp = objp;
+    flags = flags;
+}
+
+/* Executes SCSI command (or at least forwards it to lower layers)
+ * using direct interface. Clears os_err field prior to active call (whose
+ * result may set it again). */
+static int
+scsi_pt_direct(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+               int time_secs, int vb)
+{
+    BOOL status;
+    DWORD returned;
+
+    psp->os_err = 0;
+    if (0 == psp->swb_d.spt.CdbLength) {
+        if (vb)
+            pr2ws("%s: No command (cdb) given\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    psp->swb_d.spt.Length = sizeof (SCSI_PASS_THROUGH_DIRECT);
+    psp->swb_d.spt.PathId = shp->bus;
+    psp->swb_d.spt.TargetId = shp->target;
+    psp->swb_d.spt.Lun = shp->lun;
+    psp->swb_d.spt.TimeOutValue = time_secs;
+    psp->swb_d.spt.DataTransferLength = psp->dxfer_len;
+    if (vb > 4) {
+        pr2ws(" spt_direct, adapter: %s  Length=%d ScsiStatus=%d PathId=%d "
+              "TargetId=%d Lun=%d\n", shp->adapter,
+              (int)psp->swb_d.spt.Length, (int)psp->swb_d.spt.ScsiStatus,
+              (int)psp->swb_d.spt.PathId, (int)psp->swb_d.spt.TargetId,
+              (int)psp->swb_d.spt.Lun);
+        pr2ws("    CdbLength=%d SenseInfoLength=%d DataIn=%d "
+              "DataTransferLength=%u\n",
+              (int)psp->swb_d.spt.CdbLength,
+              (int)psp->swb_d.spt.SenseInfoLength,
+              (int)psp->swb_d.spt.DataIn,
+              (unsigned int)psp->swb_d.spt.DataTransferLength);
+        pr2ws("    TimeOutValue=%u SenseInfoOffset=%u\n",
+              (unsigned int)psp->swb_d.spt.TimeOutValue,
+              (unsigned int)psp->swb_d.spt.SenseInfoOffset);
+    }
+    psp->swb_d.spt.DataBuffer = psp->dxferp;
+    status = DeviceIoControl(shp->fh, IOCTL_SCSI_PASS_THROUGH_DIRECT,
+                            &psp->swb_d,
+                            sizeof(psp->swb_d),
+                            &psp->swb_d,
+                            sizeof(psp->swb_d),
+                            &returned,
+                            NULL);
+    if (! status) {
+        unsigned int u;
+
+        u = (unsigned int)GetLastError();
+        if (vb) {
+            char b[128];
+
+            pr2ws("%s: DeviceIoControl: %s [%u]\n", __func__,
+                  get_err_str(u, sizeof(b), b), u);
+        }
+        psp->transport_err = (int)u;
+        psp->os_err = EIO;
+        return 0;       /* let app find transport error */
+    }
+
+    psp->scsi_status = psp->swb_d.spt.ScsiStatus;
+    if ((SAM_STAT_CHECK_CONDITION == psp->scsi_status) ||
+        (SAM_STAT_COMMAND_TERMINATED == psp->scsi_status))
+        memcpy(psp->sensep, psp->swb_d.ucSenseBuf, psp->sense_len);
+    else
+        psp->sense_len = 0;
+    psp->sense_resid = 0;
+    if ((psp->dxfer_len > 0) && (psp->swb_d.spt.DataTransferLength > 0))
+        psp->resid = psp->dxfer_len - psp->swb_d.spt.DataTransferLength;
+    else
+        psp->resid = 0;
+
+    return 0;
+}
+
+/* Executes SCSI command (or at least forwards it to lower layers) using
+ * indirect interface. Clears os_err field prior to active call (whose
+ * result may set it again). */
+static int
+scsi_pt_indirect(struct sg_pt_base * vp, struct sg_pt_handle * shp,
+                 int time_secs, int vb)
+{
+    BOOL status;
+    DWORD returned;
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    psp->os_err = 0;
+    if (0 == psp->swb_i.spt.CdbLength) {
+        if (vb)
+            pr2ws("%s: No command (cdb) given\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (psp->dxfer_len > (int)sizeof(psp->swb_i.ucDataBuf)) {
+        int extra = psp->dxfer_len - (int)sizeof(psp->swb_i.ucDataBuf);
+        struct sg_pt_win32_scsi * epsp;
+
+        if (vb > 4)
+            pr2ws("spt_indirect: dxfer_len (%d) too large for initial data\n"
+                  "  buffer (%d bytes), try enlarging\n", psp->dxfer_len,
+                  (int)sizeof(psp->swb_i.ucDataBuf));
+        epsp = (struct sg_pt_win32_scsi *)
+               calloc(sizeof(struct sg_pt_win32_scsi) + extra, 1);
+        if (NULL == epsp) {
+            pr2ws("%s: failed to enlarge data buffer to %d bytes\n", __func__,
+                  psp->dxfer_len);
+            psp->os_err = ENOMEM;
+            return -psp->os_err;
+        }
+        memcpy(epsp, psp, sizeof(struct sg_pt_win32_scsi));
+        free(psp);
+        vp->implp = epsp;
+        psp = epsp;
+    }
+    psp->swb_i.spt.Length = sizeof (SCSI_PASS_THROUGH);
+    psp->swb_i.spt.DataBufferOffset =
+                offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucDataBuf);
+    psp->swb_i.spt.PathId = shp->bus;
+    psp->swb_i.spt.TargetId = shp->target;
+    psp->swb_i.spt.Lun = shp->lun;
+    psp->swb_i.spt.TimeOutValue = time_secs;
+    psp->swb_i.spt.DataTransferLength = psp->dxfer_len;
+    if (vb > 4) {
+        pr2ws(" spt_indirect, adapter: %s  Length=%d ScsiStatus=%d PathId=%d "
+              "TargetId=%d Lun=%d\n", shp->adapter,
+              (int)psp->swb_i.spt.Length, (int)psp->swb_i.spt.ScsiStatus,
+              (int)psp->swb_i.spt.PathId, (int)psp->swb_i.spt.TargetId,
+              (int)psp->swb_i.spt.Lun);
+        pr2ws("    CdbLength=%d SenseInfoLength=%d DataIn=%d "
+              "DataTransferLength=%u\n",
+              (int)psp->swb_i.spt.CdbLength,
+              (int)psp->swb_i.spt.SenseInfoLength,
+              (int)psp->swb_i.spt.DataIn,
+              (unsigned int)psp->swb_i.spt.DataTransferLength);
+        pr2ws("    TimeOutValue=%u DataBufferOffset=%u "
+              "SenseInfoOffset=%u\n",
+              (unsigned int)psp->swb_i.spt.TimeOutValue,
+              (unsigned int)psp->swb_i.spt.DataBufferOffset,
+              (unsigned int)psp->swb_i.spt.SenseInfoOffset);
+    }
+    if ((psp->dxfer_len > 0) &&
+        (SCSI_IOCTL_DATA_OUT == psp->swb_i.spt.DataIn))
+        memcpy(psp->swb_i.ucDataBuf, psp->dxferp, psp->dxfer_len);
+    status = DeviceIoControl(shp->fh, IOCTL_SCSI_PASS_THROUGH,
+                            &psp->swb_i,
+                            sizeof(psp->swb_i),
+                            &psp->swb_i,
+                            sizeof(psp->swb_i),
+                            &returned,
+                            NULL);
+    if (! status) {
+        uint32_t u = (uint32_t)GetLastError();
+
+        if (vb) {
+            char b[128];
+
+            pr2ws("%s: DeviceIoControl: %s [%u]\n", __func__,
+                  get_err_str(u, sizeof(b), b), u);
+        }
+        psp->transport_err = (int)u;
+        psp->os_err = EIO;
+        return 0;       /* let app find transport error */
+    }
+    if ((psp->dxfer_len > 0) && (SCSI_IOCTL_DATA_IN == psp->swb_i.spt.DataIn))
+        memcpy(psp->dxferp, psp->swb_i.ucDataBuf, psp->dxfer_len);
+
+    psp->scsi_status = psp->swb_i.spt.ScsiStatus;
+    if ((SAM_STAT_CHECK_CONDITION == psp->scsi_status) ||
+        (SAM_STAT_COMMAND_TERMINATED == psp->scsi_status))
+        memcpy(psp->sensep, psp->swb_i.ucSenseBuf, psp->sense_len);
+    else
+        psp->sense_len = 0;
+    psp->sense_resid = 0;
+    if ((psp->dxfer_len > 0) && (psp->swb_i.spt.DataTransferLength > 0))
+        psp->resid = psp->dxfer_len - psp->swb_i.spt.DataTransferLength;
+    else
+        psp->resid = 0;
+
+    return 0;
+}
+
+/* Executes SCSI or NVME command (or at least forwards it to lower layers).
+ * Clears os_err field prior to active call (whose result may set it
+ * again). Returns 0 on success, positive SCSI_PT_DO_* errors for syntax
+ * like errors and negated errnos for OS errors. For Windows its errors
+ * are placed in psp->transport_err and a errno is simulated. */
+int
+do_scsi_pt(struct sg_pt_base * vp, int dev_fd, int time_secs, int vb)
+{
+    int res;
+    struct sg_pt_win32_scsi * psp = vp->implp;
+    struct sg_pt_handle * shp;
+
+    if (! (vp && ((psp = vp->implp)))) {
+        if (vb)
+            pr2ws("%s: NULL 1st argument to this function\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    psp->os_err = 0;
+    if (dev_fd >= 0) {
+        if ((psp->dev_fd >= 0) && (dev_fd != psp->dev_fd)) {
+            if (vb)
+                pr2ws("%s: file descriptor given to create() and here "
+                      "differ\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+        psp->dev_fd = dev_fd;
+    } else if (psp->dev_fd < 0) {       /* so no dev_fd in ctor */
+        if (vb)
+            pr2ws("%s: missing device file descriptor\n", __func__);
+        return SCSI_PT_DO_BAD_PARAMS;
+    } else
+        dev_fd = psp->dev_fd;
+    shp = get_open_pt_handle(psp, dev_fd, vb > 3);
+    if (NULL == shp)
+        return -psp->os_err;
+
+    if (! (shp->bus_type_failed || shp->checked_handle)) {
+        res = get_bus_type(shp, shp->dname, NULL, vb);
+        if (res < 0) {
+            res = get_scsi_pdt(shp, vb);
+            if (res >= 0)   /* clears shp->bus_type_failed on success */
+                psp->os_err = 0;
+        }
+        if ((res < 0) && (vb > 2))
+            pr2ws("%s: get_bus_type() errno=%d\n", __func__, -res);
+    }
+    if (shp->bus_type_failed)
+        psp->os_err = EIO;
+    if (psp->os_err)
+        return -psp->os_err;
+    psp->is_nvme = shp->is_nvme;
+    psp->dev_statp = &shp->dev_stat;
+
+    if (psp->is_nvme)
+        return nvme_pt(psp, shp, time_secs, vb);
+    else if (spt_direct)
+        return scsi_pt_direct(psp, shp, time_secs, vb);
+    else
+        return scsi_pt_indirect(vp, shp, time_secs, vb);
+}
+
+int
+get_scsi_pt_result_category(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (psp->transport_err)     /* give transport error highest priority */
+        return SCSI_PT_RESULT_TRANSPORT_ERR;
+    else if (psp->os_err)
+        return SCSI_PT_RESULT_OS_ERR;
+    else if ((SAM_STAT_CHECK_CONDITION == psp->scsi_status) ||
+             (SAM_STAT_COMMAND_TERMINATED == psp->scsi_status))
+        return SCSI_PT_RESULT_SENSE;
+    else if (psp->scsi_status)
+        return SCSI_PT_RESULT_STATUS;
+    else
+        return SCSI_PT_RESULT_GOOD;
+}
+
+int
+get_scsi_pt_resid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    return psp->resid;
+}
+
+void
+get_pt_req_lengths(const struct sg_pt_base * vp, int * req_dinp,
+                   int * req_doutp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (req_dinp) {
+        if (psp->is_read && (psp->dxfer_len > 0))
+            *req_dinp = psp->dxfer_len;
+        else
+            *req_dinp = 0;
+    }
+    if (req_doutp) {
+        if ((! psp->is_read) && (psp->dxfer_len > 0))
+            *req_doutp = psp->dxfer_len;
+        else
+            *req_doutp = 0;
+    }
+}
+
+void
+get_pt_actual_lengths(const struct sg_pt_base * vp, int * act_dinp,
+                      int * act_doutp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (act_dinp) {
+        if (psp->is_read && (psp->dxfer_len > 0))
+            *act_dinp = psp->dxfer_len - psp->resid;
+        else
+            *act_dinp = 0;
+    }
+    if (act_doutp) {
+        if ((! psp->is_read) && (psp->dxfer_len > 0))
+            *act_doutp = psp->dxfer_len - psp->resid;
+        else
+            *act_doutp = 0;
+    }
+}
+
+
+int
+get_scsi_pt_status_response(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (NULL == psp)
+        return 0;
+    return psp->nvme_direct ? (int)psp->nvme_status : psp->scsi_status;
+}
+
+uint32_t
+get_pt_result(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    if (NULL == psp)
+        return 0;
+    return psp->nvme_direct ? psp->nvme_result : (uint32_t)psp->scsi_status;
+}
+
+int
+get_scsi_pt_sense_len(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+    int len;
+
+    len = psp->sense_len - psp->sense_resid;
+    return (len > 0) ? len : 0;
+}
+
+uint8_t *
+get_scsi_pt_sense_buf(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    return psp->sensep;
+}
+
+
+int
+get_scsi_pt_duration_ms(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    // const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    return -1;
+}
+
+/* If not available return 0 otherwise return number of nanoseconds that the
+ * lower layers (and hardware) took to execute the command just completed. */
+uint64_t
+get_pt_duration_ns(const struct sg_pt_base * vp __attribute__ ((unused)))
+{
+    return 0;
+}
+
+int
+get_scsi_pt_transport_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    return psp->transport_err;
+}
+
+void
+set_scsi_pt_transport_err(struct sg_pt_base * vp, int err)
+{
+    struct sg_pt_win32_scsi * psp = vp->implp;
+
+    psp->transport_err = err;
+}
+
+int
+get_scsi_pt_os_err(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    return psp->os_err;
+}
+
+bool
+pt_device_is_nvme(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    return psp ? psp->is_nvme : false;
+}
+
+/* If a NVMe block device (which includes the NSID) handle is associated
+ * with 'vp', then its NSID is returned (values range from 0x1 to
+ * 0xffffffe). Otherwise 0 is returned. */
+uint32_t
+get_pt_nvme_nsid(const struct sg_pt_base * vp)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+
+    return psp->nvme_nsid;
+}
+
+/* Use the transport_err for Windows errors. */
+char *
+get_scsi_pt_transport_err_str(const struct sg_pt_base * vp, int max_b_len,
+                              char * b)
+{
+    struct sg_pt_win32_scsi * psp = (struct sg_pt_win32_scsi *)vp->implp;
+
+    if ((max_b_len < 2) || (NULL == psp) || (NULL == b)) {
+        if (b && (max_b_len > 0))
+            b[0] = '\0';
+        return b;
+    }
+    return get_err_str(psp->transport_err, max_b_len, b);
+}
+
+char *
+get_scsi_pt_os_err_str(const struct sg_pt_base * vp, int max_b_len, char * b)
+{
+    const struct sg_pt_win32_scsi * psp = vp->implp;
+    const char * cp;
+
+    cp = safe_strerror(psp->os_err);
+    strncpy(b, cp, max_b_len);
+    if ((int)strlen(cp) >= max_b_len)
+        b[max_b_len - 1] = '\0';
+    return b;
+}
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+
+static void
+mk_sense_asc_ascq(struct sg_pt_win32_scsi * psp, int sk, int asc, int ascq,
+                  int vb)
+{
+    bool dsense = psp->dev_statp->scsi_dsense;
+    int slen = psp->sense_len;
+    int n;
+    uint8_t * sbp = (uint8_t *)psp->sensep;
+
+    psp->scsi_status = SAM_STAT_CHECK_CONDITION;
+    if ((slen < 8) || ((! dsense) && (slen < 14))) {
+        if (vb)
+            pr2ws("%s: sense_len=%d too short, want 14 or more\n",
+                  __func__, slen);
+        return;
+    }
+    if (dsense)
+        n = (slen > 32) ? 32 : slen;
+    else
+        n = (slen < 18) ? slen : 18;
+    psp->sense_resid = (slen > n) ? (slen - n) : 0;
+    memset(sbp, 0, slen);
+    sg_build_sense_buffer(dsense, sbp, sk, asc, ascq);
+    if (vb > 3)
+        pr2ws("%s:  [sense_key,asc,ascq]: [0x%x,0x%x,0x%x]\n", __func__, sk,
+              asc, ascq);
+}
+
+static void
+mk_sense_from_nvme_status(struct sg_pt_win32_scsi * psp, int vb)
+{
+    bool ok;
+    bool dsense = psp->dev_statp->scsi_dsense;
+    int n;
+    int slen = psp->sense_len;
+    uint8_t sstatus, sk, asc, ascq;
+    uint8_t * sbp = (uint8_t *)psp->sensep;
+
+    ok = sg_nvme_status2scsi(psp->nvme_status, &sstatus, &sk, &asc, &ascq);
+    if (! ok) { /* can't find a mapping to a SCSI error, so ... */
+        sstatus = SAM_STAT_CHECK_CONDITION;
+        sk = SPC_SK_ILLEGAL_REQUEST;
+        asc = 0xb;
+        ascq = 0x0;     /* asc: "WARNING" purposely vague */
+    }
+
+    psp->scsi_status = sstatus;
+    if ((slen < 8) || ((! dsense) && (slen < 14))) {
+        if (vb)
+            pr2ws("%s: sense_len=%d too short, want 14 or more\n", __func__,
+                  slen);
+        return;
+    }
+    if (dsense)
+        n = (slen > 32) ? 32 : slen;
+    else
+        n = (slen < 18) ? slen : 18;
+    psp->sense_resid = (slen > n) ? slen - n : 0;
+    memset(sbp, 0, slen);
+    sg_build_sense_buffer(dsense, sbp, sk, asc, ascq);
+    if (dsense && (psp->nvme_status > 0))
+        sg_nvme_desc2sense(sbp, false /* dnr */, false /* more */,
+                           psp->nvme_status);
+    if (vb > 3)
+        pr2ws("%s: [status, sense_key,asc,ascq]: [0x%x, 0x%x,0x%x,0x%x]\n",
+              __func__, sstatus, sk, asc, ascq);
+}
+
+/* Set in_bit to -1 to indicate no bit position of invalid field */
+static void
+mk_sense_invalid_fld(struct sg_pt_win32_scsi * psp, bool in_cdb, int in_byte,
+                     int in_bit, int vb)
+{
+    bool dsense = psp->dev_statp->scsi_dsense;
+    int sl, asc, n;
+    int slen = psp->sense_len;
+    uint8_t * sbp = (uint8_t *)psp->sensep;
+    uint8_t sks[4];
+
+    psp->scsi_status = SAM_STAT_CHECK_CONDITION;
+    asc = in_cdb ? INVALID_FIELD_IN_CDB : INVALID_FIELD_IN_PARAM_LIST;
+    if ((slen < 8) || ((! dsense) && (slen < 14))) {
+        if (vb)
+            pr2ws("%s: max_response_len=%d too short, want 14 or more\n",
+                  __func__, slen);
+        return;
+    }
+    if (dsense)
+        n = (slen > 32) ? 32 : slen;
+    else
+        n = (slen < 18) ? slen : 18;
+    psp->sense_resid = (slen > n) ? (slen - n) : 0;
+    memset(sbp, 0, slen);
+    sg_build_sense_buffer(dsense, sbp, SPC_SK_ILLEGAL_REQUEST, asc, 0);
+    memset(sks, 0, sizeof(sks));
+    sks[0] = 0x80;
+    if (in_cdb)
+        sks[0] |= 0x40;
+    if (in_bit >= 0) {
+        sks[0] |= 0x8;
+        sks[0] |= (0x7 & in_bit);
+    }
+    sg_put_unaligned_be16(in_byte, sks + 1);
+    if (dsense) {
+        sl = sbp[7] + 8;
+        sbp[7] = sl;
+        sbp[sl] = 0x2;
+        sbp[sl + 1] = 0x6;
+        memcpy(sbp + sl + 4, sks, 3);
+    } else
+        memcpy(sbp + 15, sks, 3);
+    if (vb > 3)
+        pr2ws("%s:  [sense_key,asc,ascq]: [0x5,0x%x,0x0] %c byte=%d, bit=%d\n",
+              __func__, asc, in_cdb ? 'C' : 'D', in_byte,
+              ((in_bit > 0) ? (0x7 & in_bit) : 0));
+}
+
+#if W10_NVME_NON_PASSTHRU      /* W10 and later, no real pass-through ?? */
+
+#ifndef NVME_MAX_LOG_SIZE
+#define NVME_MAX_LOG_SIZE 4096
+#endif
+
+static int
+nvme_identify(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+              const uint8_t * cmdp, uint8_t * dp, uint32_t dlen, int vb)
+{
+    bool id_ctrl;
+    int res = 0;
+    const uint32_t pg_sz = sg_get_page_size();
+    uint32_t cdw10, nsid, n;
+    const uint8_t * bp;
+    BOOL    result;
+    PVOID   buffer = NULL;
+    uint8_t * free_buffer = NULL;
+    ULONG   bufferLength = 0;
+    ULONG   returnedLength = 0;
+    STORAGE_PROPERTY_QUERY * query = NULL;
+    STORAGE_PROTOCOL_SPECIFIC_DATA * protocolData = NULL;
+    STORAGE_PROTOCOL_DATA_DESCRIPTOR * protocolDataDescr = NULL;
+
+    nsid = sg_get_unaligned_le32(cmdp + SG_NVME_PT_NSID);
+    cdw10 = sg_get_unaligned_le32(cmdp + SG_NVME_PT_CDW10);
+    id_ctrl = (0x1 == cdw10);
+    n = dlen < NVME_MAX_LOG_SIZE ? NVME_MAX_LOG_SIZE : dlen;
+    bufferLength = offsetof(STORAGE_PROPERTY_QUERY, AdditionalParameters) +
+                   sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) + n;
+    buffer = sg_memalign(bufferLength, pg_sz, &free_buffer, false);
+    if (buffer == NULL) {
+        res = sg_convert_errno(ENOMEM);
+        if (vb > 1)
+            pr2ws("%s: unable to allocate memory\n", __func__);
+        psp->os_err = res;
+        return -res;
+    }
+    query = (STORAGE_PROPERTY_QUERY *)buffer;
+
+    query->PropertyId = id_ctrl ? StorageAdapterProtocolSpecificProperty :
+                                  StorageDeviceProtocolSpecificProperty;
+    query->QueryType = PropertyStandardQuery;
+    protocolDataDescr = (STORAGE_PROTOCOL_DATA_DESCRIPTOR *)buffer;
+    protocolData = (STORAGE_PROTOCOL_SPECIFIC_DATA *)
+                        query->AdditionalParameters;
+
+    protocolData->ProtocolType = ProtocolTypeNvme;
+    protocolData->DataType = NVMeDataTypeIdentify;
+    protocolData->ProtocolDataRequestValue = cdw10;
+    if (! id_ctrl)
+        protocolData->ProtocolDataRequestSubValue = nsid;
+    protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
+    protocolData->ProtocolDataLength = dlen;
+
+    result = DeviceIoControl(shp->fh, IOCTL_STORAGE_QUERY_PROPERTY,
+                             buffer, bufferLength, buffer, bufferLength,
+                             &returnedLength, (OVERLAPPED*)0);
+    if ((! result) || (0 == returnedLength)) {
+        n = (uint32_t)GetLastError();
+        psp->transport_err = n;
+        psp->os_err = EIO;      /* simulate Unix error,  */
+        if (vb > 2) {
+            char b[128];
+
+            pr2ws("%s: IOCTL_STORAGE_QUERY_PROPERTY(id_%s) failed: %s "
+                  "[%u]\n", __func__, (id_ctrl ? "ctrl" : "ns"),
+                  get_err_str(n, sizeof(b), b), n);
+        }
+        res = -psp->os_err;
+        goto err_out;
+    }
+    if (dlen > 0) {
+        protocolData = &protocolDataDescr->ProtocolSpecificData;
+        bp = (const uint8_t *)protocolData + protocolData->ProtocolDataOffset;
+        memcpy(dp, bp, dlen);
+        if (0 == psp->nvme_nsid) {
+            uint32_t nn = sg_get_unaligned_le32(bp + 516);
+
+            if (1 == nn)        /* if physical drive has only 1 namespace */
+                psp->nvme_nsid = 1; /* then its nsid must be 1 */
+            /* N.B. Need better get_nsid_from _handle technique when 2 or
+             * more namespaces. Suggestions? */
+        }
+    }
+    psp->nvme_status = 0;
+    psp->nvme_result =
+         protocolDataDescr->ProtocolSpecificData.FixedProtocolReturnData;
+    if (vb > 3)
+        pr2ws("%s: IOCTL_STORAGE_QUERY_PROPERTY(id_ctrl) success, "
+              "returnedLength=%u\n", __func__, (uint32_t)returnedLength);
+    res = 0;
+err_out:
+    if (free_buffer)
+        free(free_buffer);
+    return res;
+}
+
+static int
+nvme_get_features(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+                  const uint8_t * cmdp, uint8_t * dp, uint32_t dlen, int vb)
+{
+    int res = 0;
+    const uint32_t pg_sz = sg_get_page_size();
+    uint32_t cdw10, nsid, n;
+    const uint8_t * bp;
+    BOOL    result;
+    PVOID   buffer = NULL;
+    uint8_t * free_buffer = NULL;
+    ULONG   bufferLength = 0;
+    ULONG   returnedLength = 0;
+    STORAGE_PROPERTY_QUERY * query = NULL;
+    STORAGE_PROTOCOL_SPECIFIC_DATA * protocolData = NULL;
+    STORAGE_PROTOCOL_DATA_DESCRIPTOR * protocolDataDescr = NULL;
+
+    nsid = sg_get_unaligned_le32(cmdp + SG_NVME_PT_NSID);
+    cdw10 = sg_get_unaligned_le32(cmdp + SG_NVME_PT_CDW10);
+    n = dlen < NVME_MAX_LOG_SIZE ? NVME_MAX_LOG_SIZE : dlen;
+    bufferLength = offsetof(STORAGE_PROPERTY_QUERY, AdditionalParameters) +
+                   sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) + n;
+    buffer = sg_memalign(bufferLength, pg_sz, &free_buffer, false);
+    if (buffer == NULL) {
+        res = sg_convert_errno(ENOMEM);
+        if (vb > 1)
+            pr2ws("%s: unable to allocate memory\n", __func__);
+        psp->os_err = res;
+        return -res;
+    }
+    query = (STORAGE_PROPERTY_QUERY *)buffer;
+
+    query->PropertyId = StorageDeviceProtocolSpecificProperty;
+    query->QueryType = PropertyStandardQuery;
+    protocolDataDescr = (STORAGE_PROTOCOL_DATA_DESCRIPTOR *)buffer;
+    protocolData = (STORAGE_PROTOCOL_SPECIFIC_DATA *)
+                        query->AdditionalParameters;
+
+    protocolData->ProtocolType = ProtocolTypeNvme;
+    protocolData->DataType = NVMeDataTypeFeature;       /* Get Features */
+    protocolData->ProtocolDataRequestValue = cdw10;
+    protocolData->ProtocolDataRequestSubValue = nsid;
+    protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
+    protocolData->ProtocolDataLength = dlen;
+
+    result = DeviceIoControl(shp->fh, IOCTL_STORAGE_QUERY_PROPERTY,
+                             buffer, bufferLength, buffer, bufferLength,
+                             &returnedLength, (OVERLAPPED*)0);
+    if ((! result) || (0 == returnedLength)) {
+        n = (uint32_t)GetLastError();
+        psp->transport_err = n;
+        psp->os_err = EIO;      /* simulate Unix error,  */
+        if (vb > 2) {
+            char b[128];
+
+            pr2ws("%s: IOCTL_STORAGE_QUERY_PROPERTY(id_ctrl) failed: %s "
+                  "[%u]\n", __func__, get_err_str(n, sizeof(b), b), n);
+        }
+        res = -psp->os_err;
+        goto err_out;
+    }
+    if (dlen > 0) {
+        protocolData = &protocolDataDescr->ProtocolSpecificData;
+        bp = (const uint8_t *)protocolData + protocolData->ProtocolDataOffset;
+        memcpy(dp, bp, dlen);
+    }
+    psp->nvme_status = 0;
+    psp->nvme_result =
+         protocolDataDescr->ProtocolSpecificData.FixedProtocolReturnData;
+    if (vb > 3)
+        pr2ws("%s: IOCTL_STORAGE_QUERY_PROPERTY(id_ctrl) success, "
+              "returnedLength=%u\n", __func__, (uint32_t)returnedLength);
+    res = 0;
+err_out:
+    if (free_buffer)
+        free(free_buffer);
+    return res;
+}
+
+static int
+nvme_get_log_page(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+                  const uint8_t * cmdp, uint8_t * dp, uint32_t dlen, int vb)
+{
+    int res = 0;
+    const uint32_t pg_sz = sg_get_page_size();
+    uint32_t cdw10, nsid, n;
+    const uint8_t * bp;
+    BOOL    result;
+    PVOID   buffer = NULL;
+    uint8_t * free_buffer = NULL;
+    ULONG   bufferLength = 0;
+    ULONG   returnedLength = 0;
+    STORAGE_PROPERTY_QUERY * query = NULL;
+    STORAGE_PROTOCOL_SPECIFIC_DATA * protocolData = NULL;
+    STORAGE_PROTOCOL_DATA_DESCRIPTOR * protocolDataDescr = NULL;
+
+    nsid = sg_get_unaligned_le32(cmdp + SG_NVME_PT_NSID);
+    cdw10 = sg_get_unaligned_le32(cmdp + SG_NVME_PT_CDW10);
+    n = dlen < NVME_MAX_LOG_SIZE ? NVME_MAX_LOG_SIZE : dlen;
+    bufferLength = offsetof(STORAGE_PROPERTY_QUERY, AdditionalParameters) +
+                   sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) + n;
+    buffer = sg_memalign(bufferLength, pg_sz, &free_buffer, false);
+    if (buffer == NULL) {
+        res = sg_convert_errno(ENOMEM);
+        if (vb > 1)
+            pr2ws("%s: unable to allocate memory\n", __func__);
+        psp->os_err = res;
+        return -res;
+    }
+    query = (STORAGE_PROPERTY_QUERY *)buffer;
+
+    query->PropertyId = StorageDeviceProtocolSpecificProperty;
+    query->QueryType = PropertyStandardQuery;
+    protocolDataDescr = (STORAGE_PROTOCOL_DATA_DESCRIPTOR *)buffer;
+    protocolData = (STORAGE_PROTOCOL_SPECIFIC_DATA *)
+                        query->AdditionalParameters;
+
+    protocolData->ProtocolType = ProtocolTypeNvme;
+    protocolData->DataType = NVMeDataTypeLogPage;       /* Get Log Page */
+    protocolData->ProtocolDataRequestValue = cdw10;
+    protocolData->ProtocolDataRequestSubValue = nsid;
+    protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
+    protocolData->ProtocolDataLength = dlen;
+
+    result = DeviceIoControl(shp->fh, IOCTL_STORAGE_QUERY_PROPERTY,
+                             buffer, bufferLength, buffer, bufferLength,
+                             &returnedLength, (OVERLAPPED*)0);
+    if ((! result) || (0 == returnedLength)) {
+        n = (uint32_t)GetLastError();
+        psp->transport_err = n;
+        psp->os_err = EIO;      /* simulate Unix error,  */
+        if (vb > 2) {
+            char b[128];
+
+            pr2ws("%s: IOCTL_STORAGE_QUERY_PROPERTY(id_ctrl) failed: %s "
+                  "[%u]\n", __func__, get_err_str(n, sizeof(b), b), n);
+        }
+        res = -psp->os_err;
+        goto err_out;
+    }
+    if (dlen > 0) {
+        protocolData = &protocolDataDescr->ProtocolSpecificData;
+        bp = (const uint8_t *)protocolData + protocolData->ProtocolDataOffset;
+        memcpy(dp, bp, dlen);
+    }
+    psp->nvme_status = 0;
+    psp->nvme_result =
+         protocolDataDescr->ProtocolSpecificData.FixedProtocolReturnData;
+    if (vb > 3)
+        pr2ws("%s: IOCTL_STORAGE_QUERY_PROPERTY(id_ctrl) success, "
+              "returnedLength=%u\n", __func__, (uint32_t)returnedLength);
+    res = 0;
+err_out:
+    if (free_buffer)
+        free(free_buffer);
+    return res;
+}
+
+static int
+nvme_real_pt(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+             const uint8_t * cmdp, uint8_t * dp, uint32_t dlen, bool is_read,
+             int time_secs, int vb)
+{
+    int res = 0;
+    const uint32_t cmd_len = 64;
+    const uint32_t pg_sz = sg_get_page_size();
+    uint32_t n, k;
+    uint32_t rd_off = 0;
+    uint32_t slen = psp->sense_len;
+    uint8_t * bp;
+    uint8_t * sbp = psp->sensep;
+    BOOL    ok;
+    PVOID   buffer = NULL;
+    uint8_t * free_buffer = NULL;
+    ULONG   bufferLength = 0;
+    ULONG   returnLength = 0;
+    STORAGE_PROTOCOL_COMMAND * protoCmdp;
+    const NVME_ERROR_INFO_LOG * neilp;
+
+    n = dlen < NVME_MAX_LOG_SIZE ? NVME_MAX_LOG_SIZE : dlen;
+    bufferLength = offsetof(STORAGE_PROTOCOL_COMMAND, Command) +
+                   cmd_len +
+                   sizeof(NVME_ERROR_INFO_LOG) + n;
+    buffer = sg_memalign(bufferLength, pg_sz, &free_buffer, false);
+    if (buffer == NULL) {
+        res = sg_convert_errno(ENOMEM);
+        if (vb > 1)
+            pr2ws("%s: unable to allocate memory\n", __func__);
+        psp->os_err = res;
+        return -res;
+    }
+    protoCmdp = (STORAGE_PROTOCOL_COMMAND *)buffer;
+    protoCmdp->Version = STORAGE_PROTOCOL_STRUCTURE_VERSION;
+    protoCmdp->Length = sizeof(STORAGE_PROTOCOL_COMMAND);
+    protoCmdp->ProtocolType = ProtocolTypeNvme;
+    /* without *_ADAPTER_REQUEST flag, goes to device */
+    protoCmdp->Flags = STORAGE_PROTOCOL_COMMAND_FLAG_ADAPTER_REQUEST;
+    /* protoCmdp->Flags = 0; */
+    protoCmdp->CommandLength = cmd_len;
+    protoCmdp->ErrorInfoLength = sizeof(NVME_ERROR_INFO_LOG);
+    if (dlen > 0) {
+        if (is_read)
+            protoCmdp->DataFromDeviceTransferLength = dlen;
+        else
+            protoCmdp->DataToDeviceTransferLength = dlen;
+    }
+    protoCmdp->TimeOutValue = (time_secs > 0) ? time_secs : DEF_TIMEOUT;
+    protoCmdp->ErrorInfoOffset =
+                 offsetof(STORAGE_PROTOCOL_COMMAND, Command) + cmd_len;
+    n = protoCmdp->ErrorInfoOffset + protoCmdp->ErrorInfoLength;
+    if (is_read) {
+        protoCmdp->DataFromDeviceBufferOffset = n;
+        rd_off = n;
+    } else
+        protoCmdp->DataToDeviceBufferOffset = n;
+    protoCmdp->CommandSpecific =
+                 STORAGE_PROTOCOL_SPECIFIC_NVME_ADMIN_COMMAND;
+    memcpy(protoCmdp->Command, cmdp, cmd_len);
+    if ((dlen > 0) && (! is_read)) {
+        bp = (uint8_t *)protoCmdp + n;
+        memcpy(bp, dp, dlen);
+    }
+
+    ok = DeviceIoControl(shp->fh, IOCTL_STORAGE_PROTOCOL_COMMAND,
+                         buffer, bufferLength, buffer, bufferLength,
+                         &returnLength, (OVERLAPPED*)0);
+    if (! ok) {
+        n = (uint32_t)GetLastError();
+        psp->transport_err = n;
+        psp->os_err = EIO;      /* simulate Unix error,  */
+        if (vb > 2) {
+            char b[128];
+
+            pr2ws("%s: IOCTL_STORAGE_PROTOCOL_COMMAND failed: %s "
+                  "[%u]\n", __func__, get_err_str(n, sizeof(b), b), n);
+            pr2ws("    ... ReturnStatus=0x%x, ReturnLength=%u\n",
+                  (uint32_t)protoCmdp->ReturnStatus, (uint32_t)returnLength);
+        }
+        res = -psp->os_err;
+        goto err_out;
+    }
+    bp = (uint8_t *)protoCmdp + protoCmdp->ErrorInfoOffset;
+    neilp = (const NVME_ERROR_INFO_LOG *)bp;
+    /* Shift over top of Phase tag bit */
+    psp->nvme_status = 0x3ff & (neilp->Status.AsUshort >> 1);
+    if ((dlen > 0) && is_read) {
+        bp = (uint8_t *)protoCmdp + rd_off;
+        memcpy(dp, bp, dlen);
+    }
+    psp->nvme_result = protoCmdp->FixedProtocolReturnData;
+    if (psp->nvme_direct && sbp && (slen > 3)) {
+        /* build 16 byte "sense" buffer from completion queue entry */
+        n = (slen < 16) ? slen : 16;
+        memset(sbp, 0 , n);
+        psp->sense_resid = (slen > 16) ? (slen - 16) : 0;
+        sg_put_unaligned_le32(psp->nvme_result, sbp + SG_NVME_PT_CQ_DW0);
+        if (n > 11) {
+            k = neilp->SQID;
+            sg_put_unaligned_le32((k << 16), sbp + SG_NVME_PT_CQ_DW2);
+            if (n > 15) {
+                k = ((uint32_t)neilp->Status.AsUshort << 16) | neilp->CMDID;
+                sg_put_unaligned_le32(k, sbp + SG_NVME_PT_CQ_DW3);
+            }
+        }
+    }
+    if (vb > 3)
+        pr2ws("%s: opcode=0x%x, status=0x%x, result=0x%x\n",
+              __func__, cmdp[0], psp->nvme_status, psp->nvme_result);
+    res = psp->nvme_status ? SG_LIB_NVME_STATUS : 0;
+err_out:
+    if (free_buffer)
+        free(free_buffer);
+    return res;
+}
+
+static int
+do_nvme_admin_cmd(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+                  const uint8_t * cmdp, uint8_t * dp, uint32_t dlen,
+                  bool is_read, int time_secs, int vb)
+{
+    const uint32_t cmd_len = 64;
+    int res;
+    uint32_t n;
+    uint8_t opcode;
+
+    psp->os_err = 0;
+    psp->transport_err = 0;
+    if (NULL == cmdp) {
+        if (! psp->have_nvme_cmd)
+            return SCSI_PT_DO_BAD_PARAMS;
+        cmdp = psp->nvme_cmd;
+        is_read = psp->is_read;
+        dlen = psp->dxfer_len;
+        dp = psp->dxferp;
+    }
+    if (vb > 2) {
+        pr2ws("NVMe is_read=%s, dlen=%u, command:\n",
+              (is_read ? "true" : "false"), dlen);
+        hex2stderr((const uint8_t *)cmdp, cmd_len, 1);
+        if ((vb > 3) && (! is_read) && dp) {
+            if (dlen > 0) {
+                n = dlen;
+                if ((dlen < 512) || (vb > 5))
+                    pr2ws("\nData-out buffer (%u bytes):\n", n);
+                else {
+                    pr2ws("\nData-out buffer (first 512 of %u bytes):\n", n);
+                    n = 512;
+                }
+                hex2stderr((const uint8_t *)dp, n, 0);
+            }
+        }
+    }
+    opcode = cmdp[0];
+    switch (opcode) {   /* The matches below are cached by W10 */
+    case 0x6:           /* Identify (controller + namespace */
+        res = nvme_identify(psp, shp, cmdp, dp, dlen, vb);
+        if (res)
+            goto err_out;
+        break;
+    case 0xa:           /* Get features */
+        res = nvme_get_features(psp, shp, cmdp, dp, dlen, vb);
+        if (res)
+            goto err_out;
+        break;
+    case 0x2:           /* Get Log Page */
+        res = nvme_get_log_page(psp, shp, cmdp, dp, dlen, vb);
+        if (res)
+            goto err_out;
+        break;
+    default:
+        res = nvme_real_pt(psp, shp, cmdp, dp, dlen, is_read, time_secs, vb);
+        if (res)
+            goto err_out;
+        break;
+        /* IOCTL_STORAGE_PROTOCOL_COMMAND base pass-through goes here */
+        res = -EINVAL;
+        goto err_out;
+    }
+
+    if ((vb > 3) && is_read && dp && (dlen > 0)) {
+        n = dlen;
+        if ((dlen < 1024) || (vb > 5))
+            pr2ws("\nData-in buffer (%u bytes):\n", n);
+        else {
+            pr2ws("\nData-in buffer (first 1024 of %u bytes):\n", n);
+            n = 1024;
+        }
+        hex2stderr((const uint8_t *)dp, n, 0);
+    }
+err_out:
+    return res;
+}
+
+#else   /*  W10_NVME_NON_PASSTHRU  */
+
+/* If cmdp is NULL then dp, dlen and is_read are ignored, those values are
+ * obtained from psp. Returns 0 for success. Returns SG_LIB_NVME_STATUS if
+ * there is non-zero NVMe status (SCT|SC from the completion queue) with the
+ * value placed in psp->nvme_status. If Unix error from ioctl then return
+ * negated value (equivalent -errno from basic Unix system functions like
+ * open()). CDW0 from the completion queue is placed in psp->nvme_result in
+ * the absence of an error.
+ * The following code is based on os_win32.cpp in smartmontools:
+ *           Copyright (C) 2004-17 Christian Franke
+ * The code is licensed with a GPL-2. */
+static int
+do_nvme_admin_cmd(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+                  const uint8_t * cmdp, uint8_t * dp, uint32_t dlen,
+                  bool is_read, int time_secs, int vb)
+{
+    const uint32_t cmd_len = 64;
+    int res;
+    uint32_t n, alloc_len;
+    const uint32_t pg_sz = sg_get_page_size();
+    uint32_t slen = psp->sense_len;
+    uint8_t * sbp = psp->sensep;
+    NVME_PASS_THROUGH_IOCTL * pthru;
+    uint8_t * free_pthru;
+    DWORD num_out = 0;
+    BOOL ok;
+
+    psp->os_err = 0;
+    psp->transport_err = 0;
+    if (NULL == cmdp) {
+        if (! psp->have_nvme_cmd)
+            return SCSI_PT_DO_BAD_PARAMS;
+        cmdp = psp->nvme_cmd;
+        is_read = psp->is_read;
+        dlen = psp->dxfer_len;
+        dp = psp->dxferp;
+    }
+    if (vb > 2) {
+        pr2ws("NVMe is_read=%s, dlen=%u, command:\n",
+              (is_read ? "true" : "false"), dlen);
+        hex2stderr((const uint8_t *)cmdp, cmd_len, 1);
+        if ((vb > 3) && (! is_read) && dp) {
+            if (dlen > 0) {
+                n = dlen;
+                if ((dlen < 512) || (vb > 5))
+                    pr2ws("\nData-out buffer (%u bytes):\n", n);
+                else {
+                    pr2ws("\nData-out buffer (first 512 of %u bytes):\n", n);
+                    n = 512;
+                }
+                hex2stderr((const uint8_t *)dp, n, 0);
+            }
+        }
+    }
+    alloc_len = sizeof(NVME_PASS_THROUGH_IOCTL) + dlen;
+    pthru = (NVME_PASS_THROUGH_IOCTL *)sg_memalign(alloc_len, pg_sz,
+                                                   &free_pthru, false);
+    if (NULL == pthru) {
+        res = sg_convert_errno(ENOMEM);
+        if (vb > 1)
+            pr2ws("%s: unable to allocate memory\n", __func__);
+        psp->os_err = res;
+        return -res;
+    }
+    if (dp && (dlen > 0) && (! is_read))
+        memcpy(pthru->DataBuffer, dp, dlen);    /* dout-out buffer */
+    /* Set NVMe command */
+    pthru->SrbIoCtrl.HeaderLength = sizeof(SRB_IO_CONTROL);
+    memcpy(pthru->SrbIoCtrl.Signature, NVME_SIG_STR, sizeof(NVME_SIG_STR)-1);
+    pthru->SrbIoCtrl.Timeout = (time_secs > 0) ? time_secs : DEF_TIMEOUT;
+    pthru->SrbIoCtrl.ControlCode = NVME_PASS_THROUGH_SRB_IO_CODE;
+    pthru->SrbIoCtrl.ReturnCode = 0;
+    pthru->SrbIoCtrl.Length = alloc_len - sizeof(SRB_IO_CONTROL);
+
+    memcpy(pthru->NVMeCmd, cmdp, cmd_len);
+    if (dlen > 0)
+        pthru->Direction = is_read ? 2 : 1;
+    else
+        pthru->Direction = 0;
+    pthru->ReturnBufferLen = alloc_len;
+    shp = get_open_pt_handle(psp, psp->dev_fd, vb > 1);
+    if (NULL == shp) {
+        res = -psp->os_err;     /* -ENODEV */
+        goto err_out;
+    }
+
+    ok = DeviceIoControl(shp->fh, IOCTL_SCSI_MINIPORT, pthru, alloc_len,
+                         pthru, alloc_len, &num_out, (OVERLAPPED*)0);
+    if (! ok) {
+        n = (uint32_t)GetLastError();
+        psp->transport_err = n;
+        psp->os_err = EIO;      /* simulate Unix error,  */
+        if (vb > 2) {
+            char b[128];
+
+            pr2ws("%s: IOCTL_SCSI_MINIPORT failed: %s [%u]\n", __func__,
+                  get_err_str(n, sizeof(b), b), n);
+        }
+    }
+    /* nvme_status is SCT|SC, therefore it excludes DNR+More */
+    psp->nvme_status = 0x3ff & (pthru->CplEntry[3] >> 17);
+    if (psp->nvme_status && (vb > 1)) {
+        uint16_t s = psp->nvme_status;
+        char b[80];
+
+        pr2ws("%s: opcode=0x%x failed: NVMe status: %s [0x%x]\n", __func__,
+              cmdp[0], sg_get_nvme_cmd_status_str(s, sizeof(b), b), s);
+    }
+    psp->nvme_result = sg_get_unaligned_le32(pthru->CplEntry + 0);
+
+    psp->sense_resid = 0;
+    if (psp->nvme_direct && sbp && (slen > 3)) {
+        /* build 16 byte "sense" buffer */
+        n = (slen < 16) ? slen : 16;
+        memset(sbp, 0 , n);
+        psp->sense_resid = (slen > 16) ? (slen - 16) : 0;
+        sg_put_unaligned_le32(pthru->CplEntry[0], sbp + SG_NVME_PT_CQ_DW0);
+        if (n > 7) {
+            sg_put_unaligned_le32(pthru->CplEntry[1],
+                                  sbp + SG_NVME_PT_CQ_DW1);
+            if (n > 11) {
+                sg_put_unaligned_le32(pthru->CplEntry[2],
+                                      sbp + SG_NVME_PT_CQ_DW2);
+                if (n > 15)
+                    sg_put_unaligned_le32(pthru->CplEntry[3],
+                                          sbp + SG_NVME_PT_CQ_DW3);
+            }
+        }
+    }
+    if (! ok) {
+        res = -psp->os_err;
+        goto err_out;
+    } else if (psp->nvme_status) {
+        res = SG_LIB_NVME_STATUS;
+        goto err_out;
+    }
+
+    if (dp && (dlen > 0) && is_read) {
+        memcpy(dp, pthru->DataBuffer, dlen);    /* data-in buffer */
+        if (vb > 3) {
+            n = dlen;
+            if ((dlen < 1024) || (vb > 5))
+                pr2ws("\nData-in buffer (%u bytes):\n", n);
+            else {
+                pr2ws("\nData-in buffer (first 1024 of %u bytes):\n", n);
+                n = 1024;
+            }
+            hex2stderr((const uint8_t *)dp, n, 0);
+        }
+    }
+    res = 0;
+err_out:
+    if (free_pthru)
+        free(free_pthru);
+    return res;
+}
+
+#endif  /*  W10_NVME_NON_PASSTHRU  */
+
+
+static void
+sntl_check_enclosure_override(struct sg_pt_win32_scsi * psp,
+                              struct sg_pt_handle * shp, int vb)
+{
+    uint8_t * up = psp->nvme_id_ctlp;
+    uint8_t nvmsr;
+
+    if (NULL == up)
+        return;
+    nvmsr = up[253];
+    if (vb > 3)
+        pr2ws("%s: enter, nvmsr=%u\n", __func__, nvmsr);
+    shp->dev_stat.id_ctl253 = nvmsr;
+    switch (shp->dev_stat.enclosure_override) {
+    case 0x0:       /* no override */
+        if (0x3 & nvmsr) {
+            shp->dev_stat.pdt = PDT_DISK;
+            shp->dev_stat.enc_serv = 1;
+        } else if (0x2 & nvmsr) {
+            shp->dev_stat.pdt = PDT_SES;
+            shp->dev_stat.enc_serv = 1;
+        } else if (0x1 & nvmsr) {
+            shp->dev_stat.pdt = PDT_DISK;
+            shp->dev_stat.enc_serv = 0;
+        } else {
+            uint32_t nn = sg_get_unaligned_le32(up + 516);
+
+            shp->dev_stat.pdt = nn ? PDT_DISK : PDT_UNKNOWN;
+            shp->dev_stat.enc_serv = 0;
+        }
+        break;
+    case 0x1:       /* override to SES device */
+        shp->dev_stat.pdt = PDT_SES;
+        shp->dev_stat.enc_serv = 1;
+        break;
+    case 0x2:       /* override to disk with attached SES device */
+        shp->dev_stat.pdt = PDT_DISK;
+        shp->dev_stat.enc_serv = 1;
+        break;
+    case 0x3:       /* override to SAFTE device (PDT_PROCESSOR) */
+        shp->dev_stat.pdt = PDT_PROCESSOR;
+        shp->dev_stat.enc_serv = 1;
+        break;
+    case 0xff:      /* override to normal disk */
+        shp->dev_stat.pdt = PDT_DISK;
+        shp->dev_stat.enc_serv = 0;
+        break;
+    default:
+        pr2ws("%s: unknown enclosure_override value: %d\n", __func__,
+              shp->dev_stat.enclosure_override);
+        break;
+    }
+}
+
+/* Returns 0 on success; otherwise a positive value is returned */
+static int
+sntl_cache_identity(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+                    int time_secs, int vb)
+{
+    static const bool is_read = true;
+    const uint32_t pg_sz = sg_get_page_size();
+    int ret;
+    uint8_t * up;
+    uint8_t * cmdp;
+
+    up = sg_memalign(((pg_sz < 4096) ? 4096 : pg_sz), pg_sz,
+                     &psp->free_nvme_id_ctlp, false);
+    psp->nvme_id_ctlp = up;
+    if (NULL == up) {
+        pr2ws("%s: sg_memalign() failed to get memory\n", __func__);
+        return -ENOMEM;
+    }
+    cmdp = psp->nvme_cmd;
+    memset(cmdp, 0, sizeof(psp->nvme_cmd));
+    cmdp[0] =  0x6;   /* Identify */
+    /* leave nsid as 0, should it be broadcast (0xffffffff) ? */
+    /* CNS=0x1 Identify controller: */
+    sg_put_unaligned_le32(0x1, cmdp + SG_NVME_PT_CDW10);
+    sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)up, cmdp + SG_NVME_PT_ADDR);
+    sg_put_unaligned_le32(pg_sz, cmdp + SG_NVME_PT_DATA_LEN);
+    ret = do_nvme_admin_cmd(psp, shp, cmdp, up, 4096, is_read, time_secs,
+                            vb);
+    if (0 == ret)
+        sntl_check_enclosure_override(psp, shp, vb);
+    return ret;
+}
+
+
+static const char * nvme_scsi_vendor_str = "NVMe    ";
+static const uint16_t inq_resp_len = 36;
+
+static int
+sntl_inq(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+         const uint8_t * cdbp, int time_secs, int vb)
+{
+    bool evpd;
+    bool cp_id_ctl = false;
+    int res;
+    uint16_t n, alloc_len, pg_cd;
+    const uint32_t pg_sz = sg_get_page_size();
+    uint8_t * nvme_id_ns = NULL;
+    uint8_t * free_nvme_id_ns = NULL;
+    uint8_t inq_dout[256];
+    uint8_t * cmdp;
+
+    if (vb > 3)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    if (0x2 & cdbp[1]) {        /* Reject CmdDt=1 */
+        mk_sense_invalid_fld(psp, true, 1, 1, vb);
+        return 0;
+    }
+    if (NULL == psp->nvme_id_ctlp) {
+        res = sntl_cache_identity(psp, shp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        } else if (res) /* should be negative errno */
+            return res;
+    }
+    memset(inq_dout, 0, sizeof(inq_dout));
+    alloc_len = sg_get_unaligned_be16(cdbp + 3);
+    evpd = !!(0x1 & cdbp[1]);
+    pg_cd = cdbp[2];
+    if (evpd) {         /* VPD page responses */
+        switch (pg_cd) {
+        case 0:
+            /* inq_dout[0] = (PQ=0)<<5 | (PDT=0); prefer pdt=0xd --> SES */
+            inq_dout[1] = pg_cd;
+            n = 11;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[4] = 0x0;
+            inq_dout[5] = 0x80;
+            inq_dout[6] = 0x83;
+            inq_dout[7] = 0x86;
+            inq_dout[8] = 0x87;
+            inq_dout[9] = 0x92;
+            inq_dout[n - 1] = SG_NVME_VPD_NICR;     /* last VPD number */
+            break;
+        case 0x80:
+            /* inq_dout[0] = (PQ=0)<<5 | (PDT=0); prefer pdt=0xd --> SES */
+            inq_dout[1] = pg_cd;
+            n = 24;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            memcpy(inq_dout + 4, psp->nvme_id_ctlp + 4, 20);    /* SN */
+            break;
+        case 0x83:
+            if ((psp->nvme_nsid > 0) &&
+                (psp->nvme_nsid < SG_NVME_BROADCAST_NSID)) {
+                nvme_id_ns = sg_memalign(pg_sz, pg_sz, &free_nvme_id_ns,
+                                         false);
+                if (nvme_id_ns) {
+                    cmdp = psp->nvme_cmd;
+                    memset(cmdp, 0, sizeof(psp->nvme_cmd));
+                    cmdp[SG_NVME_PT_OPCODE] =  0x6;   /* Identify */
+                    sg_put_unaligned_le32(psp->nvme_nsid,
+                                          cmdp + SG_NVME_PT_NSID);
+                    /* CNS=0x0 Identify controller: */
+                    sg_put_unaligned_le32(0x0, cmdp + SG_NVME_PT_CDW10);
+                    sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)nvme_id_ns,
+                                          cmdp + SG_NVME_PT_ADDR);
+                    sg_put_unaligned_le32(pg_sz, cmdp + SG_NVME_PT_DATA_LEN);
+                    res = do_nvme_admin_cmd(psp, shp, cmdp, nvme_id_ns, pg_sz,
+                                            true, time_secs, vb > 3);
+                    if (res) {
+                        free(free_nvme_id_ns);
+                        free_nvme_id_ns = NULL;
+                        nvme_id_ns = NULL;
+                    }
+                }
+            }
+            n = sg_make_vpd_devid_for_nvme(psp->nvme_id_ctlp, nvme_id_ns,
+                                           0 /* pdt */, -1 /*tproto */,
+                                           inq_dout, sizeof(inq_dout));
+            if (n > 3)
+                sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            if (free_nvme_id_ns) {
+                free(free_nvme_id_ns);
+                free_nvme_id_ns = NULL;
+                nvme_id_ns = NULL;
+            }
+            break;
+        case 0x86:      /* Extended INQUIRY (per SFS SPC Discovery 2016) */
+            inq_dout[1] = pg_cd;
+            n = 64;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[5] = 0x1;          /* SIMPSUP=1 */
+            inq_dout[7] = 0x1;          /* LUICLR=1 */
+            inq_dout[13] = 0x40;        /* max supported sense data length */
+            break;
+        case 0x87:      /* Mode page policy (per SFS SPC Discovery 2016) */
+            inq_dout[1] = pg_cd;
+            n = 8;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[4] = 0x3f;         /* all mode pages */
+            inq_dout[5] = 0xff;         /*     and their sub-pages */
+            inq_dout[6] = 0x80;         /* MLUS=1, policy=shared */
+            break;
+        case 0x92:      /* SCSI Feature set: only SPC Discovery 2016 */
+            inq_dout[1] = pg_cd;
+            n = 10;
+            sg_put_unaligned_be16(n - 4, inq_dout + 2);
+            inq_dout[9] = 0x1;  /* SFS SPC Discovery 2016 */
+            break;
+        case SG_NVME_VPD_NICR:          /* 0xde */
+            inq_dout[1] = pg_cd;
+            sg_put_unaligned_be16((16 + 4096) - 4, inq_dout + 2);
+            n = 16 + 4096;
+            cp_id_ctl = true;
+            break;
+        default:        /* Point to page_code field in cdb */
+            mk_sense_invalid_fld(psp, true, 2, 7, vb);
+            return 0;
+        }
+        if (alloc_len > 0) {
+            n = (alloc_len < n) ? alloc_len : n;
+            n = (n < psp->dxfer_len) ? n : psp->dxfer_len;
+            psp->resid = psp->dxfer_len - n;
+            if (n > 0) {
+                if (cp_id_ctl) {
+                    memcpy(psp->dxferp, inq_dout, (n < 16 ? n : 16));
+                    if (n > 16)
+                        memcpy(psp->dxferp + 16,
+                               psp->nvme_id_ctlp, n - 16);
+                } else
+                    memcpy(psp->dxferp, inq_dout, n);
+            }
+        }
+    } else {            /* Standard INQUIRY response */
+        /* pdt=0 --> disk; pdt=0xd --> SES; pdt=3 --> processor (safte) */
+        inq_dout[0] = (PDT_MASK & shp->dev_stat.pdt);  /* (PQ=0)<<5 */
+        /* inq_dout[1] = (RMD=0)<<7 | (LU_CONG=0)<<6; rest reserved */
+        inq_dout[2] = 6;   /* version: SPC-4 */
+        inq_dout[3] = 2;   /* NORMACA=0, HISUP=0, response data format: 2 */
+        inq_dout[4] = 31;  /* so response length is (or could be) 36 bytes */
+        inq_dout[6] = shp->dev_stat.enc_serv ? 0x40 : 0;
+        inq_dout[7] = 0x2;    /* CMDQUE=1 */
+        memcpy(inq_dout + 8, nvme_scsi_vendor_str, 8);  /* NVMe not Intel */
+        memcpy(inq_dout + 16, psp->nvme_id_ctlp + 24, 16); /* Prod <-- MN */
+        memcpy(inq_dout + 32, psp->nvme_id_ctlp + 64, 4);  /* Rev <-- FR */
+        if (alloc_len > 0) {
+            n = (alloc_len < inq_resp_len) ? alloc_len : inq_resp_len;
+            n = (n < psp->dxfer_len) ? n : psp->dxfer_len;
+            psp->resid = psp->dxfer_len - n;
+            if (n > 0)
+                memcpy(psp->dxferp, inq_dout, n);
+        }
+    }
+    return 0;
+}
+
+static int
+sntl_rluns(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+           const uint8_t * cdbp, int time_secs, int vb)
+{
+    int res;
+    uint16_t sel_report;
+    uint32_t alloc_len, k, n, num, max_nsid;
+    uint8_t * rl_doutp;
+    uint8_t * up;
+
+    if (vb > 3)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+
+    sel_report = cdbp[2];
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (NULL == psp->nvme_id_ctlp) {
+        res = sntl_cache_identity(psp, shp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    max_nsid = sg_get_unaligned_le32(psp->nvme_id_ctlp + 516);
+    switch (sel_report) {
+    case 0:
+    case 2:
+        num = max_nsid;
+        break;
+    case 1:
+    case 0x10:
+    case 0x12:
+        num = 0;
+        break;
+    case 0x11:
+        num = (1 == psp->nvme_nsid) ? max_nsid :  0;
+        break;
+    default:
+        if (vb > 1)
+            pr2ws("%s: bad select_report value: 0x%x\n", __func__,
+                  sel_report);
+        mk_sense_invalid_fld(psp, true, 2, 7, vb);
+        return 0;
+    }
+    rl_doutp = (uint8_t *)calloc(num + 1, 8);
+    if (NULL == rl_doutp) {
+        pr2ws("%s: calloc() failed to get memory\n", __func__);
+        return -ENOMEM;
+    }
+    for (k = 0, up = rl_doutp + 8; k < num; ++k, up += 8)
+        sg_put_unaligned_be16(k, up);
+    n = num * 8;
+    sg_put_unaligned_be32(n, rl_doutp);
+    n+= 8;
+    if (alloc_len > 0) {
+        n = (alloc_len < n) ? alloc_len : n;
+        n = (n < psp->dxfer_len) ? n : psp->dxfer_len;
+        psp->resid = psp->dxfer_len - n;
+        if (n > 0)
+            memcpy(psp->dxferp, rl_doutp, n);
+    }
+    res = 0;
+    free(rl_doutp);
+    return res;
+}
+
+static int
+sntl_tur(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+         int time_secs, int vb)
+{
+    int res;
+    uint32_t pow_state;
+    uint8_t * cmdp;
+
+    if (vb > 4)
+        pr2ws("%s: enter\n", __func__);
+    if (NULL == psp->nvme_id_ctlp) {
+        res = sntl_cache_identity(psp, shp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    cmdp = psp->nvme_cmd;
+    memset(cmdp, 0, sizeof(psp->nvme_cmd));
+    cmdp[SG_NVME_PT_OPCODE] =  0xa; /* Get features */
+    sg_put_unaligned_le32(SG_NVME_BROADCAST_NSID, cmdp + SG_NVME_PT_NSID);
+    /* SEL=0 (current), Feature=2 Power Management */
+    sg_put_unaligned_le32(0x2, cmdp + SG_NVME_PT_CDW10);
+    res = do_nvme_admin_cmd(psp, shp, cmdp, NULL, 0, false, time_secs, vb);
+    if (0 != res) {
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        } else
+            return res;
+    } else {
+        psp->os_err = 0;
+        psp->nvme_status = 0;
+    }
+    pow_state = (0x1f & psp->nvme_result);
+    if (vb > 3)
+        pr2ws("%s: pow_state=%u\n", __func__, pow_state);
+#if 0   /* pow_state bounces around too much on laptop */
+    if (pow_state)
+        mk_sense_asc_ascq(psp, SPC_SK_NOT_READY, LOW_POWER_COND_ON_ASC, 0,
+                          vb);
+#endif
+    return 0;
+}
+
+static int
+sntl_req_sense(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+               const uint8_t * cdbp, int time_secs, int vb)
+{
+    bool desc;
+    int res;
+    uint32_t pow_state, alloc_len, n;
+    uint8_t rs_dout[64];
+    uint8_t * cmdp;
+
+    if (vb > 3)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    if (NULL == psp->nvme_id_ctlp) {
+        res = sntl_cache_identity(psp, shp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    desc = !!(0x1 & cdbp[1]);
+    alloc_len = cdbp[4];
+    cmdp = psp->nvme_cmd;
+    memset(cmdp, 0, sizeof(psp->nvme_cmd));
+    cmdp[SG_NVME_PT_OPCODE] =  0xa; /* Get features */
+    sg_put_unaligned_le32(SG_NVME_BROADCAST_NSID, cmdp + SG_NVME_PT_NSID);
+    /* SEL=0 (current), Feature=2 Power Management */
+    sg_put_unaligned_le32(0x2, cmdp + SG_NVME_PT_CDW10);
+    res = do_nvme_admin_cmd(psp, shp, cmdp, NULL, 0, false, time_secs, vb);
+    if (0 != res) {
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        } else
+            return res;
+    } else {
+        psp->os_err = 0;
+        psp->nvme_status = 0;
+    }
+    psp->sense_resid = psp->sense_len;
+    pow_state = (0x1f & psp->nvme_result);
+    if (vb > 3)
+        pr2ws("%s: pow_state=%u\n", __func__, pow_state);
+    memset(rs_dout, 0, sizeof(rs_dout));
+    if (pow_state)
+        sg_build_sense_buffer(desc, rs_dout, SPC_SK_NO_SENSE,
+                              LOW_POWER_COND_ON_ASC, 0);
+    else
+        sg_build_sense_buffer(desc, rs_dout, SPC_SK_NO_SENSE,
+                              NO_ADDITIONAL_SENSE, 0);
+    n = desc ? 8 : 18;
+    n = (n < alloc_len) ? n : alloc_len;
+    n = (n < psp->dxfer_len) ? n : psp->dxfer_len;
+    psp->resid = psp->dxfer_len - n;
+    if (n > 0)
+        memcpy(psp->dxferp, rs_dout, n);
+    return 0;
+}
+
+static int
+sntl_mode_ss(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+             const uint8_t * cdbp, int time_secs, int vb)
+{
+    bool is_msense = (SCSI_MODE_SENSE10_OPC == cdbp[0]);
+    int res, n, len;
+    uint8_t * bp;
+    struct sg_sntl_result_t sntl_result;
+
+    if (vb > 3)
+        pr2ws("%s: mse%s, time_secs=%d\n", __func__,
+              (is_msense ? "nse" : "lect"), time_secs);
+    if (NULL == psp->nvme_id_ctlp) {
+        res = sntl_cache_identity(psp, shp, time_secs, vb);
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        } else if (res)
+            return res;
+    }
+    if (is_msense) {    /* MODE SENSE(10) */
+        len = psp->dxfer_len;
+        bp = psp->dxferp;
+        n = sntl_resp_mode_sense10(&shp->dev_stat, cdbp, bp, len,
+                                   &sntl_result);
+        psp->resid = (n >= 0) ? len - n : len;
+    } else {            /* MODE SELECT(10) */
+        uint8_t pre_enc_ov = shp->dev_stat.enclosure_override;
+
+        len = psp->dxfer_len;
+        bp = psp->dxferp;
+        n = sntl_resp_mode_select10(&shp->dev_stat, cdbp, bp, len,
+                                    &sntl_result);
+        if (pre_enc_ov != shp->dev_stat.enclosure_override)
+            sntl_check_enclosure_override(psp, shp, vb);  /* ENC_OV changed */
+    }
+    if (n < 0) {
+        int in_bit = (255 == sntl_result.in_bit) ? (int)sntl_result.in_bit :
+                                                   -1;
+        if ((SAM_STAT_CHECK_CONDITION == sntl_result.sstatus) &&
+            (SPC_SK_ILLEGAL_REQUEST == sntl_result.sk)) {
+            if (INVALID_FIELD_IN_CDB == sntl_result.asc)
+                mk_sense_invalid_fld(psp, true, sntl_result.in_byte, in_bit,
+                                     vb);
+            else if (INVALID_FIELD_IN_PARAM_LIST == sntl_result.asc)
+                mk_sense_invalid_fld(psp, false, sntl_result.in_byte, in_bit,
+                                     vb);
+            else
+                mk_sense_asc_ascq(psp, sntl_result.sk, sntl_result.asc,
+                                  sntl_result.ascq, vb);
+        } else
+            pr2ws("%s: error but no sense?? n=%d\n", __func__, n);
+    }
+    return 0;
+}
+
+/* This is not really a SNTL. For SCSI SEND DIAGNOSTIC(PF=1) NVMe-MI
+ * has a special command (SES Send) to tunnel through pages to an
+ * enclosure. The NVMe enclosure is meant to understand the SES
+ * (SCSI Enclosure Services) use of diagnostics pages that are
+ * related to SES. */
+static int
+sntl_senddiag(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+               const uint8_t * cdbp, int time_secs, int vb)
+{
+    bool pf, self_test;
+    int res;
+    uint8_t st_cd, dpg_cd;
+    uint32_t alloc_len, n, dout_len, dpg_len, nvme_dst;
+    uint8_t * dop;
+    uint8_t * cmdp;
+
+    st_cd = 0x7 & (cdbp[1] >> 5);
+    self_test = !! (0x4 & cdbp[1]);
+    pf = !! (0x10 & cdbp[1]);
+    if (vb > 3)
+        pr2ws("%s: pf=%d, self_test=%d (st_code=%d)\n", __func__, (int)pf,
+              (int)self_test, (int)st_cd);
+    cmdp = psp->nvme_cmd;
+    if (self_test || st_cd) {
+        memset(cmdp, 0, sizeof(psp->nvme_cmd));
+        cmdp[SG_NVME_PT_OPCODE] = 0x14;   /* Device self-test */
+        /* just this namespace (if there is one) and controller */
+        sg_put_unaligned_le32(psp->nvme_nsid, cmdp + SG_NVME_PT_NSID);
+        switch (st_cd) {
+        case 0: /* Here if self_test is set, do short self-test */
+        case 1: /* Background short */
+        case 5: /* Foreground short */
+            nvme_dst = 1;
+            break;
+        case 2: /* Background extended */
+        case 6: /* Foreground extended */
+            nvme_dst = 2;
+            break;
+        case 4: /* Abort self-test */
+            nvme_dst = 0xf;
+            break;
+        default:
+            pr2ws("%s: bad self-test code [0x%x]\n", __func__, st_cd);
+            mk_sense_invalid_fld(psp, true, 1, 7, vb);
+            return 0;
+        }
+        sg_put_unaligned_le32(nvme_dst, cmdp + SG_NVME_PT_CDW10);
+        res = do_nvme_admin_cmd(psp, shp, cmdp, NULL, 0, false, time_secs,
+                                vb);
+        if (0 != res) {
+            if (SG_LIB_NVME_STATUS == res) {
+                mk_sense_from_nvme_status(psp, vb);
+                return 0;
+            } else
+                return res;
+        }
+    }
+    alloc_len = sg_get_unaligned_be16(cdbp + 3); /* parameter list length */
+    dout_len = psp->dxfer_len;
+    if (pf) {
+        if (0 == alloc_len) {
+            mk_sense_invalid_fld(psp, true, 3, 7, vb);
+            if (vb)
+                pr2ws("%s: PF bit set bit param_list_len=0\n", __func__);
+            return 0;
+        }
+    } else {    /* PF bit clear */
+        if (alloc_len) {
+            mk_sense_invalid_fld(psp, true, 3, 7, vb);
+            if (vb)
+                pr2ws("%s: param_list_len>0 but PF clear\n", __func__);
+            return 0;
+        } else
+            return 0;     /* nothing to do */
+        if (dout_len > 0) {
+            if (vb)
+                pr2ws("%s: dout given but PF clear\n", __func__);
+            return SCSI_PT_DO_BAD_PARAMS;
+        }
+    }
+    if (dout_len < 4) {
+        if (vb)
+            pr2ws("%s: dout length (%u bytes) too short\n", __func__,
+                  dout_len);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    n = dout_len;
+    n = (n < alloc_len) ? n : alloc_len;
+    dop = psp->dxferp;
+    if (! sg_is_aligned(dop, 0)) {      /* page aligned ? */
+        if (vb)
+            pr2ws("%s: dout [0x%" PRIx64 "] not page aligned\n", __func__,
+                  (uint64_t)(sg_uintptr_t)psp->dxferp);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    dpg_cd = dop[0];
+    dpg_len = sg_get_unaligned_be16(dop + 2) + 4;
+    /* should we allow for more than one D_PG is dout ?? */
+    n = (n < dpg_len) ? n : dpg_len;    /* not yet ... */
+
+    if (vb)
+        pr2ws("%s: passing through d_pg=0x%x, len=%u to NVME_MI SES send\n",
+              __func__, dpg_cd, dpg_len);
+    memset(cmdp, 0, sizeof(psp->nvme_cmd));
+    cmdp[SG_NVME_PT_OPCODE] = 0x1d;   /* MI Send */
+    /* And 0x1d is same opcode as the SCSI SEND DIAGNOSTIC command */
+    sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)dop,
+                          cmdp + SG_NVME_PT_ADDR);
+   /* NVMe 4k page size. Maybe determine this? */
+   /* N.B. Maybe n  > 0x1000, is this a problem?? */
+    sg_put_unaligned_le32(0x1000, cmdp + SG_NVME_PT_DATA_LEN);
+    /* NVMe Message Header */
+    sg_put_unaligned_le32(0x0804, cmdp + SG_NVME_PT_CDW10);
+    /* NVME-MI SES Send; (0x8 -> NVME-MI SES Receive) */
+    sg_put_unaligned_le32(0x9, cmdp + SG_NVME_PT_CDW11);
+    /* 'n' is number of bytes SEND DIAGNOSTIC dpage */
+    sg_put_unaligned_le32(n, cmdp + SG_NVME_PT_CDW13);
+    res = do_nvme_admin_cmd(psp, shp, cmdp, dop, n, false, time_secs, vb);
+    if (0 != res) {
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        }
+    }
+    return res;
+}
+
+/* This is not really a SNTL. For SCSI RECEIVE DIAGNOSTIC RESULTS(PCV=1)
+ * NVMe-MI has a special command (SES Receive) to read pages through a
+ * tunnel from an enclosure. The NVMe enclosure is meant to understand the
+ * SES (SCSI Enclosure Services) use of diagnostics pages that are
+ * related to SES. */
+static int
+sntl_recvdiag(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+               const uint8_t * cdbp, int time_secs, int vb)
+{
+    bool pcv;
+    int res;
+    uint8_t dpg_cd;
+    uint32_t alloc_len, n, din_len;
+    uint8_t * dip;
+    uint8_t * cmdp;
+
+    pcv = !! (0x1 & cdbp[1]);
+    dpg_cd = cdbp[2];
+    alloc_len = sg_get_unaligned_be16(cdbp + 3); /* parameter list length */
+    if (vb > 3)
+        pr2ws("%s: dpg_cd=0x%x, pcv=%d, alloc_len=0x%x\n", __func__,
+              dpg_cd, (int)pcv, alloc_len);
+    din_len = psp->dxfer_len;
+    n = (din_len < alloc_len) ? din_len : alloc_len;
+    dip = psp->dxferp;
+    if (! sg_is_aligned(dip, 0)) {      /* page aligned ? */
+        if (vb)
+            pr2ws("%s: din [0x%" PRIx64 "] not page aligned\n", __func__,
+                  (uint64_t)(sg_uintptr_t)psp->dxferp);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+
+    if (vb)
+        pr2ws("%s: expecting d_pg=0x%x from NVME_MI SES receive\n", __func__,
+              dpg_cd);
+    cmdp = psp->nvme_cmd;
+    memset(cmdp, 0, sizeof(psp->nvme_cmd));
+    cmdp[SG_NVME_PT_OPCODE] = 0x1e;   /* MI Receive */
+    sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)dip,
+                          cmdp + SG_NVME_PT_ADDR);
+   /* NVMe 4k page size. Maybe determine this? */
+   /* N.B. Maybe n  > 0x1000, is this a problem?? */
+    sg_put_unaligned_le32(0x1000, cmdp + SG_NVME_PT_DATA_LEN);
+    /* NVMe Message Header */
+    sg_put_unaligned_le32(0x0804, cmdp + SG_NVME_PT_CDW10);
+    /* NVME-MI SES Receive */
+    sg_put_unaligned_le32(0x8, cmdp + SG_NVME_PT_CDW11);
+    /* Diagnostic page code */
+    sg_put_unaligned_le32(dpg_cd, cmdp + SG_NVME_PT_CDW12);
+    /* 'n' is number of bytes expected in diagnostic page */
+    sg_put_unaligned_le32(n, cmdp + SG_NVME_PT_CDW13);
+    res = do_nvme_admin_cmd(psp, shp, cmdp, dip, n, true, time_secs, vb);
+    if (0 != res) {
+        if (SG_LIB_NVME_STATUS == res) {
+            mk_sense_from_nvme_status(psp, vb);
+            return 0;
+        } else
+            return res;
+    }
+    psp->resid = din_len - n;
+    return res;
+}
+
+#define F_SA_LOW                0x80    /* cdb byte 1, bits 4 to 0 */
+#define F_SA_HIGH               0x100   /* as used by variable length cdbs */
+#define FF_SA (F_SA_HIGH | F_SA_LOW)
+#define F_INV_OP                0x200
+
+static int
+sntl_rep_opcodes(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+                 const uint8_t * cdbp, int time_secs, int vb)
+{
+    bool rctd;
+    uint8_t reporting_opts, req_opcode, supp;
+    uint16_t req_sa, u;
+    uint32_t alloc_len, offset, a_len;
+    const uint32_t pg_sz = sg_get_page_size();
+    int k, len, count, bump;
+    const struct sg_opcode_info_t *oip;
+    uint8_t *arr;
+    uint8_t *free_arr;
+
+    if (vb > 3)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    if (shp) { ; }      /* suppress warning */
+    rctd = !!(cdbp[2] & 0x80);      /* report command timeout desc. */
+    reporting_opts = cdbp[2] & 0x7;
+    req_opcode = cdbp[3];
+    req_sa = sg_get_unaligned_be16(cdbp + 4);
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (alloc_len < 4 || alloc_len > 0xffff) {
+        mk_sense_invalid_fld(psp, true, 6, -1, vb);
+        return 0;
+    }
+    a_len = pg_sz - 72;
+    arr = sg_memalign(pg_sz, pg_sz, &free_arr, false);
+    if (NULL == arr) {
+        pr2ws("%s: sg_memalign() failed to get memory\n", __func__);
+        return -ENOMEM;
+    }
+    switch (reporting_opts) {
+    case 0: /* all commands */
+        count = 0;
+        bump = rctd ? 20 : 8;
+        for (offset = 4, oip = sg_get_opcode_translation();
+             (oip->flags != 0xffff) && (offset < a_len); ++oip) {
+            if (F_INV_OP & oip->flags)
+                continue;
+            ++count;
+            arr[offset] = oip->opcode;
+            sg_put_unaligned_be16(oip->sa, arr + offset + 2);
+            if (rctd)
+                arr[offset + 5] |= 0x2;
+            if (FF_SA & oip->flags)
+                arr[offset + 5] |= 0x1;
+            sg_put_unaligned_be16(oip->len_mask[0], arr + offset + 6);
+            if (rctd)
+                sg_put_unaligned_be16(0xa, arr + offset + 8);
+            offset += bump;
+        }
+        sg_put_unaligned_be32(count * bump, arr + 0);
+        break;
+    case 1: /* one command: opcode only */
+    case 2: /* one command: opcode plus service action */
+    case 3: /* one command: if sa==0 then opcode only else opcode+sa */
+        for (oip = sg_get_opcode_translation(); oip->flags != 0xffff; ++oip) {
+            if ((req_opcode == oip->opcode) && (req_sa == oip->sa))
+                break;
+        }
+        if ((0xffff == oip->flags) || (F_INV_OP & oip->flags)) {
+            supp = 1;
+            offset = 4;
+        } else {
+            if (1 == reporting_opts) {
+                if (FF_SA & oip->flags) {
+                    mk_sense_invalid_fld(psp, true, 2, 2, vb);
+                    free(free_arr);
+                    return 0;
+                }
+                req_sa = 0;
+            } else if ((2 == reporting_opts) && 0 == (FF_SA & oip->flags)) {
+                mk_sense_invalid_fld(psp, true, 4, -1, vb);
+                free(free_arr);
+                return 0;
+            }
+            if ((0 == (FF_SA & oip->flags)) && (req_opcode == oip->opcode))
+                supp = 3;
+            else if (0 == (FF_SA & oip->flags))
+                supp = 1;
+            else if (req_sa != oip->sa)
+                supp = 1;
+            else
+                supp = 3;
+            if (3 == supp) {
+                u = oip->len_mask[0];
+                sg_put_unaligned_be16(u, arr + 2);
+                arr[4] = oip->opcode;
+                for (k = 1; k < u; ++k)
+                    arr[4 + k] = (k < 16) ?
+                oip->len_mask[k] : 0xff;
+                offset = 4 + u;
+            } else
+                offset = 4;
+        }
+        arr[1] = (rctd ? 0x80 : 0) | supp;
+        if (rctd) {
+            sg_put_unaligned_be16(0xa, arr + offset);
+            offset += 12;
+        }
+        break;
+    default:
+        mk_sense_invalid_fld(psp, true, 2, 2, vb);
+        free(free_arr);
+        return 0;
+    }
+    offset = (offset < a_len) ? offset : a_len;
+    len = (offset < alloc_len) ? offset : alloc_len;
+    psp->resid = psp->dxfer_len - len;
+    if (len > 0)
+        memcpy(psp->dxferp, arr, len);
+    free(free_arr);
+    return 0;
+}
+
+static int
+sntl_rep_tmfs(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+              const uint8_t * cdbp, int time_secs, int vb)
+{
+    bool repd;
+    uint32_t alloc_len, len;
+    uint8_t arr[16];
+
+    if (vb > 3)
+        pr2ws("%s: time_secs=%d\n", __func__, time_secs);
+    if (shp) { ; }      /* suppress warning */
+    memset(arr, 0, sizeof(arr));
+    repd = !!(cdbp[2] & 0x80);
+    alloc_len = sg_get_unaligned_be32(cdbp + 6);
+    if (alloc_len < 4) {
+        mk_sense_invalid_fld(psp, true, 6, -1, vb);
+        return 0;
+    }
+    arr[0] = 0xc8;          /* ATS | ATSS | LURS */
+    arr[1] = 0x1;           /* ITNRS */
+    if (repd) {
+        arr[3] = 0xc;
+        len = 16;
+    } else
+        len = 4;
+
+    len = (len < alloc_len) ? len : alloc_len;
+    psp->resid = psp->dxfer_len - len;
+    if (len > 0)
+        memcpy(psp->dxferp, arr, len);
+    return 0;
+}
+
+/* Executes NVMe Admin command (or at least forwards it to lower layers).
+ * Returns 0 for success, negative numbers are negated 'errno' values from
+ * OS system calls. Positive return values are errors from this package.
+ * When time_secs is 0 the Linux NVMe Admin command default of 60 seconds
+ * is used. */
+static int
+nvme_pt(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+        int time_secs, int vb)
+{
+    bool scsi_cdb = false;
+    uint32_t cmd_len = 0;
+    uint16_t sa;
+    const uint8_t * cdbp = NULL;
+
+    if (psp->have_nvme_cmd) {
+        cdbp = psp->nvme_cmd;
+        cmd_len = 64;
+        psp->nvme_direct = true;
+    } else if (spt_direct) {
+        if (psp->swb_d.spt.CdbLength > 0) {
+            cdbp = psp->swb_d.spt.Cdb;
+            cmd_len = psp->swb_d.spt.CdbLength;
+            scsi_cdb = true;
+            psp->nvme_direct = false;
+        }
+    } else {
+        if (psp->swb_i.spt.CdbLength > 0) {
+            cdbp = psp->swb_i.spt.Cdb;
+            cmd_len = psp->swb_i.spt.CdbLength;
+            scsi_cdb = true;
+            psp->nvme_direct = false;
+        }
+    }
+    if (NULL == cdbp) {
+        if (vb)
+            pr2ws("%s: Missing NVMe or SCSI command (set_scsi_pt_cdb())"
+                  " cmd_len=%u\n", __func__, cmd_len);
+        return SCSI_PT_DO_BAD_PARAMS;
+    }
+    if (vb > 3)
+        pr2ws("%s: opcode=0x%x, cmd_len=%u, fdev_name: %s, dlen=%u\n",
+              __func__, cdbp[0], cmd_len, shp->dname, psp->dxfer_len);
+    /* direct NVMe command (i.e. 64 bytes long) or SNTL */
+    if (scsi_cdb) {
+        switch (cdbp[0]) {
+        case SCSI_INQUIRY_OPC:
+            return sntl_inq(psp, shp, cdbp, time_secs, vb);
+        case SCSI_REPORT_LUNS_OPC:
+            return sntl_rluns(psp, shp, cdbp, time_secs, vb);
+        case SCSI_TEST_UNIT_READY_OPC:
+            return sntl_tur(psp, shp, time_secs, vb);
+        case SCSI_REQUEST_SENSE_OPC:
+            return sntl_req_sense(psp, shp, cdbp, time_secs, vb);
+        case SCSI_SEND_DIAGNOSTIC_OPC:
+            return sntl_senddiag(psp, shp, cdbp, time_secs, vb);
+        case SCSI_RECEIVE_DIAGNOSTIC_OPC:
+            return sntl_recvdiag(psp, shp, cdbp, time_secs, vb);
+        case SCSI_MODE_SENSE10_OPC:
+        case SCSI_MODE_SELECT10_OPC:
+            return sntl_mode_ss(psp, shp, cdbp, time_secs, vb);
+        case SCSI_MAINT_IN_OPC:
+            sa = 0x1f & cdbp[1];        /* service action */
+            if (SCSI_REP_SUP_OPCS_OPC == sa)
+                return sntl_rep_opcodes(psp, shp, cdbp, time_secs,
+                                        vb);
+            else if (SCSI_REP_SUP_TMFS_OPC == sa)
+                return sntl_rep_tmfs(psp, shp, cdbp, time_secs, vb);
+            /* fall through */
+        default:
+            if (vb > 2) {
+                char b[64];
+
+                sg_get_command_name(cdbp, -1, sizeof(b), b);
+                pr2ws("%s: no translation to NVMe for SCSI %s command\n",
+                      __func__, b);
+            }
+            mk_sense_asc_ascq(psp, SPC_SK_ILLEGAL_REQUEST, INVALID_OPCODE,
+                              0, vb);
+            return 0;
+        }
+    }
+    if(psp->dxfer_len > 0) {
+        uint8_t * cmdp = psp->nvme_cmd;
+
+        sg_put_unaligned_le32(psp->dxfer_len, cmdp + SG_NVME_PT_DATA_LEN);
+        sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)psp->dxferp,
+                              cmdp + SG_NVME_PT_ADDR);
+        if (vb > 2)
+            pr2ws("%s: NVMe command, dlen=%u, dxferp=0x%p\n", __func__,
+                  psp->dxfer_len, psp->dxferp);
+    }
+    return do_nvme_admin_cmd(psp, shp, NULL, NULL, 0, true, time_secs, vb);
+}
+
+#else           /* (HAVE_NVME && (! IGNORE_NVME)) */
+
+static int
+nvme_pt(struct sg_pt_win32_scsi * psp, struct sg_pt_handle * shp,
+        int time_secs, int vb)
+{
+    if (vb)
+        pr2ws("%s: not supported [time_secs=%d]\n", __func__, time_secs);
+    if (psp) { ; }              /* suppress warning */
+    if (shp) { ; }              /* suppress warning */
+    return -ENOTTY;             /* inappropriate ioctl error */
+}
+
+#endif          /* (HAVE_NVME && (! IGNORE_NVME)) */
+
+int
+do_nvm_pt(struct sg_pt_base * vp, int submq, int timeout_secs, int verbose)
+{
+    if (vp) { }
+    if (submq) { }
+    if (timeout_secs) { }
+    if (verbose) { }
+    return SCSI_PT_DO_NOT_SUPPORTED;
+}
diff --git a/ltmain.sh b/ltmain.sh
new file mode 100755
index 0000000..8fb8700
--- /dev/null
+++ b/ltmain.sh
@@ -0,0 +1,11448 @@
+#! /usr/bin/env sh
+## DO NOT EDIT - This file generated from ./build-aux/ltmain.in
+##               by inline-source v2019-02-19.15
+
+# libtool (GNU libtool) 2.4.7
+# Provide generalized library-building support services.
+# Written by Gordon Matzigkeit <gord@gnu.ai.mit.edu>, 1996
+
+# Copyright (C) 1996-2019, 2021-2022 Free Software Foundation, Inc.
+# This is free software; see the source for copying conditions.  There is NO
+# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+# GNU Libtool is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# As a special exception to the GNU General Public License,
+# if you distribute this file as part of a program or library that
+# is built using GNU Libtool, you may include this file under the
+# same distribution terms that you use for the rest of that program.
+#
+# GNU Libtool is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+PROGRAM=libtool
+PACKAGE=libtool
+VERSION="2.4.7 Debian-2.4.7-4"
+package_revision=2.4.7
+
+
+## ------ ##
+## Usage. ##
+## ------ ##
+
+# Run './libtool --help' for help with using this script from the
+# command line.
+
+
+## ------------------------------- ##
+## User overridable command paths. ##
+## ------------------------------- ##
+
+# After configure completes, it has a better idea of some of the
+# shell tools we need than the defaults used by the functions shared
+# with bootstrap, so set those here where they can still be over-
+# ridden by the user, but otherwise take precedence.
+
+: ${AUTOCONF="autoconf"}
+: ${AUTOMAKE="automake"}
+
+
+## -------------------------- ##
+## Source external libraries. ##
+## -------------------------- ##
+
+# Much of our low-level functionality needs to be sourced from external
+# libraries, which are installed to $pkgauxdir.
+
+# Set a version string for this script.
+scriptversion=2019-02-19.15; # UTC
+
+# General shell script boiler plate, and helper functions.
+# Written by Gary V. Vaughan, 2004
+
+# This is free software.  There is NO warranty; not even for
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+# Copyright (C) 2004-2019, 2021 Bootstrap Authors
+#
+# This file is dual licensed under the terms of the MIT license
+# <https://opensource.org/license/MIT>, and GPL version 2 or later
+# <http://www.gnu.org/licenses/gpl-2.0.html>.  You must apply one of
+# these licenses when using or redistributing this software or any of
+# the files within it.  See the URLs above, or the file `LICENSE`
+# included in the Bootstrap distribution for the full license texts.
+
+# Please report bugs or propose patches to:
+# <https://github.com/gnulib-modules/bootstrap/issues>
+
+
+## ------ ##
+## Usage. ##
+## ------ ##
+
+# Evaluate this file near the top of your script to gain access to
+# the functions and variables defined here:
+#
+#   . `echo "$0" | ${SED-sed} 's|[^/]*$||'`/build-aux/funclib.sh
+#
+# If you need to override any of the default environment variable
+# settings, do that before evaluating this file.
+
+
+## -------------------- ##
+## Shell normalisation. ##
+## -------------------- ##
+
+# Some shells need a little help to be as Bourne compatible as possible.
+# Before doing anything else, make sure all that help has been provided!
+
+DUALCASE=1; export DUALCASE # for MKS sh
+if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then :
+  emulate sh
+  NULLCMD=:
+  # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which
+  # is contrary to our usage.  Disable this feature.
+  alias -g '${1+"$@"}'='"$@"'
+  setopt NO_GLOB_SUBST
+else
+  case `(set -o) 2>/dev/null` in *posix*) set -o posix ;; esac
+fi
+
+# NLS nuisances: We save the old values in case they are required later.
+_G_user_locale=
+_G_safe_locale=
+for _G_var in LANG LANGUAGE LC_ALL LC_CTYPE LC_COLLATE LC_MESSAGES
+do
+  eval "if test set = \"\${$_G_var+set}\"; then
+          save_$_G_var=\$$_G_var
+          $_G_var=C
+	  export $_G_var
+	  _G_user_locale=\"$_G_var=\\\$save_\$_G_var; \$_G_user_locale\"
+	  _G_safe_locale=\"$_G_var=C; \$_G_safe_locale\"
+	fi"
+done
+# These NLS vars are set unconditionally (bootstrap issue #24).  Unset those
+# in case the environment reset is needed later and the $save_* variant is not
+# defined (see the code above).
+LC_ALL=C
+LANGUAGE=C
+export LANGUAGE LC_ALL
+
+# Make sure IFS has a sensible default
+sp=' '
+nl='
+'
+IFS="$sp	$nl"
+
+# There are apparently some retarded systems that use ';' as a PATH separator!
+if test "${PATH_SEPARATOR+set}" != set; then
+  PATH_SEPARATOR=:
+  (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {
+    (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||
+      PATH_SEPARATOR=';'
+  }
+fi
+
+
+# func_unset VAR
+# --------------
+# Portably unset VAR.
+# In some shells, an 'unset VAR' statement leaves a non-zero return
+# status if VAR is already unset, which might be problematic if the
+# statement is used at the end of a function (thus poisoning its return
+# value) or when 'set -e' is active (causing even a spurious abort of
+# the script in this case).
+func_unset ()
+{
+    { eval $1=; (eval unset $1) >/dev/null 2>&1 && eval unset $1 || : ; }
+}
+
+
+# Make sure CDPATH doesn't cause `cd` commands to output the target dir.
+func_unset CDPATH
+
+# Make sure ${,E,F}GREP behave sanely.
+func_unset GREP_OPTIONS
+
+
+## ------------------------- ##
+## Locate command utilities. ##
+## ------------------------- ##
+
+
+# func_executable_p FILE
+# ----------------------
+# Check that FILE is an executable regular file.
+func_executable_p ()
+{
+    test -f "$1" && test -x "$1"
+}
+
+
+# func_path_progs PROGS_LIST CHECK_FUNC [PATH]
+# --------------------------------------------
+# Search for either a program that responds to --version with output
+# containing "GNU", or else returned by CHECK_FUNC otherwise, by
+# trying all the directories in PATH with each of the elements of
+# PROGS_LIST.
+#
+# CHECK_FUNC should accept the path to a candidate program, and
+# set $func_check_prog_result if it truncates its output less than
+# $_G_path_prog_max characters.
+func_path_progs ()
+{
+    _G_progs_list=$1
+    _G_check_func=$2
+    _G_PATH=${3-"$PATH"}
+
+    _G_path_prog_max=0
+    _G_path_prog_found=false
+    _G_save_IFS=$IFS; IFS=${PATH_SEPARATOR-:}
+    for _G_dir in $_G_PATH; do
+      IFS=$_G_save_IFS
+      test -z "$_G_dir" && _G_dir=.
+      for _G_prog_name in $_G_progs_list; do
+        for _exeext in '' .EXE; do
+          _G_path_prog=$_G_dir/$_G_prog_name$_exeext
+          func_executable_p "$_G_path_prog" || continue
+          case `"$_G_path_prog" --version 2>&1` in
+            *GNU*) func_path_progs_result=$_G_path_prog _G_path_prog_found=: ;;
+            *)     $_G_check_func $_G_path_prog
+		   func_path_progs_result=$func_check_prog_result
+		   ;;
+          esac
+          $_G_path_prog_found && break 3
+        done
+      done
+    done
+    IFS=$_G_save_IFS
+    test -z "$func_path_progs_result" && {
+      echo "no acceptable sed could be found in \$PATH" >&2
+      exit 1
+    }
+}
+
+
+# We want to be able to use the functions in this file before configure
+# has figured out where the best binaries are kept, which means we have
+# to search for them ourselves - except when the results are already set
+# where we skip the searches.
+
+# Unless the user overrides by setting SED, search the path for either GNU
+# sed, or the sed that truncates its output the least.
+test -z "$SED" && {
+  _G_sed_script=s/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/
+  for _G_i in 1 2 3 4 5 6 7; do
+    _G_sed_script=$_G_sed_script$nl$_G_sed_script
+  done
+  echo "$_G_sed_script" 2>/dev/null | sed 99q >conftest.sed
+  _G_sed_script=
+
+  func_check_prog_sed ()
+  {
+    _G_path_prog=$1
+
+    _G_count=0
+    printf 0123456789 >conftest.in
+    while :
+    do
+      cat conftest.in conftest.in >conftest.tmp
+      mv conftest.tmp conftest.in
+      cp conftest.in conftest.nl
+      echo '' >> conftest.nl
+      "$_G_path_prog" -f conftest.sed <conftest.nl >conftest.out 2>/dev/null || break
+      diff conftest.out conftest.nl >/dev/null 2>&1 || break
+      _G_count=`expr $_G_count + 1`
+      if test "$_G_count" -gt "$_G_path_prog_max"; then
+        # Best one so far, save it but keep looking for a better one
+        func_check_prog_result=$_G_path_prog
+        _G_path_prog_max=$_G_count
+      fi
+      # 10*(2^10) chars as input seems more than enough
+      test 10 -lt "$_G_count" && break
+    done
+    rm -f conftest.in conftest.tmp conftest.nl conftest.out
+  }
+
+  func_path_progs "sed gsed" func_check_prog_sed "$PATH:/usr/xpg4/bin"
+  rm -f conftest.sed
+  SED=$func_path_progs_result
+}
+
+
+# Unless the user overrides by setting GREP, search the path for either GNU
+# grep, or the grep that truncates its output the least.
+test -z "$GREP" && {
+  func_check_prog_grep ()
+  {
+    _G_path_prog=$1
+
+    _G_count=0
+    _G_path_prog_max=0
+    printf 0123456789 >conftest.in
+    while :
+    do
+      cat conftest.in conftest.in >conftest.tmp
+      mv conftest.tmp conftest.in
+      cp conftest.in conftest.nl
+      echo 'GREP' >> conftest.nl
+      "$_G_path_prog" -e 'GREP$' -e '-(cannot match)-' <conftest.nl >conftest.out 2>/dev/null || break
+      diff conftest.out conftest.nl >/dev/null 2>&1 || break
+      _G_count=`expr $_G_count + 1`
+      if test "$_G_count" -gt "$_G_path_prog_max"; then
+        # Best one so far, save it but keep looking for a better one
+        func_check_prog_result=$_G_path_prog
+        _G_path_prog_max=$_G_count
+      fi
+      # 10*(2^10) chars as input seems more than enough
+      test 10 -lt "$_G_count" && break
+    done
+    rm -f conftest.in conftest.tmp conftest.nl conftest.out
+  }
+
+  func_path_progs "grep ggrep" func_check_prog_grep "$PATH:/usr/xpg4/bin"
+  GREP=$func_path_progs_result
+}
+
+
+## ------------------------------- ##
+## User overridable command paths. ##
+## ------------------------------- ##
+
+# All uppercase variable names are used for environment variables.  These
+# variables can be overridden by the user before calling a script that
+# uses them if a suitable command of that name is not already available
+# in the command search PATH.
+
+: ${CP="cp -f"}
+: ${ECHO="printf %s\n"}
+: ${EGREP="$GREP -E"}
+: ${FGREP="$GREP -F"}
+: ${LN_S="ln -s"}
+: ${MAKE="make"}
+: ${MKDIR="mkdir"}
+: ${MV="mv -f"}
+: ${RM="rm -f"}
+: ${SHELL="${CONFIG_SHELL-/bin/sh}"}
+
+
+## -------------------- ##
+## Useful sed snippets. ##
+## -------------------- ##
+
+sed_dirname='s|/[^/]*$||'
+sed_basename='s|^.*/||'
+
+# Sed substitution that helps us do robust quoting.  It backslashifies
+# metacharacters that are still active within double-quoted strings.
+sed_quote_subst='s|\([`"$\\]\)|\\\1|g'
+
+# Same as above, but do not quote variable references.
+sed_double_quote_subst='s/\(["`\\]\)/\\\1/g'
+
+# Sed substitution that turns a string into a regex matching for the
+# string literally.
+sed_make_literal_regex='s|[].[^$\\*\/]|\\&|g'
+
+# Sed substitution that converts a w32 file name or path
+# that contains forward slashes, into one that contains
+# (escaped) backslashes.  A very naive implementation.
+sed_naive_backslashify='s|\\\\*|\\|g;s|/|\\|g;s|\\|\\\\|g'
+
+# Re-'\' parameter expansions in output of sed_double_quote_subst that
+# were '\'-ed in input to the same.  If an odd number of '\' preceded a
+# '$' in input to sed_double_quote_subst, that '$' was protected from
+# expansion.  Since each input '\' is now two '\'s, look for any number
+# of runs of four '\'s followed by two '\'s and then a '$'.  '\' that '$'.
+_G_bs='\\'
+_G_bs2='\\\\'
+_G_bs4='\\\\\\\\'
+_G_dollar='\$'
+sed_double_backslash="\
+  s/$_G_bs4/&\\
+/g
+  s/^$_G_bs2$_G_dollar/$_G_bs&/
+  s/\\([^$_G_bs]\\)$_G_bs2$_G_dollar/\\1$_G_bs2$_G_bs$_G_dollar/g
+  s/\n//g"
+
+# require_check_ifs_backslash
+# ---------------------------
+# Check if we can use backslash as IFS='\' separator, and set
+# $check_ifs_backshlash_broken to ':' or 'false'.
+require_check_ifs_backslash=func_require_check_ifs_backslash
+func_require_check_ifs_backslash ()
+{
+  _G_save_IFS=$IFS
+  IFS='\'
+  _G_check_ifs_backshlash='a\\b'
+  for _G_i in $_G_check_ifs_backshlash
+  do
+  case $_G_i in
+  a)
+    check_ifs_backshlash_broken=false
+    ;;
+  '')
+    break
+    ;;
+  *)
+    check_ifs_backshlash_broken=:
+    break
+    ;;
+  esac
+  done
+  IFS=$_G_save_IFS
+  require_check_ifs_backslash=:
+}
+
+
+## ----------------- ##
+## Global variables. ##
+## ----------------- ##
+
+# Except for the global variables explicitly listed below, the following
+# functions in the '^func_' namespace, and the '^require_' namespace
+# variables initialised in the 'Resource management' section, sourcing
+# this file will not pollute your global namespace with anything
+# else. There's no portable way to scope variables in Bourne shell
+# though, so actually running these functions will sometimes place
+# results into a variable named after the function, and often use
+# temporary variables in the '^_G_' namespace. If you are careful to
+# avoid using those namespaces casually in your sourcing script, things
+# should continue to work as you expect. And, of course, you can freely
+# overwrite any of the functions or variables defined here before
+# calling anything to customize them.
+
+EXIT_SUCCESS=0
+EXIT_FAILURE=1
+EXIT_MISMATCH=63  # $? = 63 is used to indicate version mismatch to missing.
+EXIT_SKIP=77	  # $? = 77 is used to indicate a skipped test to automake.
+
+# Allow overriding, eg assuming that you follow the convention of
+# putting '$debug_cmd' at the start of all your functions, you can get
+# bash to show function call trace with:
+#
+#    debug_cmd='echo "${FUNCNAME[0]} $*" >&2' bash your-script-name
+debug_cmd=${debug_cmd-":"}
+exit_cmd=:
+
+# By convention, finish your script with:
+#
+#    exit $exit_status
+#
+# so that you can set exit_status to non-zero if you want to indicate
+# something went wrong during execution without actually bailing out at
+# the point of failure.
+exit_status=$EXIT_SUCCESS
+
+# Work around backward compatibility issue on IRIX 6.5. On IRIX 6.4+, sh
+# is ksh but when the shell is invoked as "sh" and the current value of
+# the _XPG environment variable is not equal to 1 (one), the special
+# positional parameter $0, within a function call, is the name of the
+# function.
+progpath=$0
+
+# The name of this program.
+progname=`$ECHO "$progpath" |$SED "$sed_basename"`
+
+# Make sure we have an absolute progpath for reexecution:
+case $progpath in
+  [\\/]*|[A-Za-z]:\\*) ;;
+  *[\\/]*)
+     progdir=`$ECHO "$progpath" |$SED "$sed_dirname"`
+     progdir=`cd "$progdir" && pwd`
+     progpath=$progdir/$progname
+     ;;
+  *)
+     _G_IFS=$IFS
+     IFS=${PATH_SEPARATOR-:}
+     for progdir in $PATH; do
+       IFS=$_G_IFS
+       test -x "$progdir/$progname" && break
+     done
+     IFS=$_G_IFS
+     test -n "$progdir" || progdir=`pwd`
+     progpath=$progdir/$progname
+     ;;
+esac
+
+
+## ----------------- ##
+## Standard options. ##
+## ----------------- ##
+
+# The following options affect the operation of the functions defined
+# below, and should be set appropriately depending on run-time para-
+# meters passed on the command line.
+
+opt_dry_run=false
+opt_quiet=false
+opt_verbose=false
+
+# Categories 'all' and 'none' are always available.  Append any others
+# you will pass as the first argument to func_warning from your own
+# code.
+warning_categories=
+
+# By default, display warnings according to 'opt_warning_types'.  Set
+# 'warning_func'  to ':' to elide all warnings, or func_fatal_error to
+# treat the next displayed warning as a fatal error.
+warning_func=func_warn_and_continue
+
+# Set to 'all' to display all warnings, 'none' to suppress all
+# warnings, or a space delimited list of some subset of
+# 'warning_categories' to display only the listed warnings.
+opt_warning_types=all
+
+
+## -------------------- ##
+## Resource management. ##
+## -------------------- ##
+
+# This section contains definitions for functions that each ensure a
+# particular resource (a file, or a non-empty configuration variable for
+# example) is available, and if appropriate to extract default values
+# from pertinent package files. Call them using their associated
+# 'require_*' variable to ensure that they are executed, at most, once.
+#
+# It's entirely deliberate that calling these functions can set
+# variables that don't obey the namespace limitations obeyed by the rest
+# of this file, in order that that they be as useful as possible to
+# callers.
+
+
+# require_term_colors
+# -------------------
+# Allow display of bold text on terminals that support it.
+require_term_colors=func_require_term_colors
+func_require_term_colors ()
+{
+    $debug_cmd
+
+    test -t 1 && {
+      # COLORTERM and USE_ANSI_COLORS environment variables take
+      # precedence, because most terminfo databases neglect to describe
+      # whether color sequences are supported.
+      test -n "${COLORTERM+set}" && : ${USE_ANSI_COLORS="1"}
+
+      if test 1 = "$USE_ANSI_COLORS"; then
+        # Standard ANSI escape sequences
+        tc_reset=''
+        tc_bold='';   tc_standout=''
+        tc_red='';   tc_green=''
+        tc_blue='';  tc_cyan=''
+      else
+        # Otherwise trust the terminfo database after all.
+        test -n "`tput sgr0 2>/dev/null`" && {
+          tc_reset=`tput sgr0`
+          test -n "`tput bold 2>/dev/null`" && tc_bold=`tput bold`
+          tc_standout=$tc_bold
+          test -n "`tput smso 2>/dev/null`" && tc_standout=`tput smso`
+          test -n "`tput setaf 1 2>/dev/null`" && tc_red=`tput setaf 1`
+          test -n "`tput setaf 2 2>/dev/null`" && tc_green=`tput setaf 2`
+          test -n "`tput setaf 4 2>/dev/null`" && tc_blue=`tput setaf 4`
+          test -n "`tput setaf 5 2>/dev/null`" && tc_cyan=`tput setaf 5`
+        }
+      fi
+    }
+
+    require_term_colors=:
+}
+
+
+## ----------------- ##
+## Function library. ##
+## ----------------- ##
+
+# This section contains a variety of useful functions to call in your
+# scripts. Take note of the portable wrappers for features provided by
+# some modern shells, which will fall back to slower equivalents on
+# less featureful shells.
+
+
+# func_append VAR VALUE
+# ---------------------
+# Append VALUE onto the existing contents of VAR.
+
+  # We should try to minimise forks, especially on Windows where they are
+  # unreasonably slow, so skip the feature probes when bash or zsh are
+  # being used:
+  if test set = "${BASH_VERSION+set}${ZSH_VERSION+set}"; then
+    : ${_G_HAVE_ARITH_OP="yes"}
+    : ${_G_HAVE_XSI_OPS="yes"}
+    # The += operator was introduced in bash 3.1
+    case $BASH_VERSION in
+      [12].* | 3.0 | 3.0*) ;;
+      *)
+        : ${_G_HAVE_PLUSEQ_OP="yes"}
+        ;;
+    esac
+  fi
+
+  # _G_HAVE_PLUSEQ_OP
+  # Can be empty, in which case the shell is probed, "yes" if += is
+  # useable or anything else if it does not work.
+  test -z "$_G_HAVE_PLUSEQ_OP" \
+    && (eval 'x=a; x+=" b"; test "a b" = "$x"') 2>/dev/null \
+    && _G_HAVE_PLUSEQ_OP=yes
+
+if test yes = "$_G_HAVE_PLUSEQ_OP"
+then
+  # This is an XSI compatible shell, allowing a faster implementation...
+  eval 'func_append ()
+  {
+    $debug_cmd
+
+    eval "$1+=\$2"
+  }'
+else
+  # ...otherwise fall back to using expr, which is often a shell builtin.
+  func_append ()
+  {
+    $debug_cmd
+
+    eval "$1=\$$1\$2"
+  }
+fi
+
+
+# func_append_quoted VAR VALUE
+# ----------------------------
+# Quote VALUE and append to the end of shell variable VAR, separated
+# by a space.
+if test yes = "$_G_HAVE_PLUSEQ_OP"; then
+  eval 'func_append_quoted ()
+  {
+    $debug_cmd
+
+    func_quote_arg pretty "$2"
+    eval "$1+=\\ \$func_quote_arg_result"
+  }'
+else
+  func_append_quoted ()
+  {
+    $debug_cmd
+
+    func_quote_arg pretty "$2"
+    eval "$1=\$$1\\ \$func_quote_arg_result"
+  }
+fi
+
+
+# func_append_uniq VAR VALUE
+# --------------------------
+# Append unique VALUE onto the existing contents of VAR, assuming
+# entries are delimited by the first character of VALUE.  For example:
+#
+#   func_append_uniq options " --another-option option-argument"
+#
+# will only append to $options if " --another-option option-argument "
+# is not already present somewhere in $options already (note spaces at
+# each end implied by leading space in second argument).
+func_append_uniq ()
+{
+    $debug_cmd
+
+    eval _G_current_value='`$ECHO $'$1'`'
+    _G_delim=`expr "$2" : '\(.\)'`
+
+    case $_G_delim$_G_current_value$_G_delim in
+      *"$2$_G_delim"*) ;;
+      *) func_append "$@" ;;
+    esac
+}
+
+
+# func_arith TERM...
+# ------------------
+# Set func_arith_result to the result of evaluating TERMs.
+  test -z "$_G_HAVE_ARITH_OP" \
+    && (eval 'test 2 = $(( 1 + 1 ))') 2>/dev/null \
+    && _G_HAVE_ARITH_OP=yes
+
+if test yes = "$_G_HAVE_ARITH_OP"; then
+  eval 'func_arith ()
+  {
+    $debug_cmd
+
+    func_arith_result=$(( $* ))
+  }'
+else
+  func_arith ()
+  {
+    $debug_cmd
+
+    func_arith_result=`expr "$@"`
+  }
+fi
+
+
+# func_basename FILE
+# ------------------
+# Set func_basename_result to FILE with everything up to and including
+# the last / stripped.
+if test yes = "$_G_HAVE_XSI_OPS"; then
+  # If this shell supports suffix pattern removal, then use it to avoid
+  # forking. Hide the definitions single quotes in case the shell chokes
+  # on unsupported syntax...
+  _b='func_basename_result=${1##*/}'
+  _d='case $1 in
+        */*) func_dirname_result=${1%/*}$2 ;;
+        *  ) func_dirname_result=$3        ;;
+      esac'
+
+else
+  # ...otherwise fall back to using sed.
+  _b='func_basename_result=`$ECHO "$1" |$SED "$sed_basename"`'
+  _d='func_dirname_result=`$ECHO "$1"  |$SED "$sed_dirname"`
+      if test "X$func_dirname_result" = "X$1"; then
+        func_dirname_result=$3
+      else
+        func_append func_dirname_result "$2"
+      fi'
+fi
+
+eval 'func_basename ()
+{
+    $debug_cmd
+
+    '"$_b"'
+}'
+
+
+# func_dirname FILE APPEND NONDIR_REPLACEMENT
+# -------------------------------------------
+# Compute the dirname of FILE.  If nonempty, add APPEND to the result,
+# otherwise set result to NONDIR_REPLACEMENT.
+eval 'func_dirname ()
+{
+    $debug_cmd
+
+    '"$_d"'
+}'
+
+
+# func_dirname_and_basename FILE APPEND NONDIR_REPLACEMENT
+# --------------------------------------------------------
+# Perform func_basename and func_dirname in a single function
+# call:
+#   dirname:  Compute the dirname of FILE.  If nonempty,
+#             add APPEND to the result, otherwise set result
+#             to NONDIR_REPLACEMENT.
+#             value returned in "$func_dirname_result"
+#   basename: Compute filename of FILE.
+#             value retuned in "$func_basename_result"
+# For efficiency, we do not delegate to the functions above but instead
+# duplicate the functionality here.
+eval 'func_dirname_and_basename ()
+{
+    $debug_cmd
+
+    '"$_b"'
+    '"$_d"'
+}'
+
+
+# func_echo ARG...
+# ----------------
+# Echo program name prefixed message.
+func_echo ()
+{
+    $debug_cmd
+
+    _G_message=$*
+
+    func_echo_IFS=$IFS
+    IFS=$nl
+    for _G_line in $_G_message; do
+      IFS=$func_echo_IFS
+      $ECHO "$progname: $_G_line"
+    done
+    IFS=$func_echo_IFS
+}
+
+
+# func_echo_all ARG...
+# --------------------
+# Invoke $ECHO with all args, space-separated.
+func_echo_all ()
+{
+    $ECHO "$*"
+}
+
+
+# func_echo_infix_1 INFIX ARG...
+# ------------------------------
+# Echo program name, followed by INFIX on the first line, with any
+# additional lines not showing INFIX.
+func_echo_infix_1 ()
+{
+    $debug_cmd
+
+    $require_term_colors
+
+    _G_infix=$1; shift
+    _G_indent=$_G_infix
+    _G_prefix="$progname: $_G_infix: "
+    _G_message=$*
+
+    # Strip color escape sequences before counting printable length
+    for _G_tc in "$tc_reset" "$tc_bold" "$tc_standout" "$tc_red" "$tc_green" "$tc_blue" "$tc_cyan"
+    do
+      test -n "$_G_tc" && {
+        _G_esc_tc=`$ECHO "$_G_tc" | $SED "$sed_make_literal_regex"`
+        _G_indent=`$ECHO "$_G_indent" | $SED "s|$_G_esc_tc||g"`
+      }
+    done
+    _G_indent="$progname: "`echo "$_G_indent" | $SED 's|.| |g'`"  " ## exclude from sc_prohibit_nested_quotes
+
+    func_echo_infix_1_IFS=$IFS
+    IFS=$nl
+    for _G_line in $_G_message; do
+      IFS=$func_echo_infix_1_IFS
+      $ECHO "$_G_prefix$tc_bold$_G_line$tc_reset" >&2
+      _G_prefix=$_G_indent
+    done
+    IFS=$func_echo_infix_1_IFS
+}
+
+
+# func_error ARG...
+# -----------------
+# Echo program name prefixed message to standard error.
+func_error ()
+{
+    $debug_cmd
+
+    $require_term_colors
+
+    func_echo_infix_1 "  $tc_standout${tc_red}error$tc_reset" "$*" >&2
+}
+
+
+# func_fatal_error ARG...
+# -----------------------
+# Echo program name prefixed message to standard error, and exit.
+func_fatal_error ()
+{
+    $debug_cmd
+
+    func_error "$*"
+    exit $EXIT_FAILURE
+}
+
+
+# func_grep EXPRESSION FILENAME
+# -----------------------------
+# Check whether EXPRESSION matches any line of FILENAME, without output.
+func_grep ()
+{
+    $debug_cmd
+
+    $GREP "$1" "$2" >/dev/null 2>&1
+}
+
+
+# func_len STRING
+# ---------------
+# Set func_len_result to the length of STRING. STRING may not
+# start with a hyphen.
+  test -z "$_G_HAVE_XSI_OPS" \
+    && (eval 'x=a/b/c;
+      test 5aa/bb/cc = "${#x}${x%%/*}${x%/*}${x#*/}${x##*/}"') 2>/dev/null \
+    && _G_HAVE_XSI_OPS=yes
+
+if test yes = "$_G_HAVE_XSI_OPS"; then
+  eval 'func_len ()
+  {
+    $debug_cmd
+
+    func_len_result=${#1}
+  }'
+else
+  func_len ()
+  {
+    $debug_cmd
+
+    func_len_result=`expr "$1" : ".*" 2>/dev/null || echo $max_cmd_len`
+  }
+fi
+
+
+# func_mkdir_p DIRECTORY-PATH
+# ---------------------------
+# Make sure the entire path to DIRECTORY-PATH is available.
+func_mkdir_p ()
+{
+    $debug_cmd
+
+    _G_directory_path=$1
+    _G_dir_list=
+
+    if test -n "$_G_directory_path" && test : != "$opt_dry_run"; then
+
+      # Protect directory names starting with '-'
+      case $_G_directory_path in
+        -*) _G_directory_path=./$_G_directory_path ;;
+      esac
+
+      # While some portion of DIR does not yet exist...
+      while test ! -d "$_G_directory_path"; do
+        # ...make a list in topmost first order.  Use a colon delimited
+	# list incase some portion of path contains whitespace.
+        _G_dir_list=$_G_directory_path:$_G_dir_list
+
+        # If the last portion added has no slash in it, the list is done
+        case $_G_directory_path in */*) ;; *) break ;; esac
+
+        # ...otherwise throw away the child directory and loop
+        _G_directory_path=`$ECHO "$_G_directory_path" | $SED -e "$sed_dirname"`
+      done
+      _G_dir_list=`$ECHO "$_G_dir_list" | $SED 's|:*$||'`
+
+      func_mkdir_p_IFS=$IFS; IFS=:
+      for _G_dir in $_G_dir_list; do
+	IFS=$func_mkdir_p_IFS
+        # mkdir can fail with a 'File exist' error if two processes
+        # try to create one of the directories concurrently.  Don't
+        # stop in that case!
+        $MKDIR "$_G_dir" 2>/dev/null || :
+      done
+      IFS=$func_mkdir_p_IFS
+
+      # Bail out if we (or some other process) failed to create a directory.
+      test -d "$_G_directory_path" || \
+        func_fatal_error "Failed to create '$1'"
+    fi
+}
+
+
+# func_mktempdir [BASENAME]
+# -------------------------
+# Make a temporary directory that won't clash with other running
+# libtool processes, and avoids race conditions if possible.  If
+# given, BASENAME is the basename for that directory.
+func_mktempdir ()
+{
+    $debug_cmd
+
+    _G_template=${TMPDIR-/tmp}/${1-$progname}
+
+    if test : = "$opt_dry_run"; then
+      # Return a directory name, but don't create it in dry-run mode
+      _G_tmpdir=$_G_template-$$
+    else
+
+      # If mktemp works, use that first and foremost
+      _G_tmpdir=`mktemp -d "$_G_template-XXXXXXXX" 2>/dev/null`
+
+      if test ! -d "$_G_tmpdir"; then
+        # Failing that, at least try and use $RANDOM to avoid a race
+        _G_tmpdir=$_G_template-${RANDOM-0}$$
+
+        func_mktempdir_umask=`umask`
+        umask 0077
+        $MKDIR "$_G_tmpdir"
+        umask $func_mktempdir_umask
+      fi
+
+      # If we're not in dry-run mode, bomb out on failure
+      test -d "$_G_tmpdir" || \
+        func_fatal_error "cannot create temporary directory '$_G_tmpdir'"
+    fi
+
+    $ECHO "$_G_tmpdir"
+}
+
+
+# func_normal_abspath PATH
+# ------------------------
+# Remove doubled-up and trailing slashes, "." path components,
+# and cancel out any ".." path components in PATH after making
+# it an absolute path.
+func_normal_abspath ()
+{
+    $debug_cmd
+
+    # These SED scripts presuppose an absolute path with a trailing slash.
+    _G_pathcar='s|^/\([^/]*\).*$|\1|'
+    _G_pathcdr='s|^/[^/]*||'
+    _G_removedotparts=':dotsl
+		s|/\./|/|g
+		t dotsl
+		s|/\.$|/|'
+    _G_collapseslashes='s|/\{1,\}|/|g'
+    _G_finalslash='s|/*$|/|'
+
+    # Start from root dir and reassemble the path.
+    func_normal_abspath_result=
+    func_normal_abspath_tpath=$1
+    func_normal_abspath_altnamespace=
+    case $func_normal_abspath_tpath in
+      "")
+        # Empty path, that just means $cwd.
+        func_stripname '' '/' "`pwd`"
+        func_normal_abspath_result=$func_stripname_result
+        return
+        ;;
+      # The next three entries are used to spot a run of precisely
+      # two leading slashes without using negated character classes;
+      # we take advantage of case's first-match behaviour.
+      ///*)
+        # Unusual form of absolute path, do nothing.
+        ;;
+      //*)
+        # Not necessarily an ordinary path; POSIX reserves leading '//'
+        # and for example Cygwin uses it to access remote file shares
+        # over CIFS/SMB, so we conserve a leading double slash if found.
+        func_normal_abspath_altnamespace=/
+        ;;
+      /*)
+        # Absolute path, do nothing.
+        ;;
+      *)
+        # Relative path, prepend $cwd.
+        func_normal_abspath_tpath=`pwd`/$func_normal_abspath_tpath
+        ;;
+    esac
+
+    # Cancel out all the simple stuff to save iterations.  We also want
+    # the path to end with a slash for ease of parsing, so make sure
+    # there is one (and only one) here.
+    func_normal_abspath_tpath=`$ECHO "$func_normal_abspath_tpath" | $SED \
+          -e "$_G_removedotparts" -e "$_G_collapseslashes" -e "$_G_finalslash"`
+    while :; do
+      # Processed it all yet?
+      if test / = "$func_normal_abspath_tpath"; then
+        # If we ascended to the root using ".." the result may be empty now.
+        if test -z "$func_normal_abspath_result"; then
+          func_normal_abspath_result=/
+        fi
+        break
+      fi
+      func_normal_abspath_tcomponent=`$ECHO "$func_normal_abspath_tpath" | $SED \
+          -e "$_G_pathcar"`
+      func_normal_abspath_tpath=`$ECHO "$func_normal_abspath_tpath" | $SED \
+          -e "$_G_pathcdr"`
+      # Figure out what to do with it
+      case $func_normal_abspath_tcomponent in
+        "")
+          # Trailing empty path component, ignore it.
+          ;;
+        ..)
+          # Parent dir; strip last assembled component from result.
+          func_dirname "$func_normal_abspath_result"
+          func_normal_abspath_result=$func_dirname_result
+          ;;
+        *)
+          # Actual path component, append it.
+          func_append func_normal_abspath_result "/$func_normal_abspath_tcomponent"
+          ;;
+      esac
+    done
+    # Restore leading double-slash if one was found on entry.
+    func_normal_abspath_result=$func_normal_abspath_altnamespace$func_normal_abspath_result
+}
+
+
+# func_notquiet ARG...
+# --------------------
+# Echo program name prefixed message only when not in quiet mode.
+func_notquiet ()
+{
+    $debug_cmd
+
+    $opt_quiet || func_echo ${1+"$@"}
+
+    # A bug in bash halts the script if the last line of a function
+    # fails when set -e is in force, so we need another command to
+    # work around that:
+    :
+}
+
+
+# func_relative_path SRCDIR DSTDIR
+# --------------------------------
+# Set func_relative_path_result to the relative path from SRCDIR to DSTDIR.
+func_relative_path ()
+{
+    $debug_cmd
+
+    func_relative_path_result=
+    func_normal_abspath "$1"
+    func_relative_path_tlibdir=$func_normal_abspath_result
+    func_normal_abspath "$2"
+    func_relative_path_tbindir=$func_normal_abspath_result
+
+    # Ascend the tree starting from libdir
+    while :; do
+      # check if we have found a prefix of bindir
+      case $func_relative_path_tbindir in
+        $func_relative_path_tlibdir)
+          # found an exact match
+          func_relative_path_tcancelled=
+          break
+          ;;
+        $func_relative_path_tlibdir*)
+          # found a matching prefix
+          func_stripname "$func_relative_path_tlibdir" '' "$func_relative_path_tbindir"
+          func_relative_path_tcancelled=$func_stripname_result
+          if test -z "$func_relative_path_result"; then
+            func_relative_path_result=.
+          fi
+          break
+          ;;
+        *)
+          func_dirname $func_relative_path_tlibdir
+          func_relative_path_tlibdir=$func_dirname_result
+          if test -z "$func_relative_path_tlibdir"; then
+            # Have to descend all the way to the root!
+            func_relative_path_result=../$func_relative_path_result
+            func_relative_path_tcancelled=$func_relative_path_tbindir
+            break
+          fi
+          func_relative_path_result=../$func_relative_path_result
+          ;;
+      esac
+    done
+
+    # Now calculate path; take care to avoid doubling-up slashes.
+    func_stripname '' '/' "$func_relative_path_result"
+    func_relative_path_result=$func_stripname_result
+    func_stripname '/' '/' "$func_relative_path_tcancelled"
+    if test -n "$func_stripname_result"; then
+      func_append func_relative_path_result "/$func_stripname_result"
+    fi
+
+    # Normalisation. If bindir is libdir, return '.' else relative path.
+    if test -n "$func_relative_path_result"; then
+      func_stripname './' '' "$func_relative_path_result"
+      func_relative_path_result=$func_stripname_result
+    fi
+
+    test -n "$func_relative_path_result" || func_relative_path_result=.
+
+    :
+}
+
+
+# func_quote_portable EVAL ARG
+# ----------------------------
+# Internal function to portably implement func_quote_arg.  Note that we still
+# keep attention to performance here so we as much as possible try to avoid
+# calling sed binary (so far O(N) complexity as long as func_append is O(1)).
+func_quote_portable ()
+{
+    $debug_cmd
+
+    $require_check_ifs_backslash
+
+    func_quote_portable_result=$2
+
+    # one-time-loop (easy break)
+    while true
+    do
+      if $1; then
+        func_quote_portable_result=`$ECHO "$2" | $SED \
+          -e "$sed_double_quote_subst" -e "$sed_double_backslash"`
+        break
+      fi
+
+      # Quote for eval.
+      case $func_quote_portable_result in
+        *[\\\`\"\$]*)
+          # Fallback to sed for $func_check_bs_ifs_broken=:, or when the string
+          # contains the shell wildcard characters.
+          case $check_ifs_backshlash_broken$func_quote_portable_result in
+            :*|*[\[\*\?]*)
+              func_quote_portable_result=`$ECHO "$func_quote_portable_result" \
+                  | $SED "$sed_quote_subst"`
+              break
+              ;;
+          esac
+
+          func_quote_portable_old_IFS=$IFS
+          for _G_char in '\' '`' '"' '$'
+          do
+            # STATE($1) PREV($2) SEPARATOR($3)
+            set start "" ""
+            func_quote_portable_result=dummy"$_G_char$func_quote_portable_result$_G_char"dummy
+            IFS=$_G_char
+            for _G_part in $func_quote_portable_result
+            do
+              case $1 in
+              quote)
+                func_append func_quote_portable_result "$3$2"
+                set quote "$_G_part" "\\$_G_char"
+                ;;
+              start)
+                set first "" ""
+                func_quote_portable_result=
+                ;;
+              first)
+                set quote "$_G_part" ""
+                ;;
+              esac
+            done
+          done
+          IFS=$func_quote_portable_old_IFS
+          ;;
+        *) ;;
+      esac
+      break
+    done
+
+    func_quote_portable_unquoted_result=$func_quote_portable_result
+    case $func_quote_portable_result in
+      # double-quote args containing shell metacharacters to delay
+      # word splitting, command substitution and variable expansion
+      # for a subsequent eval.
+      # many bourne shells cannot handle close brackets correctly
+      # in scan sets, so we specify it separately.
+      *[\[\~\#\^\&\*\(\)\{\}\|\;\<\>\?\'\ \	]*|*]*|"")
+        func_quote_portable_result=\"$func_quote_portable_result\"
+        ;;
+    esac
+}
+
+
+# func_quotefast_eval ARG
+# -----------------------
+# Quote one ARG (internal).  This is equivalent to 'func_quote_arg eval ARG',
+# but optimized for speed.  Result is stored in $func_quotefast_eval.
+if test xyes = `(x=; printf -v x %q yes; echo x"$x") 2>/dev/null`; then
+  printf -v _GL_test_printf_tilde %q '~'
+  if test '\~' = "$_GL_test_printf_tilde"; then
+    func_quotefast_eval ()
+    {
+      printf -v func_quotefast_eval_result %q "$1"
+    }
+  else
+    # Broken older Bash implementations.  Make those faster too if possible.
+    func_quotefast_eval ()
+    {
+      case $1 in
+        '~'*)
+          func_quote_portable false "$1"
+          func_quotefast_eval_result=$func_quote_portable_result
+          ;;
+        *)
+          printf -v func_quotefast_eval_result %q "$1"
+          ;;
+      esac
+    }
+  fi
+else
+  func_quotefast_eval ()
+  {
+    func_quote_portable false "$1"
+    func_quotefast_eval_result=$func_quote_portable_result
+  }
+fi
+
+
+# func_quote_arg MODEs ARG
+# ------------------------
+# Quote one ARG to be evaled later.  MODEs argument may contain zero or more
+# specifiers listed below separated by ',' character.  This function returns two
+# values:
+#   i) func_quote_arg_result
+#      double-quoted (when needed), suitable for a subsequent eval
+#  ii) func_quote_arg_unquoted_result
+#      has all characters that are still active within double
+#      quotes backslashified.  Available only if 'unquoted' is specified.
+#
+# Available modes:
+# ----------------
+# 'eval' (default)
+#       - escape shell special characters
+# 'expand'
+#       - the same as 'eval';  but do not quote variable references
+# 'pretty'
+#       - request aesthetic output, i.e. '"a b"' instead of 'a\ b'.  This might
+#         be used later in func_quote to get output like: 'echo "a b"' instead
+#         of 'echo a\ b'.  This is slower than default on some shells.
+# 'unquoted'
+#       - produce also $func_quote_arg_unquoted_result which does not contain
+#         wrapping double-quotes.
+#
+# Examples for 'func_quote_arg pretty,unquoted string':
+#
+#   string      | *_result              | *_unquoted_result
+#   ------------+-----------------------+-------------------
+#   "           | \"                    | \"
+#   a b         | "a b"                 | a b
+#   "a b"       | "\"a b\""             | \"a b\"
+#   *           | "*"                   | *
+#   z="${x-$y}" | "z=\"\${x-\$y}\""     | z=\"\${x-\$y}\"
+#
+# Examples for 'func_quote_arg pretty,unquoted,expand string':
+#
+#   string        |   *_result          |  *_unquoted_result
+#   --------------+---------------------+--------------------
+#   z="${x-$y}"   | "z=\"${x-$y}\""     | z=\"${x-$y}\"
+func_quote_arg ()
+{
+    _G_quote_expand=false
+    case ,$1, in
+      *,expand,*)
+        _G_quote_expand=:
+        ;;
+    esac
+
+    case ,$1, in
+      *,pretty,*|*,expand,*|*,unquoted,*)
+        func_quote_portable $_G_quote_expand "$2"
+        func_quote_arg_result=$func_quote_portable_result
+        func_quote_arg_unquoted_result=$func_quote_portable_unquoted_result
+        ;;
+      *)
+        # Faster quote-for-eval for some shells.
+        func_quotefast_eval "$2"
+        func_quote_arg_result=$func_quotefast_eval_result
+        ;;
+    esac
+}
+
+
+# func_quote MODEs ARGs...
+# ------------------------
+# Quote all ARGs to be evaled later and join them into single command.  See
+# func_quote_arg's description for more info.
+func_quote ()
+{
+    $debug_cmd
+    _G_func_quote_mode=$1 ; shift
+    func_quote_result=
+    while test 0 -lt $#; do
+      func_quote_arg "$_G_func_quote_mode" "$1"
+      if test -n "$func_quote_result"; then
+        func_append func_quote_result " $func_quote_arg_result"
+      else
+        func_append func_quote_result "$func_quote_arg_result"
+      fi
+      shift
+    done
+}
+
+
+# func_stripname PREFIX SUFFIX NAME
+# ---------------------------------
+# strip PREFIX and SUFFIX from NAME, and store in func_stripname_result.
+# PREFIX and SUFFIX must not contain globbing or regex special
+# characters, hashes, percent signs, but SUFFIX may contain a leading
+# dot (in which case that matches only a dot).
+if test yes = "$_G_HAVE_XSI_OPS"; then
+  eval 'func_stripname ()
+  {
+    $debug_cmd
+
+    # pdksh 5.2.14 does not do ${X%$Y} correctly if both X and Y are
+    # positional parameters, so assign one to ordinary variable first.
+    func_stripname_result=$3
+    func_stripname_result=${func_stripname_result#"$1"}
+    func_stripname_result=${func_stripname_result%"$2"}
+  }'
+else
+  func_stripname ()
+  {
+    $debug_cmd
+
+    case $2 in
+      .*) func_stripname_result=`$ECHO "$3" | $SED -e "s%^$1%%" -e "s%\\\\$2\$%%"`;;
+      *)  func_stripname_result=`$ECHO "$3" | $SED -e "s%^$1%%" -e "s%$2\$%%"`;;
+    esac
+  }
+fi
+
+
+# func_show_eval CMD [FAIL_EXP]
+# -----------------------------
+# Unless opt_quiet is true, then output CMD.  Then, if opt_dryrun is
+# not true, evaluate CMD.  If the evaluation of CMD fails, and FAIL_EXP
+# is given, then evaluate it.
+func_show_eval ()
+{
+    $debug_cmd
+
+    _G_cmd=$1
+    _G_fail_exp=${2-':'}
+
+    func_quote_arg pretty,expand "$_G_cmd"
+    eval "func_notquiet $func_quote_arg_result"
+
+    $opt_dry_run || {
+      eval "$_G_cmd"
+      _G_status=$?
+      if test 0 -ne "$_G_status"; then
+	eval "(exit $_G_status); $_G_fail_exp"
+      fi
+    }
+}
+
+
+# func_show_eval_locale CMD [FAIL_EXP]
+# ------------------------------------
+# Unless opt_quiet is true, then output CMD.  Then, if opt_dryrun is
+# not true, evaluate CMD.  If the evaluation of CMD fails, and FAIL_EXP
+# is given, then evaluate it.  Use the saved locale for evaluation.
+func_show_eval_locale ()
+{
+    $debug_cmd
+
+    _G_cmd=$1
+    _G_fail_exp=${2-':'}
+
+    $opt_quiet || {
+      func_quote_arg expand,pretty "$_G_cmd"
+      eval "func_echo $func_quote_arg_result"
+    }
+
+    $opt_dry_run || {
+      eval "$_G_user_locale
+	    $_G_cmd"
+      _G_status=$?
+      eval "$_G_safe_locale"
+      if test 0 -ne "$_G_status"; then
+	eval "(exit $_G_status); $_G_fail_exp"
+      fi
+    }
+}
+
+
+# func_tr_sh
+# ----------
+# Turn $1 into a string suitable for a shell variable name.
+# Result is stored in $func_tr_sh_result.  All characters
+# not in the set a-zA-Z0-9_ are replaced with '_'. Further,
+# if $1 begins with a digit, a '_' is prepended as well.
+func_tr_sh ()
+{
+    $debug_cmd
+
+    case $1 in
+    [0-9]* | *[!a-zA-Z0-9_]*)
+      func_tr_sh_result=`$ECHO "$1" | $SED -e 's/^\([0-9]\)/_\1/' -e 's/[^a-zA-Z0-9_]/_/g'`
+      ;;
+    * )
+      func_tr_sh_result=$1
+      ;;
+    esac
+}
+
+
+# func_verbose ARG...
+# -------------------
+# Echo program name prefixed message in verbose mode only.
+func_verbose ()
+{
+    $debug_cmd
+
+    $opt_verbose && func_echo "$*"
+
+    :
+}
+
+
+# func_warn_and_continue ARG...
+# -----------------------------
+# Echo program name prefixed warning message to standard error.
+func_warn_and_continue ()
+{
+    $debug_cmd
+
+    $require_term_colors
+
+    func_echo_infix_1 "${tc_red}warning$tc_reset" "$*" >&2
+}
+
+
+# func_warning CATEGORY ARG...
+# ----------------------------
+# Echo program name prefixed warning message to standard error. Warning
+# messages can be filtered according to CATEGORY, where this function
+# elides messages where CATEGORY is not listed in the global variable
+# 'opt_warning_types'.
+func_warning ()
+{
+    $debug_cmd
+
+    # CATEGORY must be in the warning_categories list!
+    case " $warning_categories " in
+      *" $1 "*) ;;
+      *) func_internal_error "invalid warning category '$1'" ;;
+    esac
+
+    _G_category=$1
+    shift
+
+    case " $opt_warning_types " in
+      *" $_G_category "*) $warning_func ${1+"$@"} ;;
+    esac
+}
+
+
+# func_sort_ver VER1 VER2
+# -----------------------
+# 'sort -V' is not generally available.
+# Note this deviates from the version comparison in automake
+# in that it treats 1.5 < 1.5.0, and treats 1.4.4a < 1.4-p3a
+# but this should suffice as we won't be specifying old
+# version formats or redundant trailing .0 in bootstrap.conf.
+# If we did want full compatibility then we should probably
+# use m4_version_compare from autoconf.
+func_sort_ver ()
+{
+    $debug_cmd
+
+    printf '%s\n%s\n' "$1" "$2" \
+      | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n -k 5,5n -k 6,6n -k 7,7n -k 8,8n -k 9,9n
+}
+
+# func_lt_ver PREV CURR
+# ---------------------
+# Return true if PREV and CURR are in the correct order according to
+# func_sort_ver, otherwise false.  Use it like this:
+#
+#  func_lt_ver "$prev_ver" "$proposed_ver" || func_fatal_error "..."
+func_lt_ver ()
+{
+    $debug_cmd
+
+    test "x$1" = x`func_sort_ver "$1" "$2" | $SED 1q`
+}
+
+
+# Local variables:
+# mode: shell-script
+# sh-indentation: 2
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-pattern: "10/scriptversion=%:y-%02m-%02d.%02H; # UTC"
+# time-stamp-time-zone: "UTC"
+# End:
+#! /bin/sh
+
+# A portable, pluggable option parser for Bourne shell.
+# Written by Gary V. Vaughan, 2010
+
+# This is free software.  There is NO warranty; not even for
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+# Copyright (C) 2010-2019, 2021 Bootstrap Authors
+#
+# This file is dual licensed under the terms of the MIT license
+# <https://opensource.org/license/MIT>, and GPL version 2 or later
+# <http://www.gnu.org/licenses/gpl-2.0.html>.  You must apply one of
+# these licenses when using or redistributing this software or any of
+# the files within it.  See the URLs above, or the file `LICENSE`
+# included in the Bootstrap distribution for the full license texts.
+
+# Please report bugs or propose patches to:
+# <https://github.com/gnulib-modules/bootstrap/issues>
+
+# Set a version string for this script.
+scriptversion=2019-02-19.15; # UTC
+
+
+## ------ ##
+## Usage. ##
+## ------ ##
+
+# This file is a library for parsing options in your shell scripts along
+# with assorted other useful supporting features that you can make use
+# of too.
+#
+# For the simplest scripts you might need only:
+#
+#   #!/bin/sh
+#   . relative/path/to/funclib.sh
+#   . relative/path/to/options-parser
+#   scriptversion=1.0
+#   func_options ${1+"$@"}
+#   eval set dummy "$func_options_result"; shift
+#   ...rest of your script...
+#
+# In order for the '--version' option to work, you will need to have a
+# suitably formatted comment like the one at the top of this file
+# starting with '# Written by ' and ending with '# Copyright'.
+#
+# For '-h' and '--help' to work, you will also need a one line
+# description of your script's purpose in a comment directly above the
+# '# Written by ' line, like the one at the top of this file.
+#
+# The default options also support '--debug', which will turn on shell
+# execution tracing (see the comment above debug_cmd below for another
+# use), and '--verbose' and the func_verbose function to allow your script
+# to display verbose messages only when your user has specified
+# '--verbose'.
+#
+# After sourcing this file, you can plug in processing for additional
+# options by amending the variables from the 'Configuration' section
+# below, and following the instructions in the 'Option parsing'
+# section further down.
+
+## -------------- ##
+## Configuration. ##
+## -------------- ##
+
+# You should override these variables in your script after sourcing this
+# file so that they reflect the customisations you have added to the
+# option parser.
+
+# The usage line for option parsing errors and the start of '-h' and
+# '--help' output messages. You can embed shell variables for delayed
+# expansion at the time the message is displayed, but you will need to
+# quote other shell meta-characters carefully to prevent them being
+# expanded when the contents are evaled.
+usage='$progpath [OPTION]...'
+
+# Short help message in response to '-h' and '--help'.  Add to this or
+# override it after sourcing this library to reflect the full set of
+# options your script accepts.
+usage_message="\
+       --debug        enable verbose shell tracing
+   -W, --warnings=CATEGORY
+                      report the warnings falling in CATEGORY [all]
+   -v, --verbose      verbosely report processing
+       --version      print version information and exit
+   -h, --help         print short or long help message and exit
+"
+
+# Additional text appended to 'usage_message' in response to '--help'.
+long_help_message="
+Warning categories include:
+       'all'          show all warnings
+       'none'         turn off all the warnings
+       'error'        warnings are treated as fatal errors"
+
+# Help message printed before fatal option parsing errors.
+fatal_help="Try '\$progname --help' for more information."
+
+
+
+## ------------------------- ##
+## Hook function management. ##
+## ------------------------- ##
+
+# This section contains functions for adding, removing, and running hooks
+# in the main code.  A hook is just a list of function names that can be
+# run in order later on.
+
+# func_hookable FUNC_NAME
+# -----------------------
+# Declare that FUNC_NAME will run hooks added with
+# 'func_add_hook FUNC_NAME ...'.
+func_hookable ()
+{
+    $debug_cmd
+
+    func_append hookable_fns " $1"
+}
+
+
+# func_add_hook FUNC_NAME HOOK_FUNC
+# ---------------------------------
+# Request that FUNC_NAME call HOOK_FUNC before it returns.  FUNC_NAME must
+# first have been declared "hookable" by a call to 'func_hookable'.
+func_add_hook ()
+{
+    $debug_cmd
+
+    case " $hookable_fns " in
+      *" $1 "*) ;;
+      *) func_fatal_error "'$1' does not accept hook functions." ;;
+    esac
+
+    eval func_append ${1}_hooks '" $2"'
+}
+
+
+# func_remove_hook FUNC_NAME HOOK_FUNC
+# ------------------------------------
+# Remove HOOK_FUNC from the list of hook functions to be called by
+# FUNC_NAME.
+func_remove_hook ()
+{
+    $debug_cmd
+
+    eval ${1}_hooks='`$ECHO "\$'$1'_hooks" |$SED "s| '$2'||"`'
+}
+
+
+# func_propagate_result FUNC_NAME_A FUNC_NAME_B
+# ---------------------------------------------
+# If the *_result variable of FUNC_NAME_A _is set_, assign its value to
+# *_result variable of FUNC_NAME_B.
+func_propagate_result ()
+{
+    $debug_cmd
+
+    func_propagate_result_result=:
+    if eval "test \"\${${1}_result+set}\" = set"
+    then
+      eval "${2}_result=\$${1}_result"
+    else
+      func_propagate_result_result=false
+    fi
+}
+
+
+# func_run_hooks FUNC_NAME [ARG]...
+# ---------------------------------
+# Run all hook functions registered to FUNC_NAME.
+# It's assumed that the list of hook functions contains nothing more
+# than a whitespace-delimited list of legal shell function names, and
+# no effort is wasted trying to catch shell meta-characters or preserve
+# whitespace.
+func_run_hooks ()
+{
+    $debug_cmd
+
+    _G_rc_run_hooks=false
+
+    case " $hookable_fns " in
+      *" $1 "*) ;;
+      *) func_fatal_error "'$1' does not support hook functions." ;;
+    esac
+
+    eval _G_hook_fns=\$$1_hooks; shift
+
+    for _G_hook in $_G_hook_fns; do
+      func_unset "${_G_hook}_result"
+      eval $_G_hook '${1+"$@"}'
+      func_propagate_result $_G_hook func_run_hooks
+      if $func_propagate_result_result; then
+        eval set dummy "$func_run_hooks_result"; shift
+      fi
+    done
+}
+
+
+
+## --------------- ##
+## Option parsing. ##
+## --------------- ##
+
+# In order to add your own option parsing hooks, you must accept the
+# full positional parameter list from your hook function.  You may remove
+# or edit any options that you action, and then pass back the remaining
+# unprocessed options in '<hooked_function_name>_result', escaped
+# suitably for 'eval'.
+#
+# The '<hooked_function_name>_result' variable is automatically unset
+# before your hook gets called; for best performance, only set the
+# *_result variable when necessary (i.e. don't call the 'func_quote'
+# function unnecessarily because it can be an expensive operation on some
+# machines).
+#
+# Like this:
+#
+#    my_options_prep ()
+#    {
+#        $debug_cmd
+#
+#        # Extend the existing usage message.
+#        usage_message=$usage_message'
+#      -s, --silent       don'\''t print informational messages
+#    '
+#        # No change in '$@' (ignored completely by this hook).  Leave
+#        # my_options_prep_result variable intact.
+#    }
+#    func_add_hook func_options_prep my_options_prep
+#
+#
+#    my_silent_option ()
+#    {
+#        $debug_cmd
+#
+#        args_changed=false
+#
+#        # Note that, for efficiency, we parse as many options as we can
+#        # recognise in a loop before passing the remainder back to the
+#        # caller on the first unrecognised argument we encounter.
+#        while test $# -gt 0; do
+#          opt=$1; shift
+#          case $opt in
+#            --silent|-s) opt_silent=:
+#                         args_changed=:
+#                         ;;
+#            # Separate non-argument short options:
+#            -s*)         func_split_short_opt "$_G_opt"
+#                         set dummy "$func_split_short_opt_name" \
+#                             "-$func_split_short_opt_arg" ${1+"$@"}
+#                         shift
+#                         args_changed=:
+#                         ;;
+#            *)           # Make sure the first unrecognised option "$_G_opt"
+#                         # is added back to "$@" in case we need it later,
+#                         # if $args_changed was set to 'true'.
+#                         set dummy "$_G_opt" ${1+"$@"}; shift; break ;;
+#          esac
+#        done
+#
+#        # Only call 'func_quote' here if we processed at least one argument.
+#        if $args_changed; then
+#          func_quote eval ${1+"$@"}
+#          my_silent_option_result=$func_quote_result
+#        fi
+#    }
+#    func_add_hook func_parse_options my_silent_option
+#
+#
+#    my_option_validation ()
+#    {
+#        $debug_cmd
+#
+#        $opt_silent && $opt_verbose && func_fatal_help "\
+#    '--silent' and '--verbose' options are mutually exclusive."
+#    }
+#    func_add_hook func_validate_options my_option_validation
+#
+# You'll also need to manually amend $usage_message to reflect the extra
+# options you parse.  It's preferable to append if you can, so that
+# multiple option parsing hooks can be added safely.
+
+
+# func_options_finish [ARG]...
+# ----------------------------
+# Finishing the option parse loop (call 'func_options' hooks ATM).
+func_options_finish ()
+{
+    $debug_cmd
+
+    func_run_hooks func_options ${1+"$@"}
+    func_propagate_result func_run_hooks func_options_finish
+}
+
+
+# func_options [ARG]...
+# ---------------------
+# All the functions called inside func_options are hookable. See the
+# individual implementations for details.
+func_hookable func_options
+func_options ()
+{
+    $debug_cmd
+
+    _G_options_quoted=false
+
+    for my_func in options_prep parse_options validate_options options_finish
+    do
+      func_unset func_${my_func}_result
+      func_unset func_run_hooks_result
+      eval func_$my_func '${1+"$@"}'
+      func_propagate_result func_$my_func func_options
+      if $func_propagate_result_result; then
+        eval set dummy "$func_options_result"; shift
+        _G_options_quoted=:
+      fi
+    done
+
+    $_G_options_quoted || {
+      # As we (func_options) are top-level options-parser function and
+      # nobody quoted "$@" for us yet, we need to do it explicitly for
+      # caller.
+      func_quote eval ${1+"$@"}
+      func_options_result=$func_quote_result
+    }
+}
+
+
+# func_options_prep [ARG]...
+# --------------------------
+# All initialisations required before starting the option parse loop.
+# Note that when calling hook functions, we pass through the list of
+# positional parameters.  If a hook function modifies that list, and
+# needs to propagate that back to rest of this script, then the complete
+# modified list must be put in 'func_run_hooks_result' before returning.
+func_hookable func_options_prep
+func_options_prep ()
+{
+    $debug_cmd
+
+    # Option defaults:
+    opt_verbose=false
+    opt_warning_types=
+
+    func_run_hooks func_options_prep ${1+"$@"}
+    func_propagate_result func_run_hooks func_options_prep
+}
+
+
+# func_parse_options [ARG]...
+# ---------------------------
+# The main option parsing loop.
+func_hookable func_parse_options
+func_parse_options ()
+{
+    $debug_cmd
+
+    _G_parse_options_requote=false
+    # this just eases exit handling
+    while test $# -gt 0; do
+      # Defer to hook functions for initial option parsing, so they
+      # get priority in the event of reusing an option name.
+      func_run_hooks func_parse_options ${1+"$@"}
+      func_propagate_result func_run_hooks func_parse_options
+      if $func_propagate_result_result; then
+        eval set dummy "$func_parse_options_result"; shift
+        # Even though we may have changed "$@", we passed the "$@" array
+        # down into the hook and it quoted it for us (because we are in
+        # this if-branch).  No need to quote it again.
+        _G_parse_options_requote=false
+      fi
+
+      # Break out of the loop if we already parsed every option.
+      test $# -gt 0 || break
+
+      # We expect that one of the options parsed in this function matches
+      # and thus we remove _G_opt from "$@" and need to re-quote.
+      _G_match_parse_options=:
+      _G_opt=$1
+      shift
+      case $_G_opt in
+        --debug|-x)   debug_cmd='set -x'
+                      func_echo "enabling shell trace mode" >&2
+                      $debug_cmd
+                      ;;
+
+        --no-warnings|--no-warning|--no-warn)
+                      set dummy --warnings none ${1+"$@"}
+                      shift
+		      ;;
+
+        --warnings|--warning|-W)
+                      if test $# = 0 && func_missing_arg $_G_opt; then
+                        _G_parse_options_requote=:
+                        break
+                      fi
+                      case " $warning_categories $1" in
+                        *" $1 "*)
+                          # trailing space prevents matching last $1 above
+                          func_append_uniq opt_warning_types " $1"
+                          ;;
+                        *all)
+                          opt_warning_types=$warning_categories
+                          ;;
+                        *none)
+                          opt_warning_types=none
+                          warning_func=:
+                          ;;
+                        *error)
+                          opt_warning_types=$warning_categories
+                          warning_func=func_fatal_error
+                          ;;
+                        *)
+                          func_fatal_error \
+                             "unsupported warning category: '$1'"
+                          ;;
+                      esac
+                      shift
+                      ;;
+
+        --verbose|-v) opt_verbose=: ;;
+        --version)    func_version ;;
+        -\?|-h)       func_usage ;;
+        --help)       func_help ;;
+
+	# Separate optargs to long options (plugins may need this):
+	--*=*)        func_split_equals "$_G_opt"
+	              set dummy "$func_split_equals_lhs" \
+                          "$func_split_equals_rhs" ${1+"$@"}
+                      shift
+                      ;;
+
+       # Separate optargs to short options:
+        -W*)
+                      func_split_short_opt "$_G_opt"
+                      set dummy "$func_split_short_opt_name" \
+                          "$func_split_short_opt_arg" ${1+"$@"}
+                      shift
+                      ;;
+
+        # Separate non-argument short options:
+        -\?*|-h*|-v*|-x*)
+                      func_split_short_opt "$_G_opt"
+                      set dummy "$func_split_short_opt_name" \
+                          "-$func_split_short_opt_arg" ${1+"$@"}
+                      shift
+                      ;;
+
+        --)           _G_parse_options_requote=: ; break ;;
+        -*)           func_fatal_help "unrecognised option: '$_G_opt'" ;;
+        *)            set dummy "$_G_opt" ${1+"$@"}; shift
+                      _G_match_parse_options=false
+                      break
+                      ;;
+      esac
+
+      if $_G_match_parse_options; then
+        _G_parse_options_requote=:
+      fi
+    done
+
+    if $_G_parse_options_requote; then
+      # save modified positional parameters for caller
+      func_quote eval ${1+"$@"}
+      func_parse_options_result=$func_quote_result
+    fi
+}
+
+
+# func_validate_options [ARG]...
+# ------------------------------
+# Perform any sanity checks on option settings and/or unconsumed
+# arguments.
+func_hookable func_validate_options
+func_validate_options ()
+{
+    $debug_cmd
+
+    # Display all warnings if -W was not given.
+    test -n "$opt_warning_types" || opt_warning_types=" $warning_categories"
+
+    func_run_hooks func_validate_options ${1+"$@"}
+    func_propagate_result func_run_hooks func_validate_options
+
+    # Bail if the options were screwed!
+    $exit_cmd $EXIT_FAILURE
+}
+
+
+
+## ----------------- ##
+## Helper functions. ##
+## ----------------- ##
+
+# This section contains the helper functions used by the rest of the
+# hookable option parser framework in ascii-betical order.
+
+
+# func_fatal_help ARG...
+# ----------------------
+# Echo program name prefixed message to standard error, followed by
+# a help hint, and exit.
+func_fatal_help ()
+{
+    $debug_cmd
+
+    eval \$ECHO \""Usage: $usage"\"
+    eval \$ECHO \""$fatal_help"\"
+    func_error ${1+"$@"}
+    exit $EXIT_FAILURE
+}
+
+
+# func_help
+# ---------
+# Echo long help message to standard output and exit.
+func_help ()
+{
+    $debug_cmd
+
+    func_usage_message
+    $ECHO "$long_help_message"
+    exit 0
+}
+
+
+# func_missing_arg ARGNAME
+# ------------------------
+# Echo program name prefixed message to standard error and set global
+# exit_cmd.
+func_missing_arg ()
+{
+    $debug_cmd
+
+    func_error "Missing argument for '$1'."
+    exit_cmd=exit
+}
+
+
+# func_split_equals STRING
+# ------------------------
+# Set func_split_equals_lhs and func_split_equals_rhs shell variables
+# after splitting STRING at the '=' sign.
+test -z "$_G_HAVE_XSI_OPS" \
+    && (eval 'x=a/b/c;
+      test 5aa/bb/cc = "${#x}${x%%/*}${x%/*}${x#*/}${x##*/}"') 2>/dev/null \
+    && _G_HAVE_XSI_OPS=yes
+
+if test yes = "$_G_HAVE_XSI_OPS"
+then
+  # This is an XSI compatible shell, allowing a faster implementation...
+  eval 'func_split_equals ()
+  {
+      $debug_cmd
+
+      func_split_equals_lhs=${1%%=*}
+      func_split_equals_rhs=${1#*=}
+      if test "x$func_split_equals_lhs" = "x$1"; then
+        func_split_equals_rhs=
+      fi
+  }'
+else
+  # ...otherwise fall back to using expr, which is often a shell builtin.
+  func_split_equals ()
+  {
+      $debug_cmd
+
+      func_split_equals_lhs=`expr "x$1" : 'x\([^=]*\)'`
+      func_split_equals_rhs=
+      test "x$func_split_equals_lhs=" = "x$1" \
+        || func_split_equals_rhs=`expr "x$1" : 'x[^=]*=\(.*\)$'`
+  }
+fi #func_split_equals
+
+
+# func_split_short_opt SHORTOPT
+# -----------------------------
+# Set func_split_short_opt_name and func_split_short_opt_arg shell
+# variables after splitting SHORTOPT after the 2nd character.
+if test yes = "$_G_HAVE_XSI_OPS"
+then
+  # This is an XSI compatible shell, allowing a faster implementation...
+  eval 'func_split_short_opt ()
+  {
+      $debug_cmd
+
+      func_split_short_opt_arg=${1#??}
+      func_split_short_opt_name=${1%"$func_split_short_opt_arg"}
+  }'
+else
+  # ...otherwise fall back to using expr, which is often a shell builtin.
+  func_split_short_opt ()
+  {
+      $debug_cmd
+
+      func_split_short_opt_name=`expr "x$1" : 'x\(-.\)'`
+      func_split_short_opt_arg=`expr "x$1" : 'x-.\(.*\)$'`
+  }
+fi #func_split_short_opt
+
+
+# func_usage
+# ----------
+# Echo short help message to standard output and exit.
+func_usage ()
+{
+    $debug_cmd
+
+    func_usage_message
+    $ECHO "Run '$progname --help |${PAGER-more}' for full usage"
+    exit 0
+}
+
+
+# func_usage_message
+# ------------------
+# Echo short help message to standard output.
+func_usage_message ()
+{
+    $debug_cmd
+
+    eval \$ECHO \""Usage: $usage"\"
+    echo
+    $SED -n 's|^# ||
+        /^Written by/{
+          x;p;x
+        }
+	h
+	/^Written by/q' < "$progpath"
+    echo
+    eval \$ECHO \""$usage_message"\"
+}
+
+
+# func_version
+# ------------
+# Echo version message to standard output and exit.
+# The version message is extracted from the calling file's header
+# comments, with leading '# ' stripped:
+#   1. First display the progname and version
+#   2. Followed by the header comment line matching  /^# Written by /
+#   3. Then a blank line followed by the first following line matching
+#      /^# Copyright /
+#   4. Immediately followed by any lines between the previous matches,
+#      except lines preceding the intervening completely blank line.
+# For example, see the header comments of this file.
+func_version ()
+{
+    $debug_cmd
+
+    printf '%s\n' "$progname $scriptversion"
+    $SED -n '
+        /^# Written by /!b
+        s|^# ||; p; n
+
+        :fwd2blnk
+        /./ {
+          n
+          b fwd2blnk
+        }
+        p; n
+
+        :holdwrnt
+        s|^# ||
+        s|^# *$||
+        /^Copyright /!{
+          /./H
+          n
+          b holdwrnt
+        }
+
+        s|\((C)\)[ 0-9,-]*[ ,-]\([1-9][0-9]* \)|\1 \2|
+        G
+        s|\(\n\)\n*|\1|g
+        p; q' < "$progpath"
+
+    exit $?
+}
+
+
+# Local variables:
+# mode: shell-script
+# sh-indentation: 2
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-pattern: "30/scriptversion=%:y-%02m-%02d.%02H; # UTC"
+# time-stamp-time-zone: "UTC"
+# End:
+
+# Set a version string.
+scriptversion='(GNU libtool) 2.4.7'
+
+
+# func_echo ARG...
+# ----------------
+# Libtool also displays the current mode in messages, so override
+# funclib.sh func_echo with this custom definition.
+func_echo ()
+{
+    $debug_cmd
+
+    _G_message=$*
+
+    func_echo_IFS=$IFS
+    IFS=$nl
+    for _G_line in $_G_message; do
+      IFS=$func_echo_IFS
+      $ECHO "$progname${opt_mode+: $opt_mode}: $_G_line"
+    done
+    IFS=$func_echo_IFS
+}
+
+
+# func_warning ARG...
+# -------------------
+# Libtool warnings are not categorized, so override funclib.sh
+# func_warning with this simpler definition.
+func_warning ()
+{
+    $debug_cmd
+
+    $warning_func ${1+"$@"}
+}
+
+
+## ---------------- ##
+## Options parsing. ##
+## ---------------- ##
+
+# Hook in the functions to make sure our own options are parsed during
+# the option parsing loop.
+
+usage='$progpath [OPTION]... [MODE-ARG]...'
+
+# Short help message in response to '-h'.
+usage_message="Options:
+       --config             show all configuration variables
+       --debug              enable verbose shell tracing
+   -n, --dry-run            display commands without modifying any files
+       --features           display basic configuration information and exit
+       --mode=MODE          use operation mode MODE
+       --no-warnings        equivalent to '-Wnone'
+       --preserve-dup-deps  don't remove duplicate dependency libraries
+       --quiet, --silent    don't print informational messages
+       --tag=TAG            use configuration variables from tag TAG
+   -v, --verbose            print more informational messages than default
+       --version            print version information
+   -W, --warnings=CATEGORY  report the warnings falling in CATEGORY [all]
+   -h, --help, --help-all   print short, long, or detailed help message
+"
+
+# Additional text appended to 'usage_message' in response to '--help'.
+func_help ()
+{
+    $debug_cmd
+
+    func_usage_message
+    $ECHO "$long_help_message
+
+MODE must be one of the following:
+
+       clean           remove files from the build directory
+       compile         compile a source file into a libtool object
+       execute         automatically set library path, then run a program
+       finish          complete the installation of libtool libraries
+       install         install libraries or executables
+       link            create a library or an executable
+       uninstall       remove libraries from an installed directory
+
+MODE-ARGS vary depending on the MODE.  When passed as first option,
+'--mode=MODE' may be abbreviated as 'MODE' or a unique abbreviation of that.
+Try '$progname --help --mode=MODE' for a more detailed description of MODE.
+
+When reporting a bug, please describe a test case to reproduce it and
+include the following information:
+
+       host-triplet:   $host
+       shell:          $SHELL
+       compiler:       $LTCC
+       compiler flags: $LTCFLAGS
+       linker:         $LD (gnu? $with_gnu_ld)
+       version:        $progname $scriptversion Debian-2.4.7-4
+       automake:       `($AUTOMAKE --version) 2>/dev/null |$SED 1q`
+       autoconf:       `($AUTOCONF --version) 2>/dev/null |$SED 1q`
+
+Report bugs to <bug-libtool@gnu.org>.
+GNU libtool home page: <http://www.gnu.org/s/libtool/>.
+General help using GNU software: <http://www.gnu.org/gethelp/>."
+    exit 0
+}
+
+
+# func_lo2o OBJECT-NAME
+# ---------------------
+# Transform OBJECT-NAME from a '.lo' suffix to the platform specific
+# object suffix.
+
+lo2o=s/\\.lo\$/.$objext/
+o2lo=s/\\.$objext\$/.lo/
+
+if test yes = "$_G_HAVE_XSI_OPS"; then
+  eval 'func_lo2o ()
+  {
+    case $1 in
+      *.lo) func_lo2o_result=${1%.lo}.$objext ;;
+      *   ) func_lo2o_result=$1               ;;
+    esac
+  }'
+
+  # func_xform LIBOBJ-OR-SOURCE
+  # ---------------------------
+  # Transform LIBOBJ-OR-SOURCE from a '.o' or '.c' (or otherwise)
+  # suffix to a '.lo' libtool-object suffix.
+  eval 'func_xform ()
+  {
+    func_xform_result=${1%.*}.lo
+  }'
+else
+  # ...otherwise fall back to using sed.
+  func_lo2o ()
+  {
+    func_lo2o_result=`$ECHO "$1" | $SED "$lo2o"`
+  }
+
+  func_xform ()
+  {
+    func_xform_result=`$ECHO "$1" | $SED 's|\.[^.]*$|.lo|'`
+  }
+fi
+
+
+# func_fatal_configuration ARG...
+# -------------------------------
+# Echo program name prefixed message to standard error, followed by
+# a configuration failure hint, and exit.
+func_fatal_configuration ()
+{
+    func_fatal_error ${1+"$@"} \
+      "See the $PACKAGE documentation for more information." \
+      "Fatal configuration error."
+}
+
+
+# func_config
+# -----------
+# Display the configuration for all the tags in this script.
+func_config ()
+{
+    re_begincf='^# ### BEGIN LIBTOOL'
+    re_endcf='^# ### END LIBTOOL'
+
+    # Default configuration.
+    $SED "1,/$re_begincf CONFIG/d;/$re_endcf CONFIG/,\$d" < "$progpath"
+
+    # Now print the configurations for the tags.
+    for tagname in $taglist; do
+      $SED -n "/$re_begincf TAG CONFIG: $tagname\$/,/$re_endcf TAG CONFIG: $tagname\$/p" < "$progpath"
+    done
+
+    exit $?
+}
+
+
+# func_features
+# -------------
+# Display the features supported by this script.
+func_features ()
+{
+    echo "host: $host"
+    if test yes = "$build_libtool_libs"; then
+      echo "enable shared libraries"
+    else
+      echo "disable shared libraries"
+    fi
+    if test yes = "$build_old_libs"; then
+      echo "enable static libraries"
+    else
+      echo "disable static libraries"
+    fi
+
+    exit $?
+}
+
+
+# func_enable_tag TAGNAME
+# -----------------------
+# Verify that TAGNAME is valid, and either flag an error and exit, or
+# enable the TAGNAME tag.  We also add TAGNAME to the global $taglist
+# variable here.
+func_enable_tag ()
+{
+    # Global variable:
+    tagname=$1
+
+    re_begincf="^# ### BEGIN LIBTOOL TAG CONFIG: $tagname\$"
+    re_endcf="^# ### END LIBTOOL TAG CONFIG: $tagname\$"
+    sed_extractcf=/$re_begincf/,/$re_endcf/p
+
+    # Validate tagname.
+    case $tagname in
+      *[!-_A-Za-z0-9,/]*)
+        func_fatal_error "invalid tag name: $tagname"
+        ;;
+    esac
+
+    # Don't test for the "default" C tag, as we know it's
+    # there but not specially marked.
+    case $tagname in
+        CC) ;;
+    *)
+        if $GREP "$re_begincf" "$progpath" >/dev/null 2>&1; then
+	  taglist="$taglist $tagname"
+
+	  # Evaluate the configuration.  Be careful to quote the path
+	  # and the sed script, to avoid splitting on whitespace, but
+	  # also don't use non-portable quotes within backquotes within
+	  # quotes we have to do it in 2 steps:
+	  extractedcf=`$SED -n -e "$sed_extractcf" < "$progpath"`
+	  eval "$extractedcf"
+        else
+	  func_error "ignoring unknown tag $tagname"
+        fi
+        ;;
+    esac
+}
+
+
+# func_check_version_match
+# ------------------------
+# Ensure that we are using m4 macros, and libtool script from the same
+# release of libtool.
+func_check_version_match ()
+{
+    if test "$package_revision" != "$macro_revision"; then
+      if test "$VERSION" != "$macro_version"; then
+        if test -z "$macro_version"; then
+          cat >&2 <<_LT_EOF
+$progname: Version mismatch error.  This is $PACKAGE $VERSION, but the
+$progname: definition of this LT_INIT comes from an older release.
+$progname: You should recreate aclocal.m4 with macros from $PACKAGE $VERSION
+$progname: and run autoconf again.
+_LT_EOF
+        else
+          cat >&2 <<_LT_EOF
+$progname: Version mismatch error.  This is $PACKAGE $VERSION, but the
+$progname: definition of this LT_INIT comes from $PACKAGE $macro_version.
+$progname: You should recreate aclocal.m4 with macros from $PACKAGE $VERSION
+$progname: and run autoconf again.
+_LT_EOF
+        fi
+      else
+        cat >&2 <<_LT_EOF
+$progname: Version mismatch error.  This is $PACKAGE $VERSION, revision $package_revision,
+$progname: but the definition of this LT_INIT comes from revision $macro_revision.
+$progname: You should recreate aclocal.m4 with macros from revision $package_revision
+$progname: of $PACKAGE $VERSION and run autoconf again.
+_LT_EOF
+      fi
+
+      exit $EXIT_MISMATCH
+    fi
+}
+
+
+# libtool_options_prep [ARG]...
+# -----------------------------
+# Preparation for options parsed by libtool.
+libtool_options_prep ()
+{
+    $debug_mode
+
+    # Option defaults:
+    opt_config=false
+    opt_dlopen=
+    opt_dry_run=false
+    opt_help=false
+    opt_mode=
+    opt_preserve_dup_deps=false
+    opt_quiet=false
+
+    nonopt=
+    preserve_args=
+
+    _G_rc_lt_options_prep=:
+
+    _G_rc_lt_options_prep=:
+
+    # Shorthand for --mode=foo, only valid as the first argument
+    case $1 in
+    clean|clea|cle|cl)
+      shift; set dummy --mode clean ${1+"$@"}; shift
+      ;;
+    compile|compil|compi|comp|com|co|c)
+      shift; set dummy --mode compile ${1+"$@"}; shift
+      ;;
+    execute|execut|execu|exec|exe|ex|e)
+      shift; set dummy --mode execute ${1+"$@"}; shift
+      ;;
+    finish|finis|fini|fin|fi|f)
+      shift; set dummy --mode finish ${1+"$@"}; shift
+      ;;
+    install|instal|insta|inst|ins|in|i)
+      shift; set dummy --mode install ${1+"$@"}; shift
+      ;;
+    link|lin|li|l)
+      shift; set dummy --mode link ${1+"$@"}; shift
+      ;;
+    uninstall|uninstal|uninsta|uninst|unins|unin|uni|un|u)
+      shift; set dummy --mode uninstall ${1+"$@"}; shift
+      ;;
+    *)
+      _G_rc_lt_options_prep=false
+      ;;
+    esac
+
+    if $_G_rc_lt_options_prep; then
+      # Pass back the list of options.
+      func_quote eval ${1+"$@"}
+      libtool_options_prep_result=$func_quote_result
+    fi
+}
+func_add_hook func_options_prep libtool_options_prep
+
+
+# libtool_parse_options [ARG]...
+# ---------------------------------
+# Provide handling for libtool specific options.
+libtool_parse_options ()
+{
+    $debug_cmd
+
+    _G_rc_lt_parse_options=false
+
+    # Perform our own loop to consume as many options as possible in
+    # each iteration.
+    while test $# -gt 0; do
+      _G_match_lt_parse_options=:
+      _G_opt=$1
+      shift
+      case $_G_opt in
+        --dry-run|--dryrun|-n)
+                        opt_dry_run=:
+                        ;;
+
+        --config)       func_config ;;
+
+        --dlopen|-dlopen)
+                        opt_dlopen="${opt_dlopen+$opt_dlopen
+}$1"
+                        shift
+                        ;;
+
+        --preserve-dup-deps)
+                        opt_preserve_dup_deps=: ;;
+
+        --features)     func_features ;;
+
+        --finish)       set dummy --mode finish ${1+"$@"}; shift ;;
+
+        --help)         opt_help=: ;;
+
+        --help-all)     opt_help=': help-all' ;;
+
+        --mode)         test $# = 0 && func_missing_arg $_G_opt && break
+                        opt_mode=$1
+                        case $1 in
+                          # Valid mode arguments:
+                          clean|compile|execute|finish|install|link|relink|uninstall) ;;
+
+                          # Catch anything else as an error
+                          *) func_error "invalid argument for $_G_opt"
+                             exit_cmd=exit
+                             break
+                             ;;
+                        esac
+                        shift
+                        ;;
+
+        --no-silent|--no-quiet)
+                        opt_quiet=false
+                        func_append preserve_args " $_G_opt"
+                        ;;
+
+        --no-warnings|--no-warning|--no-warn)
+                        opt_warning=false
+                        func_append preserve_args " $_G_opt"
+                        ;;
+
+        --no-verbose)
+                        opt_verbose=false
+                        func_append preserve_args " $_G_opt"
+                        ;;
+
+        --silent|--quiet)
+                        opt_quiet=:
+                        opt_verbose=false
+                        func_append preserve_args " $_G_opt"
+                        ;;
+
+        --tag)          test $# = 0 && func_missing_arg $_G_opt && break
+                        opt_tag=$1
+                        func_append preserve_args " $_G_opt $1"
+                        func_enable_tag "$1"
+                        shift
+                        ;;
+
+        --verbose|-v)   opt_quiet=false
+                        opt_verbose=:
+                        func_append preserve_args " $_G_opt"
+                        ;;
+
+        # An option not handled by this hook function:
+        *)              set dummy "$_G_opt" ${1+"$@"} ; shift
+                        _G_match_lt_parse_options=false
+                        break
+                        ;;
+      esac
+      $_G_match_lt_parse_options && _G_rc_lt_parse_options=:
+    done
+
+    if $_G_rc_lt_parse_options; then
+      # save modified positional parameters for caller
+      func_quote eval ${1+"$@"}
+      libtool_parse_options_result=$func_quote_result
+    fi
+}
+func_add_hook func_parse_options libtool_parse_options
+
+
+
+# libtool_validate_options [ARG]...
+# ---------------------------------
+# Perform any sanity checks on option settings and/or unconsumed
+# arguments.
+libtool_validate_options ()
+{
+    # save first non-option argument
+    if test 0 -lt $#; then
+      nonopt=$1
+      shift
+    fi
+
+    # preserve --debug
+    test : = "$debug_cmd" || func_append preserve_args " --debug"
+
+    case $host in
+      # Solaris2 added to fix http://debbugs.gnu.org/cgi/bugreport.cgi?bug=16452
+      # see also: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=59788
+      *cygwin* | *mingw* | *pw32* | *cegcc* | *solaris2* | *os2*)
+        # don't eliminate duplications in $postdeps and $predeps
+        opt_duplicate_compiler_generated_deps=:
+        ;;
+      *)
+        opt_duplicate_compiler_generated_deps=$opt_preserve_dup_deps
+        ;;
+    esac
+
+    $opt_help || {
+      # Sanity checks first:
+      func_check_version_match
+
+      test yes != "$build_libtool_libs" \
+        && test yes != "$build_old_libs" \
+        && func_fatal_configuration "not configured to build any kind of library"
+
+      # Darwin sucks
+      eval std_shrext=\"$shrext_cmds\"
+
+      # Only execute mode is allowed to have -dlopen flags.
+      if test -n "$opt_dlopen" && test execute != "$opt_mode"; then
+        func_error "unrecognized option '-dlopen'"
+        $ECHO "$help" 1>&2
+        exit $EXIT_FAILURE
+      fi
+
+      # Change the help message to a mode-specific one.
+      generic_help=$help
+      help="Try '$progname --help --mode=$opt_mode' for more information."
+    }
+
+    # Pass back the unparsed argument list
+    func_quote eval ${1+"$@"}
+    libtool_validate_options_result=$func_quote_result
+}
+func_add_hook func_validate_options libtool_validate_options
+
+
+# Process options as early as possible so that --help and --version
+# can return quickly.
+func_options ${1+"$@"}
+eval set dummy "$func_options_result"; shift
+
+
+
+## ----------- ##
+##    Main.    ##
+## ----------- ##
+
+magic='%%%MAGIC variable%%%'
+magic_exe='%%%MAGIC EXE variable%%%'
+
+# Global variables.
+extracted_archives=
+extracted_serial=0
+
+# If this variable is set in any of the actions, the command in it
+# will be execed at the end.  This prevents here-documents from being
+# left over by shells.
+exec_cmd=
+
+
+# A function that is used when there is no print builtin or printf.
+func_fallback_echo ()
+{
+  eval 'cat <<_LTECHO_EOF
+$1
+_LTECHO_EOF'
+}
+
+# func_generated_by_libtool
+# True iff stdin has been generated by Libtool. This function is only
+# a basic sanity check; it will hardly flush out determined imposters.
+func_generated_by_libtool_p ()
+{
+  $GREP "^# Generated by .*$PACKAGE" > /dev/null 2>&1
+}
+
+# func_lalib_p file
+# True iff FILE is a libtool '.la' library or '.lo' object file.
+# This function is only a basic sanity check; it will hardly flush out
+# determined imposters.
+func_lalib_p ()
+{
+    test -f "$1" &&
+      $SED -e 4q "$1" 2>/dev/null | func_generated_by_libtool_p
+}
+
+# func_lalib_unsafe_p file
+# True iff FILE is a libtool '.la' library or '.lo' object file.
+# This function implements the same check as func_lalib_p without
+# resorting to external programs.  To this end, it redirects stdin and
+# closes it afterwards, without saving the original file descriptor.
+# As a safety measure, use it only where a negative result would be
+# fatal anyway.  Works if 'file' does not exist.
+func_lalib_unsafe_p ()
+{
+    lalib_p=no
+    if test -f "$1" && test -r "$1" && exec 5<&0 <"$1"; then
+	for lalib_p_l in 1 2 3 4
+	do
+	    read lalib_p_line
+	    case $lalib_p_line in
+		\#\ Generated\ by\ *$PACKAGE* ) lalib_p=yes; break;;
+	    esac
+	done
+	exec 0<&5 5<&-
+    fi
+    test yes = "$lalib_p"
+}
+
+# func_ltwrapper_script_p file
+# True iff FILE is a libtool wrapper script
+# This function is only a basic sanity check; it will hardly flush out
+# determined imposters.
+func_ltwrapper_script_p ()
+{
+    test -f "$1" &&
+      $lt_truncate_bin < "$1" 2>/dev/null | func_generated_by_libtool_p
+}
+
+# func_ltwrapper_executable_p file
+# True iff FILE is a libtool wrapper executable
+# This function is only a basic sanity check; it will hardly flush out
+# determined imposters.
+func_ltwrapper_executable_p ()
+{
+    func_ltwrapper_exec_suffix=
+    case $1 in
+    *.exe) ;;
+    *) func_ltwrapper_exec_suffix=.exe ;;
+    esac
+    $GREP "$magic_exe" "$1$func_ltwrapper_exec_suffix" >/dev/null 2>&1
+}
+
+# func_ltwrapper_scriptname file
+# Assumes file is an ltwrapper_executable
+# uses $file to determine the appropriate filename for a
+# temporary ltwrapper_script.
+func_ltwrapper_scriptname ()
+{
+    func_dirname_and_basename "$1" "" "."
+    func_stripname '' '.exe' "$func_basename_result"
+    func_ltwrapper_scriptname_result=$func_dirname_result/$objdir/${func_stripname_result}_ltshwrapper
+}
+
+# func_ltwrapper_p file
+# True iff FILE is a libtool wrapper script or wrapper executable
+# This function is only a basic sanity check; it will hardly flush out
+# determined imposters.
+func_ltwrapper_p ()
+{
+    func_ltwrapper_script_p "$1" || func_ltwrapper_executable_p "$1"
+}
+
+
+# func_execute_cmds commands fail_cmd
+# Execute tilde-delimited COMMANDS.
+# If FAIL_CMD is given, eval that upon failure.
+# FAIL_CMD may read-access the current command in variable CMD!
+func_execute_cmds ()
+{
+    $debug_cmd
+
+    save_ifs=$IFS; IFS='~'
+    for cmd in $1; do
+      IFS=$sp$nl
+      eval cmd=\"$cmd\"
+      IFS=$save_ifs
+      func_show_eval "$cmd" "${2-:}"
+    done
+    IFS=$save_ifs
+}
+
+
+# func_source file
+# Source FILE, adding directory component if necessary.
+# Note that it is not necessary on cygwin/mingw to append a dot to
+# FILE even if both FILE and FILE.exe exist: automatic-append-.exe
+# behavior happens only for exec(3), not for open(2)!  Also, sourcing
+# 'FILE.' does not work on cygwin managed mounts.
+func_source ()
+{
+    $debug_cmd
+
+    case $1 in
+    */* | *\\*)	. "$1" ;;
+    *)		. "./$1" ;;
+    esac
+}
+
+
+# func_resolve_sysroot PATH
+# Replace a leading = in PATH with a sysroot.  Store the result into
+# func_resolve_sysroot_result
+func_resolve_sysroot ()
+{
+  func_resolve_sysroot_result=$1
+  case $func_resolve_sysroot_result in
+  =*)
+    func_stripname '=' '' "$func_resolve_sysroot_result"
+    func_resolve_sysroot_result=$lt_sysroot$func_stripname_result
+    ;;
+  esac
+}
+
+# func_replace_sysroot PATH
+# If PATH begins with the sysroot, replace it with = and
+# store the result into func_replace_sysroot_result.
+func_replace_sysroot ()
+{
+  case $lt_sysroot:$1 in
+  ?*:"$lt_sysroot"*)
+    func_stripname "$lt_sysroot" '' "$1"
+    func_replace_sysroot_result='='$func_stripname_result
+    ;;
+  *)
+    # Including no sysroot.
+    func_replace_sysroot_result=$1
+    ;;
+  esac
+}
+
+# func_infer_tag arg
+# Infer tagged configuration to use if any are available and
+# if one wasn't chosen via the "--tag" command line option.
+# Only attempt this if the compiler in the base compile
+# command doesn't match the default compiler.
+# arg is usually of the form 'gcc ...'
+func_infer_tag ()
+{
+    $debug_cmd
+
+    if test -n "$available_tags" && test -z "$tagname"; then
+      CC_quoted=
+      for arg in $CC; do
+	func_append_quoted CC_quoted "$arg"
+      done
+      CC_expanded=`func_echo_all $CC`
+      CC_quoted_expanded=`func_echo_all $CC_quoted`
+      case $@ in
+      # Blanks in the command may have been stripped by the calling shell,
+      # but not from the CC environment variable when configure was run.
+      " $CC "* | "$CC "* | " $CC_expanded "* | "$CC_expanded "* | \
+      " $CC_quoted"* | "$CC_quoted "* | " $CC_quoted_expanded "* | "$CC_quoted_expanded "*) ;;
+      # Blanks at the start of $base_compile will cause this to fail
+      # if we don't check for them as well.
+      *)
+	for z in $available_tags; do
+	  if $GREP "^# ### BEGIN LIBTOOL TAG CONFIG: $z$" < "$progpath" > /dev/null; then
+	    # Evaluate the configuration.
+	    eval "`$SED -n -e '/^# ### BEGIN LIBTOOL TAG CONFIG: '$z'$/,/^# ### END LIBTOOL TAG CONFIG: '$z'$/p' < $progpath`"
+	    CC_quoted=
+	    for arg in $CC; do
+	      # Double-quote args containing other shell metacharacters.
+	      func_append_quoted CC_quoted "$arg"
+	    done
+	    CC_expanded=`func_echo_all $CC`
+	    CC_quoted_expanded=`func_echo_all $CC_quoted`
+	    case "$@ " in
+	    " $CC "* | "$CC "* | " $CC_expanded "* | "$CC_expanded "* | \
+	    " $CC_quoted"* | "$CC_quoted "* | " $CC_quoted_expanded "* | "$CC_quoted_expanded "*)
+	      # The compiler in the base compile command matches
+	      # the one in the tagged configuration.
+	      # Assume this is the tagged configuration we want.
+	      tagname=$z
+	      break
+	      ;;
+	    esac
+	  fi
+	done
+	# If $tagname still isn't set, then no tagged configuration
+	# was found and let the user know that the "--tag" command
+	# line option must be used.
+	if test -z "$tagname"; then
+	  func_echo "unable to infer tagged configuration"
+	  func_fatal_error "specify a tag with '--tag'"
+#	else
+#	  func_verbose "using $tagname tagged configuration"
+	fi
+	;;
+      esac
+    fi
+}
+
+
+
+# func_write_libtool_object output_name pic_name nonpic_name
+# Create a libtool object file (analogous to a ".la" file),
+# but don't create it if we're doing a dry run.
+func_write_libtool_object ()
+{
+    write_libobj=$1
+    if test yes = "$build_libtool_libs"; then
+      write_lobj=\'$2\'
+    else
+      write_lobj=none
+    fi
+
+    if test yes = "$build_old_libs"; then
+      write_oldobj=\'$3\'
+    else
+      write_oldobj=none
+    fi
+
+    $opt_dry_run || {
+      cat >${write_libobj}T <<EOF
+# $write_libobj - a libtool object file
+# Generated by $PROGRAM (GNU $PACKAGE) $VERSION
+#
+# Please DO NOT delete this file!
+# It is necessary for linking the library.
+
+# Name of the PIC object.
+pic_object=$write_lobj
+
+# Name of the non-PIC object
+non_pic_object=$write_oldobj
+
+EOF
+      $MV "${write_libobj}T" "$write_libobj"
+    }
+}
+
+
+##################################################
+# FILE NAME AND PATH CONVERSION HELPER FUNCTIONS #
+##################################################
+
+# func_convert_core_file_wine_to_w32 ARG
+# Helper function used by file name conversion functions when $build is *nix,
+# and $host is mingw, cygwin, or some other w32 environment. Relies on a
+# correctly configured wine environment available, with the winepath program
+# in $build's $PATH.
+#
+# ARG is the $build file name to be converted to w32 format.
+# Result is available in $func_convert_core_file_wine_to_w32_result, and will
+# be empty on error (or when ARG is empty)
+func_convert_core_file_wine_to_w32 ()
+{
+  $debug_cmd
+
+  func_convert_core_file_wine_to_w32_result=$1
+  if test -n "$1"; then
+    # Unfortunately, winepath does not exit with a non-zero error code, so we
+    # are forced to check the contents of stdout. On the other hand, if the
+    # command is not found, the shell will set an exit code of 127 and print
+    # *an error message* to stdout. So we must check for both error code of
+    # zero AND non-empty stdout, which explains the odd construction:
+    func_convert_core_file_wine_to_w32_tmp=`winepath -w "$1" 2>/dev/null`
+    if test "$?" -eq 0 && test -n "$func_convert_core_file_wine_to_w32_tmp"; then
+      func_convert_core_file_wine_to_w32_result=`$ECHO "$func_convert_core_file_wine_to_w32_tmp" |
+        $SED -e "$sed_naive_backslashify"`
+    else
+      func_convert_core_file_wine_to_w32_result=
+    fi
+  fi
+}
+# end: func_convert_core_file_wine_to_w32
+
+
+# func_convert_core_path_wine_to_w32 ARG
+# Helper function used by path conversion functions when $build is *nix, and
+# $host is mingw, cygwin, or some other w32 environment. Relies on a correctly
+# configured wine environment available, with the winepath program in $build's
+# $PATH. Assumes ARG has no leading or trailing path separator characters.
+#
+# ARG is path to be converted from $build format to win32.
+# Result is available in $func_convert_core_path_wine_to_w32_result.
+# Unconvertible file (directory) names in ARG are skipped; if no directory names
+# are convertible, then the result may be empty.
+func_convert_core_path_wine_to_w32 ()
+{
+  $debug_cmd
+
+  # unfortunately, winepath doesn't convert paths, only file names
+  func_convert_core_path_wine_to_w32_result=
+  if test -n "$1"; then
+    oldIFS=$IFS
+    IFS=:
+    for func_convert_core_path_wine_to_w32_f in $1; do
+      IFS=$oldIFS
+      func_convert_core_file_wine_to_w32 "$func_convert_core_path_wine_to_w32_f"
+      if test -n "$func_convert_core_file_wine_to_w32_result"; then
+        if test -z "$func_convert_core_path_wine_to_w32_result"; then
+          func_convert_core_path_wine_to_w32_result=$func_convert_core_file_wine_to_w32_result
+        else
+          func_append func_convert_core_path_wine_to_w32_result ";$func_convert_core_file_wine_to_w32_result"
+        fi
+      fi
+    done
+    IFS=$oldIFS
+  fi
+}
+# end: func_convert_core_path_wine_to_w32
+
+
+# func_cygpath ARGS...
+# Wrapper around calling the cygpath program via LT_CYGPATH. This is used when
+# when (1) $build is *nix and Cygwin is hosted via a wine environment; or (2)
+# $build is MSYS and $host is Cygwin, or (3) $build is Cygwin. In case (1) or
+# (2), returns the Cygwin file name or path in func_cygpath_result (input
+# file name or path is assumed to be in w32 format, as previously converted
+# from $build's *nix or MSYS format). In case (3), returns the w32 file name
+# or path in func_cygpath_result (input file name or path is assumed to be in
+# Cygwin format). Returns an empty string on error.
+#
+# ARGS are passed to cygpath, with the last one being the file name or path to
+# be converted.
+#
+# Specify the absolute *nix (or w32) name to cygpath in the LT_CYGPATH
+# environment variable; do not put it in $PATH.
+func_cygpath ()
+{
+  $debug_cmd
+
+  if test -n "$LT_CYGPATH" && test -f "$LT_CYGPATH"; then
+    func_cygpath_result=`$LT_CYGPATH "$@" 2>/dev/null`
+    if test "$?" -ne 0; then
+      # on failure, ensure result is empty
+      func_cygpath_result=
+    fi
+  else
+    func_cygpath_result=
+    func_error "LT_CYGPATH is empty or specifies non-existent file: '$LT_CYGPATH'"
+  fi
+}
+#end: func_cygpath
+
+
+# func_convert_core_msys_to_w32 ARG
+# Convert file name or path ARG from MSYS format to w32 format.  Return
+# result in func_convert_core_msys_to_w32_result.
+func_convert_core_msys_to_w32 ()
+{
+  $debug_cmd
+
+  # awkward: cmd appends spaces to result
+  func_convert_core_msys_to_w32_result=`( cmd //c echo "$1" ) 2>/dev/null |
+    $SED -e 's/[ ]*$//' -e "$sed_naive_backslashify"`
+}
+#end: func_convert_core_msys_to_w32
+
+
+# func_convert_file_check ARG1 ARG2
+# Verify that ARG1 (a file name in $build format) was converted to $host
+# format in ARG2. Otherwise, emit an error message, but continue (resetting
+# func_to_host_file_result to ARG1).
+func_convert_file_check ()
+{
+  $debug_cmd
+
+  if test -z "$2" && test -n "$1"; then
+    func_error "Could not determine host file name corresponding to"
+    func_error "  '$1'"
+    func_error "Continuing, but uninstalled executables may not work."
+    # Fallback:
+    func_to_host_file_result=$1
+  fi
+}
+# end func_convert_file_check
+
+
+# func_convert_path_check FROM_PATHSEP TO_PATHSEP FROM_PATH TO_PATH
+# Verify that FROM_PATH (a path in $build format) was converted to $host
+# format in TO_PATH. Otherwise, emit an error message, but continue, resetting
+# func_to_host_file_result to a simplistic fallback value (see below).
+func_convert_path_check ()
+{
+  $debug_cmd
+
+  if test -z "$4" && test -n "$3"; then
+    func_error "Could not determine the host path corresponding to"
+    func_error "  '$3'"
+    func_error "Continuing, but uninstalled executables may not work."
+    # Fallback.  This is a deliberately simplistic "conversion" and
+    # should not be "improved".  See libtool.info.
+    if test "x$1" != "x$2"; then
+      lt_replace_pathsep_chars="s|$1|$2|g"
+      func_to_host_path_result=`echo "$3" |
+        $SED -e "$lt_replace_pathsep_chars"`
+    else
+      func_to_host_path_result=$3
+    fi
+  fi
+}
+# end func_convert_path_check
+
+
+# func_convert_path_front_back_pathsep FRONTPAT BACKPAT REPL ORIG
+# Modifies func_to_host_path_result by prepending REPL if ORIG matches FRONTPAT
+# and appending REPL if ORIG matches BACKPAT.
+func_convert_path_front_back_pathsep ()
+{
+  $debug_cmd
+
+  case $4 in
+  $1 ) func_to_host_path_result=$3$func_to_host_path_result
+    ;;
+  esac
+  case $4 in
+  $2 ) func_append func_to_host_path_result "$3"
+    ;;
+  esac
+}
+# end func_convert_path_front_back_pathsep
+
+
+##################################################
+# $build to $host FILE NAME CONVERSION FUNCTIONS #
+##################################################
+# invoked via '$to_host_file_cmd ARG'
+#
+# In each case, ARG is the path to be converted from $build to $host format.
+# Result will be available in $func_to_host_file_result.
+
+
+# func_to_host_file ARG
+# Converts the file name ARG from $build format to $host format. Return result
+# in func_to_host_file_result.
+func_to_host_file ()
+{
+  $debug_cmd
+
+  $to_host_file_cmd "$1"
+}
+# end func_to_host_file
+
+
+# func_to_tool_file ARG LAZY
+# converts the file name ARG from $build format to toolchain format. Return
+# result in func_to_tool_file_result.  If the conversion in use is listed
+# in (the comma separated) LAZY, no conversion takes place.
+func_to_tool_file ()
+{
+  $debug_cmd
+
+  case ,$2, in
+    *,"$to_tool_file_cmd",*)
+      func_to_tool_file_result=$1
+      ;;
+    *)
+      $to_tool_file_cmd "$1"
+      func_to_tool_file_result=$func_to_host_file_result
+      ;;
+  esac
+}
+# end func_to_tool_file
+
+
+# func_convert_file_noop ARG
+# Copy ARG to func_to_host_file_result.
+func_convert_file_noop ()
+{
+  func_to_host_file_result=$1
+}
+# end func_convert_file_noop
+
+
+# func_convert_file_msys_to_w32 ARG
+# Convert file name ARG from (mingw) MSYS to (mingw) w32 format; automatic
+# conversion to w32 is not available inside the cwrapper.  Returns result in
+# func_to_host_file_result.
+func_convert_file_msys_to_w32 ()
+{
+  $debug_cmd
+
+  func_to_host_file_result=$1
+  if test -n "$1"; then
+    func_convert_core_msys_to_w32 "$1"
+    func_to_host_file_result=$func_convert_core_msys_to_w32_result
+  fi
+  func_convert_file_check "$1" "$func_to_host_file_result"
+}
+# end func_convert_file_msys_to_w32
+
+
+# func_convert_file_cygwin_to_w32 ARG
+# Convert file name ARG from Cygwin to w32 format.  Returns result in
+# func_to_host_file_result.
+func_convert_file_cygwin_to_w32 ()
+{
+  $debug_cmd
+
+  func_to_host_file_result=$1
+  if test -n "$1"; then
+    # because $build is cygwin, we call "the" cygpath in $PATH; no need to use
+    # LT_CYGPATH in this case.
+    func_to_host_file_result=`cygpath -m "$1"`
+  fi
+  func_convert_file_check "$1" "$func_to_host_file_result"
+}
+# end func_convert_file_cygwin_to_w32
+
+
+# func_convert_file_nix_to_w32 ARG
+# Convert file name ARG from *nix to w32 format.  Requires a wine environment
+# and a working winepath. Returns result in func_to_host_file_result.
+func_convert_file_nix_to_w32 ()
+{
+  $debug_cmd
+
+  func_to_host_file_result=$1
+  if test -n "$1"; then
+    func_convert_core_file_wine_to_w32 "$1"
+    func_to_host_file_result=$func_convert_core_file_wine_to_w32_result
+  fi
+  func_convert_file_check "$1" "$func_to_host_file_result"
+}
+# end func_convert_file_nix_to_w32
+
+
+# func_convert_file_msys_to_cygwin ARG
+# Convert file name ARG from MSYS to Cygwin format.  Requires LT_CYGPATH set.
+# Returns result in func_to_host_file_result.
+func_convert_file_msys_to_cygwin ()
+{
+  $debug_cmd
+
+  func_to_host_file_result=$1
+  if test -n "$1"; then
+    func_convert_core_msys_to_w32 "$1"
+    func_cygpath -u "$func_convert_core_msys_to_w32_result"
+    func_to_host_file_result=$func_cygpath_result
+  fi
+  func_convert_file_check "$1" "$func_to_host_file_result"
+}
+# end func_convert_file_msys_to_cygwin
+
+
+# func_convert_file_nix_to_cygwin ARG
+# Convert file name ARG from *nix to Cygwin format.  Requires Cygwin installed
+# in a wine environment, working winepath, and LT_CYGPATH set.  Returns result
+# in func_to_host_file_result.
+func_convert_file_nix_to_cygwin ()
+{
+  $debug_cmd
+
+  func_to_host_file_result=$1
+  if test -n "$1"; then
+    # convert from *nix to w32, then use cygpath to convert from w32 to cygwin.
+    func_convert_core_file_wine_to_w32 "$1"
+    func_cygpath -u "$func_convert_core_file_wine_to_w32_result"
+    func_to_host_file_result=$func_cygpath_result
+  fi
+  func_convert_file_check "$1" "$func_to_host_file_result"
+}
+# end func_convert_file_nix_to_cygwin
+
+
+#############################################
+# $build to $host PATH CONVERSION FUNCTIONS #
+#############################################
+# invoked via '$to_host_path_cmd ARG'
+#
+# In each case, ARG is the path to be converted from $build to $host format.
+# The result will be available in $func_to_host_path_result.
+#
+# Path separators are also converted from $build format to $host format.  If
+# ARG begins or ends with a path separator character, it is preserved (but
+# converted to $host format) on output.
+#
+# All path conversion functions are named using the following convention:
+#   file name conversion function    : func_convert_file_X_to_Y ()
+#   path conversion function         : func_convert_path_X_to_Y ()
+# where, for any given $build/$host combination the 'X_to_Y' value is the
+# same.  If conversion functions are added for new $build/$host combinations,
+# the two new functions must follow this pattern, or func_init_to_host_path_cmd
+# will break.
+
+
+# func_init_to_host_path_cmd
+# Ensures that function "pointer" variable $to_host_path_cmd is set to the
+# appropriate value, based on the value of $to_host_file_cmd.
+to_host_path_cmd=
+func_init_to_host_path_cmd ()
+{
+  $debug_cmd
+
+  if test -z "$to_host_path_cmd"; then
+    func_stripname 'func_convert_file_' '' "$to_host_file_cmd"
+    to_host_path_cmd=func_convert_path_$func_stripname_result
+  fi
+}
+
+
+# func_to_host_path ARG
+# Converts the path ARG from $build format to $host format. Return result
+# in func_to_host_path_result.
+func_to_host_path ()
+{
+  $debug_cmd
+
+  func_init_to_host_path_cmd
+  $to_host_path_cmd "$1"
+}
+# end func_to_host_path
+
+
+# func_convert_path_noop ARG
+# Copy ARG to func_to_host_path_result.
+func_convert_path_noop ()
+{
+  func_to_host_path_result=$1
+}
+# end func_convert_path_noop
+
+
+# func_convert_path_msys_to_w32 ARG
+# Convert path ARG from (mingw) MSYS to (mingw) w32 format; automatic
+# conversion to w32 is not available inside the cwrapper.  Returns result in
+# func_to_host_path_result.
+func_convert_path_msys_to_w32 ()
+{
+  $debug_cmd
+
+  func_to_host_path_result=$1
+  if test -n "$1"; then
+    # Remove leading and trailing path separator characters from ARG.  MSYS
+    # behavior is inconsistent here; cygpath turns them into '.;' and ';.';
+    # and winepath ignores them completely.
+    func_stripname : : "$1"
+    func_to_host_path_tmp1=$func_stripname_result
+    func_convert_core_msys_to_w32 "$func_to_host_path_tmp1"
+    func_to_host_path_result=$func_convert_core_msys_to_w32_result
+    func_convert_path_check : ";" \
+      "$func_to_host_path_tmp1" "$func_to_host_path_result"
+    func_convert_path_front_back_pathsep ":*" "*:" ";" "$1"
+  fi
+}
+# end func_convert_path_msys_to_w32
+
+
+# func_convert_path_cygwin_to_w32 ARG
+# Convert path ARG from Cygwin to w32 format.  Returns result in
+# func_to_host_file_result.
+func_convert_path_cygwin_to_w32 ()
+{
+  $debug_cmd
+
+  func_to_host_path_result=$1
+  if test -n "$1"; then
+    # See func_convert_path_msys_to_w32:
+    func_stripname : : "$1"
+    func_to_host_path_tmp1=$func_stripname_result
+    func_to_host_path_result=`cygpath -m -p "$func_to_host_path_tmp1"`
+    func_convert_path_check : ";" \
+      "$func_to_host_path_tmp1" "$func_to_host_path_result"
+    func_convert_path_front_back_pathsep ":*" "*:" ";" "$1"
+  fi
+}
+# end func_convert_path_cygwin_to_w32
+
+
+# func_convert_path_nix_to_w32 ARG
+# Convert path ARG from *nix to w32 format.  Requires a wine environment and
+# a working winepath.  Returns result in func_to_host_file_result.
+func_convert_path_nix_to_w32 ()
+{
+  $debug_cmd
+
+  func_to_host_path_result=$1
+  if test -n "$1"; then
+    # See func_convert_path_msys_to_w32:
+    func_stripname : : "$1"
+    func_to_host_path_tmp1=$func_stripname_result
+    func_convert_core_path_wine_to_w32 "$func_to_host_path_tmp1"
+    func_to_host_path_result=$func_convert_core_path_wine_to_w32_result
+    func_convert_path_check : ";" \
+      "$func_to_host_path_tmp1" "$func_to_host_path_result"
+    func_convert_path_front_back_pathsep ":*" "*:" ";" "$1"
+  fi
+}
+# end func_convert_path_nix_to_w32
+
+
+# func_convert_path_msys_to_cygwin ARG
+# Convert path ARG from MSYS to Cygwin format.  Requires LT_CYGPATH set.
+# Returns result in func_to_host_file_result.
+func_convert_path_msys_to_cygwin ()
+{
+  $debug_cmd
+
+  func_to_host_path_result=$1
+  if test -n "$1"; then
+    # See func_convert_path_msys_to_w32:
+    func_stripname : : "$1"
+    func_to_host_path_tmp1=$func_stripname_result
+    func_convert_core_msys_to_w32 "$func_to_host_path_tmp1"
+    func_cygpath -u -p "$func_convert_core_msys_to_w32_result"
+    func_to_host_path_result=$func_cygpath_result
+    func_convert_path_check : : \
+      "$func_to_host_path_tmp1" "$func_to_host_path_result"
+    func_convert_path_front_back_pathsep ":*" "*:" : "$1"
+  fi
+}
+# end func_convert_path_msys_to_cygwin
+
+
+# func_convert_path_nix_to_cygwin ARG
+# Convert path ARG from *nix to Cygwin format.  Requires Cygwin installed in a
+# a wine environment, working winepath, and LT_CYGPATH set.  Returns result in
+# func_to_host_file_result.
+func_convert_path_nix_to_cygwin ()
+{
+  $debug_cmd
+
+  func_to_host_path_result=$1
+  if test -n "$1"; then
+    # Remove leading and trailing path separator characters from
+    # ARG. msys behavior is inconsistent here, cygpath turns them
+    # into '.;' and ';.', and winepath ignores them completely.
+    func_stripname : : "$1"
+    func_to_host_path_tmp1=$func_stripname_result
+    func_convert_core_path_wine_to_w32 "$func_to_host_path_tmp1"
+    func_cygpath -u -p "$func_convert_core_path_wine_to_w32_result"
+    func_to_host_path_result=$func_cygpath_result
+    func_convert_path_check : : \
+      "$func_to_host_path_tmp1" "$func_to_host_path_result"
+    func_convert_path_front_back_pathsep ":*" "*:" : "$1"
+  fi
+}
+# end func_convert_path_nix_to_cygwin
+
+
+# func_dll_def_p FILE
+# True iff FILE is a Windows DLL '.def' file.
+# Keep in sync with _LT_DLL_DEF_P in libtool.m4
+func_dll_def_p ()
+{
+  $debug_cmd
+
+  func_dll_def_p_tmp=`$SED -n \
+    -e 's/^[	 ]*//' \
+    -e '/^\(;.*\)*$/d' \
+    -e 's/^\(EXPORTS\|LIBRARY\)\([	 ].*\)*$/DEF/p' \
+    -e q \
+    "$1"`
+  test DEF = "$func_dll_def_p_tmp"
+}
+
+
+# func_mode_compile arg...
+func_mode_compile ()
+{
+    $debug_cmd
+
+    # Get the compilation command and the source file.
+    base_compile=
+    srcfile=$nonopt  #  always keep a non-empty value in "srcfile"
+    suppress_opt=yes
+    suppress_output=
+    arg_mode=normal
+    libobj=
+    later=
+    pie_flag=
+
+    for arg
+    do
+      case $arg_mode in
+      arg  )
+	# do not "continue".  Instead, add this to base_compile
+	lastarg=$arg
+	arg_mode=normal
+	;;
+
+      target )
+	libobj=$arg
+	arg_mode=normal
+	continue
+	;;
+
+      normal )
+	# Accept any command-line options.
+	case $arg in
+	-o)
+	  test -n "$libobj" && \
+	    func_fatal_error "you cannot specify '-o' more than once"
+	  arg_mode=target
+	  continue
+	  ;;
+
+	-pie | -fpie | -fPIE)
+          func_append pie_flag " $arg"
+	  continue
+	  ;;
+
+	-shared | -static | -prefer-pic | -prefer-non-pic)
+	  func_append later " $arg"
+	  continue
+	  ;;
+
+	-no-suppress)
+	  suppress_opt=no
+	  continue
+	  ;;
+
+	-Xcompiler)
+	  arg_mode=arg  #  the next one goes into the "base_compile" arg list
+	  continue      #  The current "srcfile" will either be retained or
+	  ;;            #  replaced later.  I would guess that would be a bug.
+
+	-Wc,*)
+	  func_stripname '-Wc,' '' "$arg"
+	  args=$func_stripname_result
+	  lastarg=
+	  save_ifs=$IFS; IFS=,
+	  for arg in $args; do
+	    IFS=$save_ifs
+	    func_append_quoted lastarg "$arg"
+	  done
+	  IFS=$save_ifs
+	  func_stripname ' ' '' "$lastarg"
+	  lastarg=$func_stripname_result
+
+	  # Add the arguments to base_compile.
+	  func_append base_compile " $lastarg"
+	  continue
+	  ;;
+
+	*)
+	  # Accept the current argument as the source file.
+	  # The previous "srcfile" becomes the current argument.
+	  #
+	  lastarg=$srcfile
+	  srcfile=$arg
+	  ;;
+	esac  #  case $arg
+	;;
+      esac    #  case $arg_mode
+
+      # Aesthetically quote the previous argument.
+      func_append_quoted base_compile "$lastarg"
+    done # for arg
+
+    case $arg_mode in
+    arg)
+      func_fatal_error "you must specify an argument for -Xcompile"
+      ;;
+    target)
+      func_fatal_error "you must specify a target with '-o'"
+      ;;
+    *)
+      # Get the name of the library object.
+      test -z "$libobj" && {
+	func_basename "$srcfile"
+	libobj=$func_basename_result
+      }
+      ;;
+    esac
+
+    # Recognize several different file suffixes.
+    # If the user specifies -o file.o, it is replaced with file.lo
+    case $libobj in
+    *.[cCFSifmso] | \
+    *.ada | *.adb | *.ads | *.asm | \
+    *.c++ | *.cc | *.ii | *.class | *.cpp | *.cxx | \
+    *.[fF][09]? | *.for | *.java | *.go | *.obj | *.sx | *.cu | *.cup)
+      func_xform "$libobj"
+      libobj=$func_xform_result
+      ;;
+    esac
+
+    case $libobj in
+    *.lo) func_lo2o "$libobj"; obj=$func_lo2o_result ;;
+    *)
+      func_fatal_error "cannot determine name of library object from '$libobj'"
+      ;;
+    esac
+
+    func_infer_tag $base_compile
+
+    for arg in $later; do
+      case $arg in
+      -shared)
+	test yes = "$build_libtool_libs" \
+	  || func_fatal_configuration "cannot build a shared library"
+	build_old_libs=no
+	continue
+	;;
+
+      -static)
+	build_libtool_libs=no
+	build_old_libs=yes
+	continue
+	;;
+
+      -prefer-pic)
+	pic_mode=yes
+	continue
+	;;
+
+      -prefer-non-pic)
+	pic_mode=no
+	continue
+	;;
+      esac
+    done
+
+    func_quote_arg pretty "$libobj"
+    test "X$libobj" != "X$func_quote_arg_result" \
+      && $ECHO "X$libobj" | $GREP '[]~#^*{};<>?"'"'"'	 &()|`$[]' \
+      && func_warning "libobj name '$libobj' may not contain shell special characters."
+    func_dirname_and_basename "$obj" "/" ""
+    objname=$func_basename_result
+    xdir=$func_dirname_result
+    lobj=$xdir$objdir/$objname
+
+    test -z "$base_compile" && \
+      func_fatal_help "you must specify a compilation command"
+
+    # Delete any leftover library objects.
+    if test yes = "$build_old_libs"; then
+      removelist="$obj $lobj $libobj ${libobj}T"
+    else
+      removelist="$lobj $libobj ${libobj}T"
+    fi
+
+    # On Cygwin there's no "real" PIC flag so we must build both object types
+    case $host_os in
+    cygwin* | mingw* | pw32* | os2* | cegcc*)
+      pic_mode=default
+      ;;
+    esac
+    if test no = "$pic_mode" && test pass_all != "$deplibs_check_method"; then
+      # non-PIC code in shared libraries is not supported
+      pic_mode=default
+    fi
+
+    # Calculate the filename of the output object if compiler does
+    # not support -o with -c
+    if test no = "$compiler_c_o"; then
+      output_obj=`$ECHO "$srcfile" | $SED 's%^.*/%%; s%\.[^.]*$%%'`.$objext
+      lockfile=$output_obj.lock
+    else
+      output_obj=
+      need_locks=no
+      lockfile=
+    fi
+
+    # Lock this critical section if it is needed
+    # We use this script file to make the link, it avoids creating a new file
+    if test yes = "$need_locks"; then
+      until $opt_dry_run || ln "$progpath" "$lockfile" 2>/dev/null; do
+	func_echo "Waiting for $lockfile to be removed"
+	sleep 2
+      done
+    elif test warn = "$need_locks"; then
+      if test -f "$lockfile"; then
+	$ECHO "\
+*** ERROR, $lockfile exists and contains:
+`cat $lockfile 2>/dev/null`
+
+This indicates that another process is trying to use the same
+temporary object file, and libtool could not work around it because
+your compiler does not support '-c' and '-o' together.  If you
+repeat this compilation, it may succeed, by chance, but you had better
+avoid parallel builds (make -j) in this platform, or get a better
+compiler."
+
+	$opt_dry_run || $RM $removelist
+	exit $EXIT_FAILURE
+      fi
+      func_append removelist " $output_obj"
+      $ECHO "$srcfile" > "$lockfile"
+    fi
+
+    $opt_dry_run || $RM $removelist
+    func_append removelist " $lockfile"
+    trap '$opt_dry_run || $RM $removelist; exit $EXIT_FAILURE' 1 2 15
+
+    func_to_tool_file "$srcfile" func_convert_file_msys_to_w32
+    srcfile=$func_to_tool_file_result
+    func_quote_arg pretty "$srcfile"
+    qsrcfile=$func_quote_arg_result
+
+    # Only build a PIC object if we are building libtool libraries.
+    if test yes = "$build_libtool_libs"; then
+      # Without this assignment, base_compile gets emptied.
+      fbsd_hideous_sh_bug=$base_compile
+
+      if test no != "$pic_mode"; then
+	command="$base_compile $qsrcfile $pic_flag"
+      else
+	# Don't build PIC code
+	command="$base_compile $qsrcfile"
+      fi
+
+      func_mkdir_p "$xdir$objdir"
+
+      if test -z "$output_obj"; then
+	# Place PIC objects in $objdir
+	func_append command " -o $lobj"
+      fi
+
+      func_show_eval_locale "$command"	\
+          'test -n "$output_obj" && $RM $removelist; exit $EXIT_FAILURE'
+
+      if test warn = "$need_locks" &&
+	 test "X`cat $lockfile 2>/dev/null`" != "X$srcfile"; then
+	$ECHO "\
+*** ERROR, $lockfile contains:
+`cat $lockfile 2>/dev/null`
+
+but it should contain:
+$srcfile
+
+This indicates that another process is trying to use the same
+temporary object file, and libtool could not work around it because
+your compiler does not support '-c' and '-o' together.  If you
+repeat this compilation, it may succeed, by chance, but you had better
+avoid parallel builds (make -j) in this platform, or get a better
+compiler."
+
+	$opt_dry_run || $RM $removelist
+	exit $EXIT_FAILURE
+      fi
+
+      # Just move the object if needed, then go on to compile the next one
+      if test -n "$output_obj" && test "X$output_obj" != "X$lobj"; then
+	func_show_eval '$MV "$output_obj" "$lobj"' \
+	  'error=$?; $opt_dry_run || $RM $removelist; exit $error'
+      fi
+
+      # Allow error messages only from the first compilation.
+      if test yes = "$suppress_opt"; then
+	suppress_output=' >/dev/null 2>&1'
+      fi
+    fi
+
+    # Only build a position-dependent object if we build old libraries.
+    if test yes = "$build_old_libs"; then
+      if test yes != "$pic_mode"; then
+	# Don't build PIC code
+	command="$base_compile $qsrcfile$pie_flag"
+      else
+	command="$base_compile $qsrcfile $pic_flag"
+      fi
+      if test yes = "$compiler_c_o"; then
+	func_append command " -o $obj"
+      fi
+
+      # Suppress compiler output if we already did a PIC compilation.
+      func_append command "$suppress_output"
+      func_show_eval_locale "$command" \
+        '$opt_dry_run || $RM $removelist; exit $EXIT_FAILURE'
+
+      if test warn = "$need_locks" &&
+	 test "X`cat $lockfile 2>/dev/null`" != "X$srcfile"; then
+	$ECHO "\
+*** ERROR, $lockfile contains:
+`cat $lockfile 2>/dev/null`
+
+but it should contain:
+$srcfile
+
+This indicates that another process is trying to use the same
+temporary object file, and libtool could not work around it because
+your compiler does not support '-c' and '-o' together.  If you
+repeat this compilation, it may succeed, by chance, but you had better
+avoid parallel builds (make -j) in this platform, or get a better
+compiler."
+
+	$opt_dry_run || $RM $removelist
+	exit $EXIT_FAILURE
+      fi
+
+      # Just move the object if needed
+      if test -n "$output_obj" && test "X$output_obj" != "X$obj"; then
+	func_show_eval '$MV "$output_obj" "$obj"' \
+	  'error=$?; $opt_dry_run || $RM $removelist; exit $error'
+      fi
+    fi
+
+    $opt_dry_run || {
+      func_write_libtool_object "$libobj" "$objdir/$objname" "$objname"
+
+      # Unlock the critical section if it was locked
+      if test no != "$need_locks"; then
+	removelist=$lockfile
+        $RM "$lockfile"
+      fi
+    }
+
+    exit $EXIT_SUCCESS
+}
+
+$opt_help || {
+  test compile = "$opt_mode" && func_mode_compile ${1+"$@"}
+}
+
+func_mode_help ()
+{
+    # We need to display help for each of the modes.
+    case $opt_mode in
+      "")
+        # Generic help is extracted from the usage comments
+        # at the start of this file.
+        func_help
+        ;;
+
+      clean)
+        $ECHO \
+"Usage: $progname [OPTION]... --mode=clean RM [RM-OPTION]... FILE...
+
+Remove files from the build directory.
+
+RM is the name of the program to use to delete files associated with each FILE
+(typically '/bin/rm').  RM-OPTIONS are options (such as '-f') to be passed
+to RM.
+
+If FILE is a libtool library, object or program, all the files associated
+with it are deleted. Otherwise, only FILE itself is deleted using RM."
+        ;;
+
+      compile)
+      $ECHO \
+"Usage: $progname [OPTION]... --mode=compile COMPILE-COMMAND... SOURCEFILE
+
+Compile a source file into a libtool library object.
+
+This mode accepts the following additional options:
+
+  -o OUTPUT-FILE    set the output file name to OUTPUT-FILE
+  -no-suppress      do not suppress compiler output for multiple passes
+  -prefer-pic       try to build PIC objects only
+  -prefer-non-pic   try to build non-PIC objects only
+  -shared           do not build a '.o' file suitable for static linking
+  -static           only build a '.o' file suitable for static linking
+  -Wc,FLAG
+  -Xcompiler FLAG   pass FLAG directly to the compiler
+
+COMPILE-COMMAND is a command to be used in creating a 'standard' object file
+from the given SOURCEFILE.
+
+The output file name is determined by removing the directory component from
+SOURCEFILE, then substituting the C source code suffix '.c' with the
+library object suffix, '.lo'."
+        ;;
+
+      execute)
+        $ECHO \
+"Usage: $progname [OPTION]... --mode=execute COMMAND [ARGS]...
+
+Automatically set library path, then run a program.
+
+This mode accepts the following additional options:
+
+  -dlopen FILE      add the directory containing FILE to the library path
+
+This mode sets the library path environment variable according to '-dlopen'
+flags.
+
+If any of the ARGS are libtool executable wrappers, then they are translated
+into their corresponding uninstalled binary, and any of their required library
+directories are added to the library path.
+
+Then, COMMAND is executed, with ARGS as arguments."
+        ;;
+
+      finish)
+        $ECHO \
+"Usage: $progname [OPTION]... --mode=finish [LIBDIR]...
+
+Complete the installation of libtool libraries.
+
+Each LIBDIR is a directory that contains libtool libraries.
+
+The commands that this mode executes may require superuser privileges.  Use
+the '--dry-run' option if you just want to see what would be executed."
+        ;;
+
+      install)
+        $ECHO \
+"Usage: $progname [OPTION]... --mode=install INSTALL-COMMAND...
+
+Install executables or libraries.
+
+INSTALL-COMMAND is the installation command.  The first component should be
+either the 'install' or 'cp' program.
+
+The following components of INSTALL-COMMAND are treated specially:
+
+  -inst-prefix-dir PREFIX-DIR  Use PREFIX-DIR as a staging area for installation
+
+The rest of the components are interpreted as arguments to that command (only
+BSD-compatible install options are recognized)."
+        ;;
+
+      link)
+        $ECHO \
+"Usage: $progname [OPTION]... --mode=link LINK-COMMAND...
+
+Link object files or libraries together to form another library, or to
+create an executable program.
+
+LINK-COMMAND is a command using the C compiler that you would use to create
+a program from several object files.
+
+The following components of LINK-COMMAND are treated specially:
+
+  -all-static       do not do any dynamic linking at all
+  -avoid-version    do not add a version suffix if possible
+  -bindir BINDIR    specify path to binaries directory (for systems where
+                    libraries must be found in the PATH setting at runtime)
+  -dlopen FILE      '-dlpreopen' FILE if it cannot be dlopened at runtime
+  -dlpreopen FILE   link in FILE and add its symbols to lt_preloaded_symbols
+  -export-dynamic   allow symbols from OUTPUT-FILE to be resolved with dlsym(3)
+  -export-symbols SYMFILE
+                    try to export only the symbols listed in SYMFILE
+  -export-symbols-regex REGEX
+                    try to export only the symbols matching REGEX
+  -LLIBDIR          search LIBDIR for required installed libraries
+  -lNAME            OUTPUT-FILE requires the installed library libNAME
+  -module           build a library that can dlopened
+  -no-fast-install  disable the fast-install mode
+  -no-install       link a not-installable executable
+  -no-undefined     declare that a library does not refer to external symbols
+  -o OUTPUT-FILE    create OUTPUT-FILE from the specified objects
+  -objectlist FILE  use a list of object files found in FILE to specify objects
+  -os2dllname NAME  force a short DLL name on OS/2 (no effect on other OSes)
+  -precious-files-regex REGEX
+                    don't remove output files matching REGEX
+  -release RELEASE  specify package release information
+  -rpath LIBDIR     the created library will eventually be installed in LIBDIR
+  -R[ ]LIBDIR       add LIBDIR to the runtime path of programs and libraries
+  -shared           only do dynamic linking of libtool libraries
+  -shrext SUFFIX    override the standard shared library file extension
+  -static           do not do any dynamic linking of uninstalled libtool libraries
+  -static-libtool-libs
+                    do not do any dynamic linking of libtool libraries
+  -version-info CURRENT[:REVISION[:AGE]]
+                    specify library version info [each variable defaults to 0]
+  -weak LIBNAME     declare that the target provides the LIBNAME interface
+  -Wc,FLAG
+  -Xcompiler FLAG   pass linker-specific FLAG directly to the compiler
+  -Wa,FLAG
+  -Xassembler FLAG  pass linker-specific FLAG directly to the assembler
+  -Wl,FLAG
+  -Xlinker FLAG     pass linker-specific FLAG directly to the linker
+  -XCClinker FLAG   pass link-specific FLAG to the compiler driver (CC)
+
+All other options (arguments beginning with '-') are ignored.
+
+Every other argument is treated as a filename.  Files ending in '.la' are
+treated as uninstalled libtool libraries, other files are standard or library
+object files.
+
+If the OUTPUT-FILE ends in '.la', then a libtool library is created,
+only library objects ('.lo' files) may be specified, and '-rpath' is
+required, except when creating a convenience library.
+
+If OUTPUT-FILE ends in '.a' or '.lib', then a standard library is created
+using 'ar' and 'ranlib', or on Windows using 'lib'.
+
+If OUTPUT-FILE ends in '.lo' or '.$objext', then a reloadable object file
+is created, otherwise an executable program is created."
+        ;;
+
+      uninstall)
+        $ECHO \
+"Usage: $progname [OPTION]... --mode=uninstall RM [RM-OPTION]... FILE...
+
+Remove libraries from an installation directory.
+
+RM is the name of the program to use to delete files associated with each FILE
+(typically '/bin/rm').  RM-OPTIONS are options (such as '-f') to be passed
+to RM.
+
+If FILE is a libtool library, all the files associated with it are deleted.
+Otherwise, only FILE itself is deleted using RM."
+        ;;
+
+      *)
+        func_fatal_help "invalid operation mode '$opt_mode'"
+        ;;
+    esac
+
+    echo
+    $ECHO "Try '$progname --help' for more information about other modes."
+}
+
+# Now that we've collected a possible --mode arg, show help if necessary
+if $opt_help; then
+  if test : = "$opt_help"; then
+    func_mode_help
+  else
+    {
+      func_help noexit
+      for opt_mode in compile link execute install finish uninstall clean; do
+	func_mode_help
+      done
+    } | $SED -n '1p; 2,$s/^Usage:/  or: /p'
+    {
+      func_help noexit
+      for opt_mode in compile link execute install finish uninstall clean; do
+	echo
+	func_mode_help
+      done
+    } |
+    $SED '1d
+      /^When reporting/,/^Report/{
+	H
+	d
+      }
+      $x
+      /information about other modes/d
+      /more detailed .*MODE/d
+      s/^Usage:.*--mode=\([^ ]*\) .*/Description of \1 mode:/'
+  fi
+  exit $?
+fi
+
+
+# func_mode_execute arg...
+func_mode_execute ()
+{
+    $debug_cmd
+
+    # The first argument is the command name.
+    cmd=$nonopt
+    test -z "$cmd" && \
+      func_fatal_help "you must specify a COMMAND"
+
+    # Handle -dlopen flags immediately.
+    for file in $opt_dlopen; do
+      test -f "$file" \
+	|| func_fatal_help "'$file' is not a file"
+
+      dir=
+      case $file in
+      *.la)
+	func_resolve_sysroot "$file"
+	file=$func_resolve_sysroot_result
+
+	# Check to see that this really is a libtool archive.
+	func_lalib_unsafe_p "$file" \
+	  || func_fatal_help "'$lib' is not a valid libtool archive"
+
+	# Read the libtool library.
+	dlname=
+	library_names=
+	func_source "$file"
+
+	# Skip this library if it cannot be dlopened.
+	if test -z "$dlname"; then
+	  # Warn if it was a shared library.
+	  test -n "$library_names" && \
+	    func_warning "'$file' was not linked with '-export-dynamic'"
+	  continue
+	fi
+
+	func_dirname "$file" "" "."
+	dir=$func_dirname_result
+
+	if test -f "$dir/$objdir/$dlname"; then
+	  func_append dir "/$objdir"
+	else
+	  if test ! -f "$dir/$dlname"; then
+	    func_fatal_error "cannot find '$dlname' in '$dir' or '$dir/$objdir'"
+	  fi
+	fi
+	;;
+
+      *.lo)
+	# Just add the directory containing the .lo file.
+	func_dirname "$file" "" "."
+	dir=$func_dirname_result
+	;;
+
+      *)
+	func_warning "'-dlopen' is ignored for non-libtool libraries and objects"
+	continue
+	;;
+      esac
+
+      # Get the absolute pathname.
+      absdir=`cd "$dir" && pwd`
+      test -n "$absdir" && dir=$absdir
+
+      # Now add the directory to shlibpath_var.
+      if eval "test -z \"\$$shlibpath_var\""; then
+	eval "$shlibpath_var=\"\$dir\""
+      else
+	eval "$shlibpath_var=\"\$dir:\$$shlibpath_var\""
+      fi
+    done
+
+    # This variable tells wrapper scripts just to set shlibpath_var
+    # rather than running their programs.
+    libtool_execute_magic=$magic
+
+    # Check if any of the arguments is a wrapper script.
+    args=
+    for file
+    do
+      case $file in
+      -* | *.la | *.lo ) ;;
+      *)
+	# Do a test to see if this is really a libtool program.
+	if func_ltwrapper_script_p "$file"; then
+	  func_source "$file"
+	  # Transform arg to wrapped name.
+	  file=$progdir/$program
+	elif func_ltwrapper_executable_p "$file"; then
+	  func_ltwrapper_scriptname "$file"
+	  func_source "$func_ltwrapper_scriptname_result"
+	  # Transform arg to wrapped name.
+	  file=$progdir/$program
+	fi
+	;;
+      esac
+      # Quote arguments (to preserve shell metacharacters).
+      func_append_quoted args "$file"
+    done
+
+    if $opt_dry_run; then
+      # Display what would be done.
+      if test -n "$shlibpath_var"; then
+	eval "\$ECHO \"\$shlibpath_var=\$$shlibpath_var\""
+	echo "export $shlibpath_var"
+      fi
+      $ECHO "$cmd$args"
+      exit $EXIT_SUCCESS
+    else
+      if test -n "$shlibpath_var"; then
+	# Export the shlibpath_var.
+	eval "export $shlibpath_var"
+      fi
+
+      # Restore saved environment variables
+      for lt_var in LANG LANGUAGE LC_ALL LC_CTYPE LC_COLLATE LC_MESSAGES
+      do
+	eval "if test \"\${save_$lt_var+set}\" = set; then
+                $lt_var=\$save_$lt_var; export $lt_var
+	      else
+		$lt_unset $lt_var
+	      fi"
+      done
+
+      # Now prepare to actually exec the command.
+      exec_cmd=\$cmd$args
+    fi
+}
+
+test execute = "$opt_mode" && func_mode_execute ${1+"$@"}
+
+
+# func_mode_finish arg...
+func_mode_finish ()
+{
+    $debug_cmd
+
+    libs=
+    libdirs=
+    admincmds=
+
+    for opt in "$nonopt" ${1+"$@"}
+    do
+      if test -d "$opt"; then
+	func_append libdirs " $opt"
+
+      elif test -f "$opt"; then
+	if func_lalib_unsafe_p "$opt"; then
+	  func_append libs " $opt"
+	else
+	  func_warning "'$opt' is not a valid libtool archive"
+	fi
+
+      else
+	func_fatal_error "invalid argument '$opt'"
+      fi
+    done
+
+    if test -n "$libs"; then
+      if test -n "$lt_sysroot"; then
+        sysroot_regex=`$ECHO "$lt_sysroot" | $SED "$sed_make_literal_regex"`
+        sysroot_cmd="s/\([ ']\)$sysroot_regex/\1/g;"
+      else
+        sysroot_cmd=
+      fi
+
+      # Remove sysroot references
+      if $opt_dry_run; then
+        for lib in $libs; do
+          echo "removing references to $lt_sysroot and '=' prefixes from $lib"
+        done
+      else
+        tmpdir=`func_mktempdir`
+        for lib in $libs; do
+	  $SED -e "$sysroot_cmd s/\([ ']-[LR]\)=/\1/g; s/\([ ']\)=/\1/g" $lib \
+	    > $tmpdir/tmp-la
+	  mv -f $tmpdir/tmp-la $lib
+	done
+        ${RM}r "$tmpdir"
+      fi
+    fi
+
+    if test -n "$finish_cmds$finish_eval" && test -n "$libdirs"; then
+      for libdir in $libdirs; do
+	if test -n "$finish_cmds"; then
+	  # Do each command in the finish commands.
+	  func_execute_cmds "$finish_cmds" 'admincmds="$admincmds
+'"$cmd"'"'
+	fi
+	if test -n "$finish_eval"; then
+	  # Do the single finish_eval.
+	  eval cmds=\"$finish_eval\"
+	  $opt_dry_run || eval "$cmds" || func_append admincmds "
+       $cmds"
+	fi
+      done
+    fi
+
+    # Exit here if they wanted silent mode.
+    $opt_quiet && exit $EXIT_SUCCESS
+
+    if test -n "$finish_cmds$finish_eval" && test -n "$libdirs"; then
+      echo "----------------------------------------------------------------------"
+      echo "Libraries have been installed in:"
+      for libdir in $libdirs; do
+	$ECHO "   $libdir"
+      done
+      echo
+      echo "If you ever happen to want to link against installed libraries"
+      echo "in a given directory, LIBDIR, you must either use libtool, and"
+      echo "specify the full pathname of the library, or use the '-LLIBDIR'"
+      echo "flag during linking and do at least one of the following:"
+      if test -n "$shlibpath_var"; then
+	echo "   - add LIBDIR to the '$shlibpath_var' environment variable"
+	echo "     during execution"
+      fi
+      if test -n "$runpath_var"; then
+	echo "   - add LIBDIR to the '$runpath_var' environment variable"
+	echo "     during linking"
+      fi
+      if test -n "$hardcode_libdir_flag_spec"; then
+	libdir=LIBDIR
+	eval flag=\"$hardcode_libdir_flag_spec\"
+
+	$ECHO "   - use the '$flag' linker flag"
+      fi
+      if test -n "$admincmds"; then
+	$ECHO "   - have your system administrator run these commands:$admincmds"
+      fi
+      if test -f /etc/ld.so.conf; then
+	echo "   - have your system administrator add LIBDIR to '/etc/ld.so.conf'"
+      fi
+      echo
+
+      echo "See any operating system documentation about shared libraries for"
+      case $host in
+	solaris2.[6789]|solaris2.1[0-9])
+	  echo "more information, such as the ld(1), crle(1) and ld.so(8) manual"
+	  echo "pages."
+	  ;;
+	*)
+	  echo "more information, such as the ld(1) and ld.so(8) manual pages."
+	  ;;
+      esac
+      echo "----------------------------------------------------------------------"
+    fi
+    exit $EXIT_SUCCESS
+}
+
+test finish = "$opt_mode" && func_mode_finish ${1+"$@"}
+
+
+# func_mode_install arg...
+func_mode_install ()
+{
+    $debug_cmd
+
+    # There may be an optional sh(1) argument at the beginning of
+    # install_prog (especially on Windows NT).
+    if test "$SHELL" = "$nonopt" || test /bin/sh = "$nonopt" ||
+       # Allow the use of GNU shtool's install command.
+       case $nonopt in *shtool*) :;; *) false;; esac
+    then
+      # Aesthetically quote it.
+      func_quote_arg pretty "$nonopt"
+      install_prog="$func_quote_arg_result "
+      arg=$1
+      shift
+    else
+      install_prog=
+      arg=$nonopt
+    fi
+
+    # The real first argument should be the name of the installation program.
+    # Aesthetically quote it.
+    func_quote_arg pretty "$arg"
+    func_append install_prog "$func_quote_arg_result"
+    install_shared_prog=$install_prog
+    case " $install_prog " in
+      *[\\\ /]cp\ *) install_cp=: ;;
+      *) install_cp=false ;;
+    esac
+
+    # We need to accept at least all the BSD install flags.
+    dest=
+    files=
+    opts=
+    prev=
+    install_type=
+    isdir=false
+    stripme=
+    no_mode=:
+    for arg
+    do
+      arg2=
+      if test -n "$dest"; then
+	func_append files " $dest"
+	dest=$arg
+	continue
+      fi
+
+      case $arg in
+      -d) isdir=: ;;
+      -f)
+	if $install_cp; then :; else
+	  prev=$arg
+	fi
+	;;
+      -g | -m | -o)
+	prev=$arg
+	;;
+      -s)
+	stripme=" -s"
+	continue
+	;;
+      -*)
+	;;
+      *)
+	# If the previous option needed an argument, then skip it.
+	if test -n "$prev"; then
+	  if test X-m = "X$prev" && test -n "$install_override_mode"; then
+	    arg2=$install_override_mode
+	    no_mode=false
+	  fi
+	  prev=
+	else
+	  dest=$arg
+	  continue
+	fi
+	;;
+      esac
+
+      # Aesthetically quote the argument.
+      func_quote_arg pretty "$arg"
+      func_append install_prog " $func_quote_arg_result"
+      if test -n "$arg2"; then
+	func_quote_arg pretty "$arg2"
+      fi
+      func_append install_shared_prog " $func_quote_arg_result"
+    done
+
+    test -z "$install_prog" && \
+      func_fatal_help "you must specify an install program"
+
+    test -n "$prev" && \
+      func_fatal_help "the '$prev' option requires an argument"
+
+    if test -n "$install_override_mode" && $no_mode; then
+      if $install_cp; then :; else
+	func_quote_arg pretty "$install_override_mode"
+	func_append install_shared_prog " -m $func_quote_arg_result"
+      fi
+    fi
+
+    if test -z "$files"; then
+      if test -z "$dest"; then
+	func_fatal_help "no file or destination specified"
+      else
+	func_fatal_help "you must specify a destination"
+      fi
+    fi
+
+    # Strip any trailing slash from the destination.
+    func_stripname '' '/' "$dest"
+    dest=$func_stripname_result
+
+    # Check to see that the destination is a directory.
+    test -d "$dest" && isdir=:
+    if $isdir; then
+      destdir=$dest
+      destname=
+    else
+      func_dirname_and_basename "$dest" "" "."
+      destdir=$func_dirname_result
+      destname=$func_basename_result
+
+      # Not a directory, so check to see that there is only one file specified.
+      set dummy $files; shift
+      test "$#" -gt 1 && \
+	func_fatal_help "'$dest' is not a directory"
+    fi
+    case $destdir in
+    [\\/]* | [A-Za-z]:[\\/]*) ;;
+    *)
+      for file in $files; do
+	case $file in
+	*.lo) ;;
+	*)
+	  func_fatal_help "'$destdir' must be an absolute directory name"
+	  ;;
+	esac
+      done
+      ;;
+    esac
+
+    # This variable tells wrapper scripts just to set variables rather
+    # than running their programs.
+    libtool_install_magic=$magic
+
+    staticlibs=
+    future_libdirs=
+    current_libdirs=
+    for file in $files; do
+
+      # Do each installation.
+      case $file in
+      *.$libext)
+	# Do the static libraries later.
+	func_append staticlibs " $file"
+	;;
+
+      *.la)
+	func_resolve_sysroot "$file"
+	file=$func_resolve_sysroot_result
+
+	# Check to see that this really is a libtool archive.
+	func_lalib_unsafe_p "$file" \
+	  || func_fatal_help "'$file' is not a valid libtool archive"
+
+	library_names=
+	old_library=
+	relink_command=
+	func_source "$file"
+
+	# Add the libdir to current_libdirs if it is the destination.
+	if test "X$destdir" = "X$libdir"; then
+	  case "$current_libdirs " in
+	  *" $libdir "*) ;;
+	  *) func_append current_libdirs " $libdir" ;;
+	  esac
+	else
+	  # Note the libdir as a future libdir.
+	  case "$future_libdirs " in
+	  *" $libdir "*) ;;
+	  *) func_append future_libdirs " $libdir" ;;
+	  esac
+	fi
+
+	func_dirname "$file" "/" ""
+	dir=$func_dirname_result
+	func_append dir "$objdir"
+
+	if test -n "$relink_command"; then
+	  # Determine the prefix the user has applied to our future dir.
+	  inst_prefix_dir=`$ECHO "$destdir" | $SED -e "s%$libdir\$%%"`
+
+	  # Don't allow the user to place us outside of our expected
+	  # location b/c this prevents finding dependent libraries that
+	  # are installed to the same prefix.
+	  # At present, this check doesn't affect windows .dll's that
+	  # are installed into $libdir/../bin (currently, that works fine)
+	  # but it's something to keep an eye on.
+	  test "$inst_prefix_dir" = "$destdir" && \
+	    func_fatal_error "error: cannot install '$file' to a directory not ending in $libdir"
+
+	  if test -n "$inst_prefix_dir"; then
+	    # Stick the inst_prefix_dir data into the link command.
+	    relink_command=`$ECHO "$relink_command" | $SED "s%@inst_prefix_dir@%-inst-prefix-dir $inst_prefix_dir%"`
+	  else
+	    relink_command=`$ECHO "$relink_command" | $SED "s%@inst_prefix_dir@%%"`
+	  fi
+
+	  func_warning "relinking '$file'"
+	  func_show_eval "$relink_command" \
+	    'func_fatal_error "error: relink '\''$file'\'' with the above command before installing it"'
+	fi
+
+	# See the names of the shared library.
+	set dummy $library_names; shift
+	if test -n "$1"; then
+	  realname=$1
+	  shift
+
+	  srcname=$realname
+	  test -n "$relink_command" && srcname=${realname}T
+
+	  # Install the shared library and build the symlinks.
+	  func_show_eval "$install_shared_prog $dir/$srcname $destdir/$realname" \
+	      'exit $?'
+	  tstripme=$stripme
+	  case $host_os in
+	  cygwin* | mingw* | pw32* | cegcc*)
+	    case $realname in
+	    *.dll.a)
+	      tstripme=
+	      ;;
+	    esac
+	    ;;
+	  os2*)
+	    case $realname in
+	    *_dll.a)
+	      tstripme=
+	      ;;
+	    esac
+	    ;;
+	  esac
+	  if test -n "$tstripme" && test -n "$striplib"; then
+	    func_show_eval "$striplib $destdir/$realname" 'exit $?'
+	  fi
+
+	  if test "$#" -gt 0; then
+	    # Delete the old symlinks, and create new ones.
+	    # Try 'ln -sf' first, because the 'ln' binary might depend on
+	    # the symlink we replace!  Solaris /bin/ln does not understand -f,
+	    # so we also need to try rm && ln -s.
+	    for linkname
+	    do
+	      test "$linkname" != "$realname" \
+		&& func_show_eval "(cd $destdir && { $LN_S -f $realname $linkname || { $RM $linkname && $LN_S $realname $linkname; }; })"
+	    done
+	  fi
+
+	  # Do each command in the postinstall commands.
+	  lib=$destdir/$realname
+	  func_execute_cmds "$postinstall_cmds" 'exit $?'
+	fi
+
+	# Install the pseudo-library for information purposes.
+	func_basename "$file"
+	name=$func_basename_result
+	instname=$dir/${name}i
+	func_show_eval "$install_prog $instname $destdir/$name" 'exit $?'
+
+	# Maybe install the static library, too.
+	test -n "$old_library" && func_append staticlibs " $dir/$old_library"
+	;;
+
+      *.lo)
+	# Install (i.e. copy) a libtool object.
+
+	# Figure out destination file name, if it wasn't already specified.
+	if test -n "$destname"; then
+	  destfile=$destdir/$destname
+	else
+	  func_basename "$file"
+	  destfile=$func_basename_result
+	  destfile=$destdir/$destfile
+	fi
+
+	# Deduce the name of the destination old-style object file.
+	case $destfile in
+	*.lo)
+	  func_lo2o "$destfile"
+	  staticdest=$func_lo2o_result
+	  ;;
+	*.$objext)
+	  staticdest=$destfile
+	  destfile=
+	  ;;
+	*)
+	  func_fatal_help "cannot copy a libtool object to '$destfile'"
+	  ;;
+	esac
+
+	# Install the libtool object if requested.
+	test -n "$destfile" && \
+	  func_show_eval "$install_prog $file $destfile" 'exit $?'
+
+	# Install the old object if enabled.
+	if test yes = "$build_old_libs"; then
+	  # Deduce the name of the old-style object file.
+	  func_lo2o "$file"
+	  staticobj=$func_lo2o_result
+	  func_show_eval "$install_prog \$staticobj \$staticdest" 'exit $?'
+	fi
+	exit $EXIT_SUCCESS
+	;;
+
+      *)
+	# Figure out destination file name, if it wasn't already specified.
+	if test -n "$destname"; then
+	  destfile=$destdir/$destname
+	else
+	  func_basename "$file"
+	  destfile=$func_basename_result
+	  destfile=$destdir/$destfile
+	fi
+
+	# If the file is missing, and there is a .exe on the end, strip it
+	# because it is most likely a libtool script we actually want to
+	# install
+	stripped_ext=
+	case $file in
+	  *.exe)
+	    if test ! -f "$file"; then
+	      func_stripname '' '.exe' "$file"
+	      file=$func_stripname_result
+	      stripped_ext=.exe
+	    fi
+	    ;;
+	esac
+
+	# Do a test to see if this is really a libtool program.
+	case $host in
+	*cygwin* | *mingw*)
+	    if func_ltwrapper_executable_p "$file"; then
+	      func_ltwrapper_scriptname "$file"
+	      wrapper=$func_ltwrapper_scriptname_result
+	    else
+	      func_stripname '' '.exe' "$file"
+	      wrapper=$func_stripname_result
+	    fi
+	    ;;
+	*)
+	    wrapper=$file
+	    ;;
+	esac
+	if func_ltwrapper_script_p "$wrapper"; then
+	  notinst_deplibs=
+	  relink_command=
+
+	  func_source "$wrapper"
+
+	  # Check the variables that should have been set.
+	  test -z "$generated_by_libtool_version" && \
+	    func_fatal_error "invalid libtool wrapper script '$wrapper'"
+
+	  finalize=:
+	  for lib in $notinst_deplibs; do
+	    # Check to see that each library is installed.
+	    libdir=
+	    if test -f "$lib"; then
+	      func_source "$lib"
+	    fi
+	    libfile=$libdir/`$ECHO "$lib" | $SED 's%^.*/%%g'`
+	    if test -n "$libdir" && test ! -f "$libfile"; then
+	      func_warning "'$lib' has not been installed in '$libdir'"
+	      finalize=false
+	    fi
+	  done
+
+	  relink_command=
+	  func_source "$wrapper"
+
+	  outputname=
+	  if test no = "$fast_install" && test -n "$relink_command"; then
+	    $opt_dry_run || {
+	      if $finalize; then
+	        tmpdir=`func_mktempdir`
+		func_basename "$file$stripped_ext"
+		file=$func_basename_result
+	        outputname=$tmpdir/$file
+	        # Replace the output file specification.
+	        relink_command=`$ECHO "$relink_command" | $SED 's%@OUTPUT@%'"$outputname"'%g'`
+
+	        $opt_quiet || {
+	          func_quote_arg expand,pretty "$relink_command"
+		  eval "func_echo $func_quote_arg_result"
+	        }
+	        if eval "$relink_command"; then :
+	          else
+		  func_error "error: relink '$file' with the above command before installing it"
+		  $opt_dry_run || ${RM}r "$tmpdir"
+		  continue
+	        fi
+	        file=$outputname
+	      else
+	        func_warning "cannot relink '$file'"
+	      fi
+	    }
+	  else
+	    # Install the binary that we compiled earlier.
+	    file=`$ECHO "$file$stripped_ext" | $SED "s%\([^/]*\)$%$objdir/\1%"`
+	  fi
+	fi
+
+	# remove .exe since cygwin /usr/bin/install will append another
+	# one anyway
+	case $install_prog,$host in
+	*/usr/bin/install*,*cygwin*)
+	  case $file:$destfile in
+	  *.exe:*.exe)
+	    # this is ok
+	    ;;
+	  *.exe:*)
+	    destfile=$destfile.exe
+	    ;;
+	  *:*.exe)
+	    func_stripname '' '.exe' "$destfile"
+	    destfile=$func_stripname_result
+	    ;;
+	  esac
+	  ;;
+	esac
+	func_show_eval "$install_prog\$stripme \$file \$destfile" 'exit $?'
+	$opt_dry_run || if test -n "$outputname"; then
+	  ${RM}r "$tmpdir"
+	fi
+	;;
+      esac
+    done
+
+    for file in $staticlibs; do
+      func_basename "$file"
+      name=$func_basename_result
+
+      # Set up the ranlib parameters.
+      oldlib=$destdir/$name
+      func_to_tool_file "$oldlib" func_convert_file_msys_to_w32
+      tool_oldlib=$func_to_tool_file_result
+
+      func_show_eval "$install_prog \$file \$oldlib" 'exit $?'
+
+      if test -n "$stripme" && test -n "$old_striplib"; then
+	func_show_eval "$old_striplib $tool_oldlib" 'exit $?'
+      fi
+
+      # Do each command in the postinstall commands.
+      func_execute_cmds "$old_postinstall_cmds" 'exit $?'
+    done
+
+    test -n "$future_libdirs" && \
+      func_warning "remember to run '$progname --finish$future_libdirs'"
+
+    if test -n "$current_libdirs"; then
+      # Maybe just do a dry run.
+      $opt_dry_run && current_libdirs=" -n$current_libdirs"
+      exec_cmd='$SHELL "$progpath" $preserve_args --finish$current_libdirs'
+    else
+      exit $EXIT_SUCCESS
+    fi
+}
+
+test install = "$opt_mode" && func_mode_install ${1+"$@"}
+
+
+# func_generate_dlsyms outputname originator pic_p
+# Extract symbols from dlprefiles and create ${outputname}S.o with
+# a dlpreopen symbol table.
+func_generate_dlsyms ()
+{
+    $debug_cmd
+
+    my_outputname=$1
+    my_originator=$2
+    my_pic_p=${3-false}
+    my_prefix=`$ECHO "$my_originator" | $SED 's%[^a-zA-Z0-9]%_%g'`
+    my_dlsyms=
+
+    if test -n "$dlfiles$dlprefiles" || test no != "$dlself"; then
+      if test -n "$NM" && test -n "$global_symbol_pipe"; then
+	my_dlsyms=${my_outputname}S.c
+      else
+	func_error "not configured to extract global symbols from dlpreopened files"
+      fi
+    fi
+
+    if test -n "$my_dlsyms"; then
+      case $my_dlsyms in
+      "") ;;
+      *.c)
+	# Discover the nlist of each of the dlfiles.
+	nlist=$output_objdir/$my_outputname.nm
+
+	func_show_eval "$RM $nlist ${nlist}S ${nlist}T"
+
+	# Parse the name list into a source file.
+	func_verbose "creating $output_objdir/$my_dlsyms"
+
+	$opt_dry_run || $ECHO > "$output_objdir/$my_dlsyms" "\
+/* $my_dlsyms - symbol resolution table for '$my_outputname' dlsym emulation. */
+/* Generated by $PROGRAM (GNU $PACKAGE) $VERSION */
+
+#ifdef __cplusplus
+extern \"C\" {
+#endif
+
+#if defined __GNUC__ && (((__GNUC__ == 4) && (__GNUC_MINOR__ >= 4)) || (__GNUC__ > 4))
+#pragma GCC diagnostic ignored \"-Wstrict-prototypes\"
+#endif
+
+/* Keep this code in sync between libtool.m4, ltmain, lt_system.h, and tests.  */
+#if defined _WIN32 || defined __CYGWIN__ || defined _WIN32_WCE
+/* DATA imports from DLLs on WIN32 can't be const, because runtime
+   relocations are performed -- see ld's documentation on pseudo-relocs.  */
+# define LT_DLSYM_CONST
+#elif defined __osf__
+/* This system does not cope well with relocations in const data.  */
+# define LT_DLSYM_CONST
+#else
+# define LT_DLSYM_CONST const
+#endif
+
+#define STREQ(s1, s2) (strcmp ((s1), (s2)) == 0)
+
+/* External symbol declarations for the compiler. */\
+"
+
+	if test yes = "$dlself"; then
+	  func_verbose "generating symbol list for '$output'"
+
+	  $opt_dry_run || echo ': @PROGRAM@ ' > "$nlist"
+
+	  # Add our own program objects to the symbol list.
+	  progfiles=`$ECHO "$objs$old_deplibs" | $SP2NL | $SED "$lo2o" | $NL2SP`
+	  for progfile in $progfiles; do
+	    func_to_tool_file "$progfile" func_convert_file_msys_to_w32
+	    func_verbose "extracting global C symbols from '$func_to_tool_file_result'"
+	    $opt_dry_run || eval "$NM $func_to_tool_file_result | $global_symbol_pipe >> '$nlist'"
+	  done
+
+	  if test -n "$exclude_expsyms"; then
+	    $opt_dry_run || {
+	      eval '$EGREP -v " ($exclude_expsyms)$" "$nlist" > "$nlist"T'
+	      eval '$MV "$nlist"T "$nlist"'
+	    }
+	  fi
+
+	  if test -n "$export_symbols_regex"; then
+	    $opt_dry_run || {
+	      eval '$EGREP -e "$export_symbols_regex" "$nlist" > "$nlist"T'
+	      eval '$MV "$nlist"T "$nlist"'
+	    }
+	  fi
+
+	  # Prepare the list of exported symbols
+	  if test -z "$export_symbols"; then
+	    export_symbols=$output_objdir/$outputname.exp
+	    $opt_dry_run || {
+	      $RM $export_symbols
+	      eval "$SED -n -e '/^: @PROGRAM@ $/d' -e 's/^.* \(.*\)$/\1/p' "'< "$nlist" > "$export_symbols"'
+	      case $host in
+	      *cygwin* | *mingw* | *cegcc* )
+                eval "echo EXPORTS "'> "$output_objdir/$outputname.def"'
+                eval 'cat "$export_symbols" >> "$output_objdir/$outputname.def"'
+	        ;;
+	      esac
+	    }
+	  else
+	    $opt_dry_run || {
+	      eval "$SED -e 's/\([].[*^$]\)/\\\\\1/g' -e 's/^/ /' -e 's/$/$/'"' < "$export_symbols" > "$output_objdir/$outputname.exp"'
+	      eval '$GREP -f "$output_objdir/$outputname.exp" < "$nlist" > "$nlist"T'
+	      eval '$MV "$nlist"T "$nlist"'
+	      case $host in
+	        *cygwin* | *mingw* | *cegcc* )
+	          eval "echo EXPORTS "'> "$output_objdir/$outputname.def"'
+	          eval 'cat "$nlist" >> "$output_objdir/$outputname.def"'
+	          ;;
+	      esac
+	    }
+	  fi
+	fi
+
+	for dlprefile in $dlprefiles; do
+	  func_verbose "extracting global C symbols from '$dlprefile'"
+	  func_basename "$dlprefile"
+	  name=$func_basename_result
+          case $host in
+	    *cygwin* | *mingw* | *cegcc* )
+	      # if an import library, we need to obtain dlname
+	      if func_win32_import_lib_p "$dlprefile"; then
+	        func_tr_sh "$dlprefile"
+	        eval "curr_lafile=\$libfile_$func_tr_sh_result"
+	        dlprefile_dlbasename=
+	        if test -n "$curr_lafile" && func_lalib_p "$curr_lafile"; then
+	          # Use subshell, to avoid clobbering current variable values
+	          dlprefile_dlname=`source "$curr_lafile" && echo "$dlname"`
+	          if test -n "$dlprefile_dlname"; then
+	            func_basename "$dlprefile_dlname"
+	            dlprefile_dlbasename=$func_basename_result
+	          else
+	            # no lafile. user explicitly requested -dlpreopen <import library>.
+	            $sharedlib_from_linklib_cmd "$dlprefile"
+	            dlprefile_dlbasename=$sharedlib_from_linklib_result
+	          fi
+	        fi
+	        $opt_dry_run || {
+	          if test -n "$dlprefile_dlbasename"; then
+	            eval '$ECHO ": $dlprefile_dlbasename" >> "$nlist"'
+	          else
+	            func_warning "Could not compute DLL name from $name"
+	            eval '$ECHO ": $name " >> "$nlist"'
+	          fi
+	          func_to_tool_file "$dlprefile" func_convert_file_msys_to_w32
+	          eval "$NM \"$func_to_tool_file_result\" 2>/dev/null | $global_symbol_pipe |
+	            $SED -e '/I __imp/d' -e 's/I __nm_/D /;s/_nm__//' >> '$nlist'"
+	        }
+	      else # not an import lib
+	        $opt_dry_run || {
+	          eval '$ECHO ": $name " >> "$nlist"'
+	          func_to_tool_file "$dlprefile" func_convert_file_msys_to_w32
+	          eval "$NM \"$func_to_tool_file_result\" 2>/dev/null | $global_symbol_pipe >> '$nlist'"
+	        }
+	      fi
+	    ;;
+	    *)
+	      $opt_dry_run || {
+	        eval '$ECHO ": $name " >> "$nlist"'
+	        func_to_tool_file "$dlprefile" func_convert_file_msys_to_w32
+	        eval "$NM \"$func_to_tool_file_result\" 2>/dev/null | $global_symbol_pipe >> '$nlist'"
+	      }
+	    ;;
+          esac
+	done
+
+	$opt_dry_run || {
+	  # Make sure we have at least an empty file.
+	  test -f "$nlist" || : > "$nlist"
+
+	  if test -n "$exclude_expsyms"; then
+	    $EGREP -v " ($exclude_expsyms)$" "$nlist" > "$nlist"T
+	    $MV "$nlist"T "$nlist"
+	  fi
+
+	  # Try sorting and uniquifying the output.
+	  if $GREP -v "^: " < "$nlist" |
+	      if sort -k 3 </dev/null >/dev/null 2>&1; then
+		sort -k 3
+	      else
+		sort +2
+	      fi |
+	      uniq > "$nlist"S; then
+	    :
+	  else
+	    $GREP -v "^: " < "$nlist" > "$nlist"S
+	  fi
+
+	  if test -f "$nlist"S; then
+	    eval "$global_symbol_to_cdecl"' < "$nlist"S >> "$output_objdir/$my_dlsyms"'
+	  else
+	    echo '/* NONE */' >> "$output_objdir/$my_dlsyms"
+	  fi
+
+	  func_show_eval '$RM "${nlist}I"'
+	  if test -n "$global_symbol_to_import"; then
+	    eval "$global_symbol_to_import"' < "$nlist"S > "$nlist"I'
+	  fi
+
+	  echo >> "$output_objdir/$my_dlsyms" "\
+
+/* The mapping between symbol names and symbols.  */
+typedef struct {
+  const char *name;
+  void *address;
+} lt_dlsymlist;
+extern LT_DLSYM_CONST lt_dlsymlist
+lt_${my_prefix}_LTX_preloaded_symbols[];\
+"
+
+	  if test -s "$nlist"I; then
+	    echo >> "$output_objdir/$my_dlsyms" "\
+static void lt_syminit(void)
+{
+  LT_DLSYM_CONST lt_dlsymlist *symbol = lt_${my_prefix}_LTX_preloaded_symbols;
+  for (; symbol->name; ++symbol)
+    {"
+	    $SED 's/.*/      if (STREQ (symbol->name, \"&\")) symbol->address = (void *) \&&;/' < "$nlist"I >> "$output_objdir/$my_dlsyms"
+	    echo >> "$output_objdir/$my_dlsyms" "\
+    }
+}"
+	  fi
+	  echo >> "$output_objdir/$my_dlsyms" "\
+LT_DLSYM_CONST lt_dlsymlist
+lt_${my_prefix}_LTX_preloaded_symbols[] =
+{ {\"$my_originator\", (void *) 0},"
+
+	  if test -s "$nlist"I; then
+	    echo >> "$output_objdir/$my_dlsyms" "\
+  {\"@INIT@\", (void *) &lt_syminit},"
+	  fi
+
+	  case $need_lib_prefix in
+	  no)
+	    eval "$global_symbol_to_c_name_address" < "$nlist" >> "$output_objdir/$my_dlsyms"
+	    ;;
+	  *)
+	    eval "$global_symbol_to_c_name_address_lib_prefix" < "$nlist" >> "$output_objdir/$my_dlsyms"
+	    ;;
+	  esac
+	  echo >> "$output_objdir/$my_dlsyms" "\
+  {0, (void *) 0}
+};
+
+/* This works around a problem in FreeBSD linker */
+#ifdef FREEBSD_WORKAROUND
+static const void *lt_preloaded_setup() {
+  return lt_${my_prefix}_LTX_preloaded_symbols;
+}
+#endif
+
+#ifdef __cplusplus
+}
+#endif\
+"
+	} # !$opt_dry_run
+
+	pic_flag_for_symtable=
+	case "$compile_command " in
+	*" -static "*) ;;
+	*)
+	  case $host in
+	  # compiling the symbol table file with pic_flag works around
+	  # a FreeBSD bug that causes programs to crash when -lm is
+	  # linked before any other PIC object.  But we must not use
+	  # pic_flag when linking with -static.  The problem exists in
+	  # FreeBSD 2.2.6 and is fixed in FreeBSD 3.1.
+	  *-*-freebsd2.*|*-*-freebsd3.0*|*-*-freebsdelf3.0*)
+	    pic_flag_for_symtable=" $pic_flag -DFREEBSD_WORKAROUND" ;;
+	  *-*-hpux*)
+	    pic_flag_for_symtable=" $pic_flag"  ;;
+	  *)
+	    $my_pic_p && pic_flag_for_symtable=" $pic_flag"
+	    ;;
+	  esac
+	  ;;
+	esac
+	symtab_cflags=
+	for arg in $LTCFLAGS; do
+	  case $arg in
+	  -pie | -fpie | -fPIE) ;;
+	  *) func_append symtab_cflags " $arg" ;;
+	  esac
+	done
+
+	# Now compile the dynamic symbol file.
+	func_show_eval '(cd $output_objdir && $LTCC$symtab_cflags -c$no_builtin_flag$pic_flag_for_symtable "$my_dlsyms")' 'exit $?'
+
+	# Clean up the generated files.
+	func_show_eval '$RM "$output_objdir/$my_dlsyms" "$nlist" "${nlist}S" "${nlist}T" "${nlist}I"'
+
+	# Transform the symbol file into the correct name.
+	symfileobj=$output_objdir/${my_outputname}S.$objext
+	case $host in
+	*cygwin* | *mingw* | *cegcc* )
+	  if test -f "$output_objdir/$my_outputname.def"; then
+	    compile_command=`$ECHO "$compile_command" | $SED "s%@SYMFILE@%$output_objdir/$my_outputname.def $symfileobj%"`
+	    finalize_command=`$ECHO "$finalize_command" | $SED "s%@SYMFILE@%$output_objdir/$my_outputname.def $symfileobj%"`
+	  else
+	    compile_command=`$ECHO "$compile_command" | $SED "s%@SYMFILE@%$symfileobj%"`
+	    finalize_command=`$ECHO "$finalize_command" | $SED "s%@SYMFILE@%$symfileobj%"`
+	  fi
+	  ;;
+	*)
+	  compile_command=`$ECHO "$compile_command" | $SED "s%@SYMFILE@%$symfileobj%"`
+	  finalize_command=`$ECHO "$finalize_command" | $SED "s%@SYMFILE@%$symfileobj%"`
+	  ;;
+	esac
+	;;
+      *)
+	func_fatal_error "unknown suffix for '$my_dlsyms'"
+	;;
+      esac
+    else
+      # We keep going just in case the user didn't refer to
+      # lt_preloaded_symbols.  The linker will fail if global_symbol_pipe
+      # really was required.
+
+      # Nullify the symbol file.
+      compile_command=`$ECHO "$compile_command" | $SED "s% @SYMFILE@%%"`
+      finalize_command=`$ECHO "$finalize_command" | $SED "s% @SYMFILE@%%"`
+    fi
+}
+
+# func_cygming_gnu_implib_p ARG
+# This predicate returns with zero status (TRUE) if
+# ARG is a GNU/binutils-style import library. Returns
+# with nonzero status (FALSE) otherwise.
+func_cygming_gnu_implib_p ()
+{
+  $debug_cmd
+
+  func_to_tool_file "$1" func_convert_file_msys_to_w32
+  func_cygming_gnu_implib_tmp=`$NM "$func_to_tool_file_result" | eval "$global_symbol_pipe" | $EGREP ' (_head_[A-Za-z0-9_]+_[ad]l*|[A-Za-z0-9_]+_[ad]l*_iname)$'`
+  test -n "$func_cygming_gnu_implib_tmp"
+}
+
+# func_cygming_ms_implib_p ARG
+# This predicate returns with zero status (TRUE) if
+# ARG is an MS-style import library. Returns
+# with nonzero status (FALSE) otherwise.
+func_cygming_ms_implib_p ()
+{
+  $debug_cmd
+
+  func_to_tool_file "$1" func_convert_file_msys_to_w32
+  func_cygming_ms_implib_tmp=`$NM "$func_to_tool_file_result" | eval "$global_symbol_pipe" | $GREP '_NULL_IMPORT_DESCRIPTOR'`
+  test -n "$func_cygming_ms_implib_tmp"
+}
+
+# func_win32_libid arg
+# return the library type of file 'arg'
+#
+# Need a lot of goo to handle *both* DLLs and import libs
+# Has to be a shell function in order to 'eat' the argument
+# that is supplied when $file_magic_command is called.
+# Despite the name, also deal with 64 bit binaries.
+func_win32_libid ()
+{
+  $debug_cmd
+
+  win32_libid_type=unknown
+  win32_fileres=`file -L $1 2>/dev/null`
+  case $win32_fileres in
+  *ar\ archive\ import\ library*) # definitely import
+    win32_libid_type="x86 archive import"
+    ;;
+  *ar\ archive*) # could be an import, or static
+    # Keep the egrep pattern in sync with the one in _LT_CHECK_MAGIC_METHOD.
+    if eval $OBJDUMP -f $1 | $SED -e '10q' 2>/dev/null |
+       $EGREP 'file format (pei*-i386(.*architecture: i386)?|pe-arm-wince|pe-x86-64)' >/dev/null; then
+      case $nm_interface in
+      "MS dumpbin")
+	if func_cygming_ms_implib_p "$1" ||
+	   func_cygming_gnu_implib_p "$1"
+	then
+	  win32_nmres=import
+	else
+	  win32_nmres=
+	fi
+	;;
+      *)
+	func_to_tool_file "$1" func_convert_file_msys_to_w32
+	win32_nmres=`eval $NM -f posix -A \"$func_to_tool_file_result\" |
+	  $SED -n -e '
+	    1,100{
+		/ I /{
+		    s|.*|import|
+		    p
+		    q
+		}
+	    }'`
+	;;
+      esac
+      case $win32_nmres in
+      import*)  win32_libid_type="x86 archive import";;
+      *)        win32_libid_type="x86 archive static";;
+      esac
+    fi
+    ;;
+  *DLL*)
+    win32_libid_type="x86 DLL"
+    ;;
+  *executable*) # but shell scripts are "executable" too...
+    case $win32_fileres in
+    *MS\ Windows\ PE\ Intel*)
+      win32_libid_type="x86 DLL"
+      ;;
+    esac
+    ;;
+  esac
+  $ECHO "$win32_libid_type"
+}
+
+# func_cygming_dll_for_implib ARG
+#
+# Platform-specific function to extract the
+# name of the DLL associated with the specified
+# import library ARG.
+# Invoked by eval'ing the libtool variable
+#    $sharedlib_from_linklib_cmd
+# Result is available in the variable
+#    $sharedlib_from_linklib_result
+func_cygming_dll_for_implib ()
+{
+  $debug_cmd
+
+  sharedlib_from_linklib_result=`$DLLTOOL --identify-strict --identify "$1"`
+}
+
+# func_cygming_dll_for_implib_fallback_core SECTION_NAME LIBNAMEs
+#
+# The is the core of a fallback implementation of a
+# platform-specific function to extract the name of the
+# DLL associated with the specified import library LIBNAME.
+#
+# SECTION_NAME is either .idata$6 or .idata$7, depending
+# on the platform and compiler that created the implib.
+#
+# Echos the name of the DLL associated with the
+# specified import library.
+func_cygming_dll_for_implib_fallback_core ()
+{
+  $debug_cmd
+
+  match_literal=`$ECHO "$1" | $SED "$sed_make_literal_regex"`
+  $OBJDUMP -s --section "$1" "$2" 2>/dev/null |
+    $SED '/^Contents of section '"$match_literal"':/{
+      # Place marker at beginning of archive member dllname section
+      s/.*/====MARK====/
+      p
+      d
+    }
+    # These lines can sometimes be longer than 43 characters, but
+    # are always uninteresting
+    /:[	 ]*file format pe[i]\{,1\}-/d
+    /^In archive [^:]*:/d
+    # Ensure marker is printed
+    /^====MARK====/p
+    # Remove all lines with less than 43 characters
+    /^.\{43\}/!d
+    # From remaining lines, remove first 43 characters
+    s/^.\{43\}//' |
+    $SED -n '
+      # Join marker and all lines until next marker into a single line
+      /^====MARK====/ b para
+      H
+      $ b para
+      b
+      :para
+      x
+      s/\n//g
+      # Remove the marker
+      s/^====MARK====//
+      # Remove trailing dots and whitespace
+      s/[\. \t]*$//
+      # Print
+      /./p' |
+    # we now have a list, one entry per line, of the stringified
+    # contents of the appropriate section of all members of the
+    # archive that possess that section. Heuristic: eliminate
+    # all those that have a first or second character that is
+    # a '.' (that is, objdump's representation of an unprintable
+    # character.) This should work for all archives with less than
+    # 0x302f exports -- but will fail for DLLs whose name actually
+    # begins with a literal '.' or a single character followed by
+    # a '.'.
+    #
+    # Of those that remain, print the first one.
+    $SED -e '/^\./d;/^.\./d;q'
+}
+
+# func_cygming_dll_for_implib_fallback ARG
+# Platform-specific function to extract the
+# name of the DLL associated with the specified
+# import library ARG.
+#
+# This fallback implementation is for use when $DLLTOOL
+# does not support the --identify-strict option.
+# Invoked by eval'ing the libtool variable
+#    $sharedlib_from_linklib_cmd
+# Result is available in the variable
+#    $sharedlib_from_linklib_result
+func_cygming_dll_for_implib_fallback ()
+{
+  $debug_cmd
+
+  if func_cygming_gnu_implib_p "$1"; then
+    # binutils import library
+    sharedlib_from_linklib_result=`func_cygming_dll_for_implib_fallback_core '.idata$7' "$1"`
+  elif func_cygming_ms_implib_p "$1"; then
+    # ms-generated import library
+    sharedlib_from_linklib_result=`func_cygming_dll_for_implib_fallback_core '.idata$6' "$1"`
+  else
+    # unknown
+    sharedlib_from_linklib_result=
+  fi
+}
+
+
+# func_extract_an_archive dir oldlib
+func_extract_an_archive ()
+{
+    $debug_cmd
+
+    f_ex_an_ar_dir=$1; shift
+    f_ex_an_ar_oldlib=$1
+    if test yes = "$lock_old_archive_extraction"; then
+      lockfile=$f_ex_an_ar_oldlib.lock
+      until $opt_dry_run || ln "$progpath" "$lockfile" 2>/dev/null; do
+	func_echo "Waiting for $lockfile to be removed"
+	sleep 2
+      done
+    fi
+    func_show_eval "(cd \$f_ex_an_ar_dir && $AR x \"\$f_ex_an_ar_oldlib\")" \
+		   'stat=$?; rm -f "$lockfile"; exit $stat'
+    if test yes = "$lock_old_archive_extraction"; then
+      $opt_dry_run || rm -f "$lockfile"
+    fi
+    if ($AR t "$f_ex_an_ar_oldlib" | sort | sort -uc >/dev/null 2>&1); then
+     :
+    else
+      func_fatal_error "object name conflicts in archive: $f_ex_an_ar_dir/$f_ex_an_ar_oldlib"
+    fi
+}
+
+
+# func_extract_archives gentop oldlib ...
+func_extract_archives ()
+{
+    $debug_cmd
+
+    my_gentop=$1; shift
+    my_oldlibs=${1+"$@"}
+    my_oldobjs=
+    my_xlib=
+    my_xabs=
+    my_xdir=
+
+    for my_xlib in $my_oldlibs; do
+      # Extract the objects.
+      case $my_xlib in
+	[\\/]* | [A-Za-z]:[\\/]*) my_xabs=$my_xlib ;;
+	*) my_xabs=`pwd`"/$my_xlib" ;;
+      esac
+      func_basename "$my_xlib"
+      my_xlib=$func_basename_result
+      my_xlib_u=$my_xlib
+      while :; do
+        case " $extracted_archives " in
+	*" $my_xlib_u "*)
+	  func_arith $extracted_serial + 1
+	  extracted_serial=$func_arith_result
+	  my_xlib_u=lt$extracted_serial-$my_xlib ;;
+	*) break ;;
+	esac
+      done
+      extracted_archives="$extracted_archives $my_xlib_u"
+      my_xdir=$my_gentop/$my_xlib_u
+
+      func_mkdir_p "$my_xdir"
+
+      case $host in
+      *-darwin*)
+	func_verbose "Extracting $my_xabs"
+	# Do not bother doing anything if just a dry run
+	$opt_dry_run || {
+	  darwin_orig_dir=`pwd`
+	  cd $my_xdir || exit $?
+	  darwin_archive=$my_xabs
+	  darwin_curdir=`pwd`
+	  func_basename "$darwin_archive"
+	  darwin_base_archive=$func_basename_result
+	  darwin_arches=`$LIPO -info "$darwin_archive" 2>/dev/null | $GREP Architectures 2>/dev/null || true`
+	  if test -n "$darwin_arches"; then
+	    darwin_arches=`$ECHO "$darwin_arches" | $SED -e 's/.*are://'`
+	    darwin_arch=
+	    func_verbose "$darwin_base_archive has multiple architectures $darwin_arches"
+	    for darwin_arch in  $darwin_arches; do
+	      func_mkdir_p "unfat-$$/$darwin_base_archive-$darwin_arch"
+	      $LIPO -thin $darwin_arch -output "unfat-$$/$darwin_base_archive-$darwin_arch/$darwin_base_archive" "$darwin_archive"
+	      cd "unfat-$$/$darwin_base_archive-$darwin_arch"
+	      func_extract_an_archive "`pwd`" "$darwin_base_archive"
+	      cd "$darwin_curdir"
+	      $RM "unfat-$$/$darwin_base_archive-$darwin_arch/$darwin_base_archive"
+	    done # $darwin_arches
+            ## Okay now we've a bunch of thin objects, gotta fatten them up :)
+	    darwin_filelist=`find unfat-$$ -type f -name \*.o -print -o -name \*.lo -print | $SED -e "$sed_basename" | sort -u`
+	    darwin_file=
+	    darwin_files=
+	    for darwin_file in $darwin_filelist; do
+	      darwin_files=`find unfat-$$ -name $darwin_file -print | sort | $NL2SP`
+	      $LIPO -create -output "$darwin_file" $darwin_files
+	    done # $darwin_filelist
+	    $RM -rf unfat-$$
+	    cd "$darwin_orig_dir"
+	  else
+	    cd $darwin_orig_dir
+	    func_extract_an_archive "$my_xdir" "$my_xabs"
+	  fi # $darwin_arches
+	} # !$opt_dry_run
+	;;
+      *)
+        func_extract_an_archive "$my_xdir" "$my_xabs"
+	;;
+      esac
+      my_oldobjs="$my_oldobjs "`find $my_xdir -name \*.$objext -print -o -name \*.lo -print | sort | $NL2SP`
+    done
+
+    func_extract_archives_result=$my_oldobjs
+}
+
+
+# func_emit_wrapper [arg=no]
+#
+# Emit a libtool wrapper script on stdout.
+# Don't directly open a file because we may want to
+# incorporate the script contents within a cygwin/mingw
+# wrapper executable.  Must ONLY be called from within
+# func_mode_link because it depends on a number of variables
+# set therein.
+#
+# ARG is the value that the WRAPPER_SCRIPT_BELONGS_IN_OBJDIR
+# variable will take.  If 'yes', then the emitted script
+# will assume that the directory where it is stored is
+# the $objdir directory.  This is a cygwin/mingw-specific
+# behavior.
+func_emit_wrapper ()
+{
+	func_emit_wrapper_arg1=${1-no}
+
+	$ECHO "\
+#! $SHELL
+
+# $output - temporary wrapper script for $objdir/$outputname
+# Generated by $PROGRAM (GNU $PACKAGE) $VERSION
+#
+# The $output program cannot be directly executed until all the libtool
+# libraries that it depends on are installed.
+#
+# This wrapper script should never be moved out of the build directory.
+# If it is, it will not operate correctly.
+
+# Sed substitution that helps us do robust quoting.  It backslashifies
+# metacharacters that are still active within double-quoted strings.
+sed_quote_subst='$sed_quote_subst'
+
+# Be Bourne compatible
+if test -n \"\${ZSH_VERSION+set}\" && (emulate sh) >/dev/null 2>&1; then
+  emulate sh
+  NULLCMD=:
+  # Zsh 3.x and 4.x performs word splitting on \${1+\"\$@\"}, which
+  # is contrary to our usage.  Disable this feature.
+  alias -g '\${1+\"\$@\"}'='\"\$@\"'
+  setopt NO_GLOB_SUBST
+else
+  case \`(set -o) 2>/dev/null\` in *posix*) set -o posix;; esac
+fi
+BIN_SH=xpg4; export BIN_SH # for Tru64
+DUALCASE=1; export DUALCASE # for MKS sh
+
+# The HP-UX ksh and POSIX shell print the target directory to stdout
+# if CDPATH is set.
+(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
+
+relink_command=\"$relink_command\"
+
+# This environment variable determines our operation mode.
+if test \"\$libtool_install_magic\" = \"$magic\"; then
+  # install mode needs the following variables:
+  generated_by_libtool_version='$macro_version'
+  notinst_deplibs='$notinst_deplibs'
+else
+  # When we are sourced in execute mode, \$file and \$ECHO are already set.
+  if test \"\$libtool_execute_magic\" != \"$magic\"; then
+    file=\"\$0\""
+
+    func_quote_arg pretty "$ECHO"
+    qECHO=$func_quote_arg_result
+    $ECHO "\
+
+# A function that is used when there is no print builtin or printf.
+func_fallback_echo ()
+{
+  eval 'cat <<_LTECHO_EOF
+\$1
+_LTECHO_EOF'
+}
+    ECHO=$qECHO
+  fi
+
+# Very basic option parsing. These options are (a) specific to
+# the libtool wrapper, (b) are identical between the wrapper
+# /script/ and the wrapper /executable/ that is used only on
+# windows platforms, and (c) all begin with the string "--lt-"
+# (application programs are unlikely to have options that match
+# this pattern).
+#
+# There are only two supported options: --lt-debug and
+# --lt-dump-script. There is, deliberately, no --lt-help.
+#
+# The first argument to this parsing function should be the
+# script's $0 value, followed by "$@".
+lt_option_debug=
+func_parse_lt_options ()
+{
+  lt_script_arg0=\$0
+  shift
+  for lt_opt
+  do
+    case \"\$lt_opt\" in
+    --lt-debug) lt_option_debug=1 ;;
+    --lt-dump-script)
+        lt_dump_D=\`\$ECHO \"X\$lt_script_arg0\" | $SED -e 's/^X//' -e 's%/[^/]*$%%'\`
+        test \"X\$lt_dump_D\" = \"X\$lt_script_arg0\" && lt_dump_D=.
+        lt_dump_F=\`\$ECHO \"X\$lt_script_arg0\" | $SED -e 's/^X//' -e 's%^.*/%%'\`
+        cat \"\$lt_dump_D/\$lt_dump_F\"
+        exit 0
+      ;;
+    --lt-*)
+        \$ECHO \"Unrecognized --lt- option: '\$lt_opt'\" 1>&2
+        exit 1
+      ;;
+    esac
+  done
+
+  # Print the debug banner immediately:
+  if test -n \"\$lt_option_debug\"; then
+    echo \"$outputname:$output:\$LINENO: libtool wrapper (GNU $PACKAGE) $VERSION\" 1>&2
+  fi
+}
+
+# Used when --lt-debug. Prints its arguments to stdout
+# (redirection is the responsibility of the caller)
+func_lt_dump_args ()
+{
+  lt_dump_args_N=1;
+  for lt_arg
+  do
+    \$ECHO \"$outputname:$output:\$LINENO: newargv[\$lt_dump_args_N]: \$lt_arg\"
+    lt_dump_args_N=\`expr \$lt_dump_args_N + 1\`
+  done
+}
+
+# Core function for launching the target application
+func_exec_program_core ()
+{
+"
+  case $host in
+  # Backslashes separate directories on plain windows
+  *-*-mingw | *-*-os2* | *-cegcc*)
+    $ECHO "\
+      if test -n \"\$lt_option_debug\"; then
+        \$ECHO \"$outputname:$output:\$LINENO: newargv[0]: \$progdir\\\\\$program\" 1>&2
+        func_lt_dump_args \${1+\"\$@\"} 1>&2
+      fi
+      exec \"\$progdir\\\\\$program\" \${1+\"\$@\"}
+"
+    ;;
+
+  *)
+    $ECHO "\
+      if test -n \"\$lt_option_debug\"; then
+        \$ECHO \"$outputname:$output:\$LINENO: newargv[0]: \$progdir/\$program\" 1>&2
+        func_lt_dump_args \${1+\"\$@\"} 1>&2
+      fi
+      exec \"\$progdir/\$program\" \${1+\"\$@\"}
+"
+    ;;
+  esac
+  $ECHO "\
+      \$ECHO \"\$0: cannot exec \$program \$*\" 1>&2
+      exit 1
+}
+
+# A function to encapsulate launching the target application
+# Strips options in the --lt-* namespace from \$@ and
+# launches target application with the remaining arguments.
+func_exec_program ()
+{
+  case \" \$* \" in
+  *\\ --lt-*)
+    for lt_wr_arg
+    do
+      case \$lt_wr_arg in
+      --lt-*) ;;
+      *) set x \"\$@\" \"\$lt_wr_arg\"; shift;;
+      esac
+      shift
+    done ;;
+  esac
+  func_exec_program_core \${1+\"\$@\"}
+}
+
+  # Parse options
+  func_parse_lt_options \"\$0\" \${1+\"\$@\"}
+
+  # Find the directory that this script lives in.
+  thisdir=\`\$ECHO \"\$file\" | $SED 's%/[^/]*$%%'\`
+  test \"x\$thisdir\" = \"x\$file\" && thisdir=.
+
+  # Follow symbolic links until we get to the real thisdir.
+  file=\`ls -ld \"\$file\" | $SED -n 's/.*-> //p'\`
+  while test -n \"\$file\"; do
+    destdir=\`\$ECHO \"\$file\" | $SED 's%/[^/]*\$%%'\`
+
+    # If there was a directory component, then change thisdir.
+    if test \"x\$destdir\" != \"x\$file\"; then
+      case \"\$destdir\" in
+      [\\\\/]* | [A-Za-z]:[\\\\/]*) thisdir=\"\$destdir\" ;;
+      *) thisdir=\"\$thisdir/\$destdir\" ;;
+      esac
+    fi
+
+    file=\`\$ECHO \"\$file\" | $SED 's%^.*/%%'\`
+    file=\`ls -ld \"\$thisdir/\$file\" | $SED -n 's/.*-> //p'\`
+  done
+
+  # Usually 'no', except on cygwin/mingw when embedded into
+  # the cwrapper.
+  WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=$func_emit_wrapper_arg1
+  if test \"\$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR\" = \"yes\"; then
+    # special case for '.'
+    if test \"\$thisdir\" = \".\"; then
+      thisdir=\`pwd\`
+    fi
+    # remove .libs from thisdir
+    case \"\$thisdir\" in
+    *[\\\\/]$objdir ) thisdir=\`\$ECHO \"\$thisdir\" | $SED 's%[\\\\/][^\\\\/]*$%%'\` ;;
+    $objdir )   thisdir=. ;;
+    esac
+  fi
+
+  # Try to get the absolute directory name.
+  absdir=\`cd \"\$thisdir\" && pwd\`
+  test -n \"\$absdir\" && thisdir=\"\$absdir\"
+"
+
+	if test yes = "$fast_install"; then
+	  $ECHO "\
+  program=lt-'$outputname'$exeext
+  progdir=\"\$thisdir/$objdir\"
+
+  if test ! -f \"\$progdir/\$program\" ||
+     { file=\`ls -1dt \"\$progdir/\$program\" \"\$progdir/../\$program\" 2>/dev/null | $SED 1q\`; \\
+       test \"X\$file\" != \"X\$progdir/\$program\"; }; then
+
+    file=\"\$\$-\$program\"
+
+    if test ! -d \"\$progdir\"; then
+      $MKDIR \"\$progdir\"
+    else
+      $RM \"\$progdir/\$file\"
+    fi"
+
+	  $ECHO "\
+
+    # relink executable if necessary
+    if test -n \"\$relink_command\"; then
+      if relink_command_output=\`eval \$relink_command 2>&1\`; then :
+      else
+	\$ECHO \"\$relink_command_output\" >&2
+	$RM \"\$progdir/\$file\"
+	exit 1
+      fi
+    fi
+
+    $MV \"\$progdir/\$file\" \"\$progdir/\$program\" 2>/dev/null ||
+    { $RM \"\$progdir/\$program\";
+      $MV \"\$progdir/\$file\" \"\$progdir/\$program\"; }
+    $RM \"\$progdir/\$file\"
+  fi"
+	else
+	  $ECHO "\
+  program='$outputname'
+  progdir=\"\$thisdir/$objdir\"
+"
+	fi
+
+	$ECHO "\
+
+  if test -f \"\$progdir/\$program\"; then"
+
+	# fixup the dll searchpath if we need to.
+	#
+	# Fix the DLL searchpath if we need to.  Do this before prepending
+	# to shlibpath, because on Windows, both are PATH and uninstalled
+	# libraries must come first.
+	if test -n "$dllsearchpath"; then
+	  $ECHO "\
+    # Add the dll search path components to the executable PATH
+    PATH=$dllsearchpath:\$PATH
+"
+	fi
+
+	# Export our shlibpath_var if we have one.
+	if test yes = "$shlibpath_overrides_runpath" && test -n "$shlibpath_var" && test -n "$temp_rpath"; then
+	  $ECHO "\
+    # Add our own library path to $shlibpath_var
+    $shlibpath_var=\"$temp_rpath\$$shlibpath_var\"
+
+    # Some systems cannot cope with colon-terminated $shlibpath_var
+    # The second colon is a workaround for a bug in BeOS R4 sed
+    $shlibpath_var=\`\$ECHO \"\$$shlibpath_var\" | $SED 's/::*\$//'\`
+
+    export $shlibpath_var
+"
+	fi
+
+	$ECHO "\
+    if test \"\$libtool_execute_magic\" != \"$magic\"; then
+      # Run the actual program with our arguments.
+      func_exec_program \${1+\"\$@\"}
+    fi
+  else
+    # The program doesn't exist.
+    \$ECHO \"\$0: error: '\$progdir/\$program' does not exist\" 1>&2
+    \$ECHO \"This script is just a wrapper for \$program.\" 1>&2
+    \$ECHO \"See the $PACKAGE documentation for more information.\" 1>&2
+    exit 1
+  fi
+fi\
+"
+}
+
+
+# func_emit_cwrapperexe_src
+# emit the source code for a wrapper executable on stdout
+# Must ONLY be called from within func_mode_link because
+# it depends on a number of variable set therein.
+func_emit_cwrapperexe_src ()
+{
+	cat <<EOF
+
+/* $cwrappersource - temporary wrapper executable for $objdir/$outputname
+   Generated by $PROGRAM (GNU $PACKAGE) $VERSION
+
+   The $output program cannot be directly executed until all the libtool
+   libraries that it depends on are installed.
+
+   This wrapper executable should never be moved out of the build directory.
+   If it is, it will not operate correctly.
+*/
+EOF
+	    cat <<"EOF"
+#ifdef _MSC_VER
+# define _CRT_SECURE_NO_DEPRECATE 1
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#ifdef _MSC_VER
+# include <direct.h>
+# include <process.h>
+# include <io.h>
+#else
+# include <unistd.h>
+# include <stdint.h>
+# ifdef __CYGWIN__
+#  include <io.h>
+# endif
+#endif
+#include <malloc.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#define STREQ(s1, s2) (strcmp ((s1), (s2)) == 0)
+
+/* declarations of non-ANSI functions */
+#if defined __MINGW32__
+# ifdef __STRICT_ANSI__
+int _putenv (const char *);
+# endif
+#elif defined __CYGWIN__
+# ifdef __STRICT_ANSI__
+char *realpath (const char *, char *);
+int putenv (char *);
+int setenv (const char *, const char *, int);
+# endif
+/* #elif defined other_platform || defined ... */
+#endif
+
+/* portability defines, excluding path handling macros */
+#if defined _MSC_VER
+# define setmode _setmode
+# define stat    _stat
+# define chmod   _chmod
+# define getcwd  _getcwd
+# define putenv  _putenv
+# define S_IXUSR _S_IEXEC
+#elif defined __MINGW32__
+# define setmode _setmode
+# define stat    _stat
+# define chmod   _chmod
+# define getcwd  _getcwd
+# define putenv  _putenv
+#elif defined __CYGWIN__
+# define HAVE_SETENV
+# define FOPEN_WB "wb"
+/* #elif defined other platforms ... */
+#endif
+
+#if defined PATH_MAX
+# define LT_PATHMAX PATH_MAX
+#elif defined MAXPATHLEN
+# define LT_PATHMAX MAXPATHLEN
+#else
+# define LT_PATHMAX 1024
+#endif
+
+#ifndef S_IXOTH
+# define S_IXOTH 0
+#endif
+#ifndef S_IXGRP
+# define S_IXGRP 0
+#endif
+
+/* path handling portability macros */
+#ifndef DIR_SEPARATOR
+# define DIR_SEPARATOR '/'
+# define PATH_SEPARATOR ':'
+#endif
+
+#if defined _WIN32 || defined __MSDOS__ || defined __DJGPP__ || \
+  defined __OS2__
+# define HAVE_DOS_BASED_FILE_SYSTEM
+# define FOPEN_WB "wb"
+# ifndef DIR_SEPARATOR_2
+#  define DIR_SEPARATOR_2 '\\'
+# endif
+# ifndef PATH_SEPARATOR_2
+#  define PATH_SEPARATOR_2 ';'
+# endif
+#endif
+
+#ifndef DIR_SEPARATOR_2
+# define IS_DIR_SEPARATOR(ch) ((ch) == DIR_SEPARATOR)
+#else /* DIR_SEPARATOR_2 */
+# define IS_DIR_SEPARATOR(ch) \
+	(((ch) == DIR_SEPARATOR) || ((ch) == DIR_SEPARATOR_2))
+#endif /* DIR_SEPARATOR_2 */
+
+#ifndef PATH_SEPARATOR_2
+# define IS_PATH_SEPARATOR(ch) ((ch) == PATH_SEPARATOR)
+#else /* PATH_SEPARATOR_2 */
+# define IS_PATH_SEPARATOR(ch) ((ch) == PATH_SEPARATOR_2)
+#endif /* PATH_SEPARATOR_2 */
+
+#ifndef FOPEN_WB
+# define FOPEN_WB "w"
+#endif
+#ifndef _O_BINARY
+# define _O_BINARY 0
+#endif
+
+#define XMALLOC(type, num)      ((type *) xmalloc ((num) * sizeof(type)))
+#define XFREE(stale) do { \
+  if (stale) { free (stale); stale = 0; } \
+} while (0)
+
+#if defined LT_DEBUGWRAPPER
+static int lt_debug = 1;
+#else
+static int lt_debug = 0;
+#endif
+
+const char *program_name = "libtool-wrapper"; /* in case xstrdup fails */
+
+void *xmalloc (size_t num);
+char *xstrdup (const char *string);
+const char *base_name (const char *name);
+char *find_executable (const char *wrapper);
+char *chase_symlinks (const char *pathspec);
+int make_executable (const char *path);
+int check_executable (const char *path);
+char *strendzap (char *str, const char *pat);
+void lt_debugprintf (const char *file, int line, const char *fmt, ...);
+void lt_fatal (const char *file, int line, const char *message, ...);
+static const char *nonnull (const char *s);
+static const char *nonempty (const char *s);
+void lt_setenv (const char *name, const char *value);
+char *lt_extend_str (const char *orig_value, const char *add, int to_end);
+void lt_update_exe_path (const char *name, const char *value);
+void lt_update_lib_path (const char *name, const char *value);
+char **prepare_spawn (char **argv);
+void lt_dump_script (FILE *f);
+EOF
+
+	    cat <<EOF
+#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 5)
+# define externally_visible volatile
+#else
+# define externally_visible __attribute__((externally_visible)) volatile
+#endif
+externally_visible const char * MAGIC_EXE = "$magic_exe";
+const char * LIB_PATH_VARNAME = "$shlibpath_var";
+EOF
+
+	    if test yes = "$shlibpath_overrides_runpath" && test -n "$shlibpath_var" && test -n "$temp_rpath"; then
+              func_to_host_path "$temp_rpath"
+	      cat <<EOF
+const char * LIB_PATH_VALUE   = "$func_to_host_path_result";
+EOF
+	    else
+	      cat <<"EOF"
+const char * LIB_PATH_VALUE   = "";
+EOF
+	    fi
+
+	    if test -n "$dllsearchpath"; then
+              func_to_host_path "$dllsearchpath:"
+	      cat <<EOF
+const char * EXE_PATH_VARNAME = "PATH";
+const char * EXE_PATH_VALUE   = "$func_to_host_path_result";
+EOF
+	    else
+	      cat <<"EOF"
+const char * EXE_PATH_VARNAME = "";
+const char * EXE_PATH_VALUE   = "";
+EOF
+	    fi
+
+	    if test yes = "$fast_install"; then
+	      cat <<EOF
+const char * TARGET_PROGRAM_NAME = "lt-$outputname"; /* hopefully, no .exe */
+EOF
+	    else
+	      cat <<EOF
+const char * TARGET_PROGRAM_NAME = "$outputname"; /* hopefully, no .exe */
+EOF
+	    fi
+
+
+	    cat <<"EOF"
+
+#define LTWRAPPER_OPTION_PREFIX         "--lt-"
+
+static const char *ltwrapper_option_prefix = LTWRAPPER_OPTION_PREFIX;
+static const char *dumpscript_opt       = LTWRAPPER_OPTION_PREFIX "dump-script";
+static const char *debug_opt            = LTWRAPPER_OPTION_PREFIX "debug";
+
+int
+main (int argc, char *argv[])
+{
+  char **newargz;
+  int  newargc;
+  char *tmp_pathspec;
+  char *actual_cwrapper_path;
+  char *actual_cwrapper_name;
+  char *target_name;
+  char *lt_argv_zero;
+  int rval = 127;
+
+  int i;
+
+  program_name = (char *) xstrdup (base_name (argv[0]));
+  newargz = XMALLOC (char *, (size_t) argc + 1);
+
+  /* very simple arg parsing; don't want to rely on getopt
+   * also, copy all non cwrapper options to newargz, except
+   * argz[0], which is handled differently
+   */
+  newargc=0;
+  for (i = 1; i < argc; i++)
+    {
+      if (STREQ (argv[i], dumpscript_opt))
+	{
+EOF
+	    case $host in
+	      *mingw* | *cygwin* )
+		# make stdout use "unix" line endings
+		echo "          setmode(1,_O_BINARY);"
+		;;
+	      esac
+
+	    cat <<"EOF"
+	  lt_dump_script (stdout);
+	  return 0;
+	}
+      if (STREQ (argv[i], debug_opt))
+	{
+          lt_debug = 1;
+          continue;
+	}
+      if (STREQ (argv[i], ltwrapper_option_prefix))
+        {
+          /* however, if there is an option in the LTWRAPPER_OPTION_PREFIX
+             namespace, but it is not one of the ones we know about and
+             have already dealt with, above (inluding dump-script), then
+             report an error. Otherwise, targets might begin to believe
+             they are allowed to use options in the LTWRAPPER_OPTION_PREFIX
+             namespace. The first time any user complains about this, we'll
+             need to make LTWRAPPER_OPTION_PREFIX a configure-time option
+             or a configure.ac-settable value.
+           */
+          lt_fatal (__FILE__, __LINE__,
+		    "unrecognized %s option: '%s'",
+                    ltwrapper_option_prefix, argv[i]);
+        }
+      /* otherwise ... */
+      newargz[++newargc] = xstrdup (argv[i]);
+    }
+  newargz[++newargc] = NULL;
+
+EOF
+	    cat <<EOF
+  /* The GNU banner must be the first non-error debug message */
+  lt_debugprintf (__FILE__, __LINE__, "libtool wrapper (GNU $PACKAGE) $VERSION\n");
+EOF
+	    cat <<"EOF"
+  lt_debugprintf (__FILE__, __LINE__, "(main) argv[0]: %s\n", argv[0]);
+  lt_debugprintf (__FILE__, __LINE__, "(main) program_name: %s\n", program_name);
+
+  tmp_pathspec = find_executable (argv[0]);
+  if (tmp_pathspec == NULL)
+    lt_fatal (__FILE__, __LINE__, "couldn't find %s", argv[0]);
+  lt_debugprintf (__FILE__, __LINE__,
+                  "(main) found exe (before symlink chase) at: %s\n",
+		  tmp_pathspec);
+
+  actual_cwrapper_path = chase_symlinks (tmp_pathspec);
+  lt_debugprintf (__FILE__, __LINE__,
+                  "(main) found exe (after symlink chase) at: %s\n",
+		  actual_cwrapper_path);
+  XFREE (tmp_pathspec);
+
+  actual_cwrapper_name = xstrdup (base_name (actual_cwrapper_path));
+  strendzap (actual_cwrapper_path, actual_cwrapper_name);
+
+  /* wrapper name transforms */
+  strendzap (actual_cwrapper_name, ".exe");
+  tmp_pathspec = lt_extend_str (actual_cwrapper_name, ".exe", 1);
+  XFREE (actual_cwrapper_name);
+  actual_cwrapper_name = tmp_pathspec;
+  tmp_pathspec = 0;
+
+  /* target_name transforms -- use actual target program name; might have lt- prefix */
+  target_name = xstrdup (base_name (TARGET_PROGRAM_NAME));
+  strendzap (target_name, ".exe");
+  tmp_pathspec = lt_extend_str (target_name, ".exe", 1);
+  XFREE (target_name);
+  target_name = tmp_pathspec;
+  tmp_pathspec = 0;
+
+  lt_debugprintf (__FILE__, __LINE__,
+		  "(main) libtool target name: %s\n",
+		  target_name);
+EOF
+
+	    cat <<EOF
+  newargz[0] =
+    XMALLOC (char, (strlen (actual_cwrapper_path) +
+		    strlen ("$objdir") + 1 + strlen (actual_cwrapper_name) + 1));
+  strcpy (newargz[0], actual_cwrapper_path);
+  strcat (newargz[0], "$objdir");
+  strcat (newargz[0], "/");
+EOF
+
+	    cat <<"EOF"
+  /* stop here, and copy so we don't have to do this twice */
+  tmp_pathspec = xstrdup (newargz[0]);
+
+  /* do NOT want the lt- prefix here, so use actual_cwrapper_name */
+  strcat (newargz[0], actual_cwrapper_name);
+
+  /* DO want the lt- prefix here if it exists, so use target_name */
+  lt_argv_zero = lt_extend_str (tmp_pathspec, target_name, 1);
+  XFREE (tmp_pathspec);
+  tmp_pathspec = NULL;
+EOF
+
+	    case $host_os in
+	      mingw*)
+	    cat <<"EOF"
+  {
+    char* p;
+    while ((p = strchr (newargz[0], '\\')) != NULL)
+      {
+	*p = '/';
+      }
+    while ((p = strchr (lt_argv_zero, '\\')) != NULL)
+      {
+	*p = '/';
+      }
+  }
+EOF
+	    ;;
+	    esac
+
+	    cat <<"EOF"
+  XFREE (target_name);
+  XFREE (actual_cwrapper_path);
+  XFREE (actual_cwrapper_name);
+
+  lt_setenv ("BIN_SH", "xpg4"); /* for Tru64 */
+  lt_setenv ("DUALCASE", "1");  /* for MSK sh */
+  /* Update the DLL searchpath.  EXE_PATH_VALUE ($dllsearchpath) must
+     be prepended before (that is, appear after) LIB_PATH_VALUE ($temp_rpath)
+     because on Windows, both *_VARNAMEs are PATH but uninstalled
+     libraries must come first. */
+  lt_update_exe_path (EXE_PATH_VARNAME, EXE_PATH_VALUE);
+  lt_update_lib_path (LIB_PATH_VARNAME, LIB_PATH_VALUE);
+
+  lt_debugprintf (__FILE__, __LINE__, "(main) lt_argv_zero: %s\n",
+		  nonnull (lt_argv_zero));
+  for (i = 0; i < newargc; i++)
+    {
+      lt_debugprintf (__FILE__, __LINE__, "(main) newargz[%d]: %s\n",
+		      i, nonnull (newargz[i]));
+    }
+
+EOF
+
+	    case $host_os in
+	      mingw*)
+		cat <<"EOF"
+  /* execv doesn't actually work on mingw as expected on unix */
+  newargz = prepare_spawn (newargz);
+  rval = (int) _spawnv (_P_WAIT, lt_argv_zero, (const char * const *) newargz);
+  if (rval == -1)
+    {
+      /* failed to start process */
+      lt_debugprintf (__FILE__, __LINE__,
+		      "(main) failed to launch target \"%s\": %s\n",
+		      lt_argv_zero, nonnull (strerror (errno)));
+      return 127;
+    }
+  return rval;
+EOF
+		;;
+	      *)
+		cat <<"EOF"
+  execv (lt_argv_zero, newargz);
+  return rval; /* =127, but avoids unused variable warning */
+EOF
+		;;
+	    esac
+
+	    cat <<"EOF"
+}
+
+void *
+xmalloc (size_t num)
+{
+  void *p = (void *) malloc (num);
+  if (!p)
+    lt_fatal (__FILE__, __LINE__, "memory exhausted");
+
+  return p;
+}
+
+char *
+xstrdup (const char *string)
+{
+  return string ? strcpy ((char *) xmalloc (strlen (string) + 1),
+			  string) : NULL;
+}
+
+const char *
+base_name (const char *name)
+{
+  const char *base;
+
+#if defined HAVE_DOS_BASED_FILE_SYSTEM
+  /* Skip over the disk name in MSDOS pathnames. */
+  if (isalpha ((unsigned char) name[0]) && name[1] == ':')
+    name += 2;
+#endif
+
+  for (base = name; *name; name++)
+    if (IS_DIR_SEPARATOR (*name))
+      base = name + 1;
+  return base;
+}
+
+int
+check_executable (const char *path)
+{
+  struct stat st;
+
+  lt_debugprintf (__FILE__, __LINE__, "(check_executable): %s\n",
+                  nonempty (path));
+  if ((!path) || (!*path))
+    return 0;
+
+  if ((stat (path, &st) >= 0)
+      && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
+    return 1;
+  else
+    return 0;
+}
+
+int
+make_executable (const char *path)
+{
+  int rval = 0;
+  struct stat st;
+
+  lt_debugprintf (__FILE__, __LINE__, "(make_executable): %s\n",
+                  nonempty (path));
+  if ((!path) || (!*path))
+    return 0;
+
+  if (stat (path, &st) >= 0)
+    {
+      rval = chmod (path, st.st_mode | S_IXOTH | S_IXGRP | S_IXUSR);
+    }
+  return rval;
+}
+
+/* Searches for the full path of the wrapper.  Returns
+   newly allocated full path name if found, NULL otherwise
+   Does not chase symlinks, even on platforms that support them.
+*/
+char *
+find_executable (const char *wrapper)
+{
+  int has_slash = 0;
+  const char *p;
+  const char *p_next;
+  /* static buffer for getcwd */
+  char tmp[LT_PATHMAX + 1];
+  size_t tmp_len;
+  char *concat_name;
+
+  lt_debugprintf (__FILE__, __LINE__, "(find_executable): %s\n",
+                  nonempty (wrapper));
+
+  if ((wrapper == NULL) || (*wrapper == '\0'))
+    return NULL;
+
+  /* Absolute path? */
+#if defined HAVE_DOS_BASED_FILE_SYSTEM
+  if (isalpha ((unsigned char) wrapper[0]) && wrapper[1] == ':')
+    {
+      concat_name = xstrdup (wrapper);
+      if (check_executable (concat_name))
+	return concat_name;
+      XFREE (concat_name);
+    }
+  else
+    {
+#endif
+      if (IS_DIR_SEPARATOR (wrapper[0]))
+	{
+	  concat_name = xstrdup (wrapper);
+	  if (check_executable (concat_name))
+	    return concat_name;
+	  XFREE (concat_name);
+	}
+#if defined HAVE_DOS_BASED_FILE_SYSTEM
+    }
+#endif
+
+  for (p = wrapper; *p; p++)
+    if (*p == '/')
+      {
+	has_slash = 1;
+	break;
+      }
+  if (!has_slash)
+    {
+      /* no slashes; search PATH */
+      const char *path = getenv ("PATH");
+      if (path != NULL)
+	{
+	  for (p = path; *p; p = p_next)
+	    {
+	      const char *q;
+	      size_t p_len;
+	      for (q = p; *q; q++)
+		if (IS_PATH_SEPARATOR (*q))
+		  break;
+	      p_len = (size_t) (q - p);
+	      p_next = (*q == '\0' ? q : q + 1);
+	      if (p_len == 0)
+		{
+		  /* empty path: current directory */
+		  if (getcwd (tmp, LT_PATHMAX) == NULL)
+		    lt_fatal (__FILE__, __LINE__, "getcwd failed: %s",
+                              nonnull (strerror (errno)));
+		  tmp_len = strlen (tmp);
+		  concat_name =
+		    XMALLOC (char, tmp_len + 1 + strlen (wrapper) + 1);
+		  memcpy (concat_name, tmp, tmp_len);
+		  concat_name[tmp_len] = '/';
+		  strcpy (concat_name + tmp_len + 1, wrapper);
+		}
+	      else
+		{
+		  concat_name =
+		    XMALLOC (char, p_len + 1 + strlen (wrapper) + 1);
+		  memcpy (concat_name, p, p_len);
+		  concat_name[p_len] = '/';
+		  strcpy (concat_name + p_len + 1, wrapper);
+		}
+	      if (check_executable (concat_name))
+		return concat_name;
+	      XFREE (concat_name);
+	    }
+	}
+      /* not found in PATH; assume curdir */
+    }
+  /* Relative path | not found in path: prepend cwd */
+  if (getcwd (tmp, LT_PATHMAX) == NULL)
+    lt_fatal (__FILE__, __LINE__, "getcwd failed: %s",
+              nonnull (strerror (errno)));
+  tmp_len = strlen (tmp);
+  concat_name = XMALLOC (char, tmp_len + 1 + strlen (wrapper) + 1);
+  memcpy (concat_name, tmp, tmp_len);
+  concat_name[tmp_len] = '/';
+  strcpy (concat_name + tmp_len + 1, wrapper);
+
+  if (check_executable (concat_name))
+    return concat_name;
+  XFREE (concat_name);
+  return NULL;
+}
+
+char *
+chase_symlinks (const char *pathspec)
+{
+#ifndef S_ISLNK
+  return xstrdup (pathspec);
+#else
+  char buf[LT_PATHMAX];
+  struct stat s;
+  char *tmp_pathspec = xstrdup (pathspec);
+  char *p;
+  int has_symlinks = 0;
+  while (strlen (tmp_pathspec) && !has_symlinks)
+    {
+      lt_debugprintf (__FILE__, __LINE__,
+		      "checking path component for symlinks: %s\n",
+		      tmp_pathspec);
+      if (lstat (tmp_pathspec, &s) == 0)
+	{
+	  if (S_ISLNK (s.st_mode) != 0)
+	    {
+	      has_symlinks = 1;
+	      break;
+	    }
+
+	  /* search backwards for last DIR_SEPARATOR */
+	  p = tmp_pathspec + strlen (tmp_pathspec) - 1;
+	  while ((p > tmp_pathspec) && (!IS_DIR_SEPARATOR (*p)))
+	    p--;
+	  if ((p == tmp_pathspec) && (!IS_DIR_SEPARATOR (*p)))
+	    {
+	      /* no more DIR_SEPARATORS left */
+	      break;
+	    }
+	  *p = '\0';
+	}
+      else
+	{
+	  lt_fatal (__FILE__, __LINE__,
+		    "error accessing file \"%s\": %s",
+		    tmp_pathspec, nonnull (strerror (errno)));
+	}
+    }
+  XFREE (tmp_pathspec);
+
+  if (!has_symlinks)
+    {
+      return xstrdup (pathspec);
+    }
+
+  tmp_pathspec = realpath (pathspec, buf);
+  if (tmp_pathspec == 0)
+    {
+      lt_fatal (__FILE__, __LINE__,
+		"could not follow symlinks for %s", pathspec);
+    }
+  return xstrdup (tmp_pathspec);
+#endif
+}
+
+char *
+strendzap (char *str, const char *pat)
+{
+  size_t len, patlen;
+
+  assert (str != NULL);
+  assert (pat != NULL);
+
+  len = strlen (str);
+  patlen = strlen (pat);
+
+  if (patlen <= len)
+    {
+      str += len - patlen;
+      if (STREQ (str, pat))
+	*str = '\0';
+    }
+  return str;
+}
+
+void
+lt_debugprintf (const char *file, int line, const char *fmt, ...)
+{
+  va_list args;
+  if (lt_debug)
+    {
+      (void) fprintf (stderr, "%s:%s:%d: ", program_name, file, line);
+      va_start (args, fmt);
+      (void) vfprintf (stderr, fmt, args);
+      va_end (args);
+    }
+}
+
+static void
+lt_error_core (int exit_status, const char *file,
+	       int line, const char *mode,
+	       const char *message, va_list ap)
+{
+  fprintf (stderr, "%s:%s:%d: %s: ", program_name, file, line, mode);
+  vfprintf (stderr, message, ap);
+  fprintf (stderr, ".\n");
+
+  if (exit_status >= 0)
+    exit (exit_status);
+}
+
+void
+lt_fatal (const char *file, int line, const char *message, ...)
+{
+  va_list ap;
+  va_start (ap, message);
+  lt_error_core (EXIT_FAILURE, file, line, "FATAL", message, ap);
+  va_end (ap);
+}
+
+static const char *
+nonnull (const char *s)
+{
+  return s ? s : "(null)";
+}
+
+static const char *
+nonempty (const char *s)
+{
+  return (s && !*s) ? "(empty)" : nonnull (s);
+}
+
+void
+lt_setenv (const char *name, const char *value)
+{
+  lt_debugprintf (__FILE__, __LINE__,
+		  "(lt_setenv) setting '%s' to '%s'\n",
+                  nonnull (name), nonnull (value));
+  {
+#ifdef HAVE_SETENV
+    /* always make a copy, for consistency with !HAVE_SETENV */
+    char *str = xstrdup (value);
+    setenv (name, str, 1);
+#else
+    size_t len = strlen (name) + 1 + strlen (value) + 1;
+    char *str = XMALLOC (char, len);
+    sprintf (str, "%s=%s", name, value);
+    if (putenv (str) != EXIT_SUCCESS)
+      {
+        XFREE (str);
+      }
+#endif
+  }
+}
+
+char *
+lt_extend_str (const char *orig_value, const char *add, int to_end)
+{
+  char *new_value;
+  if (orig_value && *orig_value)
+    {
+      size_t orig_value_len = strlen (orig_value);
+      size_t add_len = strlen (add);
+      new_value = XMALLOC (char, add_len + orig_value_len + 1);
+      if (to_end)
+        {
+          strcpy (new_value, orig_value);
+          strcpy (new_value + orig_value_len, add);
+        }
+      else
+        {
+          strcpy (new_value, add);
+          strcpy (new_value + add_len, orig_value);
+        }
+    }
+  else
+    {
+      new_value = xstrdup (add);
+    }
+  return new_value;
+}
+
+void
+lt_update_exe_path (const char *name, const char *value)
+{
+  lt_debugprintf (__FILE__, __LINE__,
+		  "(lt_update_exe_path) modifying '%s' by prepending '%s'\n",
+                  nonnull (name), nonnull (value));
+
+  if (name && *name && value && *value)
+    {
+      char *new_value = lt_extend_str (getenv (name), value, 0);
+      /* some systems can't cope with a ':'-terminated path #' */
+      size_t len = strlen (new_value);
+      while ((len > 0) && IS_PATH_SEPARATOR (new_value[len-1]))
+        {
+          new_value[--len] = '\0';
+        }
+      lt_setenv (name, new_value);
+      XFREE (new_value);
+    }
+}
+
+void
+lt_update_lib_path (const char *name, const char *value)
+{
+  lt_debugprintf (__FILE__, __LINE__,
+		  "(lt_update_lib_path) modifying '%s' by prepending '%s'\n",
+                  nonnull (name), nonnull (value));
+
+  if (name && *name && value && *value)
+    {
+      char *new_value = lt_extend_str (getenv (name), value, 0);
+      lt_setenv (name, new_value);
+      XFREE (new_value);
+    }
+}
+
+EOF
+	    case $host_os in
+	      mingw*)
+		cat <<"EOF"
+
+/* Prepares an argument vector before calling spawn().
+   Note that spawn() does not by itself call the command interpreter
+     (getenv ("COMSPEC") != NULL ? getenv ("COMSPEC") :
+      ({ OSVERSIONINFO v; v.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
+         GetVersionEx(&v);
+         v.dwPlatformId == VER_PLATFORM_WIN32_NT;
+      }) ? "cmd.exe" : "command.com").
+   Instead it simply concatenates the arguments, separated by ' ', and calls
+   CreateProcess().  We must quote the arguments since Win32 CreateProcess()
+   interprets characters like ' ', '\t', '\\', '"' (but not '<' and '>') in a
+   special way:
+   - Space and tab are interpreted as delimiters. They are not treated as
+     delimiters if they are surrounded by double quotes: "...".
+   - Unescaped double quotes are removed from the input. Their only effect is
+     that within double quotes, space and tab are treated like normal
+     characters.
+   - Backslashes not followed by double quotes are not special.
+   - But 2*n+1 backslashes followed by a double quote become
+     n backslashes followed by a double quote (n >= 0):
+       \" -> "
+       \\\" -> \"
+       \\\\\" -> \\"
+ */
+#define SHELL_SPECIAL_CHARS "\"\\ \001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037"
+#define SHELL_SPACE_CHARS " \001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037"
+char **
+prepare_spawn (char **argv)
+{
+  size_t argc;
+  char **new_argv;
+  size_t i;
+
+  /* Count number of arguments.  */
+  for (argc = 0; argv[argc] != NULL; argc++)
+    ;
+
+  /* Allocate new argument vector.  */
+  new_argv = XMALLOC (char *, argc + 1);
+
+  /* Put quoted arguments into the new argument vector.  */
+  for (i = 0; i < argc; i++)
+    {
+      const char *string = argv[i];
+
+      if (string[0] == '\0')
+	new_argv[i] = xstrdup ("\"\"");
+      else if (strpbrk (string, SHELL_SPECIAL_CHARS) != NULL)
+	{
+	  int quote_around = (strpbrk (string, SHELL_SPACE_CHARS) != NULL);
+	  size_t length;
+	  unsigned int backslashes;
+	  const char *s;
+	  char *quoted_string;
+	  char *p;
+
+	  length = 0;
+	  backslashes = 0;
+	  if (quote_around)
+	    length++;
+	  for (s = string; *s != '\0'; s++)
+	    {
+	      char c = *s;
+	      if (c == '"')
+		length += backslashes + 1;
+	      length++;
+	      if (c == '\\')
+		backslashes++;
+	      else
+		backslashes = 0;
+	    }
+	  if (quote_around)
+	    length += backslashes + 1;
+
+	  quoted_string = XMALLOC (char, length + 1);
+
+	  p = quoted_string;
+	  backslashes = 0;
+	  if (quote_around)
+	    *p++ = '"';
+	  for (s = string; *s != '\0'; s++)
+	    {
+	      char c = *s;
+	      if (c == '"')
+		{
+		  unsigned int j;
+		  for (j = backslashes + 1; j > 0; j--)
+		    *p++ = '\\';
+		}
+	      *p++ = c;
+	      if (c == '\\')
+		backslashes++;
+	      else
+		backslashes = 0;
+	    }
+	  if (quote_around)
+	    {
+	      unsigned int j;
+	      for (j = backslashes; j > 0; j--)
+		*p++ = '\\';
+	      *p++ = '"';
+	    }
+	  *p = '\0';
+
+	  new_argv[i] = quoted_string;
+	}
+      else
+	new_argv[i] = (char *) string;
+    }
+  new_argv[argc] = NULL;
+
+  return new_argv;
+}
+EOF
+		;;
+	    esac
+
+            cat <<"EOF"
+void lt_dump_script (FILE* f)
+{
+EOF
+	    func_emit_wrapper yes |
+	      $SED -n -e '
+s/^\(.\{79\}\)\(..*\)/\1\
+\2/
+h
+s/\([\\"]\)/\\\1/g
+s/$/\\n/
+s/\([^\n]*\).*/  fputs ("\1", f);/p
+g
+D'
+            cat <<"EOF"
+}
+EOF
+}
+# end: func_emit_cwrapperexe_src
+
+# func_win32_import_lib_p ARG
+# True if ARG is an import lib, as indicated by $file_magic_cmd
+func_win32_import_lib_p ()
+{
+    $debug_cmd
+
+    case `eval $file_magic_cmd \"\$1\" 2>/dev/null | $SED -e 10q` in
+    *import*) : ;;
+    *) false ;;
+    esac
+}
+
+# func_suncc_cstd_abi
+# !!ONLY CALL THIS FOR SUN CC AFTER $compile_command IS FULLY EXPANDED!!
+# Several compiler flags select an ABI that is incompatible with the
+# Cstd library. Avoid specifying it if any are in CXXFLAGS.
+func_suncc_cstd_abi ()
+{
+    $debug_cmd
+
+    case " $compile_command " in
+    *" -compat=g "*|*\ -std=c++[0-9][0-9]\ *|*" -library=stdcxx4 "*|*" -library=stlport4 "*)
+      suncc_use_cstd_abi=no
+      ;;
+    *)
+      suncc_use_cstd_abi=yes
+      ;;
+    esac
+}
+
+# func_mode_link arg...
+func_mode_link ()
+{
+    $debug_cmd
+
+    case $host in
+    *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-cegcc*)
+      # It is impossible to link a dll without this setting, and
+      # we shouldn't force the makefile maintainer to figure out
+      # what system we are compiling for in order to pass an extra
+      # flag for every libtool invocation.
+      # allow_undefined=no
+
+      # FIXME: Unfortunately, there are problems with the above when trying
+      # to make a dll that has undefined symbols, in which case not
+      # even a static library is built.  For now, we need to specify
+      # -no-undefined on the libtool link line when we can be certain
+      # that all symbols are satisfied, otherwise we get a static library.
+      allow_undefined=yes
+      ;;
+    *)
+      allow_undefined=yes
+      ;;
+    esac
+    libtool_args=$nonopt
+    base_compile="$nonopt $@"
+    compile_command=$nonopt
+    finalize_command=$nonopt
+
+    compile_rpath=
+    finalize_rpath=
+    compile_shlibpath=
+    finalize_shlibpath=
+    convenience=
+    old_convenience=
+    deplibs=
+    old_deplibs=
+    compiler_flags=
+    linker_flags=
+    dllsearchpath=
+    lib_search_path=`pwd`
+    inst_prefix_dir=
+    new_inherited_linker_flags=
+
+    avoid_version=no
+    bindir=
+    dlfiles=
+    dlprefiles=
+    dlself=no
+    export_dynamic=no
+    export_symbols=
+    export_symbols_regex=
+    generated=
+    libobjs=
+    ltlibs=
+    module=no
+    no_install=no
+    objs=
+    os2dllname=
+    non_pic_objects=
+    precious_files_regex=
+    prefer_static_libs=no
+    preload=false
+    prev=
+    prevarg=
+    release=
+    rpath=
+    xrpath=
+    perm_rpath=
+    temp_rpath=
+    thread_safe=no
+    vinfo=
+    vinfo_number=no
+    weak_libs=
+    single_module=$wl-single_module
+    func_infer_tag $base_compile
+
+    # We need to know -static, to get the right output filenames.
+    for arg
+    do
+      case $arg in
+      -shared)
+	test yes != "$build_libtool_libs" \
+	  && func_fatal_configuration "cannot build a shared library"
+	build_old_libs=no
+	break
+	;;
+      -all-static | -static | -static-libtool-libs)
+	case $arg in
+	-all-static)
+	  if test yes = "$build_libtool_libs" && test -z "$link_static_flag"; then
+	    func_warning "complete static linking is impossible in this configuration"
+	  fi
+	  if test -n "$link_static_flag"; then
+	    dlopen_self=$dlopen_self_static
+	  fi
+	  prefer_static_libs=yes
+	  ;;
+	-static)
+	  if test -z "$pic_flag" && test -n "$link_static_flag"; then
+	    dlopen_self=$dlopen_self_static
+	  fi
+	  prefer_static_libs=built
+	  ;;
+	-static-libtool-libs)
+	  if test -z "$pic_flag" && test -n "$link_static_flag"; then
+	    dlopen_self=$dlopen_self_static
+	  fi
+	  prefer_static_libs=yes
+	  ;;
+	esac
+	build_libtool_libs=no
+	build_old_libs=yes
+	break
+	;;
+      esac
+    done
+
+    # See if our shared archives depend on static archives.
+    test -n "$old_archive_from_new_cmds" && build_old_libs=yes
+
+    # Go through the arguments, transforming them on the way.
+    while test "$#" -gt 0; do
+      arg=$1
+      shift
+      func_quote_arg pretty,unquoted "$arg"
+      qarg=$func_quote_arg_unquoted_result
+      func_append libtool_args " $func_quote_arg_result"
+
+      # If the previous option needs an argument, assign it.
+      if test -n "$prev"; then
+	case $prev in
+	output)
+	  func_append compile_command " @OUTPUT@"
+	  func_append finalize_command " @OUTPUT@"
+	  ;;
+	esac
+
+	case $prev in
+	bindir)
+	  bindir=$arg
+	  prev=
+	  continue
+	  ;;
+	dlfiles|dlprefiles)
+	  $preload || {
+	    # Add the symbol object into the linking commands.
+	    func_append compile_command " @SYMFILE@"
+	    func_append finalize_command " @SYMFILE@"
+	    preload=:
+	  }
+	  case $arg in
+	  *.la | *.lo) ;;  # We handle these cases below.
+	  force)
+	    if test no = "$dlself"; then
+	      dlself=needless
+	      export_dynamic=yes
+	    fi
+	    prev=
+	    continue
+	    ;;
+	  self)
+	    if test dlprefiles = "$prev"; then
+	      dlself=yes
+	    elif test dlfiles = "$prev" && test yes != "$dlopen_self"; then
+	      dlself=yes
+	    else
+	      dlself=needless
+	      export_dynamic=yes
+	    fi
+	    prev=
+	    continue
+	    ;;
+	  *)
+	    if test dlfiles = "$prev"; then
+	      func_append dlfiles " $arg"
+	    else
+	      func_append dlprefiles " $arg"
+	    fi
+	    prev=
+	    continue
+	    ;;
+	  esac
+	  ;;
+	expsyms)
+	  export_symbols=$arg
+	  test -f "$arg" \
+	    || func_fatal_error "symbol file '$arg' does not exist"
+	  prev=
+	  continue
+	  ;;
+	expsyms_regex)
+	  export_symbols_regex=$arg
+	  prev=
+	  continue
+	  ;;
+	framework)
+	  case $host in
+	    *-*-darwin*)
+	      case "$deplibs " in
+		*" $qarg.ltframework "*) ;;
+		*) func_append deplibs " $qarg.ltframework" # this is fixed later
+		   ;;
+	      esac
+	      ;;
+	  esac
+	  prev=
+	  continue
+	  ;;
+	inst_prefix)
+	  inst_prefix_dir=$arg
+	  prev=
+	  continue
+	  ;;
+	mllvm)
+	  # Clang does not use LLVM to link, so we can simply discard any
+	  # '-mllvm $arg' options when doing the link step.
+	  prev=
+	  continue
+	  ;;
+	objectlist)
+	  if test -f "$arg"; then
+	    save_arg=$arg
+	    moreargs=
+	    for fil in `cat "$save_arg"`
+	    do
+#	      func_append moreargs " $fil"
+	      arg=$fil
+	      # A libtool-controlled object.
+
+	      # Check to see that this really is a libtool object.
+	      if func_lalib_unsafe_p "$arg"; then
+		pic_object=
+		non_pic_object=
+
+		# Read the .lo file
+		func_source "$arg"
+
+		if test -z "$pic_object" ||
+		   test -z "$non_pic_object" ||
+		   test none = "$pic_object" &&
+		   test none = "$non_pic_object"; then
+		  func_fatal_error "cannot find name of object for '$arg'"
+		fi
+
+		# Extract subdirectory from the argument.
+		func_dirname "$arg" "/" ""
+		xdir=$func_dirname_result
+
+		if test none != "$pic_object"; then
+		  # Prepend the subdirectory the object is found in.
+		  pic_object=$xdir$pic_object
+
+		  if test dlfiles = "$prev"; then
+		    if test yes = "$build_libtool_libs" && test yes = "$dlopen_support"; then
+		      func_append dlfiles " $pic_object"
+		      prev=
+		      continue
+		    else
+		      # If libtool objects are unsupported, then we need to preload.
+		      prev=dlprefiles
+		    fi
+		  fi
+
+		  # CHECK ME:  I think I busted this.  -Ossama
+		  if test dlprefiles = "$prev"; then
+		    # Preload the old-style object.
+		    func_append dlprefiles " $pic_object"
+		    prev=
+		  fi
+
+		  # A PIC object.
+		  func_append libobjs " $pic_object"
+		  arg=$pic_object
+		fi
+
+		# Non-PIC object.
+		if test none != "$non_pic_object"; then
+		  # Prepend the subdirectory the object is found in.
+		  non_pic_object=$xdir$non_pic_object
+
+		  # A standard non-PIC object
+		  func_append non_pic_objects " $non_pic_object"
+		  if test -z "$pic_object" || test none = "$pic_object"; then
+		    arg=$non_pic_object
+		  fi
+		else
+		  # If the PIC object exists, use it instead.
+		  # $xdir was prepended to $pic_object above.
+		  non_pic_object=$pic_object
+		  func_append non_pic_objects " $non_pic_object"
+		fi
+	      else
+		# Only an error if not doing a dry-run.
+		if $opt_dry_run; then
+		  # Extract subdirectory from the argument.
+		  func_dirname "$arg" "/" ""
+		  xdir=$func_dirname_result
+
+		  func_lo2o "$arg"
+		  pic_object=$xdir$objdir/$func_lo2o_result
+		  non_pic_object=$xdir$func_lo2o_result
+		  func_append libobjs " $pic_object"
+		  func_append non_pic_objects " $non_pic_object"
+	        else
+		  func_fatal_error "'$arg' is not a valid libtool object"
+		fi
+	      fi
+	    done
+	  else
+	    func_fatal_error "link input file '$arg' does not exist"
+	  fi
+	  arg=$save_arg
+	  prev=
+	  continue
+	  ;;
+	os2dllname)
+	  os2dllname=$arg
+	  prev=
+	  continue
+	  ;;
+	precious_regex)
+	  precious_files_regex=$arg
+	  prev=
+	  continue
+	  ;;
+	release)
+	  release=-$arg
+	  prev=
+	  continue
+	  ;;
+	rpath | xrpath)
+	  # We need an absolute path.
+	  case $arg in
+	  [\\/]* | [A-Za-z]:[\\/]*) ;;
+	  *)
+	    func_fatal_error "only absolute run-paths are allowed"
+	    ;;
+	  esac
+	  if test rpath = "$prev"; then
+	    case "$rpath " in
+	    *" $arg "*) ;;
+	    *) func_append rpath " $arg" ;;
+	    esac
+	  else
+	    case "$xrpath " in
+	    *" $arg "*) ;;
+	    *) func_append xrpath " $arg" ;;
+	    esac
+	  fi
+	  prev=
+	  continue
+	  ;;
+	shrext)
+	  shrext_cmds=$arg
+	  prev=
+	  continue
+	  ;;
+	weak)
+	  func_append weak_libs " $arg"
+	  prev=
+	  continue
+	  ;;
+	xassembler)
+	  func_append compiler_flags " -Xassembler $qarg"
+	  prev=
+	  func_append compile_command " -Xassembler $qarg"
+	  func_append finalize_command " -Xassembler $qarg"
+	  continue
+	  ;;
+	xcclinker)
+	  func_append linker_flags " $qarg"
+	  func_append compiler_flags " $qarg"
+	  prev=
+	  func_append compile_command " $qarg"
+	  func_append finalize_command " $qarg"
+	  continue
+	  ;;
+	xcompiler)
+	  func_append compiler_flags " $qarg"
+	  prev=
+	  func_append compile_command " $qarg"
+	  func_append finalize_command " $qarg"
+	  continue
+	  ;;
+	xlinker)
+	  func_append linker_flags " $qarg"
+	  func_append compiler_flags " $wl$qarg"
+	  prev=
+	  func_append compile_command " $wl$qarg"
+	  func_append finalize_command " $wl$qarg"
+	  continue
+	  ;;
+	*)
+	  eval "$prev=\"\$arg\""
+	  prev=
+	  continue
+	  ;;
+	esac
+      fi # test -n "$prev"
+
+      prevarg=$arg
+
+      case $arg in
+      -all-static)
+	if test -n "$link_static_flag"; then
+	  # See comment for -static flag below, for more details.
+	  func_append compile_command " $link_static_flag"
+	  func_append finalize_command " $link_static_flag"
+	fi
+	continue
+	;;
+
+      -allow-undefined)
+	# FIXME: remove this flag sometime in the future.
+	func_fatal_error "'-allow-undefined' must not be used because it is the default"
+	;;
+
+      -avoid-version)
+	avoid_version=yes
+	continue
+	;;
+
+      -bindir)
+	prev=bindir
+	continue
+	;;
+
+      -dlopen)
+	prev=dlfiles
+	continue
+	;;
+
+      -dlpreopen)
+	prev=dlprefiles
+	continue
+	;;
+
+      -export-dynamic)
+	export_dynamic=yes
+	continue
+	;;
+
+      -export-symbols | -export-symbols-regex)
+	if test -n "$export_symbols" || test -n "$export_symbols_regex"; then
+	  func_fatal_error "more than one -exported-symbols argument is not allowed"
+	fi
+	if test X-export-symbols = "X$arg"; then
+	  prev=expsyms
+	else
+	  prev=expsyms_regex
+	fi
+	continue
+	;;
+
+      -framework)
+	prev=framework
+	continue
+	;;
+
+      -inst-prefix-dir)
+	prev=inst_prefix
+	continue
+	;;
+
+      # The native IRIX linker understands -LANG:*, -LIST:* and -LNO:*
+      # so, if we see these flags be careful not to treat them like -L
+      -L[A-Z][A-Z]*:*)
+	case $with_gcc/$host in
+	no/*-*-irix* | /*-*-irix*)
+	  func_append compile_command " $arg"
+	  func_append finalize_command " $arg"
+	  ;;
+	esac
+	continue
+	;;
+
+      -L*)
+	func_stripname "-L" '' "$arg"
+	if test -z "$func_stripname_result"; then
+	  if test "$#" -gt 0; then
+	    func_fatal_error "require no space between '-L' and '$1'"
+	  else
+	    func_fatal_error "need path for '-L' option"
+	  fi
+	fi
+	func_resolve_sysroot "$func_stripname_result"
+	dir=$func_resolve_sysroot_result
+	# We need an absolute path.
+	case $dir in
+	[\\/]* | [A-Za-z]:[\\/]*) ;;
+	*)
+	  absdir=`cd "$dir" && pwd`
+	  test -z "$absdir" && \
+	    func_fatal_error "cannot determine absolute directory name of '$dir'"
+	  dir=$absdir
+	  ;;
+	esac
+	case "$deplibs " in
+	*" -L$dir "* | *" $arg "*)
+	  # Will only happen for absolute or sysroot arguments
+	  ;;
+	*)
+	  # Preserve sysroot, but never include relative directories
+	  case $dir in
+	    [\\/]* | [A-Za-z]:[\\/]* | =*) func_append deplibs " $arg" ;;
+	    *) func_append deplibs " -L$dir" ;;
+	  esac
+	  func_append lib_search_path " $dir"
+	  ;;
+	esac
+	case $host in
+	*-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-cegcc*)
+	  testbindir=`$ECHO "$dir" | $SED 's*/lib$*/bin*'`
+	  case :$dllsearchpath: in
+	  *":$dir:"*) ;;
+	  ::) dllsearchpath=$dir;;
+	  *) func_append dllsearchpath ":$dir";;
+	  esac
+	  case :$dllsearchpath: in
+	  *":$testbindir:"*) ;;
+	  ::) dllsearchpath=$testbindir;;
+	  *) func_append dllsearchpath ":$testbindir";;
+	  esac
+	  ;;
+	esac
+	continue
+	;;
+
+      -l*)
+	if test X-lc = "X$arg" || test X-lm = "X$arg"; then
+	  case $host in
+	  *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-beos* | *-cegcc* | *-*-haiku*)
+	    # These systems don't actually have a C or math library (as such)
+	    continue
+	    ;;
+	  *-*-os2*)
+	    # These systems don't actually have a C library (as such)
+	    test X-lc = "X$arg" && continue
+	    ;;
+	  *-*-openbsd* | *-*-freebsd* | *-*-dragonfly* | *-*-bitrig* | *-*-midnightbsd*)
+	    # Do not include libc due to us having libc/libc_r.
+	    test X-lc = "X$arg" && continue
+	    ;;
+	  *-*-rhapsody* | *-*-darwin1.[012])
+	    # Rhapsody C and math libraries are in the System framework
+	    func_append deplibs " System.ltframework"
+	    continue
+	    ;;
+	  *-*-sco3.2v5* | *-*-sco5v6*)
+	    # Causes problems with __ctype
+	    test X-lc = "X$arg" && continue
+	    ;;
+	  *-*-sysv4.2uw2* | *-*-sysv5* | *-*-unixware* | *-*-OpenUNIX*)
+	    # Compiler inserts libc in the correct place for threads to work
+	    test X-lc = "X$arg" && continue
+	    ;;
+	  esac
+	elif test X-lc_r = "X$arg"; then
+	 case $host in
+	 *-*-openbsd* | *-*-freebsd* | *-*-dragonfly* | *-*-bitrig* | *-*-midnightbsd*)
+	   # Do not include libc_r directly, use -pthread flag.
+	   continue
+	   ;;
+	 esac
+	fi
+	func_append deplibs " $arg"
+	continue
+	;;
+
+      -mllvm)
+	prev=mllvm
+	continue
+	;;
+
+      -module)
+	module=yes
+	continue
+	;;
+
+      # Tru64 UNIX uses -model [arg] to determine the layout of C++
+      # classes, name mangling, and exception handling.
+      # Darwin uses the -arch flag to determine output architecture.
+      -model|-arch|-isysroot|--sysroot)
+	func_append compiler_flags " $arg"
+	func_append compile_command " $arg"
+	func_append finalize_command " $arg"
+	prev=xcompiler
+	continue
+	;;
+     # Solaris ld rejects as of 11.4. Refer to Oracle bug 22985199.
+     -pthread)
+	case $host in
+	  *solaris2*) ;;
+	  *)
+	    case "$new_inherited_linker_flags " in
+	        *" $arg "*) ;;
+	        * ) func_append new_inherited_linker_flags " $arg" ;;
+	    esac
+	  ;;
+	esac
+	continue
+	;;
+      -mt|-mthreads|-kthread|-Kthread|-pthreads|--thread-safe \
+      |-threads|-fopenmp|-openmp|-mp|-xopenmp|-omp|-qsmp=*)
+	func_append compiler_flags " $arg"
+	func_append compile_command " $arg"
+	func_append finalize_command " $arg"
+	case "$new_inherited_linker_flags " in
+	    *" $arg "*) ;;
+	    * ) func_append new_inherited_linker_flags " $arg" ;;
+	esac
+	continue
+	;;
+
+      -multi_module)
+	single_module=$wl-multi_module
+	continue
+	;;
+
+      -no-fast-install)
+	fast_install=no
+	continue
+	;;
+
+      -no-install)
+	case $host in
+	*-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-*-darwin* | *-cegcc*)
+	  # The PATH hackery in wrapper scripts is required on Windows
+	  # and Darwin in order for the loader to find any dlls it needs.
+	  func_warning "'-no-install' is ignored for $host"
+	  func_warning "assuming '-no-fast-install' instead"
+	  fast_install=no
+	  ;;
+	*) no_install=yes ;;
+	esac
+	continue
+	;;
+
+      -no-undefined)
+	allow_undefined=no
+	continue
+	;;
+
+      -objectlist)
+	prev=objectlist
+	continue
+	;;
+
+      -os2dllname)
+	prev=os2dllname
+	continue
+	;;
+
+      -o) prev=output ;;
+
+      -precious-files-regex)
+	prev=precious_regex
+	continue
+	;;
+
+      -release)
+	prev=release
+	continue
+	;;
+
+      -rpath)
+	prev=rpath
+	continue
+	;;
+
+      -R)
+	prev=xrpath
+	continue
+	;;
+
+      -R*)
+	func_stripname '-R' '' "$arg"
+	dir=$func_stripname_result
+	# We need an absolute path.
+	case $dir in
+	[\\/]* | [A-Za-z]:[\\/]*) ;;
+	=*)
+	  func_stripname '=' '' "$dir"
+	  dir=$lt_sysroot$func_stripname_result
+	  ;;
+	*)
+	  func_fatal_error "only absolute run-paths are allowed"
+	  ;;
+	esac
+	case "$xrpath " in
+	*" $dir "*) ;;
+	*) func_append xrpath " $dir" ;;
+	esac
+	continue
+	;;
+
+      -shared)
+	# The effects of -shared are defined in a previous loop.
+	continue
+	;;
+
+      -shrext)
+	prev=shrext
+	continue
+	;;
+
+      -static | -static-libtool-libs)
+	# The effects of -static are defined in a previous loop.
+	# We used to do the same as -all-static on platforms that
+	# didn't have a PIC flag, but the assumption that the effects
+	# would be equivalent was wrong.  It would break on at least
+	# Digital Unix and AIX.
+	continue
+	;;
+
+      -thread-safe)
+	thread_safe=yes
+	continue
+	;;
+
+      -version-info)
+	prev=vinfo
+	continue
+	;;
+
+      -version-number)
+	prev=vinfo
+	vinfo_number=yes
+	continue
+	;;
+
+      -weak)
+        prev=weak
+	continue
+	;;
+
+      -Wc,*)
+	func_stripname '-Wc,' '' "$arg"
+	args=$func_stripname_result
+	arg=
+	save_ifs=$IFS; IFS=,
+	for flag in $args; do
+	  IFS=$save_ifs
+          func_quote_arg pretty "$flag"
+	  func_append arg " $func_quote_arg_result"
+	  func_append compiler_flags " $func_quote_arg_result"
+	done
+	IFS=$save_ifs
+	func_stripname ' ' '' "$arg"
+	arg=$func_stripname_result
+	;;
+
+      -Wl,*)
+	func_stripname '-Wl,' '' "$arg"
+	args=$func_stripname_result
+	arg=
+	save_ifs=$IFS; IFS=,
+	for flag in $args; do
+	  IFS=$save_ifs
+          func_quote_arg pretty "$flag"
+	  func_append arg " $wl$func_quote_arg_result"
+	  func_append compiler_flags " $wl$func_quote_arg_result"
+	  func_append linker_flags " $func_quote_arg_result"
+	done
+	IFS=$save_ifs
+	func_stripname ' ' '' "$arg"
+	arg=$func_stripname_result
+	;;
+
+      -Xassembler)
+        prev=xassembler
+        continue
+        ;;
+
+      -Xcompiler)
+	prev=xcompiler
+	continue
+	;;
+
+      -Xlinker)
+	prev=xlinker
+	continue
+	;;
+
+      -XCClinker)
+	prev=xcclinker
+	continue
+	;;
+
+      # -msg_* for osf cc
+      -msg_*)
+	func_quote_arg pretty "$arg"
+	arg=$func_quote_arg_result
+	;;
+
+      # Flags to be passed through unchanged, with rationale:
+      # -64, -mips[0-9]      enable 64-bit mode for the SGI compiler
+      # -r[0-9][0-9]*        specify processor for the SGI compiler
+      # -xarch=*, -xtarget=* enable 64-bit mode for the Sun compiler
+      # +DA*, +DD*           enable 64-bit mode for the HP compiler
+      # -q*                  compiler args for the IBM compiler
+      # -m*, -t[45]*, -txscale* architecture-specific flags for GCC
+      # -F/path              path to uninstalled frameworks, gcc on darwin
+      # -p, -pg, --coverage, -fprofile-*  profiling flags for GCC
+      # -fstack-protector*   stack protector flags for GCC
+      # @file                GCC response files
+      # -tp=*                Portland pgcc target processor selection
+      # --sysroot=*          for sysroot support
+      # -O*, -g*, -flto*, -fwhopr*, -fuse-linker-plugin GCC link-time optimization
+      # -specs=*             GCC specs files
+      # -stdlib=*            select c++ std lib with clang
+      # -fsanitize=*         Clang/GCC memory and address sanitizer
+      # -fuse-ld=*           Linker select flags for GCC
+      # -static-*            direct GCC to link specific libraries statically
+      # -fcilkplus           Cilk Plus language extension features for C/C++
+      # -Wa,*                Pass flags directly to the assembler
+      -64|-mips[0-9]|-r[0-9][0-9]*|-xarch=*|-xtarget=*|+DA*|+DD*|-q*|-m*| \
+      -t[45]*|-txscale*|-p|-pg|--coverage|-fprofile-*|-F*|@*|-tp=*|--sysroot=*| \
+      -O*|-g*|-flto*|-fwhopr*|-fuse-linker-plugin|-fstack-protector*|-stdlib=*| \
+      -specs=*|-fsanitize=*|-fuse-ld=*|-static-*|-fcilkplus|-Wa,*)
+        func_quote_arg pretty "$arg"
+	arg=$func_quote_arg_result
+        func_append compile_command " $arg"
+        func_append finalize_command " $arg"
+        func_append compiler_flags " $arg"
+        continue
+        ;;
+
+      -Z*)
+        if test os2 = "`expr $host : '.*\(os2\)'`"; then
+          # OS/2 uses -Zxxx to specify OS/2-specific options
+	  compiler_flags="$compiler_flags $arg"
+	  func_append compile_command " $arg"
+	  func_append finalize_command " $arg"
+	  case $arg in
+	  -Zlinker | -Zstack)
+	    prev=xcompiler
+	    ;;
+	  esac
+	  continue
+        else
+	  # Otherwise treat like 'Some other compiler flag' below
+	  func_quote_arg pretty "$arg"
+	  arg=$func_quote_arg_result
+        fi
+	;;
+
+      # Some other compiler flag.
+      -* | +*)
+        func_quote_arg pretty "$arg"
+	arg=$func_quote_arg_result
+	;;
+
+      *.$objext)
+	# A standard object.
+	func_append objs " $arg"
+	;;
+
+      *.lo)
+	# A libtool-controlled object.
+
+	# Check to see that this really is a libtool object.
+	if func_lalib_unsafe_p "$arg"; then
+	  pic_object=
+	  non_pic_object=
+
+	  # Read the .lo file
+	  func_source "$arg"
+
+	  if test -z "$pic_object" ||
+	     test -z "$non_pic_object" ||
+	     test none = "$pic_object" &&
+	     test none = "$non_pic_object"; then
+	    func_fatal_error "cannot find name of object for '$arg'"
+	  fi
+
+	  # Extract subdirectory from the argument.
+	  func_dirname "$arg" "/" ""
+	  xdir=$func_dirname_result
+
+	  test none = "$pic_object" || {
+	    # Prepend the subdirectory the object is found in.
+	    pic_object=$xdir$pic_object
+
+	    if test dlfiles = "$prev"; then
+	      if test yes = "$build_libtool_libs" && test yes = "$dlopen_support"; then
+		func_append dlfiles " $pic_object"
+		prev=
+		continue
+	      else
+		# If libtool objects are unsupported, then we need to preload.
+		prev=dlprefiles
+	      fi
+	    fi
+
+	    # CHECK ME:  I think I busted this.  -Ossama
+	    if test dlprefiles = "$prev"; then
+	      # Preload the old-style object.
+	      func_append dlprefiles " $pic_object"
+	      prev=
+	    fi
+
+	    # A PIC object.
+	    func_append libobjs " $pic_object"
+	    arg=$pic_object
+	  }
+
+	  # Non-PIC object.
+	  if test none != "$non_pic_object"; then
+	    # Prepend the subdirectory the object is found in.
+	    non_pic_object=$xdir$non_pic_object
+
+	    # A standard non-PIC object
+	    func_append non_pic_objects " $non_pic_object"
+	    if test -z "$pic_object" || test none = "$pic_object"; then
+	      arg=$non_pic_object
+	    fi
+	  else
+	    # If the PIC object exists, use it instead.
+	    # $xdir was prepended to $pic_object above.
+	    non_pic_object=$pic_object
+	    func_append non_pic_objects " $non_pic_object"
+	  fi
+	else
+	  # Only an error if not doing a dry-run.
+	  if $opt_dry_run; then
+	    # Extract subdirectory from the argument.
+	    func_dirname "$arg" "/" ""
+	    xdir=$func_dirname_result
+
+	    func_lo2o "$arg"
+	    pic_object=$xdir$objdir/$func_lo2o_result
+	    non_pic_object=$xdir$func_lo2o_result
+	    func_append libobjs " $pic_object"
+	    func_append non_pic_objects " $non_pic_object"
+	  else
+	    func_fatal_error "'$arg' is not a valid libtool object"
+	  fi
+	fi
+	;;
+
+      *.$libext)
+	# An archive.
+	func_append deplibs " $arg"
+	func_append old_deplibs " $arg"
+	continue
+	;;
+
+      *.la)
+	# A libtool-controlled library.
+
+	func_resolve_sysroot "$arg"
+	if test dlfiles = "$prev"; then
+	  # This library was specified with -dlopen.
+	  func_append dlfiles " $func_resolve_sysroot_result"
+	  prev=
+	elif test dlprefiles = "$prev"; then
+	  # The library was specified with -dlpreopen.
+	  func_append dlprefiles " $func_resolve_sysroot_result"
+	  prev=
+	else
+	  func_append deplibs " $func_resolve_sysroot_result"
+	fi
+	continue
+	;;
+
+      # Some other compiler argument.
+      *)
+	# Unknown arguments in both finalize_command and compile_command need
+	# to be aesthetically quoted because they are evaled later.
+	func_quote_arg pretty "$arg"
+	arg=$func_quote_arg_result
+	;;
+      esac # arg
+
+      # Now actually substitute the argument into the commands.
+      if test -n "$arg"; then
+	func_append compile_command " $arg"
+	func_append finalize_command " $arg"
+      fi
+    done # argument parsing loop
+
+    test -n "$prev" && \
+      func_fatal_help "the '$prevarg' option requires an argument"
+
+    if test yes = "$export_dynamic" && test -n "$export_dynamic_flag_spec"; then
+      eval arg=\"$export_dynamic_flag_spec\"
+      func_append compile_command " $arg"
+      func_append finalize_command " $arg"
+    fi
+
+    oldlibs=
+    # calculate the name of the file, without its directory
+    func_basename "$output"
+    outputname=$func_basename_result
+    libobjs_save=$libobjs
+
+    if test -n "$shlibpath_var"; then
+      # get the directories listed in $shlibpath_var
+      eval shlib_search_path=\`\$ECHO \"\$$shlibpath_var\" \| \$SED \'s/:/ /g\'\`
+    else
+      shlib_search_path=
+    fi
+    eval sys_lib_search_path=\"$sys_lib_search_path_spec\"
+    eval sys_lib_dlsearch_path=\"$sys_lib_dlsearch_path_spec\"
+
+    # Definition is injected by LT_CONFIG during libtool generation.
+    func_munge_path_list sys_lib_dlsearch_path "$LT_SYS_LIBRARY_PATH"
+
+    func_dirname "$output" "/" ""
+    output_objdir=$func_dirname_result$objdir
+    func_to_tool_file "$output_objdir/"
+    tool_output_objdir=$func_to_tool_file_result
+    # Create the object directory.
+    func_mkdir_p "$output_objdir"
+
+    # Determine the type of output
+    case $output in
+    "")
+      func_fatal_help "you must specify an output file"
+      ;;
+    *.$libext) linkmode=oldlib ;;
+    *.lo | *.$objext) linkmode=obj ;;
+    *.la) linkmode=lib ;;
+    *) linkmode=prog ;; # Anything else should be a program.
+    esac
+
+    specialdeplibs=
+
+    libs=
+    # Find all interdependent deplibs by searching for libraries
+    # that are linked more than once (e.g. -la -lb -la)
+    for deplib in $deplibs; do
+      if $opt_preserve_dup_deps; then
+	case "$libs " in
+	*" $deplib "*) func_append specialdeplibs " $deplib" ;;
+	esac
+      fi
+      func_append libs " $deplib"
+    done
+
+    if test lib = "$linkmode"; then
+      libs="$predeps $libs $compiler_lib_search_path $postdeps"
+
+      # Compute libraries that are listed more than once in $predeps
+      # $postdeps and mark them as special (i.e., whose duplicates are
+      # not to be eliminated).
+      pre_post_deps=
+      if $opt_duplicate_compiler_generated_deps; then
+	for pre_post_dep in $predeps $postdeps; do
+	  case "$pre_post_deps " in
+	  *" $pre_post_dep "*) func_append specialdeplibs " $pre_post_deps" ;;
+	  esac
+	  func_append pre_post_deps " $pre_post_dep"
+	done
+      fi
+      pre_post_deps=
+    fi
+
+    deplibs=
+    newdependency_libs=
+    newlib_search_path=
+    need_relink=no # whether we're linking any uninstalled libtool libraries
+    notinst_deplibs= # not-installed libtool libraries
+    notinst_path= # paths that contain not-installed libtool libraries
+
+    case $linkmode in
+    lib)
+	passes="conv dlpreopen link"
+	for file in $dlfiles $dlprefiles; do
+	  case $file in
+	  *.la) ;;
+	  *)
+	    func_fatal_help "libraries can '-dlopen' only libtool libraries: $file"
+	    ;;
+	  esac
+	done
+	;;
+    prog)
+	compile_deplibs=
+	finalize_deplibs=
+	alldeplibs=false
+	newdlfiles=
+	newdlprefiles=
+	passes="conv scan dlopen dlpreopen link"
+	;;
+    *)  passes="conv"
+	;;
+    esac
+
+    for pass in $passes; do
+      # The preopen pass in lib mode reverses $deplibs; put it back here
+      # so that -L comes before libs that need it for instance...
+      if test lib,link = "$linkmode,$pass"; then
+	## FIXME: Find the place where the list is rebuilt in the wrong
+	##        order, and fix it there properly
+        tmp_deplibs=
+	for deplib in $deplibs; do
+	  tmp_deplibs="$deplib $tmp_deplibs"
+	done
+	deplibs=$tmp_deplibs
+      fi
+
+      if test lib,link = "$linkmode,$pass" ||
+	 test prog,scan = "$linkmode,$pass"; then
+	libs=$deplibs
+	deplibs=
+      fi
+      if test prog = "$linkmode"; then
+	case $pass in
+	dlopen) libs=$dlfiles ;;
+	dlpreopen) libs=$dlprefiles ;;
+	link)
+	  libs="$deplibs %DEPLIBS%"
+	  test "X$link_all_deplibs" != Xno && libs="$libs $dependency_libs"
+	  ;;
+	esac
+      fi
+      if test lib,dlpreopen = "$linkmode,$pass"; then
+	# Collect and forward deplibs of preopened libtool libs
+	for lib in $dlprefiles; do
+	  # Ignore non-libtool-libs
+	  dependency_libs=
+	  func_resolve_sysroot "$lib"
+	  case $lib in
+	  *.la)	func_source "$func_resolve_sysroot_result" ;;
+	  esac
+
+	  # Collect preopened libtool deplibs, except any this library
+	  # has declared as weak libs
+	  for deplib in $dependency_libs; do
+	    func_basename "$deplib"
+            deplib_base=$func_basename_result
+	    case " $weak_libs " in
+	    *" $deplib_base "*) ;;
+	    *) func_append deplibs " $deplib" ;;
+	    esac
+	  done
+	done
+	libs=$dlprefiles
+      fi
+      if test dlopen = "$pass"; then
+	# Collect dlpreopened libraries
+	save_deplibs=$deplibs
+	deplibs=
+      fi
+
+      for deplib in $libs; do
+	lib=
+	found=false
+	case $deplib in
+	-mt|-mthreads|-kthread|-Kthread|-pthread|-pthreads|--thread-safe \
+        |-threads|-fopenmp|-openmp|-mp|-xopenmp|-omp|-qsmp=*)
+	  if test prog,link = "$linkmode,$pass"; then
+	    compile_deplibs="$deplib $compile_deplibs"
+	    finalize_deplibs="$deplib $finalize_deplibs"
+	  else
+	    func_append compiler_flags " $deplib"
+	    if test lib = "$linkmode"; then
+		case "$new_inherited_linker_flags " in
+		    *" $deplib "*) ;;
+		    * ) func_append new_inherited_linker_flags " $deplib" ;;
+		esac
+	    fi
+	  fi
+	  continue
+	  ;;
+	-l*)
+	  if test lib != "$linkmode" && test prog != "$linkmode"; then
+	    func_warning "'-l' is ignored for archives/objects"
+	    continue
+	  fi
+	  func_stripname '-l' '' "$deplib"
+	  name=$func_stripname_result
+	  if test lib = "$linkmode"; then
+	    searchdirs="$newlib_search_path $lib_search_path $compiler_lib_search_dirs $sys_lib_search_path $shlib_search_path"
+	  else
+	    searchdirs="$newlib_search_path $lib_search_path $sys_lib_search_path $shlib_search_path"
+	  fi
+	  for searchdir in $searchdirs; do
+	    for search_ext in .la $std_shrext .so .a; do
+	      # Search the libtool library
+	      lib=$searchdir/lib$name$search_ext
+	      if test -f "$lib"; then
+		if test .la = "$search_ext"; then
+		  found=:
+		else
+		  found=false
+		fi
+		break 2
+	      fi
+	    done
+	  done
+	  if $found; then
+	    # deplib is a libtool library
+	    # If $allow_libtool_libs_with_static_runtimes && $deplib is a stdlib,
+	    # We need to do some special things here, and not later.
+	    if test yes = "$allow_libtool_libs_with_static_runtimes"; then
+	      case " $predeps $postdeps " in
+	      *" $deplib "*)
+		if func_lalib_p "$lib"; then
+		  library_names=
+		  old_library=
+		  func_source "$lib"
+		  for l in $old_library $library_names; do
+		    ll=$l
+		  done
+		  if test "X$ll" = "X$old_library"; then # only static version available
+		    found=false
+		    func_dirname "$lib" "" "."
+		    ladir=$func_dirname_result
+		    lib=$ladir/$old_library
+		    if test prog,link = "$linkmode,$pass"; then
+		      compile_deplibs="$deplib $compile_deplibs"
+		      finalize_deplibs="$deplib $finalize_deplibs"
+		    else
+		      deplibs="$deplib $deplibs"
+		      test lib = "$linkmode" && newdependency_libs="$deplib $newdependency_libs"
+		    fi
+		    continue
+		  fi
+		fi
+		;;
+	      *) ;;
+	      esac
+	    fi
+	  else
+	    # deplib doesn't seem to be a libtool library
+	    if test prog,link = "$linkmode,$pass"; then
+	      compile_deplibs="$deplib $compile_deplibs"
+	      finalize_deplibs="$deplib $finalize_deplibs"
+	    else
+	      deplibs="$deplib $deplibs"
+	      test lib = "$linkmode" && newdependency_libs="$deplib $newdependency_libs"
+	    fi
+	    continue
+	  fi
+	  ;; # -l
+	*.ltframework)
+	  if test prog,link = "$linkmode,$pass"; then
+	    compile_deplibs="$deplib $compile_deplibs"
+	    finalize_deplibs="$deplib $finalize_deplibs"
+	  else
+	    deplibs="$deplib $deplibs"
+	    if test lib = "$linkmode"; then
+		case "$new_inherited_linker_flags " in
+		    *" $deplib "*) ;;
+		    * ) func_append new_inherited_linker_flags " $deplib" ;;
+		esac
+	    fi
+	  fi
+	  continue
+	  ;;
+	-L*)
+	  case $linkmode in
+	  lib)
+	    deplibs="$deplib $deplibs"
+	    test conv = "$pass" && continue
+	    newdependency_libs="$deplib $newdependency_libs"
+	    func_stripname '-L' '' "$deplib"
+	    func_resolve_sysroot "$func_stripname_result"
+	    func_append newlib_search_path " $func_resolve_sysroot_result"
+	    ;;
+	  prog)
+	    if test conv = "$pass"; then
+	      deplibs="$deplib $deplibs"
+	      continue
+	    fi
+	    if test scan = "$pass"; then
+	      deplibs="$deplib $deplibs"
+	    else
+	      compile_deplibs="$deplib $compile_deplibs"
+	      finalize_deplibs="$deplib $finalize_deplibs"
+	    fi
+	    func_stripname '-L' '' "$deplib"
+	    func_resolve_sysroot "$func_stripname_result"
+	    func_append newlib_search_path " $func_resolve_sysroot_result"
+	    ;;
+	  *)
+	    func_warning "'-L' is ignored for archives/objects"
+	    ;;
+	  esac # linkmode
+	  continue
+	  ;; # -L
+	-R*)
+	  if test link = "$pass"; then
+	    func_stripname '-R' '' "$deplib"
+	    func_resolve_sysroot "$func_stripname_result"
+	    dir=$func_resolve_sysroot_result
+	    # Make sure the xrpath contains only unique directories.
+	    case "$xrpath " in
+	    *" $dir "*) ;;
+	    *) func_append xrpath " $dir" ;;
+	    esac
+	  fi
+	  deplibs="$deplib $deplibs"
+	  continue
+	  ;;
+	*.la)
+	  func_resolve_sysroot "$deplib"
+	  lib=$func_resolve_sysroot_result
+	  ;;
+	*.$libext)
+	  if test conv = "$pass"; then
+	    deplibs="$deplib $deplibs"
+	    continue
+	  fi
+	  case $linkmode in
+	  lib)
+	    # Linking convenience modules into shared libraries is allowed,
+	    # but linking other static libraries is non-portable.
+	    case " $dlpreconveniencelibs " in
+	    *" $deplib "*) ;;
+	    *)
+	      valid_a_lib=false
+	      case $deplibs_check_method in
+		match_pattern*)
+		  set dummy $deplibs_check_method; shift
+		  match_pattern_regex=`expr "$deplibs_check_method" : "$1 \(.*\)"`
+		  if eval "\$ECHO \"$deplib\"" 2>/dev/null | $SED 10q \
+		    | $EGREP "$match_pattern_regex" > /dev/null; then
+		    valid_a_lib=:
+		  fi
+		;;
+		pass_all)
+		  valid_a_lib=:
+		;;
+	      esac
+	      if $valid_a_lib; then
+		echo
+		$ECHO "*** Warning: Linking the shared library $output against the"
+		$ECHO "*** static library $deplib is not portable!"
+		deplibs="$deplib $deplibs"
+	      else
+		echo
+		$ECHO "*** Warning: Trying to link with static lib archive $deplib."
+		echo "*** I have the capability to make that library automatically link in when"
+		echo "*** you link to this library.  But I can only do this if you have a"
+		echo "*** shared version of the library, which you do not appear to have"
+		echo "*** because the file extensions .$libext of this argument makes me believe"
+		echo "*** that it is just a static archive that I should not use here."
+	      fi
+	      ;;
+	    esac
+	    continue
+	    ;;
+	  prog)
+	    if test link != "$pass"; then
+	      deplibs="$deplib $deplibs"
+	    else
+	      compile_deplibs="$deplib $compile_deplibs"
+	      finalize_deplibs="$deplib $finalize_deplibs"
+	    fi
+	    continue
+	    ;;
+	  esac # linkmode
+	  ;; # *.$libext
+	*.lo | *.$objext)
+	  if test conv = "$pass"; then
+	    deplibs="$deplib $deplibs"
+	  elif test prog = "$linkmode"; then
+	    if test dlpreopen = "$pass" || test yes != "$dlopen_support" || test no = "$build_libtool_libs"; then
+	      # If there is no dlopen support or we're linking statically,
+	      # we need to preload.
+	      func_append newdlprefiles " $deplib"
+	      compile_deplibs="$deplib $compile_deplibs"
+	      finalize_deplibs="$deplib $finalize_deplibs"
+	    else
+	      func_append newdlfiles " $deplib"
+	    fi
+	  fi
+	  continue
+	  ;;
+	%DEPLIBS%)
+	  alldeplibs=:
+	  continue
+	  ;;
+	esac # case $deplib
+
+	$found || test -f "$lib" \
+	  || func_fatal_error "cannot find the library '$lib' or unhandled argument '$deplib'"
+
+	# Check to see that this really is a libtool archive.
+	func_lalib_unsafe_p "$lib" \
+	  || func_fatal_error "'$lib' is not a valid libtool archive"
+
+	func_dirname "$lib" "" "."
+	ladir=$func_dirname_result
+
+	dlname=
+	dlopen=
+	dlpreopen=
+	libdir=
+	library_names=
+	old_library=
+	inherited_linker_flags=
+	# If the library was installed with an old release of libtool,
+	# it will not redefine variables installed, or shouldnotlink
+	installed=yes
+	shouldnotlink=no
+	avoidtemprpath=
+
+
+	# Read the .la file
+	func_source "$lib"
+
+	# Convert "-framework foo" to "foo.ltframework"
+	if test -n "$inherited_linker_flags"; then
+	  tmp_inherited_linker_flags=`$ECHO "$inherited_linker_flags" | $SED 's/-framework \([^ $]*\)/\1.ltframework/g'`
+	  for tmp_inherited_linker_flag in $tmp_inherited_linker_flags; do
+	    case " $new_inherited_linker_flags " in
+	      *" $tmp_inherited_linker_flag "*) ;;
+	      *) func_append new_inherited_linker_flags " $tmp_inherited_linker_flag";;
+	    esac
+	  done
+	fi
+	dependency_libs=`$ECHO " $dependency_libs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'`
+	if test lib,link = "$linkmode,$pass" ||
+	   test prog,scan = "$linkmode,$pass" ||
+	   { test prog != "$linkmode" && test lib != "$linkmode"; }; then
+	  test -n "$dlopen" && func_append dlfiles " $dlopen"
+	  test -n "$dlpreopen" && func_append dlprefiles " $dlpreopen"
+	fi
+
+	if test conv = "$pass"; then
+	  # Only check for convenience libraries
+	  deplibs="$lib $deplibs"
+	  if test -z "$libdir"; then
+	    if test -z "$old_library"; then
+	      func_fatal_error "cannot find name of link library for '$lib'"
+	    fi
+	    # It is a libtool convenience library, so add in its objects.
+	    func_append convenience " $ladir/$objdir/$old_library"
+	    func_append old_convenience " $ladir/$objdir/$old_library"
+	    tmp_libs=
+	    for deplib in $dependency_libs; do
+	      deplibs="$deplib $deplibs"
+	      if $opt_preserve_dup_deps; then
+		case "$tmp_libs " in
+		*" $deplib "*) func_append specialdeplibs " $deplib" ;;
+		esac
+	      fi
+	      func_append tmp_libs " $deplib"
+	    done
+	  elif test prog != "$linkmode" && test lib != "$linkmode"; then
+	    func_fatal_error "'$lib' is not a convenience library"
+	  fi
+	  continue
+	fi # $pass = conv
+
+
+	# Get the name of the library we link against.
+	linklib=
+	if test -n "$old_library" &&
+	   { test yes = "$prefer_static_libs" ||
+	     test built,no = "$prefer_static_libs,$installed"; }; then
+	  linklib=$old_library
+	else
+	  for l in $old_library $library_names; do
+	    linklib=$l
+	  done
+	fi
+	if test -z "$linklib"; then
+	  func_fatal_error "cannot find name of link library for '$lib'"
+	fi
+
+	# This library was specified with -dlopen.
+	if test dlopen = "$pass"; then
+	  test -z "$libdir" \
+	    && func_fatal_error "cannot -dlopen a convenience library: '$lib'"
+	  if test -z "$dlname" ||
+	     test yes != "$dlopen_support" ||
+	     test no = "$build_libtool_libs"
+	  then
+	    # If there is no dlname, no dlopen support or we're linking
+	    # statically, we need to preload.  We also need to preload any
+	    # dependent libraries so libltdl's deplib preloader doesn't
+	    # bomb out in the load deplibs phase.
+	    func_append dlprefiles " $lib $dependency_libs"
+	  else
+	    func_append newdlfiles " $lib"
+	  fi
+	  continue
+	fi # $pass = dlopen
+
+	# We need an absolute path.
+	case $ladir in
+	[\\/]* | [A-Za-z]:[\\/]*) abs_ladir=$ladir ;;
+	*)
+	  abs_ladir=`cd "$ladir" && pwd`
+	  if test -z "$abs_ladir"; then
+	    func_warning "cannot determine absolute directory name of '$ladir'"
+	    func_warning "passing it literally to the linker, although it might fail"
+	    abs_ladir=$ladir
+	  fi
+	  ;;
+	esac
+	func_basename "$lib"
+	laname=$func_basename_result
+
+	# Find the relevant object directory and library name.
+	if test yes = "$installed"; then
+	  if test ! -f "$lt_sysroot$libdir/$linklib" && test -f "$abs_ladir/$linklib"; then
+	    func_warning "library '$lib' was moved."
+	    dir=$ladir
+	    absdir=$abs_ladir
+	    libdir=$abs_ladir
+	  else
+	    dir=$lt_sysroot$libdir
+	    absdir=$lt_sysroot$libdir
+	  fi
+	  test yes = "$hardcode_automatic" && avoidtemprpath=yes
+	else
+	  if test ! -f "$ladir/$objdir/$linklib" && test -f "$abs_ladir/$linklib"; then
+	    dir=$ladir
+	    absdir=$abs_ladir
+	    # Remove this search path later
+	    func_append notinst_path " $abs_ladir"
+	  else
+	    dir=$ladir/$objdir
+	    absdir=$abs_ladir/$objdir
+	    # Remove this search path later
+	    func_append notinst_path " $abs_ladir"
+	  fi
+	fi # $installed = yes
+	func_stripname 'lib' '.la' "$laname"
+	name=$func_stripname_result
+
+	# This library was specified with -dlpreopen.
+	if test dlpreopen = "$pass"; then
+	  if test -z "$libdir" && test prog = "$linkmode"; then
+	    func_fatal_error "only libraries may -dlpreopen a convenience library: '$lib'"
+	  fi
+	  case $host in
+	    # special handling for platforms with PE-DLLs.
+	    *cygwin* | *mingw* | *cegcc* )
+	      # Linker will automatically link against shared library if both
+	      # static and shared are present.  Therefore, ensure we extract
+	      # symbols from the import library if a shared library is present
+	      # (otherwise, the dlopen module name will be incorrect).  We do
+	      # this by putting the import library name into $newdlprefiles.
+	      # We recover the dlopen module name by 'saving' the la file
+	      # name in a special purpose variable, and (later) extracting the
+	      # dlname from the la file.
+	      if test -n "$dlname"; then
+	        func_tr_sh "$dir/$linklib"
+	        eval "libfile_$func_tr_sh_result=\$abs_ladir/\$laname"
+	        func_append newdlprefiles " $dir/$linklib"
+	      else
+	        func_append newdlprefiles " $dir/$old_library"
+	        # Keep a list of preopened convenience libraries to check
+	        # that they are being used correctly in the link pass.
+	        test -z "$libdir" && \
+	          func_append dlpreconveniencelibs " $dir/$old_library"
+	      fi
+	    ;;
+	    * )
+	      # Prefer using a static library (so that no silly _DYNAMIC symbols
+	      # are required to link).
+	      if test -n "$old_library"; then
+	        func_append newdlprefiles " $dir/$old_library"
+	        # Keep a list of preopened convenience libraries to check
+	        # that they are being used correctly in the link pass.
+	        test -z "$libdir" && \
+	          func_append dlpreconveniencelibs " $dir/$old_library"
+	      # Otherwise, use the dlname, so that lt_dlopen finds it.
+	      elif test -n "$dlname"; then
+	        func_append newdlprefiles " $dir/$dlname"
+	      else
+	        func_append newdlprefiles " $dir/$linklib"
+	      fi
+	    ;;
+	  esac
+	fi # $pass = dlpreopen
+
+	if test -z "$libdir"; then
+	  # Link the convenience library
+	  if test lib = "$linkmode"; then
+	    deplibs="$dir/$old_library $deplibs"
+	  elif test prog,link = "$linkmode,$pass"; then
+	    compile_deplibs="$dir/$old_library $compile_deplibs"
+	    finalize_deplibs="$dir/$old_library $finalize_deplibs"
+	  else
+	    deplibs="$lib $deplibs" # used for prog,scan pass
+	  fi
+	  continue
+	fi
+
+
+	if test prog = "$linkmode" && test link != "$pass"; then
+	  func_append newlib_search_path " $ladir"
+	  deplibs="$lib $deplibs"
+
+	  linkalldeplibs=false
+	  if test no != "$link_all_deplibs" || test -z "$library_names" ||
+	     test no = "$build_libtool_libs"; then
+	    linkalldeplibs=:
+	  fi
+
+	  tmp_libs=
+	  for deplib in $dependency_libs; do
+	    case $deplib in
+	    -L*) func_stripname '-L' '' "$deplib"
+	         func_resolve_sysroot "$func_stripname_result"
+	         func_append newlib_search_path " $func_resolve_sysroot_result"
+		 ;;
+	    esac
+	    # Need to link against all dependency_libs?
+	    if $linkalldeplibs; then
+	      deplibs="$deplib $deplibs"
+	    else
+	      # Need to hardcode shared library paths
+	      # or/and link against static libraries
+	      newdependency_libs="$deplib $newdependency_libs"
+	    fi
+	    if $opt_preserve_dup_deps; then
+	      case "$tmp_libs " in
+	      *" $deplib "*) func_append specialdeplibs " $deplib" ;;
+	      esac
+	    fi
+	    func_append tmp_libs " $deplib"
+	  done # for deplib
+	  continue
+	fi # $linkmode = prog...
+
+	if test prog,link = "$linkmode,$pass"; then
+	  if test -n "$library_names" &&
+	     { { test no = "$prefer_static_libs" ||
+	         test built,yes = "$prefer_static_libs,$installed"; } ||
+	       test -z "$old_library"; }; then
+	    # We need to hardcode the library path
+	    if test -n "$shlibpath_var" && test -z "$avoidtemprpath"; then
+	      # Make sure the rpath contains only unique directories.
+	      case $temp_rpath: in
+	      *"$absdir:"*) ;;
+	      *) func_append temp_rpath "$absdir:" ;;
+	      esac
+	    fi
+
+	    # Hardcode the library path.
+	    # Skip directories that are in the system default run-time
+	    # search path.
+	    case " $sys_lib_dlsearch_path " in
+	    *" $absdir "*) ;;
+	    *)
+	      case "$compile_rpath " in
+	      *" $absdir "*) ;;
+	      *) func_append compile_rpath " $absdir" ;;
+	      esac
+	      ;;
+	    esac
+	    case " $sys_lib_dlsearch_path " in
+	    *" $libdir "*) ;;
+	    *)
+	      case "$finalize_rpath " in
+	      *" $libdir "*) ;;
+	      *) func_append finalize_rpath " $libdir" ;;
+	      esac
+	      ;;
+	    esac
+	  fi # $linkmode,$pass = prog,link...
+
+	  if $alldeplibs &&
+	     { test pass_all = "$deplibs_check_method" ||
+	       { test yes = "$build_libtool_libs" &&
+		 test -n "$library_names"; }; }; then
+	    # We only need to search for static libraries
+	    continue
+	  fi
+	fi
+
+	link_static=no # Whether the deplib will be linked statically
+	use_static_libs=$prefer_static_libs
+	if test built = "$use_static_libs" && test yes = "$installed"; then
+	  use_static_libs=no
+	fi
+	if test -n "$library_names" &&
+	   { test no = "$use_static_libs" || test -z "$old_library"; }; then
+	  case $host in
+	  *cygwin* | *mingw* | *cegcc* | *os2*)
+	      # No point in relinking DLLs because paths are not encoded
+	      func_append notinst_deplibs " $lib"
+	      need_relink=no
+	    ;;
+	  *)
+	    if test no = "$installed"; then
+	      func_append notinst_deplibs " $lib"
+	      need_relink=yes
+	    fi
+	    ;;
+	  esac
+	  # This is a shared library
+
+	  # Warn about portability, can't link against -module's on some
+	  # systems (darwin).  Don't bleat about dlopened modules though!
+	  dlopenmodule=
+	  for dlpremoduletest in $dlprefiles; do
+	    if test "X$dlpremoduletest" = "X$lib"; then
+	      dlopenmodule=$dlpremoduletest
+	      break
+	    fi
+	  done
+	  if test -z "$dlopenmodule" && test yes = "$shouldnotlink" && test link = "$pass"; then
+	    echo
+	    if test prog = "$linkmode"; then
+	      $ECHO "*** Warning: Linking the executable $output against the loadable module"
+	    else
+	      $ECHO "*** Warning: Linking the shared library $output against the loadable module"
+	    fi
+	    $ECHO "*** $linklib is not portable!"
+	  fi
+	  if test lib = "$linkmode" &&
+	     test yes = "$hardcode_into_libs"; then
+	    # Hardcode the library path.
+	    # Skip directories that are in the system default run-time
+	    # search path.
+	    case " $sys_lib_dlsearch_path " in
+	    *" $absdir "*) ;;
+	    *)
+	      case "$compile_rpath " in
+	      *" $absdir "*) ;;
+	      *) func_append compile_rpath " $absdir" ;;
+	      esac
+	      ;;
+	    esac
+	    case " $sys_lib_dlsearch_path " in
+	    *" $libdir "*) ;;
+	    *)
+	      case "$finalize_rpath " in
+	      *" $libdir "*) ;;
+	      *) func_append finalize_rpath " $libdir" ;;
+	      esac
+	      ;;
+	    esac
+	  fi
+
+	  if test -n "$old_archive_from_expsyms_cmds"; then
+	    # figure out the soname
+	    set dummy $library_names
+	    shift
+	    realname=$1
+	    shift
+	    libname=`eval "\\$ECHO \"$libname_spec\""`
+	    # use dlname if we got it. it's perfectly good, no?
+	    if test -n "$dlname"; then
+	      soname=$dlname
+	    elif test -n "$soname_spec"; then
+	      # bleh windows
+	      case $host in
+	      *cygwin* | mingw* | *cegcc* | *os2*)
+	        func_arith $current - $age
+		major=$func_arith_result
+		versuffix=-$major
+		;;
+	      esac
+	      eval soname=\"$soname_spec\"
+	    else
+	      soname=$realname
+	    fi
+
+	    # Make a new name for the extract_expsyms_cmds to use
+	    soroot=$soname
+	    func_basename "$soroot"
+	    soname=$func_basename_result
+	    func_stripname 'lib' '.dll' "$soname"
+	    newlib=libimp-$func_stripname_result.a
+
+	    # If the library has no export list, then create one now
+	    if test -f "$output_objdir/$soname-def"; then :
+	    else
+	      func_verbose "extracting exported symbol list from '$soname'"
+	      func_execute_cmds "$extract_expsyms_cmds" 'exit $?'
+	    fi
+
+	    # Create $newlib
+	    if test -f "$output_objdir/$newlib"; then :; else
+	      func_verbose "generating import library for '$soname'"
+	      func_execute_cmds "$old_archive_from_expsyms_cmds" 'exit $?'
+	    fi
+	    # make sure the library variables are pointing to the new library
+	    dir=$output_objdir
+	    linklib=$newlib
+	  fi # test -n "$old_archive_from_expsyms_cmds"
+
+	  if test prog = "$linkmode" || test relink != "$opt_mode"; then
+	    add_shlibpath=
+	    add_dir=
+	    add=
+	    lib_linked=yes
+	    case $hardcode_action in
+	    immediate | unsupported)
+	      if test no = "$hardcode_direct"; then
+		add=$dir/$linklib
+		case $host in
+		  *-*-sco3.2v5.0.[024]*) add_dir=-L$dir ;;
+		  *-*-sysv4*uw2*) add_dir=-L$dir ;;
+		  *-*-sysv5OpenUNIX* | *-*-sysv5UnixWare7.[01].[10]* | \
+		    *-*-unixware7*) add_dir=-L$dir ;;
+		  *-*-darwin* )
+		    # if the lib is a (non-dlopened) module then we cannot
+		    # link against it, someone is ignoring the earlier warnings
+		    if /usr/bin/file -L $add 2> /dev/null |
+			 $GREP ": [^:]* bundle" >/dev/null; then
+		      if test "X$dlopenmodule" != "X$lib"; then
+			$ECHO "*** Warning: lib $linklib is a module, not a shared library"
+			if test -z "$old_library"; then
+			  echo
+			  echo "*** And there doesn't seem to be a static archive available"
+			  echo "*** The link will probably fail, sorry"
+			else
+			  add=$dir/$old_library
+			fi
+		      elif test -n "$old_library"; then
+			add=$dir/$old_library
+		      fi
+		    fi
+		esac
+	      elif test no = "$hardcode_minus_L"; then
+		case $host in
+		*-*-sunos*) add_shlibpath=$dir ;;
+		esac
+		add_dir=-L$dir
+		add=-l$name
+	      elif test no = "$hardcode_shlibpath_var"; then
+		add_shlibpath=$dir
+		add=-l$name
+	      else
+		lib_linked=no
+	      fi
+	      ;;
+	    relink)
+	      if test yes = "$hardcode_direct" &&
+	         test no = "$hardcode_direct_absolute"; then
+		add=$dir/$linklib
+	      elif test yes = "$hardcode_minus_L"; then
+		add_dir=-L$absdir
+		# Try looking first in the location we're being installed to.
+		if test -n "$inst_prefix_dir"; then
+		  case $libdir in
+		    [\\/]*)
+		      func_append add_dir " -L$inst_prefix_dir$libdir"
+		      ;;
+		  esac
+		fi
+		add=-l$name
+	      elif test yes = "$hardcode_shlibpath_var"; then
+		add_shlibpath=$dir
+		add=-l$name
+	      else
+		lib_linked=no
+	      fi
+	      ;;
+	    *) lib_linked=no ;;
+	    esac
+
+	    if test yes != "$lib_linked"; then
+	      func_fatal_configuration "unsupported hardcode properties"
+	    fi
+
+	    if test -n "$add_shlibpath"; then
+	      case :$compile_shlibpath: in
+	      *":$add_shlibpath:"*) ;;
+	      *) func_append compile_shlibpath "$add_shlibpath:" ;;
+	      esac
+	    fi
+	    if test prog = "$linkmode"; then
+	      test -n "$add_dir" && compile_deplibs="$add_dir $compile_deplibs"
+	      test -n "$add" && compile_deplibs="$add $compile_deplibs"
+	    else
+	      test -n "$add_dir" && deplibs="$add_dir $deplibs"
+	      test -n "$add" && deplibs="$add $deplibs"
+	      if test yes != "$hardcode_direct" &&
+		 test yes != "$hardcode_minus_L" &&
+		 test yes = "$hardcode_shlibpath_var"; then
+		case :$finalize_shlibpath: in
+		*":$libdir:"*) ;;
+		*) func_append finalize_shlibpath "$libdir:" ;;
+		esac
+	      fi
+	    fi
+	  fi
+
+	  if test prog = "$linkmode" || test relink = "$opt_mode"; then
+	    add_shlibpath=
+	    add_dir=
+	    add=
+	    # Finalize command for both is simple: just hardcode it.
+	    if test yes = "$hardcode_direct" &&
+	       test no = "$hardcode_direct_absolute"; then
+	      add=$libdir/$linklib
+	    elif test yes = "$hardcode_minus_L"; then
+	      add_dir=-L$libdir
+	      add=-l$name
+	    elif test yes = "$hardcode_shlibpath_var"; then
+	      case :$finalize_shlibpath: in
+	      *":$libdir:"*) ;;
+	      *) func_append finalize_shlibpath "$libdir:" ;;
+	      esac
+	      add=-l$name
+	    elif test yes = "$hardcode_automatic"; then
+	      if test -n "$inst_prefix_dir" &&
+		 test -f "$inst_prefix_dir$libdir/$linklib"; then
+		add=$inst_prefix_dir$libdir/$linklib
+	      else
+		add=$libdir/$linklib
+	      fi
+	    else
+	      # We cannot seem to hardcode it, guess we'll fake it.
+	      add_dir=-L$libdir
+	      # Try looking first in the location we're being installed to.
+	      if test -n "$inst_prefix_dir"; then
+		case $libdir in
+		  [\\/]*)
+		    func_append add_dir " -L$inst_prefix_dir$libdir"
+		    ;;
+		esac
+	      fi
+	      add=-l$name
+	    fi
+
+	    if test prog = "$linkmode"; then
+	      test -n "$add_dir" && finalize_deplibs="$add_dir $finalize_deplibs"
+	      test -n "$add" && finalize_deplibs="$add $finalize_deplibs"
+	    else
+	      test -n "$add_dir" && deplibs="$add_dir $deplibs"
+	      test -n "$add" && deplibs="$add $deplibs"
+	    fi
+	  fi
+	elif test prog = "$linkmode"; then
+	  # Here we assume that one of hardcode_direct or hardcode_minus_L
+	  # is not unsupported.  This is valid on all known static and
+	  # shared platforms.
+	  if test unsupported != "$hardcode_direct"; then
+	    test -n "$old_library" && linklib=$old_library
+	    compile_deplibs="$dir/$linklib $compile_deplibs"
+	    finalize_deplibs="$dir/$linklib $finalize_deplibs"
+	  else
+	    compile_deplibs="-l$name -L$dir $compile_deplibs"
+	    finalize_deplibs="-l$name -L$dir $finalize_deplibs"
+	  fi
+	elif test yes = "$build_libtool_libs"; then
+	  # Not a shared library
+	  if test pass_all != "$deplibs_check_method"; then
+	    # We're trying link a shared library against a static one
+	    # but the system doesn't support it.
+
+	    # Just print a warning and add the library to dependency_libs so
+	    # that the program can be linked against the static library.
+	    echo
+	    $ECHO "*** Warning: This system cannot link to static lib archive $lib."
+	    echo "*** I have the capability to make that library automatically link in when"
+	    echo "*** you link to this library.  But I can only do this if you have a"
+	    echo "*** shared version of the library, which you do not appear to have."
+	    if test yes = "$module"; then
+	      echo "*** But as you try to build a module library, libtool will still create "
+	      echo "*** a static module, that should work as long as the dlopening application"
+	      echo "*** is linked with the -dlopen flag to resolve symbols at runtime."
+	      if test -z "$global_symbol_pipe"; then
+		echo
+		echo "*** However, this would only work if libtool was able to extract symbol"
+		echo "*** lists from a program, using 'nm' or equivalent, but libtool could"
+		echo "*** not find such a program.  So, this module is probably useless."
+		echo "*** 'nm' from GNU binutils and a full rebuild may help."
+	      fi
+	      if test no = "$build_old_libs"; then
+		build_libtool_libs=module
+		build_old_libs=yes
+	      else
+		build_libtool_libs=no
+	      fi
+	    fi
+	  else
+	    deplibs="$dir/$old_library $deplibs"
+	    link_static=yes
+	  fi
+	fi # link shared/static library?
+
+	if test lib = "$linkmode"; then
+	  if test -n "$dependency_libs" &&
+	     { test yes != "$hardcode_into_libs" ||
+	       test yes = "$build_old_libs" ||
+	       test yes = "$link_static"; }; then
+	    # Extract -R from dependency_libs
+	    temp_deplibs=
+	    for libdir in $dependency_libs; do
+	      case $libdir in
+	      -R*) func_stripname '-R' '' "$libdir"
+	           temp_xrpath=$func_stripname_result
+		   case " $xrpath " in
+		   *" $temp_xrpath "*) ;;
+		   *) func_append xrpath " $temp_xrpath";;
+		   esac;;
+	      *) func_append temp_deplibs " $libdir";;
+	      esac
+	    done
+	    dependency_libs=$temp_deplibs
+	  fi
+
+	  func_append newlib_search_path " $absdir"
+	  # Link against this library
+	  test no = "$link_static" && newdependency_libs="$abs_ladir/$laname $newdependency_libs"
+	  # ... and its dependency_libs
+	  tmp_libs=
+	  for deplib in $dependency_libs; do
+	    newdependency_libs="$deplib $newdependency_libs"
+	    case $deplib in
+              -L*) func_stripname '-L' '' "$deplib"
+                   func_resolve_sysroot "$func_stripname_result";;
+              *) func_resolve_sysroot "$deplib" ;;
+            esac
+	    if $opt_preserve_dup_deps; then
+	      case "$tmp_libs " in
+	      *" $func_resolve_sysroot_result "*)
+                func_append specialdeplibs " $func_resolve_sysroot_result" ;;
+	      esac
+	    fi
+	    func_append tmp_libs " $func_resolve_sysroot_result"
+	  done
+
+	  if test no != "$link_all_deplibs"; then
+	    # Add the search paths of all dependency libraries
+	    for deplib in $dependency_libs; do
+	      path=
+	      case $deplib in
+	      -L*) path=$deplib ;;
+	      *.la)
+	        func_resolve_sysroot "$deplib"
+	        deplib=$func_resolve_sysroot_result
+	        func_dirname "$deplib" "" "."
+		dir=$func_dirname_result
+		# We need an absolute path.
+		case $dir in
+		[\\/]* | [A-Za-z]:[\\/]*) absdir=$dir ;;
+		*)
+		  absdir=`cd "$dir" && pwd`
+		  if test -z "$absdir"; then
+		    func_warning "cannot determine absolute directory name of '$dir'"
+		    absdir=$dir
+		  fi
+		  ;;
+		esac
+		if $GREP "^installed=no" $deplib > /dev/null; then
+		case $host in
+		*-*-darwin*)
+		  depdepl=
+		  eval deplibrary_names=`$SED -n -e 's/^library_names=\(.*\)$/\1/p' $deplib`
+		  if test -n "$deplibrary_names"; then
+		    for tmp in $deplibrary_names; do
+		      depdepl=$tmp
+		    done
+		    if test -f "$absdir/$objdir/$depdepl"; then
+		      depdepl=$absdir/$objdir/$depdepl
+		      darwin_install_name=`$OTOOL -L $depdepl | awk '{if (NR == 2) {print $1;exit}}'`
+                      if test -z "$darwin_install_name"; then
+                          darwin_install_name=`$OTOOL64 -L $depdepl  | awk '{if (NR == 2) {print $1;exit}}'`
+                      fi
+		      func_append compiler_flags " $wl-dylib_file $wl$darwin_install_name:$depdepl"
+		      func_append linker_flags " -dylib_file $darwin_install_name:$depdepl"
+		      path=
+		    fi
+		  fi
+		  ;;
+		*)
+		  path=-L$absdir/$objdir
+		  ;;
+		esac
+		else
+		  eval libdir=`$SED -n -e 's/^libdir=\(.*\)$/\1/p' $deplib`
+		  test -z "$libdir" && \
+		    func_fatal_error "'$deplib' is not a valid libtool archive"
+		  test "$absdir" != "$libdir" && \
+		    func_warning "'$deplib' seems to be moved"
+
+		  path=-L$absdir
+		fi
+		;;
+	      esac
+	      case " $deplibs " in
+	      *" $path "*) ;;
+	      *) deplibs="$path $deplibs" ;;
+	      esac
+	    done
+	  fi # link_all_deplibs != no
+	fi # linkmode = lib
+      done # for deplib in $libs
+      if test link = "$pass"; then
+	if test prog = "$linkmode"; then
+	  compile_deplibs="$new_inherited_linker_flags $compile_deplibs"
+	  finalize_deplibs="$new_inherited_linker_flags $finalize_deplibs"
+	else
+	  compiler_flags="$compiler_flags "`$ECHO " $new_inherited_linker_flags" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'`
+	fi
+      fi
+      dependency_libs=$newdependency_libs
+      if test dlpreopen = "$pass"; then
+	# Link the dlpreopened libraries before other libraries
+	for deplib in $save_deplibs; do
+	  deplibs="$deplib $deplibs"
+	done
+      fi
+      if test dlopen != "$pass"; then
+	test conv = "$pass" || {
+	  # Make sure lib_search_path contains only unique directories.
+	  lib_search_path=
+	  for dir in $newlib_search_path; do
+	    case "$lib_search_path " in
+	    *" $dir "*) ;;
+	    *) func_append lib_search_path " $dir" ;;
+	    esac
+	  done
+	  newlib_search_path=
+	}
+
+	if test prog,link = "$linkmode,$pass"; then
+	  vars="compile_deplibs finalize_deplibs"
+	else
+	  vars=deplibs
+	fi
+	for var in $vars dependency_libs; do
+	  # Add libraries to $var in reverse order
+	  eval tmp_libs=\"\$$var\"
+	  new_libs=
+	  for deplib in $tmp_libs; do
+	    # FIXME: Pedantically, this is the right thing to do, so
+	    #        that some nasty dependency loop isn't accidentally
+	    #        broken:
+	    #new_libs="$deplib $new_libs"
+	    # Pragmatically, this seems to cause very few problems in
+	    # practice:
+	    case $deplib in
+	    -L*) new_libs="$deplib $new_libs" ;;
+	    -R*) ;;
+	    *)
+	      # And here is the reason: when a library appears more
+	      # than once as an explicit dependence of a library, or
+	      # is implicitly linked in more than once by the
+	      # compiler, it is considered special, and multiple
+	      # occurrences thereof are not removed.  Compare this
+	      # with having the same library being listed as a
+	      # dependency of multiple other libraries: in this case,
+	      # we know (pedantically, we assume) the library does not
+	      # need to be listed more than once, so we keep only the
+	      # last copy.  This is not always right, but it is rare
+	      # enough that we require users that really mean to play
+	      # such unportable linking tricks to link the library
+	      # using -Wl,-lname, so that libtool does not consider it
+	      # for duplicate removal.
+	      case " $specialdeplibs " in
+	      *" $deplib "*) new_libs="$deplib $new_libs" ;;
+	      *)
+		case " $new_libs " in
+		*" $deplib "*) ;;
+		*) new_libs="$deplib $new_libs" ;;
+		esac
+		;;
+	      esac
+	      ;;
+	    esac
+	  done
+	  tmp_libs=
+	  for deplib in $new_libs; do
+	    case $deplib in
+	    -L*)
+	      case " $tmp_libs " in
+	      *" $deplib "*) ;;
+	      *) func_append tmp_libs " $deplib" ;;
+	      esac
+	      ;;
+	    *) func_append tmp_libs " $deplib" ;;
+	    esac
+	  done
+	  eval $var=\"$tmp_libs\"
+	done # for var
+      fi
+
+      # Add Sun CC postdeps if required:
+      test CXX = "$tagname" && {
+        case $host_os in
+        linux*)
+          case `$CC -V 2>&1 | $SED 5q` in
+          *Sun\ C*) # Sun C++ 5.9
+            func_suncc_cstd_abi
+
+            if test no != "$suncc_use_cstd_abi"; then
+              func_append postdeps ' -library=Cstd -library=Crun'
+            fi
+            ;;
+          esac
+          ;;
+
+        solaris*)
+          func_cc_basename "$CC"
+          case $func_cc_basename_result in
+          CC* | sunCC*)
+            func_suncc_cstd_abi
+
+            if test no != "$suncc_use_cstd_abi"; then
+              func_append postdeps ' -library=Cstd -library=Crun'
+            fi
+            ;;
+          esac
+          ;;
+        esac
+      }
+
+      # Last step: remove runtime libs from dependency_libs
+      # (they stay in deplibs)
+      tmp_libs=
+      for i in $dependency_libs; do
+	case " $predeps $postdeps $compiler_lib_search_path " in
+	*" $i "*)
+	  i=
+	  ;;
+	esac
+	if test -n "$i"; then
+	  func_append tmp_libs " $i"
+	fi
+      done
+      dependency_libs=$tmp_libs
+    done # for pass
+    if test prog = "$linkmode"; then
+      dlfiles=$newdlfiles
+    fi
+    if test prog = "$linkmode" || test lib = "$linkmode"; then
+      dlprefiles=$newdlprefiles
+    fi
+
+    case $linkmode in
+    oldlib)
+      if test -n "$dlfiles$dlprefiles" || test no != "$dlself"; then
+	func_warning "'-dlopen' is ignored for archives"
+      fi
+
+      case " $deplibs" in
+      *\ -l* | *\ -L*)
+	func_warning "'-l' and '-L' are ignored for archives" ;;
+      esac
+
+      test -n "$rpath" && \
+	func_warning "'-rpath' is ignored for archives"
+
+      test -n "$xrpath" && \
+	func_warning "'-R' is ignored for archives"
+
+      test -n "$vinfo" && \
+	func_warning "'-version-info/-version-number' is ignored for archives"
+
+      test -n "$release" && \
+	func_warning "'-release' is ignored for archives"
+
+      test -n "$export_symbols$export_symbols_regex" && \
+	func_warning "'-export-symbols' is ignored for archives"
+
+      # Now set the variables for building old libraries.
+      build_libtool_libs=no
+      oldlibs=$output
+      func_append objs "$old_deplibs"
+      ;;
+
+    lib)
+      # Make sure we only generate libraries of the form 'libNAME.la'.
+      case $outputname in
+      lib*)
+	func_stripname 'lib' '.la' "$outputname"
+	name=$func_stripname_result
+	eval shared_ext=\"$shrext_cmds\"
+	eval libname=\"$libname_spec\"
+	;;
+      *)
+	test no = "$module" \
+	  && func_fatal_help "libtool library '$output' must begin with 'lib'"
+
+	if test no != "$need_lib_prefix"; then
+	  # Add the "lib" prefix for modules if required
+	  func_stripname '' '.la' "$outputname"
+	  name=$func_stripname_result
+	  eval shared_ext=\"$shrext_cmds\"
+	  eval libname=\"$libname_spec\"
+	else
+	  func_stripname '' '.la' "$outputname"
+	  libname=$func_stripname_result
+	fi
+	;;
+      esac
+
+      if test -n "$objs"; then
+	if test pass_all != "$deplibs_check_method"; then
+	  func_fatal_error "cannot build libtool library '$output' from non-libtool objects on this host:$objs"
+	else
+	  echo
+	  $ECHO "*** Warning: Linking the shared library $output against the non-libtool"
+	  $ECHO "*** objects $objs is not portable!"
+	  func_append libobjs " $objs"
+	fi
+      fi
+
+      test no = "$dlself" \
+	|| func_warning "'-dlopen self' is ignored for libtool libraries"
+
+      set dummy $rpath
+      shift
+      test 1 -lt "$#" \
+	&& func_warning "ignoring multiple '-rpath's for a libtool library"
+
+      install_libdir=$1
+
+      oldlibs=
+      if test -z "$rpath"; then
+	if test yes = "$build_libtool_libs"; then
+	  # Building a libtool convenience library.
+	  # Some compilers have problems with a '.al' extension so
+	  # convenience libraries should have the same extension an
+	  # archive normally would.
+	  oldlibs="$output_objdir/$libname.$libext $oldlibs"
+	  build_libtool_libs=convenience
+	  build_old_libs=yes
+	fi
+
+	test -n "$vinfo" && \
+	  func_warning "'-version-info/-version-number' is ignored for convenience libraries"
+
+	test -n "$release" && \
+	  func_warning "'-release' is ignored for convenience libraries"
+      else
+
+	# Parse the version information argument.
+	save_ifs=$IFS; IFS=:
+	set dummy $vinfo 0 0 0
+	shift
+	IFS=$save_ifs
+
+	test -n "$7" && \
+	  func_fatal_help "too many parameters to '-version-info'"
+
+	# convert absolute version numbers to libtool ages
+	# this retains compatibility with .la files and attempts
+	# to make the code below a bit more comprehensible
+
+	case $vinfo_number in
+	yes)
+	  number_major=$1
+	  number_minor=$2
+	  number_revision=$3
+	  #
+	  # There are really only two kinds -- those that
+	  # use the current revision as the major version
+	  # and those that subtract age and use age as
+	  # a minor version.  But, then there is irix
+	  # that has an extra 1 added just for fun
+	  #
+	  case $version_type in
+	  # correct linux to gnu/linux during the next big refactor
+	  darwin|freebsd-elf|linux|midnightbsd-elf|osf|windows|none)
+	    func_arith $number_major + $number_minor
+	    current=$func_arith_result
+	    age=$number_minor
+	    revision=$number_revision
+	    ;;
+	  freebsd-aout|qnx|sunos)
+	    current=$number_major
+	    revision=$number_minor
+	    age=0
+	    ;;
+	  irix|nonstopux)
+	    func_arith $number_major + $number_minor
+	    current=$func_arith_result
+	    age=$number_minor
+	    revision=$number_minor
+	    lt_irix_increment=no
+	    ;;
+	  *)
+	    func_fatal_configuration "$modename: unknown library version type '$version_type'"
+	    ;;
+	  esac
+	  ;;
+	no)
+	  current=$1
+	  revision=$2
+	  age=$3
+	  ;;
+	esac
+
+	# Check that each of the things are valid numbers.
+	case $current in
+	0|[1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-9][0-9][0-9][0-9][0-9]) ;;
+	*)
+	  func_error "CURRENT '$current' must be a nonnegative integer"
+	  func_fatal_error "'$vinfo' is not valid version information"
+	  ;;
+	esac
+
+	case $revision in
+	0|[1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-9][0-9][0-9][0-9][0-9]) ;;
+	*)
+	  func_error "REVISION '$revision' must be a nonnegative integer"
+	  func_fatal_error "'$vinfo' is not valid version information"
+	  ;;
+	esac
+
+	case $age in
+	0|[1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-9][0-9][0-9][0-9][0-9]) ;;
+	*)
+	  func_error "AGE '$age' must be a nonnegative integer"
+	  func_fatal_error "'$vinfo' is not valid version information"
+	  ;;
+	esac
+
+	if test "$age" -gt "$current"; then
+	  func_error "AGE '$age' is greater than the current interface number '$current'"
+	  func_fatal_error "'$vinfo' is not valid version information"
+	fi
+
+	# Calculate the version variables.
+	major=
+	versuffix=
+	verstring=
+	case $version_type in
+	none) ;;
+
+	darwin)
+	  # Like Linux, but with the current version available in
+	  # verstring for coding it into the library header
+	  func_arith $current - $age
+	  major=.$func_arith_result
+	  versuffix=$major.$age.$revision
+	  # Darwin ld doesn't like 0 for these options...
+	  func_arith $current + 1
+	  minor_current=$func_arith_result
+	  xlcverstring="$wl-compatibility_version $wl$minor_current $wl-current_version $wl$minor_current.$revision"
+	  verstring="-compatibility_version $minor_current -current_version $minor_current.$revision"
+          # On Darwin other compilers
+          case $CC in
+              nagfor*)
+                  verstring="$wl-compatibility_version $wl$minor_current $wl-current_version $wl$minor_current.$revision"
+                  ;;
+              *)
+                  verstring="-compatibility_version $minor_current -current_version $minor_current.$revision"
+                  ;;
+          esac
+	  ;;
+
+	freebsd-aout)
+	  major=.$current
+	  versuffix=.$current.$revision
+	  ;;
+
+	freebsd-elf | midnightbsd-elf)
+	  func_arith $current - $age
+	  major=.$func_arith_result
+	  versuffix=$major.$age.$revision
+	  ;;
+
+	irix | nonstopux)
+	  if test no = "$lt_irix_increment"; then
+	    func_arith $current - $age
+	  else
+	    func_arith $current - $age + 1
+	  fi
+	  major=$func_arith_result
+
+	  case $version_type in
+	    nonstopux) verstring_prefix=nonstopux ;;
+	    *)         verstring_prefix=sgi ;;
+	  esac
+	  verstring=$verstring_prefix$major.$revision
+
+	  # Add in all the interfaces that we are compatible with.
+	  loop=$revision
+	  while test 0 -ne "$loop"; do
+	    func_arith $revision - $loop
+	    iface=$func_arith_result
+	    func_arith $loop - 1
+	    loop=$func_arith_result
+	    verstring=$verstring_prefix$major.$iface:$verstring
+	  done
+
+	  # Before this point, $major must not contain '.'.
+	  major=.$major
+	  versuffix=$major.$revision
+	  ;;
+
+	linux) # correct to gnu/linux during the next big refactor
+	  func_arith $current - $age
+	  major=.$func_arith_result
+	  versuffix=$major.$age.$revision
+	  ;;
+
+	osf)
+	  func_arith $current - $age
+	  major=.$func_arith_result
+	  versuffix=.$current.$age.$revision
+	  verstring=$current.$age.$revision
+
+	  # Add in all the interfaces that we are compatible with.
+	  loop=$age
+	  while test 0 -ne "$loop"; do
+	    func_arith $current - $loop
+	    iface=$func_arith_result
+	    func_arith $loop - 1
+	    loop=$func_arith_result
+	    verstring=$verstring:$iface.0
+	  done
+
+	  # Make executables depend on our current version.
+	  func_append verstring ":$current.0"
+	  ;;
+
+	qnx)
+	  major=.$current
+	  versuffix=.$current
+	  ;;
+
+	sco)
+	  major=.$current
+	  versuffix=.$current
+	  ;;
+
+	sunos)
+	  major=.$current
+	  versuffix=.$current.$revision
+	  ;;
+
+	windows)
+	  # Use '-' rather than '.', since we only want one
+	  # extension on DOS 8.3 file systems.
+	  func_arith $current - $age
+	  major=$func_arith_result
+	  versuffix=-$major
+	  ;;
+
+	*)
+	  func_fatal_configuration "unknown library version type '$version_type'"
+	  ;;
+	esac
+
+	# Clear the version info if we defaulted, and they specified a release.
+	if test -z "$vinfo" && test -n "$release"; then
+	  major=
+	  case $version_type in
+	  darwin)
+	    # we can't check for "0.0" in archive_cmds due to quoting
+	    # problems, so we reset it completely
+	    verstring=
+	    ;;
+	  *)
+	    verstring=0.0
+	    ;;
+	  esac
+	  if test no = "$need_version"; then
+	    versuffix=
+	  else
+	    versuffix=.0.0
+	  fi
+	fi
+
+	# Remove version info from name if versioning should be avoided
+	if test yes,no = "$avoid_version,$need_version"; then
+	  major=
+	  versuffix=
+	  verstring=
+	fi
+
+	# Check to see if the archive will have undefined symbols.
+	if test yes = "$allow_undefined"; then
+	  if test unsupported = "$allow_undefined_flag"; then
+	    if test yes = "$build_old_libs"; then
+	      func_warning "undefined symbols not allowed in $host shared libraries; building static only"
+	      build_libtool_libs=no
+	    else
+	      func_fatal_error "can't build $host shared library unless -no-undefined is specified"
+	    fi
+	  fi
+	else
+	  # Don't allow undefined symbols.
+	  allow_undefined_flag=$no_undefined_flag
+	fi
+
+      fi
+
+      func_generate_dlsyms "$libname" "$libname" :
+      func_append libobjs " $symfileobj"
+      test " " = "$libobjs" && libobjs=
+
+      if test relink != "$opt_mode"; then
+	# Remove our outputs, but don't remove object files since they
+	# may have been created when compiling PIC objects.
+	removelist=
+	tempremovelist=`$ECHO "$output_objdir/*"`
+	for p in $tempremovelist; do
+	  case $p in
+	    *.$objext | *.gcno)
+	       ;;
+	    $output_objdir/$outputname | $output_objdir/$libname.* | $output_objdir/$libname$release.*)
+	       if test -n "$precious_files_regex"; then
+		 if $ECHO "$p" | $EGREP -e "$precious_files_regex" >/dev/null 2>&1
+		 then
+		   continue
+		 fi
+	       fi
+	       func_append removelist " $p"
+	       ;;
+	    *) ;;
+	  esac
+	done
+	test -n "$removelist" && \
+	  func_show_eval "${RM}r \$removelist"
+      fi
+
+      # Now set the variables for building old libraries.
+      if test yes = "$build_old_libs" && test convenience != "$build_libtool_libs"; then
+	func_append oldlibs " $output_objdir/$libname.$libext"
+
+	# Transform .lo files to .o files.
+	oldobjs="$objs "`$ECHO "$libobjs" | $SP2NL | $SED "/\.$libext$/d; $lo2o" | $NL2SP`
+      fi
+
+      # Eliminate all temporary directories.
+      #for path in $notinst_path; do
+      #	lib_search_path=`$ECHO "$lib_search_path " | $SED "s% $path % %g"`
+      #	deplibs=`$ECHO "$deplibs " | $SED "s% -L$path % %g"`
+      #	dependency_libs=`$ECHO "$dependency_libs " | $SED "s% -L$path % %g"`
+      #done
+
+      if test -n "$xrpath"; then
+	# If the user specified any rpath flags, then add them.
+	temp_xrpath=
+	for libdir in $xrpath; do
+	  func_replace_sysroot "$libdir"
+	  func_append temp_xrpath " -R$func_replace_sysroot_result"
+	  case "$finalize_rpath " in
+	  *" $libdir "*) ;;
+	  *) func_append finalize_rpath " $libdir" ;;
+	  esac
+	done
+	if test yes != "$hardcode_into_libs" || test yes = "$build_old_libs"; then
+	  dependency_libs="$temp_xrpath $dependency_libs"
+	fi
+      fi
+
+      # Make sure dlfiles contains only unique files that won't be dlpreopened
+      old_dlfiles=$dlfiles
+      dlfiles=
+      for lib in $old_dlfiles; do
+	case " $dlprefiles $dlfiles " in
+	*" $lib "*) ;;
+	*) func_append dlfiles " $lib" ;;
+	esac
+      done
+
+      # Make sure dlprefiles contains only unique files
+      old_dlprefiles=$dlprefiles
+      dlprefiles=
+      for lib in $old_dlprefiles; do
+	case "$dlprefiles " in
+	*" $lib "*) ;;
+	*) func_append dlprefiles " $lib" ;;
+	esac
+      done
+
+      if test yes = "$build_libtool_libs"; then
+	if test -n "$rpath"; then
+	  case $host in
+	  *-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-*-beos* | *-cegcc* | *-*-haiku*)
+	    # these systems don't actually have a c library (as such)!
+	    ;;
+	  *-*-rhapsody* | *-*-darwin1.[012])
+	    # Rhapsody C library is in the System framework
+	    func_append deplibs " System.ltframework"
+	    ;;
+	  *-*-netbsd*)
+	    # Don't link with libc until the a.out ld.so is fixed.
+	    ;;
+	  *-*-openbsd* | *-*-freebsd* | *-*-dragonfly* | *-*-midnightbsd*)
+	    # Do not include libc due to us having libc/libc_r.
+	    ;;
+	  *-*-sco3.2v5* | *-*-sco5v6*)
+	    # Causes problems with __ctype
+	    ;;
+	  *-*-sysv4.2uw2* | *-*-sysv5* | *-*-unixware* | *-*-OpenUNIX*)
+	    # Compiler inserts libc in the correct place for threads to work
+	    ;;
+	  *)
+	    # Add libc to deplibs on all other systems if necessary.
+	    if test yes = "$build_libtool_need_lc"; then
+	      func_append deplibs " -lc"
+	    fi
+	    ;;
+	  esac
+	fi
+
+	# Transform deplibs into only deplibs that can be linked in shared.
+	name_save=$name
+	libname_save=$libname
+	release_save=$release
+	versuffix_save=$versuffix
+	major_save=$major
+	# I'm not sure if I'm treating the release correctly.  I think
+	# release should show up in the -l (ie -lgmp5) so we don't want to
+	# add it in twice.  Is that correct?
+	release=
+	versuffix=
+	major=
+	newdeplibs=
+	droppeddeps=no
+	case $deplibs_check_method in
+	pass_all)
+	  # Don't check for shared/static.  Everything works.
+	  # This might be a little naive.  We might want to check
+	  # whether the library exists or not.  But this is on
+	  # osf3 & osf4 and I'm not really sure... Just
+	  # implementing what was already the behavior.
+	  newdeplibs=$deplibs
+	  ;;
+	test_compile)
+	  # This code stresses the "libraries are programs" paradigm to its
+	  # limits. Maybe even breaks it.  We compile a program, linking it
+	  # against the deplibs as a proxy for the library.  Then we can check
+	  # whether they linked in statically or dynamically with ldd.
+	  $opt_dry_run || $RM conftest.c
+	  cat > conftest.c <<EOF
+	  int main() { return 0; }
+EOF
+	  $opt_dry_run || $RM conftest
+	  if $LTCC $LTCFLAGS -o conftest conftest.c $deplibs; then
+	    ldd_output=`ldd conftest`
+	    for i in $deplibs; do
+	      case $i in
+	      -l*)
+		func_stripname -l '' "$i"
+		name=$func_stripname_result
+		if test yes = "$allow_libtool_libs_with_static_runtimes"; then
+		  case " $predeps $postdeps " in
+		  *" $i "*)
+		    func_append newdeplibs " $i"
+		    i=
+		    ;;
+		  esac
+		fi
+		if test -n "$i"; then
+		  libname=`eval "\\$ECHO \"$libname_spec\""`
+		  deplib_matches=`eval "\\$ECHO \"$library_names_spec\""`
+		  set dummy $deplib_matches; shift
+		  deplib_match=$1
+		  if test `expr "$ldd_output" : ".*$deplib_match"` -ne 0; then
+		    func_append newdeplibs " $i"
+		  else
+		    droppeddeps=yes
+		    echo
+		    $ECHO "*** Warning: dynamic linker does not accept needed library $i."
+		    echo "*** I have the capability to make that library automatically link in when"
+		    echo "*** you link to this library.  But I can only do this if you have a"
+		    echo "*** shared version of the library, which I believe you do not have"
+		    echo "*** because a test_compile did reveal that the linker did not use it for"
+		    echo "*** its dynamic dependency list that programs get resolved with at runtime."
+		  fi
+		fi
+		;;
+	      *)
+		func_append newdeplibs " $i"
+		;;
+	      esac
+	    done
+	  else
+	    # Error occurred in the first compile.  Let's try to salvage
+	    # the situation: Compile a separate program for each library.
+	    for i in $deplibs; do
+	      case $i in
+	      -l*)
+		func_stripname -l '' "$i"
+		name=$func_stripname_result
+		$opt_dry_run || $RM conftest
+		if $LTCC $LTCFLAGS -o conftest conftest.c $i; then
+		  ldd_output=`ldd conftest`
+		  if test yes = "$allow_libtool_libs_with_static_runtimes"; then
+		    case " $predeps $postdeps " in
+		    *" $i "*)
+		      func_append newdeplibs " $i"
+		      i=
+		      ;;
+		    esac
+		  fi
+		  if test -n "$i"; then
+		    libname=`eval "\\$ECHO \"$libname_spec\""`
+		    deplib_matches=`eval "\\$ECHO \"$library_names_spec\""`
+		    set dummy $deplib_matches; shift
+		    deplib_match=$1
+		    if test `expr "$ldd_output" : ".*$deplib_match"` -ne 0; then
+		      func_append newdeplibs " $i"
+		    else
+		      droppeddeps=yes
+		      echo
+		      $ECHO "*** Warning: dynamic linker does not accept needed library $i."
+		      echo "*** I have the capability to make that library automatically link in when"
+		      echo "*** you link to this library.  But I can only do this if you have a"
+		      echo "*** shared version of the library, which you do not appear to have"
+		      echo "*** because a test_compile did reveal that the linker did not use this one"
+		      echo "*** as a dynamic dependency that programs can get resolved with at runtime."
+		    fi
+		  fi
+		else
+		  droppeddeps=yes
+		  echo
+		  $ECHO "*** Warning!  Library $i is needed by this library but I was not able to"
+		  echo "*** make it link in!  You will probably need to install it or some"
+		  echo "*** library that it depends on before this library will be fully"
+		  echo "*** functional.  Installing it before continuing would be even better."
+		fi
+		;;
+	      *)
+		func_append newdeplibs " $i"
+		;;
+	      esac
+	    done
+	  fi
+	  ;;
+	file_magic*)
+	  set dummy $deplibs_check_method; shift
+	  file_magic_regex=`expr "$deplibs_check_method" : "$1 \(.*\)"`
+	  for a_deplib in $deplibs; do
+	    case $a_deplib in
+	    -l*)
+	      func_stripname -l '' "$a_deplib"
+	      name=$func_stripname_result
+	      if test yes = "$allow_libtool_libs_with_static_runtimes"; then
+		case " $predeps $postdeps " in
+		*" $a_deplib "*)
+		  func_append newdeplibs " $a_deplib"
+		  a_deplib=
+		  ;;
+		esac
+	      fi
+	      if test -n "$a_deplib"; then
+		libname=`eval "\\$ECHO \"$libname_spec\""`
+		if test -n "$file_magic_glob"; then
+		  libnameglob=`func_echo_all "$libname" | $SED -e $file_magic_glob`
+		else
+		  libnameglob=$libname
+		fi
+		test yes = "$want_nocaseglob" && nocaseglob=`shopt -p nocaseglob`
+		for i in $lib_search_path $sys_lib_search_path $shlib_search_path; do
+		  if test yes = "$want_nocaseglob"; then
+		    shopt -s nocaseglob
+		    potential_libs=`ls $i/$libnameglob[.-]* 2>/dev/null`
+		    $nocaseglob
+		  else
+		    potential_libs=`ls $i/$libnameglob[.-]* 2>/dev/null`
+		  fi
+		  for potent_lib in $potential_libs; do
+		      # Follow soft links.
+		      if ls -lLd "$potent_lib" 2>/dev/null |
+			 $GREP " -> " >/dev/null; then
+			continue
+		      fi
+		      # The statement above tries to avoid entering an
+		      # endless loop below, in case of cyclic links.
+		      # We might still enter an endless loop, since a link
+		      # loop can be closed while we follow links,
+		      # but so what?
+		      potlib=$potent_lib
+		      while test -h "$potlib" 2>/dev/null; do
+			potliblink=`ls -ld $potlib | $SED 's/.* -> //'`
+			case $potliblink in
+			[\\/]* | [A-Za-z]:[\\/]*) potlib=$potliblink;;
+			*) potlib=`$ECHO "$potlib" | $SED 's|[^/]*$||'`"$potliblink";;
+			esac
+		      done
+		      if eval $file_magic_cmd \"\$potlib\" 2>/dev/null |
+			 $SED -e 10q |
+			 $EGREP "$file_magic_regex" > /dev/null; then
+			func_append newdeplibs " $a_deplib"
+			a_deplib=
+			break 2
+		      fi
+		  done
+		done
+	      fi
+	      if test -n "$a_deplib"; then
+		droppeddeps=yes
+		echo
+		$ECHO "*** Warning: linker path does not have real file for library $a_deplib."
+		echo "*** I have the capability to make that library automatically link in when"
+		echo "*** you link to this library.  But I can only do this if you have a"
+		echo "*** shared version of the library, which you do not appear to have"
+		echo "*** because I did check the linker path looking for a file starting"
+		if test -z "$potlib"; then
+		  $ECHO "*** with $libname but no candidates were found. (...for file magic test)"
+		else
+		  $ECHO "*** with $libname and none of the candidates passed a file format test"
+		  $ECHO "*** using a file magic. Last file checked: $potlib"
+		fi
+	      fi
+	      ;;
+	    *)
+	      # Add a -L argument.
+	      func_append newdeplibs " $a_deplib"
+	      ;;
+	    esac
+	  done # Gone through all deplibs.
+	  ;;
+	match_pattern*)
+	  set dummy $deplibs_check_method; shift
+	  match_pattern_regex=`expr "$deplibs_check_method" : "$1 \(.*\)"`
+	  for a_deplib in $deplibs; do
+	    case $a_deplib in
+	    -l*)
+	      func_stripname -l '' "$a_deplib"
+	      name=$func_stripname_result
+	      if test yes = "$allow_libtool_libs_with_static_runtimes"; then
+		case " $predeps $postdeps " in
+		*" $a_deplib "*)
+		  func_append newdeplibs " $a_deplib"
+		  a_deplib=
+		  ;;
+		esac
+	      fi
+	      if test -n "$a_deplib"; then
+		libname=`eval "\\$ECHO \"$libname_spec\""`
+		for i in $lib_search_path $sys_lib_search_path $shlib_search_path; do
+		  potential_libs=`ls $i/$libname[.-]* 2>/dev/null`
+		  for potent_lib in $potential_libs; do
+		    potlib=$potent_lib # see symlink-check above in file_magic test
+		    if eval "\$ECHO \"$potent_lib\"" 2>/dev/null | $SED 10q | \
+		       $EGREP "$match_pattern_regex" > /dev/null; then
+		      func_append newdeplibs " $a_deplib"
+		      a_deplib=
+		      break 2
+		    fi
+		  done
+		done
+	      fi
+	      if test -n "$a_deplib"; then
+		droppeddeps=yes
+		echo
+		$ECHO "*** Warning: linker path does not have real file for library $a_deplib."
+		echo "*** I have the capability to make that library automatically link in when"
+		echo "*** you link to this library.  But I can only do this if you have a"
+		echo "*** shared version of the library, which you do not appear to have"
+		echo "*** because I did check the linker path looking for a file starting"
+		if test -z "$potlib"; then
+		  $ECHO "*** with $libname but no candidates were found. (...for regex pattern test)"
+		else
+		  $ECHO "*** with $libname and none of the candidates passed a file format test"
+		  $ECHO "*** using a regex pattern. Last file checked: $potlib"
+		fi
+	      fi
+	      ;;
+	    *)
+	      # Add a -L argument.
+	      func_append newdeplibs " $a_deplib"
+	      ;;
+	    esac
+	  done # Gone through all deplibs.
+	  ;;
+	none | unknown | *)
+	  newdeplibs=
+	  tmp_deplibs=`$ECHO " $deplibs" | $SED 's/ -lc$//; s/ -[LR][^ ]*//g'`
+	  if test yes = "$allow_libtool_libs_with_static_runtimes"; then
+	    for i in $predeps $postdeps; do
+	      # can't use Xsed below, because $i might contain '/'
+	      tmp_deplibs=`$ECHO " $tmp_deplibs" | $SED "s|$i||"`
+	    done
+	  fi
+	  case $tmp_deplibs in
+	  *[!\	\ ]*)
+	    echo
+	    if test none = "$deplibs_check_method"; then
+	      echo "*** Warning: inter-library dependencies are not supported in this platform."
+	    else
+	      echo "*** Warning: inter-library dependencies are not known to be supported."
+	    fi
+	    echo "*** All declared inter-library dependencies are being dropped."
+	    droppeddeps=yes
+	    ;;
+	  esac
+	  ;;
+	esac
+	versuffix=$versuffix_save
+	major=$major_save
+	release=$release_save
+	libname=$libname_save
+	name=$name_save
+
+	case $host in
+	*-*-rhapsody* | *-*-darwin1.[012])
+	  # On Rhapsody replace the C library with the System framework
+	  newdeplibs=`$ECHO " $newdeplibs" | $SED 's/ -lc / System.ltframework /'`
+	  ;;
+	esac
+
+	if test yes = "$droppeddeps"; then
+	  if test yes = "$module"; then
+	    echo
+	    echo "*** Warning: libtool could not satisfy all declared inter-library"
+	    $ECHO "*** dependencies of module $libname.  Therefore, libtool will create"
+	    echo "*** a static module, that should work as long as the dlopening"
+	    echo "*** application is linked with the -dlopen flag."
+	    if test -z "$global_symbol_pipe"; then
+	      echo
+	      echo "*** However, this would only work if libtool was able to extract symbol"
+	      echo "*** lists from a program, using 'nm' or equivalent, but libtool could"
+	      echo "*** not find such a program.  So, this module is probably useless."
+	      echo "*** 'nm' from GNU binutils and a full rebuild may help."
+	    fi
+	    if test no = "$build_old_libs"; then
+	      oldlibs=$output_objdir/$libname.$libext
+	      build_libtool_libs=module
+	      build_old_libs=yes
+	    else
+	      build_libtool_libs=no
+	    fi
+	  else
+	    echo "*** The inter-library dependencies that have been dropped here will be"
+	    echo "*** automatically added whenever a program is linked with this library"
+	    echo "*** or is declared to -dlopen it."
+
+	    if test no = "$allow_undefined"; then
+	      echo
+	      echo "*** Since this library must not contain undefined symbols,"
+	      echo "*** because either the platform does not support them or"
+	      echo "*** it was explicitly requested with -no-undefined,"
+	      echo "*** libtool will only create a static version of it."
+	      if test no = "$build_old_libs"; then
+		oldlibs=$output_objdir/$libname.$libext
+		build_libtool_libs=module
+		build_old_libs=yes
+	      else
+		build_libtool_libs=no
+	      fi
+	    fi
+	  fi
+	fi
+	# Done checking deplibs!
+	deplibs=$newdeplibs
+      fi
+      # Time to change all our "foo.ltframework" stuff back to "-framework foo"
+      case $host in
+	*-*-darwin*)
+	  newdeplibs=`$ECHO " $newdeplibs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'`
+	  new_inherited_linker_flags=`$ECHO " $new_inherited_linker_flags" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'`
+	  deplibs=`$ECHO " $deplibs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'`
+	  ;;
+      esac
+
+      # move library search paths that coincide with paths to not yet
+      # installed libraries to the beginning of the library search list
+      new_libs=
+      for path in $notinst_path; do
+	case " $new_libs " in
+	*" -L$path/$objdir "*) ;;
+	*)
+	  case " $deplibs " in
+	  *" -L$path/$objdir "*)
+	    func_append new_libs " -L$path/$objdir" ;;
+	  esac
+	  ;;
+	esac
+      done
+      for deplib in $deplibs; do
+	case $deplib in
+	-L*)
+	  case " $new_libs " in
+	  *" $deplib "*) ;;
+	  *) func_append new_libs " $deplib" ;;
+	  esac
+	  ;;
+	*) func_append new_libs " $deplib" ;;
+	esac
+      done
+      deplibs=$new_libs
+
+      # All the library-specific variables (install_libdir is set above).
+      library_names=
+      old_library=
+      dlname=
+
+      # Test again, we may have decided not to build it any more
+      if test yes = "$build_libtool_libs"; then
+	# Remove $wl instances when linking with ld.
+	# FIXME: should test the right _cmds variable.
+	case $archive_cmds in
+	  *\$LD\ *) wl= ;;
+        esac
+	if test yes = "$hardcode_into_libs"; then
+	  # Hardcode the library paths
+	  hardcode_libdirs=
+	  dep_rpath=
+	  rpath=$finalize_rpath
+	  test relink = "$opt_mode" || rpath=$compile_rpath$rpath
+	  for libdir in $rpath; do
+	    if test -n "$hardcode_libdir_flag_spec"; then
+	      if test -n "$hardcode_libdir_separator"; then
+		func_replace_sysroot "$libdir"
+		libdir=$func_replace_sysroot_result
+		if test -z "$hardcode_libdirs"; then
+		  hardcode_libdirs=$libdir
+		else
+		  # Just accumulate the unique libdirs.
+		  case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in
+		  *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*)
+		    ;;
+		  *)
+		    func_append hardcode_libdirs "$hardcode_libdir_separator$libdir"
+		    ;;
+		  esac
+		fi
+	      else
+		eval flag=\"$hardcode_libdir_flag_spec\"
+		func_append dep_rpath " $flag"
+	      fi
+	    elif test -n "$runpath_var"; then
+	      case "$perm_rpath " in
+	      *" $libdir "*) ;;
+	      *) func_append perm_rpath " $libdir" ;;
+	      esac
+	    fi
+	  done
+	  # Substitute the hardcoded libdirs into the rpath.
+	  if test -n "$hardcode_libdir_separator" &&
+	     test -n "$hardcode_libdirs"; then
+	    libdir=$hardcode_libdirs
+	    eval "dep_rpath=\"$hardcode_libdir_flag_spec\""
+	  fi
+	  if test -n "$runpath_var" && test -n "$perm_rpath"; then
+	    # We should set the runpath_var.
+	    rpath=
+	    for dir in $perm_rpath; do
+	      func_append rpath "$dir:"
+	    done
+	    eval "$runpath_var='$rpath\$$runpath_var'; export $runpath_var"
+	  fi
+	  test -n "$dep_rpath" && deplibs="$dep_rpath $deplibs"
+	fi
+
+	shlibpath=$finalize_shlibpath
+	test relink = "$opt_mode" || shlibpath=$compile_shlibpath$shlibpath
+	if test -n "$shlibpath"; then
+	  eval "$shlibpath_var='$shlibpath\$$shlibpath_var'; export $shlibpath_var"
+	fi
+
+	# Get the real and link names of the library.
+	eval shared_ext=\"$shrext_cmds\"
+	eval library_names=\"$library_names_spec\"
+	set dummy $library_names
+	shift
+	realname=$1
+	shift
+
+	if test -n "$soname_spec"; then
+	  eval soname=\"$soname_spec\"
+	else
+	  soname=$realname
+	fi
+	if test -z "$dlname"; then
+	  dlname=$soname
+	fi
+
+	lib=$output_objdir/$realname
+	linknames=
+	for link
+	do
+	  func_append linknames " $link"
+	done
+
+	# Use standard objects if they are pic
+	test -z "$pic_flag" && libobjs=`$ECHO "$libobjs" | $SP2NL | $SED "$lo2o" | $NL2SP`
+	test "X$libobjs" = "X " && libobjs=
+
+	delfiles=
+	if test -n "$export_symbols" && test -n "$include_expsyms"; then
+	  $opt_dry_run || cp "$export_symbols" "$output_objdir/$libname.uexp"
+	  export_symbols=$output_objdir/$libname.uexp
+	  func_append delfiles " $export_symbols"
+	fi
+
+	orig_export_symbols=
+	case $host_os in
+	cygwin* | mingw* | cegcc*)
+	  if test -n "$export_symbols" && test -z "$export_symbols_regex"; then
+	    # exporting using user supplied symfile
+	    func_dll_def_p "$export_symbols" || {
+	      # and it's NOT already a .def file. Must figure out
+	      # which of the given symbols are data symbols and tag
+	      # them as such. So, trigger use of export_symbols_cmds.
+	      # export_symbols gets reassigned inside the "prepare
+	      # the list of exported symbols" if statement, so the
+	      # include_expsyms logic still works.
+	      orig_export_symbols=$export_symbols
+	      export_symbols=
+	      always_export_symbols=yes
+	    }
+	  fi
+	  ;;
+	esac
+
+	# Prepare the list of exported symbols
+	if test -z "$export_symbols"; then
+	  if test yes = "$always_export_symbols" || test -n "$export_symbols_regex"; then
+	    func_verbose "generating symbol list for '$libname.la'"
+	    export_symbols=$output_objdir/$libname.exp
+	    $opt_dry_run || $RM $export_symbols
+	    cmds=$export_symbols_cmds
+	    save_ifs=$IFS; IFS='~'
+	    for cmd1 in $cmds; do
+	      IFS=$save_ifs
+	      # Take the normal branch if the nm_file_list_spec branch
+	      # doesn't work or if tool conversion is not needed.
+	      case $nm_file_list_spec~$to_tool_file_cmd in
+		*~func_convert_file_noop | *~func_convert_file_msys_to_w32 | ~*)
+		  try_normal_branch=yes
+		  eval cmd=\"$cmd1\"
+		  func_len " $cmd"
+		  len=$func_len_result
+		  ;;
+		*)
+		  try_normal_branch=no
+		  ;;
+	      esac
+	      if test yes = "$try_normal_branch" \
+		 && { test "$len" -lt "$max_cmd_len" \
+		      || test "$max_cmd_len" -le -1; }
+	      then
+		func_show_eval "$cmd" 'exit $?'
+		skipped_export=false
+	      elif test -n "$nm_file_list_spec"; then
+		func_basename "$output"
+		output_la=$func_basename_result
+		save_libobjs=$libobjs
+		save_output=$output
+		output=$output_objdir/$output_la.nm
+		func_to_tool_file "$output"
+		libobjs=$nm_file_list_spec$func_to_tool_file_result
+		func_append delfiles " $output"
+		func_verbose "creating $NM input file list: $output"
+		for obj in $save_libobjs; do
+		  func_to_tool_file "$obj"
+		  $ECHO "$func_to_tool_file_result"
+		done > "$output"
+		eval cmd=\"$cmd1\"
+		func_show_eval "$cmd" 'exit $?'
+		output=$save_output
+		libobjs=$save_libobjs
+		skipped_export=false
+	      else
+		# The command line is too long to execute in one step.
+		func_verbose "using reloadable object file for export list..."
+		skipped_export=:
+		# Break out early, otherwise skipped_export may be
+		# set to false by a later but shorter cmd.
+		break
+	      fi
+	    done
+	    IFS=$save_ifs
+	    if test -n "$export_symbols_regex" && test : != "$skipped_export"; then
+	      func_show_eval '$EGREP -e "$export_symbols_regex" "$export_symbols" > "${export_symbols}T"'
+	      func_show_eval '$MV "${export_symbols}T" "$export_symbols"'
+	    fi
+	  fi
+	fi
+
+	if test -n "$export_symbols" && test -n "$include_expsyms"; then
+	  tmp_export_symbols=$export_symbols
+	  test -n "$orig_export_symbols" && tmp_export_symbols=$orig_export_symbols
+	  $opt_dry_run || eval '$ECHO "$include_expsyms" | $SP2NL >> "$tmp_export_symbols"'
+	fi
+
+	if test : != "$skipped_export" && test -n "$orig_export_symbols"; then
+	  # The given exports_symbols file has to be filtered, so filter it.
+	  func_verbose "filter symbol list for '$libname.la' to tag DATA exports"
+	  # FIXME: $output_objdir/$libname.filter potentially contains lots of
+	  # 's' commands, which not all seds can handle. GNU sed should be fine
+	  # though. Also, the filter scales superlinearly with the number of
+	  # global variables. join(1) would be nice here, but unfortunately
+	  # isn't a blessed tool.
+	  $opt_dry_run || $SED -e '/[ ,]DATA/!d;s,\(.*\)\([ \,].*\),s|^\1$|\1\2|,' < $export_symbols > $output_objdir/$libname.filter
+	  func_append delfiles " $export_symbols $output_objdir/$libname.filter"
+	  export_symbols=$output_objdir/$libname.def
+	  $opt_dry_run || $SED -f $output_objdir/$libname.filter < $orig_export_symbols > $export_symbols
+	fi
+
+	tmp_deplibs=
+	for test_deplib in $deplibs; do
+	  case " $convenience " in
+	  *" $test_deplib "*) ;;
+	  *)
+	    func_append tmp_deplibs " $test_deplib"
+	    ;;
+	  esac
+	done
+	deplibs=$tmp_deplibs
+
+	if test -n "$convenience"; then
+	  if test -n "$whole_archive_flag_spec" &&
+	    test yes = "$compiler_needs_object" &&
+	    test -z "$libobjs"; then
+	    # extract the archives, so we have objects to list.
+	    # TODO: could optimize this to just extract one archive.
+	    whole_archive_flag_spec=
+	  fi
+	  if test -n "$whole_archive_flag_spec"; then
+	    save_libobjs=$libobjs
+	    eval libobjs=\"\$libobjs $whole_archive_flag_spec\"
+	    test "X$libobjs" = "X " && libobjs=
+	  else
+	    gentop=$output_objdir/${outputname}x
+	    func_append generated " $gentop"
+
+	    func_extract_archives $gentop $convenience
+	    func_append libobjs " $func_extract_archives_result"
+	    test "X$libobjs" = "X " && libobjs=
+	  fi
+	fi
+
+	if test yes = "$thread_safe" && test -n "$thread_safe_flag_spec"; then
+	  eval flag=\"$thread_safe_flag_spec\"
+	  func_append linker_flags " $flag"
+	fi
+
+	# Make a backup of the uninstalled library when relinking
+	if test relink = "$opt_mode"; then
+	  $opt_dry_run || eval '(cd $output_objdir && $RM ${realname}U && $MV $realname ${realname}U)' || exit $?
+	fi
+
+	# Do each of the archive commands.
+	if test yes = "$module" && test -n "$module_cmds"; then
+	  if test -n "$export_symbols" && test -n "$module_expsym_cmds"; then
+	    eval test_cmds=\"$module_expsym_cmds\"
+	    cmds=$module_expsym_cmds
+	  else
+	    eval test_cmds=\"$module_cmds\"
+	    cmds=$module_cmds
+	  fi
+	else
+	  if test -n "$export_symbols" && test -n "$archive_expsym_cmds"; then
+	    eval test_cmds=\"$archive_expsym_cmds\"
+	    cmds=$archive_expsym_cmds
+	  else
+	    eval test_cmds=\"$archive_cmds\"
+	    cmds=$archive_cmds
+	  fi
+	fi
+
+	if test : != "$skipped_export" &&
+	   func_len " $test_cmds" &&
+	   len=$func_len_result &&
+	   test "$len" -lt "$max_cmd_len" || test "$max_cmd_len" -le -1; then
+	  :
+	else
+	  # The command line is too long to link in one step, link piecewise
+	  # or, if using GNU ld and skipped_export is not :, use a linker
+	  # script.
+
+	  # Save the value of $output and $libobjs because we want to
+	  # use them later.  If we have whole_archive_flag_spec, we
+	  # want to use save_libobjs as it was before
+	  # whole_archive_flag_spec was expanded, because we can't
+	  # assume the linker understands whole_archive_flag_spec.
+	  # This may have to be revisited, in case too many
+	  # convenience libraries get linked in and end up exceeding
+	  # the spec.
+	  if test -z "$convenience" || test -z "$whole_archive_flag_spec"; then
+	    save_libobjs=$libobjs
+	  fi
+	  save_output=$output
+	  func_basename "$output"
+	  output_la=$func_basename_result
+
+	  # Clear the reloadable object creation command queue and
+	  # initialize k to one.
+	  test_cmds=
+	  concat_cmds=
+	  objlist=
+	  last_robj=
+	  k=1
+
+	  if test -n "$save_libobjs" && test : != "$skipped_export" && test yes = "$with_gnu_ld"; then
+	    output=$output_objdir/$output_la.lnkscript
+	    func_verbose "creating GNU ld script: $output"
+	    echo 'INPUT (' > $output
+	    for obj in $save_libobjs
+	    do
+	      func_to_tool_file "$obj"
+	      $ECHO "$func_to_tool_file_result" >> $output
+	    done
+	    echo ')' >> $output
+	    func_append delfiles " $output"
+	    func_to_tool_file "$output"
+	    output=$func_to_tool_file_result
+	  elif test -n "$save_libobjs" && test : != "$skipped_export" && test -n "$file_list_spec"; then
+	    output=$output_objdir/$output_la.lnk
+	    func_verbose "creating linker input file list: $output"
+	    : > $output
+	    set x $save_libobjs
+	    shift
+	    firstobj=
+	    if test yes = "$compiler_needs_object"; then
+	      firstobj="$1 "
+	      shift
+	    fi
+	    for obj
+	    do
+	      func_to_tool_file "$obj"
+	      $ECHO "$func_to_tool_file_result" >> $output
+	    done
+	    func_append delfiles " $output"
+	    func_to_tool_file "$output"
+	    output=$firstobj\"$file_list_spec$func_to_tool_file_result\"
+	  else
+	    if test -n "$save_libobjs"; then
+	      func_verbose "creating reloadable object files..."
+	      output=$output_objdir/$output_la-$k.$objext
+	      eval test_cmds=\"$reload_cmds\"
+	      func_len " $test_cmds"
+	      len0=$func_len_result
+	      len=$len0
+
+	      # Loop over the list of objects to be linked.
+	      for obj in $save_libobjs
+	      do
+		func_len " $obj"
+		func_arith $len + $func_len_result
+		len=$func_arith_result
+		if test -z "$objlist" ||
+		   test "$len" -lt "$max_cmd_len"; then
+		  func_append objlist " $obj"
+		else
+		  # The command $test_cmds is almost too long, add a
+		  # command to the queue.
+		  if test 1 -eq "$k"; then
+		    # The first file doesn't have a previous command to add.
+		    reload_objs=$objlist
+		    eval concat_cmds=\"$reload_cmds\"
+		  else
+		    # All subsequent reloadable object files will link in
+		    # the last one created.
+		    reload_objs="$objlist $last_robj"
+		    eval concat_cmds=\"\$concat_cmds~$reload_cmds~\$RM $last_robj\"
+		  fi
+		  last_robj=$output_objdir/$output_la-$k.$objext
+		  func_arith $k + 1
+		  k=$func_arith_result
+		  output=$output_objdir/$output_la-$k.$objext
+		  objlist=" $obj"
+		  func_len " $last_robj"
+		  func_arith $len0 + $func_len_result
+		  len=$func_arith_result
+		fi
+	      done
+	      # Handle the remaining objects by creating one last
+	      # reloadable object file.  All subsequent reloadable object
+	      # files will link in the last one created.
+	      test -z "$concat_cmds" || concat_cmds=$concat_cmds~
+	      reload_objs="$objlist $last_robj"
+	      eval concat_cmds=\"\$concat_cmds$reload_cmds\"
+	      if test -n "$last_robj"; then
+	        eval concat_cmds=\"\$concat_cmds~\$RM $last_robj\"
+	      fi
+	      func_append delfiles " $output"
+
+	    else
+	      output=
+	    fi
+
+	    ${skipped_export-false} && {
+	      func_verbose "generating symbol list for '$libname.la'"
+	      export_symbols=$output_objdir/$libname.exp
+	      $opt_dry_run || $RM $export_symbols
+	      libobjs=$output
+	      # Append the command to create the export file.
+	      test -z "$concat_cmds" || concat_cmds=$concat_cmds~
+	      eval concat_cmds=\"\$concat_cmds$export_symbols_cmds\"
+	      if test -n "$last_robj"; then
+		eval concat_cmds=\"\$concat_cmds~\$RM $last_robj\"
+	      fi
+	    }
+
+	    test -n "$save_libobjs" &&
+	      func_verbose "creating a temporary reloadable object file: $output"
+
+	    # Loop through the commands generated above and execute them.
+	    save_ifs=$IFS; IFS='~'
+	    for cmd in $concat_cmds; do
+	      IFS=$save_ifs
+	      $opt_quiet || {
+		  func_quote_arg expand,pretty "$cmd"
+		  eval "func_echo $func_quote_arg_result"
+	      }
+	      $opt_dry_run || eval "$cmd" || {
+		lt_exit=$?
+
+		# Restore the uninstalled library and exit
+		if test relink = "$opt_mode"; then
+		  ( cd "$output_objdir" && \
+		    $RM "${realname}T" && \
+		    $MV "${realname}U" "$realname" )
+		fi
+
+		exit $lt_exit
+	      }
+	    done
+	    IFS=$save_ifs
+
+	    if test -n "$export_symbols_regex" && ${skipped_export-false}; then
+	      func_show_eval '$EGREP -e "$export_symbols_regex" "$export_symbols" > "${export_symbols}T"'
+	      func_show_eval '$MV "${export_symbols}T" "$export_symbols"'
+	    fi
+	  fi
+
+          ${skipped_export-false} && {
+	    if test -n "$export_symbols" && test -n "$include_expsyms"; then
+	      tmp_export_symbols=$export_symbols
+	      test -n "$orig_export_symbols" && tmp_export_symbols=$orig_export_symbols
+	      $opt_dry_run || eval '$ECHO "$include_expsyms" | $SP2NL >> "$tmp_export_symbols"'
+	    fi
+
+	    if test -n "$orig_export_symbols"; then
+	      # The given exports_symbols file has to be filtered, so filter it.
+	      func_verbose "filter symbol list for '$libname.la' to tag DATA exports"
+	      # FIXME: $output_objdir/$libname.filter potentially contains lots of
+	      # 's' commands, which not all seds can handle. GNU sed should be fine
+	      # though. Also, the filter scales superlinearly with the number of
+	      # global variables. join(1) would be nice here, but unfortunately
+	      # isn't a blessed tool.
+	      $opt_dry_run || $SED -e '/[ ,]DATA/!d;s,\(.*\)\([ \,].*\),s|^\1$|\1\2|,' < $export_symbols > $output_objdir/$libname.filter
+	      func_append delfiles " $export_symbols $output_objdir/$libname.filter"
+	      export_symbols=$output_objdir/$libname.def
+	      $opt_dry_run || $SED -f $output_objdir/$libname.filter < $orig_export_symbols > $export_symbols
+	    fi
+	  }
+
+	  libobjs=$output
+	  # Restore the value of output.
+	  output=$save_output
+
+	  if test -n "$convenience" && test -n "$whole_archive_flag_spec"; then
+	    eval libobjs=\"\$libobjs $whole_archive_flag_spec\"
+	    test "X$libobjs" = "X " && libobjs=
+	  fi
+	  # Expand the library linking commands again to reset the
+	  # value of $libobjs for piecewise linking.
+
+	  # Do each of the archive commands.
+	  if test yes = "$module" && test -n "$module_cmds"; then
+	    if test -n "$export_symbols" && test -n "$module_expsym_cmds"; then
+	      cmds=$module_expsym_cmds
+	    else
+	      cmds=$module_cmds
+	    fi
+	  else
+	    if test -n "$export_symbols" && test -n "$archive_expsym_cmds"; then
+	      cmds=$archive_expsym_cmds
+	    else
+	      cmds=$archive_cmds
+	    fi
+	  fi
+	fi
+
+	if test -n "$delfiles"; then
+	  # Append the command to remove temporary files to $cmds.
+	  eval cmds=\"\$cmds~\$RM $delfiles\"
+	fi
+
+	# Add any objects from preloaded convenience libraries
+	if test -n "$dlprefiles"; then
+	  gentop=$output_objdir/${outputname}x
+	  func_append generated " $gentop"
+
+	  func_extract_archives $gentop $dlprefiles
+	  func_append libobjs " $func_extract_archives_result"
+	  test "X$libobjs" = "X " && libobjs=
+	fi
+
+	save_ifs=$IFS; IFS='~'
+	for cmd in $cmds; do
+	  IFS=$sp$nl
+	  eval cmd=\"$cmd\"
+	  IFS=$save_ifs
+	  $opt_quiet || {
+	    func_quote_arg expand,pretty "$cmd"
+	    eval "func_echo $func_quote_arg_result"
+	  }
+	  $opt_dry_run || eval "$cmd" || {
+	    lt_exit=$?
+
+	    # Restore the uninstalled library and exit
+	    if test relink = "$opt_mode"; then
+	      ( cd "$output_objdir" && \
+	        $RM "${realname}T" && \
+		$MV "${realname}U" "$realname" )
+	    fi
+
+	    exit $lt_exit
+	  }
+	done
+	IFS=$save_ifs
+
+	# Restore the uninstalled library and exit
+	if test relink = "$opt_mode"; then
+	  $opt_dry_run || eval '(cd $output_objdir && $RM ${realname}T && $MV $realname ${realname}T && $MV ${realname}U $realname)' || exit $?
+
+	  if test -n "$convenience"; then
+	    if test -z "$whole_archive_flag_spec"; then
+	      func_show_eval '${RM}r "$gentop"'
+	    fi
+	  fi
+
+	  exit $EXIT_SUCCESS
+	fi
+
+	# Create links to the real library.
+	for linkname in $linknames; do
+	  if test "$realname" != "$linkname"; then
+	    func_show_eval '(cd "$output_objdir" && $RM "$linkname" && $LN_S "$realname" "$linkname")' 'exit $?'
+	  fi
+	done
+
+	# If -module or -export-dynamic was specified, set the dlname.
+	if test yes = "$module" || test yes = "$export_dynamic"; then
+	  # On all known operating systems, these are identical.
+	  dlname=$soname
+	fi
+      fi
+      ;;
+
+    obj)
+      if test -n "$dlfiles$dlprefiles" || test no != "$dlself"; then
+	func_warning "'-dlopen' is ignored for objects"
+      fi
+
+      case " $deplibs" in
+      *\ -l* | *\ -L*)
+	func_warning "'-l' and '-L' are ignored for objects" ;;
+      esac
+
+      test -n "$rpath" && \
+	func_warning "'-rpath' is ignored for objects"
+
+      test -n "$xrpath" && \
+	func_warning "'-R' is ignored for objects"
+
+      test -n "$vinfo" && \
+	func_warning "'-version-info' is ignored for objects"
+
+      test -n "$release" && \
+	func_warning "'-release' is ignored for objects"
+
+      case $output in
+      *.lo)
+	test -n "$objs$old_deplibs" && \
+	  func_fatal_error "cannot build library object '$output' from non-libtool objects"
+
+	libobj=$output
+	func_lo2o "$libobj"
+	obj=$func_lo2o_result
+	;;
+      *)
+	libobj=
+	obj=$output
+	;;
+      esac
+
+      # Delete the old objects.
+      $opt_dry_run || $RM $obj $libobj
+
+      # Objects from convenience libraries.  This assumes
+      # single-version convenience libraries.  Whenever we create
+      # different ones for PIC/non-PIC, this we'll have to duplicate
+      # the extraction.
+      reload_conv_objs=
+      gentop=
+      # if reload_cmds runs $LD directly, get rid of -Wl from
+      # whole_archive_flag_spec and hope we can get by with turning comma
+      # into space.
+      case $reload_cmds in
+        *\$LD[\ \$]*) wl= ;;
+      esac
+      if test -n "$convenience"; then
+	if test -n "$whole_archive_flag_spec"; then
+	  eval tmp_whole_archive_flags=\"$whole_archive_flag_spec\"
+	  test -n "$wl" || tmp_whole_archive_flags=`$ECHO "$tmp_whole_archive_flags" | $SED 's|,| |g'`
+	  reload_conv_objs=$reload_objs\ $tmp_whole_archive_flags
+	else
+	  gentop=$output_objdir/${obj}x
+	  func_append generated " $gentop"
+
+	  func_extract_archives $gentop $convenience
+	  reload_conv_objs="$reload_objs $func_extract_archives_result"
+	fi
+      fi
+
+      # If we're not building shared, we need to use non_pic_objs
+      test yes = "$build_libtool_libs" || libobjs=$non_pic_objects
+
+      # Create the old-style object.
+      reload_objs=$objs$old_deplibs' '`$ECHO "$libobjs" | $SP2NL | $SED "/\.$libext$/d; /\.lib$/d; $lo2o" | $NL2SP`' '$reload_conv_objs
+
+      output=$obj
+      func_execute_cmds "$reload_cmds" 'exit $?'
+
+      # Exit if we aren't doing a library object file.
+      if test -z "$libobj"; then
+	if test -n "$gentop"; then
+	  func_show_eval '${RM}r "$gentop"'
+	fi
+
+	exit $EXIT_SUCCESS
+      fi
+
+      test yes = "$build_libtool_libs" || {
+	if test -n "$gentop"; then
+	  func_show_eval '${RM}r "$gentop"'
+	fi
+
+	# Create an invalid libtool object if no PIC, so that we don't
+	# accidentally link it into a program.
+	# $show "echo timestamp > $libobj"
+	# $opt_dry_run || eval "echo timestamp > $libobj" || exit $?
+	exit $EXIT_SUCCESS
+      }
+
+      if test -n "$pic_flag" || test default != "$pic_mode"; then
+	# Only do commands if we really have different PIC objects.
+	reload_objs="$libobjs $reload_conv_objs"
+	output=$libobj
+	func_execute_cmds "$reload_cmds" 'exit $?'
+      fi
+
+      if test -n "$gentop"; then
+	func_show_eval '${RM}r "$gentop"'
+      fi
+
+      exit $EXIT_SUCCESS
+      ;;
+
+    prog)
+      case $host in
+	*cygwin*) func_stripname '' '.exe' "$output"
+	          output=$func_stripname_result.exe;;
+      esac
+      test -n "$vinfo" && \
+	func_warning "'-version-info' is ignored for programs"
+
+      test -n "$release" && \
+	func_warning "'-release' is ignored for programs"
+
+      $preload \
+	&& test unknown,unknown,unknown = "$dlopen_support,$dlopen_self,$dlopen_self_static" \
+	&& func_warning "'LT_INIT([dlopen])' not used. Assuming no dlopen support."
+
+      case $host in
+      *-*-rhapsody* | *-*-darwin1.[012])
+	# On Rhapsody replace the C library is the System framework
+	compile_deplibs=`$ECHO " $compile_deplibs" | $SED 's/ -lc / System.ltframework /'`
+	finalize_deplibs=`$ECHO " $finalize_deplibs" | $SED 's/ -lc / System.ltframework /'`
+	;;
+      esac
+
+      case $host in
+      *-*-darwin*)
+	# Don't allow lazy linking, it breaks C++ global constructors
+	# But is supposedly fixed on 10.4 or later (yay!).
+	if test CXX = "$tagname"; then
+	  case ${MACOSX_DEPLOYMENT_TARGET-10.0} in
+	    10.[0123])
+	      func_append compile_command " $wl-bind_at_load"
+	      func_append finalize_command " $wl-bind_at_load"
+	    ;;
+	  esac
+	fi
+	# Time to change all our "foo.ltframework" stuff back to "-framework foo"
+	compile_deplibs=`$ECHO " $compile_deplibs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'`
+	finalize_deplibs=`$ECHO " $finalize_deplibs" | $SED 's% \([^ $]*\).ltframework% -framework \1%g'`
+	;;
+      esac
+
+
+      # move library search paths that coincide with paths to not yet
+      # installed libraries to the beginning of the library search list
+      new_libs=
+      for path in $notinst_path; do
+	case " $new_libs " in
+	*" -L$path/$objdir "*) ;;
+	*)
+	  case " $compile_deplibs " in
+	  *" -L$path/$objdir "*)
+	    func_append new_libs " -L$path/$objdir" ;;
+	  esac
+	  ;;
+	esac
+      done
+      for deplib in $compile_deplibs; do
+	case $deplib in
+	-L*)
+	  case " $new_libs " in
+	  *" $deplib "*) ;;
+	  *) func_append new_libs " $deplib" ;;
+	  esac
+	  ;;
+	*) func_append new_libs " $deplib" ;;
+	esac
+      done
+      compile_deplibs=$new_libs
+
+
+      func_append compile_command " $compile_deplibs"
+      func_append finalize_command " $finalize_deplibs"
+
+      if test -n "$rpath$xrpath"; then
+	# If the user specified any rpath flags, then add them.
+	for libdir in $rpath $xrpath; do
+	  # This is the magic to use -rpath.
+	  case "$finalize_rpath " in
+	  *" $libdir "*) ;;
+	  *) func_append finalize_rpath " $libdir" ;;
+	  esac
+	done
+      fi
+
+      # Now hardcode the library paths
+      rpath=
+      hardcode_libdirs=
+      for libdir in $compile_rpath $finalize_rpath; do
+	if test -n "$hardcode_libdir_flag_spec"; then
+	  if test -n "$hardcode_libdir_separator"; then
+	    if test -z "$hardcode_libdirs"; then
+	      hardcode_libdirs=$libdir
+	    else
+	      # Just accumulate the unique libdirs.
+	      case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in
+	      *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*)
+		;;
+	      *)
+		func_append hardcode_libdirs "$hardcode_libdir_separator$libdir"
+		;;
+	      esac
+	    fi
+	  else
+	    eval flag=\"$hardcode_libdir_flag_spec\"
+	    func_append rpath " $flag"
+	  fi
+	elif test -n "$runpath_var"; then
+	  case "$perm_rpath " in
+	  *" $libdir "*) ;;
+	  *) func_append perm_rpath " $libdir" ;;
+	  esac
+	fi
+	case $host in
+	*-*-cygwin* | *-*-mingw* | *-*-pw32* | *-*-os2* | *-cegcc*)
+	  testbindir=`$ECHO "$libdir" | $SED -e 's*/lib$*/bin*'`
+	  case :$dllsearchpath: in
+	  *":$libdir:"*) ;;
+	  ::) dllsearchpath=$libdir;;
+	  *) func_append dllsearchpath ":$libdir";;
+	  esac
+	  case :$dllsearchpath: in
+	  *":$testbindir:"*) ;;
+	  ::) dllsearchpath=$testbindir;;
+	  *) func_append dllsearchpath ":$testbindir";;
+	  esac
+	  ;;
+	esac
+      done
+      # Substitute the hardcoded libdirs into the rpath.
+      if test -n "$hardcode_libdir_separator" &&
+	 test -n "$hardcode_libdirs"; then
+	libdir=$hardcode_libdirs
+	eval rpath=\" $hardcode_libdir_flag_spec\"
+      fi
+      compile_rpath=$rpath
+
+      rpath=
+      hardcode_libdirs=
+      for libdir in $finalize_rpath; do
+	if test -n "$hardcode_libdir_flag_spec"; then
+	  if test -n "$hardcode_libdir_separator"; then
+	    if test -z "$hardcode_libdirs"; then
+	      hardcode_libdirs=$libdir
+	    else
+	      # Just accumulate the unique libdirs.
+	      case $hardcode_libdir_separator$hardcode_libdirs$hardcode_libdir_separator in
+	      *"$hardcode_libdir_separator$libdir$hardcode_libdir_separator"*)
+		;;
+	      *)
+		func_append hardcode_libdirs "$hardcode_libdir_separator$libdir"
+		;;
+	      esac
+	    fi
+	  else
+	    eval flag=\"$hardcode_libdir_flag_spec\"
+	    func_append rpath " $flag"
+	  fi
+	elif test -n "$runpath_var"; then
+	  case "$finalize_perm_rpath " in
+	  *" $libdir "*) ;;
+	  *) func_append finalize_perm_rpath " $libdir" ;;
+	  esac
+	fi
+      done
+      # Substitute the hardcoded libdirs into the rpath.
+      if test -n "$hardcode_libdir_separator" &&
+	 test -n "$hardcode_libdirs"; then
+	libdir=$hardcode_libdirs
+	eval rpath=\" $hardcode_libdir_flag_spec\"
+      fi
+      finalize_rpath=$rpath
+
+      if test -n "$libobjs" && test yes = "$build_old_libs"; then
+	# Transform all the library objects into standard objects.
+	compile_command=`$ECHO "$compile_command" | $SP2NL | $SED "$lo2o" | $NL2SP`
+	finalize_command=`$ECHO "$finalize_command" | $SP2NL | $SED "$lo2o" | $NL2SP`
+      fi
+
+      func_generate_dlsyms "$outputname" "@PROGRAM@" false
+
+      # template prelinking step
+      if test -n "$prelink_cmds"; then
+	func_execute_cmds "$prelink_cmds" 'exit $?'
+      fi
+
+      wrappers_required=:
+      case $host in
+      *cegcc* | *mingw32ce*)
+        # Disable wrappers for cegcc and mingw32ce hosts, we are cross compiling anyway.
+        wrappers_required=false
+        ;;
+      *cygwin* | *mingw* )
+        test yes = "$build_libtool_libs" || wrappers_required=false
+        ;;
+      *)
+        if test no = "$need_relink" || test yes != "$build_libtool_libs"; then
+          wrappers_required=false
+        fi
+        ;;
+      esac
+      $wrappers_required || {
+	# Replace the output file specification.
+	compile_command=`$ECHO "$compile_command" | $SED 's%@OUTPUT@%'"$output"'%g'`
+	link_command=$compile_command$compile_rpath
+
+	# We have no uninstalled library dependencies, so finalize right now.
+	exit_status=0
+	func_show_eval "$link_command" 'exit_status=$?'
+
+	if test -n "$postlink_cmds"; then
+	  func_to_tool_file "$output"
+	  postlink_cmds=`func_echo_all "$postlink_cmds" | $SED -e 's%@OUTPUT@%'"$output"'%g' -e 's%@TOOL_OUTPUT@%'"$func_to_tool_file_result"'%g'`
+	  func_execute_cmds "$postlink_cmds" 'exit $?'
+	fi
+
+	# Delete the generated files.
+	if test -f "$output_objdir/${outputname}S.$objext"; then
+	  func_show_eval '$RM "$output_objdir/${outputname}S.$objext"'
+	fi
+
+	exit $exit_status
+      }
+
+      if test -n "$compile_shlibpath$finalize_shlibpath"; then
+	compile_command="$shlibpath_var=\"$compile_shlibpath$finalize_shlibpath\$$shlibpath_var\" $compile_command"
+      fi
+      if test -n "$finalize_shlibpath"; then
+	finalize_command="$shlibpath_var=\"$finalize_shlibpath\$$shlibpath_var\" $finalize_command"
+      fi
+
+      compile_var=
+      finalize_var=
+      if test -n "$runpath_var"; then
+	if test -n "$perm_rpath"; then
+	  # We should set the runpath_var.
+	  rpath=
+	  for dir in $perm_rpath; do
+	    func_append rpath "$dir:"
+	  done
+	  compile_var="$runpath_var=\"$rpath\$$runpath_var\" "
+	fi
+	if test -n "$finalize_perm_rpath"; then
+	  # We should set the runpath_var.
+	  rpath=
+	  for dir in $finalize_perm_rpath; do
+	    func_append rpath "$dir:"
+	  done
+	  finalize_var="$runpath_var=\"$rpath\$$runpath_var\" "
+	fi
+      fi
+
+      if test yes = "$no_install"; then
+	# We don't need to create a wrapper script.
+	link_command=$compile_var$compile_command$compile_rpath
+	# Replace the output file specification.
+	link_command=`$ECHO "$link_command" | $SED 's%@OUTPUT@%'"$output"'%g'`
+	# Delete the old output file.
+	$opt_dry_run || $RM $output
+	# Link the executable and exit
+	func_show_eval "$link_command" 'exit $?'
+
+	if test -n "$postlink_cmds"; then
+	  func_to_tool_file "$output"
+	  postlink_cmds=`func_echo_all "$postlink_cmds" | $SED -e 's%@OUTPUT@%'"$output"'%g' -e 's%@TOOL_OUTPUT@%'"$func_to_tool_file_result"'%g'`
+	  func_execute_cmds "$postlink_cmds" 'exit $?'
+	fi
+
+	exit $EXIT_SUCCESS
+      fi
+
+      case $hardcode_action,$fast_install in
+        relink,*)
+	  # Fast installation is not supported
+	  link_command=$compile_var$compile_command$compile_rpath
+	  relink_command=$finalize_var$finalize_command$finalize_rpath
+
+	  func_warning "this platform does not like uninstalled shared libraries"
+	  func_warning "'$output' will be relinked during installation"
+	  ;;
+        *,yes)
+	  link_command=$finalize_var$compile_command$finalize_rpath
+	  relink_command=`$ECHO "$compile_var$compile_command$compile_rpath" | $SED 's%@OUTPUT@%\$progdir/\$file%g'`
+          ;;
+	*,no)
+	  link_command=$compile_var$compile_command$compile_rpath
+	  relink_command=$finalize_var$finalize_command$finalize_rpath
+          ;;
+	*,needless)
+	  link_command=$finalize_var$compile_command$finalize_rpath
+	  relink_command=
+          ;;
+      esac
+
+      # Replace the output file specification.
+      link_command=`$ECHO "$link_command" | $SED 's%@OUTPUT@%'"$output_objdir/$outputname"'%g'`
+
+      # Delete the old output files.
+      $opt_dry_run || $RM $output $output_objdir/$outputname $output_objdir/lt-$outputname
+
+      func_show_eval "$link_command" 'exit $?'
+
+      if test -n "$postlink_cmds"; then
+	func_to_tool_file "$output_objdir/$outputname"
+	postlink_cmds=`func_echo_all "$postlink_cmds" | $SED -e 's%@OUTPUT@%'"$output_objdir/$outputname"'%g' -e 's%@TOOL_OUTPUT@%'"$func_to_tool_file_result"'%g'`
+	func_execute_cmds "$postlink_cmds" 'exit $?'
+      fi
+
+      # Now create the wrapper script.
+      func_verbose "creating $output"
+
+      # Quote the relink command for shipping.
+      if test -n "$relink_command"; then
+	# Preserve any variables that may affect compiler behavior
+	for var in $variables_saved_for_relink; do
+	  if eval test -z \"\${$var+set}\"; then
+	    relink_command="{ test -z \"\${$var+set}\" || $lt_unset $var || { $var=; export $var; }; }; $relink_command"
+	  elif eval var_value=\$$var; test -z "$var_value"; then
+	    relink_command="$var=; export $var; $relink_command"
+	  else
+	    func_quote_arg pretty "$var_value"
+	    relink_command="$var=$func_quote_arg_result; export $var; $relink_command"
+	  fi
+	done
+	func_quote eval cd "`pwd`"
+	func_quote_arg pretty,unquoted "($func_quote_result; $relink_command)"
+	relink_command=$func_quote_arg_unquoted_result
+      fi
+
+      # Only actually do things if not in dry run mode.
+      $opt_dry_run || {
+	# win32 will think the script is a binary if it has
+	# a .exe suffix, so we strip it off here.
+	case $output in
+	  *.exe) func_stripname '' '.exe' "$output"
+	         output=$func_stripname_result ;;
+	esac
+	# test for cygwin because mv fails w/o .exe extensions
+	case $host in
+	  *cygwin*)
+	    exeext=.exe
+	    func_stripname '' '.exe' "$outputname"
+	    outputname=$func_stripname_result ;;
+	  *) exeext= ;;
+	esac
+	case $host in
+	  *cygwin* | *mingw* )
+	    func_dirname_and_basename "$output" "" "."
+	    output_name=$func_basename_result
+	    output_path=$func_dirname_result
+	    cwrappersource=$output_path/$objdir/lt-$output_name.c
+	    cwrapper=$output_path/$output_name.exe
+	    $RM $cwrappersource $cwrapper
+	    trap "$RM $cwrappersource $cwrapper; exit $EXIT_FAILURE" 1 2 15
+
+	    func_emit_cwrapperexe_src > $cwrappersource
+
+	    # The wrapper executable is built using the $host compiler,
+	    # because it contains $host paths and files. If cross-
+	    # compiling, it, like the target executable, must be
+	    # executed on the $host or under an emulation environment.
+	    $opt_dry_run || {
+	      $LTCC $LTCFLAGS -o $cwrapper $cwrappersource
+	      $STRIP $cwrapper
+	    }
+
+	    # Now, create the wrapper script for func_source use:
+	    func_ltwrapper_scriptname $cwrapper
+	    $RM $func_ltwrapper_scriptname_result
+	    trap "$RM $func_ltwrapper_scriptname_result; exit $EXIT_FAILURE" 1 2 15
+	    $opt_dry_run || {
+	      # note: this script will not be executed, so do not chmod.
+	      if test "x$build" = "x$host"; then
+		$cwrapper --lt-dump-script > $func_ltwrapper_scriptname_result
+	      else
+		func_emit_wrapper no > $func_ltwrapper_scriptname_result
+	      fi
+	    }
+	  ;;
+	  * )
+	    $RM $output
+	    trap "$RM $output; exit $EXIT_FAILURE" 1 2 15
+
+	    func_emit_wrapper no > $output
+	    chmod +x $output
+	  ;;
+	esac
+      }
+      exit $EXIT_SUCCESS
+      ;;
+    esac
+
+    # See if we need to build an old-fashioned archive.
+    for oldlib in $oldlibs; do
+
+      case $build_libtool_libs in
+        convenience)
+	  oldobjs="$libobjs_save $symfileobj"
+	  addlibs=$convenience
+	  build_libtool_libs=no
+	  ;;
+	module)
+	  oldobjs=$libobjs_save
+	  addlibs=$old_convenience
+	  build_libtool_libs=no
+          ;;
+	*)
+	  oldobjs="$old_deplibs $non_pic_objects"
+	  $preload && test -f "$symfileobj" \
+	    && func_append oldobjs " $symfileobj"
+	  addlibs=$old_convenience
+	  ;;
+      esac
+
+      if test -n "$addlibs"; then
+	gentop=$output_objdir/${outputname}x
+	func_append generated " $gentop"
+
+	func_extract_archives $gentop $addlibs
+	func_append oldobjs " $func_extract_archives_result"
+      fi
+
+      # Do each command in the archive commands.
+      if test -n "$old_archive_from_new_cmds" && test yes = "$build_libtool_libs"; then
+	cmds=$old_archive_from_new_cmds
+      else
+
+	# Add any objects from preloaded convenience libraries
+	if test -n "$dlprefiles"; then
+	  gentop=$output_objdir/${outputname}x
+	  func_append generated " $gentop"
+
+	  func_extract_archives $gentop $dlprefiles
+	  func_append oldobjs " $func_extract_archives_result"
+	fi
+
+	# POSIX demands no paths to be encoded in archives.  We have
+	# to avoid creating archives with duplicate basenames if we
+	# might have to extract them afterwards, e.g., when creating a
+	# static archive out of a convenience library, or when linking
+	# the entirety of a libtool archive into another (currently
+	# not supported by libtool).
+	if (for obj in $oldobjs
+	    do
+	      func_basename "$obj"
+	      $ECHO "$func_basename_result"
+	    done | sort | sort -uc >/dev/null 2>&1); then
+	  :
+	else
+	  echo "copying selected object files to avoid basename conflicts..."
+	  gentop=$output_objdir/${outputname}x
+	  func_append generated " $gentop"
+	  func_mkdir_p "$gentop"
+	  save_oldobjs=$oldobjs
+	  oldobjs=
+	  counter=1
+	  for obj in $save_oldobjs
+	  do
+	    func_basename "$obj"
+	    objbase=$func_basename_result
+	    case " $oldobjs " in
+	    " ") oldobjs=$obj ;;
+	    *[\ /]"$objbase "*)
+	      while :; do
+		# Make sure we don't pick an alternate name that also
+		# overlaps.
+		newobj=lt$counter-$objbase
+		func_arith $counter + 1
+		counter=$func_arith_result
+		case " $oldobjs " in
+		*[\ /]"$newobj "*) ;;
+		*) if test ! -f "$gentop/$newobj"; then break; fi ;;
+		esac
+	      done
+	      func_show_eval "ln $obj $gentop/$newobj || cp $obj $gentop/$newobj"
+	      func_append oldobjs " $gentop/$newobj"
+	      ;;
+	    *) func_append oldobjs " $obj" ;;
+	    esac
+	  done
+	fi
+	func_to_tool_file "$oldlib" func_convert_file_msys_to_w32
+	tool_oldlib=$func_to_tool_file_result
+	eval cmds=\"$old_archive_cmds\"
+
+	func_len " $cmds"
+	len=$func_len_result
+	if test "$len" -lt "$max_cmd_len" || test "$max_cmd_len" -le -1; then
+	  cmds=$old_archive_cmds
+	elif test -n "$archiver_list_spec"; then
+	  func_verbose "using command file archive linking..."
+	  for obj in $oldobjs
+	  do
+	    func_to_tool_file "$obj"
+	    $ECHO "$func_to_tool_file_result"
+	  done > $output_objdir/$libname.libcmd
+	  func_to_tool_file "$output_objdir/$libname.libcmd"
+	  oldobjs=" $archiver_list_spec$func_to_tool_file_result"
+	  cmds=$old_archive_cmds
+	else
+	  # the command line is too long to link in one step, link in parts
+	  func_verbose "using piecewise archive linking..."
+	  save_RANLIB=$RANLIB
+	  RANLIB=:
+	  objlist=
+	  concat_cmds=
+	  save_oldobjs=$oldobjs
+	  oldobjs=
+	  # Is there a better way of finding the last object in the list?
+	  for obj in $save_oldobjs
+	  do
+	    last_oldobj=$obj
+	  done
+	  eval test_cmds=\"$old_archive_cmds\"
+	  func_len " $test_cmds"
+	  len0=$func_len_result
+	  len=$len0
+	  for obj in $save_oldobjs
+	  do
+	    func_len " $obj"
+	    func_arith $len + $func_len_result
+	    len=$func_arith_result
+	    func_append objlist " $obj"
+	    if test "$len" -lt "$max_cmd_len"; then
+	      :
+	    else
+	      # the above command should be used before it gets too long
+	      oldobjs=$objlist
+	      if test "$obj" = "$last_oldobj"; then
+		RANLIB=$save_RANLIB
+	      fi
+	      test -z "$concat_cmds" || concat_cmds=$concat_cmds~
+	      eval concat_cmds=\"\$concat_cmds$old_archive_cmds\"
+	      objlist=
+	      len=$len0
+	    fi
+	  done
+	  RANLIB=$save_RANLIB
+	  oldobjs=$objlist
+	  if test -z "$oldobjs"; then
+	    eval cmds=\"\$concat_cmds\"
+	  else
+	    eval cmds=\"\$concat_cmds~\$old_archive_cmds\"
+	  fi
+	fi
+      fi
+      func_execute_cmds "$cmds" 'exit $?'
+    done
+
+    test -n "$generated" && \
+      func_show_eval "${RM}r$generated"
+
+    # Now create the libtool archive.
+    case $output in
+    *.la)
+      old_library=
+      test yes = "$build_old_libs" && old_library=$libname.$libext
+      func_verbose "creating $output"
+
+      # Preserve any variables that may affect compiler behavior
+      for var in $variables_saved_for_relink; do
+	if eval test -z \"\${$var+set}\"; then
+	  relink_command="{ test -z \"\${$var+set}\" || $lt_unset $var || { $var=; export $var; }; }; $relink_command"
+	elif eval var_value=\$$var; test -z "$var_value"; then
+	  relink_command="$var=; export $var; $relink_command"
+	else
+	  func_quote_arg pretty,unquoted "$var_value"
+	  relink_command="$var=$func_quote_arg_unquoted_result; export $var; $relink_command"
+	fi
+      done
+      # Quote the link command for shipping.
+      func_quote eval cd "`pwd`"
+      relink_command="($func_quote_result; $SHELL \"$progpath\" $preserve_args --mode=relink $libtool_args @inst_prefix_dir@)"
+      func_quote_arg pretty,unquoted "$relink_command"
+      relink_command=$func_quote_arg_unquoted_result
+      if test yes = "$hardcode_automatic"; then
+	relink_command=
+      fi
+
+      # Only create the output if not a dry run.
+      $opt_dry_run || {
+	for installed in no yes; do
+	  if test yes = "$installed"; then
+	    if test -z "$install_libdir"; then
+	      break
+	    fi
+	    output=$output_objdir/${outputname}i
+	    # Replace all uninstalled libtool libraries with the installed ones
+	    newdependency_libs=
+	    for deplib in $dependency_libs; do
+	      case $deplib in
+	      *.la)
+		func_basename "$deplib"
+		name=$func_basename_result
+		func_resolve_sysroot "$deplib"
+		eval libdir=`$SED -n -e 's/^libdir=\(.*\)$/\1/p' $func_resolve_sysroot_result`
+		test -z "$libdir" && \
+		  func_fatal_error "'$deplib' is not a valid libtool archive"
+		func_append newdependency_libs " ${lt_sysroot:+=}$libdir/$name"
+		;;
+	      -L*)
+		func_stripname -L '' "$deplib"
+		func_replace_sysroot "$func_stripname_result"
+		func_append newdependency_libs " -L$func_replace_sysroot_result"
+		;;
+	      -R*)
+		func_stripname -R '' "$deplib"
+		func_replace_sysroot "$func_stripname_result"
+		func_append newdependency_libs " -R$func_replace_sysroot_result"
+		;;
+	      *) func_append newdependency_libs " $deplib" ;;
+	      esac
+	    done
+	    dependency_libs=$newdependency_libs
+	    newdlfiles=
+
+	    for lib in $dlfiles; do
+	      case $lib in
+	      *.la)
+	        func_basename "$lib"
+		name=$func_basename_result
+		eval libdir=`$SED -n -e 's/^libdir=\(.*\)$/\1/p' $lib`
+		test -z "$libdir" && \
+		  func_fatal_error "'$lib' is not a valid libtool archive"
+		func_append newdlfiles " ${lt_sysroot:+=}$libdir/$name"
+		;;
+	      *) func_append newdlfiles " $lib" ;;
+	      esac
+	    done
+	    dlfiles=$newdlfiles
+	    newdlprefiles=
+	    for lib in $dlprefiles; do
+	      case $lib in
+	      *.la)
+		# Only pass preopened files to the pseudo-archive (for
+		# eventual linking with the app. that links it) if we
+		# didn't already link the preopened objects directly into
+		# the library:
+		func_basename "$lib"
+		name=$func_basename_result
+		eval libdir=`$SED -n -e 's/^libdir=\(.*\)$/\1/p' $lib`
+		test -z "$libdir" && \
+		  func_fatal_error "'$lib' is not a valid libtool archive"
+		func_append newdlprefiles " ${lt_sysroot:+=}$libdir/$name"
+		;;
+	      esac
+	    done
+	    dlprefiles=$newdlprefiles
+	  else
+	    newdlfiles=
+	    for lib in $dlfiles; do
+	      case $lib in
+		[\\/]* | [A-Za-z]:[\\/]*) abs=$lib ;;
+		*) abs=`pwd`"/$lib" ;;
+	      esac
+	      func_append newdlfiles " $abs"
+	    done
+	    dlfiles=$newdlfiles
+	    newdlprefiles=
+	    for lib in $dlprefiles; do
+	      case $lib in
+		[\\/]* | [A-Za-z]:[\\/]*) abs=$lib ;;
+		*) abs=`pwd`"/$lib" ;;
+	      esac
+	      func_append newdlprefiles " $abs"
+	    done
+	    dlprefiles=$newdlprefiles
+	  fi
+	  $RM $output
+	  # place dlname in correct position for cygwin
+	  # In fact, it would be nice if we could use this code for all target
+	  # systems that can't hard-code library paths into their executables
+	  # and that have no shared library path variable independent of PATH,
+	  # but it turns out we can't easily determine that from inspecting
+	  # libtool variables, so we have to hard-code the OSs to which it
+	  # applies here; at the moment, that means platforms that use the PE
+	  # object format with DLL files.  See the long comment at the top of
+	  # tests/bindir.at for full details.
+	  tdlname=$dlname
+	  case $host,$output,$installed,$module,$dlname in
+	    *cygwin*,*lai,yes,no,*.dll | *mingw*,*lai,yes,no,*.dll | *cegcc*,*lai,yes,no,*.dll)
+	      # If a -bindir argument was supplied, place the dll there.
+	      if test -n "$bindir"; then
+		func_relative_path "$install_libdir" "$bindir"
+		tdlname=$func_relative_path_result/$dlname
+	      else
+		# Otherwise fall back on heuristic.
+		tdlname=../bin/$dlname
+	      fi
+	      ;;
+	  esac
+	  $ECHO > $output "\
+# $outputname - a libtool library file
+# Generated by $PROGRAM (GNU $PACKAGE) $VERSION
+#
+# Please DO NOT delete this file!
+# It is necessary for linking the library.
+
+# The name that we can dlopen(3).
+dlname='$tdlname'
+
+# Names of this library.
+library_names='$library_names'
+
+# The name of the static archive.
+old_library='$old_library'
+
+# Linker flags that cannot go in dependency_libs.
+inherited_linker_flags='$new_inherited_linker_flags'
+
+# Libraries that this one depends upon.
+dependency_libs='$dependency_libs'
+
+# Names of additional weak libraries provided by this library
+weak_library_names='$weak_libs'
+
+# Version information for $libname.
+current=$current
+age=$age
+revision=$revision
+
+# Is this an already installed library?
+installed=$installed
+
+# Should we warn about portability when linking against -modules?
+shouldnotlink=$module
+
+# Files to dlopen/dlpreopen
+dlopen='$dlfiles'
+dlpreopen='$dlprefiles'
+
+# Directory that this library needs to be installed in:
+libdir='$install_libdir'"
+	  if test no,yes = "$installed,$need_relink"; then
+	    $ECHO >> $output "\
+relink_command=\"$relink_command\""
+	  fi
+	done
+      }
+
+      # Do a symbolic link so that the libtool archive can be found in
+      # LD_LIBRARY_PATH before the program is installed.
+      func_show_eval '( cd "$output_objdir" && $RM "$outputname" && $LN_S "../$outputname" "$outputname" )' 'exit $?'
+      ;;
+    esac
+    exit $EXIT_SUCCESS
+}
+
+if test link = "$opt_mode" || test relink = "$opt_mode"; then
+  func_mode_link ${1+"$@"}
+fi
+
+
+# func_mode_uninstall arg...
+func_mode_uninstall ()
+{
+    $debug_cmd
+
+    RM=$nonopt
+    files=
+    rmforce=false
+    exit_status=0
+
+    # This variable tells wrapper scripts just to set variables rather
+    # than running their programs.
+    libtool_install_magic=$magic
+
+    for arg
+    do
+      case $arg in
+      -f) func_append RM " $arg"; rmforce=: ;;
+      -*) func_append RM " $arg" ;;
+      *) func_append files " $arg" ;;
+      esac
+    done
+
+    test -z "$RM" && \
+      func_fatal_help "you must specify an RM program"
+
+    rmdirs=
+
+    for file in $files; do
+      func_dirname "$file" "" "."
+      dir=$func_dirname_result
+      if test . = "$dir"; then
+	odir=$objdir
+      else
+	odir=$dir/$objdir
+      fi
+      func_basename "$file"
+      name=$func_basename_result
+      test uninstall = "$opt_mode" && odir=$dir
+
+      # Remember odir for removal later, being careful to avoid duplicates
+      if test clean = "$opt_mode"; then
+	case " $rmdirs " in
+	  *" $odir "*) ;;
+	  *) func_append rmdirs " $odir" ;;
+	esac
+      fi
+
+      # Don't error if the file doesn't exist and rm -f was used.
+      if { test -L "$file"; } >/dev/null 2>&1 ||
+	 { test -h "$file"; } >/dev/null 2>&1 ||
+	 test -f "$file"; then
+	:
+      elif test -d "$file"; then
+	exit_status=1
+	continue
+      elif $rmforce; then
+	continue
+      fi
+
+      rmfiles=$file
+
+      case $name in
+      *.la)
+	# Possibly a libtool archive, so verify it.
+	if func_lalib_p "$file"; then
+	  func_source $dir/$name
+
+	  # Delete the libtool libraries and symlinks.
+	  for n in $library_names; do
+	    func_append rmfiles " $odir/$n"
+	  done
+	  test -n "$old_library" && func_append rmfiles " $odir/$old_library"
+
+	  case $opt_mode in
+	  clean)
+	    case " $library_names " in
+	    *" $dlname "*) ;;
+	    *) test -n "$dlname" && func_append rmfiles " $odir/$dlname" ;;
+	    esac
+	    test -n "$libdir" && func_append rmfiles " $odir/$name $odir/${name}i"
+	    ;;
+	  uninstall)
+	    if test -n "$library_names"; then
+	      # Do each command in the postuninstall commands.
+	      func_execute_cmds "$postuninstall_cmds" '$rmforce || exit_status=1'
+	    fi
+
+	    if test -n "$old_library"; then
+	      # Do each command in the old_postuninstall commands.
+	      func_execute_cmds "$old_postuninstall_cmds" '$rmforce || exit_status=1'
+	    fi
+	    # FIXME: should reinstall the best remaining shared library.
+	    ;;
+	  esac
+	fi
+	;;
+
+      *.lo)
+	# Possibly a libtool object, so verify it.
+	if func_lalib_p "$file"; then
+
+	  # Read the .lo file
+	  func_source $dir/$name
+
+	  # Add PIC object to the list of files to remove.
+	  if test -n "$pic_object" && test none != "$pic_object"; then
+	    func_append rmfiles " $dir/$pic_object"
+	  fi
+
+	  # Add non-PIC object to the list of files to remove.
+	  if test -n "$non_pic_object" && test none != "$non_pic_object"; then
+	    func_append rmfiles " $dir/$non_pic_object"
+	  fi
+	fi
+	;;
+
+      *)
+	if test clean = "$opt_mode"; then
+	  noexename=$name
+	  case $file in
+	  *.exe)
+	    func_stripname '' '.exe' "$file"
+	    file=$func_stripname_result
+	    func_stripname '' '.exe' "$name"
+	    noexename=$func_stripname_result
+	    # $file with .exe has already been added to rmfiles,
+	    # add $file without .exe
+	    func_append rmfiles " $file"
+	    ;;
+	  esac
+	  # Do a test to see if this is a libtool program.
+	  if func_ltwrapper_p "$file"; then
+	    if func_ltwrapper_executable_p "$file"; then
+	      func_ltwrapper_scriptname "$file"
+	      relink_command=
+	      func_source $func_ltwrapper_scriptname_result
+	      func_append rmfiles " $func_ltwrapper_scriptname_result"
+	    else
+	      relink_command=
+	      func_source $dir/$noexename
+	    fi
+
+	    # note $name still contains .exe if it was in $file originally
+	    # as does the version of $file that was added into $rmfiles
+	    func_append rmfiles " $odir/$name $odir/${name}S.$objext"
+	    if test yes = "$fast_install" && test -n "$relink_command"; then
+	      func_append rmfiles " $odir/lt-$name"
+	    fi
+	    if test "X$noexename" != "X$name"; then
+	      func_append rmfiles " $odir/lt-$noexename.c"
+	    fi
+	  fi
+	fi
+	;;
+      esac
+      func_show_eval "$RM $rmfiles" 'exit_status=1'
+    done
+
+    # Try to remove the $objdir's in the directories where we deleted files
+    for dir in $rmdirs; do
+      if test -d "$dir"; then
+	func_show_eval "rmdir $dir >/dev/null 2>&1"
+      fi
+    done
+
+    exit $exit_status
+}
+
+if test uninstall = "$opt_mode" || test clean = "$opt_mode"; then
+  func_mode_uninstall ${1+"$@"}
+fi
+
+test -z "$opt_mode" && {
+  help=$generic_help
+  func_fatal_help "you must specify a MODE"
+}
+
+test -z "$exec_cmd" && \
+  func_fatal_help "invalid operation mode '$opt_mode'"
+
+if test -n "$exec_cmd"; then
+  eval exec "$exec_cmd"
+  exit $EXIT_FAILURE
+fi
+
+exit $exit_status
+
+
+# The TAGs below are defined such that we never get into a situation
+# where we disable both kinds of libraries.  Given conflicting
+# choices, we go for a static library, that is the most portable,
+# since we can't tell whether shared libraries were disabled because
+# the user asked for that or because the platform doesn't support
+# them.  This is particularly important on AIX, because we don't
+# support having both static and shared libraries enabled at the same
+# time on that platform, so we default to a shared-only configuration.
+# If a disable-shared tag is given, we'll fallback to a static-only
+# configuration.  But we'll never go from static-only to shared-only.
+
+# ### BEGIN LIBTOOL TAG CONFIG: disable-shared
+build_libtool_libs=no
+build_old_libs=yes
+# ### END LIBTOOL TAG CONFIG: disable-shared
+
+# ### BEGIN LIBTOOL TAG CONFIG: disable-static
+build_old_libs=`case $build_libtool_libs in yes) echo no;; *) echo yes;; esac`
+# ### END LIBTOOL TAG CONFIG: disable-static
+
+# Local Variables:
+# mode:shell-script
+# sh-indentation:2
+# End:
diff --git a/missing b/missing
new file mode 100755
index 0000000..1fe1611
--- /dev/null
+++ b/missing
@@ -0,0 +1,215 @@
+#! /bin/sh
+# Common wrapper for a few potentially missing GNU programs.
+
+scriptversion=2018-03-07.03; # UTC
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+# Originally written by Fran,cois Pinard <pinard@iro.umontreal.ca>, 1996.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+if test $# -eq 0; then
+  echo 1>&2 "Try '$0 --help' for more information"
+  exit 1
+fi
+
+case $1 in
+
+  --is-lightweight)
+    # Used by our autoconf macros to check whether the available missing
+    # script is modern enough.
+    exit 0
+    ;;
+
+  --run)
+    # Back-compat with the calling convention used by older automake.
+    shift
+    ;;
+
+  -h|--h|--he|--hel|--help)
+    echo "\
+$0 [OPTION]... PROGRAM [ARGUMENT]...
+
+Run 'PROGRAM [ARGUMENT]...', returning a proper advice when this fails due
+to PROGRAM being missing or too old.
+
+Options:
+  -h, --help      display this help and exit
+  -v, --version   output version information and exit
+
+Supported PROGRAM values:
+  aclocal   autoconf  autoheader   autom4te  automake  makeinfo
+  bison     yacc      flex         lex       help2man
+
+Version suffixes to PROGRAM as well as the prefixes 'gnu-', 'gnu', and
+'g' are ignored when checking the name.
+
+Send bug reports to <bug-automake@gnu.org>."
+    exit $?
+    ;;
+
+  -v|--v|--ve|--ver|--vers|--versi|--versio|--version)
+    echo "missing $scriptversion (GNU Automake)"
+    exit $?
+    ;;
+
+  -*)
+    echo 1>&2 "$0: unknown '$1' option"
+    echo 1>&2 "Try '$0 --help' for more information"
+    exit 1
+    ;;
+
+esac
+
+# Run the given program, remember its exit status.
+"$@"; st=$?
+
+# If it succeeded, we are done.
+test $st -eq 0 && exit 0
+
+# Also exit now if we it failed (or wasn't found), and '--version' was
+# passed; such an option is passed most likely to detect whether the
+# program is present and works.
+case $2 in --version|--help) exit $st;; esac
+
+# Exit code 63 means version mismatch.  This often happens when the user
+# tries to use an ancient version of a tool on a file that requires a
+# minimum version.
+if test $st -eq 63; then
+  msg="probably too old"
+elif test $st -eq 127; then
+  # Program was missing.
+  msg="missing on your system"
+else
+  # Program was found and executed, but failed.  Give up.
+  exit $st
+fi
+
+perl_URL=https://www.perl.org/
+flex_URL=https://github.com/westes/flex
+gnu_software_URL=https://www.gnu.org/software
+
+program_details ()
+{
+  case $1 in
+    aclocal|automake)
+      echo "The '$1' program is part of the GNU Automake package:"
+      echo "<$gnu_software_URL/automake>"
+      echo "It also requires GNU Autoconf, GNU m4 and Perl in order to run:"
+      echo "<$gnu_software_URL/autoconf>"
+      echo "<$gnu_software_URL/m4/>"
+      echo "<$perl_URL>"
+      ;;
+    autoconf|autom4te|autoheader)
+      echo "The '$1' program is part of the GNU Autoconf package:"
+      echo "<$gnu_software_URL/autoconf/>"
+      echo "It also requires GNU m4 and Perl in order to run:"
+      echo "<$gnu_software_URL/m4/>"
+      echo "<$perl_URL>"
+      ;;
+  esac
+}
+
+give_advice ()
+{
+  # Normalize program name to check for.
+  normalized_program=`echo "$1" | sed '
+    s/^gnu-//; t
+    s/^gnu//; t
+    s/^g//; t'`
+
+  printf '%s\n' "'$1' is $msg."
+
+  configure_deps="'configure.ac' or m4 files included by 'configure.ac'"
+  case $normalized_program in
+    autoconf*)
+      echo "You should only need it if you modified 'configure.ac',"
+      echo "or m4 files included by it."
+      program_details 'autoconf'
+      ;;
+    autoheader*)
+      echo "You should only need it if you modified 'acconfig.h' or"
+      echo "$configure_deps."
+      program_details 'autoheader'
+      ;;
+    automake*)
+      echo "You should only need it if you modified 'Makefile.am' or"
+      echo "$configure_deps."
+      program_details 'automake'
+      ;;
+    aclocal*)
+      echo "You should only need it if you modified 'acinclude.m4' or"
+      echo "$configure_deps."
+      program_details 'aclocal'
+      ;;
+   autom4te*)
+      echo "You might have modified some maintainer files that require"
+      echo "the 'autom4te' program to be rebuilt."
+      program_details 'autom4te'
+      ;;
+    bison*|yacc*)
+      echo "You should only need it if you modified a '.y' file."
+      echo "You may want to install the GNU Bison package:"
+      echo "<$gnu_software_URL/bison/>"
+      ;;
+    lex*|flex*)
+      echo "You should only need it if you modified a '.l' file."
+      echo "You may want to install the Fast Lexical Analyzer package:"
+      echo "<$flex_URL>"
+      ;;
+    help2man*)
+      echo "You should only need it if you modified a dependency" \
+           "of a man page."
+      echo "You may want to install the GNU Help2man package:"
+      echo "<$gnu_software_URL/help2man/>"
+    ;;
+    makeinfo*)
+      echo "You should only need it if you modified a '.texi' file, or"
+      echo "any other file indirectly affecting the aspect of the manual."
+      echo "You might want to install the Texinfo package:"
+      echo "<$gnu_software_URL/texinfo/>"
+      echo "The spurious makeinfo call might also be the consequence of"
+      echo "using a buggy 'make' (AIX, DU, IRIX), in which case you might"
+      echo "want to install GNU make:"
+      echo "<$gnu_software_URL/make/>"
+      ;;
+    *)
+      echo "You might have modified some files without having the proper"
+      echo "tools for further handling them.  Check the 'README' file, it"
+      echo "often tells you about the needed prerequisites for installing"
+      echo "this package.  You may also peek at any GNU archive site, in"
+      echo "case some other package contains this missing '$1' program."
+      ;;
+  esac
+}
+
+give_advice "$1" | sed -e '1s/^/WARNING: /' \
+                       -e '2,$s/^/         /' >&2
+
+# Propagate the correct exit status (expected to be 127 for a program
+# not found, 63 for a program that failed due to version mismatch).
+exit $st
+
+# Local variables:
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/scripts/40-usb-blacklist.rules b/scripts/40-usb-blacklist.rules
new file mode 100644
index 0000000..6631388
--- /dev/null
+++ b/scripts/40-usb-blacklist.rules
@@ -0,0 +1,14 @@
+#
+# Blacklist specific USB devices
+#
+# don't inquire sn and di on broken devices (https://bugzilla.suse.com/show_bug.cgi?id=840054)
+
+ACTION!="add|change", GOTO="usb_blacklist_end"
+KERNEL!="sd*[!0-9]|sr*", GOTO="usb_blacklist_end"
+
+# unknown device
+ATTRS{idVendor}=="0aec", ATTRS{idProduct}=="3260", ENV{ID_SCSI_INQUIRY}="1"
+# Sony/JMicron port replicator
+ATTRS{idVendor}=="054c", ATTRS{idProduct}=="06a0", ENV{ID_SCSI_INQUIRY}="1"
+
+LABEL="usb_blacklist_end"
diff --git a/scripts/54-before-scsi-sg3_id.rules b/scripts/54-before-scsi-sg3_id.rules
new file mode 100644
index 0000000..bb36650
--- /dev/null
+++ b/scripts/54-before-scsi-sg3_id.rules
@@ -0,0 +1,55 @@
+# do not edit this file, it will be overwritten on update
+
+# persistent storage links: /dev/disk/{by-id,by-path}
+# scheme based on "Linux persistent device names", 2004, Hannes Reinecke <hare@suse.de>
+
+# This file contains rules for setting udev environment variables based on
+# hardware properties (serial numbers etc), which can be obtained without
+# actually reading from the device.
+#
+# Hopefully this will be integrated into systemd/udev soon (as 54-storage-hardware.rules).
+# Until then, we ship it here in sg3-utils.
+# It's important that rules dealing with low-level hardware attributes run
+# before the generic SCSI rules in 55-scsi-sg3_utils.rules.
+
+ACTION=="remove", GOTO="storage_hardware_end"
+SUBSYSTEM!="block", GOTO="block_storage_end"
+KERNEL!="sd*|sr*|cciss*", GOTO="block_storage_end"
+
+# ignore partitions that span the entire disk
+TEST=="whole_disk", GOTO="block_storage_end"
+
+# for partitions import parent information
+ENV{DEVTYPE}=="partition", ENV{ID_SERIAL}!="?*", IMPORT{parent}="ID_*"
+
+# ATA
+KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{vendor}=="ATA", IMPORT{program}="ata_id --export $devnode"
+
+# ATAPI devices (SPC-3 or later)
+KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{type}=="5", ATTRS{scsi_level}=="[6-9]*", IMPORT{program}="ata_id --export $devnode"
+
+# Run ata_id on non-removable USB Mass Storage (SATA/PATA disks in enclosures)
+KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", ATTR{removable}=="0", SUBSYSTEMS=="usb", IMPORT{program}="ata_id --export $devnode"
+
+# Fall back usb_id for USB devices
+KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id"
+
+# FireWire
+ENV{ID_IEEE1394}!="?*", KERNEL=="sd*|sr*", ATTRS{ieee1394_id}=="?*", ENV{ID_IEEE1394}="$attr{ieee1394_id}"
+
+# by-path
+ENV{ID_PATH}!="?*", ENV{DEVTYPE}=="disk", DEVPATH!="*/virtual/*", IMPORT{builtin}="path_id"
+
+LABEL="block_storage_end"
+
+# SCSI tape devices
+SUBSYSTEM!="scsi_tape", GOTO="storage_hardware_end"
+KERNEL!="st*[0-9]|nst*[0-9]", GOTO="storage_hardware_end"
+
+ENV{ID_SERIAL}!="?*", ATTRS{ieee1394_id}=="?*", ENV{ID_SERIAL}="$attr{ieee1394_id}", ENV{ID_BUS}="ieee1394"
+ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="usb", ATTRS{serial}=="?*", IMPORT{builtin}="usb_id"
+
+# by-path
+ENV{ID_PATH}!="?*", IMPORT{builtin}="path_id"
+
+LABEL="storage_hardware_end"
diff --git a/scripts/55-scsi-sg3_id.rules b/scripts/55-scsi-sg3_id.rules
new file mode 100644
index 0000000..453210b
--- /dev/null
+++ b/scripts/55-scsi-sg3_id.rules
@@ -0,0 +1,109 @@
+# SCSI-ID mappings for sg3_utils
+
+ACTION=="remove", GOTO="sg3_utils_id_end"
+
+SUBSYSTEM=="block", GOTO="block_dev"
+
+# SCSI devices other than "block"
+# This code used to live in 60-persistent-storage-tape.rules.
+
+# type 8 devices are "Medium Changers"
+SUBSYSTEM=="scsi_generic", KERNEL=="sg*[0-9]", ATTRS{type}=="8", \
+  GOTO="scsi_inquiry"
+SUBSYSTEM=="scsi_changer", KERNEL=="sch*[0-9]", ATTRS{type}=="8", \
+  ENV{.INQUIRY_DEV}="$root/bsg/$id", GOTO="scsi_inquiry"
+
+# tapes need to be accessed through their bsg device
+KERNEL=="st*[0-9]|nst*[0-9]", SUBSYSTEMS=="scsi", KERNELS=="[0-9]*:*[0-9]", \
+  ENV{.INQUIRY_DEV}="$root/bsg/$id", GOTO="scsi_inquiry"
+
+GOTO="sg3_utils_id_end"
+
+LABEL="block_dev"
+
+# Import values for partitions
+ENV{DEVTYPE}=="partition", IMPORT{parent}="ID_SCSI", IMPORT{parent}="SCSI_*"
+ENV{DEVTYPE}=="partition", ENV{ID_SCSI}=="1", GOTO="compat"
+
+# Handle non-SCSI devices that implement SCSI inquiry
+KERNEL=="cciss*", ENV{DEVTYPE}=="disk", GOTO="sg_inquiry"
+
+# Ignore everything else except sd/sr
+KERNEL!="sd*[!0-9]|sr*", GOTO="sg3_utils_id_end"
+
+# SCSI INQUIRY values
+# If the 'inquiry' sysfs attribute is present the kernel will already
+# have scanned for VPD pages, so if the vpd page attribute is not
+# present it is not supported (or deemed unsafe to access).
+# Hence we can skip the call to sg_inq and avoid I/O altogether.
+# Set 'ID_SCSI_INQUIRY=0' in an earlier udev rule if the kernel
+# fails to scan VPD pages correctly; the rules will then fall
+# back to calling sg_vpd directly.
+LABEL="scsi_inquiry"
+ENV{ID_SCSI_INQUIRY}=="0", GOTO="sg_inquiry"
+
+# "inquiry" is an attribute of the scsi_device in sysfs,
+# we obtain it by using $id after an ATTRS match.
+SUBSYSTEMS=="scsi", ATTRS{inquiry}=="*", KERNELS=="[0-9]*:*[0-9]", \
+  ENV{.SYSFS_PATH}="$sys/class/scsi_device/$id/device"
+ENV{.SYSFS_PATH}=="", GOTO="sg_inquiry"
+
+IMPORT{program}="/usr/bin/sg_inq --export --inhex=$env{.SYSFS_PATH}/inquiry --raw", \
+  ENV{ID_SCSI}="1", ENV{ID_SCSI_INQUIRY}="1"
+# If inquiry sysfs attribute reading it failed, fallback to sg
+ENV{ID_SCSI}!="1", GOTO="sg_inquiry"
+# Read VPD pages 80 (sn) and 83 (di)
+IMPORT{program}="/usr/bin/sg_inq --export --inhex=$env{.SYSFS_PATH}/vpd_pg80 --raw"
+IMPORT{program}="/usr/bin/sg_inq --export --inhex=$env{.SYSFS_PATH}/vpd_pg83 --raw"
+GOTO="compat"
+
+LABEL="sg_inquiry"
+# Handle devices that have no inquiry attributes in sysfs
+ENV{.INQUIRY_DEV}=="", ENV{.INQUIRY_DEV}="$tempnode"
+
+IMPORT{program}="/usr/bin/sg_inq --export $env{.INQUIRY_DEV}", ENV{ID_SCSI}="1"
+# Give up if this fails, too
+ENV{ID_SCSI}!="1", GOTO="sg3_utils_id_end"
+IMPORT{program}="/usr/bin/sg_inq --export --page=sn $env{.INQUIRY_DEV}"
+IMPORT{program}="/usr/bin/sg_inq --export --page=di $env{.INQUIRY_DEV}"
+
+LABEL="compat"
+
+# scsi_id compat mappings
+ENV{ID_VENDOR}!="?*", ENV{SCSI_VENDOR}=="?*", ENV{ID_VENDOR}="$env{SCSI_VENDOR}"
+ENV{ID_VENDOR_ENC}!="?*", ENV{SCSI_VENDOR_ENC}=="?*", ENV{ID_VENDOR_ENC}="$env{SCSI_VENDOR_ENC}"
+ENV{ID_MODEL}!="?*", ENV{SCSI_MODEL}=="?*", ENV{ID_MODEL}="$env{SCSI_MODEL}"
+ENV{ID_MODEL_ENC}!="?*", ENV{SCSI_MODEL_ENC}=="?*", ENV{ID_MODEL_ENC}="$env{SCSI_MODEL_ENC}"
+ENV{ID_REVISION}!="?*", ENV{SCSI_REVISION}=="?*", ENV{ID_REVISION}="$env{SCSI_REVISION}"
+ENV{ID_TYPE}!="?*", ENV{SCSI_TYPE}=="?*", ENV{ID_TYPE}="$env{SCSI_TYPE}"
+ENV{ID_TARGET_PORT}!="?*", ENV{SCSI_IDENT_PORT_TARGET_PORT_GROUP}=="?*", \
+	PROGRAM="/bin/sh -c 'echo $env{SCSI_IDENT_PORT_TARGET_PORT_GROUP} | /bin/sed s/^0x//'", \
+	ENV{ID_TARGET_PORT}="$result"
+
+# ID_WWN compat mapping
+ENV{SCSI_IDENT_LUN_NAA_REGEXT}=="?*", ENV{ID_WWN_WITH_EXTENSION}!="?*", ENV{ID_WWN_WITH_EXTENSION}="0x$env{SCSI_IDENT_LUN_NAA_REGEXT}"
+ENV{SCSI_IDENT_LUN_NAA_REG}=="?*", ENV{ID_WWN_WITH_EXTENSION}!="?*", ENV{ID_WWN_WITH_EXTENSION}="0x$env{SCSI_IDENT_LUN_NAA_REG}"
+ENV{SCSI_IDENT_LUN_NAA_EXT}=="?*", ENV{ID_WWN_WITH_EXTENSION}!="?*", ENV{ID_WWN_WITH_EXTENSION}="0x$env{SCSI_IDENT_LUN_NAA_EXT}"
+ENV{SCSI_IDENT_LUN_NAA_LOCAL}=="?*", ENV{ID_WWN_WITH_EXTENSION}!="?*", ENV{ID_WWN_WITH_EXTENSION}="0x$env{SCSI_IDENT_LUN_NAA_LOCAL}"
+# ID_WWN has max 16 characters
+ENV{ID_WWN_WITH_EXTENSION}=="?*", ENV{ID_WWN}!="?*", \
+	PROGRAM="/bin/sh -c 'echo $env{ID_WWN_WITH_EXTENSION} | /bin/sed s/^\\\(0x.\\\{1,16\\\}\\\).*/\\1/'", \
+	ENV{ID_WWN}="$result"
+
+# ata_id compatibility
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_ATA}=="?*", ENV{ID_BUS}="ata", ENV{ID_ATA}="1", ENV{ID_SERIAL}="$env{SCSI_IDENT_LUN_ATA}"
+ENV{ID_SERIAL_SHORT}!="?*", ENV{SCSI_VENDOR}=="ATA", ENV{SCSI_IDENT_LUN_VENDOR}=="?*", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_VENDOR}"
+# Compat ID_SERIAL setting
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_NAA_REGEXT}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="3$env{SCSI_IDENT_LUN_NAA_REGEXT}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_NAA_REGEXT}"
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_NAA_REG}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="3$env{SCSI_IDENT_LUN_NAA_REG}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_NAA_REG}"
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_NAA_EXT}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="3$env{SCSI_IDENT_LUN_NAA_EXT}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_NAA_EXT}"
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_EUI64}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="2$env{SCSI_IDENT_LUN_EUI64}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_EUI64}"
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_NAME}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="8$env{SCSI_IDENT_LUN_NAME}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_NAME}"
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_T10}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="1$env{SCSI_IDENT_LUN_T10}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_T10}"
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_NAA_LOCAL}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="3$env{SCSI_IDENT_LUN_NAA_LOCAL}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_NAA_LOCAL}"
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_LUN_VENDOR}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="0$env{SCSI_VENDOR}_$env{SCSI_MODEL}_$env{SCSI_IDENT_LUN_VENDOR}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_LUN_VENDOR}"
+ENV{ID_SERIAL}!="?*", ENV{SCSI_IDENT_SERIAL}=="?*", ENV{ID_BUS}="scsi", ENV{ID_SERIAL}="S$env{SCSI_VENDOR}_$env{SCSI_MODEL}_$env{SCSI_IDENT_SERIAL}", ENV{ID_SERIAL_SHORT}="$env{SCSI_IDENT_SERIAL}"
+
+# Compat ID_SCSI_SERIAL setting
+ENV{ID_SCSI_SERIAL}!="?*", ENV{SCSI_IDENT_SERIAL}=="?*", ENV{ID_SCSI_SERIAL}="$env{SCSI_IDENT_SERIAL}"
+LABEL="sg3_utils_id_end"
diff --git a/scripts/58-scsi-sg3_symlink.rules b/scripts/58-scsi-sg3_symlink.rules
new file mode 100644
index 0000000..fe6b000
--- /dev/null
+++ b/scripts/58-scsi-sg3_symlink.rules
@@ -0,0 +1,38 @@
+# SCSI-ID symlinks for sg3_utils
+
+ACTION=="remove", GOTO="sg3_utils_symlink_end"
+
+SUBSYSTEM!="block", GOTO="sg3_utils_symlink_end"
+ENV{UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG}=="1", GOTO="sg3_utils_symlink_end"
+
+# Select which identifier to use per default
+# 0: vpd page 0x80 identifier
+ENV{SCSI_IDENT_SERIAL}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-S$env{SCSI_VENDOR}_$env{SCSI_MODEL}_$env{SCSI_IDENT_SERIAL}"
+ENV{SCSI_IDENT_SERIAL}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-S$env{SCSI_VENDOR}_$env{SCSI_MODEL}_$env{SCSI_IDENT_SERIAL}-part%n"
+# NAA identifier (prefix 3)
+# 1: IEEE Registered Extended first
+ENV{SCSI_IDENT_LUN_NAA_REGEXT}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-3$env{SCSI_IDENT_LUN_NAA_REGEXT}"
+ENV{SCSI_IDENT_LUN_NAA_REGEXT}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-3$env{SCSI_IDENT_LUN_NAA_REGEXT}-part%n"
+# 2: IEEE Registered
+ENV{SCSI_IDENT_LUN_NAA_REG}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-3$env{SCSI_IDENT_LUN_NAA_REG}"
+ENV{SCSI_IDENT_LUN_NAA_REG}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-3$env{SCSI_IDENT_LUN_NAA_REG}-part%n"
+# 3: IEEE Extended
+ENV{SCSI_IDENT_LUN_NAA_EXT}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-3$env{SCSI_IDENT_LUN_NAA_EXT}"
+ENV{SCSI_IDENT_LUN_NAA_EXT}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-3$env{SCSI_IDENT_LUN_NAA_EXT}-part%n"
+# 4: EUI-64 identifier (prefix 2)
+ENV{SCSI_IDENT_LUN_EUI64}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-2$env{SCSI_IDENT_LUN_EUI64}"
+ENV{SCSI_IDENT_LUN_EUI64}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-2$env{SCSI_IDENT_LUN_EUI64}-part%n"
+# 5: SCSI name identifier (prefix 8)
+ENV{SCSI_IDENT_LUN_NAME}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-8$env{SCSI_IDENT_LUN_NAME}"
+ENV{SCSI_IDENT_LUN_NAME}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-8$env{SCSI_IDENT_LUN_NAME}-part%n"
+# 6: T10 Vendor identifier (prefix 1)
+ENV{SCSI_IDENT_LUN_T10}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-1$env{SCSI_IDENT_LUN_T10}"
+ENV{SCSI_IDENT_LUN_T10}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-1$env{SCSI_IDENT_LUN_T10}-part%n"
+# 7: IEEE Locally assigned
+ENV{SCSI_IDENT_LUN_NAA_LOCAL}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-3$env{SCSI_IDENT_LUN_NAA_LOCAL}"
+ENV{SCSI_IDENT_LUN_NAA_LOCAL}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-3$env{SCSI_IDENT_LUN_NAA_LOCAL}-part%n"
+# 8: Vendor-specific identifier (prefix 0)
+ENV{SCSI_IDENT_LUN_VENDOR}=="?*", ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-id/scsi-0$env{SCSI_VENDOR}_$env{SCSI_MODEL}_$env{SCSI_IDENT_LUN_VENDOR}"
+ENV{SCSI_IDENT_LUN_VENDOR}=="?*", ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-id/scsi-0$env{SCSI_VENDOR}_$env{SCSI_MODEL}_$env{SCSI_IDENT_LUN_VENDOR}-part%n"
+
+LABEL="sg3_utils_symlink_end"
diff --git a/scripts/59-fc-wwpn-id.rules b/scripts/59-fc-wwpn-id.rules
new file mode 100644
index 0000000..5ad0a5c
--- /dev/null
+++ b/scripts/59-fc-wwpn-id.rules
@@ -0,0 +1,17 @@
+#
+# FC WWPN-based by-path links
+#
+
+ACTION!="add|change", GOTO="fc_wwpn_end"
+KERNEL!="sd*", GOTO="fc_wwpn_end"
+
+ENV{DEVTYPE}=="disk", IMPORT{program}="fc_wwpn_id %p"
+ENV{DEVTYPE}=="partition", IMPORT{parent}="FC_*"
+ENV{FC_TARGET_WWPN}!="?*", GOTO="fc_wwpn_end"
+ENV{FC_INITIATOR_WWPN}!="?*", GOTO="fc_wwpn_end"
+ENV{FC_TARGET_LUN}!="?*", GOTO="fc_wwpn_end"
+
+ENV{DEVTYPE}=="disk", SYMLINK+="disk/by-path/fc-$env{FC_INITIATOR_WWPN}-$env{FC_TARGET_WWPN}-lun-$env{FC_TARGET_LUN}"
+ENV{DEVTYPE}=="partition", SYMLINK+="disk/by-path/fc-$env{FC_INITIATOR_WWPN}-$env{FC_TARGET_WWPN}-lun-$env{FC_TARGET_LUN}-part%n"
+
+LABEL="fc_wwpn_end"
diff --git a/scripts/59-scsi-cciss_id.rules b/scripts/59-scsi-cciss_id.rules
new file mode 100644
index 0000000..4eb4561
--- /dev/null
+++ b/scripts/59-scsi-cciss_id.rules
@@ -0,0 +1,18 @@
+# cciss compat rules
+
+ACTION!="add|change", GOTO="cciss_compat_end"
+KERNEL!="sd*", GOTO="cciss_compat_end"
+ENV{ID_VENDOR}!="HP", ENV{ID_VENDOR}!="COMPAQ", GOTO="cciss_compat_end"
+ENV{ID_MODEL}!="LOGICAL_VOLUME", GOTO="cciss_compat_end"
+
+ENV{DEVTYPE}=="disk", DRIVERS=="hpsa", IMPORT{program}="cciss_id %p"
+ENV{DEVTYPE}=="partition", IMPORT{parent}="ID_*"
+ENV{ID_CCISS}!="?*", GOTO="cciss_compat_end"
+
+ENV{DEVTYPE}=="disk", SYMLINK+="cciss/$env{ID_CCISS}"
+ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/cciss-$env{ID_SERIAL}"
+
+ENV{DEVTYPE}=="partition", SYMLINK+="cciss/$env{ID_CCISS}p%n"
+ENV{DEVTYPE}=="partition", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/cciss-$env{ID_SERIAL}-part%n"
+
+LABEL="cciss_compat_end"
diff --git a/scripts/Makefile.am b/scripts/Makefile.am
new file mode 100644
index 0000000..01d4436
--- /dev/null
+++ b/scripts/Makefile.am
@@ -0,0 +1,3 @@
+dist_bin_SCRIPTS = scsi_logging_level scsi_mandat scsi_readcap scsi_ready \
+	      scsi_satl scsi_start scsi_stop scsi_temperature \
+	      rescan-scsi-bus.sh
diff --git a/scripts/Makefile.in b/scripts/Makefile.in
new file mode 100644
index 0000000..21673ab
--- /dev/null
+++ b/scripts/Makefile.in
@@ -0,0 +1,518 @@
+# Makefile.in generated by automake 1.16.5 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2021 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+  if test -z '$(MAKELEVEL)'; then \
+    false; \
+  elif test -n '$(MAKE_HOST)'; then \
+    true; \
+  elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+    true; \
+  else \
+    false; \
+  fi; \
+}
+am__make_running_with_option = \
+  case $${target_option-} in \
+      ?) ;; \
+      *) echo "am__make_running_with_option: internal error: invalid" \
+              "target option '$${target_option-}' specified" >&2; \
+         exit 1;; \
+  esac; \
+  has_opt=no; \
+  sane_makeflags=$$MAKEFLAGS; \
+  if $(am__is_gnu_make); then \
+    sane_makeflags=$$MFLAGS; \
+  else \
+    case $$MAKEFLAGS in \
+      *\\[\ \	]*) \
+        bs=\\; \
+        sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+          | sed "s/$$bs$$bs[$$bs $$bs	]*//g"`;; \
+    esac; \
+  fi; \
+  skip_next=no; \
+  strip_trailopt () \
+  { \
+    flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+  }; \
+  for flg in $$sane_makeflags; do \
+    test $$skip_next = yes && { skip_next=no; continue; }; \
+    case $$flg in \
+      *=*|--*) continue;; \
+        -*I) strip_trailopt 'I'; skip_next=yes;; \
+      -*I?*) strip_trailopt 'I';; \
+        -*O) strip_trailopt 'O'; skip_next=yes;; \
+      -*O?*) strip_trailopt 'O';; \
+        -*l) strip_trailopt 'l'; skip_next=yes;; \
+      -*l?*) strip_trailopt 'l';; \
+      -[dEDm]) skip_next=yes;; \
+      -[JT]) skip_next=yes;; \
+    esac; \
+    case $$flg in \
+      *$$target_option*) has_opt=yes; break;; \
+    esac; \
+  done; \
+  test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+subdir = scripts
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+	$(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(dist_bin_SCRIPTS) \
+	$(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+    $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+    *) f=$$p;; \
+  esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+  srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+  for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+  for p in $$list; do echo "$$p $$p"; done | \
+  sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+  $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+    if (++n[$$2] == $(am__install_max)) \
+      { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+    END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+  sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+  sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+  test -z "$$files" \
+    || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+    || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+         $(am__cd) "$$dir" && rm -f $$files; }; \
+  }
+am__installdirs = "$(DESTDIR)$(bindir)"
+SCRIPTS = $(dist_bin_SCRIPTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo "  GEN     " $@;
+am__v_GEN_1 = 
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 = 
+SOURCES =
+DIST_SOURCES =
+am__can_run_installinfo = \
+  case $$AM_UPDATE_INFO_DIR in \
+    n|no|NO) false;; \
+    *) (install-info --version) >/dev/null 2>&1;; \
+  esac
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+am__DIST_COMMON = $(srcdir)/Makefile.in README
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CSCOPE = @CSCOPE@
+CTAGS = @CTAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+ETAGS = @ETAGS@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FILECMD = @FILECMD@
+GETOPT_O_FILES = @GETOPT_O_FILES@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PTHREAD_LIB = @PTHREAD_LIB@
+RANLIB = @RANLIB@
+RT_LIB = @RT_LIB@
+SED = @SED@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+STRIP = @STRIP@
+VERSION = @VERSION@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+os_cflags = @os_cflags@
+os_libs = @os_libs@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+dist_bin_SCRIPTS = scsi_logging_level scsi_mandat scsi_readcap scsi_ready \
+	      scsi_satl scsi_start scsi_stop scsi_temperature \
+	      rescan-scsi-bus.sh
+
+all: all-am
+
+.SUFFIXES:
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am  $(am__configure_deps)
+	@for dep in $?; do \
+	  case '$(am__configure_deps)' in \
+	    *$$dep*) \
+	      ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+	        && { if test -f $@; then exit 0; else break; fi; }; \
+	      exit 1;; \
+	  esac; \
+	done; \
+	echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign scripts/Makefile'; \
+	$(am__cd) $(top_srcdir) && \
+	  $(AUTOMAKE) --foreign scripts/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+	@case '$?' in \
+	  *config.status*) \
+	    cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+	  *) \
+	    echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+	    cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+	esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+install-dist_binSCRIPTS: $(dist_bin_SCRIPTS)
+	@$(NORMAL_INSTALL)
+	@list='$(dist_bin_SCRIPTS)'; test -n "$(bindir)" || list=; \
+	if test -n "$$list"; then \
+	  echo " $(MKDIR_P) '$(DESTDIR)$(bindir)'"; \
+	  $(MKDIR_P) "$(DESTDIR)$(bindir)" || exit 1; \
+	fi; \
+	for p in $$list; do \
+	  if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+	  if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+	done | \
+	sed -e 'p;s,.*/,,;n' \
+	    -e 'h;s|.*|.|' \
+	    -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+	$(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+	  { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+	    if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+	      if (++n[d] == $(am__install_max)) { \
+		print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+	    else { print "f", d "/" $$4, $$1 } } \
+	  END { for (d in files) print "f", d, files[d] }' | \
+	while read type dir files; do \
+	     if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+	     test -z "$$files" || { \
+	       echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(bindir)$$dir'"; \
+	       $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \
+	     } \
+	; done
+
+uninstall-dist_binSCRIPTS:
+	@$(NORMAL_UNINSTALL)
+	@list='$(dist_bin_SCRIPTS)'; test -n "$(bindir)" || exit 0; \
+	files=`for p in $$list; do echo "$$p"; done | \
+	       sed -e 's,.*/,,;$(transform)'`; \
+	dir='$(DESTDIR)$(bindir)'; $(am__uninstall_files_from_dir)
+
+mostlyclean-libtool:
+	-rm -f *.lo
+
+clean-libtool:
+	-rm -rf .libs _libs
+tags TAGS:
+
+ctags CTAGS:
+
+cscope cscopelist:
+
+distdir: $(BUILT_SOURCES)
+	$(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+	@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	list='$(DISTFILES)'; \
+	  dist_files=`for file in $$list; do echo $$file; done | \
+	  sed -e "s|^$$srcdirstrip/||;t" \
+	      -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+	case $$dist_files in \
+	  */*) $(MKDIR_P) `echo "$$dist_files" | \
+			   sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+			   sort -u` ;; \
+	esac; \
+	for file in $$dist_files; do \
+	  if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+	  if test -d $$d/$$file; then \
+	    dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+	    if test -d "$(distdir)/$$file"; then \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+	      cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+	  else \
+	    test -f "$(distdir)/$$file" \
+	    || cp -p $$d/$$file "$(distdir)/$$file" \
+	    || exit 1; \
+	  fi; \
+	done
+check-am: all-am
+check: check-am
+all-am: Makefile $(SCRIPTS)
+installdirs:
+	for dir in "$(DESTDIR)$(bindir)"; do \
+	  test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+	done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+	@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+	if test -z '$(STRIP)'; then \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	      install; \
+	else \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	    "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+	fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+	-test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+	-test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+	@echo "This command is intended for maintainers to use"
+	@echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool mostlyclean-am
+
+distclean: distclean-am
+	-rm -f Makefile
+distclean-am: clean-am distclean-generic
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-dist_binSCRIPTS
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+	-rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-generic mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-dist_binSCRIPTS
+
+.MAKE: install-am install-strip
+
+.PHONY: all all-am check check-am clean clean-generic clean-libtool \
+	cscopelist-am ctags-am distclean distclean-generic \
+	distclean-libtool distdir dvi dvi-am html html-am info info-am \
+	install install-am install-data install-data-am \
+	install-dist_binSCRIPTS install-dvi install-dvi-am \
+	install-exec install-exec-am install-html install-html-am \
+	install-info install-info-am install-man install-pdf \
+	install-pdf-am install-ps install-ps-am install-strip \
+	installcheck installcheck-am installdirs maintainer-clean \
+	maintainer-clean-generic mostlyclean mostlyclean-generic \
+	mostlyclean-libtool pdf pdf-am ps ps-am tags-am uninstall \
+	uninstall-am uninstall-dist_binSCRIPTS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/scripts/README b/scripts/README
new file mode 100644
index 0000000..72cbdcf
--- /dev/null
+++ b/scripts/README
@@ -0,0 +1,55 @@
+                      README for sg3_utils/scripts
+                      ============================
+Introduction
+============
+This directory contains bash shell scripts. Most of them call one or
+more utilities from the sg3_utils package. They assume the sg3_utils
+package utilities are on the PATH of the user.
+
+rescan-scsi-bus.sh is written by Kurt Garloff (formerly from Suse Labs)
+with patches from Hannes Reinecke (Suse) and Redhat.
+
+scsi_logging_level is written by Andreas Herrmann <aherrman at de dot ibm
+dot com>. It sets the logging level of the SCSI subsystem in the Linux
+2.6 series kernels. See that file for more information.
+
+The other scripts are written by the author. Some do testing while others
+do bulk tasks (e.g. stopping multiple disks).
+
+Details
+=======
+Each script supplies more information, typically by supplying a '-h'
+or '--help' option. The script source often contains explanatory
+information. Following is a usage summary with a one line description:
+   rescan-scsi-bus.sh [OPTIONS]
+        - see the output of 'rescan-scsi-bus.sh --help'
+   scsi_logging_level [OPTIONS]
+        - set Linux SCSI subsystem logging level
+   scsi_mandat [-h] [-L] [-q] <device>
+        - check for mandatory SCSI command support
+   scsi_readcap [-b] [-h] [-v] <device>+
+        - fetch capacity/size information for each <device>
+   scsi_ready [-h] [-v] <device>+
+        - check the media ready status on each <device>
+   scsi_satl [-h] [-L] [-q] [-v] <device>
+        - check <device> for SCSI to ATA Translation Layer (SATL)
+   scsi_start [-h] [-v] [-w] <device>+
+        - start media (i.e. spin up) in each <device>
+   scsi_stop [-h] [-v] [-w] <device>+
+        - stop media (i.e. spin down) in each <device>
+   scsi_temperature [-h] [-v] <device>+
+        - check temperature in each <device>
+
+These scripts assume that the main sg3_utils utilities are installed
+and are on the user's PATH.
+
+This directory, prior to sg3_utils-1.28, contained the sas_disk_blink
+script. Since it depends on the sdparm utility it has been moved to
+the sdparm package in its scripts directory.
+
+59-scsi-sg3_utils.rules is a Linux specific file for udev. These rules use
+'sg_inq --export' to help udev create identifying device nodes, for example
+/dev/disk/by-id/wwn-0x5001501234567890-part1.
+
+Douglas Gilbert
+4th October 2021
diff --git a/scripts/cciss_id b/scripts/cciss_id
new file mode 100755
index 0000000..8ac11d5
--- /dev/null
+++ b/scripts/cciss_id
@@ -0,0 +1,66 @@
+#!/bin/bash
+#
+# cciss_id
+#
+# Generates device node names according to the cciss naming rules
+#
+# Copyright (C) 2011 SUSE Linux Products GmbH
+# Author:
+#       Hannes Reinecke <hare@suse.de>
+#
+#
+#       This program is free software; you can redistribute it and/or modify it
+#       under the terms of the GNU General Public License as published by the
+#       Free Software Foundation version 2 of the License.
+#
+# This script generates a device node name which is compatible
+# with the 'cciss' device naming rules.
+# It is intended to provide backward-compatible names for the
+# 'hpsa' driver.
+#
+
+cciss_enumerate()
+{
+    local last_pci_dev=${1##0000:}
+    local cur_pci_dev
+    local cciss_num=0
+
+    for cur_pci_dev in $(lspci -n | tac | sed -n 's/\(..:..\..\) .* 103c:\(3220\|3230\|3238\|323a\|323b\) .*/\1/p') ; do
+	if [ "$cur_pci_dev" == "$last_pci_dev" ] ; then
+	    echo "$cciss_num"
+	    return;
+	fi
+	cciss_num=$(($cciss_num + 1))
+    done
+    echo "$cciss_num"
+}
+
+hpsa_lun_offset()
+{
+    local scsi_host=$1
+
+    scsi_id=$(lsscsi 2>/dev/null | sed -n "s/.\(${scsi_host}:[0-9]*:[0-9]*:[0-9]*\)..*disk .*/\1/p" | head -1)
+    echo ${scsi_id##*:}
+}
+
+DEVPATH=$1
+SCSIPATH=$(cd -P /sys$DEVPATH/device; echo $PWD)
+SCSIID=${SCSIPATH##*/}
+HOSTID=${SCSIID%%:*}
+LUNID=${SCSIID##*:}
+PCIPATH=${SCSIPATH%%/host*}
+PCIDEV=${PCIPATH##*/}
+HOSTPATH=${PCIPATH}/host${HOSTID}/scsi_host/host${HOSTID}
+read controller 2>/dev/null <${HOSTPATH}/ctlr_num || controller=$(cciss_enumerate $PCIDEV)
+
+# hpsa lies about the LUN ...
+disk_offset=$(hpsa_lun_offset $HOSTID)
+if [ "$disk_offset" ] ; then
+    disk=$(( $LUNID - $disk_offset ))
+else
+    disk=$LUNID
+fi
+
+if [ "$controller" ] && [ "$disk" ] ; then
+    echo "ID_CCISS=c${controller}d${disk}"
+fi
diff --git a/scripts/fc_wwpn_id b/scripts/fc_wwpn_id
new file mode 100644
index 0000000..17c74fe
--- /dev/null
+++ b/scripts/fc_wwpn_id
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# fc_wwpn_id
+#
+# Generates device node names links based on FC WWPN
+# Copyright (c) 2016-2021 Hannes Reinecke, SUSE Linux GmbH
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation version 2 of the License.
+#
+
+DEVPATH=$1
+SCSIPATH=$(cd -P "/sys$DEVPATH/device" || exit; echo "$PWD")
+
+d=$SCSIPATH
+[ -d "$d/scsi_disk" ] || exit 0
+target_lun=${d##*:}
+
+while [ -n "$d" ] ; do
+    d=${d%/*}
+    e=${d##*/}
+    case "$e" in
+	rport*)
+	    rport=$e
+	    rport_dir="/sys/class/fc_remote_ports/$rport"
+	    if [ -d "$rport_dir" ] ; then
+		rport_wwpn=$(cat "$rport_dir/port_name")
+	    fi
+	    ;;
+	host*)
+	    host=$e
+	    host_dir="/sys/class/fc_host/$host"
+	    if [ -d "$host_dir" ] ; then
+		host_wwpn=$(cat "$host_dir/port_name")
+		break;
+	    fi
+    esac
+done
+
+if [ -n "$rport_wwpn" ] || [ -n "$host_wwpn" ] ; then
+    echo "FC_TARGET_LUN=$target_lun"
+fi
+
+if [ -n "$rport_wwpn" ] ; then
+    echo "FC_TARGET_WWPN=$rport_wwpn"
+fi
+
+if [ -n "$host_wwpn" ] ; then
+    echo "FC_INITIATOR_WWPN=$host_wwpn"
+fi
diff --git a/scripts/lunmask.service b/scripts/lunmask.service
new file mode 100644
index 0000000..03fdd96
--- /dev/null
+++ b/scripts/lunmask.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Disable LUN masking and scan SCSI Hosts
+After=systemd-udev-trigger.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/usr/lib/systemd/scripts/scsi-enable-target-scan.sh
+
+[Install]
+WantedBy=multi-user.target
diff --git a/scripts/rescan-scsi-bus.sh b/scripts/rescan-scsi-bus.sh
new file mode 100755
index 0000000..1ebc0e9
--- /dev/null
+++ b/scripts/rescan-scsi-bus.sh
@@ -0,0 +1,1436 @@
+#!/bin/bash
+# Script to rescan SCSI bus, using the scsi add-single-device mechanism.
+# (c) 1998--2010 Kurt Garloff <kurt@garloff.de>, GNU GPL v2 or v3
+# (c) 2006--2022 Hannes Reinecke, GNU GPL v2 or later
+# $Id: rescan-scsi-bus.sh,v 1.57 2012/03/31 14:08:48 garloff Exp $
+
+VERSION="20220930"
+SCAN_WILD_CARD=4294967295
+
+TMPLUNINFOFILE="/tmp/rescan-scsi-mpath-info.txt"
+
+setcolor ()
+{
+  red="\e[0;31m"
+  green="\e[0;32m"
+  yellow="\e[0;33m"
+  bold="\e[0;1m"
+  norm="\e[0;0m"
+}
+
+unsetcolor ()
+{
+  red=""; green=""
+  yellow=""; norm=""
+}
+
+echo_debug()
+{
+  if [ "$debug" -eq 1 ] ; then
+     echo "$1"
+  fi
+}
+
+# Output some text and return cursor to previous position
+# (only works for simple strings)
+# Stores length of string in LN and returns it
+print_and_scroll_back ()
+{
+  STRG="$1"
+  LN=${#STRG}
+  BK=""
+  declare -i cntr=0
+  while [ $cntr -lt "$LN" ] ; do BK="$BK\e[D"; let cntr+=1; done
+  echo -en "$STRG$BK"
+  return "$LN"
+}
+
+# Overwrite a text of length $LN with whitespace
+white_out ()
+{
+  BK=""; WH=""
+  declare -i cntr=0
+  while [ $cntr -lt "$LN" ] ; do BK="$BK\e[D"; WH="$WH "; let cntr+=1; done
+  echo -en "$WH$BK"
+}
+
+# Return hosts. sysfs must be mounted
+findhosts_26 ()
+{
+  hosts=
+  for hostdir in /sys/class/scsi_host/host* ; do
+    [ -e "$hostdir" ] || continue
+    hostno=${hostdir#/sys/class/scsi_host/host}
+    if [ -f "$hostdir/isp_name" ] ; then
+      hostname="qla2xxx"
+    elif [ -f "$hostdir/lpfc_drvr_version" ] ; then
+      hostname="lpfc"
+    else
+      hostname=$(cat "$hostdir/proc_name")
+    fi
+    hosts="$hosts $hostno"
+    echo_debug "Host adapter $hostno ($hostname) found."
+  done
+  if [ -z "$hosts" ] ; then
+    echo "No SCSI host adapters found in sysfs"
+    exit 1;
+  fi
+  # ensure numeric ordering. No quotes arount $hosts to skip leading space.
+  hosts=$(echo $hosts | tr ' ' '\n' | sort -n)
+}
+
+# Return hosts. /proc/scsi/HOSTADAPTER/? must exist
+findhosts ()
+{
+  hosts=
+  for driverdir in /proc/scsi/*; do
+    driver=${driverdir#/proc/scsi/}
+    if [ "$driver" = scsi ] || [ "$driver" = sg ] || [ "$driver" = dummy ] || [ "$driver" = device_info  ] ; then continue; fi
+    for hostdir in $driverdir/*; do
+      name=${hostdir#/proc/scsi/*/}
+      if [ "$name" = add_map ] || [ "$name" = map ]  || [ "$name" = mod_parm ] ; then continue; fi
+      num=$name
+      driverinfo=$driver
+      if [ -r "$hostdir/status" ] ; then
+        num=$(printf '%d\n' "$(sed -n 's/SCSI host number://p' "$hostdir/status")")
+        driverinfo="$driver:$name"
+      fi
+      hosts="$hosts $num"
+      echo "Host adapter $num ($driverinfo) found."
+    done
+  done
+}
+
+printtype ()
+{
+  local type=$1
+
+  case "$type" in
+    0) echo "Direct-Access" ;;
+    1) echo "Sequential-Access" ;;
+    2) echo "Printer" ;;
+    3) echo "Processor" ;;
+    4) echo "WORM" ;;
+    5) echo "CD-ROM" ;;
+    6) echo "Scanner" ;;
+    7) echo "Optical-Device" ;;
+    8) echo "Medium-Changer" ;;
+    9) echo "Communications" ;;
+    10) echo "Unknown" ;;
+    11) echo "Unknown" ;;
+    12) echo "RAID" ;;
+    13) echo "Enclosure" ;;
+    14) echo "Direct-Access-RBC" ;;
+    *) echo "Unknown" ;;
+  esac
+}
+
+print02i()
+{
+    if [ "$1" = "*" ] ; then
+        echo "00"
+    else
+        printf "%02i" "$1"
+    fi
+}
+
+# Get /proc/scsi/scsi info for device $host:$channel:$id:$lun
+# Optional parameter: Number of lines after first (default = 2),
+# result in SCSISTR, return code 1 means empty.
+procscsiscsi ()
+{
+  if [ -z "$1" ] ; then
+    LN=2
+  else
+    LN=$1
+  fi
+  CHANNEL=$(print02i "$channel")
+  ID=$(print02i "$id")
+  LUN=$(print02i "$lun")
+  if [ -d /sys/class/scsi_device ]; then
+    SCSIPATH="/sys/class/scsi_device/${host}:${channel}:${id}:${lun}"
+    if [ -d  "$SCSIPATH" ] ; then
+      SCSISTR="Host: scsi${host} Channel: $CHANNEL Id: $ID Lun: $LUN"
+      if [ "$LN" -gt 0 ] ; then
+        IVEND=$(cat "${SCSIPATH}/device/vendor")
+        IPROD=$(cat "${SCSIPATH}/device/model")
+        IPREV=$(cat "${SCSIPATH}/device/rev")
+        SCSIDEV=$(printf '  Vendor: %-08s Model: %-16s Rev: %-4s' "$IVEND" "$IPROD" "$IPREV")
+        SCSISTR="$SCSISTR
+$SCSIDEV"
+      fi
+      if [ "$LN" -gt 1 ] ; then
+        ILVL=$(cat "${SCSIPATH}/device/scsi_level")
+        type=$(cat "${SCSIPATH}/device/type")
+        ITYPE=$(printtype "$type")
+        SCSITMP=$(printf '  Type:   %-17s                ANSI SCSI revision: %02d' "$ITYPE" "$((ILVL - 1))")
+        SCSISTR="$SCSISTR
+$SCSITMP"
+      fi
+    else
+      return 1
+    fi
+  else
+    grepstr="scsi$host Channel: $CHANNEL Id: $ID Lun: $LUN"
+    SCSISTR=$(grep -A "$LN" -e "$grepstr" /proc/scsi/scsi)
+  fi
+  if [ -z "$SCSISTR" ] ; then
+    return 1
+  else
+    return 0
+  fi
+}
+
+# Find sg device with 2.6 sysfs support
+sgdevice26 ()
+{
+  local gendev
+
+  # if the scsi device has not been added, then there would not
+  # a related sgdev. So it's pointless to scan all sgs to find
+  # a related sg.
+  scsidev=/sys/class/scsi_device/${host}:${channel}:${id}:${lun}
+  if [ ! -e "$scsidev" ]; then
+    SGDEV=""
+    return
+  fi
+
+  gendev=/sys/class/scsi_device/${host}:${channel}:${id}:${lun}/device/generic
+  if [ -e "$gendev" ] ; then
+    SGDEV=$(basename "$(readlink "$gendev")")
+    return
+  fi
+  SGDEV=""
+}
+
+# Find sg device with 2.4 report-devs extensions
+sgdevice24 ()
+{
+  if procscsiscsi 3; then
+    SGDEV=$(echo "$SCSISTR" | grep 'Attached drivers:' | sed 's/^ *Attached drivers: \(sg[0-9]*\).*/\1/')
+  fi
+}
+
+# Find sg device that belongs to SCSI device $host $channel $id $lun
+# and return in SGDEV
+sgdevice ()
+{
+  SGDEV=
+  if [ -d /sys/class/scsi_device ] ; then
+    sgdevice26
+  else
+    DRV=$(grep 'Attached drivers:' /proc/scsi/scsi 2>/dev/null)
+    repdevstat=$((1-$?))
+    if [ $repdevstat = 0 ]; then
+      echo "scsi report-devs 1" >/proc/scsi/scsi
+      DRV=$(grep 'Attached drivers:' /proc/scsi/scsi 2>/dev/null)
+      [ $? -eq 1 ] && return
+    fi
+    if ! echo "$DRV" | grep -q 'drivers: sg'; then
+      modprobe sg
+    fi
+    sgdevice24
+    if [ $repdevstat = 0 ]; then
+      echo "scsi report-devs 0" >/proc/scsi/scsi
+    fi
+  fi
+}
+
+# Whether or not the RMB (removable) bit has been set in the INQUIRY response.
+# Uses ${host}, ${channel}, ${id} and ${lun}. Assumes that sg_device() has
+# already been called. How to test this function: copy/paste this function
+# in a shell and run
+# (cd /sys/class/scsi_device && for d in *; do set ${d//:/ }; echo -n "$d $(</sys/class/scsi_device/${d}/device/block/*/removable) <> "; SGDEV=bsg/$d host=$1 channel=$2 id=$3 lun=$4 is_removable; done)
+is_removable ()
+{
+  local b p
+
+  p=/sys/class/scsi_device/${host}:${channel}:${id}:${lun}/device/inquiry
+  # Extract the second byte of the INQUIRY response and check bit 7 (mask 0x80).
+  b=$(hexdump -n1 -e '/1 "%02X"' "$p" 2>/dev/null)
+  if [ -n "$b" ]; then
+    echo $(((0x$b & 0x80) != 0))
+  else
+    sg_inq /dev/$SGDEV 2>/dev/null | sed -n 's/^.*RMB=\([0-9]*\).*$/\1/p'
+  fi
+}
+
+# Test if SCSI device is still responding to commands
+# Return values:
+#   0 device is present
+#   1 device has changed
+#   2 device has been removed
+testonline ()
+{
+  local ctr RC RMB
+
+  : testonline
+  ctr=0
+  RC=0
+  # Set default values
+  IPTYPE=31
+  IPQUAL=3
+  [ ! -x /usr/bin/sg_turs ] && return 0
+  sgdevice
+  [ -z "$SGDEV" ] && return 0
+  sg_turs /dev/$SGDEV >/dev/null 2>&1
+  RC=$?
+
+  # Handle in progress of becoming ready and unit attention
+  while [ $RC = 2 -o $RC = 6 ] && [ $ctr -lt $timeout ] ; do
+    if [ $RC = 2 ] && [ "$RMB" != "1" ] && sg_inq /dev/$SGDEV | grep -q -i "PQual=0" ; then
+      echo -n "."
+      let LN+=1
+      sleep 1
+    else
+      sleep 0.02
+    fi
+    let ctr+=1
+    sg_turs /dev/$SGDEV >/dev/null 2>&1
+    RC=$?
+    # Check for removable device; TEST UNIT READY obviously will
+    # fail for a removable device with no medium
+    RMB=$(is_removable)
+    print_and_scroll_back "$host:$channel:$id:$lun $SGDEV ($RMB) "
+    [ $RC = 2 ] && [ "$RMB" = "1" ] && break
+  done
+  if [ $ctr != 0 ] ; then
+    white_out
+  fi
+  # echo -e "\e[A\e[A\e[A${yellow}Test existence of $SGDEV = $RC ${norm} \n\n\n"
+  [ $RC = 1 ] && return $RC
+  # Reset RC (might be !=0 for passive paths)
+  RC=0
+  # OK, device online, compare INQUIRY string
+  INQ=$(sg_inq "$sg_len_arg" /dev/$SGDEV 2>/dev/null)
+  if [ -z "$INQ" ] ; then
+    echo -e "\e[A\e[A\e[A\e[A${red}$SGDEV changed: ${bold}INQUIRY failed${norm}    \n\n\n"
+    return 2
+  fi
+  IVEND=$(echo "$INQ" | grep 'Vendor identification:' | sed 's/^[^:]*: \(.*\)$/\1/')
+  IPROD=$(echo "$INQ" | grep 'Product identification:' | sed 's/^[^:]*: \(.*\)$/\1/')
+  IPREV=$(echo "$INQ" | grep 'Product revision level:' | sed 's/^[^:]*: \(.*\)$/\1/')
+  STR=$(printf "  Vendor: %-08s Model: %-16s Rev: %-4s" "$IVEND" "$IPROD" "$IPREV")
+  IPTYPE=$(echo "$INQ" | sed -n 's/.* Device_type=\([0-9]*\) .*/\1/p')
+  if [ -z "$IPTYPE" ]; then
+    IPTYPE=$(echo "$INQ" | sed -n 's/.* PDT=\([0-9]*\) .*/\1/p')
+  fi
+  IPQUAL=$(echo "$INQ" | sed -n 's/ *PQual=\([0-9]*\)  Device.*/\1/p')
+  if [ -z "$IPQUAL" ] ; then
+    IPQUAL=$(echo "$INQ" | sed -n 's/ *PQual=\([0-9]*\)  PDT.*/\1/p')
+  fi
+  if [ "$IPQUAL" != 0 ] ; then
+    [ -z "$IPQUAL" ] && IPQUAL=3
+    [ -z "$IPTYPE" ] && IPTYPE=31
+    echo -e "\e[A\e[A\e[A\e[A${red}$SGDEV changed: ${bold}LU not available (PQual $IPQUAL)${norm}    \n\n\n"
+    return 2
+  fi
+
+  TYPE=$(printtype $IPTYPE)
+  if ! procscsiscsi ; then
+    echo -e "\e[A\e[A\e[A\e[A${red}$SGDEV removed. ${norm}\n\n\n"
+    return 2
+  fi
+  TMPSTR=$(echo "$SCSISTR" | grep 'Vendor:')
+  if [ "$ignore_rev" -eq 0 ] ; then
+      if [ "$TMPSTR" != "$STR" ]; then
+        echo -e "\e[A\e[A\e[A\e[A${red}$SGDEV changed: ${bold}\nfrom:${SCSISTR#* } \nto: $STR ${norm} \n\n\n"
+        return 1
+      fi
+  else
+      # Ignore disk revision change
+      local old_str_no_rev=
+      local new_str_no_rev=
+
+      old_str_no_rev=${TMPSTR%Rev:*}
+      new_str_no_rev=${STR%Rev:*}
+      if [ "$old_str_no_rev" != "$new_str_no_rev" ]; then
+        echo -e "\e[A\e[A\e[A\e[A${red}$SGDEV changed: ${bold}\nfrom:${SCSISTR#* } \nto: $STR ${norm} \n\n\n"
+        return 1
+      fi
+  fi
+  TMPSTR=$(echo "$SCSISTR" | sed -n 's/.*Type: *\(.*\) *ANSI.*/\1/p' | sed 's/ *$//g')
+  if [ "$TMPSTR" != "$TYPE" ] ; then
+    echo -e "\e[A\e[A\e[A\e[A${red}$SGDEV changed: ${bold}\nfrom:${TMPSTR} \nto: $TYPE ${norm} \n\n\n"
+    return 1
+  fi
+  return $RC
+}
+
+# Test if SCSI device $host $channel $id $lun exists
+# Outputs description from /proc/scsi/scsi (unless arg passed)
+# Returns SCSISTR (empty if no dev)
+testexist ()
+{
+  : testexist
+  SCSISTR=
+  if procscsiscsi && [ -z "$1" ] ; then
+    echo "$SCSISTR" | head -n1
+    echo "$SCSISTR" | tail -n2 | pr -o4 -l1
+  fi
+}
+
+# Returns the list of existing channels per host
+chanlist ()
+{
+  local hcil
+  local cil
+  local chan
+  local tmpchan
+
+  for dev in /sys/class/scsi_device/${host}:* ; do
+    [ -d "$dev" ] || continue;
+    hcil=${dev##*/}
+    cil=${hcil#*:}
+    chan=${cil%%:*}
+    for tmpchan in $channelsearch ; do
+      if [ "$chan" -eq "$tmpchan" ] ; then
+        chan=
+      fi
+    done
+    if [ -n "$chan" ] ; then
+      channelsearch="$channelsearch $chan"
+    fi
+  done
+  if [ -z "$channelsearch" ] ; then
+    channelsearch="0"
+  fi
+}
+
+# Returns the list of existing targets per host
+idlist ()
+{
+  local tmpid
+  local newid
+  local oldid
+
+  oldlist=$(find /sys/class/scsi_device -name "${host}:${channel}:*" -printf "%f\n")
+  # Rescan LUN 0 to check if we found new targets
+  echo "${channel} - -" > "/sys/class/scsi_host/host${host}/scan"
+  newlist=$(find /sys/class/scsi_device -name "${host}:${channel}:*" -printf "%f\n")
+  for newid in $newlist ; do
+    oldid=$newid
+    for tmpid in $oldlist ; do
+      if [ "$newid" = "$tmpid" ] ; then
+        oldid=
+        break
+      fi
+    done
+    if [ -n "$oldid" ] ; then
+      if [ -d /sys/class/scsi_device/$oldid ] ; then
+	hcil=${oldid}
+        printf "\r${green}NEW: %s ${norm}"
+        testexist
+        if [ "$SCSISTR" ] ; then
+          incrfound "$hcil"
+        fi
+      fi
+    fi
+  done
+  idsearch=$(find /sys/bus/scsi/devices -name "target${host}:${channel}:*" -printf "%f\n" | cut -f 3 -d :)
+}
+
+# Returns the list of existing LUNs from device $host $channel $id $lun
+# and returns list to stdout
+getluns()
+{
+  sgdevice
+  [ -z "$SGDEV" ] && return 1
+  if [ ! -x /usr/bin/sg_luns ] ; then
+    echo 0
+    return 1
+  fi
+  LLUN=$(sg_luns /dev/$SGDEV 2>/dev/null | sed -n 's/    \(.*\)/\1/p')
+  # Added -z $LLUN condition because $? gets the RC from sed, not sg_luns
+  if [ $? -ne 0 ] || [ -z "$LLUN" ] ; then
+    echo 0
+    return 1
+  fi
+  for lun in $LLUN ; do
+      # Swap LUN number
+      l0=0x$lun
+      l1=$(( (l0 >> 48) & 0xffff ))
+      l2=$(( (l0 >> 32) & 0xffff ))
+      l3=$(( (l0 >> 16) & 0xffff ))
+      l4=$(( l0 & 0xffff ))
+      l0=$(( ( ( (l4 * 0xffff) + l3 ) * 0xffff + l2 ) * 0xffff + l1 ))
+      printf "%u\n" $l0
+  done
+  return 0
+}
+
+# Wait for udev to settle (create device nodes etc.)
+udevadm_settle()
+{
+  local tmo=60
+  if [ -x /sbin/udevadm ] ; then
+    print_and_scroll_back " Calling udevadm settle (can take a while) "
+    # Loop for up to 60 seconds if sd devices still are settling..
+    # This allows us to continue if udev events are stuck on multipaths in recovery mode
+    while [ $tmo -gt 0 ] ; do
+      if ! /sbin/udevadm settle --timeout=1 | egrep -q sd[a-z]+ ; then
+        break;
+      fi
+      let tmo=$tmo-1
+    done
+    white_out
+  elif [ -x /sbin/udevsettle ] ; then
+    print_and_scroll_back " Calling udevsettle (can take a while) "
+    /sbin/udevsettle
+    white_out
+  else
+    sleep 0.02
+  fi
+}
+
+# Perform scan on a single lun $host $channel $id $lun
+dolunscan()
+{
+  local remappedlun0=
+  local devpath
+  SCSISTR=
+  devnr="$host $channel $id $lun"
+  echo -e " Scanning for device $devnr ... "
+  printf "${yellow}OLD: %s ${norm}"
+  testexist
+  # Device exists: Test whether it's still online
+  # (testonline returns 2 if it's gone and 1 if it has changed)
+  devpath="/sys/class/scsi_device/${host}:${channel}:${id}:${lun}/device"
+  if [ "$SCSISTR" ] ; then
+    testonline
+    RC=$?
+    # Well known lun transition case. Only for Direct-Access devs (type 0)
+    # If block directory exists && and PQUAL != 0, we unmapped lun0 and just have a well-known lun
+    # If block directory doesn't exist && PQUAL == 0, we mapped a real lun0
+    if [ "$lun" -eq 0 ] && [ $IPTYPE -eq 0 ] ; then
+      if [ $RC = 2 ] ; then
+        if [ -e "$devpath" ] ; then
+          if [ -d "$devpath/block" ] ; then
+            remappedlun0=2  # Transition from real lun 0 to well-known
+          else
+            RC=0   # Set this so the system leaves the existing well known lun alone. This is a lun 0 with no block directory
+          fi
+        fi
+      elif [ $RC = 0 ] && [ $IPTYPE -eq 0 ] ; then
+        if [ -e "$devpath" ] ; then
+          if [ ! -d "$devpath/block" ] ; then
+            remappedlun0=1  # Transition from well-known to real lun 0
+          fi
+        fi
+      fi
+    fi
+  fi
+
+  # Special case: lun 0 just got added (for reportlunscan),
+  # so make sure we correctly treat it as new
+  if [ "$lun" = "0" ] && [ "$1" = "1" ] && [ -z "$remappedlun0" ] ; then
+    SCSISTR=""
+    printf "\r\e[A\e[A\e[A"
+  fi
+
+  : f "$remove" s $SCSISTR
+  if [ "$remove" ] && [ "$SCSISTR" -o "$remappedlun0" = "1" ] ; then
+    if [ $RC != 0 ] || [ ! -z "$forceremove" ] || [ -n "$remappedlun0" ] ; then
+      if [ "$remappedlun0" != "1" ] ; then
+        echo -en "\r\e[A\e[A\e[A${red}REM: "
+        echo "$SCSISTR" | head -n1
+        echo -e "${norm}\e[B\e[B"
+      fi
+      if [ -e "$devpath" ] ; then
+        # have to preemptively do this so we can figure out the mpath device
+        # Don't do this if we're deleting a well known lun to replace it
+        if [ "$remappedlun0" != "1" ] ; then
+          incrrmvd "$host:$channel:$id:$lun"
+        fi
+        echo 1 > "$devpath/delete"
+        sleep 0.02
+      else
+        echo "scsi remove-single-device $devnr" > /proc/scsi/scsi
+        if [ $RC -eq 1 ] || [ "$lun" -eq 0 ] ; then
+          # Try readding, should fail if device is gone
+          echo "scsi add-single-device $devnr" > /proc/scsi/scsi
+        fi
+      fi
+    fi
+    if [ $RC = 0 ] || [ "$forcerescan" ] ; then
+      if [ -e "$devpath" ] ; then
+        echo 1 > "$devpath/rescan"
+      fi
+    fi
+    printf "\r\e[A\e[A\e[A${yellow}OLD: %s ${norm}"
+    testexist
+    if [ -z "$SCSISTR" ] && [ $RC != 1 ] && [ "$remappedlun0" != "1" ] ; then
+      printf "\r${red}DEL: %s\r\n\n ${norm}"
+      # In the event we're replacing with a well known node, we need to let it continue, to create the replacement node
+      [ "$remappedlun0" != "2" ] && return 2
+    fi
+  fi
+  if [ -z "$SCSISTR" ] || [ -n "$remappedlun0" ] ; then
+    if [ "$remappedlun0" != "2" ] ; then
+      # Device does not exist, try to add
+      printf "\r${green}NEW: %s ${norm}"
+    fi
+    if [ -e "/sys/class/scsi_host/host${host}/scan" ] ; then
+      echo "$channel $id $lun" > "/sys/class/scsi_host/host${host}/scan" 2> /dev/null
+    else
+      echo "scsi add-single-device $devnr" > /proc/scsi/scsi
+    fi
+    testexist
+    if [ -z "$SCSISTR" ] ; then
+      # Device not present
+      printf "\r\e[A";
+      # Optimization: if lun==0, stop here (only if in non-remove mode)
+      if [ "$lun" = 0 ] && [ -z "$remove" ] && [ "$optscan" = 1 ] ; then
+        return 1;
+      fi
+    else
+      if [ "$remappedlun0" != "2" ] ; then
+        incrfound "$host:$channel:$id:$lun"
+      fi
+    fi
+  fi
+  return 0;
+}
+
+# Perform report lun scan on $host $channel $id using REPORT_LUNS
+doreportlun()
+{
+  lun=0
+  SCSISTR=
+  devnr="$host $channel $id $lun"
+  echo -en " Scanning for device $devnr ...\r"
+  lun0added=
+  #printf "${yellow}OLD: %s ${norm}"
+  # Phase one: If LUN0 does not exist, try to add
+  testexist -q
+  if [ -z "$SCSISTR" ] ; then
+    # Device does not exist, try to add
+    #printf "\r${green}NEW: %s ${norm}"
+    if [ -e "/sys/class/scsi_host/host${host}/scan" ] ; then
+      echo "$channel $id $lun" > "/sys/class/scsi_host/host${host}/scan" 2> /dev/null
+      udevadm_settle
+    else
+      echo "scsi add-single-device $devnr" > /proc/scsi/scsi
+    fi
+    testexist -q
+    if [ -n "$SCSISTR" ] ; then
+      lun0added=1
+      #testonline
+    else
+      # Device not present
+      # return
+      # Find alternative LUN to send getluns to
+      for dev in /sys/class/scsi_device/${host}:${channel}:${id}:*; do
+        [ -d "$dev" ] || continue
+        lun=${dev##*:}
+        break
+      done
+    fi
+  fi
+  targetluns=$(getluns)
+  REPLUNSTAT=$?
+  lunremove=
+  #echo "getluns reports " $targetluns
+  olddev=$(find /sys/class/scsi_device/ -name "$host:$channel:$id:*" 2>/dev/null | sort -t: -k4 -n)
+  oldtargets="$targetluns"
+  # OK -- if we don't have a LUN to send a REPORT_LUNS to, we could
+  # fall back to wildcard scanning. Same thing if the device does not
+  # support REPORT_LUNS
+  # TODO: We might be better off to ALWAYS use wildcard scanning if
+  # it works
+  if [ "$REPLUNSTAT" = "1" ] ; then
+    if [ -e "/sys/class/scsi_host/host${host}/scan" ] ; then
+      echo "$channel $id -" > "/sys/class/scsi_host/host${host}/scan" 2> /dev/null
+      udevadm_settle
+    else
+      echo "scsi add-single-device $host $channel $id $SCAN_WILD_CARD" > /proc/scsi/scsi
+    fi
+    targetluns=$(find /sys/class/scsi_device/ -name "$host:$channel:$id:*" -printf "%f\n" | cut -d : -f 4)
+    let found+=$(echo "$targetluns" | wc -l)
+    let found-=$(echo "$olddev" | wc -l)
+  fi
+  [ -z "$targetluns" ] && targetluns="$oldtargets"
+  # Check existing luns
+  for dev in $olddev; do
+    [ -d "$dev" ] || continue
+    lun=${dev##*:}
+    newsearch=
+    inlist=
+    # OK, is existing $lun (still) in reported list
+    for tmplun in $targetluns; do
+      if [ "$tmplun" = "$lun" ] ; then
+        inlist=1
+        dolunscan $lun0added
+        [ $? -eq 1 ] && break
+      else
+        newsearch="$newsearch $tmplun"
+      fi
+    done
+    # OK, we have now done a lunscan on $lun and
+    # $newsearch is the old $targetluns without $lun
+    if [ -z "$inlist" ]; then
+      # Stale lun
+      lunremove="$lunremove $lun"
+    fi
+    # $lun removed from $lunsearch
+    targetluns=${newsearch# }
+  done
+  # Add new ones and check stale ones
+  for lun in $targetluns $lunremove; do
+    dolunscan $lun0added
+    [ $? -eq 1 ] && break
+  done
+}
+
+# Perform search (scan $host)
+dosearch ()
+{
+  if [ -z "$channelsearch" ] ; then
+    chanlist
+  fi
+  for channel in $channelsearch; do
+    if [ -z "$idsearch" ] ; then
+      if [ -z "$lunsearch" ] ; then
+	idlist
+      else
+	idsearch=$(find /sys/bus/scsi/devices -name "target${host}:${channel}:*" -printf "%f\n" | cut -f 3 -d :)
+      fi
+    fi
+    for id in $idsearch; do
+      if [ -z "$lunsearch" ] ; then
+        doreportlun
+      else
+        for lun in $lunsearch; do
+          dolunscan
+          [ $? -eq 1 ] && break
+        done
+      fi
+    done
+  done
+}
+
+expandlist ()
+{
+  list=$1
+  result=""
+  first=${list%%,*}
+  rest=${list#*,}
+  while [ ! -z "$first" ] ; do
+    beg=${first%%-*};
+    if [ "$beg" = "$first" ] ; then
+      result="$result $beg";
+    else
+      end=${first#*-}
+      result="$result $(seq -s ' ' $beg $end)"
+    fi
+    [ "$rest" = "$first" ] && rest=""
+    first=${rest%%,*}
+    rest=${rest#*,}
+  done
+  echo "$result"
+}
+
+searchexisting()
+{
+  local tmpch;
+  local tmpid
+  local match=0
+  local targets=
+
+  targets=$(find /sys/bus/scsi/devices -name "target${host}:*" -printf "%f\n" | cut -d : -f 2-3)
+  # Nothing came back on this host, so we should skip it
+  [ -z "$targets" ] && return
+
+  local target=;
+  for target in $targets ; do
+    channel=${target%:*}
+    id=${target#*:}
+    if [ -n "$channelsearch" ] ; then
+      for tmpch in $channelsearch ; do
+        [ $tmpch -eq "$channel" ] && match=1
+      done
+    else
+      match=1
+    fi
+
+    [ $match -eq 0 ] && continue
+    match=0
+
+    if [ "$filter_ids" -eq 1 ] ; then
+      for tmpid in $idsearch ; do
+        if [ "$tmpid" = "$id" ] ; then
+          match=1
+        fi
+      done
+    else
+      match=1
+    fi
+
+    [ $match -eq 0 ] && continue
+
+    if [ -z "$lunsearch" ] ; then
+      doreportlun
+    else
+      for lun in $lunsearch ; do
+        dolunscan
+        [ $? -eq 1 ] && break
+      done
+    fi
+  done
+}
+
+getallmultipathinfo()
+{
+  local mp=
+  local uuid=
+  local dmtmp=
+  local maj_min=
+  local tmpfile=
+
+  truncate -s 0 $TMPLUNINFOFILE
+  for mp in $($DMSETUP ls --target=multipath | cut -f 1) ; do
+    [ "$mp" = "No" ] && break;
+    maj_min=$($DMSETUP status "$mp" | cut -d  " " -f14)
+    if [ ! -L /dev/mapper/${mp} ]; then
+      echo "softlink /dev/mapper/${mp} not available."
+      continue
+    fi
+    local ret=$(readlink /dev/mapper/$mp 2>/dev/null)
+    if [[ $? -ne 0 || -z "$ret" ]]; then
+      echo "readlink /dev/mapper/$mp failed. check multipath status."
+      continue
+    fi
+    dmtmp=$(basename $ret)
+    uuid=$(cut -f2 -d- "/sys/block/$dmtmp/dm/uuid")
+    echo "$mp $maj_min $dmtmp $uuid" >> $TMPLUNINFOFILE
+  done
+}
+
+# Go through all of the existing devices and figure out any that have been remapped
+findremapped()
+{
+  local hctl=;
+  local devs=
+  local sddev=
+  local id_serial=
+  local id_serial_old=
+  local remapped=
+  mpaths=""
+  local tmpfile=
+
+  tmpfile=$(mktemp /tmp/rescan-scsi-bus.XXXXXXXX 2> /dev/null)
+  if [ -z "$tmpfile" ] ; then
+    tmpfile="/tmp/rescan-scsi-bus.$$"
+    rm -f $tmpfile
+  fi
+
+  # Get all of the ID_SERIAL attributes, after finding their sd node
+  devs=$(ls /sys/class/scsi_device/)
+  for hctl in $devs ; do
+    if [ -d "/sys/class/scsi_device/$hctl/device/block" ] ; then
+      sddev=$(ls "/sys/class/scsi_device/$hctl/device/block")
+      id_serial_old=$(udevadm info -q all -n "$sddev" | grep "ID_SERIAL=" | cut -d"=" -f2)
+      [ -z "$id_serial_old" ] && id_serial_old="none"
+      echo "$hctl $sddev $id_serial_old" >> $tmpfile
+    fi
+  done
+
+  # Trigger udev to update the info
+  echo -n "Triggering udev to update device information... "
+  /sbin/udevadm trigger
+  udevadm_settle 2>&1 /dev/null
+  echo "Done"
+
+  getallmultipathinfo
+
+  # See what changed and reload the respective multipath device if applicable
+  while read -r hctl sddev id_serial_old ; do
+    remapped=0
+    id_serial=$(udevadm info -q all -n "$sddev" | grep "ID_SERIAL=" | cut -d"=" -f2)
+    [ -z "$id_serial" ] && id_serial="none"
+    if [ "$id_serial_old" != "$id_serial" ] ; then
+      remapped=1
+    fi
+    # If udev events updated the disks already, but the multipath device isn't update
+    # check for old devices to make sure we found remapped luns
+    if [ -n "$mp_enable" ] && [ $remapped -eq 0 ]; then
+      findmultipath "$sddev" $id_serial
+      if [ $? -eq 1 ] ; then
+        remapped=1
+      fi
+    fi
+
+    # if uuid is 1, it's unmapped, so we don't want to treat it as a remap
+    # if remapped flag is 0, just skip the rest of the logic
+    if [ "$id_serial" = "1" ] || [ $remapped -eq 0 ] ; then
+      continue
+    fi
+    printf "${yellow}REMAPPED: %s ${norm}"
+    host=$(echo "$hctl" | cut -d":" -f1)
+    channel=$(echo "$hctl" | cut -d":" -f2)
+    id=$(echo "$hctl" | cut -d":" -f3)
+    lun=$(echo "$hctl" | cut -d":" -f4)
+    procscsiscsi
+    echo "$SCSISTR"
+    incrchgd "$hctl"
+  done < $tmpfile
+  rm -f $tmpfile
+
+  if [ -n "$mp_enable" ] && [ -n "$mpaths" ] ; then
+    echo "Updating multipath device mappings"
+    flushmpaths
+    $MULTIPATH | grep "create:" 2> /dev/null
+  fi
+}
+
+incrfound()
+{
+  local hctl="$1"
+  if [ -n "$hctl" ] ; then
+    let found+=1
+    FOUNDDEVS="$FOUNDDEVS\t[$hctl]\n"
+  else
+    return
+  fi
+}
+
+incrchgd()
+{
+  local hctl="$1"
+  if [ -n "$hctl" ] ; then
+    if ! echo "$CHGDEVS" | grep -q "\[$hctl\]"; then
+      let updated+=1
+      CHGDEVS="$CHGDEVS\t[$hctl]\n"
+    fi
+  else
+    return
+  fi
+
+  if [ -n "$mp_enable" ] ; then
+    local sdev
+
+    sdev=$(findsddev "$hctl")
+    if [ -n "$sdev" ] ; then
+      findmultipath "$sdev"
+    fi
+  fi
+}
+
+incrrmvd()
+{
+  local hctl="$1"
+  if [ -n "$hctl" ] ; then
+    let rmvd+=1;
+    RMVDDEVS="$RMVDDEVS\t[$hctl]\n"
+  else
+    return
+  fi
+
+  if [ -n "$mp_enable" ] ; then
+    local sdev
+
+    sdev=$(findsddev "$hctl")
+    if [ -n "$sdev" ] ; then
+      findmultipath "$sdev"
+    fi
+  fi
+}
+
+findsddev()
+{
+  local hctl="$1"
+  local sddev=
+  local blkpath
+
+  blkpath="/sys/class/scsi_device/$hctl/device/block"
+  if [ -e "$blkpath" ] ; then
+    sddev=$(ls "$blkpath")
+    echo "$sddev"
+  fi
+}
+
+addmpathtolist()
+{
+  local mp="$1"
+  local mp2=
+
+  for mp2 in $mpaths ; do
+    # The multipath device is already in the list
+    if [ "$mp2" = "$mp" ] ; then
+      return
+    fi
+  done
+  mpaths="$mpaths $mp"
+}
+
+findmultipath()
+{
+  local dev="$1"
+  local find_mismatch="$2"
+  local mp=
+  local found_dup=0
+  local maj_min=
+
+  # Need a sdev, and executable multipath and dmsetup command here
+  if [ -z "$dev" ] || [ ! -x "$DMSETUP" ] || [ ! -x "$MULTIPATH" ] ; then
+    return 1
+  fi
+
+  maj_min=$(cat "/sys/block/$dev/dev")
+  mp=$(cat $TMPLUNINFOFILE | grep -w "$maj_min" | cut -d " " -f1)
+  if [ -n "$mp" ]; then
+    if [ -n "$find_mismatch" ] ; then
+      uuid=$(cat $TMPLUNINFOFILE | grep -w "$maj_min" | cut -d " " -f4)
+      if [ "$find_mismatch" != "$uuid" ] ; then
+        addmpathtolist "$mp"
+        found_dup=1
+      fi
+    else
+      # Normal mode: Find the first multipath with the sdev
+      # and add it to the list
+      addmpathtolist "$mp"
+      return
+    fi
+  fi
+
+  # Return 1 to signal that a duplicate was found to the calling function
+  if [ $found_dup -eq 1 ] ; then
+    return 1
+  else
+    return 0
+  fi
+}
+
+reloadmpaths()
+{
+  local mpath
+  if [ ! -x "$MULTIPATH" ] ; then
+    echo "no -x multipath"
+    return
+  fi
+
+  # Pass 1 as the argument to reload all mpaths
+  if [ "$1" = "1" ] ; then
+    echo "Reloading all multipath devices"
+    $MULTIPATH -r > /dev/null 2>&1
+    return
+  fi
+
+  # Reload the multipath devices
+  for mpath in $mpaths ; do
+    echo -n "Reloading multipath device $mpath... "
+    if $MULTIPATH -r "$mpath" > /dev/null 2>&1 ; then
+      echo "Done"
+    else
+      echo "Fail"
+    fi
+  done
+}
+
+resizempaths()
+{
+  local mpath
+
+  for mpath in $mpaths ; do
+    echo -n "Resizing multipath map $mpath ..."
+    multipathd -k"resize map $mpath"
+    let updated+=1
+  done
+}
+
+flushmpaths()
+{
+  local mpath
+  local remove=""
+  local i
+  local flush_retries=5
+
+  if [ -n "$1" ] ; then
+    for mpath in $($DMSETUP ls --target=multipath | cut -f 1) ; do
+      [ "$mpath" = "No" ] && break
+      num=$($DMSETUP status "$mpath" | awk 'BEGIN{RS=" ";active=0}/[0-9]+:[0-9]+/{dev=1}/A/{if (dev == 1) active++; dev=0} END{ print active }')
+      if [ "$num" -eq 0 ] ; then
+        remove="$remove $mpath"
+      fi
+    done
+  else
+    remove="$mpaths"
+  fi
+
+  for mpath in $remove ; do
+    i=0
+    echo -n "Flushing multipath device $mpath... "
+    while [ $i -lt $flush_retries ] ; do
+      $DMSETUP message "$mpath" 0 fail_if_no_path > /dev/null 2>&1
+      if $MULTIPATH -f "$mpath" > /dev/null 2>&1 ; then
+        echo "Done ($i retries)"
+        break
+      elif [ $i -eq $flush_retries ] ; then
+        echo "Fail"
+      fi
+      sleep 0.02
+      let i=$i+1
+    done
+  done
+}
+
+
+# Find resized luns
+findresized()
+{
+  local devs=
+  local size=
+  local new_size=
+  local sysfs_path=
+  local sddev=
+  local i=
+  local m=
+  local mpathsize=
+  declare -a mpathsizes
+
+  if [ -z "$lunsearch" ] ; then
+    devs=$(ls /sys/class/scsi_device/)
+  else
+    for lun in $lunsearch ; do
+      devs="$devs $(cd /sys/class/scsi_device/ && ls -d *:${lun})"
+    done
+  fi
+
+  for hctl in $devs ; do
+    sysfs_path="/sys/class/scsi_device/$hctl/device"
+    if [ -d "$sysfs_path/block" ] ; then
+      sddev=$(ls "$sysfs_path/block")
+      size=$(cat "$sysfs_path/block/$sddev/size")
+
+      echo 1 > "$sysfs_path/rescan"
+      new_size=$(cat "$sysfs_path/block/$sddev/size")
+
+      if [ "$size" != "$new_size" ] && [ "$size" != "0" ] && [ "$new_size" != "0" ] ; then
+        printf "${yellow}RESIZED: %s ${norm}"
+        host=$(echo "$hctl" | cut -d":" -f1)
+        channel=$(echo "$hctl" | cut -d":" -f2)
+        id=$(echo "$hctl" | cut -d":" -f3)
+        lun=$(echo "$hctl" | cut -d":" -f4)
+
+        procscsiscsi
+        echo "$SCSISTR"
+        incrchgd "$hctl"
+      fi
+    fi
+  done
+
+  if [ -n "$mp_enable" ] && [ -n "$mpaths" ] ; then
+    i=0
+    for m in $mpaths ; do
+      mpathsizes[$i]="$($MULTIPATH -l "$m" | egrep -o [0-9]+.[0-9]+[KMGT])"
+      let i=$i+1
+    done
+    resizempaths
+    i=0
+    for m in $mpaths ; do
+      mpathsize="$($MULTIPATH -l "$m" | egrep -o [0-9\.]+[KMGT])"
+      echo "$m ${mpathsizes[$i]} => $mpathsize"
+      let i=$i+1
+    done
+  fi
+}
+
+FOUNDDEVS=""
+CHGDEVS=""
+RMVDDEVS=""
+
+# main
+if [ "@$1" = @--help ] || [ "@$1" = @-h ] || [ "@$1" = "@-?" ] ; then
+    echo "Usage: rescan-scsi-bus.sh [options] [host [host ...]]"
+    echo "Options:"
+    echo " -a      scan all targets, not just currently existing [default: disabled]"
+    echo " -c      enables scanning of channels 0 1   [default: 0 / all detected ones]"
+    echo " -d      enable debug                       [default: 0]"
+    echo " -f      flush failed multipath devices     [default: disabled]"
+    echo " -h      help: print this usage message then exit"
+    echo " -i      issue a FibreChannel LIP reset     [default: disabled]"
+    echo " -I SECS issue a FibreChannel LIP reset and wait for SECS seconds [default: disabled]"
+    echo " -l      activates scanning for LUNs 0--7   [default: 0]"
+    echo " -L NUM  activates scanning for LUNs 0--NUM [default: 0]"
+    echo " -m      update multipath devices           [default: disabled]"
+    echo " -r      enables removing of devices        [default: disabled]"
+    echo " -s      look for resized disks and reload associated multipath devices, if applicable"
+    echo " -t SECS timeout for testing if device is online. Test is skipped if 0 [default: 30]"
+    echo " -u      look for existing disks that have been remapped"
+    echo " -V      print version date then exit"
+    echo " -w      scan for target device IDs 0--15   [default: 0--7]"
+    echo "--alltargets:    same as -a"
+    echo "--attachpq3:     Tell kernel to attach sg to LUN 0 that reports PQ=3"
+    echo "--channels=LIST: Scan only channel(s) in LIST"
+    echo "--color:         use coloured prefixes OLD/NEW/DEL"
+    echo "--flush:         same as -f"
+    echo "--forceremove:   Remove stale devices (DANGEROUS)"
+    echo "--forcerescan:   Remove and readd existing devices (DANGEROUS)"
+    echo "--help:          print this usage message then exit"
+    echo "--hosts=LIST:    Scan only host(s) in LIST"
+    echo "--ids=LIST:      Scan only target ID(s) in LIST"
+    echo "--ignore-rev:    Ignore the revision change"
+    echo "--issue-lip:     same as -i"
+    echo "--issue-lip-wait=SECS:     same as -I"
+    echo "--largelun:      Tell kernel to support LUNs > 7 even on SCSI2 devs"
+    echo "--luns=LIST:     Scan only lun(s) in LIST"
+    echo "--multipath:     same as -m"
+    echo "--no-lip-scan:   don't scan FC Host with issue-lip"
+    echo "--nooptscan:     don't stop looking for LUNs if 0 is not found"
+    echo "--remove:        same as -r"
+    echo "--reportlun2:    Tell kernel to try REPORT_LUN even on SCSI2 devices"
+    echo "--resize:        same as -s"
+    echo "--sparselun:     Tell kernel to support sparse LUN numbering"
+    echo "--sync/nosync:   Issue a sync / no sync [default: sync if remove]"
+    echo "--timeout=SECS:  same as -t"
+    echo "--update:        same as -u"
+    echo "--version:       same as -V"
+    echo "--wide:          same as -w"
+    echo ""
+    echo "Host numbers may thus be specified either directly on cmd line (deprecated)"
+    echo "or with the --hosts=LIST parameter (recommended)."
+    echo "LIST: A[-B][,C[-D]]... is a comma separated list of single values and ranges"
+    echo "(No spaces allowed.)"
+    exit 0
+fi
+
+if [ "@$1" = @--version ] || [ "@$1" = @-V ] ; then
+    echo ${VERSION}
+    exit 0
+fi
+
+if [ ! -d /sys/class/scsi_host/ ] && [ ! -d /proc/scsi/ ] ; then
+  echo "Error: SCSI subsystem not active"
+  exit 1
+fi
+
+# Make sure sg is there
+modprobe sg >/dev/null 2>&1
+
+if [ -x /usr/bin/sg_inq ] ; then
+  sg_version=$(sg_inq -V 2>&1 | cut -d " " -f 3)
+  if [ -n "$sg_version" ] ; then
+    sg_ver_maj=${sg_version:0:1}
+    sg_version=${sg_version##?.}
+    let sg_version+=$((100 * sg_ver_maj))
+  fi
+  sg_version=${sg_version##0.}
+  #echo "\"$sg_version\""
+  if [ -z "$sg_version" ] || [ "$sg_version" -lt 70 ] ; then
+    sg_len_arg="-36"
+  else
+    sg_len_arg="--len=36"
+  fi
+else
+  echo "WARN: /usr/bin/sg_inq not present -- please install sg3_utils"
+  echo " or rescan-scsi-bus.sh might not fully work."
+fi
+
+# defaults
+unsetcolor
+debug=0
+lunsearch=
+opt_idsearch=$(seq -s ' ' 0 7)
+filter_ids=0
+opt_channelsearch=
+remove=
+updated=0
+update=0
+resize=0
+forceremove=
+optscan=1
+sync=1
+existing_targets=1
+mp_enable=
+lipreset=-1
+timeout=30
+declare -i scan_flags=0
+ignore_rev=0
+no_lip_scan=0
+
+# Scan options
+opt="$1"
+while [ ! -z "$opt" ] && [ -z "${opt##-*}" ] ; do
+  opt=${opt#-}
+  case "$opt" in
+    a) existing_targets=;;  #Scan ALL targets when specified
+    c) opt_channelsearch="0 1" ;;
+    d) debug=1 ;;
+    f) flush=1 ;;
+    i) lipreset=0 ;;
+    I) shift; lipreset=$1 ;;
+    l) lunsearch=$(seq -s ' ' 0 7) ;;
+    L) lunsearch=$(seq -s ' ' 0 "$2"); shift ;;
+    m) mp_enable=1 ;;
+    r) remove=1 ;;
+    s) resize=1; mp_enable=1 ;;
+    t) timeout=$2; shift ;;
+    u) update=1 ;;
+    w) opt_idsearch=$(seq -s ' ' 0 15) ;;
+    -alltargets)  existing_targets=;;
+    -attachpq3) scan_flags=$((scan_flags|0x1000000)) ;;
+    -channels=*)  arg=${opt#-channels=};opt_channelsearch=$(expandlist "$arg") ;;
+    -color) setcolor ;;
+    -flush)       flush=1 ;;
+    -forceremove) remove=1; forceremove=1 ;;
+    -forcerescan) remove=1; forcerescan=1 ;;
+    -hosts=*)     arg=${opt#-hosts=};   hosts=$(expandlist "$arg") ;;
+    -ids=*)   arg=${opt#-ids=};         opt_idsearch=$(expandlist "$arg") ; filter_ids=1;;
+    -ignore-rev) ignore_rev=1;;
+    -issue-lip) lipreset=0 ;;
+    -issue-lip-wait=*) lipreset=${opt#-issue-lip-wait=};;
+    -largelun) scan_flags=$((scan_flags|0x200)) ;;
+    -luns=*)  arg=${opt#-luns=};        lunsearch=$(expandlist "$arg") ;;
+    -multipath) mp_enable=1 ;;
+    -no-lip-scan) no_lip_scan=1 ;;
+    -nooptscan) optscan=0 ;;
+    -nosync) sync=0 ;;
+    -remove)      remove=1 ;;
+    -reportlun2) scan_flags=$((scan_flags|0x20000)) ;;
+    -resize) resize=1;;
+    -timeout=*) timeout=${opt#-timeout=};;
+    -sparselun) scan_flags=$((scan_flags|0x40)) ;;
+    -sync) sync=2 ;;
+    -update) update=1;;
+    -wide) opt_idsearch=$(seq -s ' ' 0 15) ;;
+    *) echo "Unknown option -$opt !" ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ -z "$hosts" ] ; then
+  if [ -d /sys/class/scsi_host ] ; then
+    findhosts_26
+  else
+    findhosts
+  fi
+fi
+
+if [ -d /sys/class/scsi_host ] && [ ! -w /sys/class/scsi_host ]; then
+  echo "You need to run scsi-rescan-bus.sh as root"
+  exit 2
+fi
+[ "$sync" = 1 ] && [ "$remove" = 1 ] && sync=2
+if [ "$sync" = 2 ] ; then
+  echo "Syncing file systems"
+  sync
+fi
+if [ -w /sys/module/scsi_mod/parameters/default_dev_flags ] && [ $scan_flags != 0 ] ; then
+  OLD_SCANFLAGS=$(cat /sys/module/scsi_mod/parameters/default_dev_flags)
+  NEW_SCANFLAGS=$((OLD_SCANFLAGS|scan_flags))
+  if [ "$OLD_SCANFLAGS" != "$NEW_SCANFLAGS" ] ; then
+    echo -n "Temporarily setting kernel scanning flags from "
+    printf "0x%08x to 0x%08x\n" "$OLD_SCANFLAGS" "$NEW_SCANFLAGS"
+    echo $NEW_SCANFLAGS > /sys/module/scsi_mod/parameters/default_dev_flags
+  else
+    unset OLD_SCANFLAGS
+  fi
+fi
+DMSETUP=$(which dmsetup)
+[ -z "$DMSETUP" ] && flush= && mp_enable=
+MULTIPATH=$(which multipath)
+[ -z "$MULTIPATH" ] && flush= && mp_enable=
+
+echo -n "Scanning SCSI subsystem for new devices"
+[ -z "$flush" ] || echo -n ", flush failed multipath devices,"
+[ -z "$remove" ] || echo -n " and remove devices that have disappeared"
+echo
+declare -i found=0
+declare -i updated=0
+declare -i rmvd=0
+
+if [ -n "$flush" ] ; then
+  if [ -x "$MULTIPATH" ] ; then
+    flushmpaths 1
+  fi
+fi
+
+# Update existing mappings
+if [ $update -eq 1 ] ; then
+  echo "Searching for remapped LUNs"
+  findremapped
+  # If you've changed the mapping, there's a chance it's a different size
+  mpaths=""
+  findresized
+# Search for resized LUNs
+elif [ $resize -eq 1 ] ; then
+  echo "Searching for resized LUNs"
+  findresized
+# Normal rescan mode
+else
+  for host in $hosts; do
+  echo -n "Scanning host $host "
+  if [ $no_lip_scan -eq 0 ] && [ -e "/sys/class/fc_host/host$host" ] ; then
+    # It's pointless to do a target scan on FC
+    issue_lip=/sys/class/fc_host/host$host/issue_lip
+    if [ -e "$issue_lip" ] && [ "$lipreset" -ge 0 ] ; then
+      echo 1 > "$issue_lip" 2> /dev/null;
+      udevadm_settle
+      [ "$lipreset" -gt 0 ] && sleep "$lipreset"
+    fi
+    channelsearch=
+    idsearch=
+  else
+    channelsearch=$opt_channelsearch
+    idsearch=$opt_idsearch
+  fi
+  [ -n "$channelsearch" ] && echo -n "channels $channelsearch "
+  echo -n "for "
+  if [ -n "$idsearch" ] ; then
+    echo -n " SCSI target IDs $idsearch"
+  else
+    echo -n " all SCSI target IDs"
+  fi
+  if [ -n "$lunsearch" ] ; then
+    echo ", LUNs $lunsearch"
+  else
+    echo ", all LUNs"
+  fi
+
+  if [ -n "$existing_targets" ] ; then
+    searchexisting
+  else
+    dosearch
+  fi
+  done
+  if [ -n "$OLD_SCANFLAGS" ] ; then
+    echo "$OLD_SCANFLAGS" > /sys/module/scsi_mod/parameters/default_dev_flags
+  fi
+fi
+
+let rmvd_found=$rmvd+$found
+if [ -n "$mp_enable" ] && [ $rmvd_found -gt 0 ] ; then
+  echo "Attempting to update multipath devices..."
+  if [ $rmvd -gt 0 ] ; then
+    udevadm_settle
+    echo "Removing multipath mappings for removed devices if all paths are now failed... "
+    flushmpaths 1
+  fi
+  if [ $found -gt 0 ] ; then
+    /sbin/udevadm trigger --sysname-match=sd*
+    udevadm_settle
+    if [ -x "$MULTIPATH" ] ; then
+      echo "Trying to discover new multipath mappings for newly discovered devices... "
+      $MULTIPATH | grep "create:" 2> /dev/null
+    fi
+  fi
+fi
+
+echo "$found new or changed device(s) found.          "
+if [ ! -z "$FOUNDDEVS" ] ; then
+  echo -e "$FOUNDDEVS"
+fi
+echo "$updated remapped or resized device(s) found."
+if [ ! -z "$CHGDEVS" ] ; then
+  echo -e "$CHGDEVS"
+fi
+echo "$rmvd device(s) removed.                 "
+if [ ! -z "$RMVDDEVS" ] ; then
+  echo -e "$RMVDDEVS"
+fi
+
+# Local Variables:
+# sh-basic-offset: 2
+# End:
+
diff --git a/scripts/scsi-enable-target-scan.sh b/scripts/scsi-enable-target-scan.sh
new file mode 100755
index 0000000..63bb9af
--- /dev/null
+++ b/scripts/scsi-enable-target-scan.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+
+MODPARM=/sys/module/scsi_mod/parameters
+if [ -w "$MODPARM/scan" ] ; then
+    scan_type=$(cat $MODPARM/scan)
+    if [ "$scan_type" = "manual" ] ; then
+	echo sync > $MODPARM/scan
+
+	for shost in /sys/class/scsi_host/host* ; do
+	    echo '- - -' > ${shost}/scan
+	done
+    fi
+fi
diff --git a/scripts/scsi_logging_level b/scripts/scsi_logging_level
new file mode 100755
index 0000000..2fba2b7
--- /dev/null
+++ b/scripts/scsi_logging_level
@@ -0,0 +1,268 @@
+#! /bin/bash
+###############################################################################
+# Conveniently create and set scsi logging level, show SCSI_LOG fields in human
+# readable form.
+#
+# (C) Copyright IBM Corp. 2006
+#
+# Modified by D. Gilbert to replace the use of sysctl [20080218]
+# Lat change: D. Gilbert 20150219
+###############################################################################
+
+
+REVISION="1.0"
+SCRIPTNAME="scsi_logging_level"
+
+declare -i LOG_ERROR=0
+declare -i LOG_TIMEOUT=0
+declare -i LOG_SCAN=0
+declare -i LOG_MLQUEUE=0
+declare -i LOG_MLCOMPLETE=0
+declare -i LOG_LLQUEUE=0
+declare -i LOG_LLCOMPLETE=0
+declare -i LOG_HLQUEUE=0
+declare -i LOG_HLCOMPLETE=0
+declare -i LOG_IOCTL=0
+
+declare -i LEVEL=0
+
+SET=0
+GET=0
+CREATE=0
+
+OPTS=$(getopt -o hvcgsa:E:T:S:I:M:L:H: --long \
+help,version,create,get,set,all:,error:,timeout:,scan:,ioctl:,\
+midlevel:,mlqueue:,mlcomplete:,lowlevel:,llqueue:,llcomplete:,\
+highlevel:,hlqueue:,hlcomplete: -n \'$SCRIPTNAME\' -- "$@")
+eval set -- "$OPTS"
+
+# print version info
+printversion()
+{
+    cat <<EOF
+%S390_TOOLS_VERSION% ($SCRIPTNAME $REVISION)
+(C) Copyright IBM Corp. 2006
+EOF
+}
+
+# print usage and help
+printhelp()
+{
+    cat <<EOF
+Usage: $SCRIPTNAME [OPTIONS]
+
+Create, get or set scsi logging level.
+
+Options:
+
+        -h, --help       print this help
+        -v, --version    print version information
+        -s, --set        create and set logging level as specified on
+                         command line
+        -g, --get        get current logging level and display it
+        -c, --create     create logging level as specified on command line
+        -a, --all        specify value for all SCSI_LOG fields
+        -E, --error      specify SCSI_LOG_ERROR
+        -T, --timeout    specify SCSI_LOG_TIMEOUT
+        -S, --scan       specify SCSI_LOG_SCAN
+        -M, --midlevel   specify SCSI_LOG_MLQUEUE and SCSI_LOG_MLCOMPLETE
+            --mlqueue    specify SCSI_LOG_MLQUEUE
+            --mlcomplete specify SCSI_LOG_MLCOMPLETE
+        -L, --lowlevel   specify SCSI_LOG_LLQUEUE and SCSI_LOG_LLCOMPLETE
+            --llqueue    specify SCSI_LOG_LLQUEUE
+            --llcomplete specify SCSI_LOG_LLCOMPLETE
+        -H, --highlevel  specify SCSI_LOG_HLQUEUE and SCSI_LOG_HLCOMPLETE
+            --hlqueue    specify SCSI_LOG_HLQUEUE
+            --hlcomplete specify SCSI_LOG_HLCOMPLETE
+        -I, --ioctl      specify SCSI_LOG_IOCTL
+
+Exactly one of the options "-c", "-g" and "-s" has to be specified.
+Valid values for SCSI_LOG fields are integers from 0 to 7.
+
+Note: Several SCSI_LOG fields can be specified using several options.
+When multiple options specify same SCSI_LOG field the most specific
+option has precedence.
+
+Example: "scsi_logging_level --hlqueue 3 --highlevel 2 --all 1 -s" sets
+SCSI_LOG_HLQUEUE=3, SCSI_LOG_HLCOMPLETE=2 and assigns all other SCSI_LOG
+fields the value 1.
+EOF
+}
+
+check_level()
+{
+    num=$(($1))
+    if [ $num != "$1" ] ; then
+        invalid_cmdline "log level '$1' not a number"
+    elif [ $num -lt 0  ] || [ $num -gt 7 ] ; then
+        invalid_cmdline "log level '$1' out of range, expect '0' to '7'"
+    fi
+}
+
+# check cmd line arguments
+check_cmdline()
+{
+    while true ; do
+        case "$1" in
+            -a|--all)   _ALL="$2"; check_level "$2"
+                        shift 2;;
+            -c|--create) CREATE=1;
+                        shift 1;;
+            -g|--get)   GET=1
+                        shift 1;;
+            -h|--help) printhelp
+                        exit 0;;
+            -s|--set)   SET=1
+                        shift 1;;
+            -v|--version) printversion
+                        exit 0;;
+            -E|--error) _ERROR="$2"; check_level "$2"
+                        shift 2;;
+            -T|--timeout) _TIMEOUT="$2"; check_level "$2"
+                        shift 2;;
+            -S|--scan)  _SCAN="$2"; check_level "$2"
+                        shift 2;;
+            -M|--midlevel) _ML="$2"; check_level "$2"
+                        shift 2;;
+            --mlqueue)  _MLQUEUE="$2"; check_level "$2"
+                        shift 2;;
+            --mlcomplete) _MLCOMPLETE="$2"; check_level "$2"
+                        shift 2;;
+            -L|--lowlevel) _LL="$2"; check_level "$2"
+                        shift 2;;
+            --llqueue)  _LLQUEUE="$2"; check_level "$2"
+                        shift 2;;
+            --llcomplete) _LLCOMPLETE="$2"; check_level "$2"
+                        shift 2;;
+            -H|--highlevel) _HL="$2"; check_level "$2"
+                        shift 2;;
+            --hlqueue)  _HLQUEUE="$2"; check_level "$2"
+                        shift 2;;
+            --hlcomplete) _HLCOMPLETE="$2"; check_level "$2"
+                        shift 2;;
+            -I|--ioctl) _IOCTL="$2"; check_level "$2"
+                        shift 2;;
+            --) shift; break;;
+            *) echo "Internal error!" ; exit 1;;
+        esac
+    done
+
+    if [ -n "$*" ]
+    then
+        invalid_cmdline invalid parameter "$@"
+    fi
+
+    if [ $GET = "1" -a $SET = "1" ]
+    then
+        invalid_cmdline options \'-c\', \'-g\' and \'-s\' are mutual exclusive
+    elif [ $GET = "1" -a $CREATE = "1" ]
+    then
+        invalid_cmdline options \'-c\', \'-g\' and \'-s\' are mutual exclusive
+    elif [ $SET = "1" -a $CREATE = "1" ]
+    then
+        invalid_cmdline options \'-c\', \'-g\' and \'-s\' are mutual exclusive
+    fi
+
+    LOG_ERROR=${_ERROR:-${_ALL:-0}}
+    LOG_TIMEOUT=${_TIMEOUT:-${_ALL:-0}}
+    LOG_SCAN=${_SCAN:-${_ALL:-0}}
+    LOG_MLQUEUE=${_MLQUEUE:-${_ML:-${_ALL:-0}}}
+    LOG_MLCOMPLETE=${_MLCOMPLETE:-${_ML:-${_ALL:-0}}}
+    LOG_LLQUEUE=${_LLQUEUE:-${_LL:-${_ALL:-0}}}
+    LOG_LLCOMPLETE=${_LLCOMPLETE:-${_LL:-${_ALL:-0}}}
+    LOG_HLQUEUE=${_HLQUEUE:-${_HL:-${_ALL:-0}}}
+    LOG_HLCOMPLETE=${_HLCOMPLETE:-${_HL:-${_ALL:-0}}}
+    LOG_IOCTL=${_IOCTL:-${_ALL:-0}}
+}
+
+invalid_cmdline()
+{
+        echo "$SCRIPTNAME: $*"
+        echo "$SCRIPTNAME: Try '$SCRIPTNAME --help' for more information."
+        exit 1
+}
+
+get_logging_level()
+{
+    echo "Current scsi logging level:"
+#   LEVEL=$(sysctl -n dev.scsi.logging_level)
+    LEVEL=$(cat /proc/sys/dev/scsi/logging_level)
+    if [ $? != 0 ]
+    then
+        echo "$SCRIPTNAME: could not read scsi logging level" \
+             "(kernel probably without SCSI_LOGGING support)"
+        exit 1
+    fi
+}
+
+show_logging_level()
+{
+    echo "/proc/sys/dev/scsi/logging_level = $LEVEL"
+
+    LOG_ERROR=$((LEVEL & 7)); LEVEL=$((LEVEL>>3))
+    LOG_TIMEOUT=$((LEVEL & 7)); LEVEL=$((LEVEL>>3))
+    LOG_SCAN=$((LEVEL & 7)); LEVEL=$((LEVEL>>3))
+    LOG_MLQUEUE=$((LEVEL & 7)); LEVEL=$((LEVEL>>3))
+    LOG_MLCOMPLETE=$((LEVEL & 7)); LEVEL=$((LEVEL>>3))
+    LOG_LLQUEUE=$((LEVEL & 7)); LEVEL=$((LEVEL>>3))
+    LOG_LLCOMPLETE=$((LEVEL & 7)); LEVEL=$((LEVEL>>3))
+    LOG_HLQUEUE=$((LEVEL & 7)); LEVEL=$((LEVEL>>3));
+    LOG_HLCOMPLETE=$((LEVEL & 7)); LEVEL=$((LEVEL>>3));
+    LOG_IOCTL=$((LEVEL & 7))
+
+    echo "SCSI_LOG_ERROR=$LOG_ERROR"
+    echo "SCSI_LOG_TIMEOUT=$LOG_TIMEOUT"
+    echo "SCSI_LOG_SCAN=$LOG_SCAN"
+    echo "SCSI_LOG_MLQUEUE=$LOG_MLQUEUE"
+    echo "SCSI_LOG_MLCOMPLETE=$LOG_MLCOMPLETE"
+    echo "SCSI_LOG_LLQUEUE=$LOG_LLQUEUE"
+    echo "SCSI_LOG_LLCOMPLETE=$LOG_LLCOMPLETE"
+    echo "SCSI_LOG_HLQUEUE=$LOG_HLQUEUE"
+    echo "SCSI_LOG_HLCOMPLETE=$LOG_HLCOMPLETE"
+    echo "SCSI_LOG_IOCTL=$LOG_IOCTL"
+}
+
+set_logging_level()
+{
+    echo "New scsi logging level:"
+#   sysctl -q -w dev.scsi.logging_level=$LEVEL
+    echo $LEVEL > /proc/sys/dev/scsi/logging_level
+    if [ $? != 0 ]
+    then
+        echo "$SCRIPTNAME: could not write scsi logging level $LEVEL"
+        echo "  kernel does not have SCSI_LOGGING support or needs superuser"
+        exit 1
+    fi
+}
+create_logging_level()
+{
+    LEVEL=$((LOG_IOCTL & 7)); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_HLCOMPLETE & 7))); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_HLQUEUE & 7))); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_LLCOMPLETE & 7))); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_LLQUEUE & 7))); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_MLCOMPLETE & 7))); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_MLQUEUE & 7))); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_SCAN & 7))); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_TIMEOUT & 7))); LEVEL=$((LEVEL<<3))
+    LEVEL=$((LEVEL|(LOG_ERROR & 7)))
+}
+
+check_cmdline "$@"
+
+if [ $SET = "1" ]
+then
+    create_logging_level
+    set_logging_level
+    show_logging_level
+elif [ $GET = "1" ]
+then
+    get_logging_level
+    show_logging_level
+elif [ $CREATE = "1" ]
+then
+    create_logging_level
+    show_logging_level
+else
+    invalid_cmdline missing option \'-g\', \'-s\' or \'-c\'
+fi
diff --git a/scripts/scsi_mandat b/scripts/scsi_mandat
new file mode 100755
index 0000000..1f72b40
--- /dev/null
+++ b/scripts/scsi_mandat
@@ -0,0 +1,133 @@
+#!/bin/bash
+# scsi_mandat
+#
+# Script to test compliance with SCSI mandatory commands.
+# The vintage is SPC-3 and SPC-4 (see www.t10.org).
+#
+# Coverage:
+# Command                Standard/Draft (is mandatory in)
+# -------------------------------------------------------
+# INQUIRY (standard)     SCSI-2, SPC, SPC-2, SPC-3, SPC-4
+# INQUIRY (VPD pages 0, 0x83)     SPC-2, SPC-3, SPC-4
+# REPORT LUNS            SPC-3, SPC-4
+# TEST UNIT READY        SCSI-2, SPC, SPC-2, SPC-3, SPC-4
+# REQUEST SENSE          SCSI-2, SBC, SBC-2,3, MMC-4,5, SSC-2,3
+# SEND DIAGNOSTIC        SBC, SBC-2,3, SSC-2,3
+#
+# This script uses utilities frim sg3_utils package (version
+# 1.21 or later)
+#
+# Douglas Gilbert 20131016
+
+
+log=0
+quiet=0
+verbose=""
+
+file_err=0
+inv_opcode=0
+illeg_req=0
+not_ready=0
+medium=0
+other_err=0
+recovered=0
+sanity=0
+syntax=0
+timeout=0
+unit_attention=0
+aborted_command=0
+
+## total_err=0
+
+usage()
+{
+  echo "Usage: scsi_mandat [-h] [-L] [-q] [-v] <device>"
+  echo "  where:  -h, --help        print usage message"
+  echo "          -L, --log         append stderr to 'scsi_mandat.err'"
+  echo "          -q, --quiet       suppress some output"
+  echo "          -v, --verbose     increase verbosity of output"
+  echo ""
+  echo "Check <device> for mandatory SCSI command support"
+}
+
+
+opt="$1"
+while test ! -z "$opt" -a -z "${opt##-*}"; do
+  opt=${opt#-}
+  case "$opt" in
+    h|-help) usage ; exit 0 ;;
+    L|-log) let log=$log+1 ;;
+    q|-quiet) let quiet=$quiet+1 ;;
+    v|-verbose) verbose="-v" ;;
+    vv) verbose="-vv" ;;
+    vvv) verbose="-vvv" ;;
+    *) echo "Unknown option: -$opt " ; exit 1 ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ $# -lt 1 ]
+  then
+    usage
+    exit 1
+fi
+
+for command in "sg_inq" "sg_luns" "sg_turs" "sg_requests" "sg_vpd" \
+                "sg_vpd -i" "sg_senddiag -t"
+do
+  if [ $quiet -eq 0 ]
+    then echo "$command" $verbose "$1"
+  fi
+
+  if [ $verbose ]
+  then
+    if [ $log -eq 0 ]
+    then
+      $command $verbose "$1"
+    else
+      $command $verbose "$1" >> scsi_mandat.err 2>> scsi_mandat.err
+    fi
+  else
+    if [ $log -eq 0 ]
+    then
+      $command "$1" > /dev/null 2>> /dev/null
+    else
+      $command "$1" > /dev/null 2>> scsi_mandat.err
+    fi
+  fi
+  res=$?
+  case "$res" in
+    0) ;;
+    1) echo "  syntax error" ; let syntax=$syntax+1 ;;
+    2) echo "  not ready" ; let not_ready=$not_ready+1 ;;
+    3) echo "  medium error" ; let medium=$medium+1 ;;
+    5) echo "  illegal request, general" ; let illeg_req=$illeg_req+1 ;;
+    6) echo "  unit attention" ; let unit_attention=$unit_attention+1 ;;
+    9) echo "  illegal request, invalid opcode" ; let inv_opcode=$inv_opcode+1 ;;
+    11) echo "  aborted command" ; let aborted_command=$aborted_command+1 ;;
+    15) echo "  file error with $1 " ; let file_err=$file_err+1 ;;
+    20) echo "  no sense" ; let other_err=$other_err+1 ;;
+    21) echo "  recovered error" ; let recovered=$recovered+1 ;;
+    33) echo "  timeout" ; let timeout=$timeout+1 ;;
+    97) echo "  response fails sanity" ; let sanity=$sanity+1 ;;
+    98) echo "  other SCSI error" ; let other_err=$other_err+1 ;;
+    99) echo "  other error" ; let other_err=$other_err+1 ;;
+    *) echo "  unknown exit status for sg_inq: $res" ; let other_err=$other_err+1 ;;
+  esac
+done
+
+echo ""
+let total_bad_err=$file_err+$inv_opcode+$illeg_req+$medium+$aborted_command
+let total_bad_err+=$other_err+$recovered+$sanity+$syntax+$timeout
+
+let total_allow_err=$not_ready+$unit_attention
+
+  echo "total number of bad errors: $total_bad_err "
+
+if [ $total_allow_err -gt 0 ]
+  then
+  echo "total number of allowable errors: $total_allow_err "
+fi
+
+exit $total_bad_err
diff --git a/scripts/scsi_readcap b/scripts/scsi_readcap
new file mode 100755
index 0000000..8f308f4
--- /dev/null
+++ b/scripts/scsi_readcap
@@ -0,0 +1,57 @@
+#!/bin/bash
+
+###################################################################
+#
+#  Fetch READ CAPACITY information for the given SCSI device(s).
+#
+#  This script assumes the sg3_utils package is installed.
+#
+##################################################################
+
+verbose=""
+brief=""
+long_opt=""
+
+usage()
+{
+  echo "Usage: scsi_readcap [-b] [-h] [-l] [-v] <device>+"
+  echo "  where:"
+  echo "    -b, --brief          output brief capacity data"
+  echo "    -h, --help           print usage message"
+  echo "    -l, --long           send longer SCSI READ CAPACITY (16) cdb"
+  echo "    -v, --verbose        more verbose output"
+  echo ""
+  echo "Use SCSI READ CAPACITY command to fetch the size of each <device>"
+}
+
+opt="$1"
+while test ! -z "$opt" -a -z "${opt##-*}"; do
+  opt=${opt#-}
+  case "$opt" in
+    b|-brief) brief="-b" ;;
+    h|-help) usage ; exit 0 ;;
+    l|-long) long_opt="--16" ;;
+    v|-verbose) verbose="-v" ;;
+    vv) verbose="-vv" ;;
+    vvv) verbose="-vvv" ;;
+    *) echo "Unknown option: -$opt " ; exit 1 ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ $# -lt 1 ]
+  then
+    usage
+    exit 1
+fi
+
+for i
+do
+	if [ $brief ] ; then
+        	sg_readcap $brief $long_opt $verbose $i 2> /dev/null
+	else
+		echo "sg_readcap $brief $long_opt $verbose $i"
+        	sg_readcap $brief $long_opt $verbose $i
+	fi
+done
diff --git a/scripts/scsi_ready b/scripts/scsi_ready
new file mode 100755
index 0000000..724c2c6
--- /dev/null
+++ b/scripts/scsi_ready
@@ -0,0 +1,56 @@
+#!/bin/bash
+
+################################################
+#
+#  Send a TEST UNIT READY SCSI command to each given device.
+#
+#  This script assumes the sg3_utils package is installed and uses
+#  the sg_turs utility..
+#
+###############################################
+
+verbose=""
+brief=""
+
+usage()
+{
+  echo "Usage: scsi_ready [-b] [-h] [-v] <device>+"
+  echo "  where:"
+  echo "    -b, --brief          print 'ready' or 'device not ready' only"
+  echo "    -h, --help           print usage message"
+  echo "    -v, --verbose        more verbose output"
+  echo ""
+  echo "Send SCSI TEST UNIT READY to each <device>"
+}
+
+opt="$1"
+while test ! -z "$opt" -a -z "${opt##-*}"; do
+  opt=${opt#-}
+  case "$opt" in
+    b|-brief) brief="1" ;;
+    h|-help) usage ; exit 0 ;;
+    v|-verbose) verbose="-v" ;;
+    vv) verbose="-vv" ;;
+    vvv) verbose="-vvv" ;;
+    *) echo "Unknown option: -$opt " ; exit 1 ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ $# -lt 1 ]
+  then
+    usage
+    exit 1
+fi
+
+for i
+do
+	if [ ! $brief ] ; then
+		echo "sg_turs $verbose $i"
+	fi
+	echo -n "    "
+        if sg_turs $verbose $i ; then
+		echo "ready"
+	fi
+done
diff --git a/scripts/scsi_satl b/scripts/scsi_satl
new file mode 100755
index 0000000..08042ae
--- /dev/null
+++ b/scripts/scsi_satl
@@ -0,0 +1,134 @@
+#!/bin/bash
+# scsi_satl
+#
+# Script to test compliance of SCSI commands on a SCSI to ATA
+# Translation (SAT) Layer (SATL). This script was compiled using
+# sat-r09.pdf found at www.t10.org .
+# The scripts still seems to be valid for sat2r09.pdf .
+# The vintage is SPC-3 and SPC-4 (see www.t10.org).
+#
+# Coverage:
+# Command                SATL notes
+# -------------------------------------------------------
+# INQUIRY (standard)
+# INQUIRY (VPD: 0)
+# INQUIRY (VPD: 0x83)    Device identification VPD page
+# INQUIRY (VPD: 0x89)    ATA Information VPD page
+# REPORT LUNS            SPC-3, SPC-4 (hardly mentioned in sat-r08c)
+# TEST UNIT READY
+# REQUEST SENSE
+# SEND DIAGNOSTIC        default self test
+# MODE SENSE(10)         draft unclear which mode pages, so ask for all
+# ATA PASS THROUGH(16)   send IDENTIFY DEVICE command. Assume non-packet
+#                        device, if packet device add "-p" option
+#
+# This script uses utilities from sg3_utils package (version
+# 1.22 or later)
+#
+# Douglas Gilbert 20090930
+
+
+log=0
+quiet=0
+verbose=""
+
+file_err=0
+inv_opcode=0
+illeg_req=0
+not_ready=0
+medium=0
+other_err=0
+recovered=0
+sanity=0
+syntax=0
+timeout=0
+unit_attention=0
+aborted_command=0
+
+## total_err=0
+
+usage()
+{
+  echo "Usage: scsi_satl [-h] [-L] [-q] [-v] <device>"
+  echo "  where:  -h, --help       print usage message"
+  echo "          -L, --log        append stderr to 'scsi_satl.err'"
+  echo "          -q, --quiet      suppress some output"
+  echo "          -v, --verbose    more verbose output"
+  echo ""
+  echo "Check <device> for SCSI to ATA Translation Layer (SATL) support"
+}
+
+opt="$1"
+while test ! -z "$opt" -a -z "${opt##-*}"; do
+  opt=${opt#-}
+  case "$opt" in
+    h|-help) usage ; exit 1 ;;
+    L|-log) let log=$log+1 ;;
+    q|-quiet) let quiet=$quiet+1 ;;
+    v|-verbose) verbose="-v" ;;
+    *) echo "Unknown option: -$opt " ; exit 1 ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ $# -lt 1 ]
+  then
+    usage
+    exit 1
+fi
+
+for command in "sg_inq" "sg_vpd" "sg_vpd -p di" "sg_vpd -p ai" "sg_luns" \
+               "sg_turs" "sg_requests -s" "sg_senddiag -t" "sg_modes -a" \
+               "sg_sat_identify"
+do
+  if [ $quiet -eq 0 ]
+    then echo "$command" "$1"
+  fi
+
+  if [ $log -eq 0 ]
+  then
+    if [ $verbose ]
+    then
+      $command $verbose "$1" > /dev/null
+    else
+      $command "$1" > /dev/null 2>> /dev/null
+    fi
+  else
+    $command $verbose "$1" > /dev/null 2>> scsi_satl.err
+  fi
+  res=$?
+  case "$res" in
+    0) ;;
+    1) echo "  syntax error" ; let syntax=$syntax+1 ;;
+    2) echo "  not ready" ; let not_ready=$not_ready+1 ;;
+    3) echo "  medium error" ; let medium=$medium+1 ;;
+    5) echo "  illegal request, general" ; let illeg_req=$illeg_req+1 ;;
+    6) echo "  unit attention" ; let unit_attention=$unit_attention+1 ;;
+    9) echo "  illegal request, invalid opcode" ; let inv_opcode=$inv_opcode+1 ;;
+    11) echo "  aborted command" ; let aborted_command=$aborted_command+1 ;;
+    15) echo "  file error with $1 " ; let file_err=$file_err+1 ;;
+    20) echo "  no sense" ; let other_err=$other_err+1 ;;
+    21) echo "  recovered error" ; let recovered=$recovered+1 ;;
+    33) echo "  timeout" ; let timeout=$timeout+1 ;;
+    97) echo "  response fails sanity" ; let sanity=$sanity+1 ;;
+    98) echo "  other SCSI error" ; let other_err=$other_err+1 ;;
+    99) echo "  other error" ; let other_err=$other_err+1 ;;
+    *) echo "  unknown exit status for sg_inq: $res" ; let other_err=$other_err+1 ;;
+  esac
+done
+
+echo ""
+let total_bad_err=$file_err+$inv_opcode+$illeg_req+$medium+$aborted_command
+let total_bad_err+=$other_err+$recovered+$sanity+$syntax+$timeout
+
+let total_allow_err=$not_ready+$unit_attention
+
+  echo "total number of bad errors: $total_bad_err "
+
+if [ $total_allow_err -gt 0 ]
+  then
+  echo "total number of allowable errors: $total_allow_err "
+fi
+
+exit $total_bad_err
diff --git a/scripts/scsi_start b/scripts/scsi_start
new file mode 100755
index 0000000..aec7ab9
--- /dev/null
+++ b/scripts/scsi_start
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+################################################
+#
+#  Spin up the given SCSI disk(s).
+#
+#  SCSI disks (or disks that understand SCSI commands)
+#  are assumed. By default, the immediate bit is set so the
+#  command should return immediately. The disk however will
+#  take 10 seconds or more to spin up. The '-w' option
+#  causes each start to wait until the disk reports that it
+#  has started.
+#
+#  This script assumes the sg3_utils package is installed.
+#
+###############################################
+
+verbose=""
+immediate="-i"
+
+usage()
+{
+  echo "Usage: scsi_start [-h] [-v] [-w] <device>+"
+  echo "  where:"
+  echo "    -h, --help           print usage message"
+  echo "    -v, --verbose        more verbose output"
+  echo "    -w, --wait           wait for each start to complete"
+  echo ""
+  echo "Send SCSI START STOP UNIT command to start each <device>"
+}
+
+opt="$1"
+while test ! -z "$opt" -a -z "${opt##-*}"; do
+  opt=${opt#-}
+  case "$opt" in
+    h|-help) usage ; exit 0 ;;
+    v|-verbose) verbose="-v" ;;
+    w|-wait) immediate="" ;;
+    *) echo "Unknown option: -$opt " ; exit 1 ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ $# -lt 1 ]
+  then
+    usage
+    exit 1
+fi
+
+for i
+do
+	echo "sg_start $immediate 1 $verbose $i"
+        sg_start $immediate 1 $verbose $i
+done
diff --git a/scripts/scsi_stop b/scripts/scsi_stop
new file mode 100755
index 0000000..7680723
--- /dev/null
+++ b/scripts/scsi_stop
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+################################################
+#
+#  Spin down the given SCS disk(s).
+#
+#  SCSI disks (or disks that understand SCSI commands)
+#  are assumed. By default, the immediate bit is set so the
+#  command should return immediately. The disk however will
+#  take 10 seconds or more to spin down. The '-w' option
+#  causes each stop to wait until the disk reports that it
+#  has stopped.
+#
+#  This script assumes the sg3_utils package is installed.
+#
+###############################################
+
+verbose=""
+immediate="-i"
+
+usage()
+{
+  echo "Usage: scsi_stop [-h] [-v] [-w] <device>+"
+  echo "  where:"
+  echo "    -h, --help           print usage message"
+  echo "    -v, --verbose        more verbose output"
+  echo "    -w, --wait           wait for each stop to complete"
+  echo ""
+  echo "Send SCSI START STOP UNIT command to stop each <device>"
+}
+
+opt="$1"
+while test ! -z "$opt" -a -z "${opt##-*}"; do
+  opt=${opt#-}
+  case "$opt" in
+    h|-help) usage ; exit 0 ;;
+    v|-verbose) verbose="-v" ;;
+    w|-wait) immediate="" ;;
+    *) echo "Unknown option: -$opt " ; exit 1 ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ $# -lt 1 ]
+  then
+    usage
+    exit 1
+fi
+
+for i
+do
+# Use '-r' (read-only) otherwise using a block device node
+# (e.g. 'sg_start 0 /dev/sdb') can result in a change of state
+# event causing the disk to spin up again immediately.
+        echo "sg_start -r $immediate 0 $verbose $i"
+        sg_start -r $immediate 0 $verbose $i
+done
diff --git a/scripts/scsi_temperature b/scripts/scsi_temperature
new file mode 100755
index 0000000..f7d041c
--- /dev/null
+++ b/scripts/scsi_temperature
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+###################################################################
+#
+#  Check the temperature of the given SCSI device(s).
+#
+#  This script assumes the sg3_utils package is installed.
+#
+##################################################################
+
+verbose=""
+
+usage()
+{
+  echo "Usage: scsi_temperature [-h] [-v] <device>+"
+  echo "  where:"
+  echo "    -h, --help           print usage message"
+  echo "    -v, --verbose        more verbose output"
+  echo ""
+  echo "Use SCSI LOG SENSE command to fetch temperature of each <device>"
+}
+
+opt="$1"
+while test ! -z "$opt" -a -z "${opt##-*}"; do
+  opt=${opt#-}
+  case "$opt" in
+    h|-help) usage ; exit 0 ;;
+    v|-verbose) verbose="-v" ;;
+    vv) verbose="-vv" ;;
+    *) echo "Unknown option: -$opt " ; exit 1 ;;
+  esac
+  shift
+  opt="$1"
+done
+
+if [ $# -lt 1 ]
+  then
+    usage
+    exit 1
+fi
+
+for i
+do
+	echo "sg_logs -t $verbose $i"
+        sg_logs -t $verbose $i
+done
diff --git a/sg3_utils.man8.html b/sg3_utils.man8.html
new file mode 100644
index 0000000..579985d
--- /dev/null
+++ b/sg3_utils.man8.html
@@ -0,0 +1,989 @@
+Content-type: text/html; charset=UTF-8
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<HTML><HEAD><TITLE>Man page of SG3_UTILS</TITLE>
+</HEAD><BODY>
+<H1>SG3_UTILS</H1>
+Section: SG3_UTILS (8)<BR>Updated: November 2021<BR><A HREF="#index">Index</A>
+<A HREF="../index.html">Return to Main Contents</A><HR>
+
+<A NAME="lbAB">&nbsp;</A>
+<H2>NAME</H2>
+
+sg3_utils - a package of utilities for sending SCSI commands
+<A NAME="lbAC">&nbsp;</A>
+<H2>SYNOPSIS</H2>
+
+<B>sg_*</B>
+
+[<I>--dry-run</I>] [<I>--enumerate</I>] [<I>--help</I>] [<I>--hex</I>]
+[<I>--in=FN</I>] [<I>--inhex=FN</I>] [<I>--maxlen=LEN</I>] [<I>--raw</I>]
+[<I>--timeout=SECS</I>] [<I>--verbose</I>] [<I>--version</I>]
+[<I>OTHER_OPTIONS</I>] <I>DEVICE</I>
+<A NAME="lbAD">&nbsp;</A>
+<H2>DESCRIPTION</H2>
+
+
+<P>
+
+sg3_utils is a package of utilities that send SCSI commands to the given
+<I>DEVICE</I> via a SCSI pass through interface provided by the host
+operating system.
+<P>
+
+The names of all utilities start with &quot;sg&quot; and most start with &quot;sg_&quot; often
+followed by the name, or a shortening of the name, of the SCSI command that
+they send. For example the &quot;sg_verify&quot; utility sends the SCSI VERIFY
+command. A mapping between SCSI commands and the sg3_utils utilities that
+issue them is shown in the COVERAGE file. The sg_raw utility can be used to
+send an arbitrary SCSI command (supplied on the command line) to the
+given <I>DEVICE</I>.
+<P>
+
+sg_decode_sense can be used to decode SCSI sense data given on the command
+line or in a file. sg_raw -vvv will output the T10 name of a given SCSI
+CDB which is most often 16 bytes or less in length.
+<P>
+
+SCSI draft standards can be found at <A HREF="https://www.t10.org">https://www.t10.org</A> . The standards
+themselves can be purchased from ANSI and other standards organizations.
+A good overview of various SCSI standards can be seen in
+<A HREF="https://www.t10.org/scsi-3.htm">https://www.t10.org/scsi-3.htm</A> with the SCSI command sets in the upper part
+of the diagram. The highest level (i.e. most abstract) document is the SCSI
+Architecture Model (SAM) with SAM-5 being the most recent standard (ANSI
+INCITS 515-2016) with the most recent draft being SAM-6 revision 4 . SCSI
+commands in common with all device types can be found in SCSI Primary
+Commands (SPC) of which SPC-4 is the most recent standard (ANSI INCITS
+513-2015). The most recent SPC draft is SPC-5 revision 21. Block device
+specific commands (e.g. as used by disks) are in SBC, those for tape drives
+in SSC, those for SCSI enclosures in SES and those for CD/DVD/BD drives in
+MMC.
+<P>
+
+It is becoming more common to control ATA disks with the SCSI command set.
+This involves the translation of SCSI commands to their corresponding ATA
+equivalents (and that is an imperfect mapping in some cases). The relevant
+standard is called SCSI to ATA Translation (SAT, SAT-2 and SAT-3) are
+now standards at INCITS(ANSI) and ISO while SAT-4 is at the draft stage.
+The logic to perform the command translation is often called a SAT Layer or
+SATL and may be within an operating system, in host bus adapter firmware or
+in an external device (e.g. associated with a SAS expander). See
+<A HREF="https://www.t10.org">https://www.t10.org</A> for more information.
+<P>
+
+There is some support for SCSI tape devices but not for their basic
+operation. The reader is referred to the &quot;mt&quot; utility.
+<P>
+
+There are two generations of command line option usage. The newer
+utilities (written since July 2004) use the getopt_long() function to parse
+command line options. With that function, each option has two representations:
+a short form (e.g. '-v') and a longer form (e.g. '--verbose'). If an
+argument is required then it follows a space (optionally) in the short form
+and a &quot;=&quot; in the longer form (e.g. in the sg_verify utility '-l 2a6h'
+and '--lba=2a6h' are equivalent). Note that with getopt_long(), short form
+options can be elided, for example: '-all' is equivalent to '-a -l -l'.
+The <I>DEVICE</I> argument may appear after, between or prior to any options.
+<P>
+
+The older utilities, including as sg_inq, sg_logs, sg_modes, sg_opcode,
+sg_rbuff,  sg_readcap, sg_senddiag, sg_start and sg_turs had individual
+command line processing code typically based on a single &quot;-&quot; followed by one
+or more characters. If an argument is needed then it follows a &quot;=&quot; (
+e.g. '-p=1f' in sg_modes with its older interface). Various options can be
+elided as long as it is not ambiguous (e.g. '-vv' to increase the verbosity).
+<P>
+
+Over time the command line interface of these older utilities became messy
+and overloaded with options. So in sg3_utils version 1.23 the command line
+interface of these older utilities was altered to have both a cleaner
+getopt_long() interface and their older interface for backward compatibility.
+By default these older utilities use their getopt_long() based interface.
+The getopt_long() is a GNU extension (i.e. not yet POSIX certified) but
+more recent command line utilities tend to use it. That can be overridden
+by defining the SG3_UTILS_OLD_OPTS environment variable or using '-O'
+or '--old' as the first command line option. The man pages of the older
+utilities documents the details.
+<P>
+
+Several sg3_utils utilities are based on the Unix dd command (e.g. sg_dd)
+and permit copying data at the level of SCSI READ and WRITE commands. sg_dd
+is tightly bound to Linux and hence is not ported to other OSes. A more
+generic utility (than sg_dd) called ddpt in a package of the same name has
+been ported to other OSes.
+<A NAME="lbAE">&nbsp;</A>
+<H2>ENVIRONMENT VARIABLES</H2>
+
+The SG3_UTILS_OLD_OPTS environment variable is explained in the previous
+section. It is only for backward compatibility of the command line options
+for older utilities.
+<P>
+
+The SG3_UTILS_DSENSE environment variable may be set to a number. It is
+only used by the embedded SNTL within the library used by the utilities in
+this library. SNTL is a SCSI to NVMe Translation Layer. This environment
+variable defaults to 0 which will lead to any utility that issues a SCSI
+command that is translated to a NVMe command (by the embedded SNTL) that
+fails at the NVMe dvice, to return SCSI sense in 'fixed' format. If this
+variable is non-zero then then the returned SCSI sense will be in 'descriptor'
+format.
+<P>
+
+Several utilities have their own environment variable setting (e.g.
+sg_persist has SG_PERSIST_IN_RDONLY). See individual utility man pages
+for more information.
+<P>
+
+There is a Linux specific environment variable called SG3_UTILS_LINUX_NANO
+that if defined and the sg driver in the system is 4.0.30 or later, will
+show command durations in nanoseconds rather than the default milliseconds.
+Command durations are typically only shown if --verbose is used 3 or more
+times. Due to an interface problem (a 32 bit integer that should be 64 bits
+with the benefit of hindsight) the maximum duration that can be represented
+in nanoseconds is about 4.2 seconds. If longer durations may occur then
+don't define this environment variable (or undefine it).
+<A NAME="lbAF">&nbsp;</A>
+<H2>LINUX DEVICE NAMING</H2>
+
+Most disk block devices have names like /dev/sda, /dev/sdb, /dev/sdc, etc.
+SCSI disks in Linux have always had names like that but in recent Linux
+kernels it has become more common for many other disks (including SATA
+disks and USB storage devices) to be named like that. Partitions within a
+disk are specified by a number appended to the device name, starting at
+1 (e.g. /dev/sda1 ).
+<P>
+
+Tape drives are named /dev/st&lt;num&gt; or /dev/nst&lt;num&gt; where &lt;num&gt; starts
+at zero. Additionally one letter from this list: &quot;lma&quot; may be appended to
+the name. CD, DVD and BD readers (and writers) are named /dev/sr&lt;num&gt;
+where &lt;num&gt; start at zero. There are less used SCSI device type names,
+the dmesg and the lsscsi commands may help to find if any are attached to
+a running system.
+<P>
+
+There is also a SCSI device driver which offers alternate generic access
+to SCSI devices. It uses names of the form /dev/sg&lt;num&gt; where &lt;num&gt; starts
+at zero. The &quot;lsscsi -g&quot; command may be useful in finding these and which
+generic name corresponds to a device type name (e.g. /dev/sg2 may
+correspond to /dev/sda). In the lk 2.6 series a block SCSI generic
+driver was introduced and its names are of the form
+/dev/bsg/&lt;h:c:t:l&gt; where h, c, t and l are numbers. Again see the lsscsi
+command to find the correspondence between that SCSI tuple (i.e. &lt;h:c:t:l&gt;)
+and alternate device names.
+<P>
+
+Prior to the Linux kernel 2.6 series these utilities could only use
+generic device names (e.g. /dev/sg1 ). In almost all cases in the Linux
+kernel 2.6 series, any device name can be used by these utilities.
+<P>
+
+Very little has changed in Linux device naming in the Linux kernel 3
+and 4 series.
+<A NAME="lbAG">&nbsp;</A>
+<H2>WINDOWS DEVICE NAMING</H2>
+
+Storage and related devices can have several device names in Windows.
+Probably the most common in the volume name (e.g. &quot;D:&quot;). There are also
+a &quot;class&quot; device names such as &quot;PhysicalDrive&lt;n&gt;&quot;, &quot;CDROM&lt;n&gt;&quot;
+and &quot;TAPE&lt;n&gt;&quot;. &lt;n&gt; is an integer starting at 0 allocated in ascending
+order as devices are discovered (and sometimes rediscovered).
+<P>
+
+Some storage devices have a SCSI lower level device name which starts
+with a SCSI (pseudo) adapter name of the form &quot;SCSI&lt;n&gt;:&quot;. To this is added
+sub-addressing in the form of a &quot;bus&quot; number, a &quot;target&quot; identifier and
+a LUN (Logical Unit Number). The &quot;bus&quot; number is also known as a &quot;PathId&quot;.
+These are assembled to form a device name of the
+form: &quot;SCSI&lt;n&gt;:&lt;bus&gt;,&lt;target&gt;,&lt;lun&gt;&quot;. The trailing &quot;,&lt;lun&gt;&quot; may be omitted
+in which case a LUN of zero is assumed. This lower level device name cannot
+often be used directly since Windows blocks attempts to use it if a class
+driver has &quot;claimed&quot; the device. There are SCSI device types (e.g.
+Automation/Drive interface type) for which there is no class driver. At
+least two transports (&quot;bus types&quot; in Windows jargon): USB and IEEE 1394 do
+not have a &quot;scsi&quot; device names of this form.
+<P>
+
+In keeping with DOS file system conventions, the various device names
+can be given in upper, lower or mixed case. Since &quot;PhysicalDrive&lt;n&gt;&quot; is
+tedious to write, a shortened form of &quot;PD&lt;n&gt;&quot; is permitted by all
+utilities in this package.
+<P>
+
+A single device (e.g. a disk) can have many device names. For
+example: &quot;PD0&quot; can also be &quot;C:&quot;, &quot;D:&quot; and &quot;SCSI0:0,1,0&quot;. The two volume names
+reflect that the disk has two partitions on it. Disk partitions that are
+not recognized by Windows are not usually given a volume name. However
+Vista does show a volume name for a disk which has no partitions recognized
+by it and when selected invites the user to format it (which may be rather
+unfriendly to other OSes).
+<P>
+
+These utilities assume a given device name is in the Win32 device namespace.
+To make that explicit &quot;\\.\&quot; can be prepended to the device names mentioned
+in this section. Beware that backslash is an escape character in Unix like
+shells and the C programming language. In a shell like Msys (from MinGW)
+each backslash may need to be typed twice.
+<P>
+
+The sg_scan utility within this package lists out Windows device names in
+a form that is suitable for other utilities in this package to use.
+<A NAME="lbAH">&nbsp;</A>
+<H2>FREEBSD DEVICE NAMING</H2>
+
+SCSI disks have block names of the form /dev/da&lt;num&gt; where &lt;num&gt; is an
+integer starting at zero. The &quot;da&quot; is replaced by &quot;sa&quot; for SCSI tape
+drives and &quot;cd&quot; for SCSI CD/DVD/BD drives. Each SCSI device has a
+corresponding pass-through device name of the form /dev/pass&lt;num&gt;
+where &lt;num&gt; is an integer starting at zero. The &quot;camcontrol devlist&quot;
+command may be useful for finding out which SCSI device names are
+available and the correspondence between class and pass-through names.
+<P>
+
+FreeBSD allows device names to be given without the leading &quot;/dev/&quot; (e.g.
+da0 instead of /dev/da0). That worked in this package up until version
+1.43 when the unadorned device name (e.g. &quot;da0&quot;) gave an error. The
+original action (i.e. allowing unadorned device names) has been restored
+in version 1.46 . Also note that symlinks (to device names) are followed
+before prepending &quot;/dev/&quot; if the resultant name doesn't start with a &quot;/&quot;.
+<P>
+
+FreeBSD's NVMe naming has been evolving. The controller naming is the
+same as Linux: &quot;/dev/nvme&lt;n&gt;&quot; but the namespaces have an
+extra &quot;s&quot; (e.g. &quot;/dev/nvme0ns1&quot;). The latter is not a block (GEOM)
+device (strictly speaking FreeBSD does not have block devices). Initially
+FreeBSD had &quot;/dev/nvd&lt;m&gt;&quot; GEOM devices that were not based on the CAM
+subsystem. Then in FreeBSD release 12 a new nda driver was added that is
+CAM (and GEOM) based for NVMe namespaces; it has names like &quot;/dev/nda0&quot;.
+The preferred device nodes for this package are &quot;/dev/nvme0&quot; for NVMe
+controllers and &quot;/dev/nda0&quot; for NVMe namespaces.
+<A NAME="lbAI">&nbsp;</A>
+<H2>SOLARIS DEVICE NAMING</H2>
+
+SCSI device names below the /dev directory have a form like: c5t4d3s2
+where the number following &quot;c&quot; is the controller (HBA) number, the number
+following &quot;t&quot; is the target number (from the SCSI parallel interface days)
+and the number following &quot;d&quot; is the LUN. Following the &quot;s&quot; is the slice
+number which is related to a partition and by convention &quot;s2&quot; is the whole
+disk.
+<P>
+
+OpenSolaris also has a c5t4d3p2 form where the number following the &quot;p&quot; is
+the partition number apart from &quot;p0&quot; which is the whole disk. So a whole
+disk may be referred to as either c5t4d3, c5t4d3s2 or c5t4d3p0 .
+<P>
+
+And these device names are duplicated in the /dev/dsk and /dev/rdsk
+directories. The former is the block device name and the latter is
+for &quot;raw&quot; (or char device) access which is what sg3_utils needs. So in
+OpenSolaris something of the form 'sg_inq /dev/rdsk/c5t4d3p0' should work.
+If it doesn't work then add a '-vvv' option for more debug information.
+Trying this form 'sg_inq /dev/dsk/c5t4d3p0' (note &quot;rdsk&quot; changed to &quot;dsk&quot;)
+will result in an &quot;inappropriate ioctl for device&quot; error.
+<P>
+
+The device names within the /dev directory are typically symbolic links to
+much longer topological names in the /device directory. In Solaris cd/dvd/bd
+drives seem to be treated as disks and so are found in the /dev/rdsk
+directory. Tape drives appear in the /dev/rmt directory.
+<P>
+
+There is also a sgen (SCSI generic) driver which by default does not attach
+to any device. See the /kernel/drv/sgen.conf file to control what is
+attached. Any attached device will have a device name of the
+form /dev/scsi/c5t4d3 .
+<P>
+
+Listing available SCSI devices in Solaris seems to be a challenge. &quot;Use
+the 'format' command&quot; advice works but seems a very dangerous way to list
+devices. [It does prompt again before doing any damage.] 'devfsadm -Cv'
+cleans out the clutter in the /dev/rdsk directory, only leaving what
+is &quot;live&quot;. The &quot;cfgadm -v&quot; command looks promising.
+<A NAME="lbAJ">&nbsp;</A>
+<H2>NVME SUPPORT</H2>
+
+NVMe (or NVM Express) is a relatively new storage transport and command
+set. The level of abstraction of the NVMe command set is somewhat lower
+the SCSI command sets, closer to the level of abstraction of ATA (and SATA)
+command sets. NVMe claims to be designed with flash and modern &quot;solid
+state&quot; storage in mind, something unheard of when SCSI was originally
+developed in the 1980s.
+<P>
+
+The SCSI command sets' advantage is the length of time they have been in
+place and the existing tools (like these) to support it. Plus SCSI command
+sets level of abstraction is both and advantage and disadvantage. Recently
+the NVME-MI (Management Interface) designers decide to use the SCSI
+Enclosure Services (SES-3) standard &quot;as is&quot; with the addition of two
+tunnelling NVME-MI commands: SES Send and SES Receive. This means after the
+OS interface differences are taken into account, the sg_ses, sg_ses_microcode
+and sg_senddiag utilities can be used on a NVMe device that supports a newer
+version of NVME-MI.
+<P>
+
+The NVME-MI SES Send and SES Receive commands correspond to the SCSI
+SEND DIAGNOSTIC and RECEIVE DIAGNOSTIC RESULTS commands respectively.
+There are however a few other commands that need to be translated, the
+most important of which is the SCSI INQUIRY command to the NVMe Identify
+controller/namespace. Starting in version 1.43 these utilities contain a
+small SNTL (SCSI to NVMe Translation Layer) to take care of these details.
+<P>
+
+As a side effect of this &quot;juggling&quot; if the sg_inq utility is used (without
+the --page= option) on a NVMe <I>DEVICE</I> then the actual NVMe
+Identifier (controller and possibly namespace) responses are decoded and
+output. However if 'sg_inq --page=sinq &lt;device&gt;' is given for the
+same <I>DEVICE</I> then parts of the NVMe Identify controller and namespace
+response are translated to a SCSI standard INQUIRY response which is then
+decoded and output.
+<P>
+
+Apart from the special case with the sg_inq, all other utilities in the
+package assume they are talking to a SCSI device and decode any response
+accordingly. One easy way for users to see the underlying device is a
+NVMe device is the standard INQUIRY response Vendor Identification field
+of &quot;NVMe    &quot; (an 8 character long string with 4 spaces to the right).
+<P>
+
+The following SCSI commands are currently supported by the SNTL library:
+INQUIRY, MODE SELECT(10), MODE SENSE(10), READ(10,16), READ CAPACITY(10,16),
+RECEIVE DIAGNOSTIC RESULTS, REQUEST SENSE, REPORT LUNS, REPORT SUPPORTED
+OPERATION CODES, REPORT SUPPORTED TASK MANAGEMENT FUNCTIONS, SEND
+DIAGNOSTICS, START STOP UNIT, SYNCHRONIZE CACHE(10,16), TEST UNIT READY,
+VERIFY(10,16), WRITE(10,16) and WRITE SAME(10,16).
+<A NAME="lbAK">&nbsp;</A>
+<H2>EXIT STATUS</H2>
+
+To aid scripts that call these utilities, the exit status is set to indicate
+success (0) or failure (1 or more). Note that some of the lower values
+correspond to the SCSI sense key values.
+<P>
+
+The exit status values listed below can be given to the sg_decode_sense
+utility (which is found in this package) as follows:
+<BR>
+
+<BR>&nbsp;&nbsp;sg_decode_sense&nbsp;--err=&lt;exit_status&gt;
+<BR>
+
+and a short explanatory string will be output to stdout.
+<P>
+
+The exit status values are:
+<DL COMPACT>
+<DT><B>0</B>
+
+<DD>
+success. Also used for some utilities that wish to return a boolean value
+for the &quot;true&quot; case (and that no error has occurred). The false case is
+conveyed by exit status 36.
+<DT><B>1</B>
+
+<DD>
+syntax error. Either illegal command line options, options with bad
+arguments or a combination of options that is not permitted.
+<DT><B>2</B>
+
+<DD>
+the <I>DEVICE</I> reports that it is not ready for the operation requested.
+The <I>DEVICE</I> may be in the process of becoming ready (e.g.  spinning up
+but not at speed) so the utility may work after a wait. In Linux the
+<I>DEVICE</I> may be temporarily blocked while error recovery is taking place.
+<DT><B>3</B>
+
+<DD>
+the <I>DEVICE</I> reports a medium or hardware error (or a blank check). For
+example an attempt to read a corrupted block on a disk will yield this value.
+<DT><B>5</B>
+
+<DD>
+the <I>DEVICE</I> reports an &quot;illegal request&quot; with an additional sense code
+other than &quot;invalid command operation code&quot;. This is often a supported
+command with a field set requesting an unsupported capability. For commands
+that require a &quot;service action&quot; field this value can indicate that the
+command with that service action value is not supported.
+<DT><B>6</B>
+
+<DD>
+the <I>DEVICE</I> reports a &quot;unit attention&quot; condition. This usually indicates
+that something unrelated to the requested command has occurred (e.g. a device
+reset) potentially before the current SCSI command was sent. The requested
+command has not been executed by the device. Note that unit attention
+conditions are usually only reported once by a device.
+<DT><B>7</B>
+
+<DD>
+the <I>DEVICE</I> reports a &quot;data protect&quot; sense key. This implies some
+mechanism has blocked writes (or possibly all access to the media).
+<DT><B>9</B>
+
+<DD>
+the <I>DEVICE</I> reports an illegal request with an additional sense code
+of &quot;invalid command operation code&quot; which means that it doesn't support the
+requested command.
+<DT><B>10</B>
+
+<DD>
+the <I>DEVICE</I> reports a &quot;copy aborted&quot;. This implies another command or
+device problem has stopped a copy operation. The EXTENDED COPY family of
+commands (including WRITE USING TOKEN) may return this sense key.
+<DT><B>11</B>
+
+<DD>
+the <I>DEVICE</I> reports an aborted command. In some cases aborted
+commands can be retried immediately (e.g. if the transport aborted
+the command due to congestion).
+<DT><B>14</B>
+
+<DD>
+the <I>DEVICE</I> reports a miscompare sense key. VERIFY and COMPARE AND
+WRITE commands may report this.
+<DT><B>15</B>
+
+<DD>
+the utility is unable to open, close or use the given <I>DEVICE</I> or some
+other file. The given file name could be incorrect or there may be
+permission problems. Adding the '-v' option may give more information.
+<DT><B>17</B>
+
+<DD>
+a SCSI &quot;Illegal request&quot; sense code received with a flag indicating the
+Info field is valid. This is often a LBA but its meaning is command specific.
+<DT><B>18</B>
+
+<DD>
+the <I>DEVICE</I> reports a medium or hardware error (or a blank check)
+with a flag indicating the Info field is valid. This is often a LBA (of
+the first encountered error) but its meaning is command specific.
+<DT><B>20</B>
+
+<DD>
+the <I>DEVICE</I> reports it has a check condition but &quot;no sense&quot;
+and non-zero information in its additional sense codes. Some polling
+commands (e.g. REQUEST SENSE) can receive this response. There may
+be useful information in the sense data such as a progress indication.
+<DT><B>21</B>
+
+<DD>
+the <I>DEVICE</I> reports a &quot;recovered error&quot;. The requested command
+was successful. Most likely a utility will report a recovered error
+to stderr and continue, probably leaving the utility with an exit
+status of 0 .
+<DT><B>22</B>
+
+<DD>
+the <I>DEVICE</I> reports that the current command or its parameters imply
+a logical block address (LBA) that is out of range. This happens surprisingly
+often when trying to access the last block on a storage device; either a
+classic &quot;off by one&quot; logic error or a misreading of the response from READ
+CAPACITY(10 or 16) in which the address of the last block rather than the
+number of blocks on the <I>DEVICE</I> is returned. Since LBAs are origin zero
+they range from 0 to n-1 where n is the number of blocks on the <I>DEVICE</I>,
+so the LBA of the last block is one less than the total number of blocks.
+<DT><B>24</B>
+
+<DD>
+the <I>DEVICE</I> reports a SCSI status of &quot;reservation conflict&quot;. This
+means access to the <I>DEVICE</I> with the current command has been blocked
+because another machine (HBA or SCSI &quot;initiator&quot;) holds a reservation on
+this <I>DEVICE</I>. On modern SCSI systems this is related to the use of
+the PERSISTENT RESERVATION family of commands.
+<DT><B>25</B>
+
+<DD>
+the <I>DEVICE</I> reports a SCSI status of &quot;condition met&quot;. Currently only
+the PRE-FETCH command (see SBC-4) yields this status.
+<DT><B>26</B>
+
+<DD>
+the <I>DEVICE</I> reports a SCSI status of &quot;busy&quot;. SAM-6 defines this status
+as the logical unit is temporarily unable to process a command. It is
+recommended to re-issue the command.
+<DT><B>27</B>
+
+<DD>
+the <I>DEVICE</I> reports a SCSI status of &quot;task set full&quot;.
+<DT><B>28</B>
+
+<DD>
+the <I>DEVICE</I> reports a SCSI status of &quot;ACA active&quot;. ACA is &quot;auto
+contingent allegiance&quot; and is seldom used.
+<DT><B>29</B>
+
+<DD>
+the <I>DEVICE</I> reports a SCSI status of &quot;task aborted&quot;. SAM-5 says:
+&quot;This status shall be returned if a command is aborted by a command or task
+management function on another I_T nexus and the Control mode page TAS bit
+is set to one&quot;.
+<DT><B>31</B>
+
+<DD>
+error involving two or more command line options. They may be contradicting,
+select an unsupported mode, or a required option (given the context) is
+missing.
+<DT><B>32</B>
+
+<DD>
+there is a logic error in the utility. It corresponds to code comments
+like &quot;shouldn't/can't get here&quot;. Perhaps the author should be informed.
+<DT><B>33</B>
+
+<DD>
+the command sent to <I>DEVICE</I> has timed out.
+<DT><B>34</B>
+
+<DD>
+this is a Windows only exit status and indicates that the Windows error
+number (32 bits) cannot meaningfully be mapped to an equivalent Unix error
+number returned as the exit status (7 bits).
+<DT><B>35</B>
+
+<DD>
+a transport error has occurred. This will either be in the driver (e.g. HBA
+driver) or in the interconnect between the host (initiator) and the
+device (target).  For example in SAS an expander can run out of paths and
+thus be unable to return the user data for a READ command.
+<DT><B>36</B>
+
+<DD>
+no error has occurred plus the utility wants to convey a boolean value
+of false. The corresponding true value is conveyed by a 0 exit status.
+<DT><B>40</B>
+
+<DD>
+the command sent to <I>DEVICE</I> has received an &quot;aborted command&quot; sense
+key with an additional sense code of 0x10. This value is related to
+problems with protection information (PI or DIF). For example this error
+may occur when reading a block on a drive that has never been written (or
+is unmapped) if that drive was formatted with type 1, 2 or 3 protection.
+<DT><B>41</B>
+
+<DD>
+the command sent to <I>DEVICE</I> has received an &quot;aborted command&quot; sense
+key with an additional sense code of 0x10 (as with error code) plus a flag
+indicating the Info field is valid.
+<DT><B>48</B>
+
+<DD>
+this is an internal message indicating a NVMe status field (SF) is other
+than zero after a command has been executed (i.e. something went wrong).
+Work in this area is currently experimental.
+<DT><B>49</B>
+
+<DD>
+low level driver reports a response's residual count (i.e. number of bytes
+actually received by HBA is 'requested_bytes - residual_count') that is
+nonsensical.
+<DT><B>50</B>
+
+<DD>
+OS system calls that fail often return a small integer number to help. In
+Unix these are called &quot;errno&quot; values where 0 implies no error. These error
+codes set aside 51 to 96 for mapping these errno values but that may not be
+sufficient. Higher errno values that cannot be mapped are all mapped to
+this value (i.e. 50).
+<BR>
+
+Note that an errno value of 0 is mapped to error code 0.
+<DT><B>50 + &lt;os_error_number&gt;</B>
+
+<DD>
+OS system calls that fail often return a small integer number to help
+indicate what the error is. For example in Unix the inability of a system
+call to allocate memory returns (in 'errno') ENOMEM which often is
+associated with the integer 12. So 62 (i.e. '50 + 12') may be returned
+by a utility in this case. It is also possible that a utility in this
+package reports 50+ENOMEM when it can't allocate memory, not necessarily
+from an OS system call. In recent versions of Linux the file showing the
+mapping between symbolic constants (e.g. ENOMEM) and the corresponding
+integer is in the kernel source code file:
+include/uapi/asm-generic/errno-base.h
+<BR>
+
+Note that errno values that are greater than or equal to 47 cannot fit in
+range provided. Instead they are all mapped to 50 as discussed in the
+previous entry.
+<DT><B>97</B>
+
+<DD>
+a SCSI command response failed sanity checks.
+<DT><B>98</B>
+
+<DD>
+the <I>DEVICE</I> reports it has a check condition but the error
+doesn't fit into any of the above categories.
+<DT><B>99</B>
+
+<DD>
+any errors that can't be categorized into values 1 to 98 may yield
+this value. This includes transport and operating system errors
+after the command has been sent to the device.
+<DT><B>100-125</B>
+
+<DD>
+these error codes are used by the ddpt utility which uses the sg3_utils
+library. They are mainly specialized error codes associated with offloaded
+copies.
+<DT><B>126</B>
+
+<DD>
+the utility was found but could not be executed. That might occur if the
+executable does not have execute permissions.
+<DT><B>127</B>
+
+<DD>
+This is the exit status for utility not found. That might occur when a
+script calls a utility in this package but the PATH environment variable
+has not been properly set up, so the script cannot find the executable.
+<DT><B>128 + &lt;signum&gt;</B>
+
+<DD>
+If a signal kills a utility then the exit status is 128 plus the signal
+number. For example if a segmentation fault occurs then a utility is
+typically killed by SIGSEGV which according to 'man 7 signal' has an
+associated signal number of 11; so the exit status will be 139 .
+<DT><B>255</B>
+
+<DD>
+the utility tried to yield an exit status of 255 or larger. That should
+not happen; given here for completeness.
+</DL>
+<P>
+
+Most of the error conditions reported above will be repeatable (an example
+of one that is not is &quot;unit attention&quot;) so the utility can be run again with
+the '-v' option (or several) to obtain more information.
+<A NAME="lbAL">&nbsp;</A>
+<H2>COMMON OPTIONS</H2>
+
+Arguments to long options are mandatory for short options as well. In the
+short form an argument to an option uses zero or more spaces as a
+separator (i.e. the short form does not use &quot;=&quot; as a separator).
+<P>
+
+If an option takes a numeric argument then that argument is assumed to
+be decimal unless otherwise indicated (e.g. with a leading &quot;0x&quot;, a
+trailing &quot;h&quot; or as noted in the usage message).
+<P>
+
+Some options are used uniformly in most of the utilities in this
+package. Those options are listed below. Note that there are some
+exceptions.
+<DL COMPACT>
+<DT><B>-d</B>, <B>--dry-run</B><DD>
+utilities that can cause lots of user data to be lost or overwritten
+sometimes have a <I>--dry-run</I> option. Device modifying actions are
+typically bypassed (or skipped) to implement a policy of &quot;do no harm&quot;.
+This allows complex command line invocations to be tested before the
+action required (e.g. format a disk) is performed. The <I>--dry-run</I>
+option has become a common feature of many command line utilities (e.g.
+the Unix 'patch' command), not just those from this package.
+<BR>
+
+Note that most hyphenated option names in this package also can be given
+with an underscore rather than a hyphen (e.g.  <I>--dry_run</I>).
+<DT><B>-e</B>, <B>--enumerate</B><DD>
+some utilities (e.g. sg_ses and sg_vpd) store a lot of information in
+internal tables. This option will output that information in some readable
+form (e.g. sorted by an acronym or by page number) then exit. Note that
+with this option <I>DEVICE</I> is ignored (as are most other options) and no
+SCSI IO takes place, so the invoker does not need any elevated permissions.
+<DT><B>-h</B>, <B>-?</B>, <B>--help</B><DD>
+output the usage message then exit. In a few older utilities the '-h'
+option requests hexadecimal output. In these cases the '-?' option will
+output the usage message then exit.
+<DT><B>-H</B>, <B>--hex</B><DD>
+for SCSI commands that yield a non-trivial response, print out that
+response in ASCII hexadecimal. To produce hexadecimal that can be parsed
+by other utilities (e.g. without a relative address to the left and without
+trailing ASCII) use this option three or four times.
+<DT><B>-i</B>, <B>--in</B>=<I>FN</I><DD>
+many SCSI commands fetch a significant amount of data (returned in the
+data-in buffer) which several of these utilities decode (e.g. sg_vpd and
+sg_logs). To separate the two steps of fetching the data from a SCSI device
+and then decoding it, this option has been added. The first step (fetching
+the data) can be done using the <I>--hex</I> or <I>--raw</I> option and
+redirecting the command line output to a file (often done with &quot;&gt;&quot; in Unix
+based operating systems). The difference between <I>--hex</I> and
+<I>--raw</I> is that the former produces output in ASCII hexadecimal
+while <I>--raw</I> produces its output in &quot;raw&quot; binary.
+<BR>
+
+The second step (i.e. decoding the SCSI response data now held in a file)
+can be done using this <I>--in=FN</I> option where the file name is
+<I>FN</I>. If &quot;-&quot; is used for <I>FN</I> then stdin is assumed, again this
+allows for command line redirection (or piping). That file (or stdin)
+is assumed to contain ASCII hexadecimal unless the <I>--raw</I> option is
+also given in which case it is assumed to be binary. Notice that the meaning
+of the <I>--raw</I> option is &quot;flipped&quot; when used with <I>--in=FN</I> to
+act on the input, typically it acts on the output data.
+<BR>
+
+Since the structure of the data returned by SCSI commands varies
+considerably then the usage information or the manpage of the utility being
+used should be checked. In some cases <I>--hex</I> may need to be used
+multiple times (and is more conveniently given as '-HH' or '-HHH).
+<DT><B>-i</B>, <B>--inhex</B>=<I>FN</I><DD>
+This option has the same or similar functionality as <I>--in=FN</I>. And
+perhaps 'inhex' is more descriptive since by default, ASCII hexadecimal is
+expected in the contents of file: <I>FN</I>. Alternatively the short form
+option may be <I>-I</I> or <I>-X</I>. See the &quot;FORMAT OF FILES CONTAINING
+ASCII HEX&quot; section below for more information.
+<DT><B>-m</B>, <B>--maxlen</B>=<I>LEN</I><DD>
+several important SCSI commands (e.g. INQUIRY and MODE SENSE) have response
+lengths that vary depending on many factors, only some of which these
+utilities take into account. The maximum response length is typically
+specified in the 'allocation length' field of the cdb. In the absence of
+this option, several utilities use a default allocation length (sometimes
+recommended in the SCSI draft standards) or a &quot;double fetch&quot; strategy.
+See <A HREF="../man8/sg_logs.8.html">sg_logs</A>(8) for its description of a &quot;double fetch&quot; strategy. These
+techniques are imperfect and in the presence of faulty SCSI targets can
+cause problems (e.g. some USB mass storage devices freeze if they receive
+an INQUIRY allocation length other than 36). Also use of this option
+disables any &quot;double fetch&quot; strategy that may have otherwise been used.
+<BR>
+
+To head off a class of degenerate bugs, if <I>LEN</I> is less than 16 then
+it is ignored (usually with a warning message) and the default value is
+used instead. Some utilities use 4 (bytes), rather than 16, as the cutoff
+value.
+<DT><B>-r</B>, <B>--raw</B><DD>
+for SCSI commands that yield a non-trivial response, output that response
+in binary to stdout. If any error messages or warning are produced they are
+usually sent to stderr so as to not interfere with the output from this
+option.
+<BR>
+
+Some utilities that consume data to send to the <I>DEVICE</I> along with the
+SCSI command, use this option. Alternatively the <I>--in=FN</I> option causes
+<I>DEVICE</I> to be ignored and the response data (to be decoded) fetched
+from a file named <I>FN</I>. In these cases this option may indicate that
+binary data can be read from stdin or from a nominated file (e.g. <I>FN</I>).
+<DT><B>-t</B>, <B>--timeout</B>=<I>SECS</I><DD>
+utilities that issue potentially long-running SCSI commands often have a
+<I>--timeout=SECS</I> option. This typically instructs the operating system
+to abort the SCSI command in question once the timeout expires. Aborting
+SCSI commands is typically a messy business and in the case of format like
+commands may leave the device in a &quot;format corrupt&quot; state requiring another
+long-running re-initialization command to be sent. The argument, <I>SECS</I>,
+is usually in seconds and the short form of the option may be something
+other than <I>-t</I> since the timeout option was typically added later as
+storage devices grew in size and initialization commands took longer. Since
+many utilities had relatively long internal command timeouts before this
+option was introduced, the actual command timeout given to the operating
+systems is the higher of the internal timeout and <I>SECS</I>.
+<BR>
+
+Many long running SCSI commands have an IMMED bit which causes the command
+to finish relatively quickly but the initialization process to continue. In
+such cases the REQUEST SENSE command can be used to monitor progress with
+its progress indication field (see the sg_requests and sg_turs utilities).
+Utilities that send such SCSI command either have an <I>--immed</I> option
+or a <I>--wait</I> option which is the logical inverse of the &quot;immediate&quot;
+action.
+<DT><B>-v</B>, <B>--verbose</B><DD>
+increase the level of verbosity, (i.e. debug output). Can be used multiple
+times to further increase verbosity. The additional output caused by this
+option is almost always sent to stderr.
+<DT><B>-V</B>, <B>--version</B><DD>
+print the version string and then exit. Each utility has its own version
+number and date of last code change.
+</DL>
+<A NAME="lbAM">&nbsp;</A>
+<H2>NUMERIC ARGUMENTS</H2>
+
+Many utilities have command line options that take numeric arguments. These
+numeric arguments can be large values (e.g. a logical block address (LBA) on
+a disk) and can be inconvenient to enter in the default decimal
+representation. So various other representations are permitted.
+<P>
+
+Multiplicative suffixes are accepted. They are one, two or three letter
+strings appended directly after the number to which they apply:
+<P>
+
+<BR>&nbsp;&nbsp;&nbsp;c&nbsp;C&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*1
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;w&nbsp;W&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*2
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;b&nbsp;B&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*512
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;k&nbsp;K&nbsp;KiB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*1024
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;KB&nbsp;kB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*1000
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;m&nbsp;M&nbsp;MiB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*1048576
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;MB&nbsp;mB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*1000000
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;g&nbsp;G&nbsp;GiB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*(2^30)
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;GB&nbsp;gB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*(10^9)
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;t&nbsp;T&nbsp;TiB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*(2^40)
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;TB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*(10^12)
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;p&nbsp;P&nbsp;PiB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*(2^50)
+<BR>
+
+<BR>&nbsp;&nbsp;&nbsp;PB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*(10^15)
+<P>
+
+An example is &quot;2k&quot; for 2048. The large tera and peta suffixes are only
+available for numeric arguments that might require 64 bits to represent
+internally.
+<P>
+
+These multiplicative suffixes are compatible with GNU's dd command (since
+2002) which claims compliance with SI and with IEC 60027-2.
+<P>
+
+A suffix of the form &quot;x&lt;n&gt;&quot; multiplies the preceding number by &lt;n&gt;. An
+example is &quot;2x33&quot; for &quot;66&quot;. The left argument cannot be '0' as '0x' will
+be interpreted as hexadecimal number prefix (see below). The left
+argument to the multiplication must end in a hexadecimal digit (i.e.
+0 to f) and the whole expression cannot have any embedded whitespace (e.g.
+spaces). An ugly example: &quot;0xfx0x2&quot; for 30.
+<P>
+
+A suffix of the form &quot;+&lt;n&gt;&quot; adds the preceding number to &lt;n&gt;. An example
+is &quot;3+1k&quot; for &quot;1027&quot;. The left argument to the addition must end in a
+hexadecimal digit (i.e. 0 to f) and the whole expression cannot have any
+embedded whitespace (e.g. spaces). Another example: &quot;0xf+0x2&quot; for 17.
+<P>
+
+Alternatively numerical arguments can be given in hexadecimal. There are
+two syntaxes. The number can be preceded by either &quot;0x&quot; or &quot;0X&quot; as found
+in the C programming language. The second hexadecimal representation is a
+trailing &quot;h&quot; or &quot;H&quot; as found in (storage) standards. When hex numbers are
+given, multipliers cannot be used. For example the decimal value &quot;256&quot; can
+be given as &quot;0x100&quot; or &quot;100h&quot;.
+<A NAME="lbAN">&nbsp;</A>
+<H2>FORMAT OF FILES CONTAINING ASCII HEX</H2>
+
+Such a file is assumed to contain a sequence of one or two digit ASCII
+hexadecimal values separated by whitespace. &quot;Whitespace consists of either
+spaces, tabs, blank lines, or any combination thereof&quot;. Each one or two digit
+ASCII hex pair is decoded into a byte (i.e. 8 bits). The following will be
+decoded to valid (ascending valued)
+bytes: '0', '01', '3', 'c', 'F', '4a', 'cC', 'ff'.
+Lines containing only whitespace are ignored. The contents of any line
+containing a hash mark ('#') is ignored from that point until the end of that
+line. Users are encouraged to use hash marks to introduce comments in hex
+files. The author uses the extension'.hex' on such files. Examples can be
+found in the 'inhex' directory.
+<A NAME="lbAO">&nbsp;</A>
+<H2>MICROCODE AND FIRMWARE</H2>
+
+There are two standardized methods for downloading microcode (i.e. device
+firmware) to a SCSI device. The more general way is with the SCSI WRITE
+BUFFER command, see the sg_write_buffer utility. SCSI enclosures have
+their own method based on the Download microcode control/status diagnostic
+page, see the sg_ses_microcode utility.
+<A NAME="lbAP">&nbsp;</A>
+<H2>SCRIPTS, EXAMPLES and UTILS</H2>
+
+There are several bash shell scripts in the 'scripts' subdirectory that
+invoke compiled utilities (e.g. sg_readcap). Several of the scripts start
+with 'scsi_' rather than 'sg_'. One purpose of these scripts is to call the
+same utility (e.g. sg_readcap) on multiple devices. Most of the basic
+compiled utilities only allow one device as an argument. Some distributions
+install these scripts in a more visible directory (e.g. /usr/bin). Some of
+these scripts have man page entries. See the README file in the 'scripts'
+subdirectory.
+<P>
+
+There is some example C code plus examples of complex invocations in
+the 'examples' subdirectory. There is also a README file. The example C
+may be a simpler example of how to use a SCSI pass-through in Linux
+than the main utilities (found in the 'src' subdirectory). This is due
+to the fewer abstraction layers (e.g. they don't worry the MinGW in
+Windows may open a file in text rather than binary mode).
+<P>
+
+Some utilities that the author has found useful have been placed in
+the 'utils' subdirectory.
+<A NAME="lbAQ">&nbsp;</A>
+<H2>WEB SITE</H2>
+
+There is a web page discussing this package at
+<A HREF="https://sg.danny.cz/sg/sg3_utils.html">https://sg.danny.cz/sg/sg3_utils.html</A> . The device naming used by this
+package on various operating systems is discussed at:
+<A HREF="https://sg.danny.cz/sg/device_name.html">https://sg.danny.cz/sg/device_name.html</A> . There is a git code mirror at
+<A HREF="https://github.com/hreinecke/sg3_utils">https://github.com/hreinecke/sg3_utils</A> . The principle code repository
+uses subversion and is on the author's equipment. The author keeps track
+of this via the subversion revision number which is an ascending integer
+(currently at 774 for this package). The github mirror gets updated
+periodically from the author's repository. Depending on the time of
+update, the above Downloads section at sg.danny.cz may be more up to
+date than the github mirror.
+<A NAME="lbAR">&nbsp;</A>
+<H2>AUTHORS</H2>
+
+Written by Douglas Gilbert. Some utilities have been contributed, see the
+CREDITS file and individual source files (in the 'src' directory).
+<A NAME="lbAS">&nbsp;</A>
+<H2>REPORTING BUGS</H2>
+
+Report bugs to &lt;dgilbert at interlog dot com&gt;.
+<A NAME="lbAT">&nbsp;</A>
+<H2>COPYRIGHT</H2>
+
+Copyright &#169; 1999-2021 Douglas Gilbert
+<BR>
+
+Some utilities are distributed under a GPL version 2 license while
+others, usually more recent ones, are under a FreeBSD license. The files
+that are common to almost all utilities and thus contain the most reusable
+code, namely sg_lib.[hc], sg_cmds_basic.[hc] and sg_cmds_extra.[hc] are
+under a FreeBSD license. There is NO warranty; not even for MERCHANTABILITY
+or FITNESS FOR A PARTICULAR PURPOSE.
+<A NAME="lbAU">&nbsp;</A>
+<H2>SEE ALSO</H2>
+
+<B>sdparm(sdparm), ddpt(ddpt), lsscsi(lsscsi), <A HREF="../man1/dmesg.1.html">dmesg</A>(1), <A HREF="../man1/mt.1.html">mt</A>(1)</B>
+
+<P>
+
+<HR>
+<A NAME="index">&nbsp;</A><H2>Index</H2>
+<DL>
+<DT><A HREF="#lbAB">NAME</A><DD>
+<DT><A HREF="#lbAC">SYNOPSIS</A><DD>
+<DT><A HREF="#lbAD">DESCRIPTION</A><DD>
+<DT><A HREF="#lbAE">ENVIRONMENT VARIABLES</A><DD>
+<DT><A HREF="#lbAF">LINUX DEVICE NAMING</A><DD>
+<DT><A HREF="#lbAG">WINDOWS DEVICE NAMING</A><DD>
+<DT><A HREF="#lbAH">FREEBSD DEVICE NAMING</A><DD>
+<DT><A HREF="#lbAI">SOLARIS DEVICE NAMING</A><DD>
+<DT><A HREF="#lbAJ">NVME SUPPORT</A><DD>
+<DT><A HREF="#lbAK">EXIT STATUS</A><DD>
+<DT><A HREF="#lbAL">COMMON OPTIONS</A><DD>
+<DT><A HREF="#lbAM">NUMERIC ARGUMENTS</A><DD>
+<DT><A HREF="#lbAN">FORMAT OF FILES CONTAINING ASCII HEX</A><DD>
+<DT><A HREF="#lbAO">MICROCODE AND FIRMWARE</A><DD>
+<DT><A HREF="#lbAP">SCRIPTS, EXAMPLES and UTILS</A><DD>
+<DT><A HREF="#lbAQ">WEB SITE</A><DD>
+<DT><A HREF="#lbAR">AUTHORS</A><DD>
+<DT><A HREF="#lbAS">REPORTING BUGS</A><DD>
+<DT><A HREF="#lbAT">COPYRIGHT</A><DD>
+<DT><A HREF="#lbAU">SEE ALSO</A><DD>
+</DL>
+<HR>
+This document was created by
+<A HREF="/cgi-bin/man/man2html">man2html</A>,
+using the manual pages.<BR>
+Time: 03:12:28 GMT, November 11, 2021
+</BODY>
+</HTML>
diff --git a/sg3_utils.spec b/sg3_utils.spec
new file mode 100644
index 0000000..3be40df
--- /dev/null
+++ b/sg3_utils.spec
@@ -0,0 +1,280 @@
+Summary: Utilities for devices that use SCSI command sets
+Name: sg3_utils
+Version: 1.48
+# Release: 1%{?dist}
+Release: 1
+License: GPL
+Group: Utilities/System
+Source: https://sg.danny.cz/sg/p/sg3_utils-%{version}.tar.gz
+Url: https://sg.danny.cz/sg/sg3_utils.html
+Provides: sg_utils
+# BuildRequires: libtool
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+Packager: Douglas Gilbert <dgilbert@interlog.com>
+
+%description
+Collection of Linux utilities for devices that use the SCSI command set.
+Includes utilities to copy data based on "dd" syntax and semantics (called
+sg_dd, sgp_dd and sgm_dd); check INQUIRY data and VPD pages (sg_inq); check
+mode and log pages (sginfo, sg_modes and sg_logs); spin up and down
+disks (sg_start); do self tests (sg_senddiag); and various other functions.
+See the README, ChangeLog and COVERAGE files. Requires the linux kernel 2.4
+series or later. In the 2.4 series SCSI generic device names (e.g. /dev/sg0)
+must be used. In the 2.6 series and later other device names may be used as
+well (e.g. /dev/sda). Also some support for NVMe devices, especially with
+sg_ses on NVMe enclosures.
+
+Warning: Some of these tools access the internals of your system
+and the incorrect usage of them may render your system inoperable.
+
+%package libs
+Summary: Shared library for %{name}
+Group: System/Libraries
+
+%description libs
+This package contains the shared library for %{name}.
+
+%package devel
+Summary: Static library and header files for the sgutils library
+Group: Development/C
+Requires: %{name}-libs = %{version}-%{release}
+
+%description devel
+This package contains the static %{name} library and its header files for
+developing applications.
+
+%prep
+%setup -q
+
+%build
+%configure
+
+# Don't use rpath!
+sed -i 's|^hardcode_libdir_flag_spec=.*|hardcode_libdir_flag_spec=""|g' libtool
+sed -i 's|^runpath_var=LD_RUN_PATH|runpath_var=DIE_RPATH_DIE|g' libtool
+
+%install
+if [ "$RPM_BUILD_ROOT" != "/" ]; then
+        rm -rf $RPM_BUILD_ROOT
+fi
+
+make install \
+        DESTDIR=$RPM_BUILD_ROOT
+
+%clean
+if [ "$RPM_BUILD_ROOT" != "/" ]; then
+        rm -rf $RPM_BUILD_ROOT
+fi
+
+%files
+%defattr(-,root,root)
+%doc AUTHORS ChangeLog COPYING COVERAGE CREDITS INSTALL NEWS README README.sg_start
+%attr(755,root,root) %{_bindir}/*
+%{_mandir}/man8/*
+
+%files libs
+%defattr(-,root,root)
+%{_libdir}/*.so.*
+
+%files devel
+%defattr(-,root,root)
+%{_includedir}/scsi/*.h
+%{_libdir}/*.so
+%{_libdir}/*.a
+
+%changelog
+* Sat Nov 12 2022 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.48
+
+* Tue Nov 09 2021 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.47
+
+* Mon Mar 29 2021 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.46
+
+* Sat Feb 29 2020 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.45
+
+* Wed Sep 12 2018 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.44
+
+* Tue Sep 11 2018 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.43
+
+* Wed Feb 17 2016 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.42
+
+* Tue Apr 28 2015 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.41
+
+* Mon Nov 10 2014 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.40
+
+* Thu Jun 12 2014 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.39
+
+* Tue Apr 01 2014 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.38
+
+* Mon Oct 14 2013 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.37
+
+* Fri May 31 2013 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.36
+
+* Thu Jan 17 2013 - dgilbert at interlog dot com
+- add sg_compare_and_write, track t10 changes
+  * sg3_utils-1.35
+
+* Sat Oct 13 2012 - dgilbert at interlog dot com
+- add sg_xcopy and sg_copy_results; track t10 changes
+  * sg3_utils-1.34
+
+* Wed Jan 18 2012 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.33
+
+* Wed Jun 22 2011 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.32
+
+* Wed Feb 16 2011 - dgilbert at interlog dot com
+- add sg_decode_sense; track t10 changes
+  * sg3_utils-1.31
+
+* Fri Nov 05 2010 - dgilbert at interlog dot com
+- add sg_referrals; track t10 changes
+  * sg3_utils-1.30
+
+* Wed Mar 31 2010 - dgilbert at interlog dot com
+- track t10 changes
+  * sg3_utils-1.29
+
+* Fri Oct 02 2009 - dgilbert at interlog dot com
+- add sg_get_lba_status, sg_unmap, sg_read_block_limits
+  * sg3_utils-1.28
+
+* Sat Apr 11 2009 - dgilbert at interlog dot com
+- add sg_write_same; sg_dd split; spc4r18 sync
+  * sg3_utils-1.27
+
+* Wed Jun 25 2008 - dgilbert at interlog dot com
+- add sg_sat_phy_event, sync with drafts prior to this date
+  * sg3_utils-1.26
+
+* Tue Oct 16 2007 - dgilbert at interlog dot com
+- add sg_sat_set_features, sg_stpg, sg_safte; sg_dd oflag=sparse,null
+  * sg3_utils-1.25
+
+* Mon May 07 2007 - dgilbert at interlog dot com
+- add sg_raw; sg_rtpg, sg_log, sg_inq and sg_format updates
+  * sg3_utils-1.24
+
+* Wed Jan 31 2007 - dgilbert at interlog dot com
+- add sg_read_buffer + sg_write_buffer
+  * sg3_utils-1.23
+
+* Mon Oct 16 2006 - dgilbert at interlog dot com
+- add sg_sat_identify, expand sg_format and sg_requests
+  * sg3_utils-1.22
+
+* Thu Jul 06 2006 - dgilbert at interlog dot com
+- add sg_vpd and sg_rdac, uniform exit statuses
+  * sg3_utils-1.21
+
+* Tue Apr 18 2006 - dgilbert at interlog dot com
+- sg_logs: sas port specific page decoding, sg*_dd updates
+  * sg3_utils-1.20
+
+* Fri Jan 27 2006 - dgilbert at interlog dot com
+- sg_get_config: resync features with mmc5 rev 1
+  * sg3_utils-1.19
+
+* Fri Nov 18 2005 - dgilbert at interlog dot com
+- add sg_map26; sg_inq '-rr' option to play with hdparm
+  * sg3_utils-1.18
+
+* Thu Sep 22 2005 - dgilbert at interlog dot com
+- add ATA information VPD page to sg_inq
+  * sg3_utils-1.17
+
+* Wed Aug 10 2005 - dgilbert at interlog dot com
+- add sg_ident, sg_inq VPD page extensions
+  * sg3_utils-1.16
+
+* Sun Jun 05 2005 - dgilbert at interlog dot com
+- use O_NONBLOCK on all fds that use SG_IO ioctl
+  * sg3_utils-1.15
+
+* Fri May 06 2005 - dgilbert at interlog dot com
+- produce libsgutils (+ -devel variant) as well as sg3_utils binary rpm
+  * sg3_utils-1.14
+
+* Sun Mar 13 2005 - dgilbert at interlog dot com
+- add sg_format, sg_dd extensions
+  * sg3_utils-1.13
+
+* Fri Jan 21 2005 - dgilbert at interlog dot com
+- add sg_wr_mode, sg_rtpg + sg_reassign; sginfo sas tweaks
+  * sg3_utils-1.12
+
+* Fri Nov 26 2004 - dgilbert at interlog dot com
+- add sg_sync, sg_prevent and sg_get_config; fix sg_requests
+  * sg3_utils-1.11
+
+* Sat Oct 30 2004 - dgilbert at interlog dot com
+- fix read capacity (10+16), add sg_luns
+  * sg3_utils-1.10
+
+* Thu Oct 21 2004 - dgilbert at interlog dot com
+- sg_requests, sg_ses, sg_verify, libsgutils(sg_lib.c+sg_cmds.c), devel rpm
+  * sg3_utils-1.09
+
+* Tue Aug 31 2004 - dgilbert at interlog dot com
+- 'register+move' in sg_persist, sg_opcodes sorts, sg_write_long
+  * sg3_utils-1.08
+
+* Thu Jul 08 2004 - dgilbert at interlog dot com
+- add '-fHead' to sginfo, '-i' for sg_inq, new sg_opcodes + sg_persist
+  * sg3_utils-1.07
+
+* Mon Apr 26 2004 - dgilbert at interlog dot com
+- sg3_utils.spec for mandrake; more sginfo work, sg_scan, sg_logs
+  * sg3_utils-1.06
+
+* Wed Nov 12 2003 - dgilbert at interlog dot com
+- sg_readcap: sizes; sg_logs: double fetch; sg_map 256 sg devices; sginfo
+  * sg3_utils-1.05
+
+* Tue May 13 2003 - dgilbert at interlog dot com
+- default sg_turs '-n=' to 1, sg_logs gets '-t' for temperature, CREDITS
+  * sg3_utils-1.04
+
+* Wed Apr 02 2003 - dgilbert at interlog dot com
+- 6 byte CDBs for sg_modes, sg_start on block devs, sg_senddiag, man pages
+  * sg3_utils-1.03
+
+* Wed Jan 01 2003 - dgilbert at interlog dot com
+- interwork with block SG_IO, fix in sginfo, '-t' for sg_turs
+  * sg3_utils-1.02
+
+* Wed Aug 14 2002 - dgilbert at interlog dot com
+- raw switch in sg_inq
+  * sg3_utils-1.01
+
+* Sun Jul 28 2002 - dgilbert at interlog dot com
+- decode sg_logs pages, add dio to sgm_dd, drop "gen=1" arg, "of=/dev/null"
+  * sg3_utils-1.00
diff --git a/src/BSD_LICENSE b/src/BSD_LICENSE
new file mode 100644
index 0000000..975506a
--- /dev/null
+++ b/src/BSD_LICENSE
@@ -0,0 +1,26 @@
+
+Copyright (c) 1999-2022, Douglas Gilbert
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Above is the:
+SPDX-License-Identifier: BSD-2-Clause
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..c852833
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,219 @@
+
+bin_PROGRAMS = \
+	sg_bg_ctl sg_compare_and_write sg_decode_sense sg_format \
+	sg_get_config sg_get_elem_status sg_get_lba_status sg_ident sg_inq \
+	sg_logs sg_luns sg_modes sg_opcodes sg_persist sg_prevent sg_raw \
+	sg_rdac sg_read_attr sg_read_block_limits sg_read_buffer \
+	sg_read_long sg_readcap sg_reassign sg_referrals sg_rem_rest_elem \
+	sg_rep_density sg_rep_pip sg_rep_zones sg_requests sg_reset_wp \
+	sg_rmsn sg_rtpg sg_safte sg_sanitize sg_sat_identify sg_sat_phy_event \
+	sg_sat_read_gplog sg_sat_set_features sg_seek sg_senddiag sg_ses \
+	sg_ses_microcode sg_start sg_stpg sg_stream_ctl sg_sync sg_timestamp \
+	sg_turs sg_unmap sg_verify sg_vpd sg_wr_mode sg_write_buffer \
+	sg_write_long sg_write_same sg_write_verify sg_write_x sg_zone \
+	sg_z_act_query
+sg_scan_SOURCES =
+
+
+if OS_LINUX
+if !PT_DUMMY
+bin_PROGRAMS += \
+	sg_copy_results sg_dd sg_emc_trespass sg_map sg_map26 sg_rbuf \
+	sg_read sg_reset sg_scan sg_test_rwbuf sg_xcopy sginfo sgm_dd sgp_dd
+sg_scan_SOURCES += sg_scan_linux.c
+endif
+endif
+
+
+if OS_WIN32_MINGW
+bin_PROGRAMS += sg_scan
+sg_scan_SOURCES += sg_scan_win32.c
+endif
+
+
+if OS_WIN32_CYGWIN
+bin_PROGRAMS += sg_scan
+sg_scan_SOURCES += sg_scan_win32.c
+endif
+
+# This is active if --enable-debug given to ./configure
+# removed -Wduplicated-branches because needs gcc-8
+if DEBUG
+DBG_CFLAGS = -Wextra -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -Wshadow -Wjump-misses-init
+DBG_CPPFLAGS = -DDEBUG
+else
+DBG_CFLAGS =
+DBG_CPPFLAGS =
+endif
+
+# For C++/clang testing
+## CC = gcc-9
+## CC = g++
+## CC = clang
+## CXX = clang++
+## CC = clang++
+## CC = powerpc64-linux-gnu-gcc
+
+# -std=<s> can be c99, c11, gnu11, etc. Default is gnu11
+# -Wall is no longer all warnings. Add -W (since renamed to -Wextra) for more
+AM_CPPFLAGS = -iquote ${top_srcdir}/include -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 $(DBG_CPPFLAGS)
+AM_CFLAGS = -Wall -W $(DBG_CFLAGS)
+# AM_CFLAGS = -Wall -W $(DBG_CFLAGS) -fanalyzer
+# AM_CFLAGS = -Wall -W -Wextra -Wmisleading-indentation -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wnull-dereference -Wshadow -Wjump-misses-init
+# AM_CFLAGS = -Wall -W -pedantic -std=c11
+# AM_CFLAGS = -Wall -W -pedantic -std=c11 --analyze
+# AM_CFLAGS = -Wall -W -pedantic -std=c++98
+# AM_CFLAGS = -Wall -W -pedantic -std=c++11
+# AM_CFLAGS = -Wall -W -pedantic -std=c++14
+# AM_CFLAGS = -Wall -W -pedantic -std=c++1z
+# AM_CFLAGS = -Wall -W -pedantic -std=c++20
+# AM_CFLAGS = -Wall -W -pedantic -std=c++23
+
+sg_bg_ctl_LDADD = ../lib/libsgutils2.la
+
+sg_compare_and_write_LDADD = ../lib/libsgutils2.la
+
+sg_copy_results_LDADD = ../lib/libsgutils2.la
+
+sg_dd_LDADD = ../lib/libsgutils2.la
+
+sg_decode_sense_LDADD = ../lib/libsgutils2.la
+
+sg_emc_trespass_LDADD = ../lib/libsgutils2.la
+
+sg_format_LDADD = ../lib/libsgutils2.la
+
+sg_get_config_LDADD = ../lib/libsgutils2.la
+
+sg_get_elem_status_LDADD = ../lib/libsgutils2.la
+
+sg_get_lba_status_LDADD = ../lib/libsgutils2.la
+
+sg_ident_LDADD = ../lib/libsgutils2.la
+
+sginfo_LDADD = ../lib/libsgutils2.la
+
+sg_inq_SOURCES = sg_inq.c sg_inq_data.c sg_vpd_common.c
+sg_inq_LDADD = ../lib/libsgutils2.la
+
+sg_logs_LDADD = ../lib/libsgutils2.la
+
+sg_luns_LDADD = ../lib/libsgutils2.la
+
+sg_map_LDADD = ../lib/libsgutils2.la
+
+sgm_dd_LDADD = ../lib/libsgutils2.la
+
+sg_modes_LDADD = ../lib/libsgutils2.la
+
+sg_opcodes_LDADD = ../lib/libsgutils2.la
+
+sgp_dd_LDADD = ../lib/libsgutils2.la @PTHREAD_LIB@
+
+sg_persist_LDADD = ../lib/libsgutils2.la
+
+sg_prevent_LDADD = ../lib/libsgutils2.la
+
+sg_raw_LDADD = ../lib/libsgutils2.la
+
+sg_rbuf_LDADD = ../lib/libsgutils2.la
+
+sg_rdac_LDADD = ../lib/libsgutils2.la
+
+sg_read_LDADD = ../lib/libsgutils2.la
+
+sg_read_attr_LDADD = ../lib/libsgutils2.la
+
+sg_readcap_LDADD = ../lib/libsgutils2.la
+
+sg_read_block_limits_LDADD = ../lib/libsgutils2.la
+
+sg_read_buffer_LDADD = ../lib/libsgutils2.la
+
+sg_read_long_LDADD = ../lib/libsgutils2.la
+
+sg_reassign_LDADD = ../lib/libsgutils2.la
+
+sg_referrals_LDADD = ../lib/libsgutils2.la
+
+sg_rem_rest_elem_LDADD = ../lib/libsgutils2.la
+
+sg_rep_density_LDADD = ../lib/libsgutils2.la
+
+sg_rep_pip_LDADD = ../lib/libsgutils2.la
+
+sg_rep_zones_LDADD = ../lib/libsgutils2.la
+
+sg_requests_LDADD = ../lib/libsgutils2.la
+
+sg_reset_wp_LDADD = ../lib/libsgutils2.la
+
+sg_rmsn_LDADD = ../lib/libsgutils2.la
+
+sg_rtpg_LDADD = ../lib/libsgutils2.la
+
+sg_safte_LDADD = ../lib/libsgutils2.la
+
+sg_sanitize_LDADD = ../lib/libsgutils2.la
+
+sg_sat_identify_LDADD = ../lib/libsgutils2.la
+
+sg_sat_phy_event_LDADD = ../lib/libsgutils2.la
+
+sg_sat_read_gplog_LDADD = ../lib/libsgutils2.la
+
+sg_sat_set_features_LDADD = ../lib/libsgutils2.la
+
+# sg_scan_SOURCES list is already set above in the platform-specific sections
+sg_scan_LDADD = ../lib/libsgutils2.la
+
+sg_seek_LDADD = ../lib/libsgutils2.la @RT_LIB@
+
+sg_senddiag_LDADD = ../lib/libsgutils2.la
+
+sg_ses_LDADD = ../lib/libsgutils2.la
+
+sg_ses_microcode_LDADD = ../lib/libsgutils2.la
+
+sg_start_LDADD = ../lib/libsgutils2.la
+
+sg_stpg_LDADD = ../lib/libsgutils2.la
+
+sg_stream_ctl_LDADD = ../lib/libsgutils2.la
+
+sg_sync_LDADD = ../lib/libsgutils2.la
+
+sg_test_rwbuf_LDADD = ../lib/libsgutils2.la
+
+sg_timestamp_LDADD = ../lib/libsgutils2.la
+
+sg_turs_LDADD = ../lib/libsgutils2.la @RT_LIB@
+
+sg_unmap_LDADD = ../lib/libsgutils2.la
+
+sg_verify_LDADD = ../lib/libsgutils2.la
+
+sg_vpd_SOURCES = sg_vpd.c sg_vpd_vendor.c sg_vpd_common.c
+sg_vpd_LDADD = ../lib/libsgutils2.la
+
+sg_wr_mode_LDADD = ../lib/libsgutils2.la
+
+sg_write_buffer_LDADD = ../lib/libsgutils2.la
+
+sg_write_long_LDADD = ../lib/libsgutils2.la
+
+sg_write_same_LDADD = ../lib/libsgutils2.la
+
+sg_write_verify_LDADD = ../lib/libsgutils2.la
+
+sg_write_x_LDADD = ../lib/libsgutils2.la
+
+sg_xcopy_LDADD = ../lib/libsgutils2.la
+
+sg_zone_LDADD = ../lib/libsgutils2.la
+
+sg_z_act_query_LDADD = ../lib/libsgutils2.la
+
+EXTRA_DIST = \
+	sg_vpd_common.h \
+	BSD_LICENSE
diff --git a/src/Makefile.in b/src/Makefile.in
new file mode 100644
index 0000000..4dde3e6
--- /dev/null
+++ b/src/Makefile.in
@@ -0,0 +1,1608 @@
+# Makefile.in generated by automake 1.16.5 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2021 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+  if test -z '$(MAKELEVEL)'; then \
+    false; \
+  elif test -n '$(MAKE_HOST)'; then \
+    true; \
+  elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+    true; \
+  else \
+    false; \
+  fi; \
+}
+am__make_running_with_option = \
+  case $${target_option-} in \
+      ?) ;; \
+      *) echo "am__make_running_with_option: internal error: invalid" \
+              "target option '$${target_option-}' specified" >&2; \
+         exit 1;; \
+  esac; \
+  has_opt=no; \
+  sane_makeflags=$$MAKEFLAGS; \
+  if $(am__is_gnu_make); then \
+    sane_makeflags=$$MFLAGS; \
+  else \
+    case $$MAKEFLAGS in \
+      *\\[\ \	]*) \
+        bs=\\; \
+        sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+          | sed "s/$$bs$$bs[$$bs $$bs	]*//g"`;; \
+    esac; \
+  fi; \
+  skip_next=no; \
+  strip_trailopt () \
+  { \
+    flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+  }; \
+  for flg in $$sane_makeflags; do \
+    test $$skip_next = yes && { skip_next=no; continue; }; \
+    case $$flg in \
+      *=*|--*) continue;; \
+        -*I) strip_trailopt 'I'; skip_next=yes;; \
+      -*I?*) strip_trailopt 'I';; \
+        -*O) strip_trailopt 'O'; skip_next=yes;; \
+      -*O?*) strip_trailopt 'O';; \
+        -*l) strip_trailopt 'l'; skip_next=yes;; \
+      -*l?*) strip_trailopt 'l';; \
+      -[dEDm]) skip_next=yes;; \
+      -[JT]) skip_next=yes;; \
+    esac; \
+    case $$flg in \
+      *$$target_option*) has_opt=yes; break;; \
+    esac; \
+  done; \
+  test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+bin_PROGRAMS = sg_bg_ctl$(EXEEXT) sg_compare_and_write$(EXEEXT) \
+	sg_decode_sense$(EXEEXT) sg_format$(EXEEXT) \
+	sg_get_config$(EXEEXT) sg_get_elem_status$(EXEEXT) \
+	sg_get_lba_status$(EXEEXT) sg_ident$(EXEEXT) sg_inq$(EXEEXT) \
+	sg_logs$(EXEEXT) sg_luns$(EXEEXT) sg_modes$(EXEEXT) \
+	sg_opcodes$(EXEEXT) sg_persist$(EXEEXT) sg_prevent$(EXEEXT) \
+	sg_raw$(EXEEXT) sg_rdac$(EXEEXT) sg_read_attr$(EXEEXT) \
+	sg_read_block_limits$(EXEEXT) sg_read_buffer$(EXEEXT) \
+	sg_read_long$(EXEEXT) sg_readcap$(EXEEXT) sg_reassign$(EXEEXT) \
+	sg_referrals$(EXEEXT) sg_rem_rest_elem$(EXEEXT) \
+	sg_rep_density$(EXEEXT) sg_rep_pip$(EXEEXT) \
+	sg_rep_zones$(EXEEXT) sg_requests$(EXEEXT) \
+	sg_reset_wp$(EXEEXT) sg_rmsn$(EXEEXT) sg_rtpg$(EXEEXT) \
+	sg_safte$(EXEEXT) sg_sanitize$(EXEEXT) \
+	sg_sat_identify$(EXEEXT) sg_sat_phy_event$(EXEEXT) \
+	sg_sat_read_gplog$(EXEEXT) sg_sat_set_features$(EXEEXT) \
+	sg_seek$(EXEEXT) sg_senddiag$(EXEEXT) sg_ses$(EXEEXT) \
+	sg_ses_microcode$(EXEEXT) sg_start$(EXEEXT) sg_stpg$(EXEEXT) \
+	sg_stream_ctl$(EXEEXT) sg_sync$(EXEEXT) sg_timestamp$(EXEEXT) \
+	sg_turs$(EXEEXT) sg_unmap$(EXEEXT) sg_verify$(EXEEXT) \
+	sg_vpd$(EXEEXT) sg_wr_mode$(EXEEXT) sg_write_buffer$(EXEEXT) \
+	sg_write_long$(EXEEXT) sg_write_same$(EXEEXT) \
+	sg_write_verify$(EXEEXT) sg_write_x$(EXEEXT) sg_zone$(EXEEXT) \
+	sg_z_act_query$(EXEEXT) $(am__EXEEXT_1) $(am__EXEEXT_2) \
+	$(am__EXEEXT_3)
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@am__append_1 = \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_copy_results sg_dd sg_emc_trespass sg_map sg_map26 sg_rbuf \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_read sg_reset sg_scan sg_test_rwbuf sg_xcopy sginfo sgm_dd sgp_dd
+
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@am__append_2 = sg_scan_linux.c
+@OS_WIN32_MINGW_TRUE@am__append_3 = sg_scan
+@OS_WIN32_MINGW_TRUE@am__append_4 = sg_scan_win32.c
+@OS_WIN32_CYGWIN_TRUE@am__append_5 = sg_scan
+@OS_WIN32_CYGWIN_TRUE@am__append_6 = sg_scan_win32.c
+subdir = src
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+	$(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@am__EXEEXT_1 =  \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_copy_results$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_dd$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_emc_trespass$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_map$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_map26$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_rbuf$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_read$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_reset$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_scan$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_test_rwbuf$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_xcopy$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sginfo$(EXEEXT) sgm_dd$(EXEEXT) \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sgp_dd$(EXEEXT)
+@OS_WIN32_MINGW_TRUE@am__EXEEXT_2 = sg_scan$(EXEEXT)
+@OS_WIN32_CYGWIN_TRUE@am__EXEEXT_3 = sg_scan$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(bindir)"
+PROGRAMS = $(bin_PROGRAMS)
+sg_bg_ctl_SOURCES = sg_bg_ctl.c
+sg_bg_ctl_OBJECTS = sg_bg_ctl.$(OBJEXT)
+sg_bg_ctl_DEPENDENCIES = ../lib/libsgutils2.la
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 = 
+sg_compare_and_write_SOURCES = sg_compare_and_write.c
+sg_compare_and_write_OBJECTS = sg_compare_and_write.$(OBJEXT)
+sg_compare_and_write_DEPENDENCIES = ../lib/libsgutils2.la
+sg_copy_results_SOURCES = sg_copy_results.c
+sg_copy_results_OBJECTS = sg_copy_results.$(OBJEXT)
+sg_copy_results_DEPENDENCIES = ../lib/libsgutils2.la
+sg_dd_SOURCES = sg_dd.c
+sg_dd_OBJECTS = sg_dd.$(OBJEXT)
+sg_dd_DEPENDENCIES = ../lib/libsgutils2.la
+sg_decode_sense_SOURCES = sg_decode_sense.c
+sg_decode_sense_OBJECTS = sg_decode_sense.$(OBJEXT)
+sg_decode_sense_DEPENDENCIES = ../lib/libsgutils2.la
+sg_emc_trespass_SOURCES = sg_emc_trespass.c
+sg_emc_trespass_OBJECTS = sg_emc_trespass.$(OBJEXT)
+sg_emc_trespass_DEPENDENCIES = ../lib/libsgutils2.la
+sg_format_SOURCES = sg_format.c
+sg_format_OBJECTS = sg_format.$(OBJEXT)
+sg_format_DEPENDENCIES = ../lib/libsgutils2.la
+sg_get_config_SOURCES = sg_get_config.c
+sg_get_config_OBJECTS = sg_get_config.$(OBJEXT)
+sg_get_config_DEPENDENCIES = ../lib/libsgutils2.la
+sg_get_elem_status_SOURCES = sg_get_elem_status.c
+sg_get_elem_status_OBJECTS = sg_get_elem_status.$(OBJEXT)
+sg_get_elem_status_DEPENDENCIES = ../lib/libsgutils2.la
+sg_get_lba_status_SOURCES = sg_get_lba_status.c
+sg_get_lba_status_OBJECTS = sg_get_lba_status.$(OBJEXT)
+sg_get_lba_status_DEPENDENCIES = ../lib/libsgutils2.la
+sg_ident_SOURCES = sg_ident.c
+sg_ident_OBJECTS = sg_ident.$(OBJEXT)
+sg_ident_DEPENDENCIES = ../lib/libsgutils2.la
+am_sg_inq_OBJECTS = sg_inq.$(OBJEXT) sg_inq_data.$(OBJEXT) \
+	sg_vpd_common.$(OBJEXT)
+sg_inq_OBJECTS = $(am_sg_inq_OBJECTS)
+sg_inq_DEPENDENCIES = ../lib/libsgutils2.la
+sg_logs_SOURCES = sg_logs.c
+sg_logs_OBJECTS = sg_logs.$(OBJEXT)
+sg_logs_DEPENDENCIES = ../lib/libsgutils2.la
+sg_luns_SOURCES = sg_luns.c
+sg_luns_OBJECTS = sg_luns.$(OBJEXT)
+sg_luns_DEPENDENCIES = ../lib/libsgutils2.la
+sg_map_SOURCES = sg_map.c
+sg_map_OBJECTS = sg_map.$(OBJEXT)
+sg_map_DEPENDENCIES = ../lib/libsgutils2.la
+sg_map26_SOURCES = sg_map26.c
+sg_map26_OBJECTS = sg_map26.$(OBJEXT)
+sg_map26_LDADD = $(LDADD)
+sg_modes_SOURCES = sg_modes.c
+sg_modes_OBJECTS = sg_modes.$(OBJEXT)
+sg_modes_DEPENDENCIES = ../lib/libsgutils2.la
+sg_opcodes_SOURCES = sg_opcodes.c
+sg_opcodes_OBJECTS = sg_opcodes.$(OBJEXT)
+sg_opcodes_DEPENDENCIES = ../lib/libsgutils2.la
+sg_persist_SOURCES = sg_persist.c
+sg_persist_OBJECTS = sg_persist.$(OBJEXT)
+sg_persist_DEPENDENCIES = ../lib/libsgutils2.la
+sg_prevent_SOURCES = sg_prevent.c
+sg_prevent_OBJECTS = sg_prevent.$(OBJEXT)
+sg_prevent_DEPENDENCIES = ../lib/libsgutils2.la
+sg_raw_SOURCES = sg_raw.c
+sg_raw_OBJECTS = sg_raw.$(OBJEXT)
+sg_raw_DEPENDENCIES = ../lib/libsgutils2.la
+sg_rbuf_SOURCES = sg_rbuf.c
+sg_rbuf_OBJECTS = sg_rbuf.$(OBJEXT)
+sg_rbuf_DEPENDENCIES = ../lib/libsgutils2.la
+sg_rdac_SOURCES = sg_rdac.c
+sg_rdac_OBJECTS = sg_rdac.$(OBJEXT)
+sg_rdac_DEPENDENCIES = ../lib/libsgutils2.la
+sg_read_SOURCES = sg_read.c
+sg_read_OBJECTS = sg_read.$(OBJEXT)
+sg_read_DEPENDENCIES = ../lib/libsgutils2.la
+sg_read_attr_SOURCES = sg_read_attr.c
+sg_read_attr_OBJECTS = sg_read_attr.$(OBJEXT)
+sg_read_attr_DEPENDENCIES = ../lib/libsgutils2.la
+sg_read_block_limits_SOURCES = sg_read_block_limits.c
+sg_read_block_limits_OBJECTS = sg_read_block_limits.$(OBJEXT)
+sg_read_block_limits_DEPENDENCIES = ../lib/libsgutils2.la
+sg_read_buffer_SOURCES = sg_read_buffer.c
+sg_read_buffer_OBJECTS = sg_read_buffer.$(OBJEXT)
+sg_read_buffer_DEPENDENCIES = ../lib/libsgutils2.la
+sg_read_long_SOURCES = sg_read_long.c
+sg_read_long_OBJECTS = sg_read_long.$(OBJEXT)
+sg_read_long_DEPENDENCIES = ../lib/libsgutils2.la
+sg_readcap_SOURCES = sg_readcap.c
+sg_readcap_OBJECTS = sg_readcap.$(OBJEXT)
+sg_readcap_DEPENDENCIES = ../lib/libsgutils2.la
+sg_reassign_SOURCES = sg_reassign.c
+sg_reassign_OBJECTS = sg_reassign.$(OBJEXT)
+sg_reassign_DEPENDENCIES = ../lib/libsgutils2.la
+sg_referrals_SOURCES = sg_referrals.c
+sg_referrals_OBJECTS = sg_referrals.$(OBJEXT)
+sg_referrals_DEPENDENCIES = ../lib/libsgutils2.la
+sg_rem_rest_elem_SOURCES = sg_rem_rest_elem.c
+sg_rem_rest_elem_OBJECTS = sg_rem_rest_elem.$(OBJEXT)
+sg_rem_rest_elem_DEPENDENCIES = ../lib/libsgutils2.la
+sg_rep_density_SOURCES = sg_rep_density.c
+sg_rep_density_OBJECTS = sg_rep_density.$(OBJEXT)
+sg_rep_density_DEPENDENCIES = ../lib/libsgutils2.la
+sg_rep_pip_SOURCES = sg_rep_pip.c
+sg_rep_pip_OBJECTS = sg_rep_pip.$(OBJEXT)
+sg_rep_pip_DEPENDENCIES = ../lib/libsgutils2.la
+sg_rep_zones_SOURCES = sg_rep_zones.c
+sg_rep_zones_OBJECTS = sg_rep_zones.$(OBJEXT)
+sg_rep_zones_DEPENDENCIES = ../lib/libsgutils2.la
+sg_requests_SOURCES = sg_requests.c
+sg_requests_OBJECTS = sg_requests.$(OBJEXT)
+sg_requests_DEPENDENCIES = ../lib/libsgutils2.la
+sg_reset_SOURCES = sg_reset.c
+sg_reset_OBJECTS = sg_reset.$(OBJEXT)
+sg_reset_LDADD = $(LDADD)
+sg_reset_wp_SOURCES = sg_reset_wp.c
+sg_reset_wp_OBJECTS = sg_reset_wp.$(OBJEXT)
+sg_reset_wp_DEPENDENCIES = ../lib/libsgutils2.la
+sg_rmsn_SOURCES = sg_rmsn.c
+sg_rmsn_OBJECTS = sg_rmsn.$(OBJEXT)
+sg_rmsn_DEPENDENCIES = ../lib/libsgutils2.la
+sg_rtpg_SOURCES = sg_rtpg.c
+sg_rtpg_OBJECTS = sg_rtpg.$(OBJEXT)
+sg_rtpg_DEPENDENCIES = ../lib/libsgutils2.la
+sg_safte_SOURCES = sg_safte.c
+sg_safte_OBJECTS = sg_safte.$(OBJEXT)
+sg_safte_DEPENDENCIES = ../lib/libsgutils2.la
+sg_sanitize_SOURCES = sg_sanitize.c
+sg_sanitize_OBJECTS = sg_sanitize.$(OBJEXT)
+sg_sanitize_DEPENDENCIES = ../lib/libsgutils2.la
+sg_sat_identify_SOURCES = sg_sat_identify.c
+sg_sat_identify_OBJECTS = sg_sat_identify.$(OBJEXT)
+sg_sat_identify_DEPENDENCIES = ../lib/libsgutils2.la
+sg_sat_phy_event_SOURCES = sg_sat_phy_event.c
+sg_sat_phy_event_OBJECTS = sg_sat_phy_event.$(OBJEXT)
+sg_sat_phy_event_DEPENDENCIES = ../lib/libsgutils2.la
+sg_sat_read_gplog_SOURCES = sg_sat_read_gplog.c
+sg_sat_read_gplog_OBJECTS = sg_sat_read_gplog.$(OBJEXT)
+sg_sat_read_gplog_DEPENDENCIES = ../lib/libsgutils2.la
+sg_sat_set_features_SOURCES = sg_sat_set_features.c
+sg_sat_set_features_OBJECTS = sg_sat_set_features.$(OBJEXT)
+sg_sat_set_features_DEPENDENCIES = ../lib/libsgutils2.la
+am__sg_scan_SOURCES_DIST = sg_scan_linux.c sg_scan_win32.c
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@am__objects_1 =  \
+@OS_LINUX_TRUE@@PT_DUMMY_FALSE@	sg_scan_linux.$(OBJEXT)
+@OS_WIN32_MINGW_TRUE@am__objects_2 = sg_scan_win32.$(OBJEXT)
+@OS_WIN32_CYGWIN_TRUE@am__objects_3 = sg_scan_win32.$(OBJEXT)
+am_sg_scan_OBJECTS = $(am__objects_1) $(am__objects_2) \
+	$(am__objects_3)
+sg_scan_OBJECTS = $(am_sg_scan_OBJECTS)
+sg_scan_DEPENDENCIES = ../lib/libsgutils2.la
+sg_seek_SOURCES = sg_seek.c
+sg_seek_OBJECTS = sg_seek.$(OBJEXT)
+sg_seek_DEPENDENCIES = ../lib/libsgutils2.la
+sg_senddiag_SOURCES = sg_senddiag.c
+sg_senddiag_OBJECTS = sg_senddiag.$(OBJEXT)
+sg_senddiag_DEPENDENCIES = ../lib/libsgutils2.la
+sg_ses_SOURCES = sg_ses.c
+sg_ses_OBJECTS = sg_ses.$(OBJEXT)
+sg_ses_DEPENDENCIES = ../lib/libsgutils2.la
+sg_ses_microcode_SOURCES = sg_ses_microcode.c
+sg_ses_microcode_OBJECTS = sg_ses_microcode.$(OBJEXT)
+sg_ses_microcode_DEPENDENCIES = ../lib/libsgutils2.la
+sg_start_SOURCES = sg_start.c
+sg_start_OBJECTS = sg_start.$(OBJEXT)
+sg_start_DEPENDENCIES = ../lib/libsgutils2.la
+sg_stpg_SOURCES = sg_stpg.c
+sg_stpg_OBJECTS = sg_stpg.$(OBJEXT)
+sg_stpg_DEPENDENCIES = ../lib/libsgutils2.la
+sg_stream_ctl_SOURCES = sg_stream_ctl.c
+sg_stream_ctl_OBJECTS = sg_stream_ctl.$(OBJEXT)
+sg_stream_ctl_DEPENDENCIES = ../lib/libsgutils2.la
+sg_sync_SOURCES = sg_sync.c
+sg_sync_OBJECTS = sg_sync.$(OBJEXT)
+sg_sync_DEPENDENCIES = ../lib/libsgutils2.la
+sg_test_rwbuf_SOURCES = sg_test_rwbuf.c
+sg_test_rwbuf_OBJECTS = sg_test_rwbuf.$(OBJEXT)
+sg_test_rwbuf_DEPENDENCIES = ../lib/libsgutils2.la
+sg_timestamp_SOURCES = sg_timestamp.c
+sg_timestamp_OBJECTS = sg_timestamp.$(OBJEXT)
+sg_timestamp_DEPENDENCIES = ../lib/libsgutils2.la
+sg_turs_SOURCES = sg_turs.c
+sg_turs_OBJECTS = sg_turs.$(OBJEXT)
+sg_turs_DEPENDENCIES = ../lib/libsgutils2.la
+sg_unmap_SOURCES = sg_unmap.c
+sg_unmap_OBJECTS = sg_unmap.$(OBJEXT)
+sg_unmap_DEPENDENCIES = ../lib/libsgutils2.la
+sg_verify_SOURCES = sg_verify.c
+sg_verify_OBJECTS = sg_verify.$(OBJEXT)
+sg_verify_DEPENDENCIES = ../lib/libsgutils2.la
+am_sg_vpd_OBJECTS = sg_vpd.$(OBJEXT) sg_vpd_vendor.$(OBJEXT) \
+	sg_vpd_common.$(OBJEXT)
+sg_vpd_OBJECTS = $(am_sg_vpd_OBJECTS)
+sg_vpd_DEPENDENCIES = ../lib/libsgutils2.la
+sg_wr_mode_SOURCES = sg_wr_mode.c
+sg_wr_mode_OBJECTS = sg_wr_mode.$(OBJEXT)
+sg_wr_mode_DEPENDENCIES = ../lib/libsgutils2.la
+sg_write_buffer_SOURCES = sg_write_buffer.c
+sg_write_buffer_OBJECTS = sg_write_buffer.$(OBJEXT)
+sg_write_buffer_DEPENDENCIES = ../lib/libsgutils2.la
+sg_write_long_SOURCES = sg_write_long.c
+sg_write_long_OBJECTS = sg_write_long.$(OBJEXT)
+sg_write_long_DEPENDENCIES = ../lib/libsgutils2.la
+sg_write_same_SOURCES = sg_write_same.c
+sg_write_same_OBJECTS = sg_write_same.$(OBJEXT)
+sg_write_same_DEPENDENCIES = ../lib/libsgutils2.la
+sg_write_verify_SOURCES = sg_write_verify.c
+sg_write_verify_OBJECTS = sg_write_verify.$(OBJEXT)
+sg_write_verify_DEPENDENCIES = ../lib/libsgutils2.la
+sg_write_x_SOURCES = sg_write_x.c
+sg_write_x_OBJECTS = sg_write_x.$(OBJEXT)
+sg_write_x_DEPENDENCIES = ../lib/libsgutils2.la
+sg_xcopy_SOURCES = sg_xcopy.c
+sg_xcopy_OBJECTS = sg_xcopy.$(OBJEXT)
+sg_xcopy_DEPENDENCIES = ../lib/libsgutils2.la
+sg_z_act_query_SOURCES = sg_z_act_query.c
+sg_z_act_query_OBJECTS = sg_z_act_query.$(OBJEXT)
+sg_z_act_query_DEPENDENCIES = ../lib/libsgutils2.la
+sg_zone_SOURCES = sg_zone.c
+sg_zone_OBJECTS = sg_zone.$(OBJEXT)
+sg_zone_DEPENDENCIES = ../lib/libsgutils2.la
+sginfo_SOURCES = sginfo.c
+sginfo_OBJECTS = sginfo.$(OBJEXT)
+sginfo_DEPENDENCIES = ../lib/libsgutils2.la
+sgm_dd_SOURCES = sgm_dd.c
+sgm_dd_OBJECTS = sgm_dd.$(OBJEXT)
+sgm_dd_DEPENDENCIES = ../lib/libsgutils2.la
+sgp_dd_SOURCES = sgp_dd.c
+sgp_dd_OBJECTS = sgp_dd.$(OBJEXT)
+sgp_dd_DEPENDENCIES = ../lib/libsgutils2.la
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo "  GEN     " $@;
+am__v_GEN_1 = 
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 = 
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/sg_bg_ctl.Po \
+	./$(DEPDIR)/sg_compare_and_write.Po \
+	./$(DEPDIR)/sg_copy_results.Po ./$(DEPDIR)/sg_dd.Po \
+	./$(DEPDIR)/sg_decode_sense.Po ./$(DEPDIR)/sg_emc_trespass.Po \
+	./$(DEPDIR)/sg_format.Po ./$(DEPDIR)/sg_get_config.Po \
+	./$(DEPDIR)/sg_get_elem_status.Po \
+	./$(DEPDIR)/sg_get_lba_status.Po ./$(DEPDIR)/sg_ident.Po \
+	./$(DEPDIR)/sg_inq.Po ./$(DEPDIR)/sg_inq_data.Po \
+	./$(DEPDIR)/sg_logs.Po ./$(DEPDIR)/sg_luns.Po \
+	./$(DEPDIR)/sg_map.Po ./$(DEPDIR)/sg_map26.Po \
+	./$(DEPDIR)/sg_modes.Po ./$(DEPDIR)/sg_opcodes.Po \
+	./$(DEPDIR)/sg_persist.Po ./$(DEPDIR)/sg_prevent.Po \
+	./$(DEPDIR)/sg_raw.Po ./$(DEPDIR)/sg_rbuf.Po \
+	./$(DEPDIR)/sg_rdac.Po ./$(DEPDIR)/sg_read.Po \
+	./$(DEPDIR)/sg_read_attr.Po \
+	./$(DEPDIR)/sg_read_block_limits.Po \
+	./$(DEPDIR)/sg_read_buffer.Po ./$(DEPDIR)/sg_read_long.Po \
+	./$(DEPDIR)/sg_readcap.Po ./$(DEPDIR)/sg_reassign.Po \
+	./$(DEPDIR)/sg_referrals.Po ./$(DEPDIR)/sg_rem_rest_elem.Po \
+	./$(DEPDIR)/sg_rep_density.Po ./$(DEPDIR)/sg_rep_pip.Po \
+	./$(DEPDIR)/sg_rep_zones.Po ./$(DEPDIR)/sg_requests.Po \
+	./$(DEPDIR)/sg_reset.Po ./$(DEPDIR)/sg_reset_wp.Po \
+	./$(DEPDIR)/sg_rmsn.Po ./$(DEPDIR)/sg_rtpg.Po \
+	./$(DEPDIR)/sg_safte.Po ./$(DEPDIR)/sg_sanitize.Po \
+	./$(DEPDIR)/sg_sat_identify.Po ./$(DEPDIR)/sg_sat_phy_event.Po \
+	./$(DEPDIR)/sg_sat_read_gplog.Po \
+	./$(DEPDIR)/sg_sat_set_features.Po \
+	./$(DEPDIR)/sg_scan_linux.Po ./$(DEPDIR)/sg_scan_win32.Po \
+	./$(DEPDIR)/sg_seek.Po ./$(DEPDIR)/sg_senddiag.Po \
+	./$(DEPDIR)/sg_ses.Po ./$(DEPDIR)/sg_ses_microcode.Po \
+	./$(DEPDIR)/sg_start.Po ./$(DEPDIR)/sg_stpg.Po \
+	./$(DEPDIR)/sg_stream_ctl.Po ./$(DEPDIR)/sg_sync.Po \
+	./$(DEPDIR)/sg_test_rwbuf.Po ./$(DEPDIR)/sg_timestamp.Po \
+	./$(DEPDIR)/sg_turs.Po ./$(DEPDIR)/sg_unmap.Po \
+	./$(DEPDIR)/sg_verify.Po ./$(DEPDIR)/sg_vpd.Po \
+	./$(DEPDIR)/sg_vpd_common.Po ./$(DEPDIR)/sg_vpd_vendor.Po \
+	./$(DEPDIR)/sg_wr_mode.Po ./$(DEPDIR)/sg_write_buffer.Po \
+	./$(DEPDIR)/sg_write_long.Po ./$(DEPDIR)/sg_write_same.Po \
+	./$(DEPDIR)/sg_write_verify.Po ./$(DEPDIR)/sg_write_x.Po \
+	./$(DEPDIR)/sg_xcopy.Po ./$(DEPDIR)/sg_z_act_query.Po \
+	./$(DEPDIR)/sg_zone.Po ./$(DEPDIR)/sginfo.Po \
+	./$(DEPDIR)/sgm_dd.Po ./$(DEPDIR)/sgp_dd.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+	$(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+	$(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+	$(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+	$(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo "  CC      " $@;
+am__v_CC_1 = 
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+	$(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+	$(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo "  CCLD    " $@;
+am__v_CCLD_1 = 
+SOURCES = sg_bg_ctl.c sg_compare_and_write.c sg_copy_results.c sg_dd.c \
+	sg_decode_sense.c sg_emc_trespass.c sg_format.c \
+	sg_get_config.c sg_get_elem_status.c sg_get_lba_status.c \
+	sg_ident.c $(sg_inq_SOURCES) sg_logs.c sg_luns.c sg_map.c \
+	sg_map26.c sg_modes.c sg_opcodes.c sg_persist.c sg_prevent.c \
+	sg_raw.c sg_rbuf.c sg_rdac.c sg_read.c sg_read_attr.c \
+	sg_read_block_limits.c sg_read_buffer.c sg_read_long.c \
+	sg_readcap.c sg_reassign.c sg_referrals.c sg_rem_rest_elem.c \
+	sg_rep_density.c sg_rep_pip.c sg_rep_zones.c sg_requests.c \
+	sg_reset.c sg_reset_wp.c sg_rmsn.c sg_rtpg.c sg_safte.c \
+	sg_sanitize.c sg_sat_identify.c sg_sat_phy_event.c \
+	sg_sat_read_gplog.c sg_sat_set_features.c $(sg_scan_SOURCES) \
+	sg_seek.c sg_senddiag.c sg_ses.c sg_ses_microcode.c sg_start.c \
+	sg_stpg.c sg_stream_ctl.c sg_sync.c sg_test_rwbuf.c \
+	sg_timestamp.c sg_turs.c sg_unmap.c sg_verify.c \
+	$(sg_vpd_SOURCES) sg_wr_mode.c sg_write_buffer.c \
+	sg_write_long.c sg_write_same.c sg_write_verify.c sg_write_x.c \
+	sg_xcopy.c sg_z_act_query.c sg_zone.c sginfo.c sgm_dd.c \
+	sgp_dd.c
+DIST_SOURCES = sg_bg_ctl.c sg_compare_and_write.c sg_copy_results.c \
+	sg_dd.c sg_decode_sense.c sg_emc_trespass.c sg_format.c \
+	sg_get_config.c sg_get_elem_status.c sg_get_lba_status.c \
+	sg_ident.c $(sg_inq_SOURCES) sg_logs.c sg_luns.c sg_map.c \
+	sg_map26.c sg_modes.c sg_opcodes.c sg_persist.c sg_prevent.c \
+	sg_raw.c sg_rbuf.c sg_rdac.c sg_read.c sg_read_attr.c \
+	sg_read_block_limits.c sg_read_buffer.c sg_read_long.c \
+	sg_readcap.c sg_reassign.c sg_referrals.c sg_rem_rest_elem.c \
+	sg_rep_density.c sg_rep_pip.c sg_rep_zones.c sg_requests.c \
+	sg_reset.c sg_reset_wp.c sg_rmsn.c sg_rtpg.c sg_safte.c \
+	sg_sanitize.c sg_sat_identify.c sg_sat_phy_event.c \
+	sg_sat_read_gplog.c sg_sat_set_features.c \
+	$(am__sg_scan_SOURCES_DIST) sg_seek.c sg_senddiag.c sg_ses.c \
+	sg_ses_microcode.c sg_start.c sg_stpg.c sg_stream_ctl.c \
+	sg_sync.c sg_test_rwbuf.c sg_timestamp.c sg_turs.c sg_unmap.c \
+	sg_verify.c $(sg_vpd_SOURCES) sg_wr_mode.c sg_write_buffer.c \
+	sg_write_long.c sg_write_same.c sg_write_verify.c sg_write_x.c \
+	sg_xcopy.c sg_z_act_query.c sg_zone.c sginfo.c sgm_dd.c \
+	sgp_dd.c
+am__can_run_installinfo = \
+  case $$AM_UPDATE_INFO_DIR in \
+    n|no|NO) false;; \
+    *) (install-info --version) >/dev/null 2>&1;; \
+  esac
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates.  Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+  BEGIN { nonempty = 0; } \
+  { items[$$0] = 1; nonempty = 1; } \
+  END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique.  This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+  list='$(am__tagged_files)'; \
+  unique=`for i in $$list; do \
+    if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+  done | $(am__uniquify_input)`
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CSCOPE = @CSCOPE@
+CTAGS = @CTAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+ETAGS = @ETAGS@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FILECMD = @FILECMD@
+GETOPT_O_FILES = @GETOPT_O_FILES@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LD = @LD@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBTOOL = @LIBTOOL@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+NM = @NM@
+NMEDIT = @NMEDIT@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PTHREAD_LIB = @PTHREAD_LIB@
+RANLIB = @RANLIB@
+RT_LIB = @RT_LIB@
+SED = @SED@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+STRIP = @STRIP@
+VERSION = @VERSION@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+os_cflags = @os_cflags@
+os_libs = @os_libs@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+sg_scan_SOURCES = $(am__append_2) $(am__append_4) $(am__append_6)
+@DEBUG_FALSE@DBG_CFLAGS = 
+
+# This is active if --enable-debug given to ./configure
+# removed -Wduplicated-branches because needs gcc-8
+@DEBUG_TRUE@DBG_CFLAGS = -Wextra -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -Wshadow -Wjump-misses-init
+@DEBUG_FALSE@DBG_CPPFLAGS = 
+@DEBUG_TRUE@DBG_CPPFLAGS = -DDEBUG
+
+# For C++/clang testing
+
+# -std=<s> can be c99, c11, gnu11, etc. Default is gnu11
+# -Wall is no longer all warnings. Add -W (since renamed to -Wextra) for more
+AM_CPPFLAGS = -iquote ${top_srcdir}/include -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 $(DBG_CPPFLAGS)
+AM_CFLAGS = -Wall -W $(DBG_CFLAGS)
+# AM_CFLAGS = -Wall -W $(DBG_CFLAGS) -fanalyzer
+# AM_CFLAGS = -Wall -W -Wextra -Wmisleading-indentation -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wnull-dereference -Wshadow -Wjump-misses-init
+# AM_CFLAGS = -Wall -W -pedantic -std=c11
+# AM_CFLAGS = -Wall -W -pedantic -std=c11 --analyze
+# AM_CFLAGS = -Wall -W -pedantic -std=c++98
+# AM_CFLAGS = -Wall -W -pedantic -std=c++11
+# AM_CFLAGS = -Wall -W -pedantic -std=c++14
+# AM_CFLAGS = -Wall -W -pedantic -std=c++1z
+# AM_CFLAGS = -Wall -W -pedantic -std=c++20
+# AM_CFLAGS = -Wall -W -pedantic -std=c++23
+sg_bg_ctl_LDADD = ../lib/libsgutils2.la
+sg_compare_and_write_LDADD = ../lib/libsgutils2.la
+sg_copy_results_LDADD = ../lib/libsgutils2.la
+sg_dd_LDADD = ../lib/libsgutils2.la
+sg_decode_sense_LDADD = ../lib/libsgutils2.la
+sg_emc_trespass_LDADD = ../lib/libsgutils2.la
+sg_format_LDADD = ../lib/libsgutils2.la
+sg_get_config_LDADD = ../lib/libsgutils2.la
+sg_get_elem_status_LDADD = ../lib/libsgutils2.la
+sg_get_lba_status_LDADD = ../lib/libsgutils2.la
+sg_ident_LDADD = ../lib/libsgutils2.la
+sginfo_LDADD = ../lib/libsgutils2.la
+sg_inq_SOURCES = sg_inq.c sg_inq_data.c sg_vpd_common.c
+sg_inq_LDADD = ../lib/libsgutils2.la
+sg_logs_LDADD = ../lib/libsgutils2.la
+sg_luns_LDADD = ../lib/libsgutils2.la
+sg_map_LDADD = ../lib/libsgutils2.la
+sgm_dd_LDADD = ../lib/libsgutils2.la
+sg_modes_LDADD = ../lib/libsgutils2.la
+sg_opcodes_LDADD = ../lib/libsgutils2.la
+sgp_dd_LDADD = ../lib/libsgutils2.la @PTHREAD_LIB@
+sg_persist_LDADD = ../lib/libsgutils2.la
+sg_prevent_LDADD = ../lib/libsgutils2.la
+sg_raw_LDADD = ../lib/libsgutils2.la
+sg_rbuf_LDADD = ../lib/libsgutils2.la
+sg_rdac_LDADD = ../lib/libsgutils2.la
+sg_read_LDADD = ../lib/libsgutils2.la
+sg_read_attr_LDADD = ../lib/libsgutils2.la
+sg_readcap_LDADD = ../lib/libsgutils2.la
+sg_read_block_limits_LDADD = ../lib/libsgutils2.la
+sg_read_buffer_LDADD = ../lib/libsgutils2.la
+sg_read_long_LDADD = ../lib/libsgutils2.la
+sg_reassign_LDADD = ../lib/libsgutils2.la
+sg_referrals_LDADD = ../lib/libsgutils2.la
+sg_rem_rest_elem_LDADD = ../lib/libsgutils2.la
+sg_rep_density_LDADD = ../lib/libsgutils2.la
+sg_rep_pip_LDADD = ../lib/libsgutils2.la
+sg_rep_zones_LDADD = ../lib/libsgutils2.la
+sg_requests_LDADD = ../lib/libsgutils2.la
+sg_reset_wp_LDADD = ../lib/libsgutils2.la
+sg_rmsn_LDADD = ../lib/libsgutils2.la
+sg_rtpg_LDADD = ../lib/libsgutils2.la
+sg_safte_LDADD = ../lib/libsgutils2.la
+sg_sanitize_LDADD = ../lib/libsgutils2.la
+sg_sat_identify_LDADD = ../lib/libsgutils2.la
+sg_sat_phy_event_LDADD = ../lib/libsgutils2.la
+sg_sat_read_gplog_LDADD = ../lib/libsgutils2.la
+sg_sat_set_features_LDADD = ../lib/libsgutils2.la
+
+# sg_scan_SOURCES list is already set above in the platform-specific sections
+sg_scan_LDADD = ../lib/libsgutils2.la
+sg_seek_LDADD = ../lib/libsgutils2.la @RT_LIB@
+sg_senddiag_LDADD = ../lib/libsgutils2.la
+sg_ses_LDADD = ../lib/libsgutils2.la
+sg_ses_microcode_LDADD = ../lib/libsgutils2.la
+sg_start_LDADD = ../lib/libsgutils2.la
+sg_stpg_LDADD = ../lib/libsgutils2.la
+sg_stream_ctl_LDADD = ../lib/libsgutils2.la
+sg_sync_LDADD = ../lib/libsgutils2.la
+sg_test_rwbuf_LDADD = ../lib/libsgutils2.la
+sg_timestamp_LDADD = ../lib/libsgutils2.la
+sg_turs_LDADD = ../lib/libsgutils2.la @RT_LIB@
+sg_unmap_LDADD = ../lib/libsgutils2.la
+sg_verify_LDADD = ../lib/libsgutils2.la
+sg_vpd_SOURCES = sg_vpd.c sg_vpd_vendor.c sg_vpd_common.c
+sg_vpd_LDADD = ../lib/libsgutils2.la
+sg_wr_mode_LDADD = ../lib/libsgutils2.la
+sg_write_buffer_LDADD = ../lib/libsgutils2.la
+sg_write_long_LDADD = ../lib/libsgutils2.la
+sg_write_same_LDADD = ../lib/libsgutils2.la
+sg_write_verify_LDADD = ../lib/libsgutils2.la
+sg_write_x_LDADD = ../lib/libsgutils2.la
+sg_xcopy_LDADD = ../lib/libsgutils2.la
+sg_zone_LDADD = ../lib/libsgutils2.la
+sg_z_act_query_LDADD = ../lib/libsgutils2.la
+EXTRA_DIST = \
+	sg_vpd_common.h \
+	BSD_LICENSE
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am  $(am__configure_deps)
+	@for dep in $?; do \
+	  case '$(am__configure_deps)' in \
+	    *$$dep*) \
+	      ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+	        && { if test -f $@; then exit 0; else break; fi; }; \
+	      exit 1;; \
+	  esac; \
+	done; \
+	echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/Makefile'; \
+	$(am__cd) $(top_srcdir) && \
+	  $(AUTOMAKE) --foreign src/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+	@case '$?' in \
+	  *config.status*) \
+	    cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+	  *) \
+	    echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+	    cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+	esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+install-binPROGRAMS: $(bin_PROGRAMS)
+	@$(NORMAL_INSTALL)
+	@list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+	if test -n "$$list"; then \
+	  echo " $(MKDIR_P) '$(DESTDIR)$(bindir)'"; \
+	  $(MKDIR_P) "$(DESTDIR)$(bindir)" || exit 1; \
+	fi; \
+	for p in $$list; do echo "$$p $$p"; done | \
+	sed 's/$(EXEEXT)$$//' | \
+	while read p p1; do if test -f $$p \
+	 || test -f $$p1 \
+	  ; then echo "$$p"; echo "$$p"; else :; fi; \
+	done | \
+	sed -e 'p;s,.*/,,;n;h' \
+	    -e 's|.*|.|' \
+	    -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+	sed 'N;N;N;s,\n, ,g' | \
+	$(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+	  { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+	    if ($$2 == $$4) files[d] = files[d] " " $$1; \
+	    else { print "f", $$3 "/" $$4, $$1; } } \
+	  END { for (d in files) print "f", d, files[d] }' | \
+	while read type dir files; do \
+	    if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+	    test -z "$$files" || { \
+	    echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(bindir)$$dir'"; \
+	    $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \
+	    } \
+	; done
+
+uninstall-binPROGRAMS:
+	@$(NORMAL_UNINSTALL)
+	@list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+	files=`for p in $$list; do echo "$$p"; done | \
+	  sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+	      -e 's/$$/$(EXEEXT)/' \
+	`; \
+	test -n "$$list" || exit 0; \
+	echo " ( cd '$(DESTDIR)$(bindir)' && rm -f" $$files ")"; \
+	cd "$(DESTDIR)$(bindir)" && rm -f $$files
+
+clean-binPROGRAMS:
+	@list='$(bin_PROGRAMS)'; test -n "$$list" || exit 0; \
+	echo " rm -f" $$list; \
+	rm -f $$list || exit $$?; \
+	test -n "$(EXEEXT)" || exit 0; \
+	list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+	echo " rm -f" $$list; \
+	rm -f $$list
+
+sg_bg_ctl$(EXEEXT): $(sg_bg_ctl_OBJECTS) $(sg_bg_ctl_DEPENDENCIES) $(EXTRA_sg_bg_ctl_DEPENDENCIES) 
+	@rm -f sg_bg_ctl$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_bg_ctl_OBJECTS) $(sg_bg_ctl_LDADD) $(LIBS)
+
+sg_compare_and_write$(EXEEXT): $(sg_compare_and_write_OBJECTS) $(sg_compare_and_write_DEPENDENCIES) $(EXTRA_sg_compare_and_write_DEPENDENCIES) 
+	@rm -f sg_compare_and_write$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_compare_and_write_OBJECTS) $(sg_compare_and_write_LDADD) $(LIBS)
+
+sg_copy_results$(EXEEXT): $(sg_copy_results_OBJECTS) $(sg_copy_results_DEPENDENCIES) $(EXTRA_sg_copy_results_DEPENDENCIES) 
+	@rm -f sg_copy_results$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_copy_results_OBJECTS) $(sg_copy_results_LDADD) $(LIBS)
+
+sg_dd$(EXEEXT): $(sg_dd_OBJECTS) $(sg_dd_DEPENDENCIES) $(EXTRA_sg_dd_DEPENDENCIES) 
+	@rm -f sg_dd$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_dd_OBJECTS) $(sg_dd_LDADD) $(LIBS)
+
+sg_decode_sense$(EXEEXT): $(sg_decode_sense_OBJECTS) $(sg_decode_sense_DEPENDENCIES) $(EXTRA_sg_decode_sense_DEPENDENCIES) 
+	@rm -f sg_decode_sense$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_decode_sense_OBJECTS) $(sg_decode_sense_LDADD) $(LIBS)
+
+sg_emc_trespass$(EXEEXT): $(sg_emc_trespass_OBJECTS) $(sg_emc_trespass_DEPENDENCIES) $(EXTRA_sg_emc_trespass_DEPENDENCIES) 
+	@rm -f sg_emc_trespass$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_emc_trespass_OBJECTS) $(sg_emc_trespass_LDADD) $(LIBS)
+
+sg_format$(EXEEXT): $(sg_format_OBJECTS) $(sg_format_DEPENDENCIES) $(EXTRA_sg_format_DEPENDENCIES) 
+	@rm -f sg_format$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_format_OBJECTS) $(sg_format_LDADD) $(LIBS)
+
+sg_get_config$(EXEEXT): $(sg_get_config_OBJECTS) $(sg_get_config_DEPENDENCIES) $(EXTRA_sg_get_config_DEPENDENCIES) 
+	@rm -f sg_get_config$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_get_config_OBJECTS) $(sg_get_config_LDADD) $(LIBS)
+
+sg_get_elem_status$(EXEEXT): $(sg_get_elem_status_OBJECTS) $(sg_get_elem_status_DEPENDENCIES) $(EXTRA_sg_get_elem_status_DEPENDENCIES) 
+	@rm -f sg_get_elem_status$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_get_elem_status_OBJECTS) $(sg_get_elem_status_LDADD) $(LIBS)
+
+sg_get_lba_status$(EXEEXT): $(sg_get_lba_status_OBJECTS) $(sg_get_lba_status_DEPENDENCIES) $(EXTRA_sg_get_lba_status_DEPENDENCIES) 
+	@rm -f sg_get_lba_status$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_get_lba_status_OBJECTS) $(sg_get_lba_status_LDADD) $(LIBS)
+
+sg_ident$(EXEEXT): $(sg_ident_OBJECTS) $(sg_ident_DEPENDENCIES) $(EXTRA_sg_ident_DEPENDENCIES) 
+	@rm -f sg_ident$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_ident_OBJECTS) $(sg_ident_LDADD) $(LIBS)
+
+sg_inq$(EXEEXT): $(sg_inq_OBJECTS) $(sg_inq_DEPENDENCIES) $(EXTRA_sg_inq_DEPENDENCIES) 
+	@rm -f sg_inq$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_inq_OBJECTS) $(sg_inq_LDADD) $(LIBS)
+
+sg_logs$(EXEEXT): $(sg_logs_OBJECTS) $(sg_logs_DEPENDENCIES) $(EXTRA_sg_logs_DEPENDENCIES) 
+	@rm -f sg_logs$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_logs_OBJECTS) $(sg_logs_LDADD) $(LIBS)
+
+sg_luns$(EXEEXT): $(sg_luns_OBJECTS) $(sg_luns_DEPENDENCIES) $(EXTRA_sg_luns_DEPENDENCIES) 
+	@rm -f sg_luns$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_luns_OBJECTS) $(sg_luns_LDADD) $(LIBS)
+
+sg_map$(EXEEXT): $(sg_map_OBJECTS) $(sg_map_DEPENDENCIES) $(EXTRA_sg_map_DEPENDENCIES) 
+	@rm -f sg_map$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_map_OBJECTS) $(sg_map_LDADD) $(LIBS)
+
+sg_map26$(EXEEXT): $(sg_map26_OBJECTS) $(sg_map26_DEPENDENCIES) $(EXTRA_sg_map26_DEPENDENCIES) 
+	@rm -f sg_map26$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_map26_OBJECTS) $(sg_map26_LDADD) $(LIBS)
+
+sg_modes$(EXEEXT): $(sg_modes_OBJECTS) $(sg_modes_DEPENDENCIES) $(EXTRA_sg_modes_DEPENDENCIES) 
+	@rm -f sg_modes$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_modes_OBJECTS) $(sg_modes_LDADD) $(LIBS)
+
+sg_opcodes$(EXEEXT): $(sg_opcodes_OBJECTS) $(sg_opcodes_DEPENDENCIES) $(EXTRA_sg_opcodes_DEPENDENCIES) 
+	@rm -f sg_opcodes$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_opcodes_OBJECTS) $(sg_opcodes_LDADD) $(LIBS)
+
+sg_persist$(EXEEXT): $(sg_persist_OBJECTS) $(sg_persist_DEPENDENCIES) $(EXTRA_sg_persist_DEPENDENCIES) 
+	@rm -f sg_persist$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_persist_OBJECTS) $(sg_persist_LDADD) $(LIBS)
+
+sg_prevent$(EXEEXT): $(sg_prevent_OBJECTS) $(sg_prevent_DEPENDENCIES) $(EXTRA_sg_prevent_DEPENDENCIES) 
+	@rm -f sg_prevent$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_prevent_OBJECTS) $(sg_prevent_LDADD) $(LIBS)
+
+sg_raw$(EXEEXT): $(sg_raw_OBJECTS) $(sg_raw_DEPENDENCIES) $(EXTRA_sg_raw_DEPENDENCIES) 
+	@rm -f sg_raw$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_raw_OBJECTS) $(sg_raw_LDADD) $(LIBS)
+
+sg_rbuf$(EXEEXT): $(sg_rbuf_OBJECTS) $(sg_rbuf_DEPENDENCIES) $(EXTRA_sg_rbuf_DEPENDENCIES) 
+	@rm -f sg_rbuf$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_rbuf_OBJECTS) $(sg_rbuf_LDADD) $(LIBS)
+
+sg_rdac$(EXEEXT): $(sg_rdac_OBJECTS) $(sg_rdac_DEPENDENCIES) $(EXTRA_sg_rdac_DEPENDENCIES) 
+	@rm -f sg_rdac$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_rdac_OBJECTS) $(sg_rdac_LDADD) $(LIBS)
+
+sg_read$(EXEEXT): $(sg_read_OBJECTS) $(sg_read_DEPENDENCIES) $(EXTRA_sg_read_DEPENDENCIES) 
+	@rm -f sg_read$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_read_OBJECTS) $(sg_read_LDADD) $(LIBS)
+
+sg_read_attr$(EXEEXT): $(sg_read_attr_OBJECTS) $(sg_read_attr_DEPENDENCIES) $(EXTRA_sg_read_attr_DEPENDENCIES) 
+	@rm -f sg_read_attr$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_read_attr_OBJECTS) $(sg_read_attr_LDADD) $(LIBS)
+
+sg_read_block_limits$(EXEEXT): $(sg_read_block_limits_OBJECTS) $(sg_read_block_limits_DEPENDENCIES) $(EXTRA_sg_read_block_limits_DEPENDENCIES) 
+	@rm -f sg_read_block_limits$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_read_block_limits_OBJECTS) $(sg_read_block_limits_LDADD) $(LIBS)
+
+sg_read_buffer$(EXEEXT): $(sg_read_buffer_OBJECTS) $(sg_read_buffer_DEPENDENCIES) $(EXTRA_sg_read_buffer_DEPENDENCIES) 
+	@rm -f sg_read_buffer$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_read_buffer_OBJECTS) $(sg_read_buffer_LDADD) $(LIBS)
+
+sg_read_long$(EXEEXT): $(sg_read_long_OBJECTS) $(sg_read_long_DEPENDENCIES) $(EXTRA_sg_read_long_DEPENDENCIES) 
+	@rm -f sg_read_long$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_read_long_OBJECTS) $(sg_read_long_LDADD) $(LIBS)
+
+sg_readcap$(EXEEXT): $(sg_readcap_OBJECTS) $(sg_readcap_DEPENDENCIES) $(EXTRA_sg_readcap_DEPENDENCIES) 
+	@rm -f sg_readcap$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_readcap_OBJECTS) $(sg_readcap_LDADD) $(LIBS)
+
+sg_reassign$(EXEEXT): $(sg_reassign_OBJECTS) $(sg_reassign_DEPENDENCIES) $(EXTRA_sg_reassign_DEPENDENCIES) 
+	@rm -f sg_reassign$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_reassign_OBJECTS) $(sg_reassign_LDADD) $(LIBS)
+
+sg_referrals$(EXEEXT): $(sg_referrals_OBJECTS) $(sg_referrals_DEPENDENCIES) $(EXTRA_sg_referrals_DEPENDENCIES) 
+	@rm -f sg_referrals$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_referrals_OBJECTS) $(sg_referrals_LDADD) $(LIBS)
+
+sg_rem_rest_elem$(EXEEXT): $(sg_rem_rest_elem_OBJECTS) $(sg_rem_rest_elem_DEPENDENCIES) $(EXTRA_sg_rem_rest_elem_DEPENDENCIES) 
+	@rm -f sg_rem_rest_elem$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_rem_rest_elem_OBJECTS) $(sg_rem_rest_elem_LDADD) $(LIBS)
+
+sg_rep_density$(EXEEXT): $(sg_rep_density_OBJECTS) $(sg_rep_density_DEPENDENCIES) $(EXTRA_sg_rep_density_DEPENDENCIES) 
+	@rm -f sg_rep_density$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_rep_density_OBJECTS) $(sg_rep_density_LDADD) $(LIBS)
+
+sg_rep_pip$(EXEEXT): $(sg_rep_pip_OBJECTS) $(sg_rep_pip_DEPENDENCIES) $(EXTRA_sg_rep_pip_DEPENDENCIES) 
+	@rm -f sg_rep_pip$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_rep_pip_OBJECTS) $(sg_rep_pip_LDADD) $(LIBS)
+
+sg_rep_zones$(EXEEXT): $(sg_rep_zones_OBJECTS) $(sg_rep_zones_DEPENDENCIES) $(EXTRA_sg_rep_zones_DEPENDENCIES) 
+	@rm -f sg_rep_zones$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_rep_zones_OBJECTS) $(sg_rep_zones_LDADD) $(LIBS)
+
+sg_requests$(EXEEXT): $(sg_requests_OBJECTS) $(sg_requests_DEPENDENCIES) $(EXTRA_sg_requests_DEPENDENCIES) 
+	@rm -f sg_requests$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_requests_OBJECTS) $(sg_requests_LDADD) $(LIBS)
+
+sg_reset$(EXEEXT): $(sg_reset_OBJECTS) $(sg_reset_DEPENDENCIES) $(EXTRA_sg_reset_DEPENDENCIES) 
+	@rm -f sg_reset$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_reset_OBJECTS) $(sg_reset_LDADD) $(LIBS)
+
+sg_reset_wp$(EXEEXT): $(sg_reset_wp_OBJECTS) $(sg_reset_wp_DEPENDENCIES) $(EXTRA_sg_reset_wp_DEPENDENCIES) 
+	@rm -f sg_reset_wp$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_reset_wp_OBJECTS) $(sg_reset_wp_LDADD) $(LIBS)
+
+sg_rmsn$(EXEEXT): $(sg_rmsn_OBJECTS) $(sg_rmsn_DEPENDENCIES) $(EXTRA_sg_rmsn_DEPENDENCIES) 
+	@rm -f sg_rmsn$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_rmsn_OBJECTS) $(sg_rmsn_LDADD) $(LIBS)
+
+sg_rtpg$(EXEEXT): $(sg_rtpg_OBJECTS) $(sg_rtpg_DEPENDENCIES) $(EXTRA_sg_rtpg_DEPENDENCIES) 
+	@rm -f sg_rtpg$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_rtpg_OBJECTS) $(sg_rtpg_LDADD) $(LIBS)
+
+sg_safte$(EXEEXT): $(sg_safte_OBJECTS) $(sg_safte_DEPENDENCIES) $(EXTRA_sg_safte_DEPENDENCIES) 
+	@rm -f sg_safte$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_safte_OBJECTS) $(sg_safte_LDADD) $(LIBS)
+
+sg_sanitize$(EXEEXT): $(sg_sanitize_OBJECTS) $(sg_sanitize_DEPENDENCIES) $(EXTRA_sg_sanitize_DEPENDENCIES) 
+	@rm -f sg_sanitize$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_sanitize_OBJECTS) $(sg_sanitize_LDADD) $(LIBS)
+
+sg_sat_identify$(EXEEXT): $(sg_sat_identify_OBJECTS) $(sg_sat_identify_DEPENDENCIES) $(EXTRA_sg_sat_identify_DEPENDENCIES) 
+	@rm -f sg_sat_identify$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_sat_identify_OBJECTS) $(sg_sat_identify_LDADD) $(LIBS)
+
+sg_sat_phy_event$(EXEEXT): $(sg_sat_phy_event_OBJECTS) $(sg_sat_phy_event_DEPENDENCIES) $(EXTRA_sg_sat_phy_event_DEPENDENCIES) 
+	@rm -f sg_sat_phy_event$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_sat_phy_event_OBJECTS) $(sg_sat_phy_event_LDADD) $(LIBS)
+
+sg_sat_read_gplog$(EXEEXT): $(sg_sat_read_gplog_OBJECTS) $(sg_sat_read_gplog_DEPENDENCIES) $(EXTRA_sg_sat_read_gplog_DEPENDENCIES) 
+	@rm -f sg_sat_read_gplog$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_sat_read_gplog_OBJECTS) $(sg_sat_read_gplog_LDADD) $(LIBS)
+
+sg_sat_set_features$(EXEEXT): $(sg_sat_set_features_OBJECTS) $(sg_sat_set_features_DEPENDENCIES) $(EXTRA_sg_sat_set_features_DEPENDENCIES) 
+	@rm -f sg_sat_set_features$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_sat_set_features_OBJECTS) $(sg_sat_set_features_LDADD) $(LIBS)
+
+sg_scan$(EXEEXT): $(sg_scan_OBJECTS) $(sg_scan_DEPENDENCIES) $(EXTRA_sg_scan_DEPENDENCIES) 
+	@rm -f sg_scan$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_scan_OBJECTS) $(sg_scan_LDADD) $(LIBS)
+
+sg_seek$(EXEEXT): $(sg_seek_OBJECTS) $(sg_seek_DEPENDENCIES) $(EXTRA_sg_seek_DEPENDENCIES) 
+	@rm -f sg_seek$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_seek_OBJECTS) $(sg_seek_LDADD) $(LIBS)
+
+sg_senddiag$(EXEEXT): $(sg_senddiag_OBJECTS) $(sg_senddiag_DEPENDENCIES) $(EXTRA_sg_senddiag_DEPENDENCIES) 
+	@rm -f sg_senddiag$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_senddiag_OBJECTS) $(sg_senddiag_LDADD) $(LIBS)
+
+sg_ses$(EXEEXT): $(sg_ses_OBJECTS) $(sg_ses_DEPENDENCIES) $(EXTRA_sg_ses_DEPENDENCIES) 
+	@rm -f sg_ses$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_ses_OBJECTS) $(sg_ses_LDADD) $(LIBS)
+
+sg_ses_microcode$(EXEEXT): $(sg_ses_microcode_OBJECTS) $(sg_ses_microcode_DEPENDENCIES) $(EXTRA_sg_ses_microcode_DEPENDENCIES) 
+	@rm -f sg_ses_microcode$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_ses_microcode_OBJECTS) $(sg_ses_microcode_LDADD) $(LIBS)
+
+sg_start$(EXEEXT): $(sg_start_OBJECTS) $(sg_start_DEPENDENCIES) $(EXTRA_sg_start_DEPENDENCIES) 
+	@rm -f sg_start$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_start_OBJECTS) $(sg_start_LDADD) $(LIBS)
+
+sg_stpg$(EXEEXT): $(sg_stpg_OBJECTS) $(sg_stpg_DEPENDENCIES) $(EXTRA_sg_stpg_DEPENDENCIES) 
+	@rm -f sg_stpg$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_stpg_OBJECTS) $(sg_stpg_LDADD) $(LIBS)
+
+sg_stream_ctl$(EXEEXT): $(sg_stream_ctl_OBJECTS) $(sg_stream_ctl_DEPENDENCIES) $(EXTRA_sg_stream_ctl_DEPENDENCIES) 
+	@rm -f sg_stream_ctl$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_stream_ctl_OBJECTS) $(sg_stream_ctl_LDADD) $(LIBS)
+
+sg_sync$(EXEEXT): $(sg_sync_OBJECTS) $(sg_sync_DEPENDENCIES) $(EXTRA_sg_sync_DEPENDENCIES) 
+	@rm -f sg_sync$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_sync_OBJECTS) $(sg_sync_LDADD) $(LIBS)
+
+sg_test_rwbuf$(EXEEXT): $(sg_test_rwbuf_OBJECTS) $(sg_test_rwbuf_DEPENDENCIES) $(EXTRA_sg_test_rwbuf_DEPENDENCIES) 
+	@rm -f sg_test_rwbuf$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_test_rwbuf_OBJECTS) $(sg_test_rwbuf_LDADD) $(LIBS)
+
+sg_timestamp$(EXEEXT): $(sg_timestamp_OBJECTS) $(sg_timestamp_DEPENDENCIES) $(EXTRA_sg_timestamp_DEPENDENCIES) 
+	@rm -f sg_timestamp$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_timestamp_OBJECTS) $(sg_timestamp_LDADD) $(LIBS)
+
+sg_turs$(EXEEXT): $(sg_turs_OBJECTS) $(sg_turs_DEPENDENCIES) $(EXTRA_sg_turs_DEPENDENCIES) 
+	@rm -f sg_turs$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_turs_OBJECTS) $(sg_turs_LDADD) $(LIBS)
+
+sg_unmap$(EXEEXT): $(sg_unmap_OBJECTS) $(sg_unmap_DEPENDENCIES) $(EXTRA_sg_unmap_DEPENDENCIES) 
+	@rm -f sg_unmap$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_unmap_OBJECTS) $(sg_unmap_LDADD) $(LIBS)
+
+sg_verify$(EXEEXT): $(sg_verify_OBJECTS) $(sg_verify_DEPENDENCIES) $(EXTRA_sg_verify_DEPENDENCIES) 
+	@rm -f sg_verify$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_verify_OBJECTS) $(sg_verify_LDADD) $(LIBS)
+
+sg_vpd$(EXEEXT): $(sg_vpd_OBJECTS) $(sg_vpd_DEPENDENCIES) $(EXTRA_sg_vpd_DEPENDENCIES) 
+	@rm -f sg_vpd$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_vpd_OBJECTS) $(sg_vpd_LDADD) $(LIBS)
+
+sg_wr_mode$(EXEEXT): $(sg_wr_mode_OBJECTS) $(sg_wr_mode_DEPENDENCIES) $(EXTRA_sg_wr_mode_DEPENDENCIES) 
+	@rm -f sg_wr_mode$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_wr_mode_OBJECTS) $(sg_wr_mode_LDADD) $(LIBS)
+
+sg_write_buffer$(EXEEXT): $(sg_write_buffer_OBJECTS) $(sg_write_buffer_DEPENDENCIES) $(EXTRA_sg_write_buffer_DEPENDENCIES) 
+	@rm -f sg_write_buffer$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_write_buffer_OBJECTS) $(sg_write_buffer_LDADD) $(LIBS)
+
+sg_write_long$(EXEEXT): $(sg_write_long_OBJECTS) $(sg_write_long_DEPENDENCIES) $(EXTRA_sg_write_long_DEPENDENCIES) 
+	@rm -f sg_write_long$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_write_long_OBJECTS) $(sg_write_long_LDADD) $(LIBS)
+
+sg_write_same$(EXEEXT): $(sg_write_same_OBJECTS) $(sg_write_same_DEPENDENCIES) $(EXTRA_sg_write_same_DEPENDENCIES) 
+	@rm -f sg_write_same$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_write_same_OBJECTS) $(sg_write_same_LDADD) $(LIBS)
+
+sg_write_verify$(EXEEXT): $(sg_write_verify_OBJECTS) $(sg_write_verify_DEPENDENCIES) $(EXTRA_sg_write_verify_DEPENDENCIES) 
+	@rm -f sg_write_verify$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_write_verify_OBJECTS) $(sg_write_verify_LDADD) $(LIBS)
+
+sg_write_x$(EXEEXT): $(sg_write_x_OBJECTS) $(sg_write_x_DEPENDENCIES) $(EXTRA_sg_write_x_DEPENDENCIES) 
+	@rm -f sg_write_x$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_write_x_OBJECTS) $(sg_write_x_LDADD) $(LIBS)
+
+sg_xcopy$(EXEEXT): $(sg_xcopy_OBJECTS) $(sg_xcopy_DEPENDENCIES) $(EXTRA_sg_xcopy_DEPENDENCIES) 
+	@rm -f sg_xcopy$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_xcopy_OBJECTS) $(sg_xcopy_LDADD) $(LIBS)
+
+sg_z_act_query$(EXEEXT): $(sg_z_act_query_OBJECTS) $(sg_z_act_query_DEPENDENCIES) $(EXTRA_sg_z_act_query_DEPENDENCIES) 
+	@rm -f sg_z_act_query$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_z_act_query_OBJECTS) $(sg_z_act_query_LDADD) $(LIBS)
+
+sg_zone$(EXEEXT): $(sg_zone_OBJECTS) $(sg_zone_DEPENDENCIES) $(EXTRA_sg_zone_DEPENDENCIES) 
+	@rm -f sg_zone$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sg_zone_OBJECTS) $(sg_zone_LDADD) $(LIBS)
+
+sginfo$(EXEEXT): $(sginfo_OBJECTS) $(sginfo_DEPENDENCIES) $(EXTRA_sginfo_DEPENDENCIES) 
+	@rm -f sginfo$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sginfo_OBJECTS) $(sginfo_LDADD) $(LIBS)
+
+sgm_dd$(EXEEXT): $(sgm_dd_OBJECTS) $(sgm_dd_DEPENDENCIES) $(EXTRA_sgm_dd_DEPENDENCIES) 
+	@rm -f sgm_dd$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sgm_dd_OBJECTS) $(sgm_dd_LDADD) $(LIBS)
+
+sgp_dd$(EXEEXT): $(sgp_dd_OBJECTS) $(sgp_dd_DEPENDENCIES) $(EXTRA_sgp_dd_DEPENDENCIES) 
+	@rm -f sgp_dd$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(sgp_dd_OBJECTS) $(sgp_dd_LDADD) $(LIBS)
+
+mostlyclean-compile:
+	-rm -f *.$(OBJEXT)
+
+distclean-compile:
+	-rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_bg_ctl.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_compare_and_write.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_copy_results.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_dd.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_decode_sense.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_emc_trespass.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_format.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_get_config.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_get_elem_status.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_get_lba_status.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_ident.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_inq.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_inq_data.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_logs.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_luns.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_map.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_map26.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_modes.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_opcodes.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_persist.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_prevent.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_raw.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_rbuf.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_rdac.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_read.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_read_attr.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_read_block_limits.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_read_buffer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_read_long.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_readcap.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_reassign.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_referrals.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_rem_rest_elem.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_rep_density.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_rep_pip.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_rep_zones.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_requests.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_reset.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_reset_wp.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_rmsn.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_rtpg.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_safte.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_sanitize.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_sat_identify.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_sat_phy_event.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_sat_read_gplog.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_sat_set_features.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_scan_linux.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_scan_win32.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_seek.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_senddiag.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_ses.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_ses_microcode.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_start.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_stpg.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_stream_ctl.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_sync.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_test_rwbuf.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_timestamp.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_turs.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_unmap.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_verify.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_vpd.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_vpd_common.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_vpd_vendor.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_wr_mode.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_write_buffer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_write_long.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_write_same.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_write_verify.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_write_x.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_xcopy.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_z_act_query.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sg_zone.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sginfo.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sgm_dd.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sgp_dd.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+	@$(MKDIR_P) $(@D)
+	@echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	$(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	$(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	$(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+	-rm -f *.lo
+
+clean-libtool:
+	-rm -rf .libs _libs
+
+ID: $(am__tagged_files)
+	$(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+	set x; \
+	here=`pwd`; \
+	$(am__define_uniq_tagged_files); \
+	shift; \
+	if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+	  test -n "$$unique" || unique=$$empty_fix; \
+	  if test $$# -gt 0; then \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      "$$@" $$unique; \
+	  else \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      $$unique; \
+	  fi; \
+	fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+	$(am__define_uniq_tagged_files); \
+	test -z "$(CTAGS_ARGS)$$unique" \
+	  || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+	     $$unique
+
+GTAGS:
+	here=`$(am__cd) $(top_builddir) && pwd` \
+	  && $(am__cd) $(top_srcdir) \
+	  && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+	list='$(am__tagged_files)'; \
+	case "$(srcdir)" in \
+	  [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+	  *) sdir=$(subdir)/$(srcdir) ;; \
+	esac; \
+	for i in $$list; do \
+	  if test -f "$$i"; then \
+	    echo "$(subdir)/$$i"; \
+	  else \
+	    echo "$$sdir/$$i"; \
+	  fi; \
+	done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+	-rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+distdir: $(BUILT_SOURCES)
+	$(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+	@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	list='$(DISTFILES)'; \
+	  dist_files=`for file in $$list; do echo $$file; done | \
+	  sed -e "s|^$$srcdirstrip/||;t" \
+	      -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+	case $$dist_files in \
+	  */*) $(MKDIR_P) `echo "$$dist_files" | \
+			   sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+			   sort -u` ;; \
+	esac; \
+	for file in $$dist_files; do \
+	  if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+	  if test -d $$d/$$file; then \
+	    dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+	    if test -d "$(distdir)/$$file"; then \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+	      cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+	  else \
+	    test -f "$(distdir)/$$file" \
+	    || cp -p $$d/$$file "$(distdir)/$$file" \
+	    || exit 1; \
+	  fi; \
+	done
+check-am: all-am
+check: check-am
+all-am: Makefile $(PROGRAMS)
+installdirs:
+	for dir in "$(DESTDIR)$(bindir)"; do \
+	  test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+	done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+	@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+	if test -z '$(STRIP)'; then \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	      install; \
+	else \
+	  $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	    install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	    "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+	fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+	-test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+	-test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+	@echo "This command is intended for maintainers to use"
+	@echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-binPROGRAMS clean-generic clean-libtool mostlyclean-am
+
+distclean: distclean-am
+		-rm -f ./$(DEPDIR)/sg_bg_ctl.Po
+	-rm -f ./$(DEPDIR)/sg_compare_and_write.Po
+	-rm -f ./$(DEPDIR)/sg_copy_results.Po
+	-rm -f ./$(DEPDIR)/sg_dd.Po
+	-rm -f ./$(DEPDIR)/sg_decode_sense.Po
+	-rm -f ./$(DEPDIR)/sg_emc_trespass.Po
+	-rm -f ./$(DEPDIR)/sg_format.Po
+	-rm -f ./$(DEPDIR)/sg_get_config.Po
+	-rm -f ./$(DEPDIR)/sg_get_elem_status.Po
+	-rm -f ./$(DEPDIR)/sg_get_lba_status.Po
+	-rm -f ./$(DEPDIR)/sg_ident.Po
+	-rm -f ./$(DEPDIR)/sg_inq.Po
+	-rm -f ./$(DEPDIR)/sg_inq_data.Po
+	-rm -f ./$(DEPDIR)/sg_logs.Po
+	-rm -f ./$(DEPDIR)/sg_luns.Po
+	-rm -f ./$(DEPDIR)/sg_map.Po
+	-rm -f ./$(DEPDIR)/sg_map26.Po
+	-rm -f ./$(DEPDIR)/sg_modes.Po
+	-rm -f ./$(DEPDIR)/sg_opcodes.Po
+	-rm -f ./$(DEPDIR)/sg_persist.Po
+	-rm -f ./$(DEPDIR)/sg_prevent.Po
+	-rm -f ./$(DEPDIR)/sg_raw.Po
+	-rm -f ./$(DEPDIR)/sg_rbuf.Po
+	-rm -f ./$(DEPDIR)/sg_rdac.Po
+	-rm -f ./$(DEPDIR)/sg_read.Po
+	-rm -f ./$(DEPDIR)/sg_read_attr.Po
+	-rm -f ./$(DEPDIR)/sg_read_block_limits.Po
+	-rm -f ./$(DEPDIR)/sg_read_buffer.Po
+	-rm -f ./$(DEPDIR)/sg_read_long.Po
+	-rm -f ./$(DEPDIR)/sg_readcap.Po
+	-rm -f ./$(DEPDIR)/sg_reassign.Po
+	-rm -f ./$(DEPDIR)/sg_referrals.Po
+	-rm -f ./$(DEPDIR)/sg_rem_rest_elem.Po
+	-rm -f ./$(DEPDIR)/sg_rep_density.Po
+	-rm -f ./$(DEPDIR)/sg_rep_pip.Po
+	-rm -f ./$(DEPDIR)/sg_rep_zones.Po
+	-rm -f ./$(DEPDIR)/sg_requests.Po
+	-rm -f ./$(DEPDIR)/sg_reset.Po
+	-rm -f ./$(DEPDIR)/sg_reset_wp.Po
+	-rm -f ./$(DEPDIR)/sg_rmsn.Po
+	-rm -f ./$(DEPDIR)/sg_rtpg.Po
+	-rm -f ./$(DEPDIR)/sg_safte.Po
+	-rm -f ./$(DEPDIR)/sg_sanitize.Po
+	-rm -f ./$(DEPDIR)/sg_sat_identify.Po
+	-rm -f ./$(DEPDIR)/sg_sat_phy_event.Po
+	-rm -f ./$(DEPDIR)/sg_sat_read_gplog.Po
+	-rm -f ./$(DEPDIR)/sg_sat_set_features.Po
+	-rm -f ./$(DEPDIR)/sg_scan_linux.Po
+	-rm -f ./$(DEPDIR)/sg_scan_win32.Po
+	-rm -f ./$(DEPDIR)/sg_seek.Po
+	-rm -f ./$(DEPDIR)/sg_senddiag.Po
+	-rm -f ./$(DEPDIR)/sg_ses.Po
+	-rm -f ./$(DEPDIR)/sg_ses_microcode.Po
+	-rm -f ./$(DEPDIR)/sg_start.Po
+	-rm -f ./$(DEPDIR)/sg_stpg.Po
+	-rm -f ./$(DEPDIR)/sg_stream_ctl.Po
+	-rm -f ./$(DEPDIR)/sg_sync.Po
+	-rm -f ./$(DEPDIR)/sg_test_rwbuf.Po
+	-rm -f ./$(DEPDIR)/sg_timestamp.Po
+	-rm -f ./$(DEPDIR)/sg_turs.Po
+	-rm -f ./$(DEPDIR)/sg_unmap.Po
+	-rm -f ./$(DEPDIR)/sg_verify.Po
+	-rm -f ./$(DEPDIR)/sg_vpd.Po
+	-rm -f ./$(DEPDIR)/sg_vpd_common.Po
+	-rm -f ./$(DEPDIR)/sg_vpd_vendor.Po
+	-rm -f ./$(DEPDIR)/sg_wr_mode.Po
+	-rm -f ./$(DEPDIR)/sg_write_buffer.Po
+	-rm -f ./$(DEPDIR)/sg_write_long.Po
+	-rm -f ./$(DEPDIR)/sg_write_same.Po
+	-rm -f ./$(DEPDIR)/sg_write_verify.Po
+	-rm -f ./$(DEPDIR)/sg_write_x.Po
+	-rm -f ./$(DEPDIR)/sg_xcopy.Po
+	-rm -f ./$(DEPDIR)/sg_z_act_query.Po
+	-rm -f ./$(DEPDIR)/sg_zone.Po
+	-rm -f ./$(DEPDIR)/sginfo.Po
+	-rm -f ./$(DEPDIR)/sgm_dd.Po
+	-rm -f ./$(DEPDIR)/sgp_dd.Po
+	-rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+	distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-binPROGRAMS
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+		-rm -f ./$(DEPDIR)/sg_bg_ctl.Po
+	-rm -f ./$(DEPDIR)/sg_compare_and_write.Po
+	-rm -f ./$(DEPDIR)/sg_copy_results.Po
+	-rm -f ./$(DEPDIR)/sg_dd.Po
+	-rm -f ./$(DEPDIR)/sg_decode_sense.Po
+	-rm -f ./$(DEPDIR)/sg_emc_trespass.Po
+	-rm -f ./$(DEPDIR)/sg_format.Po
+	-rm -f ./$(DEPDIR)/sg_get_config.Po
+	-rm -f ./$(DEPDIR)/sg_get_elem_status.Po
+	-rm -f ./$(DEPDIR)/sg_get_lba_status.Po
+	-rm -f ./$(DEPDIR)/sg_ident.Po
+	-rm -f ./$(DEPDIR)/sg_inq.Po
+	-rm -f ./$(DEPDIR)/sg_inq_data.Po
+	-rm -f ./$(DEPDIR)/sg_logs.Po
+	-rm -f ./$(DEPDIR)/sg_luns.Po
+	-rm -f ./$(DEPDIR)/sg_map.Po
+	-rm -f ./$(DEPDIR)/sg_map26.Po
+	-rm -f ./$(DEPDIR)/sg_modes.Po
+	-rm -f ./$(DEPDIR)/sg_opcodes.Po
+	-rm -f ./$(DEPDIR)/sg_persist.Po
+	-rm -f ./$(DEPDIR)/sg_prevent.Po
+	-rm -f ./$(DEPDIR)/sg_raw.Po
+	-rm -f ./$(DEPDIR)/sg_rbuf.Po
+	-rm -f ./$(DEPDIR)/sg_rdac.Po
+	-rm -f ./$(DEPDIR)/sg_read.Po
+	-rm -f ./$(DEPDIR)/sg_read_attr.Po
+	-rm -f ./$(DEPDIR)/sg_read_block_limits.Po
+	-rm -f ./$(DEPDIR)/sg_read_buffer.Po
+	-rm -f ./$(DEPDIR)/sg_read_long.Po
+	-rm -f ./$(DEPDIR)/sg_readcap.Po
+	-rm -f ./$(DEPDIR)/sg_reassign.Po
+	-rm -f ./$(DEPDIR)/sg_referrals.Po
+	-rm -f ./$(DEPDIR)/sg_rem_rest_elem.Po
+	-rm -f ./$(DEPDIR)/sg_rep_density.Po
+	-rm -f ./$(DEPDIR)/sg_rep_pip.Po
+	-rm -f ./$(DEPDIR)/sg_rep_zones.Po
+	-rm -f ./$(DEPDIR)/sg_requests.Po
+	-rm -f ./$(DEPDIR)/sg_reset.Po
+	-rm -f ./$(DEPDIR)/sg_reset_wp.Po
+	-rm -f ./$(DEPDIR)/sg_rmsn.Po
+	-rm -f ./$(DEPDIR)/sg_rtpg.Po
+	-rm -f ./$(DEPDIR)/sg_safte.Po
+	-rm -f ./$(DEPDIR)/sg_sanitize.Po
+	-rm -f ./$(DEPDIR)/sg_sat_identify.Po
+	-rm -f ./$(DEPDIR)/sg_sat_phy_event.Po
+	-rm -f ./$(DEPDIR)/sg_sat_read_gplog.Po
+	-rm -f ./$(DEPDIR)/sg_sat_set_features.Po
+	-rm -f ./$(DEPDIR)/sg_scan_linux.Po
+	-rm -f ./$(DEPDIR)/sg_scan_win32.Po
+	-rm -f ./$(DEPDIR)/sg_seek.Po
+	-rm -f ./$(DEPDIR)/sg_senddiag.Po
+	-rm -f ./$(DEPDIR)/sg_ses.Po
+	-rm -f ./$(DEPDIR)/sg_ses_microcode.Po
+	-rm -f ./$(DEPDIR)/sg_start.Po
+	-rm -f ./$(DEPDIR)/sg_stpg.Po
+	-rm -f ./$(DEPDIR)/sg_stream_ctl.Po
+	-rm -f ./$(DEPDIR)/sg_sync.Po
+	-rm -f ./$(DEPDIR)/sg_test_rwbuf.Po
+	-rm -f ./$(DEPDIR)/sg_timestamp.Po
+	-rm -f ./$(DEPDIR)/sg_turs.Po
+	-rm -f ./$(DEPDIR)/sg_unmap.Po
+	-rm -f ./$(DEPDIR)/sg_verify.Po
+	-rm -f ./$(DEPDIR)/sg_vpd.Po
+	-rm -f ./$(DEPDIR)/sg_vpd_common.Po
+	-rm -f ./$(DEPDIR)/sg_vpd_vendor.Po
+	-rm -f ./$(DEPDIR)/sg_wr_mode.Po
+	-rm -f ./$(DEPDIR)/sg_write_buffer.Po
+	-rm -f ./$(DEPDIR)/sg_write_long.Po
+	-rm -f ./$(DEPDIR)/sg_write_same.Po
+	-rm -f ./$(DEPDIR)/sg_write_verify.Po
+	-rm -f ./$(DEPDIR)/sg_write_x.Po
+	-rm -f ./$(DEPDIR)/sg_xcopy.Po
+	-rm -f ./$(DEPDIR)/sg_z_act_query.Po
+	-rm -f ./$(DEPDIR)/sg_zone.Po
+	-rm -f ./$(DEPDIR)/sginfo.Po
+	-rm -f ./$(DEPDIR)/sgm_dd.Po
+	-rm -f ./$(DEPDIR)/sgp_dd.Po
+	-rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+	mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-binPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \
+	clean-binPROGRAMS clean-generic clean-libtool cscopelist-am \
+	ctags ctags-am distclean distclean-compile distclean-generic \
+	distclean-libtool distclean-tags distdir dvi dvi-am html \
+	html-am info info-am install install-am install-binPROGRAMS \
+	install-data install-data-am install-dvi install-dvi-am \
+	install-exec install-exec-am install-html install-html-am \
+	install-info install-info-am install-man install-pdf \
+	install-pdf-am install-ps install-ps-am install-strip \
+	installcheck installcheck-am installdirs maintainer-clean \
+	maintainer-clean-generic mostlyclean mostlyclean-compile \
+	mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \
+	tags tags-am uninstall uninstall-am uninstall-binPROGRAMS
+
+.PRECIOUS: Makefile
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/sg_bg_ctl.c b/src/sg_bg_ctl.c
new file mode 100644
index 0000000..81e43bd
--- /dev/null
+++ b/src/sg_bg_ctl.c
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2016-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI BACKGROUND CONTROL command to the given SCSI
+ * device. Based on sbc4r10.pdf .
+ */
+
+static const char * version_str = "1.13 20211114";
+
+#define BACKGROUND_CONTROL_SA 0x15
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+static const char * cmd_name = "Background control";
+
+
+static struct option long_options[] = {
+        {"ctl", required_argument, 0, 'c'},
+        {"help", no_argument, 0, 'h'},
+        {"time", required_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_bg_ctl  [--ctl=CTL] [--help] [--time=TN] [--verbose] "
+            "[--version]\n"
+            "                  DEVICE\n");
+    pr2serr("  where:\n"
+            "    --ctl=CTL|-c CTL    CTL is background operation control "
+            "value\n"
+            "                        default: 0 -> don't change background "
+            "operations\n"
+            "                        1 -> start; 2 -> stop\n"
+            "    --help|-h          print out usage message\n"
+            "    --time=TN|-t TN    TN (units 100 ms) is max time to perform "
+            "background\n"
+            "                       operations (def: 0 -> no limit)\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Performs a SCSI BACKGROUND CONTROL command. It can start or "
+            "stop\n'advanced background operations'. Operations started by "
+            "this command\n(i.e. when ctl=1) are termed as 'host initiated' "
+            "and allow a resource or\nthin provisioned device (disk) to "
+            "perform garbage collection type operations.\nThese may "
+            "degrade performance while they occur. Hence it is best to\n"
+            "perform this action while the computer is not too busy.\n");
+}
+
+/* Invokes a SCSI BACKGROUND CONTROL command (SBC-4).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_background_control(int sg_fd, unsigned int bo_ctl, unsigned int bo_time,
+                         bool noisy, int verbose)
+{
+    int ret, res, sense_cat;
+    uint8_t bcCDB[16] = {SG_SERVICE_ACTION_IN_16,
+           BACKGROUND_CONTROL_SA, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
+           0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (bo_ctl)
+        bcCDB[2] |= (bo_ctl & 0x3) << 6;
+    if (bo_time)
+        bcCDB[3] = bo_time;
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", cmd_name,
+                sg_get_command_str(bcCDB, (int)sizeof(bcCDB), false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", cmd_name);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, bcCDB, sizeof(bcCDB));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cmd_name, res, noisy, verbose,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool verbose_given = false;
+    bool version_given = false;
+    int sg_fd = -1;
+    int res, c;
+    unsigned int ctl = 0;
+    unsigned int time_tnth = 0;
+    int verbose = 0;
+    const char * device_name = NULL;
+    int ret = 0;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "c:ht:vV", long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            if ((1 != sscanf(optarg, "%4u", &ctl)) || (ctl > 3)) {
+                pr2serr("--ctl= expects a number from 0 to 3\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 't':
+            if ((1 != sscanf(optarg, "%4u", &time_tnth)) ||
+                (time_tnth > 255)) {
+                pr2serr("--time= expects a number from 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    res = sg_ll_background_control(sg_fd, ctl, time_tnth, true, verbose);
+    ret = res;
+    if (res) {
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("%s command not supported\n", cmd_name);
+        else {
+            char b[80];
+
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("%s command: %s\n", cmd_name, b);
+        }
+    }
+
+fini:
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_bg_ctl failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_compare_and_write.c b/src/sg_compare_and_write.c
new file mode 100644
index 0000000..b8ed82d
--- /dev/null
+++ b/src/sg_compare_and_write.c
@@ -0,0 +1,623 @@
+/*
+*  Copyright (c) 2012-2022, Kaminario Technologies LTD
+*  All rights reserved.
+*  Redistribution and use in source and binary forms, with or without
+*  modification, are permitted provided that the following conditions are met:
+*    * Redistributions of source code must retain the above copyright
+*        notice, this list of conditions and the following disclaimer.
+*    * Redistributions in binary form must reproduce the above copyright
+*        notice, this list of conditions and the following disclaimer in the
+*        documentation and/or other materials provided with the distribution.
+*    * Neither the name of the <organization> nor the
+*        names of its contributors may be used to endorse or promote products
+*        derived from this software without specific prior written permission.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+*  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+*  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+*  ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+*  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+*  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+*  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+*  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+*  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+*  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * This command performs a SCSI COMPARE AND WRITE. See SBC-3 at
+ * https://www.t10.org
+ *
+ */
+
+#ifndef __sun
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.32 20220127";
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_NUM_BLOCKS (1)
+#define DEF_BLOCKS_PER_TRANSFER 8
+#define DEF_TIMEOUT_SECS 60
+
+#define COMPARE_AND_WRITE_OPCODE (0x89)
+#define COMPARE_AND_WRITE_CDB_SIZE (16)
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+
+#define ME "sg_compare_and_write: "
+
+static struct option long_options[] = {
+        {"dpo", no_argument, 0, 'd'},
+        {"fua", no_argument, 0, 'f'},
+        {"fua_nv", no_argument, 0, 'F'},
+        {"fua-nv", no_argument, 0, 'F'},
+        {"group", required_argument, 0, 'g'},
+        {"grpnum", required_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"in", required_argument, 0, 'i'},
+        {"inc", required_argument, 0, 'C'},
+        {"inw", required_argument, 0, 'D'},
+        {"lba", required_argument, 0, 'l'},
+        {"num", required_argument, 0, 'n'},
+        {"quiet", no_argument, 0, 'q'},
+        {"timeout", required_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"wrprotect", required_argument, 0, 'w'},
+        {"xferlen", required_argument, 0, 'x'},
+        {0, 0, 0, 0},
+};
+
+struct caw_flags {
+        bool dpo;
+        bool fua;
+        bool fua_nv;
+        int group;
+        int wrprotect;
+};
+
+struct opts_t {
+        bool quiet;
+        bool verbose_given;
+        bool version_given;
+        bool wfn_given;
+        int numblocks;
+        int verbose;
+        int timeout;
+        int xfer_len;
+        uint64_t lba;
+        const char * ifn;
+        const char * wfn;
+        const char * device_name;
+        struct caw_flags flags;
+};
+
+
+static void
+usage()
+{
+        pr2serr("Usage: sg_compare_and_write [--dpo] [--fua] [--fua_nv] "
+                "[--grpnum=GN] [--help]\n"
+                "                            --in=IF|--inc=IF [--inw=WF] "
+                "--lba=LBA "
+                "[--num=NUM]\n"
+                "                            [--quiet] [--timeout=TO] "
+                "[--verbose] [--version]\n"
+                "                            [--wrprotect=WP] [--xferlen=LEN] "
+                "DEVICE\n"
+                "  where:\n"
+                "    --dpo|-d            set the dpo bit in cdb (def: "
+                "clear)\n"
+                "    --fua|-f            set the fua bit in cdb (def: "
+                "clear)\n"
+                "    --fua_nv|-F         set the fua_nv bit in cdb (def: "
+                "clear)\n"
+                "    --grpnum=GN|-g GN    GN is GROUP NUMBER to set in "
+                "cdb (def: 0)\n"
+                "    --help|-h           print out usage message\n"
+                "    --in=IF|-i IF       IF is a file containing a compare "
+                "buffer and\n"
+                "                        optionally a write buffer (when "
+                "--inw=WF is\n"
+                "                        not given)\n"
+                "    --inc=IF|-C IF      The same as the --in option\n"
+                "    --inw=WF|-D WF      WF is a file containing a write "
+                "buffer\n"
+                "    --lba=LBA|-l LBA    LBA of the first block to compare "
+                "and write\n"
+                "    --num=NUM|-n NUM    number of blocks to "
+                "compare/write (def: 1)\n"
+                "    --quiet|-q          suppress MISCOMPARE report to "
+                "stderr,\n"
+                "                        still sets exit status of 14\n"
+                "    --timeout=TO|-t TO    timeout for the command "
+                "(def: 60 secs)\n"
+                "    --verbose|-v        increase verbosity (use '-vv' for "
+                "more)\n"
+                "    --version|-V        print version string then exit\n"
+                "    --wrprotect=WP|-w WP    write protect information "
+                "(def: 0)\n"
+                "    --xferlen=LEN|-x LEN    number of bytes to transfer. "
+                "Default is\n"
+                "                            (2 * NUM * 512) or 1024 when "
+                "NUM is 1\n"
+                "\n"
+                "Performs a SCSI COMPARE AND WRITE operation. Sends a double "
+                "size\nbuffer, the first half is used to compare what is at "
+                "LBA for NUM\nblocks. If and only if the comparison is "
+                "equal, then the second\nhalf of the buffer is written to "
+                "LBA for NUM blocks.\n");
+}
+
+static int
+parse_args(int argc, char* argv[], struct opts_t * op)
+{
+        bool lba_given = false;
+        bool if_given = false;
+        int c;
+        int64_t ll;
+
+        op->numblocks = DEF_NUM_BLOCKS;
+        /* COMPARE AND WRITE defines 2*buffers compare + write */
+        op->xfer_len = 0;
+        op->timeout = DEF_TIMEOUT_SECS;
+        op->device_name = NULL;
+        while (1) {
+                int option_index = 0;
+
+                c = getopt_long(argc, argv, "C:dD:fFg:hi:l:n:qt:vVw:x:",
+                                long_options, &option_index);
+                if (c == -1)
+                        break;
+
+                switch (c) {
+                case 'C':
+                case 'i':
+                        op->ifn = optarg;
+                        if_given = true;
+                        break;
+                case 'd':
+                        op->flags.dpo = true;
+                        break;
+                case 'D':
+                        op->wfn = optarg;
+                        op->wfn_given = true;
+                        break;
+                case 'F':
+                        op->flags.fua_nv = true;
+                        break;
+                case 'f':
+                        op->flags.fua = true;
+                        break;
+                case 'g':
+                        op->flags.group = sg_get_num(optarg);
+                        if ((op->flags.group < 0) ||
+                            (op->flags.group > 63))  {
+                                pr2serr("argument to '--grpnum=' expected to "
+                                        "be 0 to 63\n");
+                                goto out_err_no_usage;
+                        }
+                        break;
+                case 'h':
+                case '?':
+                        usage();
+                        exit(0);
+                case 'l':
+                        ll = sg_get_llnum(optarg);
+                        if (-1 == ll) {
+                                pr2serr("bad argument to '--lba'\n");
+                                goto out_err_no_usage;
+                        }
+                        op->lba = (uint64_t)ll;
+                        lba_given = true;
+                        break;
+                case 'n':
+                        op->numblocks = sg_get_num(optarg);
+                        if ((op->numblocks < 0) || (op->numblocks > 255))  {
+                                pr2serr("bad argument to '--num', expect 0 "
+                                        "to 255\n");
+                                goto out_err_no_usage;
+                        }
+                        break;
+                case 'q':
+                        op->quiet = true;
+                        break;
+                case 't':
+                        op->timeout = sg_get_num(optarg);
+                        if (op->timeout < 0)  {
+                                pr2serr("bad argument to '--timeout'\n");
+                                goto out_err_no_usage;
+                        }
+                        break;
+                case 'v':
+                        op->verbose_given = true;
+                        ++op->verbose;
+                        break;
+                case 'V':
+                        op->version_given = true;
+                        break;
+                case 'w':
+                        op->flags.wrprotect = sg_get_num(optarg);
+                        if (op->flags.wrprotect >> 3) {
+                                pr2serr("bad argument to '--wrprotect' not "
+                                        "in range 0-7\n");
+                                goto out_err_no_usage;
+                        }
+                        break;
+                case 'x':
+                        op->xfer_len = sg_get_num(optarg);
+                        if (op->xfer_len < 0) {
+                                pr2serr("bad argument to '--xferlen'\n");
+                                goto out_err_no_usage;
+                        }
+                        break;
+                default:
+                        pr2serr("unrecognised option code 0x%x ??\n", c);
+                        goto out_err;
+                }
+        }
+        if (optind < argc) {
+                if (NULL == op->device_name) {
+                        op->device_name = argv[optind];
+                        ++optind;
+                }
+                if (optind < argc) {
+                        for (; optind < argc; ++optind)
+                                pr2serr("Unexpected extra argument: %s\n",
+                                        argv[optind]);
+                        goto out_err;
+                }
+        }
+        if (op->version_given && (! op->verbose_given))
+                return 0;
+        if (NULL == op->device_name) {
+                pr2serr("missing device name!\n");
+                goto out_err;
+        }
+        if (! if_given) {
+                pr2serr("missing input file\n");
+                goto out_err;
+        }
+        if (! lba_given) {
+                pr2serr("missing lba\n");
+                goto out_err;
+        }
+        if (0 == op->xfer_len)
+            op->xfer_len = 2 * op->numblocks * DEF_BLOCK_SIZE;
+        return 0;
+
+out_err:
+        usage();
+
+out_err_no_usage:
+        exit(1);
+}
+
+#define FLAG_FUA        (0x8)
+#define FLAG_FUA_NV     (0x2)
+#define FLAG_DPO        (0x10)
+#define WRPROTECT_MASK  (0x7)
+#define WRPROTECT_SHIFT (5)
+
+static int
+sg_build_scsi_cdb(uint8_t * cdbp, unsigned int blocks,
+                  int64_t start_block, struct caw_flags flags)
+{
+        memset(cdbp, 0, COMPARE_AND_WRITE_CDB_SIZE);
+        cdbp[0] = COMPARE_AND_WRITE_OPCODE;
+        cdbp[1] = (flags.wrprotect & WRPROTECT_MASK) << WRPROTECT_SHIFT;
+        if (flags.dpo)
+                cdbp[1] |= FLAG_DPO;
+        if (flags.fua)
+                cdbp[1] |= FLAG_FUA;
+        if (flags.fua_nv)
+                cdbp[1] |= FLAG_FUA_NV;
+        sg_put_unaligned_be64((uint64_t)start_block, cdbp + 2);
+        /* cdbp[10-12] are reserved */
+        cdbp[13] = (uint8_t)(blocks & 0xff);
+        cdbp[14] = (uint8_t)(flags.group & GRPNUM_MASK);
+        return 0;
+}
+
+/* Returns 0 for success, SG_LIB_CAT_MISCOMPARE if compare fails,
+ * various other SG_LIB_CAT_*, otherwise -1 . */
+static int
+sg_ll_compare_and_write(int sg_fd, uint8_t * buff, int blocks,
+                        int64_t lba, int xfer_len, struct caw_flags flags,
+                        bool noisy, int verbose)
+{
+        bool valid;
+        int sense_cat, slen, res, ret;
+        uint64_t ull = 0;
+        struct sg_pt_base * ptvp;
+        uint8_t cawCmd[COMPARE_AND_WRITE_CDB_SIZE];
+        uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+        if (sg_build_scsi_cdb(cawCmd, blocks, lba, flags)) {
+                pr2serr(ME "bad cdb build, lba=0x%" PRIx64 ", blocks=%d\n",
+                        lba, blocks);
+                return -1;
+        }
+        ptvp = construct_scsi_pt_obj();
+        if (NULL == ptvp) {
+                pr2serr("Could not construct scsit_pt_obj, out of memory\n");
+                return -1;
+        }
+
+        set_scsi_pt_cdb(ptvp, cawCmd, COMPARE_AND_WRITE_CDB_SIZE);
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+        set_scsi_pt_data_out(ptvp, buff, xfer_len);
+        if (verbose > 1) {
+                char b[128];
+
+                pr2serr("    Compare and write cdb: %s\n",
+                sg_get_command_str(cawCmd, COMPARE_AND_WRITE_CDB_SIZE, false,
+                                   sizeof(b), b));
+        }
+        if ((verbose > 2) && (xfer_len > 0)) {
+                pr2serr("    Data-out buffer contents:\n");
+                hex2stderr(buff, xfer_len, 1);
+        }
+        res = do_scsi_pt(ptvp, sg_fd, DEF_TIMEOUT_SECS, verbose);
+        ret = sg_cmds_process_resp(ptvp, "COMPARE AND WRITE", res,
+                                   noisy, verbose, &sense_cat);
+        if (-1 == ret) {
+            if (get_scsi_pt_transport_err(ptvp))
+                ret = SG_LIB_TRANSPORT_ERROR;
+            else
+                ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+        } else if (-2 == ret) {
+                switch (sense_cat) {
+                case SG_LIB_CAT_RECOVERED:
+                case SG_LIB_CAT_NO_SENSE:
+                        ret = 0;
+                        break;
+                case SG_LIB_CAT_MEDIUM_HARD:
+                        slen = get_scsi_pt_sense_len(ptvp);
+                        valid = sg_get_sense_info_fld(sense_b, slen,
+                                                      &ull);
+                        if (valid)
+                                pr2serr("Medium or hardware error starting "
+                                        "at lba=%" PRIu64 " [0x%" PRIx64
+                                        "]\n", ull, ull);
+                        else
+                                pr2serr("Medium or hardware error\n");
+                        ret = sense_cat;
+                        break;
+                case SG_LIB_CAT_MISCOMPARE:
+                        ret = sense_cat;
+                        if (! (noisy || verbose))
+                                break;
+                        slen = get_scsi_pt_sense_len(ptvp);
+                        valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                        if (valid)
+                                pr2serr("Miscompare at byte offset: %" PRIu64
+                                        " [0x%" PRIx64 "]\n", ull, ull);
+                        else
+                                pr2serr("Miscompare reported\n");
+                        break;
+                case SG_LIB_CAT_ILLEGAL_REQ:
+                        if (verbose)
+                                sg_print_command_len(cawCmd,
+                                             COMPARE_AND_WRITE_CDB_SIZE);
+                        /* FALL THROUGH */
+                default:
+                        ret = sense_cat;
+                        break;
+                }
+        } else
+                ret = 0;
+
+        destruct_scsi_pt_obj(ptvp);
+        return ret;
+}
+
+static int
+open_if(const char * fn, bool got_stdin)
+{
+        int fd;
+
+        if (got_stdin)
+                fd = STDIN_FILENO;
+        else {
+                fd = open(fn, O_RDONLY);
+                if (fd < 0) {
+                        pr2serr(ME "open error: %s: %s\n", fn,
+                                safe_strerror(errno));
+                        return -SG_LIB_FILE_ERROR;
+                }
+        }
+        if (sg_set_binary_mode(fd) < 0) {
+                perror("sg_set_binary_mode");
+                return -SG_LIB_FILE_ERROR;
+        }
+        return fd;
+}
+
+static int
+open_dev(const char * outf, int verbose)
+{
+        int sg_fd = sg_cmds_open_device(outf, false /* rw */, verbose);
+
+        if ((sg_fd < 0) && verbose)
+                pr2serr(ME "open error: %s: %s\n", outf,
+                        safe_strerror(-sg_fd));
+        return sg_fd;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+        bool ifn_stdin;
+        int res, half_xlen, vb;
+        int infd = -1;
+        int wfd = -1;
+        int devfd = -1;
+        uint8_t * wrkBuff = NULL;
+        uint8_t * free_wrkBuff = NULL;
+        struct opts_t * op;
+        struct opts_t opts;
+
+        op = &opts;
+        memset(op, 0, sizeof(opts));
+        res = parse_args(argc, argv, op);
+        if (res != 0) {
+                pr2serr("Failed parsing args\n");
+                goto out;
+        }
+
+#ifdef DEBUG
+        pr2serr("In DEBUG mode, ");
+        if (op->verbose_given && op->version_given) {
+                pr2serr("but override: '-vV' given, zero verbose and "
+                        "continue\n");
+                op->verbose_given = false;
+                op->version_given = false;
+                op->verbose = 0;
+        } else if (! op->verbose_given) {
+                pr2serr("set '-vv'\n");
+                op->verbose = 2;
+        } else
+                pr2serr("keep verbose=%d\n", op->verbose);
+#else
+        if (op->verbose_given && op->version_given)
+                pr2serr("Not in DEBUG mode, so '-vV' has no special "
+                        "action\n");
+#endif
+        if (op->version_given) {
+                pr2serr(ME "version: %s\n", version_str);
+                return 0;
+        }
+        vb = op->verbose;
+
+        if (vb) {
+                pr2serr("Running COMPARE AND WRITE command with the "
+                        "following options:\n  in=%s ", op->ifn);
+                if (op->wfn_given)
+                        pr2serr("inw=%s ", op->wfn);
+                pr2serr("device=%s\n  lba=0x%" PRIx64 " num_blocks=%d "
+                        "xfer_len=%d timeout=%d\n", op->device_name,
+                        op->lba, op->numblocks, op->xfer_len, op->timeout);
+        }
+        ifn_stdin = ((1 == strlen(op->ifn)) && ('-' == op->ifn[0]));
+        infd = open_if(op->ifn, ifn_stdin);
+        if (infd < 0) {
+                res = -infd;
+                goto out;
+        }
+        if (op->wfn_given) {
+                if ((1 == strlen(op->wfn)) && ('-' == op->wfn[0])) {
+                        pr2serr(ME "don't allow stdin for write file\n");
+                        res = SG_LIB_FILE_ERROR;
+                        goto out;
+                }
+                wfd = open_if(op->wfn, false);
+                if (wfd < 0) {
+                        res = -wfd;
+                        goto out;
+                }
+        }
+
+        devfd = open_dev(op->device_name, vb);
+        if (devfd < 0) {
+                res = sg_convert_errno(-devfd);
+                goto out;
+        }
+
+        wrkBuff = (uint8_t *)sg_memalign(op->xfer_len, 0, &free_wrkBuff,
+                                         vb > 3);
+        if (NULL == wrkBuff) {
+                pr2serr("Not enough user memory\n");
+                res = sg_convert_errno(ENOMEM);
+                goto out;
+        }
+
+        if (op->wfn_given) {
+                half_xlen = op->xfer_len / 2;
+                res = read(infd, wrkBuff, half_xlen);
+                if (res < 0) {
+                        pr2serr("Could not read from %s", op->ifn);
+                        goto out;
+                } else if (res < half_xlen) {
+                        pr2serr("Read only %d bytes (expected %d) from %s\n",
+                                res, half_xlen, op->ifn);
+                        goto out;
+                }
+                res = read(wfd, wrkBuff + half_xlen, half_xlen);
+                if (res < 0) {
+                        pr2serr("Could not read from %s", op->wfn);
+                        goto out;
+                } else if (res < half_xlen) {
+                        pr2serr("Read only %d bytes (expected %d) from %s\n",
+                                res, half_xlen, op->wfn);
+                        goto out;
+                }
+        } else {
+                res = read(infd, wrkBuff, op->xfer_len);
+                if (res < 0) {
+                        pr2serr("Could not read from %s", op->ifn);
+                        goto out;
+                } else if (res < op->xfer_len) {
+                        pr2serr("Read only %d bytes (expected %d) from %s\n",
+                                res, op->xfer_len, op->ifn);
+                        goto out;
+                }
+        }
+        res = sg_ll_compare_and_write(devfd, wrkBuff, op->numblocks, op->lba,
+                                      op->xfer_len, op->flags, ! op->quiet,
+                                      vb);
+        if (0 != res) {
+                char b[80];
+
+                switch (res) {
+                case SG_LIB_CAT_MEDIUM_HARD:
+                case SG_LIB_CAT_MISCOMPARE:
+                case SG_LIB_FILE_ERROR:
+                        break;  /* already reported */
+                default:
+                        sg_get_category_sense_str(res, sizeof(b), b, vb);
+                        pr2serr(ME "SCSI COMPARE AND WRITE: %s\n", b);
+                        break;
+                }
+        }
+out:
+        if (free_wrkBuff)
+                free(free_wrkBuff);
+        if ((infd >= 0) && (! ifn_stdin))
+                close(infd);
+        if (wfd >= 0)
+                close(wfd);
+        if (devfd >= 0)
+                close(devfd);
+        if (0 == op->verbose) {
+                if (! sg_if_can2stderr("sg_compare_and_write failed: ", res))
+                        pr2serr("Some error occurred, try again with '-v' "
+                                "or '-vv' for more information\n");
+        }
+        return res;
+}
diff --git a/src/sg_copy_results.c b/src/sg_copy_results.c
new file mode 100644
index 0000000..17012be
--- /dev/null
+++ b/src/sg_copy_results.c
@@ -0,0 +1,502 @@
+/*
+ * Copyright (c) 2011-2018 Hannes Reinecke, SUSE Labs
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ */
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/*
+ * A utility program for the Linux OS SCSI subsystem.
+ *  Copyright (C) 2004-2010 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program issues the SCSI command RECEIVE COPY RESULTS to a given
+ * SCSI device.
+ * It sends the command with the service action passed as the sa argument,
+ * and the optional list identifier passed as the list_id argument.
+ */
+
+static const char * version_str = "1.23 20180625";
+
+
+#define MAX_XFER_LEN 10000
+
+
+#define ME "sg_copy_results: "
+
+#define EBUFF_SZ 256
+
+struct descriptor_type {
+    int code;
+    char desc[124];
+};
+
+struct descriptor_type target_descriptor_codes[] = {
+    { 0xe0, "Fibre Channel N_Port_Name"},
+    { 0xe1, "Fibre Channel N_port_ID"},
+    { 0xe2, "Fibre Channesl N_port_ID with N_Port_Name checking"},
+    { 0xe3, "Parallel Interface T_L" },
+    { 0xe4, "Identification descriptor" },
+    { 0xe5, "IPv4" },
+    { 0xe6, "Alias" },
+    { 0xe7, "RDMA" },
+    { 0xe8, "IEEE 1395 EUI-64" },
+    { 0xe9, "SAS Serial SCSI Protocol" },
+    { 0xea, "IPv6" },
+    { 0xeb, "IP Copy Service" },
+    { -1, "" }
+};
+
+struct descriptor_type segment_descriptor_codes [] = {
+    { 0x00, "Copy from block device to stream device" },
+    { 0x01, "Copy from stream device to block device" },
+    { 0x02, "Copy from block device to block device" },
+    { 0x03, "Copy from stream device to stream device" },
+    { 0x04, "Copy inline data to stream device" },
+    { 0x05, "Copy embedded data to stream device" },
+    { 0x06, "Read from stream device and discard" },
+    { 0x07, "Verify block or stream device operation" },
+    { 0x08, "Copy block device with offset to stream device" },
+    { 0x09, "Copy stream device to block device with offset" },
+    { 0x0A, "Copy block device with offset to block device with offset" },
+    { 0x0B, "Copy from block device to stream device "
+      "and hold a copy of processed data for the application client" },
+    { 0x0C, "Copy from stream device to block device "
+      "and hold a copy of processed data for the application client" },
+    { 0x0D, "Copy from block device to block device "
+      "and hold a copy of processed data for the application client" },
+    { 0x0E, "Copy from stream device to stream device "
+      "and hold a copy of processed data for the application client" },
+    { 0x0F, "Read from stream device "
+      "and hold a copy of processed data for the application client" },
+    { 0x10, "Write filemarks to sequential-access device" },
+    { 0x11, "Space records or filemarks on sequential-access device" },
+    { 0x12, "Locate on sequential-access device" },
+    { 0x13, "Image copy from sequential-access device to sequential-access "
+            "device" },
+    { 0x14, "Register persistent reservation key" },
+    { 0x15, "Third party persistent reservations source I_T nexus" },
+    { -1, "" }
+};
+
+
+static void
+scsi_failed_segment_details(uint8_t *rcBuff, unsigned int rcBuffLen)
+{
+    int senseLen;
+    unsigned int len;
+    char senseBuff[1024];
+
+    if (rcBuffLen < 4) {
+        pr2serr("  <<not enough data to procedd report>>\n");
+        return;
+    }
+    len = sg_get_unaligned_be32(rcBuff + 0);
+    if (len + 4 > rcBuffLen) {
+        pr2serr("  <<report len %d > %d too long for internal buffer, output "
+                "truncated\n", len, rcBuffLen);
+    }
+    if (len < 52) {
+        pr2serr("  <<no segment details, response data length %d\n", len);
+        return;
+    }
+    printf("Receive copy results (failed segment details):\n");
+    printf("    Extended copy command status: %d\n", rcBuff[56]);
+    senseLen = sg_get_unaligned_be16(rcBuff + 58);
+    sg_get_sense_str("    ", &rcBuff[60], senseLen, 0, 1024, senseBuff);
+    printf("%s", senseBuff);
+}
+
+static void
+scsi_copy_status(uint8_t *rcBuff, unsigned int rcBuffLen)
+{
+    unsigned int len;
+
+    if (rcBuffLen < 4) {
+        pr2serr("  <<not enough data to proceed report>>\n");
+        return;
+    }
+    len = sg_get_unaligned_be32(rcBuff + 0);
+    if (len + 4 > rcBuffLen) {
+        pr2serr("  <<report len %d > %d too long for internal buffer, output "
+                "truncated\n", len, rcBuffLen);
+    }
+    printf("Receive copy results (copy status):\n");
+    printf("    Held data discarded: %s\n", rcBuff[4] & 0x80 ? "Yes":"No");
+    printf("    Copy manager status: ");
+    switch (rcBuff[4] & 0x7f) {
+    case 0:
+        printf("Operation in progress\n");
+        break;
+    case 1:
+        printf("Operation completed without errors\n");
+        break;
+    case 2:
+        printf("Operation completed with errors\n");
+        break;
+    default:
+        printf("Unknown/Reserved\n");
+        break;
+    }
+    printf("    Segments processed: %u\n", sg_get_unaligned_be16(rcBuff + 5));
+    printf("    Transfer count units: %u\n", rcBuff[7]);
+    printf("    Transfer count: %u\n", sg_get_unaligned_be32(rcBuff + 8));
+}
+
+static void
+scsi_operating_parameters(uint8_t *rcBuff, unsigned int rcBuffLen)
+{
+    unsigned int len, n;
+
+    len = sg_get_unaligned_be32(rcBuff + 0);
+    if (len + 4 > rcBuffLen) {
+        pr2serr("  <<report len %d > %d too long for internal buffer, output "
+                "truncated\n", len, rcBuffLen);
+    }
+    printf("Receive copy results (report operating parameters):\n");
+    printf("    Supports no list identifier (SNLID): %s\n",
+           rcBuff[4] & 1 ? "yes" : "no");
+    n = sg_get_unaligned_be16(rcBuff + 8);
+    printf("    Maximum target descriptor count: %u\n", n);
+    n = sg_get_unaligned_be16(rcBuff + 10);
+    printf("    Maximum segment descriptor count: %u\n", n);
+    n = sg_get_unaligned_be32(rcBuff + 12);
+    printf("    Maximum descriptor list length: %u bytes\n", n);
+    n = sg_get_unaligned_be32(rcBuff + 16);
+    printf("    Maximum segment length: %u bytes\n", n);
+    n = sg_get_unaligned_be32(rcBuff + 20);
+    if (n == 0) {
+        printf("    Inline data not supported\n");
+    } else {
+        printf("    Maximum inline data length: %u bytes\n", n);
+    }
+    n = sg_get_unaligned_be32(rcBuff + 24);
+    printf("    Held data limit: %u bytes\n", n);
+    n = sg_get_unaligned_be32(rcBuff + 28);
+    printf("    Maximum stream device transfer size: %u bytes\n", n);
+    n = sg_get_unaligned_be16(rcBuff + 34);
+    printf("    Total concurrent copies: %u\n", n);
+    printf("    Maximum concurrent copies: %u\n", rcBuff[36]);
+    if (rcBuff[37] > 30)
+        printf("    Data segment granularity: 2**%u bytes\n", rcBuff[37]);
+    else
+        printf("    Data segment granularity: %u bytes\n",
+               (unsigned int)(1 << rcBuff[37]));
+    if (rcBuff[38] > 30)
+        printf("    Inline data granularity: %u bytes\n", rcBuff[38]);
+    else
+        printf("    Inline data granularity: %u bytes\n",
+               (unsigned int)(1 << rcBuff[38]));
+    if (rcBuff[39] > 30)
+        printf("    Held data granularity: 2**%u bytes\n", rcBuff[39]);
+    else
+        printf("    Held data granularity: %u bytes\n",
+               (unsigned int)(1 << rcBuff[39]));
+
+    printf("    Implemented descriptor list:\n");
+    for (n = 0; n < rcBuff[43]; n++) {
+        int code = rcBuff[44 + n];
+
+        if (code < 0x16) {
+            struct descriptor_type *seg_desc = segment_descriptor_codes;
+            while (strlen(seg_desc->desc)) {
+                if (seg_desc->code == code)
+                    break;
+                seg_desc++;
+            }
+            printf("        Segment descriptor 0x%02x: %s\n", code,
+                   strlen(seg_desc->desc) ? seg_desc->desc : "Reserved");
+        } else if (code < 0xc0) {
+            printf("        Segment descriptor 0x%02x: Reserved\n", code);
+        } else if (code < 0xe0) {
+            printf("        Vendor specific descriptor 0x%02x\n", code);
+        } else {
+            struct descriptor_type *tgt_desc = target_descriptor_codes;
+
+            while (strlen(tgt_desc->desc)) {
+                if (tgt_desc->code == code)
+                    break;
+                tgt_desc++;
+            }
+            printf("        Target descriptor 0x%02x: %s\n", code,
+                   strlen(tgt_desc->desc) ? tgt_desc->desc : "Reserved");
+        }
+    }
+    printf("\n");
+}
+
+static struct option long_options[] = {
+        {"failed", no_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"list_id", required_argument, 0, 'l'},
+        {"list-id", required_argument, 0, 'l'},
+        {"params", no_argument, 0, 'p'},
+        {"readonly", no_argument, 0, 'R'},
+        {"receive", no_argument, 0, 'r'},
+        {"status", no_argument, 0, 's'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"xfer_len", required_argument, 0, 'x'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+  pr2serr("Usage: "
+          "sg_copy_results [--failed|--params|--receive|--status] [--help]\n"
+          "                       [--hex] [--list_id=ID] [--readonly] "
+          "[--verbose]\n"
+          "                       [--version] [--xfer_len=BTL] DEVICE\n"
+          "  where:\n"
+          "    --failed|-f          use FAILED SEGMENT DETAILS service "
+          "action\n"
+          "    --help|-h            print out usage message\n"
+          "    --hex|-H             print out response buffer in hex\n"
+          "    --list_id=ID|-l ID   list identifier (default: 0)\n"
+          "    --params|-p          use OPERATING PARAMETERS service "
+          "action\n"
+          "    --readonly|-R        open DEVICE read-only (def: read-write)\n"
+          "    --receive|-r         use RECEIVE DATA service action\n"
+          "    --status|-s          use COPY STATUS service action\n"
+          "    --verbose|-v         increase verbosity\n"
+          "    --version|-V         print version string then exit\n"
+          "    --xfer_len=BTL|-x BTL    byte transfer length (< 10000) "
+          "(default:\n"
+          "                             520 bytes)\n\n"
+          "Performs a SCSI RECEIVE COPY RESULTS command. Returns the "
+          "response as\nspecified by the service action parameters.\n"
+          );
+}
+
+static const char * rec_copy_name_arr[] = {
+    "Receive copy status(LID1)",
+    "Receive copy data(LID1)",
+    "Receive copy [0x2]",
+    "Receive copy operating parameters",
+    "Receive copy failure details(LID1)",
+};
+
+int
+main(int argc, char * argv[])
+{
+    bool do_hex = false;
+    bool o_readonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, k;
+    int ret = 1;
+    int sa = 3;
+    int sg_fd = -1;
+    int verbose = 0;
+    int xfer_len = 520;
+    uint32_t list_id = 0;
+    const char * cp;
+    uint8_t * cpResultBuff = NULL;
+    uint8_t * free_cprb = NULL;
+    const char * device_name = NULL;
+    char file_name[256];
+
+    memset(file_name, 0, sizeof file_name);
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "fhHl:prRsvVx:", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'f':
+            sa = 4;
+            break;
+        case 'H':
+            do_hex = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'l':
+            k = sg_get_num(optarg);
+            if (-1 == k) {
+                pr2serr("bad argument to '--list_id'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            list_id = (uint32_t)k;
+            break;
+        case 'p':
+            sa = 3;
+            break;
+        case 'r':
+            sa = 1;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 's':
+            sa = 0;
+            break;
+        case 'v':
+            ++verbose;
+            verbose_given = true;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        case 'x':
+            xfer_len = sg_get_num(optarg);
+            if (-1 == xfer_len) {
+                pr2serr("bad argument to '--xfer_len'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (xfer_len >= MAX_XFER_LEN) {
+        pr2serr("xfer_len (%d) is out of range ( < %d)\n", xfer_len,
+                MAX_XFER_LEN);
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    cpResultBuff = (uint8_t *)sg_memalign(xfer_len, 0, &free_cprb,
+                                          verbose > 3);
+    if (NULL == cpResultBuff) {
+            pr2serr(ME "out of memory\n");
+            return sg_convert_errno(ENOMEM);
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto finish;
+    }
+
+    if ((sa < 0) || (sa >= (int)SG_ARRAY_SIZE(rec_copy_name_arr)))
+        cp = "Out of range service action";
+    else
+        cp = rec_copy_name_arr[sa];
+    if (verbose)
+        pr2serr(ME "issue %s to device %s\n\t\txfer_len= %d (0x%x), list_id=%"
+                PRIu32 "\n", cp, device_name, xfer_len, xfer_len, list_id);
+
+    res = sg_ll_receive_copy_results(sg_fd, sa, list_id, cpResultBuff,
+                                     xfer_len, true, verbose);
+    ret = res;
+    if (res) {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("  SCSI %s failed: %s\n", cp, b);
+        goto finish;
+    }
+    if (do_hex) {
+        hex2stdout(cpResultBuff, xfer_len, 1);
+        goto finish;
+    }
+    switch (sa) {
+    case 4: /* Failed segment details */
+        scsi_failed_segment_details(cpResultBuff, xfer_len);
+        break;
+    case 3: /* Operating parameters */
+        scsi_operating_parameters(cpResultBuff, xfer_len);
+        break;
+    case 0: /* Copy status */
+        scsi_copy_status(cpResultBuff, xfer_len);
+        break;
+    default:
+        hex2stdout(cpResultBuff, xfer_len, 1);
+        break;
+    }
+
+finish:
+    if (free_cprb)
+        free(free_cprb);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr(ME "close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_copy_results failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_dd.c b/src/sg_dd.c
new file mode 100644
index 0000000..e099c33
--- /dev/null
+++ b/src/sg_dd.c
@@ -0,0 +1,2750 @@
+/* A utility program for copying files. Specialised for "files" that
+ * represent devices that understand the SCSI command set.
+ *
+ * Copyright (C) 1999 - 2022 D. Gilbert and P. Allworth
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is a specialisation of the Unix "dd" command in which
+ * either the input or the output file is a scsi generic device, raw
+ * device, a block device or a normal file. The logical block size ('bs')
+ * is assumed to be 512 if not given. This program complains if 'ibs' or
+ * 'obs' are given with a value that differs from 'bs' (or the default 512).
+ * If 'if' is not given or 'if=-' then stdin is assumed. If 'of' is
+ * not given or 'of=-' then stdout assumed.
+ *
+ * A non-standard argument "bpt" (blocks per transfer) is added to control
+ * the maximum number of blocks in each transfer. The default value is 128.
+ * For example if "bs=512" and "bpt=32" then a maximum of 32 blocks (16 KiB
+ * in this case) is transferred to or from the sg device in a single SCSI
+ * command. The actual size of the SCSI READ or WRITE command block can be
+ * selected with the "cdbsz" argument.
+ *
+ * This version is designed for the Linux kernel 2, 3, 4 and 5 series.
+ */
+
+#define _XOPEN_SOURCE 600
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <signal.h>
+#include <ctype.h>
+#include <errno.h>
+#include <time.h>
+#include <limits.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/file.h>
+#include <sys/sysmacros.h>
+#ifndef major
+#include <sys/types.h>
+#endif
+#include <linux/major.h>        /* for MEM_MAJOR, SCSI_GENERIC_MAJOR, etc */
+#include <linux/fs.h>           /* for BLKSSZGET and friends */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#ifdef HAVE_GETRANDOM
+#include <sys/random.h>         /* for getrandom() system call */
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "6.35 20220826";
+
+
+#define ME "sg_dd: "
+
+
+#define STR_SZ 1024
+#define INOUTF_SZ 512
+#define EBUFF_SZ 768
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BLOCKS_PER_TRANSFER 128
+#define DEF_BLOCKS_PER_2048TRANSFER 32
+#define DEF_SCSI_CDBSZ 10
+#define MAX_SCSI_CDBSZ 16
+#define MAX_BPT_VALUE (1 << 24)         /* used for maximum bs as well */
+#define MAX_COUNT_SKIP_SEEK (1LL << 48) /* coverity wants upper bound */
+
+#define DEF_MODE_CDB_SZ 10
+#define DEF_MODE_RESP_LEN 252
+#define RW_ERR_RECOVERY_MP 1
+#define CACHING_MP 8
+#define CONTROL_MP 0xa
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define READ_CAP_REPLY_LEN 8
+#define RCAP16_REPLY_LEN 32
+#define READ_LONG_OPCODE 0x3E
+#define READ_LONG_CMD_LEN 10
+#define READ_LONG_DEF_BLK_INC 8
+#define VERIFY10 0x2f
+#define VERIFY12 0xaf
+#define VERIFY16 0x8f
+
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs == 60 seconds */
+
+#ifndef RAW_MAJOR
+#define RAW_MAJOR 255   /*unlikely value */
+#endif
+
+#define SG_LIB_FLOCK_ERR 90
+
+#define FT_OTHER 1              /* filetype is probably normal */
+#define FT_SG 2                 /* filetype is sg char device or supports
+                                   SG_IO ioctl */
+#define FT_RAW 4                /* filetype is raw char device */
+#define FT_DEV_NULL 8           /* either "/dev/null" or "." as filename */
+#define FT_ST 16                /* filetype is st char device (tape) */
+#define FT_BLOCK 32             /* filetype is block device */
+#define FT_FIFO 64              /* filetype is a fifo (name pipe) */
+#define FT_NVME 128             /* NVMe char device (e.g. /dev/nvme2) */
+#define FT_RANDOM_0_FF 256      /* iflag=00, iflag=ff and iflag=random
+                                   overriding if=IFILE */
+#define FT_ERROR 512            /* couldn't "stat" file */
+
+#define DEV_NULL_MINOR_NUM 3
+
+#define SG_DD_BYPASS 999        /* failed but coe set */
+
+/* If platform does not support O_DIRECT then define it harmlessly */
+#ifndef O_DIRECT
+#define O_DIRECT 0
+#endif
+
+#define MIN_RESERVED_SIZE 8192
+
+#define MAX_UNIT_ATTENTIONS 10
+#define MAX_ABORTED_CMDS 256
+
+#define PROGRESS_TRIGGER_MS 120000      /* milliseconds: 2 minutes */
+#define PROGRESS2_TRIGGER_MS 60000      /* milliseconds: 1 minute */
+#define PROGRESS3_TRIGGER_MS 30000      /* milliseconds: 30 seconds */
+
+static int sum_of_resids = 0;
+
+static int64_t dd_count = -1;
+static int64_t req_count = 0;
+static int64_t in_full = 0;
+static int in_partial = 0;
+static int64_t out_full = 0;
+static int out_partial = 0;
+static int64_t out_sparse_num = 0;
+static int recovered_errs = 0;
+static int unrecovered_errs = 0;
+static int miscompare_errs = 0;
+static int read_longs = 0;
+static int num_retries = 0;
+static int progress = 0;
+static int dry_run = 0;
+
+static bool do_time = false;
+static bool start_tm_valid = false;
+static bool do_verify = false;          /* when false: do copy */
+static int verbose = 0;
+static int blk_sz = 0;
+static int max_uas = MAX_UNIT_ATTENTIONS;
+static int max_aborted = MAX_ABORTED_CMDS;
+static int coe_limit = 0;
+static int coe_count = 0;
+static int cmd_timeout = DEF_TIMEOUT;   /* in milliseconds */
+static uint32_t glob_pack_id = 0;       /* pre-increment */
+static struct timeval start_tm;
+
+static uint8_t * zeros_buff = NULL;
+static uint8_t * free_zeros_buff = NULL;
+static int read_long_blk_inc = READ_LONG_DEF_BLK_INC;
+
+static long seed;
+#ifdef HAVE_SRAND48_R   /* gcc extension. N.B. non-reentrant version slower */
+static struct drand48_data drand;/* opaque, used by srand48_r and mrand48_r */
+#endif
+
+static const char * sg_allow_dio = "/sys/module/sg/parameters/allow_dio";
+
+struct flags_t {
+    bool append;
+    bool dio;
+    bool direct;
+    bool dpo;
+    bool dsync;
+    bool excl;
+    bool flock;
+    bool ff;
+    bool fua;
+    bool nocreat;
+    bool random;
+    bool sgio;
+    bool sparse;
+    bool zero;
+    int cdbsz;
+    int cdl;
+    int coe;
+    int nocache;
+    int pdt;
+    int retries;
+};
+
+static struct flags_t iflag;
+static struct flags_t oflag;
+
+static void calc_duration_throughput(bool contin);
+
+
+static void
+install_handler(int sig_num, void (*sig_handler)(int sig))
+{
+    struct sigaction sigact;
+
+    sigaction(sig_num, NULL, &sigact);
+    if (sigact.sa_handler != SIG_IGN) {
+        sigact.sa_handler = sig_handler;
+        sigemptyset(&sigact.sa_mask);
+        sigact.sa_flags = 0;
+        sigaction(sig_num, &sigact, NULL);
+    }
+}
+
+
+static void
+print_stats(const char * str)
+{
+    if (0 != dd_count)
+        pr2serr("  remaining block count=%" PRId64 "\n", dd_count);
+    pr2serr("%s%" PRId64 "+%d records in\n", str, in_full - in_partial,
+            in_partial);
+    pr2serr("%s%" PRId64 "+%d records %s\n", str, out_full - out_partial,
+            out_partial, (do_verify ? "verified" : "out"));
+    if (oflag.sparse)
+        pr2serr("%s%" PRId64 " bypassed records out\n", str, out_sparse_num);
+    if (recovered_errs > 0)
+        pr2serr("%s%d recovered errors\n", str, recovered_errs);
+    if (num_retries > 0)
+        pr2serr("%s%d retries attempted\n", str, num_retries);
+    if (unrecovered_errs > 0) {
+        pr2serr("%s%d unrecovered error(s)\n", str, unrecovered_errs);
+        if (iflag.coe || oflag.coe)
+            pr2serr("%s%d read_longs fetched part of unrecovered read "
+                    "errors\n", str, read_longs);
+    }
+    if (miscompare_errs > 0)
+        pr2serr("%s%d miscompare error(s)\n", str, miscompare_errs);
+}
+
+
+static void
+interrupt_handler(int sig)
+{
+    struct sigaction sigact;
+
+    sigact.sa_handler = SIG_DFL;
+    sigemptyset(&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigaction(sig, &sigact, NULL);
+    pr2serr("Interrupted by signal,");
+    if (do_time)
+        calc_duration_throughput(false);
+    print_stats("");
+    kill(getpid (), sig);
+}
+
+
+static void
+siginfo_handler(int sig)
+{
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    pr2serr("Progress report, continuing ...\n");
+    if (do_time)
+        calc_duration_throughput(true);
+    print_stats("  ");
+}
+
+static bool bsg_major_checked = false;
+static int bsg_major = 0;
+
+static void
+find_bsg_major(void)
+{
+    int n;
+    char *cp;
+    FILE *fp;
+    const char *proc_devices = "/proc/devices";
+    char a[128];
+    char b[128];
+
+    if (NULL == (fp = fopen(proc_devices, "r"))) {
+        if (verbose)
+            pr2serr("fopen %s failed: %s\n", proc_devices, strerror(errno));
+        return;
+    }
+    while ((cp = fgets(b, sizeof(b), fp))) {
+        if ((1 == sscanf(b, "%126s", a)) &&
+            (0 == memcmp(a, "Character", 9)))
+            break;
+    }
+    while (cp && (cp = fgets(b, sizeof(b), fp))) {
+        if (2 == sscanf(b, "%d %126s", &n, a)) {
+            if (0 == strcmp("bsg", a)) {
+                bsg_major = n;
+                break;
+            }
+        } else
+            break;
+    }
+    if (verbose > 5) {
+        if (cp)
+            pr2serr("found bsg_major=%d\n", bsg_major);
+        else
+            pr2serr("found no bsg char device in %s\n", proc_devices);
+    }
+    fclose(fp);
+}
+
+static bool nvme_major_checked = false;
+static int nvme_major = 0;
+
+static void
+find_nvme_major(void)
+{
+    int n;
+    char *cp;
+    FILE *fp;
+    const char *proc_devices = "/proc/devices";
+    char a[128];
+    char b[128];
+
+    if (NULL == (fp = fopen(proc_devices, "r"))) {
+        if (verbose)
+            pr2serr("fopen %s failed: %s\n", proc_devices, strerror(errno));
+        return;
+    }
+    while ((cp = fgets(b, sizeof(b), fp))) {
+        if ((1 == sscanf(b, "%126s", a)) &&
+            (0 == memcmp(a, "Character", 9)))
+            break;
+    }
+    while (cp && (cp = fgets(b, sizeof(b), fp))) {
+        if (2 == sscanf(b, "%d %126s", &n, a)) {
+            if (0 == strcmp("nvme", a)) {
+                nvme_major = n;
+                break;
+            }
+        } else
+            break;
+    }
+    if (verbose > 5) {
+        if (cp)
+            pr2serr("found nvme_major=%d\n", bsg_major);
+        else
+            pr2serr("found no nvme char device in %s\n", proc_devices);
+    }
+    fclose(fp);
+}
+
+
+static int
+dd_filetype(const char * filename)
+{
+    size_t len = strlen(filename);
+    struct stat st;
+
+    if ((1 == len) && ('.' == filename[0]))
+        return FT_DEV_NULL;
+    if (stat(filename, &st) < 0)
+        return FT_ERROR;
+    if (S_ISCHR(st.st_mode)) {
+        /* major() and minor() defined in sys/sysmacros.h */
+        if ((MEM_MAJOR == major(st.st_rdev)) &&
+            (DEV_NULL_MINOR_NUM == minor(st.st_rdev)))
+            return FT_DEV_NULL;
+        if (RAW_MAJOR == major(st.st_rdev))
+            return FT_RAW;
+        if (SCSI_GENERIC_MAJOR == major(st.st_rdev))
+            return FT_SG;
+        if (SCSI_TAPE_MAJOR == major(st.st_rdev))
+            return FT_ST;
+        if (! bsg_major_checked) {
+            bsg_major_checked = true;
+            find_bsg_major();
+        }
+        if (bsg_major == (int)major(st.st_rdev))
+            return FT_SG;
+        if (! nvme_major_checked) {
+            nvme_major_checked = true;
+            find_nvme_major();
+        }
+        if (nvme_major == (int)major(st.st_rdev))
+            return FT_NVME;     /* treat as sg device */
+    } else if (S_ISBLK(st.st_mode))
+        return FT_BLOCK;
+    else if (S_ISFIFO(st.st_mode))
+        return FT_FIFO;
+    return FT_OTHER;
+}
+
+
+static char *
+dd_filetype_str(int ft, char * buff)
+{
+    int off = 0;
+
+    if (FT_DEV_NULL & ft)
+        off += sg_scnpr(buff + off, 32, "null device ");
+    if (FT_SG & ft)
+        off += sg_scnpr(buff + off, 32, "SCSI generic (sg) device ");
+    if (FT_BLOCK & ft)
+        off += sg_scnpr(buff + off, 32, "block device ");
+    if (FT_FIFO & ft)
+        off += sg_scnpr(buff + off, 32, "fifo (named pipe) ");
+    if (FT_ST & ft)
+        off += sg_scnpr(buff + off, 32, "SCSI tape device ");
+    if (FT_RAW & ft)
+        off += sg_scnpr(buff + off, 32, "raw device ");
+    if (FT_NVME & ft)
+        off += sg_scnpr(buff + off, 32, "NVMe char device ");
+    if (FT_OTHER & ft)
+        off += sg_scnpr(buff + off, 32, "other (perhaps ordinary file) ");
+    if (FT_ERROR & ft)
+        sg_scnpr(buff + off, 32, "unable to 'stat' file ");
+    return buff;
+}
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_dd  [bs=BS] [conv=CONV] [count=COUNT] [ibs=BS] "
+            "[if=IFILE]\n"
+            "              [iflag=FLAGS] [obs=BS] [of=OFILE] [oflag=FLAGS] "
+            "[seek=SEEK]\n"
+            "              [skip=SKIP] [--dry-run] [--help] [--verbose] "
+            "[--version]\n\n"
+            "              [blk_sgio=0|1] [bpt=BPT] [cdbsz=6|10|12|16] "
+            "[cdl=CDL]\n"
+            "              [coe=0|1|2|3] [coe_limit=CL] [dio=0|1] "
+            "[odir=0|1]\n"
+            "              [of2=OFILE2] [retries=RETR] [sync=0|1] "
+            "[time=0|1[,TO]]\n"
+            "              [verbose=VERB] [--progress] [--verify]\n"
+            "  where:\n"
+            "    blk_sgio    0->block device use normal I/O(def), 1->use "
+            "SG_IO\n"
+            "    bpt         is blocks_per_transfer (default is 128 or 32 "
+            "when BS>=2048)\n"
+            "    bs          logical block size (default is 512)\n");
+    pr2serr("    cdbsz       size of SCSI READ or WRITE cdb (default is "
+            "10)\n"
+            "    cdl         command duration limits value 0 to 7 (def: "
+            "0 (no cdl))\n"
+            "    coe         0->exit on error (def), 1->continue on sg "
+            "error (zero\n"
+            "                fill), 2->also try read_long on unrecovered "
+            "reads,\n"
+            "                3->and set the CORRCT bit on the read long\n"
+            "    coe_limit   limit consecutive 'bad' blocks on reads to CL "
+            "times\n"
+            "                when COE>1 (default: 0 which is no limit)\n"
+            "    conv        comma separated list from: [nocreat,noerror,"
+            "notrunc,\n"
+            "                null,sparse,sync]\n"
+            "    count       number of blocks to copy (def: device size)\n"
+            "    dio         for direct IO, 1->attempt, 0->indirect IO "
+            "(def)\n"
+            "    ibs         input logical block size (if given must be same "
+            "as 'bs=')\n"
+            "    if          file or device to read from (def: stdin)\n"
+            "    iflag       comma separated list from: [00,coe,dio,direct,"
+            "dpo,dsync,\n"
+            "                excl,ff,flock,fua,nocache,null,random,sgio]\n"
+            "    obs         output logical block size (if given must be "
+            "same as 'bs=')\n"
+            "    odir        1->use O_DIRECT when opening block dev, "
+            "0->don't(def)\n"
+            "    of          file or device to write to (def: stdout), "
+            "OFILE of '.'\n");
+    pr2serr("                treated as /dev/null\n"
+            "    of2         additional output file (def: /dev/null), "
+            "OFILE2 should be\n"
+            "                normal file or pipe\n"
+            "    oflag       comma separated list from: [append,coe,dio,"
+            "direct,dpo,\n"
+            "                dsync,excl,flock,fua,nocache,nocreat,null,sgio,"
+            "sparse]\n"
+            "    retries     retry sgio errors RETR times (def: 0)\n"
+            "    seek        block position to start writing to OFILE\n"
+            "    skip        block position to start reading from IFILE\n"
+            "    sync        0->no sync(def), 1->SYNCHRONIZE CACHE on "
+            "OFILE after copy\n"
+            "    time        0->no timing(def), 1->time plus calculate "
+            "throughput;\n"
+            "                TO is command timeout in seconds (def: 60)\n"
+            "    verbose     0->quiet(def), 1->some noise, 2->more noise, "
+            "etc\n"
+            "    --dry-run    do preparation but bypass copy (or read)\n"
+            "    --help|-h    print out this usage message then exit\n"
+            "    --progress|-p    print progress report every 2 minutes\n"
+            "    --verbose|-v   same as 'verbose=1', can be used multiple "
+            "times\n"
+            "    --verify|-x    do verify/compare rather than copy "
+            "(OFILE must\n"
+            "                   be a sg device)\n"
+            "    --version|-V    print version information then exit\n\n"
+            "Copy from IFILE to OFILE, similar to dd command; specialized "
+            "for SCSI\ndevices. If the --verify option is given then IFILE "
+            "is read and that data\nis used to compare with OFILE using "
+            "the VERIFY(n) SCSI command (with\nBYTCHK=1).\n");
+}
+
+
+/* Return of 0 -> success, see sg_ll_read_capacity*() otherwise */
+static int
+scsi_read_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+    int res, verb;
+    unsigned int ui;
+    uint8_t rcBuff[RCAP16_REPLY_LEN];
+
+    verb = (verbose ? verbose - 1: 0);
+    res = sg_ll_readcap_10(sg_fd, false, 0, rcBuff, READ_CAP_REPLY_LEN, true,
+                           verb);
+    if (0 != res)
+        return res;
+
+    if ((0xff == rcBuff[0]) && (0xff == rcBuff[1]) && (0xff == rcBuff[2]) &&
+        (0xff == rcBuff[3])) {
+        int64_t ls;
+
+        res = sg_ll_readcap_16(sg_fd, false, 0, rcBuff, RCAP16_REPLY_LEN,
+                               true, verb);
+        if (0 != res)
+            return res;
+        ls = (int64_t)sg_get_unaligned_be64(rcBuff);
+        *num_sect = ls + 1;
+        *sect_sz = (int)sg_get_unaligned_be32(rcBuff + 8);
+    } else {
+        ui = sg_get_unaligned_be32(rcBuff);
+        /* take care not to sign extend values > 0x7fffffff */
+        *num_sect = (int64_t)ui + 1;
+        *sect_sz = (int)sg_get_unaligned_be32(rcBuff + 4);
+    }
+    if (verbose)
+        pr2serr("      number of blocks=%" PRId64 " [0x%" PRIx64 "], "
+                "logical block size=%d\n", *num_sect, *num_sect, *sect_sz);
+    return 0;
+}
+
+
+/* Return of 0 -> success, -1 -> failure. BLKGETSIZE64, BLKGETSIZE and */
+/* BLKSSZGET macros problematic (from <linux/fs.h> or <sys/mount.h>). */
+static int
+read_blkdev_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+#ifdef BLKSSZGET
+    if ((ioctl(sg_fd, BLKSSZGET, sect_sz) < 0) && (*sect_sz > 0)) {
+        perror("BLKSSZGET ioctl error");
+        return -1;
+    } else {
+ #ifdef BLKGETSIZE64
+        uint64_t ull;
+
+        if (ioctl(sg_fd, BLKGETSIZE64, &ull) < 0) {
+
+            perror("BLKGETSIZE64 ioctl error");
+            return -1;
+        }
+        *num_sect = ((int64_t)ull / (int64_t)*sect_sz);
+        if (verbose)
+            pr2serr("      [bgs64] number of blocks=%" PRId64 " [0x%" PRIx64
+                    "], logical block size=%d\n", *num_sect, *num_sect,
+                    *sect_sz);
+ #else
+        unsigned long ul;
+
+        if (ioctl(sg_fd, BLKGETSIZE, &ul) < 0) {
+            perror("BLKGETSIZE ioctl error");
+            return -1;
+        }
+        *num_sect = (int64_t)ul;
+        if (verbose)
+            pr2serr("      [bgs] number of blocks=%" PRId64 " [0x%" PRIx64
+                    "],  logical block size=%d\n", *num_sect, *num_sect,
+                    *sect_sz);
+ #endif
+    }
+    return 0;
+#else
+    if (verbose)
+        pr2serr("      BLKSSZGET+BLKGETSIZE ioctl not available\n");
+    *num_sect = 0;
+    *sect_sz = 0;
+    return -1;
+#endif
+}
+
+
+static int
+sg_build_scsi_cdb(uint8_t * cdbp, int cdb_sz, unsigned int blocks,
+                  int64_t start_block, bool is_verify, bool write_true,
+                  bool fua, bool dpo, int cdl)
+{
+    int sz_ind;
+    int rd_opcode[] = {0x8, 0x28, 0xa8, 0x88};
+    int ve_opcode[] = {0xff /* no VERIFY(6) */, VERIFY10, VERIFY12, VERIFY16};
+    int wr_opcode[] = {0xa, 0x2a, 0xaa, 0x8a};
+
+    memset(cdbp, 0, cdb_sz);
+    if (is_verify)
+        cdbp[1] = 0x2;  /* (BYTCHK=1) << 1 */
+    else {
+        if (dpo)
+            cdbp[1] |= 0x10;
+        if (fua)
+            cdbp[1] |= 0x8;
+    }
+    switch (cdb_sz) {
+    case 6:
+        sz_ind = 0;
+        if (is_verify && write_true) {
+            pr2serr(ME "there is no VERIFY(6), choose a larger cdbsz\n");
+            return 1;
+        }
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be24(0x1fffff & start_block, cdbp + 1);
+        cdbp[4] = (256 == blocks) ? 0 : (uint8_t)blocks;
+        if (blocks > 256) {
+            pr2serr(ME "for 6 byte commands, maximum number of blocks is "
+                    "256\n");
+            return 1;
+        }
+        if ((start_block + blocks - 1) & (~0x1fffff)) {
+            pr2serr(ME "for 6 byte commands, can't address blocks beyond "
+                    "%d\n", 0x1fffff);
+            return 1;
+        }
+        if (dpo || fua) {
+            pr2serr(ME "for 6 byte commands, neither dpo nor fua bits "
+                    "supported\n");
+            return 1;
+        }
+        break;
+    case 10:
+        sz_ind = 1;
+        if (is_verify && write_true)
+            cdbp[0] = ve_opcode[sz_ind];
+        else
+            cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                             rd_opcode[sz_ind]);
+        sg_put_unaligned_be32(start_block, cdbp + 2);
+        sg_put_unaligned_be16(blocks, cdbp + 7);
+        if (blocks & (~0xffff)) {
+            pr2serr(ME "for 10 byte commands, maximum number of blocks is "
+                    "%d\n", 0xffff);
+            return 1;
+        }
+        break;
+    case 12:
+        sz_ind = 2;
+        if (is_verify && write_true)
+            cdbp[0] = ve_opcode[sz_ind];
+        else
+            cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                             rd_opcode[sz_ind]);
+        sg_put_unaligned_be32(start_block, cdbp + 2);
+        sg_put_unaligned_be32(blocks, cdbp + 6);
+        break;
+    case 16:
+        sz_ind = 3;
+        if (is_verify && write_true)
+            cdbp[0] = ve_opcode[sz_ind];
+        else
+            cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                             rd_opcode[sz_ind]);
+        if ((! is_verify) && (cdl > 0)) {
+            if (cdl & 0x4)
+                cdbp[1] |= 0x1;
+            if (cdl & 0x3)
+                cdbp[14] |= ((cdl & 0x3) << 6);
+        }
+        sg_put_unaligned_be64(start_block, cdbp + 2);
+        sg_put_unaligned_be32(blocks, cdbp + 10);
+        break;
+    default:
+        pr2serr(ME "expected cdb size of 6, 10, 12, or 16 but got %d\n",
+                cdb_sz);
+        return 1;
+    }
+    return 0;
+}
+
+
+/* Does SCSI READ on IFILE. Returns 0 -> successful,
+ * SG_LIB_SYNTAX_ERROR -> unable to build cdb,
+ * SG_LIB_CAT_UNIT_ATTENTION -> try again,
+ * SG_LIB_CAT_MEDIUM_HARD_WITH_INFO -> 'io_addrp' written to,
+ * SG_LIB_CAT_MEDIUM_HARD -> no info field,
+ * SG_LIB_CAT_NOT_READY, SG_LIB_CAT_ABORTED_COMMAND,
+ * -2 -> ENOMEM, -1 other errors */
+static int
+sg_read_low(int sg_fd, uint8_t * buff, int blocks, int64_t from_block,
+            int bs, const struct flags_t * ifp, bool * diop,
+            uint64_t * io_addrp)
+{
+    bool info_valid;
+    bool print_cdb_after = false;
+    int res, slen;
+    const uint8_t * sbp;
+    uint8_t rdCmd[MAX_SCSI_CDBSZ];
+    uint8_t senseBuff[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr;
+
+    if (sg_build_scsi_cdb(rdCmd, ifp->cdbsz, blocks, from_block, do_verify,
+                          false, ifp->fua, ifp->dpo, ifp->cdl)) {
+        pr2serr(ME "bad rd cdb build, from_block=%" PRId64 ", blocks=%d\n",
+                from_block, blocks);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = ifp->cdbsz;
+    io_hdr.cmdp = rdCmd;
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = bs * blocks;
+    io_hdr.dxferp = buff;
+    io_hdr.mx_sb_len = SENSE_BUFF_LEN;
+    io_hdr.sbp = senseBuff;
+    io_hdr.timeout = cmd_timeout;
+    io_hdr.pack_id = (int)++glob_pack_id;
+    if (diop && *diop)
+        io_hdr.flags |= SG_FLAG_DIRECT_IO;
+
+    if (verbose > 2)
+        sg_print_command_len(rdCmd, ifp->cdbsz);
+
+    while (((res = ioctl(sg_fd, SG_IO, &io_hdr)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    if (res < 0) {
+        if (ENOMEM == errno)
+            return -2;
+        perror("reading (SG_IO) on sg device, error");
+        return -1;
+    }
+    if (verbose > 2)
+        pr2serr("      duration=%u ms\n", io_hdr.duration);
+    res = sg_err_category3(&io_hdr);
+    sbp = io_hdr.sbp;
+    slen = io_hdr.sb_len_wr;
+    switch (res) {
+    case SG_LIB_CAT_CLEAN:
+    case SG_LIB_CAT_CONDITION_MET:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        ++recovered_errs;
+        info_valid = sg_get_sense_info_fld(sbp, slen, io_addrp);
+        if (info_valid) {
+            pr2serr("    lba of last recovered error in this READ=0x%" PRIx64
+                    "\n", *io_addrp);
+            if (verbose > 1)
+                sg_chk_n_print3("reading", &io_hdr, true);
+        } else {
+            pr2serr("Recovered error: [no info] reading from block=0x%" PRIx64
+                    ", num=%d\n", from_block, blocks);
+            sg_chk_n_print3("reading", &io_hdr, verbose > 1);
+        }
+        break;
+    case SG_LIB_CAT_ABORTED_COMMAND:
+    case SG_LIB_CAT_UNIT_ATTENTION:
+        sg_chk_n_print3("reading", &io_hdr, verbose > 1);
+        return res;
+    case SG_LIB_CAT_MEDIUM_HARD:
+        if (verbose > 1)
+            sg_chk_n_print3("reading", &io_hdr, verbose > 1);
+        ++unrecovered_errs;
+        info_valid = sg_get_sense_info_fld(sbp, slen, io_addrp);
+        /* MMC devices don't necessarily set VALID bit */
+        if (info_valid || ((5 == ifp->pdt) && (*io_addrp > 0)))
+            return SG_LIB_CAT_MEDIUM_HARD_WITH_INFO;
+        else {
+            pr2serr("Medium, hardware or blank check error but no lba of "
+                    "failure in sense\n");
+            return res;
+        }
+        break;
+    case SG_LIB_CAT_NOT_READY:
+        ++unrecovered_errs;
+        if (verbose > 0)
+            sg_chk_n_print3("reading", &io_hdr, verbose > 1);
+        return res;
+    case SG_LIB_CAT_ILLEGAL_REQ:
+        if (5 == ifp->pdt) {    /* MMC READs can go down this path */
+            bool ili;
+            struct sg_scsi_sense_hdr ssh;
+
+            if (verbose > 1)
+                sg_chk_n_print3("reading", &io_hdr, verbose > 1);
+            if (sg_scsi_normalize_sense(sbp, slen, &ssh) &&
+                (0x64 == ssh.asc) && (0x0 == ssh.ascq)) {
+                if (sg_get_sense_filemark_eom_ili(sbp, slen, NULL, NULL,
+                                                  &ili) && ili) {
+                    sg_get_sense_info_fld(sbp, slen, io_addrp);
+                    if (*io_addrp > 0) {
+                        ++unrecovered_errs;
+                        return SG_LIB_CAT_MEDIUM_HARD_WITH_INFO;
+                    } else
+                        pr2serr("MMC READ gave 'illegal mode for this track' "
+                                "and ILI but no LBA of failure\n");
+                }
+                ++unrecovered_errs;
+                return SG_LIB_CAT_MEDIUM_HARD;
+            }
+        }
+        if (verbose > 0)
+            print_cdb_after = true;
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+        __attribute__((fallthrough));
+        /* FALL THROUGH */
+#endif
+#endif
+    default:
+        ++unrecovered_errs;
+        if (verbose > 0)
+            sg_chk_n_print3("reading", &io_hdr, verbose > 1);
+        if (print_cdb_after)
+            sg_print_command_len(rdCmd, ifp->cdbsz);
+        return res;
+    }
+    if (diop && *diop &&
+        ((io_hdr.info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+        *diop = false;      /* flag that dio not done (completely) */
+    sum_of_resids += io_hdr.resid;
+    return 0;
+}
+
+
+/* Does repeats associated with a SCSI READ on IFILE. Returns 0 -> successful,
+ * SG_LIB_SYNTAX_ERROR  -> unable to build cdb, SG_LIB_CAT_UNIT_ATTENTION ->
+ * try again, SG_LIB_CAT_NOT_READY, SG_LIB_CAT_MEDIUM_HARD,
+ * SG_LIB_CAT_ABORTED_COMMAND, -2 -> ENOMEM, -1 other errors */
+static int
+sg_read(int sg_fd, uint8_t * buff, int blocks, int64_t from_block,
+        int bs, struct flags_t * ifp, bool * diop, int * blks_readp)
+{
+    bool may_coe = false;
+    bool repeat;
+    int res, blks, xferred;
+    int ret = 0;
+    int retries_tmp;
+    uint64_t io_addr;
+    int64_t lba;
+    uint8_t * bp;
+
+    retries_tmp = ifp->retries;
+    for (xferred = 0, blks = blocks, lba = from_block, bp = buff;
+         blks > 0; blks = blocks - xferred) {
+        io_addr = 0;
+        repeat = false;
+        may_coe = false;
+        res = sg_read_low(sg_fd, bp, blks, lba, bs, ifp, diop, &io_addr);
+        switch (res) {
+        case 0:
+            if (blks_readp)
+                *blks_readp = xferred + blks;
+            if (coe_limit > 0)
+                coe_count = 0;  /* good read clears coe_count */
+            return 0;
+        case -2:        /* ENOMEM */
+            return res;
+        case SG_LIB_CAT_NOT_READY:
+            pr2serr("Device (r) not ready\n");
+            return res;
+        case SG_LIB_CAT_ABORTED_COMMAND:
+            if (--max_aborted > 0) {
+                pr2serr("Aborted command, continuing (r)\n");
+                repeat = true;
+            } else {
+                pr2serr("Aborted command, too many (r)\n");
+                return res;
+            }
+            break;
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            if (--max_uas > 0) {
+                pr2serr("Unit attention, continuing (r)\n");
+                repeat = true;
+            } else {
+                pr2serr("Unit attention, too many (r)\n");
+                return res;
+            }
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD_WITH_INFO:
+            if (retries_tmp > 0) {
+                pr2serr(">>> retrying a sgio read, lba=0x%" PRIx64 "\n",
+                        (uint64_t)lba);
+                --retries_tmp;
+                ++num_retries;
+                if (unrecovered_errs > 0)
+                    --unrecovered_errs;
+                repeat = true;
+            }
+            ret = SG_LIB_CAT_MEDIUM_HARD;
+            break; /* unrecovered read error at lba=io_addr */
+        case SG_LIB_SYNTAX_ERROR:
+            ifp->coe = 0;
+            ret = res;
+            goto err_out;
+        case -1:
+            ret = res;
+            goto err_out;
+        case SG_LIB_CAT_MEDIUM_HARD:
+            may_coe = true;
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        default:
+            if (retries_tmp > 0) {
+                pr2serr(">>> retrying a sgio read, lba=0x%" PRIx64 "\n",
+                        (uint64_t)lba);
+                --retries_tmp;
+                ++num_retries;
+                if (unrecovered_errs > 0)
+                    --unrecovered_errs;
+                repeat = true;
+                break;
+            }
+            ret = res;
+            goto err_out;
+        }
+        if (repeat)
+            continue;
+        if ((io_addr < (uint64_t)lba) ||
+            (io_addr >= (uint64_t)(lba + blks))) {
+                pr2serr("  Unrecovered error lba 0x%" PRIx64 " not in "
+                        "correct range:\n\t[0x%" PRIx64 ",0x%" PRIx64 "]\n",
+                        io_addr, (uint64_t)lba,
+                        (uint64_t)(lba + blks - 1));
+            may_coe = true;
+            goto err_out;
+        }
+        blks = (int)(io_addr - (uint64_t)lba);
+        if (blks > 0) {
+            if (verbose)
+                pr2serr("  partial read of %d blocks prior to medium error\n",
+                        blks);
+            res = sg_read_low(sg_fd, bp, blks, lba, bs, ifp, diop, &io_addr);
+            switch (res) {
+            case 0:
+                break;
+            case -1:
+                ifp->coe = 0;
+                ret = res;
+                goto err_out;
+            case -2:
+                pr2serr("ENOMEM again, unexpected (r)\n");
+                return -1;
+            case SG_LIB_CAT_NOT_READY:
+                pr2serr("device (r) not ready\n");
+                return res;
+            case SG_LIB_CAT_UNIT_ATTENTION:
+                pr2serr("Unit attention, unexpected (r)\n");
+                return res;
+            case SG_LIB_CAT_ABORTED_COMMAND:
+                pr2serr("Aborted command, unexpected (r)\n");
+                return res;
+            case SG_LIB_CAT_MEDIUM_HARD_WITH_INFO:
+            case SG_LIB_CAT_MEDIUM_HARD:
+                ret = SG_LIB_CAT_MEDIUM_HARD;
+                goto err_out;
+            case SG_LIB_SYNTAX_ERROR:
+            default:
+                pr2serr(">> unexpected result=%d from sg_read_low() 2\n",
+                        res);
+                ret = res;
+                goto err_out;
+            }
+        }
+        xferred += blks;
+        if (0 == ifp->coe) {
+            /* give up at block before problem unless 'coe' */
+            if (blks_readp)
+                *blks_readp = xferred;
+            return ret;
+        }
+        if (bs < 32) {
+            pr2serr(">> bs=%d too small for read_long\n", bs);
+            return -1;  /* nah, block size can't be that small */
+        }
+        bp += (blks * bs);
+        lba += blks;
+        if ((0 != ifp->pdt) || (ifp->coe < 2)) {
+            pr2serr(">> unrecovered read error at blk=%" PRId64 ", pdt=%d, "
+                    "use zeros\n", lba, ifp->pdt);
+            memset(bp, 0, bs);
+        } else if (io_addr < UINT_MAX) {
+            bool corrct, ok;
+            int offset, nl, r;
+            uint8_t * buffp;
+            uint8_t * free_buffp;
+
+            buffp = sg_memalign(bs * 2, 0, &free_buffp, false);
+            if (NULL == buffp) {
+                pr2serr(">> heap problems\n");
+                return -1;
+            }
+            corrct = (ifp->coe > 2);
+            res = sg_ll_read_long10(sg_fd, /* pblock */false, corrct, lba,
+                                    buffp, bs + read_long_blk_inc, &offset,
+                                    true, verbose);
+            ok = false;
+            switch (res) {
+            case 0:
+                ok = true;
+                ++read_longs;
+                break;
+            case SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO:
+                nl = bs + read_long_blk_inc - offset;
+                if ((nl < 32) || (nl > (bs * 2))) {
+                    pr2serr(">> read_long(10) len=%d unexpected\n", nl);
+                    break;
+                }
+                /* remember for next read_long attempt, if required */
+                read_long_blk_inc = nl - bs;
+
+                if (verbose)
+                    pr2serr("read_long(10): adjusted len=%d\n", nl);
+                r = sg_ll_read_long10(sg_fd, false, corrct, lba, buffp, nl,
+                                      &offset, true, verbose);
+                if (0 == r) {
+                    ok = true;
+                    ++read_longs;
+                    break;
+                } else
+                    pr2serr(">> unexpected result=%d on second "
+                            "read_long(10)\n", r);
+                break;
+            case SG_LIB_CAT_INVALID_OP:
+                pr2serr(">> read_long(10); not supported\n");
+                break;
+            case SG_LIB_CAT_ILLEGAL_REQ:
+                pr2serr(">> read_long(10): bad cdb field\n");
+                break;
+            case SG_LIB_CAT_NOT_READY:
+                pr2serr(">> read_long(10): device not ready\n");
+                break;
+            case SG_LIB_CAT_UNIT_ATTENTION:
+                pr2serr(">> read_long(10): unit attention\n");
+                break;
+            case SG_LIB_CAT_ABORTED_COMMAND:
+                pr2serr(">> read_long(10): aborted command\n");
+                break;
+            default:
+                pr2serr(">> read_long(10): problem (%d)\n", res);
+                break;
+            }
+            if (ok)
+                memcpy(bp, buffp, bs);
+            else
+                memset(bp, 0, bs);
+            free(free_buffp);
+        } else {
+            pr2serr(">> read_long(10) cannot handle blk=%" PRId64 ", use "
+                    "zeros\n", lba);
+            memset(bp, 0, bs);
+        }
+        ++xferred;
+        bp += bs;
+        ++lba;
+        if ((coe_limit > 0) && (++coe_count > coe_limit)) {
+            if (blks_readp)
+                *blks_readp = xferred + blks;
+            pr2serr(">> coe_limit on consecutive reads exceeded\n");
+            return SG_LIB_CAT_MEDIUM_HARD;
+        }
+    }
+    if (blks_readp)
+        *blks_readp = xferred;
+    return 0;
+
+err_out:
+    if (ifp->coe) {
+        memset(bp, 0, bs * blks);
+        pr2serr(">> unable to read at blk=%" PRId64 " for %d bytes, use "
+                "zeros\n", lba, bs * blks);
+        if (blks > 1)
+            pr2serr(">>   try reducing bpt to limit number of zeros written "
+                    "near bad block(s)\n");
+        /* fudge success */
+        if (blks_readp)
+            *blks_readp = xferred + blks;
+        if ((coe_limit > 0) && (++coe_count > coe_limit)) {
+            pr2serr(">> coe_limit on consecutive reads exceeded\n");
+            return ret;
+        }
+        return may_coe ? 0 : ret;
+    } else
+        return ret;
+}
+
+
+/* Does a SCSI WRITE or VERIFY (if do_verify set) on OFILE. Returns:
+ * 0 -> successful, SG_LIB_SYNTAX_ERROR -> unable to build cdb,
+ * SG_LIB_CAT_NOT_READY, SG_LIB_CAT_UNIT_ATTENTION, SG_LIB_CAT_MEDIUM_HARD,
+ * SG_LIB_CAT_ABORTED_COMMAND, -2 -> recoverable (ENOMEM),
+ * -1 -> unrecoverable error + others. SG_DD_BYPASS -> failed but coe set. */
+static int
+sg_write(int sg_fd, uint8_t * buff, int blocks, int64_t to_block,
+         int bs, const struct flags_t * ofp, bool * diop)
+{
+    bool info_valid;
+    int res;
+    uint64_t io_addr = 0;
+    const char * op_str = do_verify ? "verifying" : "writing";
+    uint8_t wrCmd[MAX_SCSI_CDBSZ];
+    uint8_t senseBuff[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr;
+
+    if (sg_build_scsi_cdb(wrCmd, ofp->cdbsz, blocks, to_block, do_verify,
+                          true, ofp->fua, ofp->dpo, ofp->cdl)) {
+        pr2serr(ME "bad wr cdb build, to_block=%" PRId64 ", blocks=%d\n",
+                to_block, blocks);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = ofp->cdbsz;
+    io_hdr.cmdp = wrCmd;
+    io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
+    io_hdr.dxfer_len = bs * blocks;
+    io_hdr.dxferp = buff;
+    io_hdr.mx_sb_len = SENSE_BUFF_LEN;
+    io_hdr.sbp = senseBuff;
+    io_hdr.timeout = cmd_timeout;
+    io_hdr.pack_id = (int)++glob_pack_id;
+    if (diop && *diop)
+        io_hdr.flags |= SG_FLAG_DIRECT_IO;
+
+    if (verbose > 2)
+        sg_print_command_len(wrCmd, ofp->cdbsz);
+
+    while (((res = ioctl(sg_fd, SG_IO, &io_hdr)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    if (res < 0) {
+        if (ENOMEM == errno)
+            return -2;
+        if (do_verify)
+            perror("verifying (SG_IO) on sg device, error");
+        else
+            perror("writing (SG_IO) on sg device, error");
+        return -1;
+    }
+
+    if (verbose > 2)
+        pr2serr("      duration=%u ms\n", io_hdr.duration);
+    res = sg_err_category3(&io_hdr);
+    switch (res) {
+    case SG_LIB_CAT_CLEAN:
+    case SG_LIB_CAT_CONDITION_MET:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        ++recovered_errs;
+        info_valid = sg_get_sense_info_fld(io_hdr.sbp, io_hdr.sb_len_wr,
+                                           &io_addr);
+        if (info_valid) {
+            pr2serr("    lba of last recovered error in this WRITE=0x%" PRIx64
+                    "\n", io_addr);
+            if (verbose > 1)
+                sg_chk_n_print3(op_str, &io_hdr, true);
+        } else {
+            pr2serr("Recovered error: [no info] %s to block=0x%" PRIx64
+                    ", num=%d\n", op_str, to_block, blocks);
+            sg_chk_n_print3(op_str, &io_hdr, verbose > 1);
+        }
+        break;
+    case SG_LIB_CAT_ABORTED_COMMAND:
+    case SG_LIB_CAT_UNIT_ATTENTION:
+        sg_chk_n_print3(op_str, &io_hdr, verbose > 1);
+        return res;
+    case SG_LIB_CAT_MISCOMPARE: /* must be VERIFY cpommand */
+        ++miscompare_errs;
+        if (ofp->coe) {
+            if (verbose > 1)
+                pr2serr(">> bypass due to miscompare: out blk=%" PRId64
+                        " for %d blocks\n", to_block, blocks);
+            return SG_DD_BYPASS; /* fudge success */
+        } else {
+            pr2serr("VERIFY reports miscompare\n");
+            return res;
+        }
+    case SG_LIB_CAT_NOT_READY:
+        ++unrecovered_errs;
+        pr2serr("device not ready (w)\n");
+        return res;
+    case SG_LIB_CAT_MEDIUM_HARD:
+    default:
+        sg_chk_n_print3(op_str, &io_hdr, verbose > 1);
+        if ((SG_LIB_CAT_ILLEGAL_REQ == res) && verbose)
+            sg_print_command_len(wrCmd, ofp->cdbsz);
+        ++unrecovered_errs;
+        if (ofp->coe) {
+            if (verbose > 1)
+                pr2serr(">> ignored errors for out blk=%" PRId64 " for %d "
+                        "bytes\n", to_block, bs * blocks);
+            return SG_DD_BYPASS; /* fudge success */
+        } else
+            return res;
+    }
+    if (diop && *diop &&
+        ((io_hdr.info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+        *diop = false;      /* flag that dio not done (completely) */
+    return 0;
+}
+
+
+static void
+calc_duration_throughput(bool contin)
+{
+    struct timeval end_tm, res_tm;
+    double a, b;
+    int64_t blks;
+
+    if (start_tm_valid && (start_tm.tv_sec || start_tm.tv_usec)) {
+        blks = (in_full > out_full) ? in_full : out_full;
+        gettimeofday(&end_tm, NULL);
+        res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+        res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+        if (res_tm.tv_usec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_usec += 1000000;
+        }
+        a = res_tm.tv_sec;
+        a += (0.000001 * res_tm.tv_usec);
+        b = (double)blk_sz * blks;
+        pr2serr("time to %s data%s: %d.%06d secs",
+                (do_verify ? "verify" : "copy"), (contin ? " so far" : ""),
+                (int)res_tm.tv_sec, (int)res_tm.tv_usec);
+        if ((a > 0.00001) && (b > 511))
+            pr2serr(" at %.2f MB/sec\n", b / (a * 1000000.0));
+        else
+            pr2serr("\n");
+    }
+}
+
+/* Process arguments given to 'iflag=" or 'oflag=" options. Returns 0
+ * on success, 1 on error. */
+static int
+process_flags(const char * arg, struct flags_t * fp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no flag found\n");
+        return 1;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "00"))
+            fp->zero = true;
+        else if (0 == strcmp(cp, "append"))
+            fp->append = true;
+        else if (0 == strcmp(cp, "coe"))
+            ++fp->coe;
+        else if (0 == strcmp(cp, "dio"))
+            fp->dio = true;
+        else if (0 == strcmp(cp, "direct"))
+            fp->direct = true;
+        else if (0 == strcmp(cp, "dpo"))
+            fp->dpo = true;
+        else if (0 == strcmp(cp, "dsync"))
+            fp->dsync = true;
+        else if (0 == strcmp(cp, "excl"))
+            fp->excl = true;
+        else if (0 == strcmp(cp, "flock"))
+            fp->flock = true;
+        else if (0 == strcmp(cp, "ff"))
+            fp->ff = true;
+        else if (0 == strcmp(cp, "fua"))
+            fp->fua = true;
+        else if (0 == strcmp(cp, "nocache"))
+            ++fp->nocache;
+        else if (0 == strcmp(cp, "nocreat"))
+            fp->nocreat = true;
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else if (0 == strcmp(cp, "random"))
+            fp->random = true;
+        else if (0 == strcmp(cp, "sgio"))
+            fp->sgio = true;
+        else if (0 == strcmp(cp, "sparse"))
+            fp->sparse = true;
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return 1;
+        }
+        cp = np;
+    } while (cp);
+    return 0;
+}
+
+/* Process arguments given to 'conv=" option. Returns 0 on success,
+ * 1 on error. */
+static int
+process_conv(const char * arg, struct flags_t * ifp, struct flags_t * ofp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no conversions found\n");
+        return 1;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "nocreat"))
+            ofp->nocreat = true;
+        else if (0 == strcmp(cp, "noerror"))
+            ++ifp->coe;         /* will still fail on write error */
+        else if (0 == strcmp(cp, "notrunc"))
+            ;         /* this is the default action of sg_dd so ignore */
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else if (0 == strcmp(cp, "sparse"))
+            ofp->sparse = true;
+        else if (0 == strcmp(cp, "sync"))
+            ;   /* dd(susv4): pad errored block(s) with zeros but sg_dd does
+                 * that by default. Typical dd use: 'conv=noerror,sync' */
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return 1;
+        }
+        cp = np;
+    } while (cp);
+    return 0;
+}
+
+/* Returns open input file descriptor (>= 0) or a negative value
+ * (-SG_LIB_FILE_ERROR or -SG_LIB_CAT_OTHER) if error.
+ */
+static int
+open_if(const char * inf, int64_t skip, int bpt, struct flags_t * ifp,
+        int * in_typep, int vb)
+{
+    int infd = -1;
+    int flags, fl, t, res;
+    char ebuff[EBUFF_SZ];
+    struct sg_simple_inquiry_resp sir;
+
+    *in_typep = dd_filetype(inf);
+    if (vb)
+        pr2serr(" >> Input file type: %s\n",
+                dd_filetype_str(*in_typep, ebuff));
+    if (FT_ERROR & *in_typep) {
+        pr2serr(ME "unable access %s\n", inf);
+        goto file_err;
+    } else if ((FT_BLOCK & *in_typep) && ifp->sgio)
+        *in_typep |= FT_SG;
+
+    if (FT_ST & *in_typep) {
+        pr2serr(ME "unable to use scsi tape device %s\n", inf);
+        goto file_err;
+    } else if (FT_SG & *in_typep) {
+        flags = O_NONBLOCK;
+        if (ifp->direct)
+            flags |= O_DIRECT;
+        if (ifp->excl)
+            flags |= O_EXCL;
+        if (ifp->dsync)
+            flags |= O_SYNC;
+        fl = O_RDWR;
+        if ((infd = open(inf, fl | flags)) < 0) {
+            fl = O_RDONLY;
+            if ((infd = open(inf, fl | flags)) < 0) {
+                snprintf(ebuff, EBUFF_SZ,
+                         ME "could not open %s for sg reading", inf);
+                perror(ebuff);
+                goto file_err;
+            }
+        }
+        if (vb)
+            pr2serr("        open input(sg_io), flags=0x%x\n", fl | flags);
+        if (sg_simple_inquiry(infd, &sir, false, (vb ? (vb - 1) : 0))) {
+            pr2serr("INQUIRY failed on %s\n", inf);
+            goto other_err;
+        }
+        ifp->pdt = sir.peripheral_type;
+        if (vb)
+            pr2serr("    %s: %.8s  %.16s  %.4s  [pdt=%d]\n", inf, sir.vendor,
+                    sir.product, sir.revision, ifp->pdt);
+        if (! (FT_BLOCK & *in_typep)) {
+            t = blk_sz * bpt;
+            res = ioctl(infd, SG_SET_RESERVED_SIZE, &t);
+            if (res < 0)
+                perror(ME "SG_SET_RESERVED_SIZE error");
+            res = ioctl(infd, SG_GET_VERSION_NUM, &t);
+            if ((res < 0) || (t < 30000)) {
+                if (FT_BLOCK & *in_typep)
+                    pr2serr(ME "SG_IO unsupported on this block device\n");
+                else
+                    pr2serr(ME "sg driver prior to 3.x.y\n");
+                goto file_err;
+            }
+        }
+    } else if (FT_NVME & *in_typep) {
+        pr2serr("Don't support NVMe char devices as IFILE\n");
+        goto file_err;
+    } else {
+        flags = O_RDONLY;
+        if (ifp->direct)
+            flags |= O_DIRECT;
+        if (ifp->excl)
+            flags |= O_EXCL;
+        if (ifp->dsync)
+            flags |= O_SYNC;
+        infd = open(inf, flags);
+        if (infd < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                     ME "could not open %s for reading", inf);
+            perror(ebuff);
+            goto file_err;
+        } else {
+            if (vb)
+                pr2serr("        open input, flags=0x%x\n", flags);
+            if (skip > 0) {
+                off64_t offset = skip;
+
+                offset *= blk_sz;       /* could exceed 32 bits here! */
+                if (lseek64(infd, offset, SEEK_SET) < 0) {
+                    snprintf(ebuff, EBUFF_SZ, ME "couldn't skip to "
+                             "required position on %s", inf);
+                    perror(ebuff);
+                    goto file_err;
+                }
+                if (vb)
+                    pr2serr("  >> skip: lseek64 SEEK_SET, byte offset=0x%"
+                            PRIx64 "\n", (uint64_t)offset);
+            }
+#ifdef HAVE_POSIX_FADVISE
+            if (ifp->nocache) {
+                int rt;
+
+                rt = posix_fadvise(infd, 0, 0, POSIX_FADV_SEQUENTIAL);
+                if (rt)
+                    pr2serr("open_if: posix_fadvise(SEQUENTIAL), err=%d\n",
+                            rt);
+            }
+#endif
+        }
+    }
+    if (ifp->flock && (infd >= 0)) {
+        res = flock(infd, LOCK_EX | LOCK_NB);
+        if (res < 0) {
+            close(infd);
+            snprintf(ebuff, EBUFF_SZ, ME "flock(LOCK_EX | LOCK_NB) on %s "
+                     "failed", inf);
+            perror(ebuff);
+            return -SG_LIB_FLOCK_ERR;
+        }
+    }
+    return infd;
+
+file_err:
+    if (infd >= 0)
+        close(infd);
+    return -SG_LIB_FILE_ERROR;
+other_err:
+    if (infd >= 0)
+        close(infd);
+    return -SG_LIB_CAT_OTHER;
+}
+
+/* Returns open output file descriptor (>= 0), -1 for don't
+ * bother opening (e.g. /dev/null), or a more negative value
+ * (-SG_LIB_FILE_ERROR or -SG_LIB_CAT_OTHER) if error.
+ */
+static int
+open_of(const char * outf, int64_t seek, int bpt, struct flags_t * ofp,
+        int * out_typep, int vb)
+{
+    bool not_found;
+    int outfd = -1;
+    int flags, t, res;
+    char ebuff[EBUFF_SZ];
+    struct sg_simple_inquiry_resp sir;
+
+    *out_typep = dd_filetype(outf);
+    if (vb)
+        pr2serr(" >> Output file type: %s\n",
+                dd_filetype_str(*out_typep, ebuff));
+    not_found = (FT_ERROR == *out_typep);  /* assume error was not found */
+
+    if ((FT_BLOCK & *out_typep) && ofp->sgio)
+        *out_typep |= FT_SG;
+
+    if (FT_ST & *out_typep) {
+        pr2serr(ME "unable to use scsi tape device %s\n", outf);
+        goto file_err;
+    } else if (FT_SG & *out_typep) {
+        flags = O_RDWR | O_NONBLOCK;
+        if (ofp->direct)
+            flags |= O_DIRECT;
+        if (ofp->excl)
+            flags |= O_EXCL;
+        if (ofp->dsync)
+            flags |= O_SYNC;
+        if ((outfd = open(outf, flags)) < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                     ME "could not open %s for sg writing", outf);
+            perror(ebuff);
+            goto file_err;
+        }
+        if (vb)
+            pr2serr("        open output(sg_io), flags=0x%x\n", flags);
+        if (sg_simple_inquiry(outfd, &sir, false, (vb ? (vb - 1) : 0))) {
+            pr2serr("INQUIRY failed on %s\n", outf);
+            goto other_err;
+        }
+        ofp->pdt = sir.peripheral_type;
+        if (vb)
+            pr2serr("    %s: %.8s  %.16s  %.4s  [pdt=%d]\n", outf, sir.vendor,
+                    sir.product, sir.revision, ofp->pdt);
+        if (! (FT_BLOCK & *out_typep)) {
+            t = blk_sz * bpt;
+            res = ioctl(outfd, SG_SET_RESERVED_SIZE, &t);
+            if (res < 0)
+                perror(ME "SG_SET_RESERVED_SIZE error");
+            res = ioctl(outfd, SG_GET_VERSION_NUM, &t);
+            if ((res < 0) || (t < 30000)) {
+                pr2serr(ME "sg driver prior to 3.x.y\n");
+                goto file_err;
+            }
+        }
+    } else if (FT_NVME & *out_typep) {
+        pr2serr("Don't support NVMe char devices as OFILE\n");
+        goto file_err;
+    } else if (FT_DEV_NULL & *out_typep)
+        outfd = -1; /* don't bother opening */
+    else if (FT_RAW & *out_typep) {
+        flags = O_WRONLY;
+        if (ofp->direct)
+            flags |= O_DIRECT;
+        if (ofp->excl)
+            flags |= O_EXCL;
+        if (ofp->dsync)
+            flags |= O_SYNC;
+        if ((outfd = open(outf, flags)) < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                    ME "could not open %s for raw writing", outf);
+            perror(ebuff);
+            goto file_err;
+        }
+    } else {    /* FT_OTHER or FT_ERROR (not found so create) */
+        flags = O_WRONLY;
+        if (! ofp->nocreat)
+            flags |= O_CREAT;
+        if (ofp->direct)
+            flags |= O_DIRECT;
+        if (ofp->excl)
+            flags |= O_EXCL;
+        if (ofp->dsync)
+            flags |= O_SYNC;
+        if (ofp->append)
+            flags |= O_APPEND;
+        if ((outfd = open(outf, flags, 0666)) < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                    ME "could not open %s for writing", outf);
+            perror(ebuff);
+            goto file_err;
+        }
+        if (vb)
+            pr2serr("        %s output, flags=0x%x\n",
+                    (not_found ? "create" : "open"), flags);
+        if (seek > 0) {
+            off64_t offset = seek;
+
+            offset *= blk_sz;       /* could exceed 32 bits here! */
+            if (lseek64(outfd, offset, SEEK_SET) < 0) {
+                snprintf(ebuff, EBUFF_SZ,
+                    ME "couldn't seek to required position on %s", outf);
+                perror(ebuff);
+                goto file_err;
+            }
+            if (vb)
+                pr2serr("   >> seek: lseek64 SEEK_SET, byte offset=0x%" PRIx64
+                        "\n", (uint64_t)offset);
+        }
+    }
+    if (ofp->flock && (outfd >= 0)) {
+        res = flock(outfd, LOCK_EX | LOCK_NB);
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ, ME "flock(LOCK_EX | LOCK_NB) on %s "
+                     "failed", outf);
+            perror(ebuff);
+            close(outfd);
+            return -SG_LIB_FLOCK_ERR;
+        }
+    }
+    return outfd;
+
+file_err:
+    if (outfd >= 0)
+        close(outfd);
+    return -SG_LIB_FILE_ERROR;
+other_err:
+    if (outfd >= 0)
+        close(outfd);
+    return -SG_LIB_CAT_OTHER;
+}
+
+/* Returns the number of times 'ch' is found in string 's' given the
+ * string's length. */
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+/* Returns true when it time to output a progress report; else false. */
+static bool
+check_progress(void)
+{
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+    static bool have_prev, measure;
+    static struct timespec prev_true_tm;
+    static int count, threshold;
+    bool res = false;
+    uint32_t elapsed_ms, ms;
+    struct timespec now_tm, res_tm;
+
+    if (progress) {
+        if (! have_prev) {
+            have_prev = true;
+            measure = true;
+            clock_gettime(CLOCK_MONOTONIC, &prev_true_tm);
+            return false;       /* starting reference */
+        }
+        if (! measure) {
+            if (++count >= threshold)
+                count = 0;
+            else
+                return false;
+        }
+        clock_gettime(CLOCK_MONOTONIC, &now_tm);
+        res_tm.tv_sec = now_tm.tv_sec - prev_true_tm.tv_sec;
+        res_tm.tv_nsec = now_tm.tv_nsec - prev_true_tm.tv_nsec;
+        if (res_tm.tv_nsec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_nsec += 1000000000;
+        }
+        elapsed_ms = (1000 * res_tm.tv_sec) + (res_tm.tv_nsec / 1000000);
+        if (measure) {
+            ++threshold;
+            if (elapsed_ms > 80)        /* 80 milliseconds */
+                measure = false;
+        }
+        if (elapsed_ms >= PROGRESS3_TRIGGER_MS) {
+            if (elapsed_ms >= PROGRESS2_TRIGGER_MS) {
+                if (elapsed_ms >= PROGRESS_TRIGGER_MS) {
+                    ms = PROGRESS_TRIGGER_MS;
+                    res = true;
+                } else if (progress > 1) {
+                    ms = PROGRESS2_TRIGGER_MS;
+                    res = true;
+                }
+            } else if (progress > 2) {
+                ms = PROGRESS3_TRIGGER_MS;
+                res = true;
+            }
+        }
+        if (res) {
+            prev_true_tm.tv_sec += (ms / 1000);
+            prev_true_tm.tv_nsec += (ms % 1000) * 1000000;
+            if (prev_true_tm.tv_nsec >= 1000000000) {
+                ++prev_true_tm.tv_sec;
+                prev_true_tm.tv_nsec -= 1000000000;
+            }
+        }
+    }
+    return res;
+
+#elif defined(HAVE_GETTIMEOFDAY)
+    static bool have_prev, measure;
+    static struct timeval prev_true_tm;
+    static int count, threshold;
+    bool res = false;
+    uint32_t elapsed_ms, ms;
+    struct timeval now_tm, res_tm;
+
+    if (progress) {
+        if (! have_prev) {
+            have_prev = true;
+            gettimeofday(&prev_true_tm, NULL);
+            return false;       /* starting reference */
+        }
+        if (! measure) {
+            if (++count >= threshold)
+                count = 0;
+            else
+                return false;
+        }
+        gettimeofday(&now_tm, NULL);
+        res_tm.tv_sec = now_tm.tv_sec - prev_true_tm.tv_sec;
+        res_tm.tv_usec = now_tm.tv_usec - prev_true_tm.tv_usec;
+        if (res_tm.tv_usec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_usec += 1000000;
+        }
+        elapsed_ms = (1000 * res_tm.tv_sec) + (res_tm.tv_usec / 1000);
+        if (measure) {
+            ++threshold;
+            if (elapsed_ms > 80)        /* 80 milliseconds */
+                measure = false;
+        }
+        if (elapsed_ms >= PROGRESS3_TRIGGER_MS) {
+            if (elapsed_ms >= PROGRESS2_TRIGGER_MS) {
+                if (elapsed_ms >= PROGRESS_TRIGGER_MS) {
+                    ms = PROGRESS_TRIGGER_MS;
+                    res = true;
+                } else if (progress > 1) {
+                    ms = PROGRESS2_TRIGGER_MS;
+                    res = true;
+                }
+            } else if (progress > 2) {
+                ms = PROGRESS3_TRIGGER_MS;
+                res = true;
+            }
+        }
+        if (res) {
+            prev_true_tm.tv_sec += (ms / 1000);
+            prev_true_tm.tv_usec += (ms % 1000) * 1000;
+            if (prev_true_tm.tv_usec >= 1000000) {
+                ++prev_true_tm.tv_sec;
+                prev_true_tm.tv_usec -= 1000000;
+            }
+        }
+    }
+    return res;
+
+#else   /* no clock reading functions available */
+    return false;
+#endif
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool bpt_given = false;
+    bool cdbsz_given = false;
+    bool cdl_given = false;
+    bool dio_tmp, first;
+    bool do_sync = false;
+    bool penult_sparse_skip = false;
+    bool sparse_skip = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, k, n, t, buf_sz, blocks_per, infd, outfd, out2fd, keylen;
+    int retries_tmp, blks_read, bytes_read, bytes_of2, bytes_of;
+    int in_sect_sz, out_sect_sz;
+    int blocks = 0;
+    int bpt = DEF_BLOCKS_PER_TRANSFER;
+    int dio_incomplete_count = 0;
+    int ibs = 0;
+    int in_type = FT_OTHER;
+    int obs = 0;
+    int out_type = FT_OTHER;
+    int out2_type = FT_OTHER;
+    int penult_blocks = 0;
+    int ret = 0;
+    int64_t skip = 0;
+    int64_t seek = 0;
+    int64_t in_num_sect = -1;
+    int64_t out_num_sect = -1;
+    char * key;
+    char * buf;
+    const char * ccp = NULL;
+    const char * cc2p;
+    uint8_t * wrkBuff = NULL;
+    uint8_t * wrkPos;
+    char inf[INOUTF_SZ];
+    char outf[INOUTF_SZ];
+    char out2f[INOUTF_SZ];
+    char str[STR_SZ];
+    char ebuff[EBUFF_SZ];
+
+    inf[0] = '\0';
+    outf[0] = '\0';
+    out2f[0] = '\0';
+    iflag.cdbsz = DEF_SCSI_CDBSZ;
+    oflag.cdbsz = DEF_SCSI_CDBSZ;
+
+    for (k = 1; k < argc; k++) {
+        if (argv[k]) {
+            strncpy(str, argv[k], STR_SZ);
+            str[STR_SZ - 1] = '\0';
+        } else
+            continue;
+        for (key = str, buf = key; *buf && *buf != '=';)
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        keylen = strlen(key);
+        if (0 == strncmp(key, "app", 3)) {
+            iflag.append = !! sg_get_num(buf);
+            oflag.append = iflag.append;
+        } else if (0 == strcmp(key, "blk_sgio")) {
+            iflag.sgio = !! sg_get_num(buf);
+            oflag.sgio = iflag.sgio;
+        } else if (0 == strcmp(key, "bpt")) {
+            bpt = sg_get_num(buf);
+            if (-1 == bpt) {
+                pr2serr(ME "bad argument to 'bpt='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            bpt_given = true;
+        } else if (0 == strcmp(key, "bs")) {
+            blk_sz = sg_get_num(buf);
+            if ((blk_sz < 0) || (blk_sz > MAX_BPT_VALUE)) {
+                pr2serr(ME "bad argument to 'bs='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "cdbsz")) {
+            iflag.cdbsz = sg_get_num(buf);
+            if ((iflag.cdbsz < 6) || (iflag.cdbsz > 32)) {
+                pr2serr(ME "'cdbsz' expects 6, 10, 12, 16 or 32\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            oflag.cdbsz = iflag.cdbsz;
+            cdbsz_given = true;
+        } else if (0 == strcmp(key, "cdl")) {
+            const char * cp = strchr(buf, ',');
+
+            iflag.cdl = sg_get_num(buf);
+            if ((iflag.cdl < 0) || (iflag.cdl > 7)) {
+                pr2serr(ME "bad argument to 'cdl=', expect 0 to 7\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (cp) {
+                oflag.cdl = sg_get_num(cp + 1);
+                if ((oflag.cdl < 0) || (oflag.cdl > 7)) {
+                    pr2serr(ME "bad argument to 'cdl=ICDL,OCDL', expect OCDL "
+                            "to be 0 to 7\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else
+                oflag.cdl = iflag.cdl;
+            cdl_given = true;
+        } else if (0 == strcmp(key, "coe")) {
+            iflag.coe = sg_get_num(buf);
+            oflag.coe = iflag.coe;
+        } else if (0 == strcmp(key, "coe_limit")) {
+            coe_limit = sg_get_num(buf);
+            if (-1 == coe_limit) {
+                pr2serr(ME "bad argument to 'coe_limit='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "conv")) {
+            if (process_conv(buf, &iflag, &oflag)) {
+                pr2serr(ME "bad argument to 'conv='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "count")) {
+            if (0 != strcmp("-1", buf)) {
+                dd_count = sg_get_llnum(buf);
+                if ((dd_count < 0) || (dd_count > MAX_COUNT_SKIP_SEEK)) {
+                    pr2serr(ME "bad argument to 'count='\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }   /* treat 'count=-1' as calculate count (same as not given) */
+        } else if (0 == strcmp(key, "dio")) {
+            oflag.dio = !! sg_get_num(buf);
+            iflag.dio = oflag.dio;
+        } else if (0 == strcmp(key, "fua")) {
+            t = sg_get_num(buf);
+            oflag.fua = !! (t & 1);
+            iflag.fua = !! (t & 2);
+        } else if (0 == strcmp(key, "ibs")) {
+            ibs = sg_get_num(buf);
+            if ((ibs < 0) || (ibs > MAX_BPT_VALUE)) {
+                pr2serr(ME "bad argument to 'ibs='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key, "if") == 0) {
+            if ('\0' != inf[0]) {
+                pr2serr("Second IFILE argument??\n");
+                return SG_LIB_SYNTAX_ERROR;
+            } else {
+                memcpy(inf, buf, INOUTF_SZ - 1);
+                inf[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (0 == strcmp(key, "iflag")) {
+            if (process_flags(buf, &iflag)) {
+                pr2serr(ME "bad argument to 'iflag='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "obs")) {
+            obs = sg_get_num(buf);
+            if ((obs < 0) || (obs > MAX_BPT_VALUE)) {
+                pr2serr(ME "bad argument to 'obs='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "odir")) {
+            iflag.direct = !! sg_get_num(buf);
+            oflag.direct = iflag.direct;
+        } else if (strcmp(key, "of") == 0) {
+            if ('\0' != outf[0]) {
+                pr2serr("Second OFILE argument??\n");
+                return SG_LIB_CONTRADICT;
+            } else {
+                memcpy(outf, buf, INOUTF_SZ - 1);
+                outf[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (strcmp(key, "of2") == 0) {
+            if ('\0' != out2f[0]) {
+                pr2serr("Second OFILE2 argument??\n");
+                return SG_LIB_CONTRADICT;
+            } else {
+                memcpy(out2f, buf, INOUTF_SZ - 1);
+                out2f[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (0 == strcmp(key, "oflag")) {
+            if (process_flags(buf, &oflag)) {
+                pr2serr(ME "bad argument to 'oflag='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "retries")) {
+            iflag.retries = sg_get_num(buf);
+            oflag.retries = iflag.retries;
+            if (-1 == iflag.retries) {
+                pr2serr(ME "bad argument to 'retries='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "seek")) {
+            seek = sg_get_llnum(buf);
+            if ((seek < 0) || (seek > MAX_COUNT_SKIP_SEEK)) {
+                pr2serr(ME "bad argument to 'seek='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "skip")) {
+            skip = sg_get_llnum(buf);
+            if ((skip < 0) || (skip > MAX_COUNT_SKIP_SEEK)) {
+                pr2serr(ME "bad argument to 'skip='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "sync"))
+            do_sync = !! sg_get_num(buf);
+        else if (0 == strcmp(key, "time")) {
+            const char * cp = strchr(buf, ',');
+
+            do_time = !! sg_get_num(buf);
+            if (cp) {
+                n = sg_get_num(cp + 1);
+                if (n < 0) {
+                    pr2serr(ME "bad argument to 'time=0|1,TO'\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                cmd_timeout = n ? (n * 1000) : DEF_TIMEOUT;
+            }
+        } else if (0 == strncmp(key, "verb", 4))
+            verbose = sg_get_num(buf);
+        else if ((keylen > 1) && ('-' == key[0]) && ('-' != key[1])) {
+            res = 0;
+            n = num_chs_in_str(key + 1, keylen - 1, 'd');
+            dry_run += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'h');
+            if (n > 0) {
+                usage();
+                return 0;
+            }
+            n = num_chs_in_str(key + 1, keylen - 1, 'p');
+            progress += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'v');
+            if (n > 0)
+                verbose_given = true;
+            verbose += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'V');
+            if (n > 0)
+                version_given = true;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'x');
+            if (n > 0)
+                do_verify = true;
+            res += n;
+            if (res < (keylen - 1)) {
+                pr2serr("Unrecognised short option in '%s', try '--help'\n",
+                        key);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if ((0 == strncmp(key, "--dry-run", 9)) ||
+                 (0 == strncmp(key, "--dry_run", 9)))
+            ++dry_run;
+        else if ((0 == strncmp(key, "--help", 6)) ||
+                 (0 == strcmp(key, "-?"))) {
+            usage();
+            return 0;
+        } else if (0 == strncmp(key, "--progress", 10))
+            ++progress;
+        else if (0 == strncmp(key, "--verb", 6)) {
+            verbose_given = true;
+            ++verbose;
+        } else if (0 == strncmp(key, "--veri", 6))
+            do_verify = true;
+        else if (0 == strncmp(key, "--vers", 6))
+            version_given = true;
+        else {
+            pr2serr("Unrecognized option '%s'\n", key);
+            pr2serr("For more information use '--help'\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+    if (progress > 0 && !do_time)
+        do_time = true;
+    if (argc < 2) {
+        pr2serr("Won't default both IFILE to stdin _and_ OFILE to stdout\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (blk_sz <= 0) {
+        blk_sz = DEF_BLOCK_SIZE;
+        pr2serr("Assume default 'bs' ((logical) block size) of %d bytes\n",
+                blk_sz);
+    }
+    if ((ibs && (ibs != blk_sz)) || (obs && (obs != blk_sz))) {
+        pr2serr("If 'ibs' or 'obs' given must be same as 'bs'\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if ((skip < 0) || (seek < 0)) {
+        pr2serr("skip and seek cannot be negative\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (oflag.append && (seek > 0)) {
+        pr2serr("Can't use both append and seek switches\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if ((bpt < 1) || (bpt > MAX_BPT_VALUE)) {
+        pr2serr("bpt must be > 0 and <= %d\n", MAX_BPT_VALUE);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (iflag.sparse)
+        pr2serr("sparse flag ignored for iflag\n");
+
+    /* defaulting transfer size to 128*2048 for CD/DVDs is too large
+       for the block layer in lk 2.6 and results in an EIO on the
+       SG_IO ioctl. So reduce it in that case. */
+    if ((blk_sz >= 2048) && (! bpt_given))
+        bpt = DEF_BLOCKS_PER_2048TRANSFER;
+#ifdef DEBUG
+    pr2serr(ME "if=%s skip=%" PRId64 " of=%s seek=%" PRId64 " count=%" PRId64
+            "\n", inf, skip, outf, seek, dd_count);
+#endif
+    install_handler(SIGINT, interrupt_handler);
+    install_handler(SIGQUIT, interrupt_handler);
+    install_handler(SIGPIPE, interrupt_handler);
+    install_handler(SIGUSR1, siginfo_handler);
+
+    infd = STDIN_FILENO;
+    outfd = STDOUT_FILENO;
+    iflag.pdt = -1;
+    oflag.pdt = -1;
+    if (iflag.zero && iflag.ff) {
+        ccp = "<addr_as_data>";
+        cc2p = "addr_as_data";
+    } else if (iflag.ff) {
+        ccp = "<0xff bytes>";
+        cc2p = "ff";
+    } else if (iflag.random) {
+        ccp = "<random>";
+        cc2p = "random";
+#ifdef HAVE_GETRANDOM
+        {
+            ssize_t ssz = getrandom(&seed, sizeof(seed), GRND_NONBLOCK);
+
+            if (ssz < (ssize_t)sizeof(seed)) {
+                pr2serr("getrandom() failed, ret=%d\n", (int)ssz);
+                seed = (long)time(NULL);
+            }
+        }
+#else
+        seed = (long)time(NULL);    /* use seconds since epoch as proxy */
+#endif
+        if (verbose > 1)
+            pr2serr("seed=%ld\n", seed);
+#ifdef HAVE_SRAND48_R
+        srand48_r(seed, &drand);
+#else
+        srand48(seed);
+#endif
+    } else if (iflag.zero) {
+       ccp = "<zero bytes>";
+       cc2p = "00";
+    }
+    if (ccp) {
+        if (inf[0]) {
+            pr2serr("iflag=%s and if=%s contradict\n", cc2p, inf);
+            return SG_LIB_CONTRADICT;
+        }
+        in_type = FT_RANDOM_0_FF;
+        strcpy(inf, ccp);
+        infd = -1;
+    } else if (inf[0] && ('-' != inf[0])) {
+        infd = open_if(inf, skip, bpt, &iflag, &in_type, verbose);
+        if (infd < 0)
+            return -infd;
+    }
+
+    if (outf[0] && ('-' != outf[0])) {
+        outfd = open_of(outf, seek, bpt, &oflag, &out_type, verbose);
+        if (outfd < -1)
+            return -outfd;
+    }
+    if (do_verify) {
+        if (! (FT_SG & out_type)) {
+            pr2serr("--verify only supported when OFILE is a sg device or "
+                    "oflag=sgio\n");
+            ret = SG_LIB_CONTRADICT;
+            goto bypass_copy;
+        }
+        if (oflag.sparse) {
+            pr2serr("--verify cannot be used with oflag=sparse\n");
+            ret = SG_LIB_CONTRADICT;
+            goto bypass_copy;
+        }
+    }
+    if (cdl_given && (! cdbsz_given)) {
+        bool changed = false;
+
+        if ((iflag.cdbsz < 16) && (iflag.cdl > 0)) {
+            iflag.cdbsz = 16;
+            changed = true;
+        }
+        if ((oflag.cdbsz < 16) && (! do_verify) && (oflag.cdl > 0)) {
+            oflag.cdbsz = 16;
+            changed = true;
+        }
+        if (changed)
+            pr2serr(">> increasing cdbsz to 16 due to cdl > 0\n");
+    }
+    if (out2f[0]) {
+        out2_type = dd_filetype(out2f);
+        if ((out2fd = open(out2f, O_WRONLY | O_CREAT, 0666)) < 0) {
+            res = errno;
+            snprintf(ebuff, EBUFF_SZ,
+                     ME "could not open %s for writing", out2f);
+            perror(ebuff);
+            return res;
+        }
+    } else
+        out2fd = -1;
+
+    if ((STDIN_FILENO == infd) && (STDOUT_FILENO == outfd)) {
+        pr2serr("Can't have both 'if' as stdin _and_ 'of' as stdout\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (oflag.sparse) {
+        if (STDOUT_FILENO == outfd) {
+            pr2serr("oflag=sparse needs seekable output file\n");
+            return SG_LIB_CONTRADICT;
+        }
+    }
+
+    if ((dd_count < 0) || ((verbose > 0) && (0 == dd_count))) {
+        in_num_sect = -1;
+        in_sect_sz = -1;
+        if (FT_SG & in_type) {
+            res = scsi_read_capacity(infd, &in_num_sect, &in_sect_sz);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Unit attention (readcap in), continuing\n");
+                res = scsi_read_capacity(infd, &in_num_sect, &in_sect_sz);
+            } else if (SG_LIB_CAT_ABORTED_COMMAND == res) {
+                pr2serr("Aborted command (readcap in), continuing\n");
+                res = scsi_read_capacity(infd, &in_num_sect, &in_sect_sz);
+            }
+            if (0 != res) {
+                if (res == SG_LIB_CAT_INVALID_OP)
+                    pr2serr("read capacity not supported on %s\n", inf);
+                else if (res == SG_LIB_CAT_NOT_READY)
+                    pr2serr("read capacity failed on %s - not ready\n", inf);
+                else
+                    pr2serr("Unable to read capacity on %s\n", inf);
+                in_num_sect = -1;
+            } else if (in_sect_sz != blk_sz)
+                pr2serr(">> warning: logical block size on %s confusion: "
+                        "bs=%d, device claims=%d\n", inf, blk_sz, in_sect_sz);
+        } else if (FT_BLOCK & in_type) {
+            if (0 != read_blkdev_capacity(infd, &in_num_sect, &in_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", inf);
+                in_num_sect = -1;
+            }
+            if (blk_sz != in_sect_sz) {
+                pr2serr("logical block size on %s confusion: bs=%d, device "
+                        "claims=%d\n", inf, blk_sz, in_sect_sz);
+                in_num_sect = -1;
+            }
+        }
+        if (in_num_sect > skip)
+            in_num_sect -= skip;
+
+        out_num_sect = -1;
+        out_sect_sz = -1;
+        if (FT_SG & out_type) {
+            res = scsi_read_capacity(outfd, &out_num_sect, &out_sect_sz);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Unit attention (readcap out), continuing\n");
+                res = scsi_read_capacity(outfd, &out_num_sect, &out_sect_sz);
+            } else if (SG_LIB_CAT_ABORTED_COMMAND == res) {
+                pr2serr("Aborted command (readcap out), continuing\n");
+                res = scsi_read_capacity(outfd, &out_num_sect, &out_sect_sz);
+            }
+            if (0 != res) {
+                if (res == SG_LIB_CAT_INVALID_OP)
+                    pr2serr("read capacity not supported on %s\n", outf);
+                else
+                    pr2serr("Unable to read capacity on %s\n", outf);
+                out_num_sect = -1;
+            } else if (blk_sz != out_sect_sz)
+                pr2serr(">> warning: logical block size on %s confusion: "
+                        "bs=%d, device claims=%d\n", outf, blk_sz,
+                        out_sect_sz);
+        } else if (FT_BLOCK & out_type) {
+            if (0 != read_blkdev_capacity(outfd, &out_num_sect,
+                                          &out_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", outf);
+                out_num_sect = -1;
+            } else if (blk_sz != out_sect_sz) {
+                pr2serr("logical block size on %s confusion: bs=%d, device "
+                        "claims=%d\n", outf, blk_sz, out_sect_sz);
+                out_num_sect = -1;
+            }
+        }
+        if (out_num_sect > seek)
+            out_num_sect -= seek;
+#ifdef DEBUG
+        pr2serr("Start of loop, count=%" PRId64 ", in_num_sect=%" PRId64
+                ", out_num_sect=%" PRId64 "\n", dd_count, in_num_sect,
+                out_num_sect);
+#endif
+        if (dd_count < 0) {
+            if (in_num_sect > 0) {
+                if (out_num_sect > 0)
+                    dd_count = (in_num_sect > out_num_sect) ? out_num_sect :
+                                                           in_num_sect;
+                else
+                    dd_count = in_num_sect;
+            } else
+                dd_count = out_num_sect;
+        }
+    }
+
+    if (dd_count < 0) {
+        pr2serr("Couldn't calculate count, please give one\n");
+        return SG_LIB_CAT_OTHER;
+    }
+    if (! cdbsz_given) {
+        if ((FT_SG & in_type) && (MAX_SCSI_CDBSZ != iflag.cdbsz) &&
+            (((dd_count + skip) > UINT_MAX) || (bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'if')\n");
+            iflag.cdbsz = MAX_SCSI_CDBSZ;
+        }
+        if ((FT_SG & out_type) && (MAX_SCSI_CDBSZ != oflag.cdbsz) &&
+            (((dd_count + seek) > UINT_MAX) || (bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'of')\n");
+            oflag.cdbsz = MAX_SCSI_CDBSZ;
+        }
+    }
+
+    if (iflag.dio || iflag.direct || oflag.direct || (FT_RAW & in_type) ||
+        (FT_RAW & out_type)) {  /* want heap buffer aligned to page_size */
+
+        wrkPos = sg_memalign(blk_sz * bpt, 0, &wrkBuff, false);
+        if (NULL == wrkPos) {
+            pr2serr("sg_memalign: error, out of memory?\n");
+            return sg_convert_errno(ENOMEM);
+        }
+    } else {
+        wrkPos = sg_memalign(blk_sz * bpt, 0, &wrkBuff, false);
+        if (0 == wrkPos) {
+            pr2serr("Not enough user memory\n");
+            return sg_convert_errno(ENOMEM);
+        }
+    }
+
+    blocks_per = bpt;
+#ifdef DEBUG
+    pr2serr("Start of loop, count=%" PRId64 ", blocks_per=%d\n", dd_count,
+            blocks_per);
+#endif
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+        start_tm_valid = true;
+    }
+    req_count = dd_count;
+
+    if (dry_run > 0) {
+        pr2serr("Since --dry-run option given, bypassing copy\n");
+        goto bypass_copy;
+    }
+
+    /* <<< main loop that does the copy >>> */
+    while (dd_count > 0) {
+        bytes_read = 0;
+        bytes_of = 0;
+        bytes_of2 = 0;
+        penult_sparse_skip = sparse_skip;
+        penult_blocks = penult_sparse_skip ? blocks : 0;
+        sparse_skip = false;
+        blocks = (dd_count > blocks_per) ? blocks_per : dd_count;
+        if (FT_SG & in_type) {
+            dio_tmp = iflag.dio;
+            res = sg_read(infd, wrkPos, blocks, skip, blk_sz, &iflag,
+                          &dio_tmp, &blks_read);
+            if (-2 == res) {     /* ENOMEM, find what's available+try that */
+                if (ioctl(infd, SG_GET_RESERVED_SIZE, &buf_sz) < 0) {
+                    perror("RESERVED_SIZE ioctls failed");
+                    ret = res;
+                    break;
+                }
+                if (buf_sz < MIN_RESERVED_SIZE)
+                    buf_sz = MIN_RESERVED_SIZE;
+                blocks_per = (buf_sz + blk_sz - 1) / blk_sz;
+                if (blocks_per < blocks) {
+                    blocks = blocks_per;
+                    pr2serr("Reducing read to %d blocks per loop\n",
+                            blocks_per);
+                    res = sg_read(infd, wrkPos, blocks, skip, blk_sz,
+                                  &iflag, &dio_tmp, &blks_read);
+                }
+            }
+            if (res) {
+                pr2serr("sg_read failed,%s at or after lba=%" PRId64 " [0x%"
+                        PRIx64 "]\n", ((-2 == res) ?
+                                 " try reducing bpt," : ""), skip, skip);
+                ret = res;
+                break;
+            } else {
+                if (blks_read < blocks) {
+                    dd_count = 0;   /* force exit after write */
+                    blocks = blks_read;
+                }
+                in_full += blocks;
+                if (iflag.dio && (! dio_tmp))
+                    dio_incomplete_count++;
+            }
+        } else if (FT_RANDOM_0_FF == in_type) {
+            int j;
+
+            res = blocks * blk_sz;
+            if (iflag.zero && iflag.ff && (blk_sz >= 4)) {
+                uint32_t pos = (uint32_t)skip;
+                uint32_t off;
+
+                for (k = 0, off = 0; k < blocks; ++k, off += blk_sz, ++pos) {
+                    for (j = 0; j < (blk_sz - 3); j += 4)
+                        sg_put_unaligned_be32(pos, wrkPos + off + j);
+                }
+            } else if (iflag.zero)
+                memset(wrkPos, 0, res);
+            else if (iflag.ff)
+                memset(wrkPos, 0xff, res);
+            else {
+                int kk, jj;
+                const int jbump = sizeof(uint32_t);
+                long rn;
+                uint8_t * bp;
+
+                bp = wrkPos;
+                for (kk = 0; kk < blocks; ++kk, bp += blk_sz) {
+                    for (jj = 0; jj < blk_sz; jj += jbump) {
+                       /* mrand48 takes uniformly from [-2^31, 2^31) */
+#ifdef HAVE_SRAND48_R
+                        mrand48_r(&drand, &rn);
+#else
+                        rn = mrand48();
+#endif
+                        *((uint32_t *)(bp + jj)) = (uint32_t)rn;
+                    }
+                }
+            }
+            bytes_read = res;
+            in_full += blocks;
+        } else {
+            while (((res = read(infd, wrkPos, blocks * blk_sz)) < 0) &&
+                   ((EINTR == errno) || (EAGAIN == errno) ||
+                    (EBUSY == errno)))
+                ;
+            if (verbose > 2)
+                pr2serr("read(unix): count=%d, res=%d\n", blocks * blk_sz,
+                        res);
+            if (res < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "reading, skip=%" PRId64 " ",
+                         skip);
+                perror(ebuff);
+                ret = -1;
+                break;
+            } else if (res < blocks * blk_sz) {
+                dd_count = 0;
+                blocks = res / blk_sz;
+                if ((res % blk_sz) > 0) {
+                    blocks++;
+                    in_partial++;
+                }
+            }
+            bytes_read = res;
+            in_full += blocks;
+        }
+
+        if (0 == blocks)
+            break;      /* nothing read so leave loop */
+
+        if (out2f[0]) {
+            while (((res = write(out2fd, wrkPos, blocks * blk_sz)) < 0) &&
+                   ((EINTR == errno) || (EAGAIN == errno) ||
+                    (EBUSY == errno)))
+                ;
+            if (verbose > 2)
+                pr2serr("write to of2: count=%d, res=%d\n", blocks * blk_sz,
+                        res);
+            if (res < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "writing to of2, seek=%" PRId64
+                         " ", seek);
+                perror(ebuff);
+                ret = -1;
+                break;
+            }
+            bytes_of2 = res;
+        }
+
+        if (oflag.sparse && (dd_count > blocks) &&
+            (! (FT_DEV_NULL & out_type))) {
+            if (NULL == zeros_buff) {
+                zeros_buff = sg_memalign(blocks * blk_sz, 0, &free_zeros_buff,
+                                         false);
+                if (NULL == zeros_buff) {
+                    pr2serr("zeros_buff sg_memalign failed\n");
+                    ret = -1;
+                    break;
+                }
+            }
+            if (0 == memcmp(wrkPos, zeros_buff, blocks * blk_sz))
+                sparse_skip = true;
+        }
+        if (sparse_skip) {
+            if (FT_SG & out_type) {
+                out_sparse_num += blocks;
+                if (verbose > 2)
+                    pr2serr("sparse bypassing sg_write: seek blk=%" PRId64
+                            ", offset blks=%d\n", seek, blocks);
+            } else if (FT_DEV_NULL & out_type)
+                ;
+            else {
+                off64_t offset = (off64_t)blocks * blk_sz;
+                off64_t off_res;
+
+                if (verbose > 2)
+                    pr2serr("sparse bypassing write: seek=%" PRId64 ", rel "
+                            "offset=%" PRId64 "\n", (seek * blk_sz),
+                            (int64_t)offset);
+                off_res = lseek64(outfd, offset, SEEK_CUR);
+                if (off_res < 0) {
+                    pr2serr("sparse tried to bypass write: seek=%" PRId64
+                            ", rel offset=%" PRId64 " but ...\n",
+                            (seek * blk_sz), (int64_t)offset);
+                    perror("lseek64 on output");
+                    ret = SG_LIB_FILE_ERROR;
+                    break;
+                } else if (verbose > 4)
+                    pr2serr("oflag=sparse lseek64 result=%" PRId64 "\n",
+                            (int64_t)off_res);
+                out_sparse_num += blocks;
+            }
+        } else if (FT_SG & out_type) {
+            dio_tmp = oflag.dio;
+            retries_tmp = oflag.retries;
+            first = true;
+            while (1) {
+                ret = sg_write(outfd, wrkPos, blocks, seek, blk_sz, &oflag,
+                               &dio_tmp);
+                if ((0 == ret) || (SG_DD_BYPASS == ret))
+                    break;
+                if ((SG_LIB_CAT_NOT_READY == ret) ||
+                    (SG_LIB_SYNTAX_ERROR == ret))
+                    break;
+                else if ((-2 == ret) && first) {
+                    /* ENOMEM: find what's available and try that */
+                    if (ioctl(outfd, SG_GET_RESERVED_SIZE, &buf_sz) < 0) {
+                        perror("RESERVED_SIZE ioctls failed");
+                        break;
+                    }
+                    if (buf_sz < MIN_RESERVED_SIZE)
+                        buf_sz = MIN_RESERVED_SIZE;
+                    blocks_per = (buf_sz + blk_sz - 1) / blk_sz;
+                    if (blocks_per < blocks) {
+                        blocks = blocks_per;
+                        pr2serr("Reducing %s to %d blocks per loop\n",
+                                (do_verify ? "verify" : "write"), blocks);
+                    } else
+                        break;
+                } else if ((SG_LIB_CAT_UNIT_ATTENTION == ret) && first) {
+                    if (--max_uas > 0)
+                        pr2serr("Unit attention, continuing (w)\n");
+                    else {
+                        pr2serr("Unit attention, too many (w)\n");
+                        break;
+                    }
+                } else if ((SG_LIB_CAT_ABORTED_COMMAND == ret) && first) {
+                    if (--max_aborted > 0)
+                        pr2serr("Aborted command, continuing (w)\n");
+                    else {
+                        pr2serr("Aborted command, too many (w)\n");
+                        break;
+                    }
+                } else if (ret < 0)
+                    break;
+                else if (retries_tmp > 0) {
+                    pr2serr(">>> retrying a sgio %s, lba=0x%" PRIx64 "\n",
+                            (do_verify ? "verify" : "write"), (uint64_t)seek);
+                    --retries_tmp;
+                    ++num_retries;
+                    if (unrecovered_errs > 0)
+                        --unrecovered_errs;
+                } else
+                    break;
+                first = false;
+            }
+            if (SG_DD_BYPASS == ret)
+                ret = 0;        /* not bumping out_full */
+            else if (0 != ret) {
+                pr2serr("sg_write failed,%s seek=%" PRId64 "\n",
+                        ((-2 == ret) ? " try reducing bpt," : ""), seek);
+                break;
+            } else {
+                out_full += blocks;
+                if (oflag.dio && (! dio_tmp))
+                    dio_incomplete_count++;
+            }
+        } else if (FT_DEV_NULL & out_type)
+            out_full += blocks; /* act as if written out without error */
+        else {
+            while (((res = write(outfd, wrkPos, blocks * blk_sz)) < 0) &&
+                   ((EINTR == errno) || (EAGAIN == errno) ||
+                    (EBUSY == errno)))
+                ;
+            if (verbose > 2)
+                pr2serr("write(unix): count=%d, res=%d\n", blocks * blk_sz,
+                        res);
+            if (res < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "writing, seek=%" PRId64 " ",
+                         seek);
+                perror(ebuff);
+                ret = -1;
+                break;
+            } else if (res < blocks * blk_sz) {
+                pr2serr("output file probably full, seek=%" PRId64 " ", seek);
+                blocks = res / blk_sz;
+                out_full += blocks;
+                if ((res % blk_sz) > 0)
+                    out_partial++;
+                ret = -1;
+                break;
+            } else {
+                out_full += blocks;
+                bytes_of = res;
+            }
+        }
+#ifdef HAVE_POSIX_FADVISE
+        {
+            int rt, in_valid, out2_valid, out_valid;
+
+            in_valid = ((FT_OTHER == in_type) || (FT_BLOCK == in_type));
+            out2_valid = ((FT_OTHER == out2_type) || (FT_BLOCK == out2_type));
+            out_valid = ((FT_OTHER == out_type) || (FT_BLOCK == out_type));
+            if (iflag.nocache && (bytes_read > 0) && in_valid) {
+                rt = posix_fadvise(infd, 0, (skip * blk_sz) + bytes_read,
+                                   POSIX_FADV_DONTNEED);
+                // rt = posix_fadvise(infd, (skip * blk_sz), bytes_read,
+                                   // POSIX_FADV_DONTNEED);
+                // rt = posix_fadvise(infd, 0, 0, POSIX_FADV_DONTNEED);
+                if (rt)         /* returns error as result */
+                    pr2serr("posix_fadvise on read, skip=%" PRId64
+                            " ,err=%d\n", skip, rt);
+            }
+            if ((oflag.nocache & 2) && (bytes_of2 > 0) && out2_valid) {
+                rt = posix_fadvise(out2fd, 0, 0, POSIX_FADV_DONTNEED);
+                if (rt)
+                    pr2serr("posix_fadvise on of2, seek=%" PRId64
+                            " ,err=%d\n", seek, rt);
+            }
+            if ((oflag.nocache & 1) && (bytes_of > 0) && out_valid) {
+                rt = posix_fadvise(outfd, 0, 0, POSIX_FADV_DONTNEED);
+                if (rt)
+                    pr2serr("posix_fadvise on output, seek=%" PRId64
+                            " ,err=%d\n", seek, rt);
+            }
+        }
+#endif
+        if (dd_count > 0)
+            dd_count -= blocks;
+        skip += blocks;
+        seek += blocks;
+        if (progress > 0) {
+            if (check_progress()) {
+                calc_duration_throughput(true);
+                print_stats("");
+            }
+        }
+    } /* end of main loop that does the copy ... */
+
+    if (ret && penult_sparse_skip && (penult_blocks > 0)) {
+        /* if error and skipped last output due to sparse ... */
+        if ((FT_SG & out_type) || (FT_DEV_NULL & out_type))
+            ;
+        else {
+            /* ... try writing to extend ofile to length prior to error */
+            while (((res = write(outfd, zeros_buff, penult_blocks * blk_sz))
+                    < 0) && ((EINTR == errno) || (EAGAIN == errno) ||
+                             (EBUSY == errno)))
+                ;
+            if (verbose > 2)
+                pr2serr("write(unix, sparse after error): count=%d, res=%d\n",
+                        penult_blocks * blk_sz, res);
+            if (res < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "writing(sparse after error), "
+                        "seek=%" PRId64 " ", seek);
+                perror(ebuff);
+            }
+        }
+    }
+
+    if (do_sync) {
+        if (FT_SG & out_type) {
+            pr2serr(">> Synchronizing cache on %s\n", outf);
+            res = sg_ll_sync_cache_10(outfd, false, false, 0, 0, 0, true, 0);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Unit attention (out, sync cache), continuing\n");
+                res = sg_ll_sync_cache_10(outfd, false, false, 0, 0, 0,
+                                          false, 0);
+            }
+            if (0 != res)
+                pr2serr("Unable to synchronize cache\n");
+        }
+    }
+
+bypass_copy:
+    if (do_time)
+        calc_duration_throughput(false);
+    if (progress > 0)
+        pr2serr("\nCompleted:\n");
+
+    if (wrkBuff)
+        free(wrkBuff);
+    if (free_zeros_buff)
+        free(free_zeros_buff);
+    if ((STDIN_FILENO != infd) && (infd >= 0))
+        close(infd);
+    if (! ((STDOUT_FILENO == outfd) || (FT_DEV_NULL & out_type))) {
+        if (outfd >= 0)
+            close(outfd);
+    }
+    if (dry_run > 0)
+        goto bypass2;
+
+    if (0 != dd_count) {
+        pr2serr("Some error occurred,");
+        if (0 == ret)
+            ret = SG_LIB_CAT_OTHER;
+    }
+    print_stats("");
+    if (dio_incomplete_count) {
+        int fd;
+        char c;
+
+        pr2serr(">> Direct IO requested but incomplete %d times\n",
+                dio_incomplete_count);
+        if ((fd = open(sg_allow_dio, O_RDONLY)) >= 0) {
+            if (1 == read(fd, &c, 1)) {
+                if ('0' == c)
+                    pr2serr(">>> %s set to '0' but should be set to '1' for "
+                            "direct IO\n", sg_allow_dio);
+            }
+            close(fd);
+        }
+    }
+    if (sum_of_resids)
+        pr2serr(">> Non-zero sum of residual counts=%d\n", sum_of_resids);
+
+bypass2:
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_decode_sense.c b/src/sg_decode_sense.c
new file mode 100644
index 0000000..db54e0b
--- /dev/null
+++ b/src/sg_decode_sense.c
@@ -0,0 +1,547 @@
+/*
+ * Copyright (c) 2010-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <limits.h>
+#include <ctype.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_pr2serr.h"
+#include "sg_unaligned.h"
+
+
+static const char * version_str = "1.32 20220730";
+
+#define MY_NAME "sg_decode_sense"
+
+#define MAX_SENSE_LEN 8192 /* max descriptor format actually: 255+8 */
+
+static struct option long_options[] = {
+    {"binary", required_argument, 0, 'b'},
+    {"cdb", no_argument, 0, 'c'},
+    {"err", required_argument, 0, 'e'},
+    {"exit-status", required_argument, 0, 'e'},
+    {"exit_status", required_argument, 0, 'e'},
+    {"file", required_argument, 0, 'f'},
+    {"help", no_argument, 0, 'h'},
+    {"hex", no_argument, 0, 'H'},
+    {"in", required_argument, 0, 'i'},          /* don't advertise */
+    {"inhex", required_argument, 0, 'i'},       /* same as --file */
+    {"ignore-first", no_argument, 0, 'I'},
+    {"ignore_first", no_argument, 0, 'I'},
+    {"json", optional_argument, 0, 'j'},
+    {"nodecode", no_argument, 0, 'N'},
+    {"nospace", no_argument, 0, 'n'},
+    {"status", required_argument, 0, 's'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {"write", required_argument, 0, 'w'},
+    {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool do_binary;
+    bool do_cdb;
+    bool do_help;
+    bool no_decode;
+    bool no_space;
+    bool do_status;
+    bool verbose_given;
+    bool version_given;
+    bool err_given;
+    bool file_given;
+    bool ignore_first;
+    const char * fname;
+    int es_val;
+    int hex_count;
+    int sense_len;
+    int sstatus;
+    int verbose;
+    const char * wfname;
+    const char * no_space_str;
+    sgj_state json_st;
+    uint8_t sense[MAX_SENSE_LEN + 4];
+};
+
+static char concat_buff[1024];
+
+
+static void
+usage()
+{
+  pr2serr("Usage: sg_decode_sense [--binary=BFN] [--cdb] [--err=ES] "
+          "[--file=HFN]\n"
+          "                       [--help] [--hex] [--inhex=HFN] "
+          "[--ignore-first]\n"
+          "                       [--json[=JO]] [--nodecode] [--nospace] "
+          "[--status=SS]\n"
+          "                       [--verbose] [--version] [--write=WFN] "
+          "H1 H2 H3 ...\n"
+          "  where:\n"
+          "    --binary=BFN|-b BFN    BFN is a file name to read sense "
+          "data in\n"
+          "                          binary from. If BFN is '-' then read "
+          "from stdin\n"
+          "    --cdb|-c              decode given hex as cdb rather than "
+          "sense data\n"
+          "    --err=ES|-e ES        ES is Exit Status from utility in this "
+          "package\n"
+          "    --file=HFN|-f HFN     HFN is a file name from which to read "
+          "sense data\n"
+          "                          in ASCII hexadecimal. Interpret '-' "
+          "as stdin\n"
+          "    --help|-h             print out usage message\n"
+          "    --hex|-H              used together with --write=WFN, to "
+          "write out\n"
+          "                          C language style ASCII hex (instead "
+          "of binary).\n"
+          "                          Otherwise don't decode, output incoming "
+          "data in\n"
+          "                          hex (used '-HH' or '-HHH' for different "
+          "formats)\n"
+          "    --inhex=HFN|-i HFN    same as action as --file=HFN\n"
+          "    --ignore-first|-I     when reading hex (e.g. with --file=HFN) "
+          "skip\n"
+          "                          the first hexadecimal value on each "
+          "line\n"
+          "    --json[=JO]|-j[JO]    output in JSON instead of human "
+          "readable text.\n"
+          "                          Use --json=? for JSON help\n"
+          "    --nodecode|-N         do not decode, may be neither sense "
+          "nor cdb\n"
+          "    --nospace|-n          no spaces or other separators between "
+          "pairs of\n"
+          "                          hex digits (e.g. '3132330A')\n"
+          "    --status=SS |-s SS    SCSI status value in hex\n"
+          "    --verbose|-v          increase verbosity\n"
+          "    --version|-V          print version string then exit\n"
+          "    --write=WFN |-w WFN    write sense data in binary to WFN, "
+          "create if\n"
+          "                           required else truncate prior to "
+          "writing\n\n"
+          "Decodes SCSI sense data given on the command line as a sequence "
+          "of\nhexadecimal bytes (H1 H2 H3 ...) . Alternatively the sense "
+          "data can\nbe in a binary file or in a file containing ASCII "
+          "hexadecimal. If\n'--cdb' is given then interpret hex as SCSI CDB "
+          "rather than sense data.\n"
+          );
+}
+
+static int
+parse_cmd_line(struct opts_t *op, int argc, char *argv[])
+{
+    int c, n;
+    unsigned int ui;
+    long val;
+    char * avp;
+    char *endptr;
+
+    while (1) {
+        c = getopt_long(argc, argv, "b:ce:f:hHi:Ij::nNs:vVw:", long_options,
+                        NULL);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            if (op->fname) {
+                pr2serr("expect only one '--binary=BFN', '--file=HFN' or "
+                        "'--inhex=HFN' option\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->do_binary = true;
+            op->fname = optarg;
+            break;
+        case 'c':
+            op->do_cdb = true;
+            break;
+        case 'e':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 255)) {
+                pr2serr("--err= expected number from 0 to 255 inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->err_given = true;
+            op->es_val = n;
+            break;
+        case 'f':
+            if (op->fname) {
+                pr2serr("expect only one '--binary=BFN', '--file=HFN' or "
+                        "'--inhex=HFN' option\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->file_given = true;
+            op->fname = optarg;
+            break;
+        case 'h':
+        case '?':
+            op->do_help = true;
+            return 0;
+        case 'H':
+            op->hex_count++;
+            break;
+        case 'i':
+            if (op->fname) {
+                pr2serr("expect only one '--binary=BFN', '--file=HFN' or "
+                        "'--inhex=HFN' option\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->file_given = true;
+            op->fname = optarg;
+            break;
+        case 'I':
+            op->ignore_first = true;
+            break;
+       case 'j':
+            if (! sgj_init_state(&op->json_st, optarg)) {
+                int bad_char = op->json_st.first_bad_char;
+                char e[1500];
+
+                if (bad_char) {
+                    pr2serr("bad argument to --json= option, unrecognized "
+                            "character '%c'\n\n", bad_char);
+                }
+                sg_json_usage(0, e, sizeof(e));
+                pr2serr("%s", e);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'n':
+            op->no_space = true;
+            break;
+        case 'N':
+            op->no_decode = true;
+            break;
+        case 's':
+            if (1 != sscanf(optarg, "%x", &ui)) {
+                pr2serr("'--status=SS' expects a byte value\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (ui > 0xff) {
+                pr2serr("'--status=SS' byte value exceeds FF\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_status = true;
+            op->sstatus = ui;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'w':
+            op->wfname = optarg;
+            break;
+        default:
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (op->err_given)
+        goto the_end;
+
+    while (optind < argc) {
+        avp = argv[optind++];
+        if (op->no_space) {
+            if (op->no_space_str) {
+                if ('\0' == concat_buff[0]) {
+                    if (strlen(op->no_space_str) > sizeof(concat_buff)) {
+                        pr2serr("'--nospace' concat_buff overflow\n");
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    strcpy(concat_buff, op->no_space_str);
+                }
+                if ((strlen(concat_buff) + strlen(avp)) >=
+                    sizeof(concat_buff)) {
+                    pr2serr("'--nospace' concat_buff overflow\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                if (op->version_given)
+                    pr2serr("'--nospace' and found whitespace so "
+                            "concatenate\n");
+                strcat(concat_buff, avp);
+                op->no_space_str = concat_buff;
+            } else
+                op->no_space_str = avp;
+            continue;
+        }
+        val = strtol(avp, &endptr, 16);
+        if (*avp == '\0' || *endptr != '\0' || val < 0x00 || val > 0xff) {
+            pr2serr("Invalid byte '%s'\n", avp);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+
+        if (op->sense_len > MAX_SENSE_LEN) {
+            pr2serr("sense data too long (max. %d bytes)\n", MAX_SENSE_LEN);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        op->sense[op->sense_len++] = (uint8_t)val;
+    }
+the_end:
+    return 0;
+}
+
+/* Keep this format (e.g. 0xff,0x12,...) for backward compatibility */
+static void
+write2wfn(FILE * fp, struct opts_t * op)
+{
+    int k, n;
+    size_t s;
+    char b[128];
+
+    for (k = 0, n = 0; k < op->sense_len; ++k) {
+        n += sprintf(b + n, "0x%02x,", op->sense[k]);
+        if (15 == (k % 16)) {
+            b[n] = '\n';
+            s = fwrite(b, 1, n + 1, fp);
+            if ((int)s != (n + 1))
+                pr2serr("only able to write %d of %d bytes to %s\n",
+                        (int)s, n + 1, op->wfname);
+            n = 0;
+        }
+    }
+    if (n > 0) {
+        b[n] = '\n';
+        s = fwrite(b, 1, n + 1, fp);
+        if ((int)s != (n + 1))
+            pr2serr("only able to write %d of %d bytes to %s\n", (int)s,
+                    n + 1, op->wfname);
+    }
+}
+
+
+int
+main(int argc, char *argv[])
+{
+    bool as_json;
+    int k, err, blen;
+    int ret = 0;
+    unsigned int ui;
+    size_t s;
+    struct opts_t * op;
+    FILE * fp = NULL;
+    const char * cp;
+    sgj_state * jsp;
+    sgj_opaque_p jop = NULL;
+    uint8_t * free_op_buff = NULL;
+    char b[2048];
+
+    op = (struct opts_t *)sg_memalign(sizeof(*op), 0 /* page align */,
+				      &free_op_buff, false);
+    if (NULL == op) {
+        pr2serr("Unable to allocate heap for options structure\n");
+        ret = sg_convert_errno(ENOMEM);
+        goto clean_op;
+    }
+    blen = sizeof(b);
+    memset(b, 0, blen);
+    ret = parse_cmd_line(op, argc, argv);
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("version: %s\n", version_str);
+        goto clean_op;
+    }
+    if (ret != 0) {
+        usage();
+        goto clean_op;
+    } else if (op->do_help) {
+        usage();
+        goto clean_op;
+    }
+    as_json = op->json_st.pr_as_json;
+    jsp = &op->json_st;
+    if (as_json)
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+
+    if (op->err_given) {
+        char d[128];
+        const int dlen = sizeof(d);
+
+        if (! sg_exit2str(op->es_val, op->verbose > 1, dlen, d))
+            snprintf(d, dlen, "Unable to decode exit status %d", op->es_val);
+        if (1 & op->verbose) /* odd values of verbose print to stderr */
+            pr2serr("%s\n", d);
+        else    /* even values of verbose (including not given) to stdout */
+            printf("%s\n", d);
+        goto fini;
+    }
+
+    if (op->do_status) {
+        sg_get_scsi_status_str(op->sstatus, blen, b);
+        printf("SCSI status: %s\n", b);
+    }
+
+    if ((0 == op->sense_len) && op->no_space_str) {
+        if (op->verbose > 2)
+            pr2serr("no_space str: %s\n", op->no_space_str);
+        cp = op->no_space_str;
+        for (k = 0; isxdigit((uint8_t)cp[k]) &&
+                    isxdigit((uint8_t)cp[k + 1]); k += 2) {
+            if (1 != sscanf(cp + k, "%2x", &ui)) {
+                pr2serr("bad no_space hex string: %s\n", cp);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto fini;
+            }
+            op->sense[op->sense_len++] = (uint8_t)ui;
+        }
+    }
+
+    if ((0 == op->sense_len) && (! op->do_binary) && (! op->file_given)) {
+        if (op->do_status) {
+            ret = 0;
+            goto fini;
+        }
+        pr2serr(">> Need sense/cdb/arbitrary data on the command line or "
+                "in a file\n\n");
+        usage();
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto fini;
+    }
+    if (op->sense_len && (op->do_binary || op->file_given)) {
+        pr2serr(">> Need sense data on command line or in a file, not "
+                "both\n\n");
+        ret = SG_LIB_CONTRADICT;
+        goto fini;
+    }
+    if (op->do_binary && op->file_given) {
+        pr2serr(">> Either a binary file or a ASCII hexadecimal, file not "
+                "both\n\n");
+        ret = SG_LIB_CONTRADICT;
+        goto fini;
+    }
+
+    if (op->do_binary) {
+        fp = fopen(op->fname, "r");
+        if (NULL == fp) {
+            err = errno;
+            pr2serr("unable to open file: %s: %s\n", op->fname,
+                    safe_strerror(err));
+            ret = sg_convert_errno(err);
+            goto fini;
+        }
+        s = fread(op->sense, 1, MAX_SENSE_LEN, fp);
+        fclose(fp);
+        if (0 == s) {
+            pr2serr("read nothing from file: %s\n", op->fname);
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto fini;
+        }
+        op->sense_len = s;
+    } else if (op->file_given) {
+        ret = sg_f2hex_arr(op->fname, false, op->no_space, op->sense,
+                           &op->sense_len,
+                           (op->ignore_first ? -MAX_SENSE_LEN :
+                                               MAX_SENSE_LEN));
+        if (ret) {
+            pr2serr("unable to decode ASCII hex from file: %s\n", op->fname);
+            goto fini;
+        }
+    }
+
+    if (op->sense_len > 0) {
+        if (op->wfname || op->hex_count) {
+            if (op->wfname) {
+                if (NULL == ((fp = fopen(op->wfname, "w")))) {
+                    err =errno;
+                    perror("open");
+                    pr2serr("trying to write to %s\n", op->wfname);
+                    ret = sg_convert_errno(err);
+                    goto fini;
+                }
+            } else
+                fp = stdout;
+
+            if (op->wfname && (1 == op->hex_count))
+                write2wfn(fp, op);
+            else if (op->hex_count && (2 != op->hex_count))
+                dStrHexFp((const char *)op->sense, op->sense_len,
+                           ((1 == op->hex_count) ? 1 : -1), fp);
+            else if (op->hex_count)
+                dStrHexFp((const char *)op->sense, op->sense_len, 0, fp);
+            else {
+                s = fwrite(op->sense, 1, op->sense_len, fp);
+                if ((int)s != op->sense_len)
+                    pr2serr("only able to write %d of %d bytes to %s\n",
+                            (int)s, op->sense_len, op->wfname);
+            }
+            if (op->wfname)
+                fclose(fp);
+        } else if (op->no_decode) {
+            if (op->verbose > 1)
+                pr2serr("Not decoding as %s because --nodecode given\n",
+                        (op->do_cdb ? "cdb" : "sense"));
+        } else if (op->do_cdb) {
+            int sa, opcode;
+
+            opcode = op->sense[0];
+            if ((0x75 == opcode) || (0x7e == opcode) || (op->sense_len > 16))
+                sa = sg_get_unaligned_be16(op->sense + 8);
+            else if (op->sense_len > 1)
+                sa = op->sense[1] & 0x1f;
+            else
+                sa = 0;
+            sg_get_opcode_sa_name(opcode, sa, 0, blen, b);
+            printf("%s\n", b);
+        } else {
+            if (as_json) {
+                sgj_js_sense(jsp, jop, op->sense, op->sense_len);
+                if (jsp->pr_out_hr) {
+                    sg_get_sense_str(NULL, op->sense, op->sense_len,
+                                     op->verbose, blen, b);
+                     sgj_js_str_out(jsp, b, strlen(b));
+                }
+            } else {
+                sg_get_sense_str(NULL, op->sense, op->sense_len,
+                                 op->verbose, blen, b);
+                printf("%s\n", b);
+            }
+        }
+    }
+fini:
+   if (as_json) {
+        if (0 == op->hex_count)
+            sgj_js2file(&op->json_st, NULL, ret, stdout);
+        sgj_finish(jsp);
+    }
+clean_op:
+    if (free_op_buff)
+        free(free_op_buff);
+    return ret;
+}
diff --git a/src/sg_emc_trespass.c b/src/sg_emc_trespass.c
new file mode 100644
index 0000000..146e26b
--- /dev/null
+++ b/src/sg_emc_trespass.c
@@ -0,0 +1,176 @@
+/* The program allows the user to send a trespass command to change the
+ * LUN ownership from one Service-Processor to this one on an EMC
+ * CLARiiON and potentially other devices.
+ *
+ * Copyright (C) 2004-2018 Lars Marowsky-Bree <lmb@suse.de>
+ *
+ * Based on sg_start.c; credits from there also apply.
+ * Minor modifications for sg_lib, D. Gilbert 2004/10/19
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "0.23 20180219";
+
+static int debug = 0;
+
+#define TRESPASS_PAGE           0x22
+
+static int
+do_trespass(int fd, bool hr, bool short_cmd)
+{
+        uint8_t long_trespass_pg[] =
+                { 0, 0, 0, 0, 0, 0, 0, 0x00,
+                  TRESPASS_PAGE,        /* Page code */
+                  0x09,                 /* Page length - 2 */
+                  0x81,                 /* Trespass code + Honor reservation
+                                         * bit */
+                  0xff, 0xff,           /* Trespass target */
+                  0, 0, 0, 0, 0, 0      /* Reserved bytes / unknown */
+        };
+        uint8_t short_trespass_pg[] =
+                { 0, 0, 0, 0,
+                  TRESPASS_PAGE,        /* Page code */
+                  0x02,                 /* Page length - 2 */
+                  0x81,                 /* Trespass code + Honor reservation
+                                         * bit */
+                  0xff,                 /* Trespass target */
+        };
+        int res;
+        char b[80];
+
+        if (hr) {       /* override Trespass code + Honor reservation bit */
+                short_trespass_pg[6] = 0x01;
+                long_trespass_pg[10] = 0x01;
+        }
+        if (short_cmd)
+                res = sg_ll_mode_select6(fd, true /* pf */, false /* sp */,
+                                 short_trespass_pg, sizeof(short_trespass_pg),
+                                 true, (debug ? 2 : 0));
+        else
+                res = sg_ll_mode_select10(fd, true /* pf */, false /* sp */,
+                                 long_trespass_pg, sizeof(long_trespass_pg),
+                                 true, (debug ? 2 : 0));
+
+        switch (res) {
+        case 0:
+                if (debug)
+                        pr2serr("%s trespass successful\n",
+                                short_cmd ? "short" : "long");
+                break;
+        case SG_LIB_CAT_INVALID_OP:
+        case SG_LIB_CAT_ILLEGAL_REQ:
+                pr2serr("%s form trepass page failed, try again %s '-s' "
+                        "option\n", short_cmd ? "short" : "long",
+                        short_cmd ? "without" : "with");
+                break;
+        case SG_LIB_CAT_NOT_READY:
+                pr2serr("device not ready\n");
+                break;
+        case SG_LIB_CAT_UNIT_ATTENTION:
+                pr2serr("unit attention\n");
+                break;
+        default:
+                sg_get_category_sense_str(res, sizeof(b), b, debug);
+                pr2serr("%s trespass failed: %s\n",
+                        (short_cmd ? "short" : "long"), b);
+                break;
+        }
+        return res;
+}
+
+void usage ()
+{
+        pr2serr("Usage:  sg_emc_trespass [-d] [-hr] [-s] [-V] DEVICE\n"
+                "  Change ownership of a LUN from another SP to this one.\n"
+                "  EMC CLARiiON CX-/AX-family + FC5300/FC4500/FC4700.\n"
+                "    -d : output debug\n"
+                "    -hr: Set Honor Reservation bit\n"
+                "    -s : Send Short Trespass Command page (default: long)\n"
+                "         (for FC series)\n"
+                "    -V: print version string then exit\n"
+                "     DEVICE   sg or block device (latter in lk 2.6 or lk 3 "
+                "series)\n"
+                "        Example: sg_emc_trespass /dev/sda\n");
+        exit (1);
+}
+
+int main(int argc, char * argv[])
+{
+        char **argptr;
+        char * file_name = 0;
+        int k, fd;
+        bool hr = false;
+        bool short_cmd = false;
+        int ret = 0;
+
+        if (argc < 2)
+                usage ();
+
+        for (k = 1; k < argc; ++k) {
+                argptr = argv + k;
+                if (!strcmp (*argptr, "-d"))
+                        ++debug;
+                else if (!strcmp (*argptr, "-s"))
+                        short_cmd = true;
+                else if (!strcmp (*argptr, "-hr"))
+                        hr = true;
+                else if (!strcmp (*argptr, "-V")) {
+                        printf("Version string: %s\n", version_str);
+                        exit(0);
+                }
+                else if (*argv[k] == '-') {
+                        pr2serr("Unrecognized switch: %s\n", argv[k]);
+                        file_name = NULL;
+                        break;
+                }
+                else if (NULL == file_name)
+                        file_name = argv[k];
+                else {
+                        pr2serr("too many arguments\n");
+                        file_name = NULL;
+                        break;
+                }
+        }
+        if (NULL == file_name) {
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+
+        fd = open(file_name, O_RDWR | O_NONBLOCK);
+        if (fd < 0) {
+                pr2serr("Error trying to open %s\n", file_name);
+                perror("");
+                usage();
+                return SG_LIB_FILE_ERROR;
+        }
+
+        ret = do_trespass(fd, hr, short_cmd);
+
+        close (fd);
+        return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_format.c b/src/sg_format.c
new file mode 100644
index 0000000..4f3793b
--- /dev/null
+++ b/src/sg_format.c
@@ -0,0 +1,1729 @@
+/*
+ * sg_format : format a SCSI disk
+ *             potentially with a different number of blocks and block size
+ *
+ * formerly called blk512-linux.c (v0.4)
+ *
+ * Copyright (C) 2003  Grant Grundler    grundler at parisc-linux dot org
+ * Copyright (C) 2003  James Bottomley       jejb at parisc-linux dot org
+ * Copyright (C) 2005-2022  Douglas Gilbert   dgilbert at interlog dot com
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2, or (at your option)
+ *   any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * See https://www.t10.org for relevant standards and drafts. The most recent
+ * draft is SBC-4 revision 2.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#include <unistd.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+#include "sg_pt.h"
+
+static const char * version_str = "1.68 20220609";
+
+
+#define RW_ERROR_RECOVERY_PAGE 1  /* can give alternate with --mode=MP */
+
+#define SHORT_TIMEOUT           20   /* 20 seconds unless --wait given */
+#define FORMAT_TIMEOUT          (20 * 3600)       /* 20 hours ! */
+#define FOUR_TBYTE      (4LL * 1000 * 1000 * 1000 * 1000)
+#define LONG_FORMAT_TIMEOUT     (40 * 3600)       /* 40 hours */
+#define EIGHT_TBYTE     (FOUR_TBYTE * 2)
+#define VLONG_FORMAT_TIMEOUT    (80 * 3600)       /* 3 days, 8 hours */
+
+#define POLL_DURATION_SECS 60
+#define POLL_DURATION_FFMT_SECS 10
+#define DEF_POLL_TYPE_RS false     /* false -> test unit ready;
+                                      true -> request sense */
+#define MAX_BUFF_SZ     252
+
+/* FORMAT UNIT (SBC) and FORMAT MEDIUM (SSC) share the same opcode */
+#define SG_FORMAT_MEDIUM_CMD 0x4
+#define SG_FORMAT_MEDIUM_CMDLEN 6
+
+/* FORMAT WITH PRESET (new in sbc4r18) */
+#define SG_FORMAT_WITH_PRESET_CMD 0x38
+#define SG_FORMAT_WITH_PRESET_CMDLEN 10
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+
+struct opts_t {
+        bool cmplst;            /* -C value */
+        bool cmplst_given;
+        bool dry_run;           /* -d */
+        bool early;             /* -e */
+        bool fmtmaxlba;         /* -b (only with F_WITH_PRESET) */
+        bool fwait;             /* -w (negated form IMMED) */
+        bool ip_def;            /* -I */
+        bool long_lba;          /* -l */
+        bool mode6;             /* -6 */
+        bool pinfo;             /* -p, deprecated, prefer fmtpinfo */
+        bool poll_type;         /* -x 0|1 */
+        bool poll_type_given;
+        bool preset;            /* -E */
+        bool quick;             /* -Q */
+        bool do_rcap16;         /* -l */
+        bool resize;            /* -r */
+        bool rto_req;           /* -R, deprecated, prefer fmtpinfo */
+        bool verbose_given;
+        bool verify;            /* -y */
+        bool version_given;
+        int dcrt;              /* -D (can be given once or twice) */
+        int lblk_sz;            /* -s value */
+        int ffmt;               /* -t value; fast_format if > 0 */
+        int fmtpinfo;
+        int format;             /* -F */
+        uint32_t p_id;          /* set by argument of --preset=id  */
+        int mode_page;          /* -M value */
+        int pfu;                /* -P value */
+        int pie;                /* -q value */
+        int sec_init;           /* -S */
+        int tape;               /* -T <format>, def: -1 */
+        int timeout;            /* -m SECS, def: depends on IMMED bit */
+        int verbose;            /* -v */
+        int64_t blk_count;      /* -c value */
+        int64_t total_byte_count;      /* from READ CAPACITY command */
+        const char * device_name;
+};
+
+
+
+static struct option long_options[] = {
+        {"count", required_argument, 0, 'c'},
+        {"cmplst", required_argument, 0, 'C'},
+        {"dcrt", no_argument, 0, 'D'},
+        {"dry-run", no_argument, 0, 'd'},
+        {"dry_run", no_argument, 0, 'd'},
+        {"early", no_argument, 0, 'e'},
+        {"ffmt", required_argument, 0, 't'},
+        {"fmtmaxlba", no_argument, 0, 'b'},
+        {"fmtpinfo", required_argument, 0, 'f'},
+        {"format", no_argument, 0, 'F'},
+        {"help", no_argument, 0, 'h'},
+        {"ip-def", no_argument, 0, 'I'},
+        {"ip_def", no_argument, 0, 'I'},
+        {"long", no_argument, 0, 'l'},
+        {"mode", required_argument, 0, 'M'},
+        {"pinfo", no_argument, 0, 'p'},
+        {"pfu", required_argument, 0, 'P'},
+        {"pie", required_argument, 0, 'q'},
+        {"poll", required_argument, 0, 'x'},
+        {"preset", required_argument, 0, 'E'},
+        {"quick", no_argument, 0, 'Q'},
+        {"resize", no_argument, 0, 'r'},
+        {"rto_req", no_argument, 0, 'R'},
+        {"security", no_argument, 0, 'S'},
+        {"six", no_argument, 0, '6'},
+        {"size", required_argument, 0, 's'},
+        {"tape", required_argument, 0, 'T'},
+        {"timeout", required_argument, 0, 'm'},
+        {"verbose", no_argument, 0, 'v'},
+        {"verify", no_argument, 0, 'y'},
+        {"version", no_argument, 0, 'V'},
+        {"wait", no_argument, 0, 'w'},
+        {0, 0, 0, 0},
+};
+
+static const char * fu_s = "Format unit";
+static const char * fm_s = "Format medium";
+static const char * fwp_s = "Format with preset";
+
+
+static void
+usage()
+{
+        printf("Usage:\n"
+               "  sg_format [--cmplst=0|1] [--count=COUNT] [--dcrt] "
+               "[--dry-run] [--early]\n"
+               "            [--ffmt=FFMT] [--fmtmaxlba] [--fmtpinfo=FPI] "
+               "[--format] [--help]\n"
+               "            [--ip-def] [--long] [--mode=MP] [--pfu=PFU] "
+               "[--pie=PIE]\n"
+               "            [--pinfo] [--poll=PT] [--preset=ID] [--quick] "
+               "[--resize]\n"
+               "            [--rto_req] [--security] [--six] [--size=LB_SZ] "
+               "[--tape=FM]\n"
+               "            [--timeout=SECS] [--verbose] [--verify] "
+               "[--version] [--wait]\n"
+               "            DEVICE\n"
+               "  where:\n"
+               "    --cmplst=0|1\n"
+               "      -C 0|1        sets CMPLST bit in format cdb "
+               "(def: 1; if FFMT: 0)\n"
+               "    --count=COUNT|-c COUNT    number of blocks to report "
+               "after format or\n"
+               "                              resize. Format default is "
+               "same as current\n"
+               "    --dcrt|-D       disable certification (doesn't "
+               "verify media)\n"
+               "                    use twice to enable certification and "
+               "set FOV bit\n"
+               "    --dry-run|-d    bypass device modifying commands (i.e. "
+               "don't format)\n"
+               "    --early|-e      exit once format started (user can "
+               "monitor progress)\n"
+               "    --ffmt=FFMT|-t FFMT    fast format (def: 0 -> slow, "
+               "may visit every\n"
+               "                           block). 1 and 2 are fast formats; "
+               "1: after\n"
+               "                           format, unwritten data read "
+               "without error\n"
+               "    --fmtpinfo=FPI|-f FPI    FMTPINFO field value "
+               "(default: 0)\n"
+               "    --format|-F     do FORMAT UNIT (default: report current "
+               "count and size)\n"
+               "                    use thrice for FORMAT UNIT command "
+               "only\n"
+               "    --fmtmaxlba|-b    sets FMTMAXLBA field in FORMAT WITH "
+               "PRESET\n"
+               "    --help|-h       prints out this usage message\n"
+               "    --ip-def|-I     use default initialization pattern\n"
+               "    --long|-l       allow for 64 bit lbas (default: assume "
+               "32 bit lbas)\n"
+               "    --mode=MP|-M MP     mode page (def: 1 -> RW error "
+               "recovery mpage)\n"
+               "    --pie=PIE|-q PIE    Protection Information Exponent "
+               "(default: 0)\n"
+               "    --pinfo|-p      set upper bit of FMTPINFO field\n"
+               "                    (deprecated, use '--fmtpinfo=FPI' "
+               "instead)\n"
+               "    --poll=PT|-x PT    PT is poll type, 0 for test unit "
+               "ready\n"
+               "                       1 for request sense (def: 0 (1 "
+               "for tape and\n"
+               "                       format with preset))\n");
+        printf("    --preset=ID|-E ID    do FORMAT WITH PRESET command "
+               "with PRESET\n"
+               "                         IDENTIFIER field set to ID\n"
+               "    --quick|-Q      start format without pause for user "
+               "intervention\n"
+               "                    (i.e. no time to reconsider)\n"
+               "    --resize|-r     resize (rather than format) to COUNT "
+               "value\n"
+               "    --rto_req|-R    set lower bit of FMTPINFO field\n"
+               "                    (deprecated use '--fmtpinfo=FPI' "
+               "instead)\n"
+               "    --security|-S    set security initialization (SI) bit\n"
+               "    --six|-6        use 6 byte MODE SENSE/SELECT to probe "
+               "disk\n"
+               "                    (def: use 10 byte MODE SENSE/SELECT)\n"
+               "    --size=LB_SZ|-s LB_SZ    bytes per logical block, "
+               "defaults to DEVICE's\n"
+               "                           current logical block size. Only "
+               "needed to\n"
+               "                           change current logical block "
+               "size\n"
+               "    --tape=FM|-T FM    request FORMAT MEDIUM with FORMAT "
+               "field set\n"
+               "                       to FM (def: 0 --> default format)\n"
+               "    --timeout=SECS|-m SECS    FORMAT UNIT/MEDIUM command "
+               "timeout in seconds\n"
+               "    --verbose|-v    increase verbosity\n"
+               "    --verify|-y     sets VERIFY bit in FORMAT MEDIUM (tape)\n"
+               "    --version|-V    print version details and exit\n"
+               "    --wait|-w       format commands wait until format "
+               "operations complete\n"
+               "                    (default: set IMMED=1 and poll with "
+               "Test Unit Ready)\n\n"
+               "\tExample: sg_format --format /dev/sdc\n\n"
+               "This utility formats a SCSI disk [FORMAT UNIT] or resizes "
+               "it. Alternatively\nif '--tape=FM' is given formats a tape "
+               "[FORMAT MEDIUM]. Another alternative\nis doing the FORMAT "
+               "WITH PRESET command when '--preset=ID' is given.\n\n");
+        printf("WARNING: This utility will destroy all the data on the "
+               "DEVICE when\n\t '--format', '--tape=FM' or '--preset=ID' "
+               "is given. Double check\n\t that you have specified the "
+               "correct DEVICE.\n");
+}
+
+/* Invokes a SCSI FORMAT MEDIUM command (SSC).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_format_medium(int sg_fd, bool verify, bool immed, int format,
+                    void * paramp, int transfer_len, int timeout, bool noisy,
+                    int verbose)
+{
+        int ret, res, sense_cat;
+        uint8_t fm_cdb[SG_FORMAT_MEDIUM_CMDLEN] =
+                                  {SG_FORMAT_MEDIUM_CMD, 0, 0, 0, 0, 0};
+        uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+        struct sg_pt_base * ptvp;
+
+        if (verify)
+                fm_cdb[1] |= 0x2;
+        if (immed)
+                fm_cdb[1] |= 0x1;
+        if (format)
+                fm_cdb[2] |= (0xf & format);
+        if (transfer_len > 0)
+                sg_put_unaligned_be16(transfer_len, fm_cdb + 3);
+        if (verbose) {
+                char b[128];
+
+                pr2serr("    %s cdb: %s\n", fm_s,
+                        sg_get_command_str(fm_cdb, SG_FORMAT_MEDIUM_CMDLEN,
+                                           false, sizeof(b), b));
+        }
+
+        ptvp = construct_scsi_pt_obj();
+        if (NULL == ptvp) {
+                pr2serr("%s: out of memory\n", __func__);
+                return sg_convert_errno(ENOMEM);
+        }
+        set_scsi_pt_cdb(ptvp, fm_cdb, sizeof(fm_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+        set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, transfer_len);
+        res = do_scsi_pt(ptvp, sg_fd, timeout, verbose);
+        ret = sg_cmds_process_resp(ptvp, fm_s, res, noisy, verbose,
+                                   &sense_cat);
+        if (-1 == ret) {
+            if (get_scsi_pt_transport_err(ptvp))
+                ret = SG_LIB_TRANSPORT_ERROR;
+            else
+                ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+        } else if (-2 == ret) {
+                switch (sense_cat) {
+                case SG_LIB_CAT_RECOVERED:
+                case SG_LIB_CAT_NO_SENSE:
+                        ret = 0;
+                        break;
+                default:
+                        ret = sense_cat;
+                        break;
+                }
+        } else {
+                ret = 0;
+                if (verbose)
+                        pr2serr("%s command %s without error\n", fm_s,
+                                (immed ? "launched" : "completed"));
+        }
+        destruct_scsi_pt_obj(ptvp);
+        return ret;
+}
+
+/* Invokes a SCSI FORMAT WITH PRESET command (SBC).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_format_with_preset(int sg_fd, bool immed, bool fmtmaxlba,
+                         uint32_t preset_id, int timeout, bool noisy,
+                         int verbose)
+{
+        int ret, res, sense_cat;
+        uint8_t fwp_cdb[SG_FORMAT_WITH_PRESET_CMDLEN] =
+                     {SG_FORMAT_WITH_PRESET_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+        struct sg_pt_base * ptvp;
+
+        if (immed)
+                fwp_cdb[1] |= 0x80;
+        if (fmtmaxlba)
+                fwp_cdb[1] |= 0x40;
+        if (preset_id > 0)
+                sg_put_unaligned_be32(preset_id, fwp_cdb + 2);
+        if (verbose) {
+                char b[128];
+
+                pr2serr("    %s cdb: %s\n", fwp_s,
+                        sg_get_command_str(fwp_cdb,
+                                           SG_FORMAT_WITH_PRESET_CMDLEN,
+                                           false, sizeof(b), b));
+        }
+        ptvp = construct_scsi_pt_obj();
+        if (NULL == ptvp) {
+                pr2serr("%s: out of memory\n", __func__);
+                return sg_convert_errno(ENOMEM);
+        }
+        set_scsi_pt_cdb(ptvp, fwp_cdb, sizeof(fwp_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+        res = do_scsi_pt(ptvp, sg_fd, timeout, verbose);
+        ret = sg_cmds_process_resp(ptvp, fwp_s, res, noisy, verbose,
+                                   &sense_cat);
+        if (-1 == ret) {
+            if (get_scsi_pt_transport_err(ptvp))
+                ret = SG_LIB_TRANSPORT_ERROR;
+            else
+                ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+        } else if (-2 == ret) {
+                switch (sense_cat) {
+                case SG_LIB_CAT_RECOVERED:
+                case SG_LIB_CAT_NO_SENSE:
+                        ret = 0;
+                        break;
+                default:
+                        ret = sense_cat;
+                        break;
+                }
+        } else {
+                ret = 0;
+                if (verbose)
+                        pr2serr("%s command %s without error\n", fwp_s,
+                                (immed ? "launched" : "completed"));
+        }
+        destruct_scsi_pt_obj(ptvp);
+        return ret;
+}
+
+/* Return 0 on success, else see sg_ll_format_unit_v2() */
+static int
+scsi_format_unit(int fd, const struct opts_t * op)
+{
+        bool need_param_lst, longlist, ip_desc, first;
+        bool immed = ! op->fwait;
+        int res, progress, pr, rem, param_sz, off, resp_len, tmout;
+        int poll_wait_secs;
+        int vb = op->verbose;
+        const int SH_FORMAT_HEADER_SZ = 4;
+        const int LONG_FORMAT_HEADER_SZ = 8;
+        const int INIT_PATTERN_DESC_SZ = 4;
+        const int max_param_sz = LONG_FORMAT_HEADER_SZ + INIT_PATTERN_DESC_SZ;
+        uint8_t * param;
+        uint8_t * free_param = NULL;
+        char b[80];
+
+        param = sg_memalign(max_param_sz, 0, &free_param, false);
+        if (NULL == param) {
+                pr2serr("%s: unable to obtain heap for parameter list\n",
+                        __func__);
+                return sg_convert_errno(ENOMEM);
+        }
+        if (immed)
+                tmout = SHORT_TIMEOUT;
+        else {
+                if (op->total_byte_count > EIGHT_TBYTE)
+                        tmout = VLONG_FORMAT_TIMEOUT;
+                else if (op->total_byte_count > FOUR_TBYTE)
+                        tmout = LONG_FORMAT_TIMEOUT;
+                else
+                        tmout = FORMAT_TIMEOUT;
+        }
+        if (op->timeout > tmout)
+                tmout = op->timeout;
+        longlist = (op->pie > 0);  /* only set LONGLIST if PI_EXPONENT>0 */
+        ip_desc = (op->ip_def || op->sec_init);
+        off = longlist ? LONG_FORMAT_HEADER_SZ : SH_FORMAT_HEADER_SZ;
+        param[0] = op->pfu & 0x7;  /* PROTECTION_FIELD_USAGE (bits 2-0) */
+        param[1] = (immed ? 0x2 : 0); /* FOV=0, [DPRY,DCRT,STPF,IP=0] */
+        if (1 == op->dcrt)
+                param[1] |= 0xa0;     /* FOV=1, DCRT=1 */
+        else if (op->dcrt > 1)
+                param[1] |= 0x80;     /* FOV=1, DCRT=0 */
+        if (ip_desc) {
+                param[1] |= 0x88;     /* FOV=1, IP=1 */
+                if (op->sec_init)
+                        param[off + 0] = 0x20; /* SI=1 in IP desc */
+        }
+        if (longlist)
+                param[3] = (op->pie & 0xf);/* PROTECTION_INTERVAL_EXPONENT */
+        /* with the long parameter list header, P_I_INFORMATION is always 0 */
+
+        need_param_lst = (immed || op->cmplst || (op->dcrt > 0) || ip_desc ||
+                          (op->pfu > 0) || (op->pie > 0));
+        param_sz = need_param_lst ?
+                    (off + (ip_desc ? INIT_PATTERN_DESC_SZ : 0)) : 0;
+
+        if (op->dry_run) {
+                res = 0;
+                pr2serr("Due to --dry-run option bypassing FORMAT UNIT "
+                        "command\n");
+                if (vb) {
+                        if (need_param_lst) {
+                                pr2serr("  %s would have received parameter "
+                                        "list: ", fu_s);
+                                hex2stderr(param, max_param_sz, -1);
+                        } else
+                                pr2serr("  %s would not have received a "
+                                        "parameter list\n", fu_s);
+                        pr2serr("  %s cdb fields: fmtpinfo=0x%x, "
+                                "longlist=%d, fmtdata=%d, cmplst=%d, "
+                                "ffmt=%d [timeout=%d secs]\n", fu_s,
+                                op->fmtpinfo, longlist, need_param_lst,
+                                op->cmplst, op->ffmt, tmout);
+                }
+        } else
+                res = sg_ll_format_unit_v2(fd, op->fmtpinfo, longlist,
+                                           need_param_lst, op->cmplst, 0,
+                                           op->ffmt, tmout, param, param_sz,
+                                           true, vb);
+        if (free_param)
+            free(free_param);
+
+        if (res) {
+                sg_get_category_sense_str(res, sizeof(b), b, vb);
+                pr2serr("%s command: %s\n", fu_s, b);
+                return res;
+        } else if (op->verbose)
+                pr2serr("%s command %s without error\n", fu_s,
+                        (immed ? "launched" : "completed"));
+        if (! immed)
+                return 0;
+
+        if (! op->dry_run)
+                printf("\n%s has started\n", fu_s);
+
+        if (op->early) {
+                if (immed)
+                        printf("%s continuing,\n    request sense or "
+                               "test unit ready can be used to monitor "
+                               "progress\n", fu_s);
+                return 0;
+        }
+
+        if (op->dry_run) {
+                printf("No point in polling for progress, so exit\n");
+                return 0;
+        }
+        poll_wait_secs = op->ffmt ? POLL_DURATION_FFMT_SECS :
+                                    POLL_DURATION_SECS;
+        if (! op->poll_type) {
+                for(first = true; ; first = false) {
+                        sg_sleep_secs(poll_wait_secs);
+                        progress = -1;
+                        res = sg_ll_test_unit_ready_progress(fd, 0, &progress,
+                                             true, (vb > 1) ? (vb - 1) : 0);
+                        if (progress >= 0) {
+                                pr = (progress * 100) / 65536;
+                                rem = ((progress * 100) % 65536) / 656;
+                                printf("%s in progress, %d.%02d%% done\n",
+                                       fu_s, pr, rem);
+                        } else {
+                                if (first && op->verbose)
+                                        pr2serr("%s seems to be successful "
+                                                "and finished quickly\n",
+                                                fu_s);
+                                break;
+                        }
+                }
+        }
+        if (op->poll_type || (SG_LIB_CAT_NOT_READY == res)) {
+                uint8_t * reqSense;
+                uint8_t * free_reqSense = NULL;
+
+                reqSense = sg_memalign(MAX_BUFF_SZ, 0, &free_reqSense, false);
+                if (NULL == reqSense) {
+                        pr2serr("%s: unable to obtain heap for Request "
+                                "Sense\n", __func__);
+                        return sg_convert_errno(ENOMEM);
+                }
+                for(first = true; ; first = false) {
+                        sg_sleep_secs(poll_wait_secs);
+                        memset(reqSense, 0x0, MAX_BUFF_SZ);
+                        res = sg_ll_request_sense(fd, false, reqSense,
+                                                  MAX_BUFF_SZ, false,
+                                                  (vb > 1) ? (vb - 1) : 0);
+                        if (res) {
+                                pr2serr("polling with Request Sense command "
+                                        "failed [res=%d]\n", res);
+                                break;
+                        }
+                        resp_len = reqSense[7] + 8;
+                        if (vb > 1) {
+                                pr2serr("Parameter data in hex:\n");
+                                hex2stderr(reqSense, resp_len, 1);
+                        }
+                        progress = -1;
+                        sg_get_sense_progress_fld(reqSense, resp_len,
+                                                  &progress);
+                        if (progress >= 0) {
+                                pr = (progress * 100) / 65536;
+                                rem = ((progress * 100) % 65536) / 656;
+                                printf("%s in progress, %d.%02d%% done\n",
+                                       fu_s, pr, rem);
+                        } else {
+                                if (first && op->verbose)
+                                        pr2serr("%s seems to be successful "
+                                                "and finished quickly\n",
+                                                fu_s);
+                                break;
+                        }
+                }
+                if (free_reqSense)
+                        free(free_reqSense);
+        }
+        printf("FORMAT UNIT Complete\n");
+        return 0;
+}
+
+/* Return 0 on success, else see sg_ll_format_medium() above */
+static int
+scsi_format_medium(int fd, const struct opts_t * op)
+{
+        bool first;
+        bool immed = ! op->fwait;
+        int res, progress, pr, rem, resp_len, tmout;
+        int vb = op->verbose;
+        char b[80];
+
+        if (immed)
+                tmout = SHORT_TIMEOUT;
+        else {
+                if (op->total_byte_count > EIGHT_TBYTE)
+                        tmout = VLONG_FORMAT_TIMEOUT;
+                else if (op->total_byte_count > FOUR_TBYTE)
+                        tmout = LONG_FORMAT_TIMEOUT;
+                else
+                        tmout = FORMAT_TIMEOUT;
+        }
+        if (op->timeout > tmout)
+                tmout = op->timeout;
+        if (op->dry_run) {
+                res = 0;
+                pr2serr("Due to --dry-run option bypassing %s command\n",
+                        fm_s);
+        } else
+                res = sg_ll_format_medium(fd, op->verify, immed,
+                                          0xf & op->tape, NULL, 0, tmout,
+                                          true, vb);
+        if (res) {
+                sg_get_category_sense_str(res, sizeof(b), b, vb);
+                pr2serr("%s command: %s\n", fm_s, b);
+                return res;
+        }
+        if (! immed)
+                return 0;
+
+        if (! op->dry_run)
+                printf("\n%s has started\n", fm_s);
+        if (op->early) {
+                if (immed)
+                        printf("%s continuing,\n    request sense or "
+                               "test unit ready can be used to monitor "
+                               "progress\n", fm_s);
+                return 0;
+        }
+
+        if (op->dry_run) {
+                printf("No point in polling for progress, so exit\n");
+                return 0;
+        }
+        if (! op->poll_type) {
+                for(first = true; ; first = false) {
+                        sg_sleep_secs(POLL_DURATION_SECS);
+                        progress = -1;
+                        res = sg_ll_test_unit_ready_progress(fd, 0, &progress,
+                                             true, (vb > 1) ? (vb - 1) : 0);
+                        if (progress >= 0) {
+                                pr = (progress * 100) / 65536;
+                                rem = ((progress * 100) % 65536) / 656;
+                                printf("%s in progress, %d.%02d%% done\n",
+                                       fm_s, pr, rem);
+                        } else {
+                                if (first && op->verbose)
+                                        pr2serr("%s seems to be successful "
+                                                "and finished quickly\n",
+                                                fm_s);
+                                break;
+                        }
+                }
+        }
+        if (op->poll_type || (SG_LIB_CAT_NOT_READY == res)) {
+                uint8_t * reqSense;
+                uint8_t * free_reqSense = NULL;
+
+                reqSense = sg_memalign(MAX_BUFF_SZ, 0, &free_reqSense, false);
+                if (NULL == reqSense) {
+                        pr2serr("%s: unable to obtain heap for Request "
+                                "Sense\n", __func__);
+                        return sg_convert_errno(ENOMEM);
+                }
+                for(first = true; ; first = false) {
+                        sg_sleep_secs(POLL_DURATION_SECS);
+                        memset(reqSense, 0x0, MAX_BUFF_SZ);
+                        res = sg_ll_request_sense(fd, false, reqSense,
+                                                  MAX_BUFF_SZ, false,
+                                                  (vb > 1) ? (vb - 1) : 0);
+                        if (res) {
+                                pr2serr("polling with Request Sense command "
+                                        "failed [res=%d]\n", res);
+                                break;
+                        }
+                        resp_len = reqSense[7] + 8;
+                        if (vb > 1) {
+                                pr2serr("Parameter data in hex:\n");
+                                hex2stderr(reqSense, resp_len, 1);
+                        }
+                        progress = -1;
+                        sg_get_sense_progress_fld(reqSense, resp_len,
+                                                  &progress);
+                        if (progress >= 0) {
+                                pr = (progress * 100) / 65536;
+                                rem = ((progress * 100) % 65536) / 656;
+                                printf("%s in progress, %d.%02d%% done\n",
+                                       fm_s, pr, rem);
+                        } else {
+                                if (first && op->verbose)
+                                        pr2serr("%s seems to be successful "
+                                                "and finished quickly\n",
+                                                fm_s);
+                                break;
+                        }
+                }
+                if (free_reqSense)
+                        free(free_reqSense);
+        }
+        printf("FORMAT MEDIUM Complete\n");
+        return 0;
+}
+
+/* Return 0 on success, else see sg_ll_format_medium() above */
+static int
+scsi_format_with_preset(int fd, const struct opts_t * op)
+{
+        bool first;
+        bool immed = ! op->fwait;
+        int res, progress, pr, rem, resp_len, tmout;
+        int vb = op->verbose;
+        char b[80];
+
+        if (immed)
+                tmout = SHORT_TIMEOUT;
+        else {
+                if (op->total_byte_count > EIGHT_TBYTE)
+                        tmout = VLONG_FORMAT_TIMEOUT;
+                else if (op->total_byte_count > FOUR_TBYTE)
+                        tmout = LONG_FORMAT_TIMEOUT;
+                else
+                        tmout = FORMAT_TIMEOUT;
+        }
+        if (op->timeout > tmout)
+                tmout = op->timeout;
+        if (op->dry_run) {
+                res = 0;
+                pr2serr("Due to --dry-run option bypassing FORMAT WITH "
+                        "PRESET command\n");
+        } else
+                res = sg_ll_format_with_preset(fd, immed, op->fmtmaxlba,
+                                               op->p_id, tmout, true, vb);
+        if (res) {
+                sg_get_category_sense_str(res, sizeof(b), b, vb);
+                pr2serr("%s command: %s\n", fwp_s, b);
+                return res;
+        }
+        if (! immed)
+                return 0;
+
+        if (! op->dry_run)
+                printf("\n%s has started\n", fwp_s);
+        if (op->early) {
+                if (immed)
+                        printf("%s continuing,\n    Request sense can "
+                               "be used to monitor progress\n", fwp_s);
+                return 0;
+        }
+
+        if (op->dry_run) {
+                printf("No point in polling for progress, so exit\n");
+                return 0;
+        }
+        if (! op->poll_type) {
+                for(first = true; ; first = false) {
+                        sg_sleep_secs(POLL_DURATION_SECS);
+                        progress = -1;
+                        res = sg_ll_test_unit_ready_progress(fd, 0, &progress,
+                                             true, (vb > 1) ? (vb - 1) : 0);
+                        if (progress >= 0) {
+                                pr = (progress * 100) / 65536;
+                                rem = ((progress * 100) % 65536) / 656;
+                                printf("%s in progress, %d.%02d%% done\n",
+                                       fwp_s, pr, rem);
+                        } else {
+                                if (first && op->verbose)
+                                        pr2serr("%s seems to be successful "
+                                                "and finished quickly\n",
+                                                fwp_s);
+                                break;
+                        }
+                }
+        }
+        if (op->poll_type || (SG_LIB_CAT_NOT_READY == res)) {
+                uint8_t * reqSense;
+                uint8_t * free_reqSense = NULL;
+
+                reqSense = sg_memalign(MAX_BUFF_SZ, 0, &free_reqSense, false);
+                if (NULL == reqSense) {
+                        pr2serr("%s: unable to obtain heap for Request "
+                                "Sense\n", __func__);
+                        return sg_convert_errno(ENOMEM);
+                }
+                for(first = true; ; first = false) {
+                        sg_sleep_secs(POLL_DURATION_SECS);
+                        memset(reqSense, 0x0, MAX_BUFF_SZ);
+                        res = sg_ll_request_sense(fd, false, reqSense,
+                                                  MAX_BUFF_SZ, false,
+                                                  (vb > 1) ? (vb - 1) : 0);
+                        if (res) {
+                                pr2serr("polling with Request Sense command "
+                                        "failed [res=%d]\n", res);
+                                break;
+                        }
+                        resp_len = reqSense[7] + 8;
+                        if (vb > 1) {
+                                pr2serr("Parameter data in hex:\n");
+                                hex2stderr(reqSense, resp_len, 1);
+                        }
+                        progress = -1;
+                        sg_get_sense_progress_fld(reqSense, resp_len,
+                                                  &progress);
+                        if (progress >= 0) {
+                                pr = (progress * 100) / 65536;
+                                rem = ((progress * 100) % 65536) / 656;
+                                printf("%s in progress, %d.%02d%% done\n",
+                                       fwp_s, pr, rem);
+                        } else {
+                                if (first && op->verbose)
+                                        pr2serr("%s seems to be successful "
+                                                "and finished quickly\n",
+                                                fwp_s);
+                                break;
+                        }
+                }
+                if (free_reqSense)
+                        free(free_reqSense);
+        }
+        printf("FORMAT WITH PRESET Complete\n");
+        return 0;
+}
+
+#define VPD_DEVICE_ID 0x83
+#define VPD_ASSOC_LU 0
+#define VPD_ASSOC_TPORT 1
+#define TPROTO_ISCSI 5
+
+static char *
+get_lu_name(const uint8_t * bp, int u_len, char * b, int b_len)
+{
+        int len, off, sns_dlen, dlen, k;
+        uint8_t u_sns[512];
+        char * cp;
+
+        len = u_len - 4;
+        bp += 4;
+        off = -1;
+        if (0 == sg_vpd_dev_id_iter(bp, len, &off, VPD_ASSOC_LU,
+                                    8 /* SCSI name string (sns) */,
+                                    3 /* UTF-8 */)) {
+                sns_dlen = bp[off + 3];
+                memcpy(u_sns, bp + off + 4, sns_dlen);
+                /* now want to check if this is iSCSI */
+                off = -1;
+                if (0 == sg_vpd_dev_id_iter(bp, len, &off, VPD_ASSOC_TPORT,
+                                            8 /* SCSI name string (sns) */,
+                                            3 /* UTF-8 */)) {
+                        if ((0x80 & bp[1]) &&
+                            (TPROTO_ISCSI == (bp[0] >> 4))) {
+                                snprintf(b, b_len, "%.*s", sns_dlen, u_sns);
+                                return b;
+                        }
+                }
+        } else
+                sns_dlen = 0;
+        if (0 == sg_vpd_dev_id_iter(bp, len, &off, VPD_ASSOC_LU,
+                                    3 /* NAA */, 1 /* binary */)) {
+                dlen = bp[off + 3];
+                if (! ((8 == dlen) || (16 ==dlen)))
+                        return b;
+                cp = b;
+                for (k = 0; ((k < dlen) && (b_len > 1)); ++k) {
+                        snprintf(cp, b_len, "%02x", bp[off + 4 + k]);
+                        cp += 2;
+                        b_len -= 2;
+                }
+        } else if (0 == sg_vpd_dev_id_iter(bp, len, &off, VPD_ASSOC_LU,
+                                           2 /* EUI */, 1 /* binary */)) {
+                dlen = bp[off + 3];
+                if (! ((8 == dlen) || (12 == dlen) || (16 ==dlen)))
+                        return b;
+                cp = b;
+                for (k = 0; ((k < dlen) && (b_len > 1)); ++k) {
+                        snprintf(cp, b_len, "%02x", bp[off + 4 + k]);
+                        cp += 2;
+                        b_len -= 2;
+                }
+        } else if (sns_dlen > 0)
+                snprintf(b, b_len, "%.*s", sns_dlen, u_sns);
+        return b;
+}
+
+#define SAFE_STD_INQ_RESP_LEN 36
+#define VPD_SUPPORTED_VPDS 0x0
+#define VPD_UNIT_SERIAL_NUM 0x80
+#define VPD_DEVICE_ID 0x83
+#define MAX_VPD_RESP_LEN 256
+
+static int
+print_dev_id(int fd, uint8_t * sinq_resp, int max_rlen,
+             const struct opts_t * op)
+{
+        int k, n, verb, pdt, has_sn, has_di;
+        int res = 0;
+        uint8_t  * b;
+        uint8_t  * free_b = NULL;
+        char a[MAX_VPD_RESP_LEN];
+        char pdt_name[64];
+
+        verb = (op->verbose > 1) ? op->verbose - 1 : 0;
+        memset(sinq_resp, 0, max_rlen);
+        b = sg_memalign(MAX_VPD_RESP_LEN, 0, &free_b, false);
+        if (NULL == b) {
+                res = sg_convert_errno(ENOMEM);
+                goto out;
+        }
+        /* Standard INQUIRY */
+        res = sg_ll_inquiry(fd, false, false, 0, b, SAFE_STD_INQ_RESP_LEN,
+                            true, verb);
+        if (res)
+                goto out;
+        n = b[4] + 5;
+        if (n > SAFE_STD_INQ_RESP_LEN)
+                n = SAFE_STD_INQ_RESP_LEN;
+        memcpy(sinq_resp, b, (n < max_rlen) ? n : max_rlen);
+        if (n == SAFE_STD_INQ_RESP_LEN) {
+                pdt = b[0] & PDT_MASK;
+                printf("    %.8s  %.16s  %.4s   peripheral_type: %s [0x%x]\n",
+                       (const char *)(b + 8), (const char *)(b + 16),
+                       (const char *)(b + 32),
+                       sg_get_pdt_str(pdt, sizeof(pdt_name), pdt_name), pdt);
+                if (op->verbose)
+                        printf("      PROTECT=%d\n", !!(b[5] & 1));
+                if (b[5] & 1)
+                        printf("      << supports protection information>>"
+                               "\n");
+        } else {
+                pr2serr("Short INQUIRY response: %d bytes, expect at least "
+                        "36\n", n);
+                res = SG_LIB_CAT_OTHER;
+                goto out;
+        }
+        res = sg_ll_inquiry(fd, false, true, VPD_SUPPORTED_VPDS, b,
+                            SAFE_STD_INQ_RESP_LEN, true, verb);
+        if (res) {
+                if (op->verbose)
+                        pr2serr("VPD_SUPPORTED_VPDS gave res=%d\n", res);
+                res = 0;
+                goto out;
+        }
+        if (VPD_SUPPORTED_VPDS != b[1]) {
+                if (op->verbose)
+                        pr2serr("VPD_SUPPORTED_VPDS corrupted\n");
+                goto out;
+        }
+        n = sg_get_unaligned_be16(b + 2);
+        if (n > (SAFE_STD_INQ_RESP_LEN - 4))
+                n = (SAFE_STD_INQ_RESP_LEN - 4);
+        for (k = 0, has_sn = 0, has_di = 0; k < n; ++k) {
+                if (VPD_UNIT_SERIAL_NUM == b[4 + k])
+                        ++has_sn;
+                else if (VPD_DEVICE_ID == b[4 + k]) {
+                        ++has_di;
+                        break;
+                }
+        }
+        if (has_sn) {
+                res = sg_ll_inquiry(fd, false, true /* evpd */,
+                                    VPD_UNIT_SERIAL_NUM, b, MAX_VPD_RESP_LEN,
+                                    true, verb);
+                if (res) {
+                        if (op->verbose)
+                                pr2serr("VPD_UNIT_SERIAL_NUM gave res=%d\n",
+                                        res);
+                        res = 0;
+                        goto out;
+                }
+                if (VPD_UNIT_SERIAL_NUM != b[1]) {
+                        if (op->verbose)
+                                pr2serr("VPD_UNIT_SERIAL_NUM corrupted\n");
+                        goto out;
+                }
+                n = sg_get_unaligned_be16(b + 2);
+                if (n > (int)(MAX_VPD_RESP_LEN - 4))
+                        n = (MAX_VPD_RESP_LEN - 4);
+                printf("      Unit serial number: %.*s\n", n,
+                       (const char *)(b + 4));
+        }
+        if (has_di) {
+                res = sg_ll_inquiry(fd, false, true /* evpd */, VPD_DEVICE_ID,
+                                    b, MAX_VPD_RESP_LEN, true, verb);
+                if (res) {
+                        if (op->verbose)
+                                pr2serr("VPD_DEVICE_ID gave res=%d\n", res);
+                        res = 0;
+                        goto out;
+                }
+                if (VPD_DEVICE_ID != b[1]) {
+                        if (op->verbose)
+                                pr2serr("VPD_DEVICE_ID corrupted\n");
+                        goto out;
+                }
+                n = sg_get_unaligned_be16(b + 2);
+                if (n > (int)(MAX_VPD_RESP_LEN - 4))
+                        n = (MAX_VPD_RESP_LEN - 4);
+                n = strlen(get_lu_name(b, n + 4, a, sizeof(a)));
+                if (n > 0)
+                        printf("      LU name: %.*s\n", n, a);
+        }
+out:
+        if (free_b)
+                free(free_b);
+        return res;
+}
+
+#define RCAP_REPLY_LEN 32
+
+/* Returns block size or -2 if do_16==0 and the number of blocks is too
+ * big, or returns -1 for other error. */
+static int
+print_read_cap(int fd, struct opts_t * op)
+{
+        int res = 0;
+        uint8_t * resp_buff;
+        uint8_t * free_resp_buff = NULL;
+        unsigned int last_blk_addr, block_size;
+        uint64_t llast_blk_addr;
+        int64_t ll;
+        char b[80];
+
+        resp_buff = sg_memalign(RCAP_REPLY_LEN, 0, &free_resp_buff, false);
+        if (NULL == resp_buff) {
+                pr2serr("%s: unable to obtain heap\n", __func__);
+                res = -1;
+                goto out;
+        }
+        if (op->do_rcap16) {
+                res = sg_ll_readcap_16(fd, false /* pmi */, 0 /* llba */,
+                                       resp_buff, RCAP_REPLY_LEN, true,
+                                       op->verbose);
+                if (0 == res) {
+                        llast_blk_addr = sg_get_unaligned_be64(resp_buff + 0);
+                        block_size = sg_get_unaligned_be32(resp_buff + 8);
+                        printf("Read Capacity (16) results:\n");
+                        printf("   Protection: prot_en=%d, p_type=%d, "
+                               "p_i_exponent=%d\n",
+                               !!(resp_buff[12] & 0x1),
+                               ((resp_buff[12] >> 1) & 0x7),
+                               ((resp_buff[13] >> 4) & 0xf));
+                        printf("   Logical block provisioning: lbpme=%d, "
+                               "lbprz=%d\n", !!(resp_buff[14] & 0x80),
+                               !!(resp_buff[14] & 0x40));
+                        printf("   Logical blocks per physical block "
+                               "exponent=%d\n", resp_buff[13] & 0xf);
+                        printf("   Lowest aligned logical block address=%d\n",
+                               0x3fff & sg_get_unaligned_be16(resp_buff +
+                                                              14));
+                        printf("   Number of logical blocks=%" PRIu64 "\n",
+                               llast_blk_addr + 1);
+                        printf("   Logical block size=%u bytes\n",
+                               block_size);
+                        ll = (int64_t)(llast_blk_addr + 1) * block_size;
+                        if (ll > op->total_byte_count)
+                                op->total_byte_count = ll;
+                        res = (int)block_size;
+                        goto out;
+                }
+        } else {
+                res = sg_ll_readcap_10(fd, false /* pmi */, 0 /* lba */,
+                                       resp_buff, 8, true, op->verbose);
+                if (0 == res) {
+                        last_blk_addr = sg_get_unaligned_be32(resp_buff + 0);
+                        block_size = sg_get_unaligned_be32(resp_buff + 4);
+                        if (0xffffffff == last_blk_addr) {
+                                if (op->verbose)
+                                        printf("Read Capacity (10) response "
+                                               "indicates that Read Capacity "
+                                               "(16) is required\n");
+                                res = -2;
+                                goto out;
+                        }
+                        printf("Read Capacity (10) results:\n");
+                        printf("   Number of logical blocks=%u\n",
+                               last_blk_addr + 1);
+                        printf("   Logical block size=%u bytes\n",
+                               block_size);
+                        ll = (int64_t)(last_blk_addr + 1) * block_size;
+                        if (ll > op->total_byte_count)
+                                op->total_byte_count = ll;
+                        res = (int)block_size;
+                        goto out;
+                }
+        }
+        sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+        pr2serr("READ CAPACITY (%d): %s\n", (op->do_rcap16 ? 16 : 10), b);
+        res = -1;
+out:
+        if (free_resp_buff)
+                free(free_resp_buff);
+        return res;
+}
+
+/* Use MODE SENSE(6 or 10) to fetch blocks descriptor(s), if any. Analyze
+ * the first block descriptor and if required, start preparing for a
+ * MODE SELECT(6 or 10). Returns 0 on success. */
+static int
+fetch_block_desc(int fd, uint8_t * dbuff, int * calc_lenp, int * bd_lb_szp,
+                 struct opts_t * op)
+{
+        bool first = true;
+        bool prob;
+        int bd_lbsz, bd_len, dev_specific_param, offset, res, rq_lb_sz;
+        int rsp_len;
+        int resid = 0;
+        int vb = op->verbose;
+        uint64_t ull;
+        int64_t ll;
+        char b[80];
+
+again_with_long_lba:
+        memset(dbuff, 0, MAX_BUFF_SZ);
+        if (op->mode6)
+                res = sg_ll_mode_sense6(fd, false /* DBD */, 0 /* current */,
+                                        op->mode_page, 0 /* subpage */, dbuff,
+                                        MAX_BUFF_SZ, true, vb);
+        else
+                res = sg_ll_mode_sense10_v2(fd, op->long_lba, false /* DBD */,
+                                            0 /* current */, op->mode_page,
+                                            0 /* subpage */, dbuff,
+                                            MAX_BUFF_SZ, 0, &resid, true,
+                                            vb);
+        if (res) {
+                if (SG_LIB_CAT_ILLEGAL_REQ == res) {
+                        if (op->long_lba && (! op->mode6))
+                                pr2serr("bad field in MODE SENSE (%d) "
+                                        "[longlba flag not supported?]\n",
+                                        (op->mode6 ? 6 : 10));
+                        else
+                                pr2serr("bad field in MODE SENSE (%d) "
+                                        "[mode_page %d not supported?]\n",
+                                        (op->mode6 ? 6 : 10), op->mode_page);
+                } else {
+                        sg_get_category_sense_str(res, sizeof(b), b, vb);
+                        pr2serr("MODE SENSE (%d) command: %s\n",
+                                (op->mode6 ? 6 : 10), b);
+                }
+                if (0 == vb)
+                        pr2serr("    try '-v' for more information\n");
+                return res;
+        }
+        rsp_len = (resid > 0) ? (MAX_BUFF_SZ - resid) : MAX_BUFF_SZ;
+        if (rsp_len < 0) {
+                pr2serr("%s: resid=%d implies negative response "
+                        "length of %d\n", __func__, resid, rsp_len);
+                return SG_LIB_WILD_RESID;
+        }
+        *calc_lenp = sg_msense_calc_length(dbuff, rsp_len, op->mode6, &bd_len);
+        if (op->mode6) {
+                if (rsp_len < 4) {
+                        pr2serr("%s: MS(6) response length too short (%d)\n",
+                                __func__, rsp_len);
+                        return SG_LIB_CAT_MALFORMED;
+                }
+                dev_specific_param = dbuff[2];
+                op->long_lba = false;
+                offset = 4;
+                /* prepare for mode select */
+                dbuff[0] = 0;
+                dbuff[1] = 0;
+                dbuff[2] = 0;
+        } else {        /* MODE SENSE(10) */
+                if (rsp_len < 8) {
+                        pr2serr("%s: MS(10) response length too short (%d)\n",
+                                __func__, rsp_len);
+                        return SG_LIB_CAT_MALFORMED;
+                }
+                dev_specific_param = dbuff[3];
+                op->long_lba = !! (dbuff[4] & 1);
+                offset = 8;
+                /* prepare for mode select */
+                dbuff[0] = 0;
+                dbuff[1] = 0;
+                dbuff[2] = 0;
+                dbuff[3] = 0;
+        }
+        if (rsp_len < *calc_lenp) {
+                pr2serr("%s: MS response length truncated (%d < %d)\n",
+                        __func__, rsp_len, *calc_lenp);
+                return SG_LIB_CAT_MALFORMED;
+        }
+        if ((offset + bd_len) < *calc_lenp)
+                dbuff[offset + bd_len] &= 0x7f;  /* clear PS bit in mpage */
+        prob = false;
+        bd_lbsz = 0;
+        *bd_lb_szp = bd_lbsz;
+        rq_lb_sz = op->lblk_sz;
+        if (first) {
+                first = false;
+                printf("Mode Sense (block descriptor) data, prior to "
+                       "changes:\n");
+        }
+        if (dev_specific_param & 0x40)
+                printf("  <<< Write Protect (WP) bit set >>>\n");
+        if (bd_len > 0) {
+                ull = op->long_lba ? sg_get_unaligned_be64(dbuff + offset) :
+                                 sg_get_unaligned_be32(dbuff + offset);
+                bd_lbsz = op->long_lba ?
+                                 sg_get_unaligned_be32(dbuff + offset + 12) :
+                                 sg_get_unaligned_be24(dbuff + offset + 5);
+                *bd_lb_szp = bd_lbsz;
+                if (! op->long_lba) {
+                        if (0xffffffff == ull) {
+                                if (vb)
+                                        pr2serr("block count maxed out, set "
+                                                "<<longlba>>\n");
+                                op->long_lba = true;
+                                op->mode6 = false;
+                                op->do_rcap16 = true;
+                                goto again_with_long_lba;
+                        } else if ((rq_lb_sz > 0) && (rq_lb_sz < bd_lbsz) &&
+                                   (((ull * bd_lbsz) / rq_lb_sz) >=
+                                    0xffffffff)) {
+                                if (vb)
+                                        pr2serr("number of blocks will max "
+                                                "out, set <<longlba>>\n");
+                                op->long_lba = true;
+                                op->mode6 = false;
+                                op->do_rcap16 = true;
+                                goto again_with_long_lba;
+                        }
+                }
+                if (op->long_lba) {
+                        printf("  <<< longlba flag set (64 bit lba) >>>\n");
+                        if (bd_len != 16)
+                                prob = true;
+                } else if (bd_len != 8)
+                        prob = true;
+                printf("  Number of blocks=%" PRIu64 " [0x%" PRIx64 "]\n",
+                       ull, ull);
+                printf("  Block size=%d [0x%x]\n", bd_lbsz, bd_lbsz);
+                ll = (int64_t)ull * bd_lbsz;
+                if (ll > op->total_byte_count)
+                        op->total_byte_count = ll;
+        } else {
+                printf("  No block descriptors present\n");
+                prob = true;
+        }
+        if (op->resize || (op->format && ((op->blk_count != 0) ||
+              ((rq_lb_sz > 0) && (rq_lb_sz != bd_lbsz))))) {
+                /* want to run MODE SELECT, prepare now */
+
+                if (prob) {
+                        pr2serr("Need to perform MODE SELECT (to change "
+                                "number or blocks or block length)\n");
+                        pr2serr("but (single) block descriptor not found "
+                                "in earlier MODE SENSE\n");
+                        return SG_LIB_CAT_MALFORMED;
+                }
+                if (op->blk_count != 0)  { /* user supplied blk count */
+                        if (op->long_lba)
+                                sg_put_unaligned_be64(op->blk_count,
+                                                      dbuff + offset);
+                        else
+                                sg_put_unaligned_be32(op->blk_count,
+                                                      dbuff + offset);
+                } else if ((rq_lb_sz > 0) && (rq_lb_sz != bd_lbsz))
+                        /* 0 implies max capacity with new LB size */
+                        memset(dbuff + offset, 0, op->long_lba ? 8 : 4);
+
+                if ((rq_lb_sz > 0) && (rq_lb_sz != bd_lbsz)) {
+                        if (op->long_lba)
+                                sg_put_unaligned_be32((uint32_t)rq_lb_sz,
+                                                      dbuff + offset + 12);
+                        else
+                                sg_put_unaligned_be24((uint32_t)rq_lb_sz,
+                                                      dbuff + offset + 5);
+                }
+        }
+        return 0;
+}
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char **argv)
+{
+        int j;
+        int64_t ll;
+
+        op->cmplst = true;      /* will be set false if FFMT > 0 */
+        op->mode_page = RW_ERROR_RECOVERY_PAGE;
+        op->poll_type = DEF_POLL_TYPE_RS;
+        op->tape = -1;
+        while (1) {
+                int option_index = 0;
+                int c;
+
+                c = getopt_long(argc, argv,
+                                "bc:C:dDeE:f:FhIlm:M:pP:q:QrRs:St:T:vVwx:y6",
+                                long_options, &option_index);
+                if (c == -1)
+                        break;
+
+                switch (c) {
+                case 'b':
+                        op->fmtmaxlba = true;
+                        break;
+                case 'c':
+                        if (0 == strcmp("-1", optarg))
+                                op->blk_count = -1;
+                        else {
+                                op->blk_count = sg_get_llnum(optarg);
+                                if (-1 == op->blk_count) {
+                                        pr2serr("bad argument to '--count'\n");
+                                        return SG_LIB_SYNTAX_ERROR;
+                                }
+                        }
+                        break;
+                case 'C':
+                        j = sg_get_num(optarg);
+                        if ((j < 0) || (j > 1)) {
+                                pr2serr("bad argument to '--cmplst', want 0 "
+                                        "or 1\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        op->cmplst_given = true;
+                        op->cmplst = !! j;
+                        break;
+                case 'd':
+                        op->dry_run = true;
+                        break;
+                case 'D':
+                        ++op->dcrt;
+                        break;
+                case 'e':
+                        op->early = true;
+                        break;
+                case 'E':
+                        ll = sg_get_llnum(optarg);
+                        if ((ll < 0) || (ll > UINT32_MAX)) {
+                                pr2serr("bad argument to '--preset', need 32 "
+                                        "bit integer\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        op->p_id = (uint32_t)ll;
+                        op->preset = true;
+                        op->poll_type = 1;      /* poll with REQUEST SENSE */
+                        break;
+                case 'f':
+                        op->fmtpinfo = sg_get_num(optarg);
+                        if ((op->fmtpinfo < 0) || ( op->fmtpinfo > 3)) {
+                                pr2serr("bad argument to '--fmtpinfo', "
+                                        "accepts 0 to 3 inclusive\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'F':
+                        ++op->format;
+                        break;
+                case 'h':
+                        usage();
+                        return SG_LIB_OK_FALSE;
+                case 'I':
+                        op->ip_def = true;
+                        break;
+                case 'l':
+                        op->long_lba = true;
+                        op->do_rcap16 = true;
+                        break;
+                case 'm':
+                        op->timeout = sg_get_num(optarg);
+                        if (op->timeout < 0) {
+                                pr2serr("bad argument to '--timeout=', "
+                                        "accepts 0 or more\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'M':
+                        op->mode_page = sg_get_num(optarg);
+                        if ((op->mode_page < 0) || ( op->mode_page > 62)) {
+                                pr2serr("bad argument to '--mode', accepts "
+                                        "0 to 62 inclusive\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'p':
+                        op->pinfo = true;
+                        break;
+                case 'P':
+                        op->pfu = sg_get_num(optarg);
+                        if ((op->pfu < 0) || ( op->pfu > 7)) {
+                                pr2serr("bad argument to '--pfu', accepts 0 "
+                                        "to 7 inclusive\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'q':
+                        op->pie = sg_get_num(optarg);
+                        if ((op->pie < 0) || (op->pie > 15)) {
+                                pr2serr("bad argument to '--pie', accepts 0 "
+                                        "to 15 inclusive\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'Q':
+                        op->quick = true;
+                        break;
+                case 'r':
+                        op->resize = true;
+                        break;
+                case 'R':
+                        op->rto_req = true;
+                        break;
+                case 's':
+                        op->lblk_sz = sg_get_num(optarg);
+                        if (op->lblk_sz <= 0) {
+                                pr2serr("bad argument to '--size', want arg "
+                                        "> 0\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'S':
+                        op->sec_init = true;
+                        break;
+                case 't':
+                        op->ffmt = sg_get_num(optarg);
+                        if ((op->ffmt < 0) || ( op->ffmt > 3)) {
+                                pr2serr("bad argument to '--ffmt', "
+                                        "accepts 0 to 3 inclusive\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'T':
+                        if (('-' == optarg[0]) && ('1' == optarg[1]) &&
+                            ('\0' == optarg[2])) {
+                                op->tape = -1;
+                                break;
+                        }
+                        op->tape = sg_get_num(optarg);
+                        if ((op->tape < 0) || ( op->tape > 15)) {
+                                pr2serr("bad argument to '--tape', accepts "
+                                        "0 to 15 inclusive\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'v':
+                        op->verbose_given = true;
+                        op->verbose++;
+                        break;
+                case 'V':
+                        op->version_given = true;
+                        break;
+                case 'w':
+                        op->fwait = true;
+                        break;
+                case 'x':       /* false: TUR; true: request sense */
+                        op->poll_type = !! sg_get_num(optarg);
+                        op->poll_type_given = true;
+                        break;
+                case 'y':
+                        op->verify = true;
+                        break;
+                case '6':
+                        op->mode6 = true;
+                        break;
+                default:
+                        usage();
+                        return SG_LIB_SYNTAX_ERROR;
+                }
+        }
+        if (optind < argc) {
+                if (NULL == op->device_name) {
+                        op->device_name = argv[optind];
+                        ++optind;
+                }
+        }
+        if (optind < argc) {
+                for (; optind < argc; ++optind)
+                        pr2serr("Unexpected extra argument: %s\n",
+                                argv[optind]);
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+#ifdef DEBUG
+        pr2serr("In DEBUG mode, ");
+        if (op->verbose_given && op->version_given) {
+                pr2serr("but override: '-vV' given, zero verbose and "
+                        "continue\n");
+                op->verbose_given = false;
+                op->version_given = false;
+                op->verbose = 0;
+        } else if (! op->verbose_given) {
+                pr2serr("set '-vv'\n");
+                op->verbose = 2;
+        } else
+                pr2serr("keep verbose=%d\n", op->verbose);
+#else
+        if (op->verbose_given && op->version_given)
+                pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+        if (op->version_given) {
+                pr2serr("sg_format version: %s\n", version_str);
+                return SG_LIB_OK_FALSE;
+        }
+        if (NULL == op->device_name) {
+                pr2serr("no DEVICE name given\n\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+        if (((int)(op->format > 0) + (int)(op->tape >= 0) + (int)op->preset)
+            > 1) {
+                pr2serr("Can choose only one of: '--format', '--tape=' and "
+                        "'--preset='\n");
+                return SG_LIB_CONTRADICT;
+        }
+        if (op->ip_def && op->sec_init) {
+                pr2serr("'--ip_def' and '--security' contradict, choose "
+                        "one\n");
+                return SG_LIB_CONTRADICT;
+        }
+        if (op->resize) {
+                if (op->format) {
+                        pr2serr("both '--format' and '--resize' not "
+                                "permitted\n");
+                        usage();
+                        return SG_LIB_CONTRADICT;
+                } else if (0 == op->blk_count) {
+                        pr2serr("'--resize' needs a '--count' (other than "
+                                "0)\n");
+                        usage();
+                        return SG_LIB_CONTRADICT;
+                } else if (0 != op->lblk_sz) {
+                        pr2serr("'--resize' not compatible with '--size'\n");
+                        usage();
+                        return SG_LIB_CONTRADICT;
+                }
+        }
+        if ((op->pinfo > 0) || (op->rto_req > 0) || (op->fmtpinfo > 0)) {
+                if ((op->pinfo || op->rto_req) && op->fmtpinfo) {
+                        pr2serr("confusing with both '--pinfo' or "
+                                "'--rto_req' together with\n'--fmtpinfo', "
+                                "best use '--fmtpinfo' only\n");
+                        usage();
+                        return SG_LIB_CONTRADICT;
+                }
+                if (op->pinfo)
+                        op->fmtpinfo |= 2;
+                if (op->rto_req)
+                        op->fmtpinfo |= 1;
+        }
+        if ((op->ffmt > 0) && (! op->cmplst_given))
+                op->cmplst = false; /* SBC-4 silent; FFMT&&CMPLST unlikely */
+        return 0;
+}
+
+
+int
+main(int argc, char **argv)
+{
+        int bd_lb_sz, calc_len, pdt, res, rq_lb_sz, vb;
+        int fd = -1;
+        int ret = 0;
+        const int dbuff_sz = MAX_BUFF_SZ;
+        const int inq_resp_sz = SAFE_STD_INQ_RESP_LEN;
+        struct opts_t * op;
+        uint8_t * dbuff;
+        uint8_t * free_dbuff = NULL;
+        uint8_t * inq_resp;
+        uint8_t * free_inq_resp = NULL;
+        struct opts_t opts;
+        char b[80];
+
+        op = &opts;
+        memset(op, 0, sizeof(opts));
+        ret = parse_cmd_line(op, argc, argv);
+        if (ret)
+                return (SG_LIB_OK_FALSE == ret) ? 0 : ret;
+        vb = op->verbose;
+
+        dbuff = sg_memalign(dbuff_sz, 0, &free_dbuff, false);
+        inq_resp = sg_memalign(inq_resp_sz, 0, &free_inq_resp, false);
+        if ((NULL == dbuff) || (NULL == inq_resp)) {
+                pr2serr("Unable to allocate heap\n");
+                ret = sg_convert_errno(ENOMEM);
+                goto out;
+        }
+
+        if ((fd = sg_cmds_open_device(op->device_name, false, vb)) < 0) {
+                pr2serr("error opening device file: %s: %s\n",
+                        op->device_name, safe_strerror(-fd));
+                ret = sg_convert_errno(-fd);
+                goto out;
+        }
+
+        if (op->format > 2)
+                goto format_only;
+
+        ret = print_dev_id(fd, inq_resp, inq_resp_sz, op);
+        if (ret) {
+                if (op->dry_run) {
+                        pr2serr("INQUIRY failed, assume device is a disk\n");
+                        pdt = 0;
+                } else
+                        goto out;
+        } else
+                pdt = PDT_MASK & inq_resp[0];
+        if (op->format) {
+                if ((PDT_DISK != pdt) && (PDT_OPTICAL != pdt) &&
+                    (PDT_RBC != pdt) && (PDT_ZBC != pdt)) {
+                        pr2serr("This format is only defined for disks "
+                                "(using SBC-2+, ZBC or RBC) and MO media\n");
+                        ret = SG_LIB_CAT_MALFORMED;
+                        goto out;
+                }
+        } else if (op->tape >= 0) {
+                if (! ((PDT_TAPE == pdt) || (PDT_MCHANGER == pdt) ||
+                       (PDT_ADC == pdt))) {
+                        pr2serr("This format is only defined for tapes\n");
+                        ret = SG_LIB_CAT_MALFORMED;
+                        goto out;
+                }
+                goto format_med;
+        } else if (op->preset)
+                goto format_with_pre;
+
+        ret = fetch_block_desc(fd, dbuff, &calc_len, &bd_lb_sz, op);
+        if (ret) {
+                if (op->dry_run) {
+                        /* pick some numbers ... */
+                        calc_len = 1024 * 1024 * 1024;
+                        bd_lb_sz = 512;
+                } else
+                        goto out;
+        }
+        rq_lb_sz = op->lblk_sz;
+        if (op->resize || (op->format && ((op->blk_count != 0) ||
+              ((rq_lb_sz > 0) && (rq_lb_sz != bd_lb_sz))))) {
+                /* want to run MODE SELECT */
+                if (op->dry_run) {
+                        pr2serr("Due to --dry-run option bypass MODE "
+                                "SELECT(%d) command\n", (op->mode6 ? 6 : 10));
+                        res = 0;
+                } else {
+                        bool sp = true;   /* may not be able to save pages */
+
+again_sp_false:
+                        if (op->mode6)
+                                res = sg_ll_mode_select6(fd, true /* PF */,
+                                                         sp, dbuff, calc_len,
+                                                         true, vb);
+                        else
+                                res = sg_ll_mode_select10(fd, true /* PF */,
+                                                          sp, dbuff, calc_len,
+                                                          true, vb);
+                        if ((SG_LIB_CAT_ILLEGAL_REQ == res) && sp) {
+                                pr2serr("Try MODE SELECT again with SP=0 "
+                                        "this time\n");
+                                sp = false;
+                                goto again_sp_false;
+                        }
+                }
+                ret = res;
+                if (res) {
+                        sg_get_category_sense_str(res, sizeof(b), b, vb);
+                        pr2serr("MODE SELECT command: %s\n", b);
+                        if (0 == vb)
+                                pr2serr("    try '-v' for more information\n");
+                        goto out;
+                }
+        }
+        if (op->resize) {
+                printf("Resize operation seems to have been successful\n");
+                goto out;
+        } else if (! op->format) {
+                res = print_read_cap(fd, op);
+                if (-2 == res) {
+                        op->do_rcap16 = true;
+                        res = print_read_cap(fd, op);
+                }
+                if (res < 0)
+                        ret = -1;
+                if ((res > 0) && (bd_lb_sz > 0) &&
+                    (res != (int)bd_lb_sz)) {
+                        printf("  Warning: mode sense and read capacity "
+                               "report different block sizes [%d,%d]\n",
+                               bd_lb_sz, res);
+                        printf("           Probably needs format\n");
+                }
+                if ((PDT_TAPE == pdt) || (PDT_MCHANGER == pdt) ||
+                    (PDT_ADC == pdt))
+                        printf("No changes made. To format use '--tape='.\n");
+                else
+                        printf("No changes made. To format use '--format'. "
+                               "To resize use '--resize'\n");
+                goto out;
+        }
+
+        if (op->format) {
+format_only:
+                if (! op->quick)
+                    sg_warn_and_wait("FORMAT UNIT", op->device_name, true);
+                res = scsi_format_unit(fd, op);
+                ret = res;
+                if (res) {
+                        pr2serr("FORMAT UNIT failed\n");
+                        if (0 == vb)
+                                pr2serr("    try '-v' for more "
+                                        "information\n");
+                }
+        }
+        goto out;
+
+format_med:
+        if (! op->poll_type_given) /* SSC-5 specifies REQUEST SENSE polling */
+                op->poll_type = true;
+        if (! op->quick)
+            sg_warn_and_wait("FORMAT MEDIUM", op->device_name, true);
+        res = scsi_format_medium(fd, op);
+        ret = res;
+        if (res) {
+                pr2serr("FORMAT MEDIUM failed\n");
+                if (0 == vb)
+                        pr2serr("    try '-v' for more information\n");
+        }
+        goto out;
+
+format_with_pre:
+        if (! op->quick)
+            sg_warn_and_wait("FORMAT WITH PRESET", op->device_name, true);
+        res = scsi_format_with_preset(fd, op);
+        ret = res;
+        if (res) {
+                pr2serr("FORMAT WITH PRESET failed\n");
+                if (0 == vb)
+                        pr2serr("    try '-v' for more information\n");
+        }
+
+out:
+        if (free_dbuff)
+                free(free_dbuff);
+        if (free_inq_resp)
+                free(free_inq_resp);
+        if (fd >= 0) {
+            res = sg_cmds_close_device(fd);
+            if (res < 0) {
+                    pr2serr("close error: %s\n", safe_strerror(-res));
+                    if (0 == ret)
+                            ret = sg_convert_errno(-res);
+            }
+        }
+        if (0 == vb) {
+                if (! sg_if_can2stderr("sg_format failed: ", ret))
+                        pr2serr("Some error occurred, try again with '-v' "
+                                "or '-vv' for more information\n");
+        }
+        return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_get_config.c b/src/sg_get_config.c
new file mode 100644
index 0000000..ad3bce9
--- /dev/null
+++ b/src/sg_get_config.c
@@ -0,0 +1,1145 @@
+/*
+ * Copyright (c) 2004-2018 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_mmc.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ * This program outputs information provided by a SCSI "Get Configuration"
+   command [0x46] which is only defined for CD/DVDs (in MMC-2,3,4,5,6).
+
+*/
+
+static const char * version_str = "0.49 20180626";    /* mmc6r02 */
+
+#define MX_ALLOC_LEN 8192
+#define NAME_BUFF_SZ 64
+
+#define ME "sg_get_config: "
+
+
+static uint8_t resp_buffer[MX_ALLOC_LEN];
+
+static struct option long_options[] = {
+        {"brief", no_argument, 0, 'b'},
+        {"current", no_argument, 0, 'c'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"inner-hex", no_argument, 0, 'i'},
+        {"list", no_argument, 0, 'l'},
+        {"raw", no_argument, 0, 'R'},
+        {"readonly", no_argument, 0, 'q'},
+        {"rt", required_argument, 0, 'r'},
+        {"starting", required_argument, 0, 's'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage:  sg_get_config [--brief] [--current] [--help] [--hex] "
+            "[--inner-hex]\n"
+            "                      [--list] [--raw] [--readonly] [--rt=RT]\n"
+            "                      [--starting=FC] [--verbose] [--version] "
+            "DEVICE\n"
+            "  where:\n"
+            "    --brief|-b       only give feature names of DEVICE "
+            "(don't decode)\n"
+            "    --current|-c     equivalent to '--rt=1' (show "
+            "current)\n"
+            "    --help|-h        print usage message then exit\n"
+            "    --hex|-H         output response in hex\n"
+            "    --inner-hex|-i    decode to feature name, then output "
+            "features in hex\n"
+            "    --list|-l        list all known features + profiles "
+            "(ignore DEVICE)\n"
+            "    --raw|-R         output in binary (to stdout)\n"
+            "    --readonly|-q    open DEVICE read-only (def: open it "
+            "read-write)\n"
+            "    --rt=RT|-r RT    default value is 0\n"
+            "                     0 -> all feature descriptors (regardless "
+            "of currency)\n"
+            "                     1 -> all current feature descriptors\n"
+            "                     2 -> only feature descriptor matching "
+            "'starting'\n"
+            "    --starting=FC|-s FC    starting from feature "
+            "code (FC) value\n"
+            "    --verbose|-v     verbose\n"
+            "    --version|-V     output version string\n\n"
+            "Get configuration information for MMC drive and/or media\n");
+}
+
+struct val_desc_t {
+        int val;
+        const char * desc;
+};
+
+static struct val_desc_t profile_desc_arr[] = {
+        {0x0, "No current profile"},
+        {0x1, "Non-removable disk (obs)"},
+        {0x2, "Removable disk"},
+        {0x3, "Magneto optical erasable"},
+        {0x4, "Optical write once"},
+        {0x5, "AS-MO"},
+        {0x8, "CD-ROM"},
+        {0x9, "CD-R"},
+        {0xa, "CD-RW"},
+        {0x10, "DVD-ROM"},
+        {0x11, "DVD-R sequential recording"},
+        {0x12, "DVD-RAM"},
+        {0x13, "DVD-RW restricted overwrite"},
+        {0x14, "DVD-RW sequential recording"},
+        {0x15, "DVD-R dual layer sequental recording"},
+        {0x16, "DVD-R dual layer jump recording"},
+        {0x17, "DVD-RW dual layer"},
+        {0x18, "DVD-Download disc recording"},
+        {0x1a, "DVD+RW"},
+        {0x1b, "DVD+R"},
+        {0x20, "DDCD-ROM"},
+        {0x21, "DDCD-R"},
+        {0x22, "DDCD-RW"},
+        {0x2a, "DVD+RW dual layer"},
+        {0x2b, "DVD+R dual layer"},
+        {0x40, "BD-ROM"},
+        {0x41, "BD-R SRM"},
+        {0x42, "BD-R RRM"},
+        {0x43, "BD-RE"},
+        {0x50, "HD DVD-ROM"},
+        {0x51, "HD DVD-R"},
+        {0x52, "HD DVD-RAM"},
+        {0x53, "HD DVD-RW"},
+        {0x58, "HD DVD-R dual layer"},
+        {0x5a, "HD DVD-RW dual layer"},
+        {0xffff, "Non-conforming profile"},
+        {-1, NULL},
+};
+
+static const char *
+get_profile_str(int profile_num, char * buff)
+{
+    const struct val_desc_t * pdp;
+
+    for (pdp = profile_desc_arr; pdp->desc; ++pdp) {
+        if (pdp->val == profile_num) {
+            strcpy(buff, pdp->desc);
+            return buff;
+        }
+    }
+    snprintf(buff, 64, "0x%x", profile_num);
+    return buff;
+}
+
+static struct val_desc_t feature_desc_arr[] = {
+        {0x0, "Profile list"},
+        {0x1, "Core"},
+        {0x2, "Morphing"},
+        {0x3, "Removable media"},
+        {0x4, "Write Protect"},
+        {0x10, "Random readable"},
+        {0x1d, "Multi-read"},
+        {0x1e, "CD read"},
+        {0x1f, "DVD read"},
+        {0x20, "Random writable"},
+        {0x21, "Incremental streaming writable"},
+        {0x22, "Sector erasable"},
+        {0x23, "Formattable"},
+        {0x24, "Hardware defect management"},
+        {0x25, "Write once"},
+        {0x26, "Restricted overwrite"},
+        {0x27, "CD-RW CAV write"},
+        {0x28, "MRW"},          /* Mount Rainier reWritable */
+        {0x29, "Enhanced defect reporting"},
+        {0x2a, "DVD+RW"},
+        {0x2b, "DVD+R"},
+        {0x2c, "Rigid restricted overwrite"},
+        {0x2d, "CD track-at-once"},
+        {0x2e, "CD mastering (session at once)"},
+        {0x2f, "DVD-R/-RW write"},
+        {0x30, "Double density CD read"},
+        {0x31, "Double density CD-R write"},
+        {0x32, "Double density CD-RW write"},
+        {0x33, "Layer jump recording"},
+        {0x34, "LJ rigid restricted oberwrite"},
+        {0x35, "Stop long operation"},
+        {0x37, "CD-RW media write support"},
+        {0x38, "BD-R POW"},
+        {0x3a, "DVD+RW dual layer"},
+        {0x3b, "DVD+R dual layer"},
+        {0x40, "BD read"},
+        {0x41, "BD write"},
+        {0x42, "TSR (timely safe recording)"},
+        {0x50, "HD DVD read"},
+        {0x51, "HD DVD write"},
+        {0x52, "HD DVD-RW fragment recording"},
+        {0x80, "Hybrid disc"},
+        {0x100, "Power management"},
+        {0x101, "SMART"},
+        {0x102, "Embedded changer"},
+        {0x103, "CD audio external play"},
+        {0x104, "Microcode upgrade"},
+        {0x105, "Timeout"},
+        {0x106, "DVD CSS"},
+        {0x107, "Real time streaming"},
+        {0x108, "Drive serial number"},
+        {0x109, "Media serial number"},
+        {0x10a, "Disc control blocks"},
+        {0x10b, "DVD CPRM"},
+        {0x10c, "Firmware information"},
+        {0x10d, "AACS"},
+        {0x10e, "DVD CSS managed recording"},
+        {0x110, "VCPS"},
+        {0x113, "SecurDisc"},
+        {0x120, "BD CPS"},
+        {0x142, "OSSC"},
+};
+
+static const char *
+get_feature_str(int feature_num, char * buff)
+{
+    int k, num;
+
+    num = SG_ARRAY_SIZE(feature_desc_arr);
+    for (k = 0; k < num; ++k) {
+        if (feature_desc_arr[k].val == feature_num) {
+            strcpy(buff, feature_desc_arr[k].desc);
+            return buff;
+        }
+    }
+    snprintf(buff, 64, "0x%x", feature_num);
+    return buff;
+}
+
+static void
+dStrRaw(const char * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+static void
+decode_feature(int feature, uint8_t * bp, int len)
+{
+    int k, num, n, profile;
+    char buff[128];
+    const char * cp;
+
+    switch (feature) {
+    case 0:     /* Profile list */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 2), !!(bp[2] & 1),
+               feature);
+        printf("    available profiles [more recent typically higher "
+               "in list]:\n");
+        for (k = 4; k < len; k += 4) {
+            profile = sg_get_unaligned_be16(bp + k);
+            printf("      profile: %s , currentP=%d\n",
+                   get_profile_str(profile, buff), !!(bp[k + 2] & 1));
+        }
+        break;
+    case 1:     /* Core */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 2), !!(bp[2] & 1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        num = sg_get_unaligned_be32(bp + 4);
+        switch (num) {
+        case 0: cp = "unspecified"; break;
+        case 1: cp = "SCSI family"; break;
+        case 2: cp = "ATAPI"; break;
+        case 3: cp = "IEEE 1394 - 1995"; break;
+        case 4: cp = "IEEE 1394A"; break;
+        case 5: cp = "Fibre channel"; break;
+        case 6: cp = "IEEE 1394B"; break;
+        case 7: cp = "Serial ATAPI"; break;
+        case 8: cp = "USB (both 1 and 2)"; break;
+        case 0xffff: cp = "vendor unique"; break;
+        default:
+            snprintf(buff, sizeof(buff), "[0x%x]", num);
+            cp = buff;
+            break;
+        }
+        printf("      Physical interface standard: %s", cp);
+        if (len > 8)
+            printf(", INQ2=%d, DBE=%d\n", !!(bp[8] & 2), !!(bp[8] & 1));
+        else
+            printf("\n");
+        break;
+    case 2:     /* Morphing */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 2), !!(bp[2] & 1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      OCEvent=%d, ASYNC=%d\n", !!(bp[4] & 2), !!(bp[4] & 1));
+        break;
+    case 3:     /* Removable medium */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 2), !!(bp[2] & 1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        num = (bp[4] >> 5) & 0x7;
+        switch (num) {
+        case 0: cp = "Caddy/slot type"; break;
+        case 1: cp = "Tray type"; break;
+        case 2: cp = "Pop-up type"; break;
+        case 4: cp = "Embedded changer with individually changeable discs";
+            break;
+        case 5: cp = "Embedded changer using a magazine"; break;
+        default:
+            snprintf(buff, sizeof(buff), "[0x%x]", num);
+            cp = buff;
+            break;
+        }
+        printf("      Loading mechanism: %s\n", cp);
+        printf("      Load=%d, Eject=%d, Prevent jumper=%d, Lock=%d\n",
+               !!(bp[4] & 0x10), !!(bp[4] & 0x8), !!(bp[4] & 0x4),
+               !!(bp[4] & 0x1));
+        break;
+    case 4:     /* Write protect */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      DWP=%d, WDCB=%d, SPWP=%d, SSWPP=%d\n", !!(bp[4] & 0x8),
+               !!(bp[4] & 0x4), !!(bp[4] & 0x2), !!(bp[4] & 0x1));
+        break;
+    case 0x10:     /* Random readable */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 12) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        num = sg_get_unaligned_be32(bp + 4);
+        printf("      Logical block size=0x%x, blocking=0x%x, PP=%d\n",
+               num, sg_get_unaligned_be16(bp + 8), !!(bp[10] & 0x1));
+        break;
+    case 0x1d:     /* Multi-read */
+    case 0x22:     /* Sector erasable */
+    case 0x26:     /* Restricted overwrite */
+    case 0x27:     /* CDRW CAV write */
+    case 0x35:     /* Stop long operation */
+    case 0x38:     /* BD-R pseudo-overwrite (POW) */
+    case 0x42:     /* TSR (timely safe recording) */
+    case 0x100:    /* Power management */
+    case 0x109:    /* Media serial number */
+    case 0x110:    /* VCPS */
+    case 0x113:    /* SecurDisc */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        break;
+    case 0x1e:     /* CD read */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      DAP=%d, C2 flags=%d, CD-Text=%d\n", !!(bp[4] & 0x80),
+               !!(bp[4] & 0x2), !!(bp[4] & 0x1));
+        break;
+    case 0x1f:     /* DVD read */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len > 7)
+            printf("      MULTI110=%d, Dual-RW=%d, Dual-R=%d\n",
+                   !!(bp[4] & 0x1), !!(bp[6] & 0x2), !!(bp[6] & 0x1));
+        break;
+    case 0x20:     /* Random writable */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 16) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        num = sg_get_unaligned_be32(bp + 4);
+        n = sg_get_unaligned_be32(bp + 8);
+        printf("      Last lba=0x%x, Logical block size=0x%x, blocking=0x%x,"
+               " PP=%d\n", num, n, sg_get_unaligned_be16(bp + 12),
+               !!(bp[14] & 0x1));
+        break;
+    case 0x21:     /* Incremental streaming writable */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      Data block types supported=0x%x, TRIO=%d, ARSV=%d, "
+               "BUF=%d\n", sg_get_unaligned_be16(bp + 4), !!(bp[6] & 0x4),
+               !!(bp[6] & 0x2), !!(bp[6] & 0x1));
+        num = bp[7];
+        printf("      Number of link sizes=%d\n", num);
+        for (k = 0; k < num; ++k)
+            printf("        %d\n", bp[8 + k]);
+        break;
+    /* case 0x22:     Sector erasable -> see 0x1d entry */
+    case 0x23:     /* Formattable */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len > 4)
+            printf("      BD-RE: RENoSA=%d, Expand=%d, QCert=%d, Cert=%d, "
+                   "FRF=%d\n", !!(bp[4] & 0x8), !!(bp[4] & 0x4),
+                   !!(bp[4] & 0x2), !!(bp[4] & 0x1), !!(bp[5] & 0x80));
+        if (len > 8)
+            printf("      BD-R: RRM=%d\n", !!(bp[8] & 0x1));
+        break;
+    case 0x24:     /* Hardware defect management */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len > 4)
+            printf("      SSA=%d\n", !!(bp[4] & 0x80));
+        break;
+    case 0x25:     /* Write once */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 12) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        num = sg_get_unaligned_be16(bp + 4);
+        printf("      Logical block size=0x%x, blocking=0x%x, PP=%d\n",
+               num, sg_get_unaligned_be16(bp + 8), !!(bp[10] & 0x1));
+        break;
+    /* case 0x26:     Restricted overwrite -> see 0x1d entry */
+    /* case 0x27:     CDRW CAV write -> see 0x1d entry */
+    case 0x28:     /* MRW  (Mount Rainier reWriteable) */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len > 4)
+            printf("      DVD+Write=%d, DVD+Read=%d, Write=%d\n",
+                   !!(bp[4] & 0x4), !!(bp[4] & 0x2), !!(bp[4] & 0x1));
+        break;
+    case 0x29:     /* Enhanced defect reporting */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      DRT-DM=%d, number of DBI cache zones=0x%x, number of "
+               "entries=0x%x\n", !!(bp[4] & 0x1), bp[5],
+               sg_get_unaligned_be16(bp + 6));
+        break;
+    case 0x2a:     /* DVD+RW */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      Write=%d, Quick start=%d, Close only=%d\n",
+               !!(bp[4] & 0x1), !!(bp[5] & 0x2), !!(bp[5] & 0x1));
+        break;
+    case 0x2b:     /* DVD+R */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      Write=%d\n", !!(bp[4] & 0x1));
+        break;
+    case 0x2c:     /* Rigid restricted overwrite */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      DSDG=%d, DSDR=%d, Intermediate=%d, Blank=%d\n",
+               !!(bp[4] & 0x8), !!(bp[4] & 0x4), !!(bp[4] & 0x2),
+               !!(bp[4] & 0x1));
+        break;
+    case 0x2d:     /* CD Track at once */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      BUF=%d, R-W raw=%d, R-W pack=%d, Test write=%d\n",
+               !!(bp[4] & 0x40), !!(bp[4] & 0x10), !!(bp[4] & 0x8),
+               !!(bp[4] & 0x4));
+        printf("      CD-RW=%d, R-W sub-code=%d, Data type supported=%d\n",
+               !!(bp[4] & 0x2), !!(bp[4] & 0x1),
+               sg_get_unaligned_be16(bp + 6));
+        break;
+    case 0x2e:     /* CD mastering (session at once) */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      BUF=%d, SAO=%d, Raw MS=%d, Raw=%d\n",
+               !!(bp[4] & 0x40), !!(bp[4] & 0x20), !!(bp[4] & 0x10),
+               !!(bp[4] & 0x8));
+        printf("      Test write=%d, CD-RW=%d, R-W=%d\n",
+               !!(bp[4] & 0x4), !!(bp[4] & 0x2), !!(bp[4] & 0x1));
+        printf("      Maximum cue sheet length=0x%x\n",
+               sg_get_unaligned_be24(bp + 5));
+        break;
+    case 0x2f:     /* DVD-R/-RW write */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      BUF=%d, RDL=%d, Test write=%d, DVD-RW SL=%d\n",
+               !!(bp[4] & 0x40), !!(bp[4] & 0x8), !!(bp[4] & 0x4),
+               !!(bp[4] & 0x2));
+        break;
+    case 0x33:     /* Layer jump recording */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        num = bp[7];
+        printf("      Number of link sizes=%d\n", num);
+        for (k = 0; k < num; ++k)
+            printf("        %d\n", bp[8 + k]);
+        break;
+    case 0x34:     /* Layer jump rigid restricted overwrite */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      CLJB=%d\n", !!(bp[4] & 0x1));
+        printf("      Buffer block size=%d\n", bp[7]);
+        break;
+    /* case 0x35:     Stop long operation -> see 0x1d entry */
+    case 0x37:     /* CD-RW media write support */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      CD-RW media sub-type support (bitmask)=0x%x\n", bp[5]);
+        break;
+    /* case 0x38:     BD-R pseudo-overwrite (POW) -> see 0x1d entry */
+    case 0x3a:     /* DVD+RW dual layer */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      write=%d, quick_start=%d, close_only=%d\n",
+               !!(bp[4] & 0x1), !!(bp[5] & 0x2), !!(bp[5] & 0x1));
+        break;
+    case 0x3b:     /* DVD+R dual layer */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      write=%d\n", !!(bp[4] & 0x1));
+        break;
+    case 0x40:     /* BD Read */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 32) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      Bitmaps for BD-RE read support:\n");
+        printf("        Class 0=0x%x, Class 1=0x%x, Class 2=0x%x, "
+               "Class 3=0x%x\n", sg_get_unaligned_be16(bp + 8),
+               sg_get_unaligned_be16(bp + 10),
+               sg_get_unaligned_be16(bp + 12),
+               sg_get_unaligned_be16(bp + 14));
+        printf("      Bitmaps for BD-R read support:\n");
+        printf("        Class 0=0x%x, Class 1=0x%x, Class 2=0x%x, "
+               "Class 3=0x%x\n", sg_get_unaligned_be16(bp + 16),
+               sg_get_unaligned_be16(bp + 18),
+               sg_get_unaligned_be16(bp + 20),
+               sg_get_unaligned_be16(bp + 22));
+        printf("      Bitmaps for BD-ROM read support:\n");
+        printf("        Class 0=0x%x, Class 1=0x%x, Class 2=0x%x, "
+               "Class 3=0x%x\n", sg_get_unaligned_be16(bp + 24),
+               sg_get_unaligned_be16(bp + 26),
+               sg_get_unaligned_be16(bp + 28),
+               sg_get_unaligned_be16(bp + 30));
+        break;
+    case 0x41:     /* BD Write */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 32) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      SVNR=%d\n", !!(bp[4] & 0x1));
+        printf("      Bitmaps for BD-RE write support:\n");
+        printf("        Class 0=0x%x, Class 1=0x%x, Class 2=0x%x, "
+               "Class 3=0x%x\n", sg_get_unaligned_be16(bp + 8),
+               sg_get_unaligned_be16(bp + 10),
+               sg_get_unaligned_be16(bp + 12),
+               sg_get_unaligned_be16(bp + 14));
+        printf("      Bitmaps for BD-R write support:\n");
+        printf("        Class 0=0x%x, Class 1=0x%x, Class 2=0x%x, "
+               "Class 3=0x%x\n", sg_get_unaligned_be16(bp + 16),
+               sg_get_unaligned_be16(bp + 18),
+               sg_get_unaligned_be16(bp + 20),
+               sg_get_unaligned_be16(bp + 22));
+        printf("      Bitmaps for BD-ROM write support:\n");
+        printf("        Class 0=0x%x, Class 1=0x%x, Class 2=0x%x, "
+               "Class 3=0x%x\n", sg_get_unaligned_be16(bp + 24),
+               sg_get_unaligned_be16(bp + 26),
+               sg_get_unaligned_be16(bp + 28),
+               sg_get_unaligned_be16(bp + 30));
+        break;
+    /* case 0x42:     TSR (timely safe recording) -> see 0x1d entry */
+    case 0x50:     /* HD DVD Read */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      HD DVD-R=%d, HD DVD-RAM=%d\n", !!(bp[4] & 0x1),
+               !!(bp[6] & 0x1));
+        break;
+    case 0x51:     /* HD DVD Write */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      HD DVD-R=%d, HD DVD-RAM=%d\n", !!(bp[4] & 0x1),
+               !!(bp[6] & 0x1));
+        break;
+    case 0x52:     /* HD DVD-RW fragment recording */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      BGP=%d\n", !!(bp[4] & 0x1));
+        break;
+    case 0x80:     /* Hybrid disc */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      RI=%d\n", !!(bp[4] & 0x1));
+        break;
+    /* case 0x100:    Power management -> see 0x1d entry */
+    case 0x101:    /* SMART */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      PP=%d\n", !!(bp[4] & 0x1));
+        break;
+    case 0x102:    /* Embedded changer */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      SCC=%d, SDP=%d, highest slot number=%d\n",
+               !!(bp[4] & 0x10), !!(bp[4] & 0x4), (bp[7] & 0x1f));
+        break;
+    case 0x103:    /* CD audio external play (obsolete) */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      Scan=%d, SCM=%d, SV=%d, number of volume levels=%d\n",
+               !!(bp[4] & 0x4), !!(bp[4] & 0x2), !!(bp[4] & 0x1),
+               sg_get_unaligned_be16(bp + 6));
+        break;
+    case 0x104:    /* Firmware upgrade */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 4) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        if (len > 4)
+            printf("      M5=%d\n", !!(bp[4] & 0x1));
+        break;
+    case 0x105:    /* Timeout */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len > 7) {
+            printf("      Group 3=%d, unit length=%d\n",
+                   !!(bp[4] & 0x1), sg_get_unaligned_be16(bp + 6));
+        }
+        break;
+    case 0x106:    /* DVD CSS */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      CSS version=%d\n", bp[7]);
+        break;
+    case 0x107:    /* Real time streaming */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      RBCB=%d, SCS=%d, MP2A=%d, WSPD=%d, SW=%d\n",
+               !!(bp[4] & 0x10), !!(bp[4] & 0x8), !!(bp[4] & 0x4),
+               !!(bp[4] & 0x2), !!(bp[4] & 0x1));
+        break;
+    case 0x108:    /* Drive serial number */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        num = len - 4;
+        n = sizeof(buff) - 1;
+        n = ((num < n) ? num : n);
+        strncpy(buff, (const char *)(bp + 4), n);
+        buff[n] = '\0';
+        printf("      Drive serial number: %s\n", buff);
+        break;
+    /* case 0x109:    Media serial number -> see 0x1d entry */
+    case 0x10a:    /* Disc control blocks */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        printf("      Disc control blocks:\n");
+        for (k = 4; k < len; k += 4) {
+            printf("        0x%x\n", sg_get_unaligned_be32(bp + k));
+        }
+        break;
+    case 0x10b:    /* DVD CPRM */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      CPRM version=%d\n", bp[7]);
+        break;
+    case 0x10c:    /* firmware information */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 20) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      %.2s%.2s/%.2s/%.2s %.2s:%.2s:%.2s\n", bp + 4,
+               bp + 6, bp + 8, bp + 10, bp + 12, bp + 14, bp + 16);
+        break;
+    case 0x10d:    /* AACS */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      BNG=%d, Block count for binding nonce=%d\n",
+               !!(bp[4] & 0x1), bp[5]);
+        printf("      Number of AGIDs=%d, AACS version=%d\n",
+               (bp[6] & 0xf), bp[7]);
+        break;
+    case 0x10e:    /* DVD CSS managed recording */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      Maximum number of scrambled extent information "
+               "entries=%d\n", bp[4]);
+        break;
+    /* case 0x110:    VCPS -> see 0x1d entry */
+    /* case 0x113:    SecurDisc -> see 0x1d entry */
+    case 0x120:    /* BD CPS */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("      BD CPS major:minor version number=%d:%d, max open "
+               "SACs=%d\n", ((bp[5] >> 4) & 0xf), (bp[5] & 0xf),
+               bp[6] & 0x3);
+        break;
+    case 0x142:    /* OSSC (Optical Security Subsystem Class) */
+        printf("    version=%d, persist=%d, current=%d [0x%x]\n",
+               ((bp[2] >> 2) & 0xf), !!(bp[2] & 0x2), !!(bp[2] & 0x1),
+               feature);
+        if (len < 8) {
+            printf("      additional length [%d] too short\n", len - 4);
+            break;
+        }
+        printf("    PSAU=%d, LOSPB=%d, ME=%d\n", !!(bp[4] & 0x80),
+               !!(bp[4] & 0x40), !!(bp[4] & 0x1));
+        num = bp[5];
+        printf("      Profile numbers:\n");
+        for (k = 6; (num > 0) && (k < len); --num, k += 2) {
+            printf("        %u\n", sg_get_unaligned_be16(bp + k));
+        }
+        break;
+    default:
+        pr2serr("    Unknown feature [0x%x], version=%d persist=%d, "
+                "current=%d\n", feature, ((bp[2] >> 2) & 0xf),
+                !!(bp[2] & 0x2), !!(bp[2] & 0x1));
+        hex2stderr(bp, len, 1);
+        break;
+    }
+}
+
+static void
+decode_config(uint8_t * resp, int max_resp_len, int len, bool brief,
+              bool inner_hex)
+{
+    int k, curr_profile, extra_len, feature;
+    uint8_t * bp;
+    char buff[128];
+
+    if (max_resp_len < len) {
+        pr2serr("<<<warning: response to long for buffer, resp_len=%d>>>\n",
+                len);
+            len = max_resp_len;
+    }
+    if (len < 8) {
+        pr2serr("response length too short: %d\n", len);
+        return;
+    }
+    curr_profile = sg_get_unaligned_be16(resp + 6);
+    if (0 == curr_profile)
+        pr2serr("No current profile\n");
+    else
+        printf("Current profile: %s\n", get_profile_str(curr_profile, buff));
+    printf("Features%s:\n", (brief ? " (in brief)" : ""));
+    bp = resp + 8;
+    len -= 8;
+    for (k = 0; k < len; k += extra_len, bp += extra_len) {
+        extra_len = 4 + bp[3];
+        feature = sg_get_unaligned_be16(bp + 0);
+        printf("  %s feature\n", get_feature_str(feature, buff));
+        if (brief)
+            continue;
+        if (inner_hex) {
+            hex2stdout(bp, extra_len, 1);
+            continue;
+        }
+        if (0 != (extra_len % 4))
+            printf("    additional length [%d] not a multiple of 4, ignore\n",
+                   extra_len - 4);
+        else
+            decode_feature(feature, bp, extra_len);
+    }
+}
+
+static void
+list_known(bool brief)
+{
+    int k, num;
+
+    num = SG_ARRAY_SIZE(feature_desc_arr);
+    printf("Known features:\n");
+    for (k = 0; k < num; ++k)
+        printf("  %s [0x%x]\n", feature_desc_arr[k].desc,
+               feature_desc_arr[k].val);
+    if (! brief) {
+        printf("Known profiles:\n");
+        num = SG_ARRAY_SIZE(profile_desc_arr);
+        for (k = 0; k < num; ++k)
+            printf("  %s [0x%x]\n", profile_desc_arr[k].desc,
+                   profile_desc_arr[k].val);
+    }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool brief = false;
+    bool inner_hex = false;
+    bool list = false;
+    bool do_raw = false;
+    bool readonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int sg_fd, res, c, len;
+    int peri_type = 0;
+    int rt = 0;
+    int starting = 0;
+    int verbose = 0;
+    int do_hex = 0;
+    const char * device_name = NULL;
+    char buff[64];
+    const char * cp;
+    struct sg_simple_inquiry_resp inq_resp;
+    int ret = 0;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "bchHilqr:Rs:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            brief = true;
+            break;
+        case 'c':
+            rt = 1;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'i':
+            inner_hex = true;
+            break;
+        case 'l':
+            list = true;
+            break;
+        case 'q':
+            readonly = true;
+            break;
+        case 'r':
+            rt = sg_get_num(optarg);
+            if ((rt < 0) || (rt > 3)) {
+                pr2serr("bad argument to '--rt'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'R':
+            do_raw = true;
+            break;
+        case 's':
+            starting = sg_get_num(optarg);
+            if ((starting < 0) || (starting > 0xffff)) {
+                pr2serr("bad argument to '--starting'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (list) {
+        list_known(brief);
+        return 0;
+    }
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((sg_fd = sg_cmds_open_device(device_name, true /* ro */, verbose))
+        < 0) {
+        pr2serr(ME "error opening file: %s (ro): %s\n", device_name,
+                safe_strerror(-sg_fd));
+        return sg_convert_errno(-sg_fd);
+    }
+    if (0 == sg_simple_inquiry(sg_fd, &inq_resp, true, verbose)) {
+        if (! do_raw)
+            printf("  %.8s  %.16s  %.4s\n", inq_resp.vendor, inq_resp.product,
+                   inq_resp.revision);
+        peri_type = inq_resp.peripheral_type;
+        cp = sg_get_pdt_str(peri_type, sizeof(buff), buff);
+        if (! do_raw) {
+            if (strlen(cp) > 0)
+                printf("  Peripheral device type: %s\n", cp);
+            else
+                printf("  Peripheral device type: 0x%x\n", peri_type);
+        }
+    } else {
+        pr2serr(ME "%s doesn't respond to a SCSI INQUIRY\n", device_name);
+        return SG_LIB_CAT_OTHER;
+    }
+    sg_cmds_close_device(sg_fd);
+
+    sg_fd = sg_cmds_open_device(device_name, readonly, verbose);
+    if (sg_fd < 0) {
+        pr2serr(ME "open error (rw): %s\n", safe_strerror(-sg_fd));
+        return sg_convert_errno(-sg_fd);
+    }
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    res = sg_ll_get_config(sg_fd, rt, starting, resp_buffer,
+                              sizeof(resp_buffer), true, verbose);
+    ret = res;
+    if (0 == res) {
+        len = sg_get_unaligned_be32(resp_buffer + 0) + 4;
+        if (do_hex) {
+            if (len > (int)sizeof(resp_buffer))
+                len = sizeof(resp_buffer);
+            hex2stdout(resp_buffer, len, 0);
+        } else if (do_raw)
+            dStrRaw((const char *)resp_buffer, len);
+        else
+            decode_config(resp_buffer, sizeof(resp_buffer), len, brief,
+                          inner_hex);
+    } else {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Get Configuration command: %s\n", b);
+        if (0 == verbose)
+            pr2serr("    try '-v' option for more information\n");
+    }
+
+    res = sg_cmds_close_device(sg_fd);
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-ret);
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_get_config failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_get_elem_status.c b/src/sg_get_elem_status.c
new file mode 100644
index 0000000..574052c
--- /dev/null
+++ b/src/sg_get_elem_status.c
@@ -0,0 +1,659 @@
+/*
+ * Copyright (c) 2019-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI GET PHYSICAL ELEMENT STATUS command to the
+ * given SCSI device.
+ */
+
+static const char * version_str = "1.15 20220807";      /* sbc5r03 */
+
+#define MY_NAME "sg_get_elem_status"
+
+#ifndef UINT32_MAX
+#define UINT32_MAX ((uint32_t)-1)
+#endif
+
+#define GET_PHY_ELEM_STATUS_SA 0x17
+#define DEF_GPES_BUFF_LEN (1024 + 32)
+#define MAX_GPES_BUFF_LEN ((1024 * 1024) + DEF_GPES_BUFF_LEN)
+#define GPES_DESC_OFFSET 32     /* descriptors starts at this byte offset */
+#define GPES_DESC_LEN 32
+#define MIN_MAXLEN 16
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+struct gpes_desc_t {    /* info in returned physical status descriptor */
+    bool restoration_allowed;   /* RALWD bit in sbc4r20a */
+    uint32_t elem_id;
+    uint8_t phys_elem_type;
+    uint8_t phys_elem_health;
+    uint64_t assoc_cap;   /* number of LBs removed if depopulated */
+};
+
+static uint8_t gpesBuff[DEF_GPES_BUFF_LEN];
+
+
+static struct option long_options[] = {
+    {"brief", no_argument, 0, 'b'},
+    {"filter", required_argument, 0, 'f'},
+    {"help", no_argument, 0, 'h'},
+    {"hex", no_argument, 0, 'H'},
+    {"in", required_argument, 0, 'i'},      /* silent, same as --inhex= */
+    {"inhex", required_argument, 0, 'i'},
+    {"json", optional_argument, 0, 'j'},
+    {"maxlen", required_argument, 0, 'm'},
+    {"raw", no_argument, 0, 'r'},
+    {"readonly", no_argument, 0, 'R'},
+    {"report-type", required_argument, 0, 't'},
+    {"report_type", required_argument, 0, 't'},
+    {"starting", required_argument, 0, 's'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_get_elem_status  [--brief] [--filter=FLT] [--help] "
+            "[--hex]\n"
+            "                           [--inhex=FN] [--json[=JO]] "
+            "[--maxlen=LEN]\n"
+            "                           [--raw] [--readonly] "
+            "[--report-type=RT]\n"
+            "                           [--starting=ELEM] [--verbose] "
+            "[--version]\n"
+            "                           DEVICE\n"
+            "  where:\n"
+            "    --brief|-b        one descriptor per line\n"
+            "    --filter=FLT|-f FLT    FLT is 0 (def) for all physical "
+            "elements;\n"
+            "                           1 for out of spec and depopulated "
+            "elements\n"
+            "    --help|-h         print out usage message\n"
+            "    --hex|-H          output in hexadecimal\n"
+            "    --inhex=FN|-i FN    input taken from file FN rather than "
+            "DEVICE,\n"
+            "                        assumed to be ASCII hex or, if --raw, "
+            "in binary\n"
+            "    --json[=JO]|-j[JO]     output in JSON instead of human "
+            "readable text\n"
+            "                           use --json=? for JSON help\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 0 -> %d bytes)\n",
+            DEF_GPES_BUFF_LEN );
+    pr2serr("    --raw|-r          output in binary, unless --inhex=FN is "
+            "given in\n"
+            "                      in which case the input is assumed to be "
+            "binary\n"
+            "    --readonly|-R     open DEVICE read-only (def: read-write)\n"
+            "    --report-type=RT|-t RT    report type: 0-> physical "
+            "elements (def);\n"
+            "                                           1-> storage "
+            "elements\n"
+            "    --starting=ELEM|-s ELEM    ELEM is the lowest identifier "
+            "returned\n"
+            "                               (def: 1 which is lowest "
+            "identifier)\n"
+            "    --verbose|-v      increase verbosity\n"
+            "    --version|-V      print version string and exit\n\n"
+            "Performs a SCSI GET PHYSICAL ELEMENT STATUS command (see SBC-3 "
+            "or SBC-4).\nStorage elements are a sub-set of physical "
+            "elements. Currently the only\ntype of physical element is a "
+            "storage element. If --inhex=FN is given then\ncontents of FN "
+            "is assumed to be a response to this command in ASCII hex.\n"
+            "Returned element descriptors should be in ascending "
+            "identifier order.\n"
+            );
+}
+
+/* Invokes a SCSI GET PHYSICAL ELEMENT STATUS command (SBC-4).  Return of
+ * 0 -> success, various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_get_phy_elem_status(int sg_fd, uint32_t starting_elem, uint8_t filter,
+                          uint8_t report_type, uint8_t * resp,
+                          uint32_t alloc_len, int * residp, bool noisy,
+                          int verbose)
+{
+    int k, ret, res, sense_cat;
+    uint8_t gpesCmd[16] = {SG_SERVICE_ACTION_IN_16,
+                           GET_PHY_ELEM_STATUS_SA, 0, 0, 0, 0,
+                           0, 0, 0, 0,  0, 0, 0, 0,  0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+    static const char * const cmd_name = "Get physical element status";
+
+    if (starting_elem)
+        sg_put_unaligned_be32(starting_elem, gpesCmd + 6);
+    sg_put_unaligned_be32(alloc_len, gpesCmd + 10);
+    if (filter)
+        gpesCmd[14] |= filter << 6;
+    if (report_type)
+        gpesCmd[14] |= (0xf & report_type);
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", cmd_name,
+                sg_get_command_str(gpesCmd, (int)sizeof(gpesCmd), false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj_with_fd(sg_fd, verbose);
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", cmd_name);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, gpesCmd, sizeof(gpesCmd));
+    set_scsi_pt_data_in(ptvp, resp, alloc_len);
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, -1, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cmd_name, res, noisy, verbose,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    k = ret ? (int)alloc_len : get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = k;
+    if ((verbose > 2) && ((alloc_len - k) > 0)) {
+        pr2serr("%s: parameter data returned:\n", cmd_name);
+        hex2stderr((const uint8_t *)resp, alloc_len - k,
+                   ((verbose > 3) ? -1 : 1));
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+static void
+dStrRaw(const char * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* Decodes given physical element status descriptor.  */
+static void
+decode_elem_status_desc(const uint8_t * bp, struct gpes_desc_t * pedp)
+{
+    if ((NULL == bp) || (NULL == pedp))
+        return;
+    pedp->elem_id = sg_get_unaligned_be32(bp + 4);
+    pedp->restoration_allowed = (bool)(bp[13] & 1);
+    pedp->phys_elem_type = bp[14];
+    pedp->phys_elem_health = bp[15];
+    pedp->assoc_cap = sg_get_unaligned_be64(bp + 16);
+}
+
+static bool
+fetch_health_str(uint8_t health, char * bp, int max_blen)
+{
+    bool add_val = false;
+    const char * cp = NULL;
+
+    if  (0 == health)
+        cp = "not reported";
+    else if (health < 0x64) {
+        cp = "within manufacturer's specification limits";
+        add_val = true;
+    } else if (0x64 == health) {
+        cp = "at manufacturer's specification limits";
+        add_val = true;
+    } else if (health < 0xd0) {
+        cp = "outside manufacturer's specification limits";
+        add_val = true;
+    } else if (health < 0xfb) {
+        cp = "reserved";
+        add_val = true;
+    } else if (0xfb == health)
+        cp = "depopulation revocation completed, errors detected";
+    else if (0xfc == health)
+        cp = "depopulation revocation in progress";
+    else if (0xfd == health)
+        cp = "depopulation completed, errors detected";
+    else if (0xfe == health)
+        cp = "depopulation operations in progress";
+    else if (0xff == health)
+        cp = "depopulation completed, no errors";
+    snprintf(bp, max_blen, "%s", cp);
+    return add_val;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool do_raw = false;
+    bool no_final_msg = false;
+    bool o_readonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int k, j, m, n, res, c, rlen, in_len;
+    int sg_fd = -1;
+    int do_brief = 0;
+    int do_hex = 0;
+    int resid = 0;
+    int ret = 0;
+    int maxlen = DEF_GPES_BUFF_LEN;
+    int verbose = 0;
+    uint8_t filter = 0;
+    uint8_t rt = 0;
+    uint32_t num_desc, num_desc_ret, id_elem_depop;
+    uint32_t starting_elem = 0;
+    int64_t ll;
+    const char * device_name = NULL;
+    const char * in_fn = NULL;
+    const char * cp;
+    const uint8_t * bp;
+    uint8_t * gpesBuffp = gpesBuff;
+    uint8_t * free_gpesBuffp = NULL;
+    sgj_opaque_p jop = NULL;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jap = NULL;
+    struct gpes_desc_t a_ped;
+    sgj_state json_st SG_C_CPP_ZERO_INIT;
+    sgj_state * jsp = &json_st;
+    char b[80];
+    static const int blen = sizeof(b);
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "bf:hHi:j::m:rRs:St:TvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            ++do_brief;
+            break;
+        case 'f':
+            n = sg_get_num_nomult(optarg);
+            if ((n < 0) || (n > 15)) {
+                pr2serr("'--filter=RT' should be between 0 and 15 "
+                        "(inclusive)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            filter = n;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'i':
+            in_fn = optarg;
+            break;
+        case 'j':
+            if (! sgj_init_state(&json_st, optarg)) {
+                int bad_char = json_st.first_bad_char;
+                char e[1500];
+
+                if (bad_char) {
+                    pr2serr("bad argument to --json= option, unrecognized "
+                            "character '%c'\n\n", bad_char);
+                }
+                sg_json_usage(0, e, sizeof(e));
+                pr2serr("%s", e);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'm':
+            maxlen = sg_get_num(optarg);
+            if ((maxlen < 0) || (maxlen > MAX_GPES_BUFF_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or less\n",
+                        MAX_GPES_BUFF_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (0 == maxlen)
+                maxlen = DEF_GPES_BUFF_LEN;
+            else if (maxlen < MIN_MAXLEN) {
+                pr2serr("Warning: --maxlen=LEN less than %d ignored\n",
+                        MIN_MAXLEN);
+                maxlen = DEF_GPES_BUFF_LEN;
+            }
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 's':
+            ll = sg_get_llnum(optarg);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("bad argument to '--starting='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            starting_elem = (uint32_t)ll;
+            break;
+        case 't':       /* --report-type=RT */
+            n = sg_get_num_nomult(optarg);
+            if ((n < 0) || (n > 15)) {
+                pr2serr("'--report-type=RT' should be between 0 and 15 "
+                        "(inclusive)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            rt = n;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+    if (jsp->pr_as_json)
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+
+    if (maxlen > DEF_GPES_BUFF_LEN) {
+        gpesBuffp = (uint8_t *)sg_memalign(maxlen, 0, &free_gpesBuffp,
+                                           verbose > 3);
+        if (NULL == gpesBuffp) {
+            pr2serr("unable to allocate %d bytes on heap\n", maxlen);
+            return sg_convert_errno(ENOMEM);
+        }
+    }
+    if (device_name && in_fn) {
+        pr2serr("ignoring DEVICE, best to give DEVICE or --inhex=FN, but "
+                "not both\n");
+        device_name = NULL;
+    }
+    if (NULL == device_name) {
+        if (in_fn) {
+            if ((ret = sg_f2hex_arr(in_fn, do_raw, false, gpesBuffp,
+                                    &in_len, maxlen))) {
+                if (SG_LIB_LBA_OUT_OF_RANGE == ret) {
+                    pr2serr("--maxlen=%d needs to be increased", maxlen);
+                    if (in_len > 7) {
+                        n = (sg_get_unaligned_be32(gpesBuffp + 4) *
+                             GPES_DESC_LEN) + GPES_DESC_OFFSET;
+                        pr2serr(" to at least %d\n", n);
+                    } else
+                        pr2serr("\n");
+                    pr2serr("... decode what we have\n");
+                    no_final_msg = true;
+                } else
+                    goto fini;
+            }
+            if (verbose > 2)
+                pr2serr("Read %d [0x%x] bytes of user supplied data\n",
+                        in_len, in_len);
+            if (do_raw)
+                do_raw = false;    /* can interfere on decode */
+            if (in_len < 4) {
+                pr2serr("--in=%s only decoded %d bytes (needs 4 at least)\n",
+                        in_fn, in_len);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto fini;
+            }
+            res = 0;
+            goto start_response;
+        } else {
+            pr2serr("missing device name!\n\n");
+            usage();
+            ret = SG_LIB_FILE_ERROR;
+            no_final_msg = true;
+            goto fini;
+        }
+    }
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto fini;
+        }
+    }
+    sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose);
+    if (sg_fd < 0) {
+        pr2serr("open error: %s: %s\n", device_name, safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    res = sg_ll_get_phy_elem_status(sg_fd, starting_elem, filter, rt,
+                                    gpesBuffp, maxlen, &resid, true, verbose);
+    ret = res;
+    if (res)
+        goto error;
+
+start_response:
+    k = maxlen - resid;
+    if (k < 4) {
+        pr2serr("Response too short (%d bytes) due to resid (%d)\n", k,
+                resid);
+        if ((k > 0) && (do_raw || do_hex)) {
+            if (do_hex) {
+                if (do_hex > 2)
+                    hex2stdout(gpesBuffp, k, -1);
+                else
+                    hex2stdout(gpesBuffp, k, (2 == do_hex) ? 0 : 1);
+            } else
+                dStrRaw((const char *)gpesBuffp, k);
+        }
+        ret = SG_LIB_CAT_MALFORMED;
+        goto fini;
+    } else
+        maxlen -= resid;
+    num_desc = sg_get_unaligned_be32(gpesBuffp + 0);
+    if (maxlen > 7) {
+        num_desc_ret = sg_get_unaligned_be32(gpesBuffp + 4);
+        id_elem_depop = (maxlen > 11) ? sg_get_unaligned_be32(gpesBuffp + 8) :
+                                        0;
+    } else {
+        num_desc_ret = 0;
+        id_elem_depop = 0;
+    }
+    rlen = (num_desc_ret * GPES_DESC_LEN) + GPES_DESC_OFFSET;
+    if ((verbose > 1) || (verbose && (rlen > maxlen))) {
+        pr2serr("response length %d bytes\n", rlen);
+        if (rlen > maxlen)
+            pr2serr("  ... which is greater than maxlen (allocation "
+                    "length %d), truncation\n", maxlen);
+    }
+    if (rlen > maxlen)
+        rlen = maxlen;
+    if (do_raw) {
+        dStrRaw((const char *)gpesBuffp, rlen);
+        goto fini;
+    }
+    if (do_hex) {
+        if (do_hex > 2)
+            hex2stdout(gpesBuffp, rlen, -1);
+        else
+            hex2stdout(gpesBuffp, rlen,  (2 == do_hex) ? 0 : 1);
+        goto fini;
+    }
+
+    sgj_haj_vi(jsp, jop, 0, "Number of descriptors",
+               SGJ_SEP_COLON_1_SPACE, num_desc, true);
+    sgj_haj_vi(jsp, jop, 0, "Number of descriptors returned",
+               SGJ_SEP_COLON_1_SPACE, num_desc_ret, true);
+    sgj_haj_vi(jsp, jop, 0, "Identifier of element being depopulated",
+               SGJ_SEP_COLON_1_SPACE, id_elem_depop, true);
+    if (rlen < 64) {
+        sgj_pr_hr(jsp, "No complete physical element status descriptors "
+                  "available\n");
+        goto fini;
+    } else {
+        if (do_brief > 2)
+            goto fini;
+        sgj_pr_hr(jsp, "\n");
+    }
+
+    if (jsp->pr_as_json)
+        jap = sgj_named_subarray_r(jsp, jop,
+                                   "physical_element_status_descriptor");
+    for (bp = gpesBuffp + GPES_DESC_OFFSET, k = 0; k < (int)num_desc_ret;
+         bp += GPES_DESC_LEN, ++k) {
+        if ((0 == k) && (do_brief < 2))
+            sgj_pr_hr(jsp, "Element descriptors:\n");
+        decode_elem_status_desc(bp, &a_ped);
+        if (jsp->pr_as_json) {
+            jo2p = sgj_new_unattached_object_r(jsp);
+            sgj_js_nv_ihex(jsp, jo2p, "element_identifier",
+                           (int64_t)a_ped.elem_id);
+            cp = (1 == a_ped.phys_elem_type) ? "storage" : "reserved";
+            sgj_js_nv_istr(jsp, jo2p, "physical_element_type",
+                           a_ped.phys_elem_type, "meaning", cp);
+            j = a_ped.phys_elem_health;
+            fetch_health_str(j, b, blen);
+            sgj_js_nv_istr(jsp, jo2p, "physical_element_health", j, NULL, b);
+            sgj_js_nv_ihex(jsp, jo2p, "associated_capacity",
+                           (int64_t)a_ped.assoc_cap);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+        } else if (do_brief) {
+            sgj_pr_hr(jsp, "%u: %u,%u\n", a_ped.elem_id, a_ped.phys_elem_type,
+                      a_ped.phys_elem_health);
+        } else {
+            char b2[144];
+            static const int b2len = sizeof(b2);
+
+            m = 0;
+            m += sg_scnpr(b2 + m, b2len - m, "[%d] identifier: 0x%06x",
+                          k + 1, a_ped.elem_id);
+            if (sg_all_ffs((const uint8_t *)&a_ped.assoc_cap, 8))
+                m += sg_scnpr(b2 + m, b2len - m,
+                              "  associated LBs: not specified;  ");
+            else
+                m += sg_scnpr(b2 + m, b2len - m, "  associated LBs: 0x%"
+                              PRIx64 ";  ", a_ped.assoc_cap);
+            m += sg_scnpr(b2 + m, b2len - m, "health: ");
+            j = a_ped.phys_elem_health;
+            if (fetch_health_str(j, b, blen))
+                m += sg_scnpr(b2 + m, b2len - m, "%s <%d>", b, j);
+            else
+                m += sg_scnpr(b2 + m, b2len - m, "%s", b);
+            if (a_ped.restoration_allowed)
+                m += sg_scnpr(b2 + m, b2len - m,
+                              " [restoration allowed [RALWD]]");
+            sgj_pr_hr(jsp, "%s\n", b2);
+        }
+    }
+    goto fini;
+
+error:
+    if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("Get LBA Status command not supported\n");
+    else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+        pr2serr("Get LBA Status command: bad field in cdb\n");
+    else {
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Get LBA Status command: %s\n", b);
+    }
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (free_gpesBuffp)
+        free(free_gpesBuffp);
+    if ((0 == verbose) && (! no_final_msg)) {
+        if (! sg_if_can2stderr("sg_get_elem_status failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    ret = (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+    if (jsp->pr_as_json) {
+        if (0 == do_hex)
+            sgj_js2file(jsp, NULL, ret, stdout);
+        sgj_finish(jsp);
+    }
+    return ret;
+}
diff --git a/src/sg_get_lba_status.c b/src/sg_get_lba_status.c
new file mode 100644
index 0000000..97b967d
--- /dev/null
+++ b/src/sg_get_lba_status.c
@@ -0,0 +1,693 @@
+/*
+ * Copyright (c) 2009-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI GET LBA STATUS command to the given SCSI
+ * device.
+ */
+
+static const char * version_str = "1.31 20220807";      /* sbc5r03 */
+
+#define MY_NAME "sg_get_lba_status"
+
+#ifndef UINT32_MAX
+#define UINT32_MAX ((uint32_t)-1)
+#endif
+
+#define MAX_GLBAS_BUFF_LEN (1024 * 1024)
+#define DEF_GLBAS_BUFF_LEN 1024
+#define MIN_MAXLEN 16
+
+static uint8_t glbasFixedBuff[DEF_GLBAS_BUFF_LEN];
+
+
+static struct option long_options[] = {
+        {"16", no_argument, 0, 'S'},
+        {"32", no_argument, 0, 'T'},
+        {"brief", no_argument, 0, 'b'},
+        {"blockhex", no_argument, 0, 'B'},
+        {"element-id", required_argument, 0, 'e'},
+        {"element_id", required_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"in", required_argument, 0, 'i'},      /* silent, same as --inhex= */
+        {"inhex", required_argument, 0, 'i'},
+        {"json", optional_argument, 0, 'j'},
+        {"lba", required_argument, 0, 'l'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"report-type", required_argument, 0, 't'},
+        {"report_type", required_argument, 0, 't'},
+        {"scan-len", required_argument, 0, 's'},
+        {"scan_len", required_argument, 0, 's'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_get_lba_status  [--16] [--32] [--blockhex] "
+            "[--brief]\n"
+            "                          [--element-id=EI] [--help] [--hex] "
+            "[--inhex=FN]\n"
+            "                          [--lba=LBA] [--maxlen=LEN] [--raw] "
+            "[--readonly]\n"
+            "                          [--report-type=RT] [--scan-len=SL] "
+            "[--verbose]\n"
+            "                          [--version] DEVICE\n"
+            "  where:\n"
+            "    --16|-S           use GET LBA STATUS(16) cdb (def)\n"
+            "    --32|-T           use GET LBA STATUS(32) cdb\n"
+            "    --blockhex|-B     outputs the (number of) blocks field "
+            " in hex\n"
+            "    --brief|-b        a descriptor per line:\n"
+            "                          <lba_hex blocks_hex p_status "
+            "add_status>\n"
+            "                      use twice ('-bb') for given LBA "
+            "provisioning status\n"
+            "    --element-id=EI|-e EI      EI is the element identifier "
+            "(def: 0)\n"
+            "    --help|-h         print out usage message\n"
+            "    --hex|-H          output in hexadecimal\n"
+            "    --inhex=FN|-i FN    input taken from file FN rather than "
+            "DEVICE,\n"
+            "                        assumed to be ASCII hex or, if --raw, "
+            "in binary\n"
+            "    --json[=JO]|-j[JO]    output in JSON instead of human "
+            "readable text.\n"
+            "                          Use --json=? for JSON help\n"
+            "    --lba=LBA|-l LBA    starting LBA (logical block address) "
+            "(def: 0)\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 0 -> %d bytes)\n",
+            DEF_GLBAS_BUFF_LEN );
+    pr2serr("    --raw|-r          output in binary, unless if --inhex=FN "
+            "is given,\n"
+            "                      in which case input file is binary\n"
+            "    --readonly|-R     open DEVICE read-only (def: read-write)\n"
+            "    --report-type=RT|-t RT    report type: 0->all LBAs (def);\n"
+            "                                1-> LBAs with non-zero "
+            "provisioning status\n"
+            "                                2-> LBAs that are mapped\n"
+            "                                3-> LBAs that are deallocated\n"
+            "                                4-> LBAs that are anchored\n"
+            "                                16-> LBAs that may return "
+            "unrecovered error\n"
+            "    --scan-len=SL|-s SL    SL in maximum scan length (unit: "
+            "logical blocks)\n"
+            "                           (def: 0 which implies no limit)\n"
+            "    --verbose|-v      increase verbosity\n"
+            "    --version|-V      print version string and exit\n\n"
+            "Performs a SCSI GET LBA STATUS(16) or GET LBA STATUS(32) "
+            "command (SBC-3 and\nSBC-4). The --element-id=EI and the "
+            "--scan-len=SL fields are only active\non the 32 byte cdb "
+            "variant. If --inhex=FN is given then contents of FN is\n"
+            "assumed to be a response to this command.\n"
+            );
+}
+
+static void
+dStrRaw(const char * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* Decodes given LBA status descriptor passing back the starting LBA,
+ * the number of blocks and returns the provisioning status, -1 for error.
+ */
+static int
+decode_lba_status_desc(const uint8_t * bp, uint64_t * slbap,
+                       uint32_t * blocksp, uint8_t * add_statusp)
+{
+    uint32_t blocks;
+    uint64_t ull;
+
+    if (NULL == bp)
+        return -1;
+    ull = sg_get_unaligned_be64(bp + 0);
+    blocks = sg_get_unaligned_be32(bp + 8);
+    if (slbap)
+        *slbap = ull;
+    if (blocksp)
+        *blocksp = blocks;
+    if (add_statusp)
+        *add_statusp = bp[13];
+    return bp[12] & 0xf;
+}
+
+static char *
+get_prov_status_str(int ps, char * b, int blen)
+{
+    switch (ps) {
+    case 0:
+        sg_scnpr(b, blen, "mapped (or unknown)");
+        break;
+    case 1:
+        sg_scnpr(b, blen, "deallocated");
+        break;
+    case 2:
+        sg_scnpr(b, blen, "anchored");
+        break;
+    case 3:
+        sg_scnpr(b, blen, "mapped");         /* sbc4r12 */
+        break;
+    case 4:
+        sg_scnpr(b, blen, "unknown");        /* sbc4r12 */
+        break;
+    default:
+        sg_scnpr(b, blen, "unknown provisioning status: %d", ps);
+        break;
+    }
+    return b;
+}
+
+static char *
+get_pr_status_str(int as, char * b, int blen)
+{
+    switch (as) {
+    case 0:
+        sg_scnpr(b, blen, "%s", "");
+        break;
+    case 1:
+        sg_scnpr(b, blen, "may contain unrecovered errors");
+        break;
+    default:
+        sg_scnpr(b, blen, "unknown additional status: %d", as);
+        break;
+    }
+    return b;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool do_16 = false;
+    bool do_32 = false;
+    bool do_raw = false;
+    bool no_final_msg = false;
+    bool o_readonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int k, j, res, c, n, rlen, num_descs, completion_cond, in_len;
+    int sg_fd = -1;
+    int blockhex = 0;
+    int do_brief = 0;
+    int do_hex = 0;
+    int ret = 0;
+    int maxlen = DEF_GLBAS_BUFF_LEN;
+    int rt = 0;
+    int verbose = 0;
+    uint8_t add_status = 0;     /* keep gcc quiet */
+    uint64_t d_lba = 0;
+    uint32_t d_blocks = 0;
+    uint32_t element_id = 0;
+    uint32_t scan_len = 0;
+    int64_t ll;
+    uint64_t lba = 0;
+    const char * device_name = NULL;
+    const char * in_fn = NULL;
+    const uint8_t * bp;
+    uint8_t * glbasBuffp = glbasFixedBuff;
+    uint8_t * free_glbasBuffp = NULL;
+    sgj_opaque_p jop = NULL;
+    sgj_opaque_p jo2p = NULL;
+    sgj_opaque_p jap = NULL;
+    sgj_state json_st SG_C_CPP_ZERO_INIT;
+    sgj_state * jsp = &json_st;
+    char b[144];
+    static const size_t blen = sizeof(b);
+    static const char * prov_stat_s = "Provisoning status";
+    static const char * add_stat_s = "Additional status";
+    static const char * compl_cond_s = "Completion condition";
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "bBe:hi:j::Hl:m:rRs:St:TvV",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            ++do_brief;
+            break;
+        case 'B':
+            ++blockhex;
+            break;
+        case 'e':
+            ll = sg_get_llnum(optarg);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("bad argument to '--element-id'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            element_id = (uint32_t)ll;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'i':
+            in_fn = optarg;
+            break;
+        case 'j':
+            if (! sgj_init_state(&json_st, optarg)) {
+                int bad_char = json_st.first_bad_char;
+                char e[1500];
+
+                if (bad_char) {
+                    pr2serr("bad argument to --json= option, unrecognized "
+                            "character '%c'\n\n", bad_char);
+                }
+                sg_json_usage(0, e, sizeof(e));
+                pr2serr("%s", e);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'l':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            lba = (uint64_t)ll;
+            break;
+        case 'm':
+            maxlen = sg_get_num(optarg);
+            if ((maxlen < 0) || (maxlen > MAX_GLBAS_BUFF_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or less\n",
+                        MAX_GLBAS_BUFF_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (0 == maxlen)
+                maxlen = DEF_GLBAS_BUFF_LEN;
+            else if (maxlen < MIN_MAXLEN) {
+                pr2serr("Warning: --maxlen=LEN less than %d ignored\n",
+                        MIN_MAXLEN);
+                maxlen = DEF_GLBAS_BUFF_LEN;
+            }
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 's':
+            ll = sg_get_llnum(optarg);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("bad argument to '--scan-len'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            scan_len = (uint32_t)ll;
+            break;
+        case 'S':
+            do_16 = true;
+            break;
+        case 't':
+            rt = sg_get_num_nomult(optarg);
+            if ((rt < 0) || (rt > 255)) {
+                pr2serr("'--report-type=RT' should be between 0 and 255 "
+                        "(inclusive)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'T':
+            do_32 = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+    if (jsp->pr_as_json)
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+
+    if (maxlen > DEF_GLBAS_BUFF_LEN) {
+        glbasBuffp = (uint8_t *)sg_memalign(maxlen, 0, &free_glbasBuffp,
+                                            verbose > 3);
+        if (NULL == glbasBuffp) {
+            pr2serr("unable to allocate %d bytes on heap\n", maxlen);
+            return sg_convert_errno(ENOMEM);
+        }
+    }
+    if (device_name && in_fn) {
+        pr2serr("ignoring DEVICE, best to give DEVICE or --inhex=FN, but "
+                "not both\n");
+        device_name = NULL;
+    }
+    if (NULL == device_name) {
+        if (in_fn) {
+            if ((ret = sg_f2hex_arr(in_fn, do_raw, false, glbasBuffp,
+                                    &in_len, maxlen))) {
+                if (SG_LIB_LBA_OUT_OF_RANGE == ret) {
+                    no_final_msg = true;
+                    pr2serr("... decode what we have, --maxlen=%d needs to "
+                            "be increased\n", maxlen);
+                } else
+                    goto fini;
+            }
+            if (verbose > 2)
+                pr2serr("Read %d [0x%x] bytes of user supplied data\n",
+                        in_len, in_len);
+            if (do_raw)
+                do_raw = false;    /* can interfere on decode */
+            if (in_len < 4) {
+                pr2serr("--in=%s only decoded %d bytes (needs 4 at least)\n",
+                        in_fn, in_len);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto fini;
+            }
+            goto start_response;
+        } else {
+            pr2serr("missing device name!\n\n");
+            usage();
+            ret = SG_LIB_FILE_ERROR;
+            no_final_msg = true;
+            goto fini;
+        }
+    }
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto fini;
+        }
+    }
+    if (do_16 && do_32) {
+        pr2serr("both --16 and --32 given, choose --16\n");
+        do_32 = false;
+    } else if ((! do_16) && (! do_32)) {
+        if (verbose > 3)
+            pr2serr("choosing --16\n");
+        do_16 = true;
+    }
+    if (do_16) {
+        if (element_id != 0)
+            pr2serr("Warning: --element_id= ignored with 16 byte cdb\n");
+        if (scan_len != 0)
+            pr2serr("Warning: --scan_len= ignored with 16 byte cdb\n");
+    }
+    sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose);
+    if (sg_fd < 0) {
+        pr2serr("open error: %s: %s\n", device_name, safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    res = 0;
+    if (do_16)
+        res = sg_ll_get_lba_status16(sg_fd, lba, rt, glbasBuffp, maxlen, true,
+                                     verbose);
+    else if (do_32)     /* keep analyser happy since do_32 must be true */
+        res = sg_ll_get_lba_status32(sg_fd, lba, scan_len, element_id, rt,
+                                     glbasBuffp, maxlen, true, verbose);
+
+    ret = res;
+    if (res)
+        goto error;
+
+start_response:
+    /* in sbc3r25 offset for calculating the 'parameter data length'
+     * (rlen variable below) was reduced from 8 to 4. */
+    if (maxlen >= 4)
+        rlen = sg_get_unaligned_be32(glbasBuffp + 0) + 4;
+    else
+        rlen = maxlen;
+    k = (rlen > maxlen) ? maxlen : rlen;
+    if (do_raw) {
+        dStrRaw((const char *)glbasBuffp, k);
+        goto fini;
+    }
+    if (do_hex) {
+        if (do_hex > 2)
+            hex2stdout(glbasBuffp, k, -1);
+        else
+            hex2stdout(glbasBuffp, k, (2 == do_hex) ? 0 : 1);
+        goto fini;
+    }
+    if (maxlen < 4) {
+        if (verbose)
+            pr2serr("Exiting because allocation length (maxlen) less "
+                    "than 4\n");
+        goto fini;
+    }
+    if ((verbose > 1) || (verbose && (rlen > maxlen))) {
+        pr2serr("response length %d bytes\n", rlen);
+        if (rlen > maxlen)
+            pr2serr("  ... which is greater than maxlen (allocation "
+                    "length %d), truncation\n", maxlen);
+    }
+    if (rlen > maxlen)
+        rlen = maxlen;
+
+    if (do_brief > 1) {
+        if (rlen > DEF_GLBAS_BUFF_LEN) {
+            pr2serr("Need maxlen and response length to be at least %d, "
+                    "have %d bytes\n", DEF_GLBAS_BUFF_LEN, rlen);
+            ret = SG_LIB_CAT_OTHER;
+            goto fini;
+        }
+        res = decode_lba_status_desc(glbasBuffp + 8, &d_lba, &d_blocks,
+                                     &add_status);
+        if ((res < 0) || (res > 15)) {
+            pr2serr("first LBA status descriptor returned %d ??\n", res);
+            ret = SG_LIB_LOGIC_ERROR;
+            goto fini;
+        }
+        if ((lba < d_lba) || (lba >= (d_lba + d_blocks))) {
+            pr2serr("given LBA not in range of first descriptor:\n"
+                    "  descriptor LBA: 0x");
+            for (j = 0; j < 8; ++j)
+                pr2serr("%02x", glbasBuffp[8 + j]);
+            pr2serr("  blocks: 0x%x  p_status: %d  add_status: 0x%x\n",
+                    (unsigned int)d_blocks, res,
+                    (unsigned int)add_status);
+            ret = SG_LIB_CAT_OTHER;
+            goto fini;
+        }
+        sgj_pr_hr(jsp,"p_status: %d  add_status: 0x%x\n", res,
+                  (unsigned int)add_status);
+        if (jsp->pr_as_json) {
+            sgj_js_nv_i(jsp, jop, prov_stat_s, res);
+            sgj_js_nv_i(jsp, jop, add_stat_s, add_status);
+        }
+        goto fini;
+    }
+
+    if (rlen < 24) {
+        sgj_pr_hr(jsp, "No complete LBA status descriptors available\n");
+        goto fini;
+    }
+    num_descs = (rlen - 8) / 16;
+    completion_cond = (*(glbasBuffp + 7) >> 1) & 7; /* added sbc4r14 */
+    if (do_brief)
+        sgj_haj_vi(jsp, jop, 0, compl_cond_s, SGJ_SEP_EQUAL_NO_SPACE,
+                   completion_cond, true);
+    else {
+        switch (completion_cond) {
+        case 0:
+            snprintf(b, blen, "No indication of the completion condition");
+            break;
+        case 1:
+            snprintf(b, blen, "Command completed due to meeting allocation "
+                     "length");
+            break;
+        case 2:
+            snprintf(b, blen, "Command completed due to meeting scan length");
+            break;
+        case 3:
+            snprintf(b, blen, "Command completed due to meeting capacity of "
+                   "medium");
+            break;
+        default:
+            snprintf(b, blen, "Command completion is reserved [%d]",
+                   completion_cond);
+            break;
+        }
+        sgj_pr_hr(jsp, "%s\n", b);
+        sgj_js_nv_istr(jsp, jop, compl_cond_s, completion_cond,
+                       NULL /* "meaning" */, b);
+    }
+    sgj_haj_vi(jsp, jop, 0, "RTP", SGJ_SEP_EQUAL_NO_SPACE,
+               *(glbasBuffp + 7) & 0x1, true);    /* added sbc4r12 */
+    if (verbose)
+        pr2serr("%d complete LBA status descriptors found\n", num_descs);
+    if (jsp->pr_as_json)
+        jap = sgj_named_subarray_r(jsp, jop, "lba_status_descriptor");
+
+    for (bp = glbasBuffp + 8, k = 0; k < num_descs; bp += 16, ++k) {
+        res = decode_lba_status_desc(bp, &d_lba, &d_blocks, &add_status);
+        if ((res < 0) || (res > 15))
+            pr2serr("descriptor %d: bad LBA status descriptor returned "
+                    "%d\n", k + 1, res);
+        if (jsp->pr_as_json)
+            jo2p = sgj_new_unattached_object_r(jsp);
+        if (do_brief) {
+            n = 0;
+            n += sg_scnpr(b + n, blen - n, "0x");
+            for (j = 0; j < 8; ++j)
+                n += sg_scnpr(b + n, blen - n, "%02x", bp[j]);
+            if ((0 == blockhex) || (1 == (blockhex % 2)))
+                n += sg_scnpr(b + n, blen - n, "  0x%x  %d  %d",
+                              (unsigned int)d_blocks, res, add_status);
+            else
+                n += sg_scnpr(b + n, blen - n, "  %u  %d  %d",
+                              (unsigned int)d_blocks, res, add_status);
+            sgj_pr_hr(jsp, "%s\n", b);
+            sgj_js_nv_ihex(jsp, jo2p, "lba", d_lba);
+            sgj_js_nv_ihex(jsp, jo2p, "blocks", d_blocks);
+            sgj_js_nv_i(jsp, jo2p, prov_stat_s, res);
+            sgj_js_nv_i(jsp, jo2p, add_stat_s, add_status);
+        } else {
+            if (jsp->pr_as_json) {
+                sgj_js_nv_ihex(jsp, jo2p, "lba", d_lba);
+                sgj_js_nv_ihex(jsp, jo2p, "blocks", d_blocks);
+                sgj_js_nv_istr(jsp, jo2p, prov_stat_s, res, NULL,
+                               get_prov_status_str(res, b, blen));
+                sgj_js_nv_istr(jsp, jo2p, add_stat_s, add_status, NULL,
+                               get_pr_status_str(add_status, b, blen));
+            } else {
+                char d[64];
+
+                n = 0;
+                n += sg_scnpr(b + n, blen - n, "[%d] LBA: 0x", k + 1);
+                for (j = 0; j < 8; ++j)
+                    n += sg_scnpr(b + n, blen - n, "%02x", bp[j]);
+                if (1 == (blockhex % 2)) {
+
+                    snprintf(d, sizeof(d), "0x%x", d_blocks);
+                    n += sg_scnpr(b + n, blen - n, "  blocks: %10s", d);
+                } else
+                    n += sg_scnpr(b + n, blen - n, "  blocks: %10u",
+                                  (unsigned int)d_blocks);
+                get_prov_status_str(res, d, sizeof(d));
+                n += sg_scnpr(b + n, blen - n, "  %s", d);
+                get_pr_status_str(add_status, d, sizeof(d));
+                if (strlen(d) > 0)
+                    n += sg_scnpr(b + n, blen - n, "  [%s]", d);
+                sgj_pr_hr(jsp, "%s\n", b);
+            }
+        }
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+    if ((num_descs * 16) + 8 < rlen)
+        pr2serr("incomplete trailing LBA status descriptors found\n");
+    goto fini;
+
+error:
+    if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("Get LBA Status command not supported\n");
+    else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+        pr2serr("Get LBA Status command: bad field in cdb\n");
+    else {
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Get LBA Status command: %s\n", b);
+    }
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (free_glbasBuffp)
+        free(free_glbasBuffp);
+    if ((0 == verbose) && (! no_final_msg)) {
+        if (! sg_if_can2stderr("sg_get_lba_status failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    ret = (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+    if (jsp->pr_as_json) {
+        if (0 == do_hex)
+            sgj_js2file(jsp, NULL, ret, stdout);
+        sgj_finish(jsp);
+    }
+    return ret;
+}
diff --git a/src/sg_ident.c b/src/sg_ident.c
new file mode 100644
index 0000000..2bf00bb
--- /dev/null
+++ b/src/sg_ident.c
@@ -0,0 +1,318 @@
+/*
+ * Copyright (c) 2005-2018 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues these SCSI commands: REPORT IDENTIFYING INFORMATION
+ * and SET IDENTIFYING INFORMATION. These commands were called REPORT
+ * DEVICE IDENTIFIER and SET DEVICE IDENTIFIER prior to spc4r07.
+ */
+
+static const char * version_str = "1.23 20180814";
+
+#define ME "sg_ident: "
+
+#define REPORT_ID_INFO_SANITY_LEN 512
+
+
+static struct option long_options[] = {
+        {"ascii", no_argument, 0, 'A'},
+        {"clear", no_argument, 0, 'C'},
+        {"help", no_argument, 0, 'h'},
+        {"itype", required_argument, 0, 'i'},
+        {"raw", no_argument, 0, 'r'},
+        {"set", no_argument, 0, 'S'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void
+decode_ii(const uint8_t * iip, int ii_len, int itype, bool ascii,
+          bool raw, int verbose)
+{
+    int k;
+
+    if (raw) {
+        if (ii_len > 0) {
+            int n;
+
+            if (sg_set_binary_mode(STDOUT_FILENO) < 0)
+                perror("sg_set_binary_mode");
+#if 0
+            n = fwrite(iip, 1, ii_len, stdout);
+#else
+            n = write(STDOUT_FILENO, iip, ii_len);
+#endif
+            if (verbose && (n < 1))
+                pr2serr("unable to write to stdout\n");
+        }
+        return;
+    }
+    if (0x7f == itype) {  /* list of available information types */
+        for (k = 0; k < (ii_len - 3); k += 4)
+            printf("  Information type: %d, Maximum information length: "
+                   "%d bytes\n", iip[k], sg_get_unaligned_be16(iip + 2));
+    } else {        /* single element */
+        if (verbose)
+            printf("Information:\n");
+        if (ii_len > 0) {
+            if (ascii)
+                printf("%.*s\n", ii_len, (const char *)iip);
+            else
+                hex2stdout(iip, ii_len, 0);
+        }
+    }
+}
+
+static void
+usage(void)
+{
+    pr2serr("Usage: sg_ident   [--ascii] [--clear] [--help] [--itype=IT] "
+            "[--raw] [--set]\n"
+            "                  [--verbose] [--version] DEVICE\n"
+            "  where:\n"
+            "    --ascii|-A      report identifying information as ASCII "
+            "(or UTF8) string\n"
+            "    --clear|-C      clear (set to zero length) identifying "
+            "information\n"
+            "    --help|-h       print out usage message\n"
+            "    --itype=IT|-i IT    specify identifying information type "
+            "(def: 0)\n"
+            "    --raw|-r        output identifying information to "
+            "stdout\n"
+            "    --set|-S        invoke set identifying information with "
+            "data from stdin\n"
+            "    --verbose|-v    increase verbosity of output\n"
+            "    --version|-V    print version string and exit\n\n"
+            "Performs a SCSI REPORT (or SET) IDENTIFYING INFORMATION "
+            "command. When no\noptions are given then REPORT IDENTIFYING "
+            "INFORMATION is sent and the\nresponse is output in "
+            "hexadecimal with ASCII to the right.\n");
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool ascii = false;
+    bool do_clear = false;
+    bool raw = false;
+    bool do_set = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int sg_fd, res, c, ii_len;
+    uint8_t rdi_buff[REPORT_ID_INFO_SANITY_LEN + 4];
+    char b[80];
+    uint8_t * bp = NULL;
+    int itype = 0;
+    int verbose = 0;
+    const char * device_name = NULL;
+    int ret = 0;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "AChi:rSvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'A':
+            ascii = true;
+            break;
+        case 'C':
+            do_clear = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+           itype = sg_get_num(optarg);
+           if ((itype < 0) || (itype > 127)) {
+                pr2serr("argument to '--itype' should be in range 0 to 127\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'r':
+            raw = true;
+            break;
+        case 'S':
+            do_set = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (do_set && do_clear) {
+        pr2serr("only one of '--clear' and '--set' can be given\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    if (ascii && raw) {
+        pr2serr("only one of '--ascii' and '--raw' can be given\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    if ((do_set || do_clear) && (raw || ascii)) {
+        pr2serr("'--set' cannot be used with either '--ascii' or '--raw'\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    sg_fd = sg_cmds_open_device(device_name, false /* rw=false */, verbose);
+    if (sg_fd < 0) {
+        pr2serr(ME "open error: %s: %s\n", device_name, safe_strerror(-sg_fd));
+        return sg_convert_errno(-sg_fd);
+    }
+
+    memset(rdi_buff, 0x0, sizeof(rdi_buff));
+    if (do_set || do_clear) {
+        if (do_set) {
+            res = fread(rdi_buff, 1, REPORT_ID_INFO_SANITY_LEN + 2, stdin);
+            if (res <= 0) {
+                pr2serr("no data read from stdin; to clear identifying "
+                        "information use '--clear' instead\n");
+                ret = -1;
+                goto err_out;
+            } else if (res > REPORT_ID_INFO_SANITY_LEN) {
+                pr2serr("SPC-4 limits information length to 512 bytes\n");
+                ret = -1;
+                goto err_out;
+            }
+            ii_len = res;
+            res = sg_ll_set_id_info(sg_fd, itype, rdi_buff, ii_len, true,
+                                    verbose);
+        } else    /* do_clear */
+            res = sg_ll_set_id_info(sg_fd, itype, rdi_buff, 0, true, verbose);
+        if (res) {
+            ret = res;
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("Set identifying information: %s\n", b);
+            if (0 == verbose)
+                pr2serr("    try '-v' for more information\n");
+        }
+    } else {    /* do report identifying information */
+        res = sg_ll_report_id_info(sg_fd, itype, rdi_buff, 4, true, verbose);
+        if (0 == res) {
+            ii_len = sg_get_unaligned_be32(rdi_buff + 0);
+            if ((! raw) && (verbose > 0))
+                printf("Reported identifying information length = %d\n",
+                       ii_len);
+            if (0 == ii_len) {
+                if (verbose > 1)
+                    pr2serr("    This implies the device has an empty "
+                            "information field\n");
+                goto err_out;
+            }
+            if (ii_len > REPORT_ID_INFO_SANITY_LEN) {
+                pr2serr("    That length (%d) seems too long for an "
+                        "information\n", ii_len);
+                ret = -1;
+                goto err_out;
+            }
+            bp = rdi_buff;
+            res = sg_ll_report_id_info(sg_fd, itype, bp, ii_len + 4, true,
+                                       verbose);
+            if (0 == res) {
+                ii_len = sg_get_unaligned_be32(bp + 0);
+                decode_ii(bp + 4, ii_len, itype, ascii, raw, verbose);
+            } else
+                ret = res;
+        } else
+            ret = res;
+        if (ret) {
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("Report identifying information: %s\n", b);
+            if (0 == verbose)
+                pr2serr("    try '-v' for more information\n");
+        }
+    }
+
+err_out:
+    res = sg_cmds_close_device(sg_fd);
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_ident failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_inq.c b/src/sg_inq.c
new file mode 100644
index 0000000..bcf5960
--- /dev/null
+++ b/src/sg_inq.c
@@ -0,0 +1,4881 @@
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ * Copyright (C) 2000-2022 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program outputs information provided by a SCSI INQUIRY command.
+ * It is mainly based on the SCSI SPC-6 document at https://www.t10.org .
+ *
+ * Acknowledgment:
+ *    - Martin Schwenke <martin at meltin dot net> added the raw switch and
+ *      other improvements [20020814]
+ *    - Lars Marowsky-Bree <lmb at suse dot de> contributed Unit Path Report
+ *      VPD page decoding for EMC CLARiiON devices [20041016]
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <errno.h>
+
+#ifdef SG_LIB_LINUX
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <linux/hdreg.h>
+#endif
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_cmds_basic.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+#if (HAVE_NVME && (! IGNORE_NVME))
+#include "sg_pt_nvme.h"
+#endif
+
+#include "sg_vpd_common.h"  /* for shared VPD page processing with sg_vpd */
+
+static const char * version_str = "2.31 20220915";  /* spc6r06, sbc5r03 */
+
+#define MY_NAME "sg_inq"
+
+/* INQUIRY notes:
+ * It is recommended that the initial allocation length given to a
+ * standard INQUIRY is 36 (bytes), especially if this is the first
+ * SCSI command sent to a logical unit. This is compliant with SCSI-2
+ * and another major operating system. There are devices out there
+ * that use one of the SCSI commands sets and lock up if they receive
+ * an allocation length other than 36. This technique is sometimes
+ * referred to as a "36 byte INQUIRY".
+ *
+ * A "standard" INQUIRY is one that has the EVPD and the CmdDt bits
+ * clear.
+ *
+ * When doing device discovery on a SCSI transport (e.g. bus scanning)
+ * the first SCSI command sent to a device should be a standard (36
+ * byte) INQUIRY.
+ *
+ * The allocation length field in the INQUIRY command was changed
+ * from 1 to 2 bytes in SPC-3, revision 9, 17 September 2002.
+ * Be careful using allocation lengths greater than 252 bytes, especially
+ * if the lower byte is 0x0 (e.g. a 512 byte allocation length may
+ * not be a good arbitrary choice (as 512 == 0x200) ).
+ *
+ * From SPC-3 revision 16 the CmdDt bit in an INQUIRY is obsolete. There
+ * is now a REPORT SUPPORTED OPERATION CODES command that yields similar
+ * information [MAINTENANCE IN, service action = 0xc]; see sg_opcodes.
+ */
+
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TESTING
+// #undef SG_SCSI_STRINGS
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TESTING
+
+#define VPD_NOPE_WANT_STD_INQ -2        /* request for standard inquiry */
+
+/* Vendor specific VPD pages (typically >= 0xc0) */
+#define VPD_UPR_EMC 0xc0
+#define VPD_RDAC_VERS 0xc2
+#define VPD_RDAC_VAC 0xc9
+
+/* values for selection one or more associations (2**vpd_assoc),
+   except _AS_IS */
+#define VPD_DI_SEL_LU 1
+#define VPD_DI_SEL_TPORT 2
+#define VPD_DI_SEL_TARGET 4
+#define VPD_DI_SEL_AS_IS 32
+
+#define DEF_ALLOC_LEN 252       /* highest 1 byte value that is modulo 4 */
+#define SAFE_STD_INQ_RESP_LEN 36
+#define MX_ALLOC_LEN (0xc000 + 0x80)
+#define VPD_ATA_INFO_LEN  572
+
+#define SENSE_BUFF_LEN  64       /* Arbitrary, could be larger */
+#define INQUIRY_CMD     0x12
+#define INQUIRY_CMDLEN  6
+#define DEF_PT_TIMEOUT  60       /* 60 seconds */
+
+
+uint8_t * rsp_buff;
+
+static uint8_t * free_rsp_buff;
+static const int rsp_buff_sz = MX_ALLOC_LEN + 1;
+
+static char xtra_buff[MX_ALLOC_LEN + 1];
+static char usn_buff[MX_ALLOC_LEN + 1];
+
+static const char * find_version_descriptor_str(int value);
+static void decode_dev_ids(const char * leadin, uint8_t * buff, int len,
+                           struct opts_t * op, sgj_opaque_p jop);
+static int vpd_decode(int sg_fd, struct opts_t * op, sgj_opaque_p jop,
+                      int off);
+
+// Test define that will only work for Linux
+// #define HDIO_GET_IDENTITY 1
+
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+#include <sys/ioctl.h>
+
+static int try_ata_identify(int ata_fd, int do_hex, int do_raw,
+                            int verbose);
+static void prepare_ata_identify(const struct opts_t * op, int inhex_len);
+#endif
+
+
+/* Note that this table is sorted by acronym */
+static struct svpd_values_name_t t10_vpd_pg[] = {
+    {VPD_AUTOMATION_DEV_SN, 0, 1, "adsn", "Automation device serial "
+     "number (SSC)"},
+    {VPD_ATA_INFO, 0, -1, "ai", "ATA information (SAT)"},
+    {VPD_BLOCK_DEV_CHARS, 0, 0, "bdc",
+     "Block device characteristics (SBC)"},
+    {VPD_BLOCK_DEV_C_EXTENS, 0, 0, "bdce", "Block device characteristics "
+     "extension (SBC)"},
+    {VPD_BLOCK_LIMITS, 0, 0, "bl", "Block limits (SBC)"},
+    {VPD_BLOCK_LIMITS_EXT, 0, 0, "ble", "Block limits extension (SBC)"},
+    {VPD_CFA_PROFILE_INFO, 0, 0, "cfa", "CFA profile information"},
+    {VPD_CON_POS_RANGE, 0, 0, "cpr", "Concurrent positioning ranges "
+     "(SBC)"},
+    {VPD_DEVICE_CONSTITUENTS, 0, -1, "dc", "Device constituents"},
+    {VPD_DEVICE_ID, 0, -1, "di", "Device identification"},
+#if 0           /* following found in sg_vpd */
+    {VPD_DEVICE_ID, VPD_DI_SEL_AS_IS, -1, "di_asis", "Like 'di' "
+     "but designators ordered as found"},
+    {VPD_DEVICE_ID, VPD_DI_SEL_LU, -1, "di_lu", "Device identification, "
+     "lu only"},
+    {VPD_DEVICE_ID, VPD_DI_SEL_TPORT, -1, "di_port", "Device "
+     "identification, target port only"},
+    {VPD_DEVICE_ID, VPD_DI_SEL_TARGET, -1, "di_target", "Device "
+     "identification, target device only"},
+#endif
+    {VPD_EXT_INQ, 0, -1, "ei", "Extended inquiry data"},
+    {VPD_FORMAT_PRESETS, 0, 0, "fp", "Format presets"},
+    {VPD_LB_PROTECTION, 0, 0, "lbpro", "Logical block protection (SSC)"},
+    {VPD_LB_PROVISIONING, 0, 0, "lbpv", "Logical block provisioning "
+     "(SBC)"},
+    {VPD_MAN_ASS_SN, 0, 1, "mas", "Manufacturer assigned serial number (SSC)"},
+    {VPD_MAN_ASS_SN, 0, 0x12, "masa",
+     "Manufacturer assigned serial number (ADC)"},
+    {VPD_MAN_NET_ADDR, 0, -1, "mna", "Management network addresses"},
+    {VPD_MODE_PG_POLICY, 0, -1, "mpp", "Mode page policy"},
+    {VPD_POWER_CONDITION, 0, -1, "po", "Power condition"},/* "pc" in sg_vpd */
+    {VPD_POWER_CONSUMPTION, 0, -1, "psm", "Power consumption"},
+    {VPD_PROTO_LU, 0, -1, "pslu", "Protocol-specific logical unit "
+     "information"},
+    {VPD_PROTO_PORT, 0, -1, "pspo", "Protocol-specific port information"},
+    {VPD_REFERRALS, 0, 0, "ref", "Referrals (SBC)"},
+    {VPD_SA_DEV_CAP, 0, 1, "sad",
+     "Sequential access device capabilities (SSC)"},
+    {VPD_SUP_BLOCK_LENS, 0, 0, "sbl", "Supported block lengths and "
+     "protection types (SBC)"},
+    {VPD_SCSI_FEATURE_SETS, 0, -1, "sfs", "SCSI Feature sets"},
+    {VPD_SOFTW_INF_ID, 0, -1, "sii", "Software interface identification"},
+    {VPD_NOPE_WANT_STD_INQ, 0, -1, "sinq", "Standard inquiry data format"},
+    {VPD_UNIT_SERIAL_NUM, 0, -1, "sn", "Unit serial number"},
+    {VPD_SCSI_PORTS, 0, -1, "sp", "SCSI ports"},
+    {VPD_SUPPORTED_VPDS, 0, -1, "sv", "Supported VPD pages"},
+    {VPD_TA_SUPPORTED, 0, 1, "tas", "TapeAlert supported flags (SSC)"},
+    {VPD_3PARTY_COPY, 0, -1, "tpc", "Third party copy"},
+    {VPD_ZBC_DEV_CHARS, 0, 0, "zbdch", "Zoned block device "
+     "characteristics"},
+    {0, 0, 0, NULL, NULL},
+};
+
+/* Some alternate acronyms for T10 VPD pages (compatibility with sg_vpd) */
+static struct svpd_values_name_t alt_t10_vpd_pg[] = {
+    {VPD_NOPE_WANT_STD_INQ, 0, -1, "stdinq", "Standard inquiry data format"},
+    {VPD_POWER_CONDITION, 0, -1, "pc", "Power condition"},
+    {0, 0, 0, NULL, NULL},
+};
+
+static struct svpd_values_name_t vs_vpd_pg[] = {
+    /* Following are vendor specific */
+    {SG_NVME_VPD_NICR, 0, -1, "nicr",
+     "NVMe Identify Controller Response (sg3_utils)"},
+    {VPD_RDAC_VAC, 0, -1, "rdac_vac", "RDAC volume access control (RDAC)"},
+    {VPD_RDAC_VERS, 0, -1, "rdac_vers", "RDAC software version (RDAC)"},
+    {VPD_UPR_EMC, 0, -1, "upr", "Unit path report (EMC)"},
+    {0, 0, 0, NULL, NULL},
+};
+
+static struct option long_options[] = {
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+        {"ata", no_argument, 0, 'a'},
+#endif
+        {"block", required_argument, 0, 'B'},
+        {"cmddt", no_argument, 0, 'c'},
+        {"descriptors", no_argument, 0, 'd'},
+        {"export", no_argument, 0, 'u'},
+        {"extended", no_argument, 0, 'x'},
+        {"force", no_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"id", no_argument, 0, 'i'},
+        {"inhex", required_argument, 0, 'I'},
+        {"len", required_argument, 0, 'l'},
+        {"long", no_argument, 0, 'L'},
+        {"maxlen", required_argument, 0, 'm'},
+#ifdef SG_SCSI_STRINGS
+        {"new", no_argument, 0, 'N'},
+        {"old", no_argument, 0, 'O'},
+#endif
+        {"only", no_argument, 0, 'o'},
+        {"page", required_argument, 0, 'p'},
+        {"raw", no_argument, 0, 'r'},
+        {"sinq_inraw", required_argument, 0, 'Q'},
+        {"sinq-inraw", required_argument, 0, 'Q'},
+        {"vendor", no_argument, 0, 's'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"vpd", no_argument, 0, 'e'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+
+    pr2serr("Usage: sg_inq [--ata] [--block=0|1] [--cmddt] [--descriptors] "
+            "[--export]\n"
+            "              [--extended] [--help] [--hex] [--id] "
+            "[--inhex=FN]\n"
+            "              [--json[=JO]] [--len=LEN] [--long] "
+            "[--maxlen=LEN]\n"
+            "              [--only] [--page=PG] [--raw] [--sinq_inraw=RFN] "
+            "[--vendor]\n"
+            "              [--verbose] [--version] [--vpd] DEVICE\n"
+            "  where:\n"
+            "    --ata|-a        treat DEVICE as (directly attached) ATA "
+            "device\n");
+#else
+    pr2serr("Usage: sg_inq [--block=0|1] [--cmddt] [--descriptors] "
+            "[--export]\n"
+            "              [--extended] [--help] [--hex] [--id] "
+            "[--inhex=FN]\n"
+            "              [--json[=JO]] [--len=LEN] [--long] "
+            "[--maxlen=LEN]\n"
+            "              [--only] [--page=PG] [--raw] [--sinq_inraw=RFN] "
+            "[--verbose]\n"
+            "              [--version] [--vpd] DEVICE\n"
+            "  where:\n");
+#endif
+    pr2serr("    --block=0|1     0-> open(non-blocking); 1-> "
+            "open(blocking)\n"
+            "      -B 0|1        (def: depends on OS; Linux pt: 0)\n"
+            "    --cmddt|-c      command support data mode (set opcode "
+            "with '--page=PG')\n"
+            "                    use twice for list of supported "
+            "commands; obsolete\n"
+            "    --descriptors|-d    fetch and decode version descriptors\n"
+            "    --export|-u     SCSI_IDENT_<assoc>_<type>=<ident> output "
+            "format.\n"
+            "                    Defaults to device id page (0x83) if --page "
+            "not given,\n"
+            "                    only supported for VPD pages 0x80 and 0x83\n"
+            "    --extended|-E|-x    decode extended INQUIRY data VPD page "
+            "(0x86)\n"
+            "    --force|-f      skip VPD page 0 check; directly fetch "
+            "requested page\n"
+            "    --help|-h       print usage message then exit\n"
+            "    --hex|-H        output response in hex\n"
+            "    --id|-i         decode device identification VPD page "
+            "(0x83)\n"
+            "    --inhex=FN|-I FN    read ASCII hex from file FN instead of "
+            "DEVICE;\n"
+            "                        if used with --raw then read binary "
+            "from FN\n"
+            "    --json[=JO]|-j[JO]    output in JSON instead of human "
+            "readable text.\n"
+            "                          Use --json=? for JSON help\n"
+            "    --len=LEN|-l LEN    requested response length (def: 0 "
+            "-> fetch 36\n"
+            "                        bytes first, then fetch again as "
+            "indicated)\n"
+            "    --long|-L       supply extra information on NVMe devices\n"
+            "    --maxlen=LEN|-m LEN    same as '--len='\n"
+            "    --old|-O        use old interface (use as first option)\n"
+            "    --only|-o       for std inquiry do not fetch serial number "
+            "vpd page;\n"
+            "                    for NVMe device only do Identify "
+            "controller\n"
+            "    --page=PG|-p PG     Vital Product Data (VPD) page number "
+            "or\n"
+            "                        abbreviation (opcode number if "
+            "'--cmddt' given)\n"
+            "    --raw|-r        output response in binary (to stdout)\n"
+            "    --sinq_inraw=RFN|-Q RFN    read raw (binary) standard "
+            "INQUIRY\n"
+            "                               response from the RFN filename\n"
+            "    --vendor|-s     show vendor specific fields in std "
+            "inquiry\n"
+            "    --verbose|-v    increase verbosity\n"
+            "    --version|-V    print version string then exit\n"
+            "    --vpd|-e        vital product data (set page with "
+            "'--page=PG')\n\n"
+            "Sends a SCSI INQUIRY command to the DEVICE and decodes the "
+            "response.\nAlternatively it decodes the INQUIRY response held "
+            "in file FN. If no\noptions given then it sends a 'standard' "
+            "INQUIRY command to DEVICE. Can\nlist VPD pages with '--vpd' or "
+            "'--page=PG' option.\n");
+}
+
+#ifdef SG_SCSI_STRINGS
+static void
+usage_old()
+{
+#ifdef SG_LIB_LINUX
+    pr2serr("Usage:  sg_inq [-a] [-A] [-b] [-B=0|1] [-c] [-cl] [-d] [-e] "
+            "[-h]\n"
+            "               [-H] [-i] [-I=FN] [-j[=JO]] [-l=LEN] [-L] [-m] "
+            "[-M]\n"
+            "               [-o] [-p=VPD_PG] [-P] [-r] [-s] [-u] [-U] [-v] "
+            "[-V]\n"
+            "               [-x] [-36] [-?] DEVICE\n"
+            "  where:\n"
+            "    -a    decode ATA information VPD page (0x89)\n"
+            "    -A    treat <device> as (directly attached) ATA device\n");
+#else
+    pr2serr("Usage:  sg_inq [-a] [-b] [-B 0|1] [-c] [-cl] [-d] [-e] [-h] "
+            "[-H]\n"
+            "               [-i] [-l=LEN] [-L] [-m] [-M] [-o] "
+            "[-p=VPD_PG]\n"
+            "               [-P] [-r] [-s] [-u] [-v] [-V] [-x] [-36] "
+            "[-?]\n"
+            "               DEVICE\n"
+            "  where:\n"
+            "    -a    decode ATA information VPD page (0x89)\n");
+
+#endif  /* SG_LIB_LINUX */
+    pr2serr("    -b    decode Block limits VPD page (0xb0) (SBC)\n"
+            "    -B=0|1    0-> open(non-blocking); 1->open(blocking)\n"
+            "    -c    set CmdDt mode (use -o for opcode) [obsolete]\n"
+            "    -cl   list supported commands using CmdDt mode [obsolete]\n"
+            "    -d    decode: version descriptors or VPD page\n"
+            "    -e    set VPD mode (use -p for page code)\n"
+            "    -h    output in hex (ASCII to the right)\n"
+            "    -H    output in hex (ASCII to the right) [same as '-h']\n"
+            "    -i    decode device identification VPD page (0x83)\n"
+            "    -I=FN    use ASCII hex in file FN instead of DEVICE\n"
+            "    -j[=JO]    output in JSON instead of human readable "
+            "text.\n"
+            "    -l=LEN    requested response length (def: 0 "
+            "-> fetch 36\n"
+            "                    bytes first, then fetch again as "
+            "indicated)\n"
+            "    -L    supply extra information on NVMe devices\n"
+            "    -m    decode management network addresses VPD page "
+            "(0x85)\n"
+            "    -M    decode mode page policy VPD page (0x87)\n"
+            "    -N|--new   use new interface\n"
+            "    -o    for std inquiry only do that, not serial number vpd "
+            "page\n"
+            "    -p=VPD_PG    vpd page code in hex (def: 0)\n"
+            "    -P    decode Unit Path Report VPD page (0xc0) (EMC)\n"
+            "    -r    output response in binary ('-rr': output for hdparm)\n"
+            "    -s    decode SCSI Ports VPD page (0x88)\n"
+            "    -u    SCSI_IDENT_<assoc>_<type>=<ident> output format\n"
+            "    -v    verbose (output cdb and, if non-zero, resid)\n"
+            "    -V    output version string\n"
+            "    -x    decode extended INQUIRY data VPD page (0x86)\n"
+            "    -36   perform standard INQUIRY with a 36 byte response\n"
+            "    -?    output this usage message\n\n"
+            "If no options given then sends a standard SCSI INQUIRY "
+            "command and\ndecodes the response.\n");
+}
+
+static void
+usage_for(const struct opts_t * op)
+{
+    if (op->opt_new)
+        usage();
+    else
+        usage_old();
+}
+
+#else  /* SG_SCSI_STRINGS */
+
+static void
+usage_for(const struct opts_t * op)
+{
+    if (op) { }         /* suppress warning */
+    usage();
+}
+
+#endif /* SG_SCSI_STRINGS */
+
+/* Processes command line options according to new option format. Returns
+ * 0 is ok, else SG_LIB_SYNTAX_ERROR is returned. */
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int c, n;
+
+    while (1) {
+        int option_index = 0;
+
+#ifdef SG_LIB_LINUX
+#ifdef SG_SCSI_STRINGS
+        c = getopt_long(argc, argv, "aB:cdeEfhHiI:j::l:Lm:M:NoOp:Q:rsuvVx",
+                        long_options, &option_index);
+#else
+        c = getopt_long(argc, argv, "B:cdeEfhHiI:j::l:Lm:M:op:Q:rsuvVx",
+                        long_options, &option_index);
+#endif /* SG_SCSI_STRINGS */
+#else  /* SG_LIB_LINUX */
+#ifdef SG_SCSI_STRINGS
+        c = getopt_long(argc, argv, "B:cdeEfhHiI:j::l:Lm:M:NoOp:Q:rsuvVx",
+                        long_options, &option_index);
+#else
+        c = getopt_long(argc, argv, "B:cdeEfhHiI:j::l:Lm:M:op:Q:rsuvVx",
+                        long_options, &option_index);
+#endif /* SG_SCSI_STRINGS */
+#endif /* SG_LIB_LINUX */
+        if (c == -1)
+            break;
+
+        switch (c) {
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+        case 'a':
+            op->do_ata = true;
+            break;
+#endif
+        case 'B':
+            if ('-' == optarg[0])
+                n = -1;
+            else {
+                n = sg_get_num(optarg);
+                if ((n < 0) || (n > 1)) {
+                    pr2serr("bad argument to '--block=' want 0 or 1\n");
+                    usage_for(op);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            op->do_block = n;
+            break;
+        case 'c':
+            ++op->do_cmddt;
+            break;
+        case 'd':
+            op->do_descriptors = true;
+            break;
+        case 'e':
+            op->do_vpd = true;
+            break;
+        case 'E':       /* --extended */
+        case 'x':
+            op->do_decode = true;
+            op->do_vpd = true;
+            op->vpd_pn = VPD_EXT_INQ;
+            op->page_given = true;
+            break;
+        case 'f':
+            op->do_force = true;
+            break;
+        case 'h':
+            ++op->do_help;
+            break;
+        case 'j':
+            if (! sgj_init_state(&op->json_st, optarg)) {
+                int bad_char = op->json_st.first_bad_char;
+                char e[1500];
+
+                if (bad_char) {
+                    pr2serr("bad argument to --json= option, unrecognized "
+                            "character '%c'\n\n", bad_char);
+                }
+                sg_json_usage(0, e, sizeof(e));
+                pr2serr("%s", e);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'o':
+            op->do_only = true;
+            break;
+        case '?':
+            if (! op->do_help)
+                ++op->do_help;
+            break;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'i':
+            op->do_decode = true;
+            op->do_vpd = true;
+            op->vpd_pn = VPD_DEVICE_ID;
+            op->page_given = true;
+            break;
+        case 'I':
+            op->inhex_fn = optarg;
+            break;
+        case 'l':
+        case 'm':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 65532)) {
+                pr2serr("bad argument to '--len='\n");
+                usage_for(op);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((n > 0) && (n < 4)) {
+                pr2serr("Changing that '--maxlen=' value to 4\n");
+                n = 4;
+            }
+            op->maxlen = n;
+            break;
+        case 'M':
+            if (op->vend_prod) {
+                pr2serr("only one '--vendor=' option permitted\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            } else
+                op->vend_prod = optarg;
+            break;
+        case 'L':
+            ++op->do_long;
+            break;
+#ifdef SG_SCSI_STRINGS
+        case 'N':
+            break;      /* ignore */
+        case 'O':
+            op->opt_new = false;
+            return 0;
+#endif
+        case 'p':
+            op->page_str = optarg;
+            op->page_given = true;
+            break;
+        case 'Q':
+            op->sinq_inraw_fn = optarg;
+            break;
+        case 'r':
+            ++op->do_raw;
+            break;
+        case 's':
+            ++op->do_vendor;
+            break;
+        case 'u':
+            op->do_export = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage_for(op);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage_for(op);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+#ifdef SG_SCSI_STRINGS
+/* Processes command line options according to old option format. Returns
+ * 0 is ok, else SG_LIB_SYNTAX_ERROR is returned. */
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool jmp_out;
+    int k, plen, num, n;
+    const char * cp;
+
+    for (k = 1; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case '3':
+                    if ('6' == *(cp + 1)) {
+                        op->maxlen = 36;
+                        --plen;
+                        ++cp;
+                    } else
+                        jmp_out = true;
+                    break;
+                case 'a':
+                    op->vpd_pn = VPD_ATA_INFO;
+                    op->do_vpd = true;
+                    op->page_given = true;
+                    ++op->num_pages;
+                    break;
+#ifdef SG_LIB_LINUX
+                case 'A':
+                    op->do_ata = true;
+                    break;
+#endif
+                case 'b':
+                    op->vpd_pn = VPD_BLOCK_LIMITS;
+                    op->do_vpd = true;
+                    op->page_given = true;
+                    ++op->num_pages;
+                    break;
+                case 'c':
+                    ++op->do_cmddt;
+                    if ('l' == *(cp + 1)) {
+                        ++op->do_cmddt;
+                        --plen;
+                        ++cp;
+                    }
+                    break;
+                case 'd':
+                    op->do_descriptors = true;
+                    op->do_decode = true;
+                    break;
+                case 'e':
+                    op->do_vpd = true;
+                    break;
+                case 'f':
+                    op->do_force = true;
+                    break;
+                case 'h':
+                case 'H':
+                    ++op->do_hex;
+                    break;
+                case 'i':
+                    op->vpd_pn = VPD_DEVICE_ID;
+                    op->do_vpd = true;
+                    op->page_given = true;
+                    ++op->num_pages;
+                    break;
+                case 'L':
+                    ++op->do_long;
+                    break;
+                case 'm':
+                    op->vpd_pn = VPD_MAN_NET_ADDR;
+                    op->do_vpd = true;
+                    ++op->num_pages;
+                    op->page_given = true;
+                    break;
+                case 'M':
+                    op->vpd_pn = VPD_MODE_PG_POLICY;
+                    op->do_vpd = true;
+                    op->page_given = true;
+                    ++op->num_pages;
+                    break;
+                case 'N':
+                    op->opt_new = true;
+                    return 0;
+                case 'o':
+                    op->do_only = true;
+                    break;
+                case 'O':
+                    break;
+                case 'P':
+                    op->vpd_pn = VPD_UPR_EMC;
+                    op->do_vpd = true;
+                    op->page_given = true;
+                    ++op->num_pages;
+                    break;
+                case 'r':
+                    ++op->do_raw;
+                    break;
+                case 's':
+                    op->vpd_pn = VPD_SCSI_PORTS;
+                    op->do_vpd = true;
+                    op->page_given = true;
+                    ++op->num_pages;
+                    break;
+                case 'u':
+                    op->do_export = true;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                case 'x':
+                    op->vpd_pn = VPD_EXT_INQ;
+                    op->do_vpd = true;
+                    op->page_given = true;
+                    ++op->num_pages;
+                    break;
+                case '?':
+                    if (! op->do_help)
+                        ++op->do_help;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            else if (0 == strncmp("B=", cp, 2)) {
+                num = sscanf(cp + 2, "%d", &n);
+                if ((1 != num) || (n < 0) || (n > 1)) {
+                    pr2serr("'B=' option expects 0 or 1\n");
+                    usage_for(op);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->do_block = n;
+            } else if (0 == strncmp("I=", cp, 2))
+                op->inhex_fn = cp + 2;
+            else if ('j' == *cp) { /* handle either '-j' or '-j=<JO>' */
+                const char * c2p = (('=' == *(cp + 1)) ? cp + 2 : NULL);
+
+                if (! sgj_init_state(&op->json_st, c2p)) {
+                    int bad_char = op->json_st.first_bad_char;
+                    char e[1500];
+
+                    if (bad_char) {
+                        pr2serr("bad argument to --json= option, unrecognized "
+                                "character '%c'\n\n", bad_char);
+                    }
+                    sg_json_usage(0, e, sizeof(e));
+                    pr2serr("%s", e);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else if (0 == strncmp("l=", cp, 2)) {
+                num = sscanf(cp + 2, "%d", &n);
+                if ((1 != num) || (n < 1)) {
+                    pr2serr("Inappropriate value after 'l=' option\n");
+                    usage_for(op);
+                    return SG_LIB_SYNTAX_ERROR;
+                } else if (n > MX_ALLOC_LEN) {
+                    pr2serr("value after 'l=' option too large\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                if ((n > 0) && (n < 4)) {
+                    pr2serr("Changing that '-l=' value to 4\n");
+                    n = 4;
+                }
+                op->maxlen = n;
+            } else if (0 == strncmp("p=", cp, 2)) {
+                op->page_str = cp + 2;
+                op->page_given = true;
+            } else if (0 == strncmp("-old", cp, 4))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage_for(op);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == op->device_name)
+            op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not expecting: %s\n",
+                    op->device_name, cp);
+            usage_for(op);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+/* Process command line options. First check using new option format unless
+ * the SG3_UTILS_OLD_OPTS environment variable is defined which causes the
+ * old option format to be checked first. Both new and old format can be
+ * countermanded by a '-O' and '-N' options respectively. As soon as either
+ * of these options is detected (when processing the other format), processing
+ * stops and is restarted using the other format. Clear? */
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opt_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opt_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opt_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (! op->opt_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+#else  /* SG_SCSI_STRINGS */
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    return new_parse_cmd_line(op, argc, argv);
+}
+
+#endif  /* SG_SCSI_STRINGS */
+
+
+static const struct svpd_values_name_t *
+sdp_find_vpd_by_acron(const char * ap)
+{
+    const struct svpd_values_name_t * vnp;
+
+    for (vnp = t10_vpd_pg; vnp->acron; ++vnp) {
+        if (0 == strcmp(vnp->acron, ap))
+            return vnp;
+    }
+    for (vnp = alt_t10_vpd_pg; vnp->acron; ++vnp) {
+        if (0 == strcmp(vnp->acron, ap))
+            return vnp;
+    }
+    for (vnp = vs_vpd_pg; vnp->acron; ++vnp) {
+        if (0 == strcmp(vnp->acron, ap))
+            return vnp;
+    }
+    return NULL;
+}
+
+static void
+enumerate_vpds()
+{
+    const struct svpd_values_name_t * vnp;
+
+    printf("T10 defined VPD pages:\n");
+    for (vnp = t10_vpd_pg; vnp->acron; ++vnp) {
+        if (vnp->name) {
+            if (vnp->value < 0)
+                printf("  %-10s   -1      %s\n", vnp->acron, vnp->name);
+            else
+                printf("  %-10s 0x%02x      %s\n", vnp->acron, vnp->value,
+                       vnp->name);
+        }
+    }
+    printf("Vendor specific VPD pages:\n");
+    for (vnp = vs_vpd_pg; vnp->acron; ++vnp) {
+        if (vnp->name) {
+            if (vnp->value < 0)
+                printf("  %-10s   -1      %s\n", vnp->acron, vnp->name);
+            else
+                printf("  %-10s 0x%02x      %s\n", vnp->acron, vnp->value,
+                       vnp->name);
+        }
+    }
+}
+
+static void
+dStrRaw(const char * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* Strip initial and trailing whitespaces; convert one or repeated
+ * whitespaces to a single "_"; convert non-printable characters to "."
+ * and if there are no valid (i.e. printable) characters return 0.
+ * Process 'str' in place (i.e. it's input and output) and return the
+ * length of the output, excluding the trailing '\0'. To cover any
+ * potential unicode string an intermediate zero is skipped; two
+ * consecutive zeroes indicate a string termination.
+ */
+static int
+encode_whitespaces(uint8_t *str, int inlen)
+{
+    int k, res;
+    int j;
+    bool valid = false;
+    int outlen = inlen, zeroes = 0;
+
+    /* Skip initial whitespaces */
+    for (j = 0; (j < inlen) && isblank(str[j]); ++j)
+        ;
+    if (j < inlen) {
+        /* Skip possible unicode prefix characters */
+        for ( ; (j < inlen) && (str[j] < 0x20); ++j)
+            ;
+    }
+    k = j;
+    /* Strip trailing whitespaces */
+    while ((outlen > k) &&
+           (isblank(str[outlen - 1]) || ('\0' == str[outlen - 1]))) {
+        str[outlen - 1] = '\0';
+        outlen--;
+    }
+    for (res = 0; k < outlen; ++k) {
+        if (isblank(str[k])) {
+            if ((res > 0) && ('_' != str[res - 1])) {
+                str[res++] = '_';
+                valid = true;
+            }
+            zeroes = 0;
+        } else if (! isprint(str[k])) {
+            if (str[k] == 0x00) {
+                /* Stop on more than one consecutive zero */
+                if (zeroes)
+                    break;
+                zeroes++;
+                continue;
+            }
+            str[res++] = '.';
+            zeroes = 0;
+        } else {
+            str[res++] = str[k];
+            valid = true;
+            zeroes = 0;
+        }
+    }
+    if (! valid)
+        res = 0;
+    if (res < inlen)
+        str[res] = '\0';
+    return res;
+}
+
+static int
+encode_unicode(uint8_t *str, int inlen)
+{
+    int k = 0, res;
+    int zeroes = 0;
+
+    for (res = 0; k < inlen; ++k) {
+        if (str[k] == 0x00) {
+            if (zeroes) {
+                str[res++] = '\0';
+                break;
+            }
+            zeroes++;
+        } else {
+            zeroes = 0;
+            if (isprint(str[k]))
+                str[res++] = str[k];
+            else
+                str[res++] = ' ';
+        }
+    }
+
+    return res;
+}
+
+static int
+encode_string(char *out, const uint8_t *in, int inlen)
+{
+    int i, j = 0;
+
+    for (i = 0; (i < inlen); ++i) {
+        if (isblank(in[i]) || !isprint(in[i])) {
+            sprintf(&out[j], "\\x%02x", in[i]);
+            j += 4;
+        } else {
+            out[j] = in[i];
+            j++;
+        }
+    }
+    out[j] = '\0';
+    return j;
+}
+
+static const struct svpd_values_name_t *
+get_vpd_page_info(int vpd_page_num, int dev_pdt)
+{
+    int decay_pdt;
+    const struct svpd_values_name_t * vnp;
+    const struct svpd_values_name_t * prev_vnp;
+
+    if (vpd_page_num < 0xb0) {  /* take T10 first match */
+        for (vnp = t10_vpd_pg; vnp->acron; ++vnp) {
+            if (vnp->value == vpd_page_num)
+                return vnp;
+        }
+        return NULL;
+    } else if (vpd_page_num < 0xc0) {
+        for (vnp = t10_vpd_pg; vnp->acron; ++vnp) {
+            if (vnp->value == vpd_page_num)
+                break;
+        }
+        if (NULL == vnp->acron)
+            return NULL;
+        if (vnp->pdt == dev_pdt)        /* exact match */
+            return vnp;
+        prev_vnp = vnp;
+
+        for (++vnp; vnp->acron; ++vnp) {
+            if (vnp->value == vpd_page_num)
+                break;
+        }
+        decay_pdt = sg_lib_pdt_decay(dev_pdt);
+        if (NULL == vnp->acron) {
+            if (decay_pdt == prev_vnp->pdt)
+                return prev_vnp;
+            return NULL;
+        }
+        if ((vnp->pdt == dev_pdt) || (vnp->pdt == decay_pdt))
+            return vnp;
+        if (decay_pdt == prev_vnp->pdt)
+            return prev_vnp;
+
+        for (++vnp; vnp->acron; ++vnp) {
+            if (vnp->value == vpd_page_num)
+                break;
+        }
+        if (NULL == vnp->acron)
+            return NULL;
+        if ((vnp->pdt == dev_pdt) || (vnp->pdt == decay_pdt))
+            return vnp;
+        return NULL;            /* give up */
+    } else {    /* vendor specific: vpd >= 0xc0 */
+        for (vnp = vs_vpd_pg; vnp->acron; ++vnp) {
+            if (vnp->pdt == dev_pdt)
+                return vnp;
+        }
+        return NULL;
+    }
+}
+
+static int
+svpd_inhex_decode_all(struct opts_t * op, sgj_opaque_p jop)
+{
+    int k, res, pn;
+    int max_pn = 255;
+    int bump, off;
+    int in_len = op->maxlen;
+    int prev_pn = -1;
+    sgj_state * jsp = &op->json_st;
+    uint8_t vpd0_buff[512];
+    uint8_t * rp = vpd0_buff;
+
+    if (op->vpd_pn > 0)
+        max_pn = op->vpd_pn;
+
+    res = 0;
+    if (op->page_given && (VPD_NOPE_WANT_STD_INQ == op->vpd_pn))
+        return vpd_decode(-1, op, jop, 0);
+
+    for (k = 0, off = 0; off < in_len; ++k, off += bump) {
+        rp = rsp_buff + off;
+        pn = rp[1];
+        bump = sg_get_unaligned_be16(rp + 2) + 4;
+        if ((off + bump) > in_len) {
+            pr2serr("%s: page 0x%x size (%d) exceeds buffer\n", __func__,
+                    pn, bump);
+            bump = in_len - off;
+        }
+        if (op->page_given && (pn != op->vpd_pn))
+            continue;
+        if (pn <= prev_pn) {
+            pr2serr("%s: prev_pn=0x%x, this pn=0x%x, not ascending so "
+                    "exit\n", __func__, prev_pn, pn);
+            break;
+        }
+        prev_pn = pn;
+        op->vpd_pn = pn;
+        if (pn > max_pn) {
+            if (op->verbose > 2)
+                pr2serr("%s: skipping as this pn=0x%x exceeds "
+                        "max_pn=0x%x\n", __func__, pn, max_pn);
+            continue;
+        }
+        if (op->do_long) {
+            if (jsp->pr_as_json)
+               sgj_pr_hr(jsp, "[0x%x]:\n", pn);
+            else
+               sgj_pr_hr(jsp, "[0x%x] ", pn);
+        }
+
+        res = vpd_decode(-1, op, jop, off);
+        if (SG_LIB_CAT_OTHER == res) {
+            if (op->verbose)
+                pr2serr("Can't decode VPD page=0x%x\n", pn);
+        }
+    }
+    return res;
+}
+
+static void
+decode_supported_vpd_4inq(uint8_t * buff, int len, struct opts_t * op,
+                          sgj_opaque_p jap)
+{
+    int vpd, k, rlen, pdt;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    const struct svpd_values_name_t * vnp;
+    char b[64];
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    if (len < 4) {
+        pr2serr("Supported VPD pages VPD page length too short=%d\n", len);
+        return;
+    }
+    pdt = PDT_MASK & buff[0];
+    rlen = buff[3] + 4;
+    if (rlen > len)
+        pr2serr("Supported VPD pages VPD page truncated, indicates %d, got "
+                "%d\n", rlen, len);
+    else
+        len = rlen;
+    sgj_pr_hr(jsp, "   Supported VPD pages:\n");
+    for (k = 0; k < len - 4; ++k) {
+        vpd = buff[4 + k];
+        snprintf(b, sizeof(b), "0x%x", vpd);
+        vnp = get_vpd_page_info(vpd, pdt);
+        if (jsp->pr_as_json && jap) {
+            jo2p = sgj_new_unattached_object_r(jsp);
+            sgj_js_nv_i(jsp, jo2p, "i", vpd);
+            sgj_js_nv_s(jsp, jo2p, "hex", b + 2);
+            sgj_js_nv_s(jsp, jo2p, "name", vnp ? vnp->name : "unknown");
+            sgj_js_nv_s(jsp, jo2p, "acronym", vnp ? vnp->acron : "unknown");
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+        }
+        if (vnp)
+            sgj_pr_hr(jsp, "     %s\t%s\n", b, vnp->name);
+        else
+            sgj_pr_hr(jsp, "     %s\n", b);
+    }
+}
+
+static bool
+vpd_page_is_supported(uint8_t * vpd_pg0, int v0_len, int pg_num, int vb)
+{
+    int k, rlen;
+
+    if (v0_len < 4)
+        return false;
+
+    rlen = vpd_pg0[3] + 4;
+    if (rlen > v0_len)
+        pr2serr("Supported VPD pages VPD page truncated, indicates %d, got "
+                "%d\n", rlen, v0_len);
+    else
+        v0_len = rlen;
+    if (vb > 1) {
+        pr2serr("Supported VPD pages, hex list: ");
+        hex2stderr(vpd_pg0 + 4, v0_len - 4, -1);
+    }
+    for (k = 4; k < v0_len; ++k) {
+        if(vpd_pg0[k] == pg_num)
+            return true;
+    }
+    return false;
+}
+
+/* ASCII Information VPD pages (page numbers: 0x1 to 0x7f) */
+static void
+decode_ascii_inf(uint8_t * buff, int len, struct opts_t * op)
+{
+    int al, k, bump;
+    uint8_t * bp;
+    uint8_t * p;
+    sgj_state * jsp = &op->json_st;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    if (len < 4) {
+        pr2serr("ASCII information VPD page length too short=%d\n", len);
+        return;
+    }
+    if (4 == len)
+        return;
+    al = buff[4];
+    if ((al + 5) > len)
+        al = len - 5;
+    for (k = 0, bp = buff + 5; k < al; k += bump, bp += bump) {
+        p = (uint8_t *)memchr(bp, 0, al - k);
+        if (! p) {
+            sgj_pr_hr(jsp, "  %.*s\n", al - k, (const char *)bp);
+            break;
+        }
+        sgj_pr_hr(jsp, "  %s\n", (const char *)bp);
+        bump = (p - bp) + 1;
+    }
+    bp = buff + 5 + al;
+    if (bp < (buff + len)) {
+        sgj_pr_hr(jsp, "Vendor specific information in hex:\n");
+        hex2stdout(bp, len - (al + 5), 0);
+    }
+}
+
+static void
+decode_id_vpd(uint8_t * buff, int len, struct opts_t * op, sgj_opaque_p jap)
+{
+    if (len < 4) {
+        pr2serr("Device identification VPD page length too "
+                "short=%d\n", len);
+        return;
+    }
+    decode_dev_ids("Device identification", buff + 4, len - 4, op, jap);
+}
+
+/* VPD_SCSI_PORTS   0x88  ["sp"] */
+static void
+decode_scsi_ports_vpd_4inq(uint8_t * buff, int len, struct opts_t * op,
+                           sgj_opaque_p jap)
+{
+    int k, bump, rel_port, ip_tid_len, tpd_len;
+    uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+
+    if (len < 4) {
+        pr2serr("SCSI Ports VPD page length too short=%d\n", len);
+        return;
+    }
+    if (op->do_hex > 2) {
+        hex2stdout(buff, len, -1);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        rel_port = sg_get_unaligned_be16(bp + 2);
+        sgj_pr_hr(jsp, "Relative port=%d\n", rel_port);
+        sgj_js_nv_i(jsp, jo2p, "relative_port", rel_port);
+        ip_tid_len = sg_get_unaligned_be16(bp + 6);
+        bump = 8 + ip_tid_len;
+        if ((k + bump) > len) {
+            pr2serr("SCSI Ports VPD page, short descriptor "
+                    "length=%d, left=%d\n", bump, (len - k));
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            return;
+        }
+        if (ip_tid_len > 0) {
+            if (op->do_hex) {
+                printf(" Initiator port transport id:\n");
+                hex2stdout((bp + 8), ip_tid_len, no_ascii_4hex(op));
+            } else {
+                char b[1024];
+
+                sg_decode_transportid_str("    ", bp + 8, ip_tid_len,
+                                          true, sizeof(b), b);
+                if (jsp->pr_as_json)
+                    sgj_js_nv_s(jsp, jo2p, "initiator_port_transport_id", b);
+                sgj_pr_hr(jsp, "%s",
+                          sg_decode_transportid_str("    ", bp + 8,
+                                            ip_tid_len, true, sizeof(b), b));
+            }
+        }
+        tpd_len = sg_get_unaligned_be16(bp + bump + 2);
+        if ((k + bump + tpd_len + 4) > len) {
+            pr2serr("SCSI Ports VPD page, short descriptor(tgt) "
+                    "length=%d, left=%d\n", bump, (len - k));
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            return;
+        }
+        if (tpd_len > 0) {
+            sgj_pr_hr(jsp, " Target port descriptor(s):\n");
+            if (op->do_hex)
+                hex2stdout(bp + bump + 4, tpd_len, no_ascii_4hex(op));
+            else {
+                sgj_opaque_p ja2p = sgj_named_subarray_r(jsp, jo2p,
+                                        "target_port_descriptor_list");
+
+                decode_dev_ids("SCSI Ports", bp + bump + 4, tpd_len,
+                               op, ja2p);
+            }
+        }
+        bump += tpd_len + 4;
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+/* These are target port, device server (i.e. target) and LU identifiers */
+static void
+decode_dev_ids(const char * leadin, uint8_t * buff, int len,
+               struct opts_t * op, sgj_opaque_p jap)
+{
+    int u, j, m, id_len, p_id, c_set, piv, assoc, desig_type, i_len;
+    int off, ci_off, c_id, d_id, naa, vsi, k, n;
+    uint64_t vsei, id_ext, ccc_id;
+    const uint8_t * bp;
+    const uint8_t * ip;
+    const char * cp;
+    sgj_state * jsp = &op->json_st;
+    char b[256];
+    char d[64];
+    static const int blen = sizeof(b);
+    static const int dlen = sizeof(d);
+
+    if (jsp->pr_as_json) {
+        int ret = filter_json_dev_ids(buff, len, -1, op, jap);
+
+        if (ret || (! jsp->pr_out_hr))
+            return;
+    }
+    if (buff[2] > 2) {  /* SPC-3,4,5 buff[2] is upper byte of length */
+        /*
+         * Reference the 3rd byte of the first Identification descriptor
+         * of a page 83 reply to determine whether the reply is compliant
+         * with SCSI-2 or SPC-2/3 specifications.  A zero value in the
+         * 3rd byte indicates an SPC-2/3 conforming reply ( the field is
+         * reserved ).  This byte will be non-zero for a SCSI-2
+         * conforming page 83 reply from these EMC Symmetrix models since
+         * the 7th byte of the reply corresponds to the 4th and 5th
+         * nibbles of the 6-byte OUI for EMC, that is, 0x006048.
+         */
+        i_len = len;
+        ip = bp = buff;
+        c_set = 1;
+        assoc = 0;
+        piv = 0;
+        p_id = 0xf;
+        desig_type = 3;
+        j = 1;
+        off = 16;
+        sgj_pr_hr(jsp, "  Pre-SPC descriptor, descriptor length: %d\n",
+                  i_len);
+        goto decode;
+    }
+
+    for (j = 1, off = -1;
+         (u = sg_vpd_dev_id_iter(buff, len, &off, -1, -1, -1)) == 0;
+         ++j) {
+        bp = buff + off;
+        i_len = bp[3];
+        id_len = i_len + 4;
+        sgj_pr_hr(jsp, "  Designation descriptor number %d, "
+                  "descriptor length: %d\n", j, id_len);
+        if ((off + id_len) > len) {
+            pr2serr("%s VPD page error: designator length longer "
+                    "than\n     remaining response length=%d\n", leadin,
+                    (len - off));
+            return;
+        }
+        ip = bp + 4;
+        p_id = ((bp[0] >> 4) & 0xf);   /* protocol identifier */
+        c_set = (bp[0] & 0xf);         /* code set */
+        piv = ((bp[1] & 0x80) ? 1 : 0); /* protocol identifier valid */
+        assoc = ((bp[1] >> 4) & 0x3);
+        desig_type = (bp[1] & 0xf);
+  decode:
+        if (piv && ((1 == assoc) || (2 == assoc)))
+            sgj_pr_hr(jsp, "    transport: %s\n",
+                      sg_get_trans_proto_str(p_id, dlen, d));
+        n = 0;
+        cp = sg_get_desig_type_str(desig_type);
+        n += sg_scnpr(b + n, blen - n, "    designator_type: %s,  ",
+                      cp ? cp : "-");
+        cp = sg_get_desig_code_set_str(c_set);
+        sgj_pr_hr(jsp, "%scode_set: %s\n", b, cp ? cp : "-");
+        cp = sg_get_desig_assoc_str(assoc);
+        sgj_pr_hr(jsp, "    associated with the %s\n", cp ? cp : "-");
+        if (op->do_hex) {
+            sgj_pr_hr(jsp, "    designator header(hex): %.2x %.2x %.2x %.2x\n",
+                   bp[0], bp[1], bp[2], bp[3]);
+            sgj_pr_hr(jsp, "    designator:\n");
+            hex2stdout(ip, i_len, 0);
+            continue;
+        }
+        switch (desig_type) {
+        case 0: /* vendor specific */
+            k = 0;
+            if ((2 == c_set) || (3 == c_set)) { /* ASCII or UTF-8 */
+                for (k = 0; (k < i_len) && isprint(ip[k]); ++k)
+                    ;
+                if (k >= i_len)
+                    k = 1;
+            }
+            if (k)
+                sgj_pr_hr(jsp, "      vendor specific: %.*s\n", i_len, ip);
+            else {
+                sgj_pr_hr(jsp, "      vendor specific:\n");
+                hex2stdout(ip, i_len, -1);
+            }
+            break;
+        case 1: /* T10 vendor identification */
+            sgj_pr_hr(jsp, "      vendor id: %.8s\n", ip);
+            if (i_len > 8) {
+                if ((2 == c_set) || (3 == c_set)) { /* ASCII or UTF-8 */
+                    sgj_pr_hr(jsp, "      vendor specific: %.*s\n", i_len - 8,
+                              ip + 8);
+                } else {
+                    n = 0;
+                    n += sg_scnpr(b + n, blen - n,
+                                  "      vendor specific: 0x");
+                    for (m = 8; m < i_len; ++m)
+                        n += sg_scnpr(b + n, blen - n, "%02x", ip[m]);
+                    sgj_pr_hr(jsp, "%s\n", b);
+                }
+            }
+            break;
+        case 2: /* EUI-64 based */
+            sgj_pr_hr(jsp, "      EUI-64 based %d byte identifier\n", i_len);
+            if (1 != c_set) {
+                pr2serr("      << expected binary code_set (1)>>\n");
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            ci_off = 0;
+            n = 0;
+            b[0] = '\0';
+            if (16 == i_len) {
+                ci_off = 8;
+                id_ext = sg_get_unaligned_be64(ip);
+                n += sg_scnpr(b + n, blen - n,
+                              "      Identifier extension: 0x%" PRIx64 "\n",
+                              id_ext);
+            } else if ((8 != i_len) && (12 != i_len)) {
+                pr2serr("      << can only decode 8, 12 and 16 "
+                        "byte ids>>\n");
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            ccc_id = sg_get_unaligned_be64(ip + ci_off);
+            sgj_pr_hr(jsp, "%s      IEEE identifier: 0x%" PRIx64 "\n", b,
+                      ccc_id);
+            if (12 == i_len) {
+                d_id = sg_get_unaligned_be32(ip + 8);
+                sgj_pr_hr(jsp, "      Directory ID: 0x%x\n", d_id);
+            }
+            n = 0;
+            n += sg_scnpr(b + n, blen - n, "      [0x");
+            for (m = 0; m < i_len; ++m)
+                n += sg_scnpr(b + n, blen - n, "%02x", ip[m]);
+            sgj_pr_hr(jsp, "%s]\n", b);
+            break;
+        case 3: /* NAA <n> */
+            naa = (ip[0] >> 4) & 0xff;
+            if (1 != c_set) {
+                pr2serr("      << expected binary code_set (1), got %d for "
+                        "NAA=%d>>\n", c_set, naa);
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            switch (naa) {
+            case 2:     /* NAA 2: IEEE Extended */
+                if (8 != i_len) {
+                    pr2serr("      << unexpected NAA 2 identifier "
+                            "length: 0x%x>>\n", i_len);
+                    hex2stderr(ip, i_len, -1);
+                    break;
+                }
+                d_id = (((ip[0] & 0xf) << 8) | ip[1]);
+                c_id = sg_get_unaligned_be24(ip + 2);
+                vsi = sg_get_unaligned_be24(ip + 5);
+                sgj_pr_hr(jsp, "      NAA 2, vendor specific identifier A: "
+                          "0x%x\n", d_id);
+                sgj_pr_hr(jsp, "      AOI: 0x%x\n", c_id);
+                sgj_pr_hr(jsp, "      vendor specific identifier B: 0x%x\n",
+                          vsi);
+                n = 0;
+                n += sg_scnpr(b + n, blen - n, "      [0x");
+                for (m = 0; m < 8; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", ip[m]);
+                sgj_pr_hr(jsp, "%s]\n", b);
+                break;
+            case 3:     /* NAA 3: Locally assigned */
+                if (8 != i_len) {
+                    pr2serr("      << unexpected NAA 3 identifier "
+                            "length: 0x%x>>\n", i_len);
+                    hex2stderr(ip, i_len, -1);
+                    break;
+                }
+                sgj_pr_hr(jsp, "      NAA 3, Locally assigned:\n");
+                n = 0;
+                n += sg_scnpr(b + n, blen - n, "      [0x");
+                for (m = 0; m < 8; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", ip[m]);
+                sgj_pr_hr(jsp, "%s]\n", b);
+                break;
+            case 5:     /* NAA 5: IEEE Registered */
+                if (8 != i_len) {
+                    pr2serr("      << unexpected NAA 5 identifier "
+                            "length: 0x%x>>\n", i_len);
+                    hex2stderr(ip, i_len, -1);
+                    break;
+                }
+                c_id = (((ip[0] & 0xf) << 20) | (ip[1] << 12) |
+                        (ip[2] << 4) | ((ip[3] & 0xf0) >> 4));
+                vsei = ip[3] & 0xf;
+                for (m = 1; m < 5; ++m) {
+                    vsei <<= 8;
+                    vsei |= ip[3 + m];
+                }
+                sgj_pr_hr(jsp, "      NAA 5, AOI: 0x%x\n", c_id);
+                n = 0;
+                n += sg_scnpr(b + n, blen - n, "      Vendor Specific "
+                              "Identifier: 0x%" PRIx64 "\n", vsei);
+                n += sg_scnpr(b + n, blen - n, "      [0x");
+                for (m = 0; m < 8; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", ip[m]);
+                sgj_pr_hr(jsp, "%s]\n", b);
+                break;
+            case 6:     /* NAA 6: IEEE Registered extended */
+                if (16 != i_len) {
+                    pr2serr("      << unexpected NAA 6 identifier "
+                            "length: 0x%x>>\n", i_len);
+                    hex2stderr(ip, i_len, 0);
+                    break;
+                }
+                c_id = (((ip[0] & 0xf) << 20) | (ip[1] << 12) |
+                        (ip[2] << 4) | ((ip[3] & 0xf0) >> 4));
+                vsei = ip[3] & 0xf;
+                for (m = 1; m < 5; ++m) {
+                    vsei <<= 8;
+                    vsei |= ip[3 + m];
+                }
+                sgj_pr_hr(jsp, "      NAA 6, AOI: 0x%x\n", c_id);
+                sgj_pr_hr(jsp, "      Vendor Specific Identifier: 0x%"
+                          PRIx64 "\n", vsei);
+                vsei = sg_get_unaligned_be64(ip + 8);
+                sgj_pr_hr(jsp, "      Vendor Specific Identifier Extension: "
+                          "0x%" PRIx64 "\n", vsei);
+                n = 0;
+                n += sg_scnpr(b + n, blen - n, "      [0x");
+                for (m = 0; m < 16; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", ip[m]);
+                sgj_pr_hr(jsp, "%s]\n", b);
+                break;
+            default:
+                pr2serr("      << bad NAA nibble , expect 2, 3, 5 or 6, "
+                        "got %d>>\n", naa);
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            break;
+        case 4: /* Relative target port */
+            if ((1 != c_set) || (1 != assoc) || (4 != i_len)) {
+                pr2serr("      << expected binary code_set, target "
+                        "port association, length 4>>\n");
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            d_id = sg_get_unaligned_be16(ip + 2);
+            sgj_pr_hr(jsp, "      Relative target port: 0x%x\n", d_id);
+            break;
+        case 5: /* (primary) Target port group */
+            if ((1 != c_set) || (1 != assoc) || (4 != i_len)) {
+                pr2serr("      << expected binary code_set, target "
+                        "port association, length 4>>\n");
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            d_id = sg_get_unaligned_be16(ip + 2);
+            sgj_pr_hr(jsp, "      Target port group: 0x%x\n", d_id);
+            break;
+        case 6: /* Logical unit group */
+            if ((1 != c_set) || (0 != assoc) || (4 != i_len)) {
+                pr2serr("      << expected binary code_set, logical "
+                        "unit association, length 4>>\n");
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            d_id = sg_get_unaligned_be16(ip + 2);
+            sgj_pr_hr(jsp, "      Logical unit group: 0x%x\n", d_id);
+            break;
+        case 7: /* MD5 logical unit identifier */
+            if ((1 != c_set) || (0 != assoc)) {
+                pr2serr("      << expected binary code_set, logical "
+                        "unit association>>\n");
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            sgj_pr_hr(jsp, "      MD5 logical unit identifier:\n");
+            if (jsp->pr_out_hr)
+                sgj_js_str_out(jsp, (const char *)ip, i_len);
+            else
+                hex2stdout(ip, i_len, -1);
+            break;
+        case 8: /* SCSI name string */
+            if (3 != c_set) {
+                if (2 == c_set) {
+                    if (op->verbose)
+                        pr2serr("      << expected UTF-8, use ASCII>>\n");
+                } else {
+                    pr2serr("      << expected UTF-8 code_set>>\n");
+                    hex2stderr(ip, i_len, -1);
+                    break;
+                }
+            }
+            sgj_pr_hr(jsp, "      SCSI name string:\n");
+            /* does %s print out UTF-8 ok??
+             * Seems to depend on the locale. Looks ok here with my
+             * locale setting: en_AU.UTF-8
+             */
+            sgj_pr_hr(jsp, "      %.*s\n", i_len, (const char *)ip);
+            break;
+        case 9: /* Protocol specific port identifier */
+            /* added in spc4r36, PIV must be set, proto_id indicates */
+            /* whether UAS (USB) or SOP (PCIe) or ... */
+            if (! piv)
+                pr2serr("      >>>> Protocol specific port identifier "
+                        "expects protocol\n"
+                        "           identifier to be valid and it is not\n");
+            if (TPROTO_UAS == p_id) {
+                sgj_pr_hr(jsp, "      USB device address: 0x%x\n",
+                          0x7f & ip[0]);
+                sgj_pr_hr(jsp, "      USB interface number: 0x%x\n", ip[2]);
+            } else if (TPROTO_SOP == p_id) {
+                sgj_pr_hr(jsp, "      PCIe routing ID, bus number: 0x%x\n",
+                          ip[0]);
+                sgj_pr_hr(jsp, "          function number: 0x%x\n", ip[1]);
+                sgj_pr_hr(jsp, "          [or device number: 0x%x, function "
+                          "number: 0x%x]\n", (0x1f & (ip[1] >> 3)),
+                          0x7 & ip[1]);
+            } else
+                sgj_pr_hr(jsp, "      >>>> unexpected protocol identifier: "
+                          "%s\n           with Protocol specific port "
+                          "identifier\n", sg_get_trans_proto_str(p_id, dlen,
+                                                                 d));
+            break;
+        case 0xa: /* UUID identifier [spc5r08] RFC 4122 */
+            if (1 != c_set) {
+                pr2serr("      << expected binary code_set >>\n");
+                hex2stderr(ip, i_len, 0);
+                break;
+            }
+            if ((1 != ((ip[0] >> 4) & 0xf)) || (18 != i_len)) {
+                pr2serr("      << expected locally assigned UUID, 16 bytes "
+                        "long >>\n");
+                hex2stderr(ip, i_len, 0);
+                break;
+            }
+            n = 0;
+            n += sg_scnpr(b + n, blen - n, "      Locally assigned UUID: ");
+            for (m = 0; m < 16; ++m) {
+                if ((4 == m) || (6 == m) || (8 == m) || (10 == m))
+                    n += sg_scnpr(b + n, blen - n, "-");
+                n += sg_scnpr(b + n, blen - n, "%02x", ip[2 + m]);
+            }
+            sgj_pr_hr(jsp, "%s\n", b);
+            break;
+        default: /* reserved */
+            pr2serr("      reserved designator=0x%x\n", desig_type);
+            hex2stderr(ip, i_len, -1);
+            break;
+        }
+    }
+    if (-2 == u)
+        pr2serr("%s VPD page error: around offset=%d\n", leadin, off);
+}
+
+/* The --export and --json options are assumed to be mutually exclusive.
+ * Here the former takes precedence. */
+static void
+export_dev_ids(uint8_t * buff, int len, int verbose)
+{
+    int u, j, m, id_len, c_set, assoc, desig_type, i_len;
+    int off, d_id, naa, k, p_id;
+    uint8_t * bp;
+    uint8_t * ip;
+    const char * assoc_str;
+    const char * suffix;
+
+    if (buff[2] != 0) {
+        /*
+         * Cf decode_dev_ids() for details
+         */
+        i_len = len;
+        ip = buff;
+        c_set = 1;
+        assoc = 0;
+        p_id = 0xf;
+        desig_type = 3;
+        j = 1;
+        off = 16;
+        goto decode;
+    }
+
+    for (j = 1, off = -1;
+         (u = sg_vpd_dev_id_iter(buff, len, &off, -1, -1, -1)) == 0;
+         ++j) {
+        bp = buff + off;
+        i_len = bp[3];
+        id_len = i_len + 4;
+        if ((off + id_len) > len) {
+            if (verbose)
+                pr2serr("Device Identification VPD page error: designator "
+                        "length longer than\n     remaining response "
+                        "length=%d\n", (len - off));
+            return;
+        }
+        ip = bp + 4;
+        p_id = ((bp[0] >> 4) & 0xf);   /* protocol identifier */
+        c_set = (bp[0] & 0xf);
+        assoc = ((bp[1] >> 4) & 0x3);
+        desig_type = (bp[1] & 0xf);
+  decode:
+        switch (assoc) {
+            case 0:
+                assoc_str = "LUN";
+                break;
+            case 1:
+                assoc_str = "PORT";
+                break;
+            case 2:
+                assoc_str = "TARGET";
+                break;
+            default:
+                if (verbose)
+                    pr2serr("    Invalid association %d\n", assoc);
+                return;
+        }
+        switch (desig_type) {
+        case 0: /* vendor specific */
+            if (i_len == 0 || i_len > 128)
+                break;
+            if ((2 == c_set) || (3 == c_set)) { /* ASCII or UTF-8 */
+                k = encode_whitespaces(ip, i_len);
+                /* udev-conforming character encoding */
+                if (k > 0) {
+                    printf("SCSI_IDENT_%s_VENDOR=", assoc_str);
+                    for (m = 0; m < k; ++m) {
+                        if ((ip[m] >= '0' && ip[m] <= '9') ||
+                            (ip[m] >= 'A' && ip[m] <= 'Z') ||
+                            (ip[m] >= 'a' && ip[m] <= 'z') ||
+                            strchr("#+-.:=@_", ip[m]) != NULL)
+                            printf("%c", ip[m]);
+                        else
+                            printf("\\x%02x", ip[m]);
+                    }
+                    printf("\n");
+                }
+            } else {
+                printf("SCSI_IDENT_%s_VENDOR=", assoc_str);
+                for (m = 0; m < i_len; ++m)
+                    printf("%02x", (unsigned int)ip[m]);
+                printf("\n");
+            }
+            break;
+        case 1: /* T10 vendor identification */
+            printf("SCSI_IDENT_%s_T10=", assoc_str);
+            if ((2 == c_set) || (3 == c_set)) {
+                k = encode_whitespaces(ip, i_len);
+                /* udev-conforming character encoding */
+                for (m = 0; m < k; ++m) {
+                    if ((ip[m] >= '0' && ip[m] <= '9') ||
+                        (ip[m] >= 'A' && ip[m] <= 'Z') ||
+                        (ip[m] >= 'a' && ip[m] <= 'z') ||
+                        strchr("#+-.:=@_", ip[m]) != NULL)
+                        printf("%c", ip[m]);
+                    else
+                        printf("\\x%02x", ip[m]);
+                }
+                printf("\n");
+                if (!memcmp(ip, "ATA_", 4)) {
+                    printf("SCSI_IDENT_%s_ATA=%.*s\n", assoc_str,
+                           k - 4, ip + 4);
+                }
+            } else {
+                for (m = 0; m < i_len; ++m)
+                    printf("%02x", (unsigned int)ip[m]);
+                printf("\n");
+            }
+            break;
+        case 2: /* EUI-64 based */
+            if (1 != c_set) {
+                if (verbose) {
+                    pr2serr("      << expected binary code_set (1)>>\n");
+                    hex2stderr(ip, i_len, 0);
+                }
+                break;
+            }
+            printf("SCSI_IDENT_%s_EUI64=", assoc_str);
+            for (m = 0; m < i_len; ++m)
+                printf("%02x", (unsigned int)ip[m]);
+            printf("\n");
+            break;
+        case 3: /* NAA */
+            if (1 != c_set) {
+                if (verbose) {
+                    pr2serr("      << expected binary code_set (1)>>\n");
+                    hex2stderr(ip, i_len, 0);
+                }
+                break;
+            }
+            /*
+             * Unfortunately, there are some (broken) implementations
+             * which return _several_ NAA descriptors.
+             * So add a suffix to differentiate between them.
+             */
+            naa = (ip[0] >> 4) & 0xff;
+            switch (naa) {
+                case 6:
+                    suffix="REGEXT";
+                    break;
+                case 5:
+                    suffix="REG";
+                    break;
+                case 2:
+                    suffix="EXT";
+                    break;
+                case 3:
+                default:
+                    suffix="LOCAL";
+                    break;
+            }
+            printf("SCSI_IDENT_%s_NAA_%s=", assoc_str, suffix);
+            for (m = 0; m < i_len; ++m)
+                printf("%02x", (unsigned int)ip[m]);
+            printf("\n");
+            break;
+        case 4: /* Relative target port */
+            if ((1 != c_set) || (1 != assoc) || (4 != i_len)) {
+                if (verbose) {
+                    pr2serr("      << expected binary code_set, target "
+                            "port association, length 4>>\n");
+                    hex2stderr(ip, i_len, 0);
+                }
+                break;
+            }
+            d_id = sg_get_unaligned_be16(ip + 2);
+            printf("SCSI_IDENT_%s_RELATIVE=%d\n", assoc_str, d_id);
+            break;
+        case 5: /* (primary) Target port group */
+            if ((1 != c_set) || (1 != assoc) || (4 != i_len)) {
+                if (verbose) {
+                    pr2serr("      << expected binary code_set, target "
+                            "port association, length 4>>\n");
+                    hex2stderr(ip, i_len, 0);
+                }
+                break;
+            }
+            d_id = sg_get_unaligned_be16(ip + 2);
+            printf("SCSI_IDENT_%s_TARGET_PORT_GROUP=0x%x\n", assoc_str, d_id);
+            break;
+        case 6: /* Logical unit group */
+            if ((1 != c_set) || (0 != assoc) || (4 != i_len)) {
+                if (verbose) {
+                    pr2serr("      << expected binary code_set, logical "
+                            "unit association, length 4>>\n");
+                    hex2stderr(ip, i_len, 0);
+                }
+                break;
+            }
+            d_id = sg_get_unaligned_be16(ip + 2);
+            printf("SCSI_IDENT_%s_LOGICAL_UNIT_GROUP=0x%x\n", assoc_str, d_id);
+            break;
+        case 7: /* MD5 logical unit identifier */
+            if ((1 != c_set) || (0 != assoc)) {
+                if (verbose) {
+                    pr2serr("      << expected binary code_set, logical "
+                            "unit association>>\n");
+                    hex2stderr(ip, i_len, 0);
+                }
+                break;
+            }
+            printf("SCSI_IDENT_%s_MD5=", assoc_str);
+            hex2stdout(ip, i_len, -1);
+            break;
+        case 8: /* SCSI name string */
+            if (3 != c_set) {
+                if (verbose) {
+                    pr2serr("      << expected UTF-8 code_set>>\n");
+                    hex2stderr(ip, i_len, -1);
+                }
+                break;
+            }
+            if (! (strncmp((const char *)ip, "eui.", 4) ||
+                   strncmp((const char *)ip, "EUI.", 4) ||
+                   strncmp((const char *)ip, "naa.", 4) ||
+                   strncmp((const char *)ip, "NAA.", 4) ||
+                   strncmp((const char *)ip, "iqn.", 4))) {
+                if (verbose) {
+                    pr2serr("      << expected name string prefix>>\n");
+                    hex2stderr(ip, i_len, -1);
+                }
+                break;
+            }
+
+            printf("SCSI_IDENT_%s_NAME=%.*s\n", assoc_str, i_len,
+                   (const char *)ip);
+            break;
+        case 9: /*  Protocol specific port identifier */
+            if (TPROTO_UAS == p_id) {
+                if ((4 != i_len) || (1 != assoc)) {
+                    if (verbose) {
+                        pr2serr("      << UAS (USB) expected target "
+                                "port association>>\n");
+                        hex2stderr(ip, i_len, 0);
+                    }
+                    break;
+                }
+                printf("SCSI_IDENT_%s_UAS_DEVICE_ADDRESS=0x%x\n", assoc_str,
+                       ip[0] & 0x7f);
+                printf("SCSI_IDENT_%s_UAS_INTERFACE_NUMBER=0x%x\n", assoc_str,
+                       ip[2]);
+            } else if (TPROTO_SOP == p_id) {
+                if ((4 != i_len) && (8 != i_len)) {   /* spc4r36h confused */
+                    if (verbose) {
+                        pr2serr("      << SOP (PCIe) descriptor "
+                                "length=%d >>\n", i_len);
+                        hex2stderr(ip, i_len, 0);
+                    }
+                    break;
+                }
+                printf("SCSI_IDENT_%s_SOP_ROUTING_ID=0x%x\n", assoc_str,
+                       sg_get_unaligned_be16(ip + 0));
+            } else {
+                pr2serr("      << Protocol specific port identifier "
+                        "protocol_id=0x%x>>\n", p_id);
+            }
+            break;
+        case 0xa: /* UUID based */
+            if (1 != c_set) {
+                if (verbose) {
+                    pr2serr("      << expected binary code_set (1)>>\n");
+                    hex2stderr(ip, i_len, 0);
+                }
+                break;
+            }
+            if (i_len < 18) {
+                if (verbose) {
+                    pr2serr("      << short UUID field expected 18 or more, "
+                            "got %d >>\n", i_len);
+                    hex2stderr(ip, i_len, 0);
+                }
+                break;
+            }
+            printf("SCSI_IDENT_%s_UUID=", assoc_str);
+            for (m = 2; m < i_len; ++m) {
+                if ((6 == m) || (8 == m) || (10 == m) || (12 == m))
+                    printf("-%02x", (unsigned int)ip[m]);
+                else
+                    printf("%02x", (unsigned int)ip[m]);
+            }
+            printf("\n");
+            break;
+        default: /* reserved */
+            if (verbose) {
+                pr2serr("      reserved designator=0x%x\n", desig_type);
+                hex2stderr(ip, i_len, -1);
+            }
+            break;
+        }
+    }
+    if (-2 == u && verbose)
+        pr2serr("Device identification VPD page error: "
+                "around offset=%d\n", off);
+}
+
+/* VPD_BLOCK_LIMITS  0xb0 ["bl"]  (SBC) */
+/* VPD_SA_DEV_CAP  0xb0 ["sad"]  (SSC) */
+/* Sequential access device characteristics,  ssc+smc */
+/* OSD information, osd */
+static void
+decode_b0_vpd(uint8_t * buff, int len, struct opts_t * op, sgj_opaque_p jop)
+{
+    int pdt = PDT_MASK & buff[0];
+    sgj_state * jsp = &op->json_st;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+        /* done by decode_block_limits_vpd() */
+        break;
+    case PDT_TAPE: case PDT_MCHANGER:
+        sgj_haj_vi_nex(jsp, jop, 2, "TSMC", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(buff[4] & 0x2), false, "Tape Stream Mirror "
+                       "Capable");
+        sgj_haj_vi_nex(jsp, jop, 2, "WORM", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(buff[4] & 0x1), false, "Write Once Read Multiple "
+                       "supported");
+
+        break;
+    case PDT_OSD:
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(buff, len, 0);
+        break;
+    }
+}
+
+/* VPD_BLOCK_DEV_CHARS sbc  0xb1 ["bdc"] */
+/* VPD_MAN_ASS_SN ssc */
+static void
+decode_b1_vpd(uint8_t * buff, int len, struct opts_t * op, sgj_opaque_p jop)
+{
+    int pdt;
+    sgj_state * jsp = &op->json_st;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    pdt = PDT_MASK & buff[0];
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+        /* now done by decode_block_dev_ch_vpd() in sg_vpd_common.c */
+        break;
+    case PDT_TAPE: case PDT_MCHANGER: case PDT_ADC:
+        sgj_pr_hr(jsp, "  Manufacturer-assigned serial number: %.*s\n",
+                  len - 4, buff + 4);
+        sgj_js_nv_s_len(jsp, jop, "manufacturer_assigned_serial_number",
+                        (const char *)buff + 4, len - 4);
+        break;
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(buff, len, 0);
+        break;
+    }
+}
+
+/* VPD_REFERRALS sbc          0xb3 ["ref"] */
+/* VPD_AUTOMATION_DEV_SN ssc  0xb3 ["adsn"] */
+static void
+decode_b3_vpd(uint8_t * buff, int len, struct opts_t * op, sgj_opaque_p jop)
+{
+    int pdt;
+    sgj_state * jsp = &op->json_st;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    pdt = buff[0] & PDT_MASK;
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+        /* now done in decode_referrals_vpd() in sg_vpd_common.c */
+        break;
+    case PDT_TAPE: case PDT_MCHANGER:
+        sgj_pr_hr(jsp, "  Automation device serial number: %.*s\n",
+                  len - 4, buff + 4);
+        sgj_js_nv_s_len(jsp, jop, "automation_device_serial_number",
+                        (const char *)buff + 4, len - 4);
+        break;
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(buff, len, 0);
+        break;
+    }
+}
+
+#if 0
+static void
+decode_rdac_vpd_c9_rtpg_data(uint8_t aas, uint8_t vendor)
+{
+    printf("  Asymmetric Access State:");
+    switch(aas & 0x0F) {
+        case 0x0:
+            printf(" Active/Optimized");
+            break;
+        case 0x1:
+            printf(" Active/Non-Optimized");
+            break;
+        case 0x2:
+            printf(" Standby");
+            break;
+        case 0x3:
+            printf(" Unavailable");
+            break;
+        case 0xE:
+            printf(" Offline");
+            break;
+        case 0xF:
+            printf(" Transitioning");
+            break;
+        default:
+            printf(" (unknown)");
+            break;
+    }
+    printf("\n");
+
+    printf("  Vendor Specific Field:");
+    switch(vendor) {
+        case 0x01:
+            printf(" Operating normally");
+            break;
+        case 0x02:
+            printf(" Non-responsive to queries");
+            break;
+        case 0x03:
+            printf(" Controller being held in reset");
+            break;
+        case 0x04:
+            printf(" Performing controller firmware download (1st "
+                   "controller)");
+            break;
+        case 0x05:
+            printf(" Performing controller firmware download (2nd "
+                   "controller)");
+            break;
+        case 0x06:
+            printf(" Quiesced as a result of an administrative request");
+            break;
+        case 0x07:
+            printf(" Service mode as a result of an administrative request");
+            break;
+        case 0xFF:
+            printf(" Details are not available");
+            break;
+        default:
+            printf(" (unknown)");
+            break;
+    }
+    printf("\n");
+}
+
+static void
+decode_rdac_vpd_c9(uint8_t * buff, int len, struct opts_t * op)
+{
+    if (len < 3) {
+        pr2serr("Volume Access Control VPD page length too short=%d\n", len);
+        return;
+    }
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    if (buff[4] != 'v' && buff[5] != 'a' && buff[6] != 'c') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding "
+                "not possible.\n" , buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    if (buff[7] != '1') {
+        pr2serr("Invalid page version '%c' (should be 1)\n", buff[7]);
+    }
+    if ( (buff[8] & 0xE0) == 0xE0 ) {
+        printf("  IOShipping (ALUA): Enabled\n");
+    } else {
+        printf("  AVT:");
+        if (buff[8] & 0x80) {
+            printf(" Enabled");
+            if (buff[8] & 0x40)
+                printf(" (Allow reads on sector 0)");
+            printf("\n");
+        } else {
+            printf(" Disabled\n");
+        }
+    }
+    printf("  Volume Access via: ");
+    if (buff[8] & 0x01)
+        printf("primary controller\n");
+    else
+        printf("alternate controller\n");
+
+    if (buff[8] & 0x08) {
+        printf("  Path priority: %d ", buff[15] & 0xf);
+        switch(buff[15] & 0xf) {
+            case 0x1:
+                printf("(preferred path)\n");
+                break;
+            case 0x2:
+                printf("(secondary path)\n");
+                break;
+            default:
+                printf("(unknown)\n");
+                break;
+        }
+
+        printf("  Preferred Path Auto Changeable:");
+        switch(buff[14] & 0x3C) {
+            case 0x14:
+                printf(" No (User Disabled and Host Type Restricted)\n");
+                break;
+            case 0x18:
+                printf(" No (User Disabled)\n");
+                break;
+            case 0x24:
+                printf(" No (Host Type Restricted)\n");
+                break;
+            case 0x28:
+                printf(" Yes\n");
+                break;
+            default:
+                printf(" (Unknown)\n");
+                break;
+        }
+
+        printf("  Implicit Failback:");
+        switch(buff[14] & 0x03) {
+            case 0x1:
+                printf(" Disabled\n");
+                break;
+            case 0x2:
+                printf(" Enabled\n");
+                break;
+            default:
+                printf(" (Unknown)\n");
+                break;
+        }
+    } else {
+        printf("  Path priority: %d ", buff[9] & 0xf);
+        switch(buff[9] & 0xf) {
+            case 0x1:
+                printf("(preferred path)\n");
+                break;
+            case 0x2:
+                printf("(secondary path)\n");
+                break;
+            default:
+                printf("(unknown)\n");
+                break;
+        }
+    }
+
+    if (buff[8] & 0x80) {
+        printf(" Target Port Group Data (This controller):\n");
+        decode_rdac_vpd_c9_rtpg_data(buff[10], buff[11]);
+
+        printf(" Target Port Group Data (Alternate controller):\n");
+        decode_rdac_vpd_c9_rtpg_data(buff[12], buff[13]);
+    }
+
+    return;
+}
+#endif
+
+extern const char * sg_ansi_version_arr[];
+
+static const char *
+get_ansi_version_str(int version, char * b, int blen)
+{
+    version &= 0xf;
+    b[blen - 1] = '\0';
+    strncpy(b, sg_ansi_version_arr[version], blen - 1);
+    return b;
+}
+
+static void
+std_inq_decode(struct opts_t * op, sgj_opaque_p jop, int off)
+{
+    int len, pqual, pdt, ansi_version, k, j;
+    sgj_state * jsp = &op->json_st;
+    bool as_json = jsp->pr_as_json;
+    const char * cp;
+    const uint8_t * rp;
+    int vdesc_arr[8];
+    char b[128];
+    static const int blen = sizeof(b);
+
+    rp = rsp_buff + off;
+    memset(vdesc_arr, 0, sizeof(vdesc_arr));
+    if (op->do_raw) {
+        dStrRaw((const char *)rp, op->maxlen);
+        return;
+    } else if (op->do_hex) {
+        /* with -H, print with address, -HH without */
+        hex2stdout(rp, op->maxlen, no_ascii_4hex(op));
+        return;
+    }
+    pqual = (rp[0] & 0xe0) >> 5;
+    if (! op->do_raw && ! op->do_export) {
+        strcpy(b, "standard INQUIRY:");
+        if (0 == pqual)
+            sgj_pr_hr(jsp, "%s\n", b);
+        else if (1 == pqual)
+            sgj_pr_hr(jsp, "%s [PQ indicates LU temporarily unavailable]\n",
+                      b);
+        else if (3 == pqual)
+            sgj_pr_hr(jsp, "%s [PQ indicates LU not accessible via this "
+                      "port]\n", b);
+        else
+            sgj_pr_hr(jsp, "%s [reserved or vendor specific qualifier "
+                      "[%d]]\n", b, pqual);
+    }
+    len = rp[4] + 5;
+    /* N.B. rp[2] full byte is 'version' in SPC-2,3,4 but in SPC
+     * [spc-r11a (1997)] bits 6,7: ISO/IEC version; bits 3-5: ECMA
+     * version; bits 0-2: SCSI version */
+    ansi_version = rp[2] & 0x7;       /* Only take SCSI version */
+    pdt = rp[0] & PDT_MASK;
+    if (op->do_export) {
+        printf("SCSI_TPGS=%d\n", (rp[5] & 0x30) >> 4);
+        cp = sg_get_pdt_str(pdt, blen, b);
+        if (strlen(cp) > 0)
+            printf("SCSI_TYPE=%s\n", cp);
+    } else {
+        sgj_pr_hr(jsp, "  PQual=%d  PDT=%d  RMB=%d  LU_CONG=%d  "
+                  "hot_pluggable=%d  version=0x%02x ", pqual, pdt,
+                  !!(rp[1] & 0x80), !!(rp[1] & 0x40), (rp[1] >> 4) & 0x3,
+                  (unsigned int)rp[2]);
+        sgj_pr_hr(jsp, " [%s]\n", get_ansi_version_str(ansi_version, b,
+                                                       blen));
+        sgj_pr_hr(jsp, "  [AERC=%d]  [TrmTsk=%d]  NormACA=%d  HiSUP=%d "
+                  " Resp_data_format=%d\n  SCCS=%d  ", !!(rp[3] & 0x80),
+                  !!(rp[3] & 0x40), !!(rp[3] & 0x20), !!(rp[3] & 0x10),
+                  rp[3] & 0x0f, !!(rp[5] & 0x80));
+        sgj_pr_hr(jsp, "ACC=%d  TPGS=%d  3PC=%d  Protect=%d ",
+                  !!(rp[5] & 0x40), ((rp[5] & 0x30) >> 4), !!(rp[5] & 0x08),
+                  !!(rp[5] & 0x01));
+        sgj_pr_hr(jsp, " [BQue=%d]\n  EncServ=%d  ", !!(rp[6] & 0x80),
+                  !!(rp[6] & 0x40));
+        if (rp[6] & 0x10)
+            sgj_pr_hr(jsp, "MultiP=1 (VS=%d)  ", !!(rp[6] & 0x20));
+        else
+            sgj_pr_hr(jsp, "MultiP=0  ");
+        sgj_pr_hr(jsp, "[MChngr=%d]  [ACKREQQ=%d]  Addr16=%d\n  "
+                  "[RelAdr=%d]  ", !!(rp[6] & 0x08), !!(rp[6] & 0x04),
+                  !!(rp[6] & 0x01), !!(rp[7] & 0x80));
+        sgj_pr_hr(jsp, "WBus16=%d  Sync=%d  [Linked=%d]  [TranDis=%d]  ",
+                  !!(rp[7] & 0x20), !!(rp[7] & 0x10), !!(rp[7] & 0x08),
+                  !!(rp[7] & 0x04));
+        sgj_pr_hr(jsp, "CmdQue=%d\n", !!(rp[7] & 0x02));
+        if (op->maxlen > 56)
+            sgj_pr_hr(jsp, "  [SPI: Clocking=0x%x  QAS=%d  IUS=%d]\n",
+                      (rp[56] & 0x0c) >> 2, !!(rp[56] & 0x2),
+                      !!(rp[56] & 0x1));
+        if (op->maxlen >= len)
+            sgj_pr_hr(jsp, "    length=%d (0x%x)", len, len);
+        else
+            sgj_pr_hr(jsp, "    length=%d (0x%x), but only fetched %d bytes",
+                      len, len, op->maxlen);
+        if ((ansi_version >= 2) && (len < SAFE_STD_INQ_RESP_LEN))
+            sgj_pr_hr(jsp, "\n  [for SCSI>=2, len>=36 is expected]");
+        cp = sg_get_pdt_str(pdt, blen, b);
+        if (strlen(cp) > 0)
+            sgj_pr_hr(jsp, "   Peripheral device type: %s\n", cp);
+    }
+    if (op->maxlen <= 8) {
+        if (! op->do_export)
+            sgj_pr_hr(jsp, " Inquiry response length=%d, no vendor, product "
+                      "or revision data\n", op->maxlen);
+    } else {
+        int i;
+
+        memcpy(xtra_buff, &rp[8], 8);
+        xtra_buff[8] = '\0';
+        /* Fixup any tab characters */
+        for (i = 0; i < 8; ++i)
+            if (xtra_buff[i] == 0x09)
+                xtra_buff[i] = ' ';
+        if (op->do_export) {
+            len = encode_whitespaces((uint8_t *)xtra_buff, 8);
+            if (len > 0) {
+                printf("SCSI_VENDOR=%s\n", xtra_buff);
+                encode_string(xtra_buff, &rp[8], 8);
+                printf("SCSI_VENDOR_ENC=%s\n", xtra_buff);
+            }
+        } else
+            sgj_pr_hr(jsp, " Vendor identification: %s\n", xtra_buff);
+        if (op->maxlen <= 16) {
+            if (! op->do_export)
+                sgj_pr_hr(jsp, " Product identification: <none>\n");
+        } else {
+            memcpy(xtra_buff, &rp[16], 16);
+            xtra_buff[16] = '\0';
+            if (op->do_export) {
+                len = encode_whitespaces((uint8_t *)xtra_buff, 16);
+                if (len > 0) {
+                    printf("SCSI_MODEL=%s\n", xtra_buff);
+                    encode_string(xtra_buff, &rp[16], 16);
+                    printf("SCSI_MODEL_ENC=%s\n", xtra_buff);
+                }
+            } else
+                sgj_pr_hr(jsp, " Product identification: %s\n", xtra_buff);
+        }
+        if (op->maxlen <= 32) {
+            if (! op->do_export)
+                sgj_pr_hr(jsp, " Product revision level: <none>\n");
+        } else {
+            memcpy(xtra_buff, &rp[32], 4);
+            xtra_buff[4] = '\0';
+            if (op->do_export) {
+                len = encode_whitespaces((uint8_t *)xtra_buff, 4);
+                if (len > 0)
+                    printf("SCSI_REVISION=%s\n", xtra_buff);
+            } else
+                sgj_pr_hr(jsp, " Product revision level: %s\n", xtra_buff);
+        }
+        if (op->do_vendor && (op->maxlen > 36) && ('\0' != rp[36]) &&
+            (' ' != rp[36])) {
+            memcpy(xtra_buff, &rp[36], op->maxlen < 56 ? op->maxlen - 36 :
+                   20);
+            if (op->do_export) {
+                len = encode_whitespaces((uint8_t *)xtra_buff, 20);
+                if (len > 0)
+                    printf("VENDOR_SPECIFIC=%s\n", xtra_buff);
+            } else
+                sgj_pr_hr(jsp, " Vendor specific: %s\n", xtra_buff);
+        }
+        if (op->do_descriptors) {
+            for (j = 0, k = 58; ((j < 8) && ((k + 1) < op->maxlen));
+                 k +=2, ++j)
+                vdesc_arr[j] = sg_get_unaligned_be16(rp + k);
+        }
+        if ((op->do_vendor > 1) && (op->maxlen > 96)) {
+            memcpy(xtra_buff, &rp[96], op->maxlen - 96);
+            if (op->do_export) {
+                len = encode_whitespaces((uint8_t *)xtra_buff,
+                                         op->maxlen - 96);
+                if (len > 0)
+                    printf("VENDOR_SPECIFIC=%s\n", xtra_buff);
+            } else
+                sgj_pr_hr(jsp, " Vendor specific: %s\n", xtra_buff);
+        }
+        if (op->do_vendor && (op->maxlen > 243) &&
+            (0 == strncmp("OPEN-V", (const char *)&rp[16], 6))) {
+           memcpy(xtra_buff, &rp[212], 32);
+           if (op->do_export) {
+                len = encode_whitespaces((uint8_t *)xtra_buff, 32);
+                if (len > 0)
+                    printf("VENDOR_SPECIFIC_OPEN-V_LDEV_NAME=%s\n", xtra_buff);
+            } else
+                sgj_pr_hr(jsp, " Vendor specific OPEN-V LDEV Name: %s\n",
+                          xtra_buff);
+        }
+    }
+    if (! op->do_export) {
+        sgj_opaque_p jo2p = NULL;
+
+        if (as_json)
+            jo2p = std_inq_decode_js(rp, op->maxlen, op, jop);
+        if ((0 == op->maxlen) && usn_buff[0])
+            sgj_pr_hr(jsp, " Unit serial number: %s\n", usn_buff);
+        if (op->do_descriptors) {
+            sgj_opaque_p jap = sgj_named_subarray_r(jsp, jo2p,
+                                                "version_descriptor_list");
+            if (0 == vdesc_arr[0]) {
+                sgj_pr_hr(jsp, "\n");
+                sgj_pr_hr(jsp, "  No version descriptors available\n");
+            } else {
+                sgj_pr_hr(jsp, "\n");
+                sgj_pr_hr(jsp, "  Version descriptors:\n");
+                for (k = 0; k < 8; ++k) {
+                    sgj_opaque_p jo3p = sgj_new_unattached_object_r(jsp);
+                    int vdv = vdesc_arr[k];
+
+                    if (0 == vdv)
+                        break;
+                    cp = find_version_descriptor_str(vdv);
+                    if (cp)
+                        sgj_pr_hr(jsp, "    %s\n", cp);
+                    else
+                        sgj_pr_hr(jsp, "    [unrecognised version descriptor "
+                                  "code: 0x%x]\n", vdv);
+                    sgj_js_nv_ihexstr(jsp, jo3p, "version_descriptor", vdv,
+                                      NULL, cp ? cp : "unknown");
+                    sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+                }
+            }
+        }
+    }
+}
+
+/* Returns 0 if Unit Serial Number VPD page contents found, else see
+ * sg_ll_inquiry_v2() return values */
+static int
+fetch_unit_serial_num(int sg_fd, char * obuff, int obuff_len, int verbose)
+{
+    int len, k, res, c;
+    uint8_t * b;
+    uint8_t * free_b;
+
+    b = sg_memalign(DEF_ALLOC_LEN, 0, &free_b, false);
+    if (NULL == b) {
+        pr2serr("%s: unable to allocate on heap\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    res = vpd_fetch_page(sg_fd, b, VPD_SUPPORTED_VPDS,
+                         -1 /* 1 byte alloc_len */, false, verbose, &len);
+    if (res) {
+        if (verbose > 2)
+            pr2serr("%s: no supported VPDs page\n", __func__);
+        res = SG_LIB_CAT_MALFORMED;
+        goto fini;
+    }
+    if (! vpd_page_is_supported(b, len, VPD_UNIT_SERIAL_NUM, verbose)) {
+        res = sg_convert_errno(EDOM); /* was SG_LIB_CAT_ILLEGAL_REQ */
+        goto fini;
+    }
+
+    memset(b, 0xff, 4); /* guard against empty response */
+    res = vpd_fetch_page(sg_fd, b, VPD_UNIT_SERIAL_NUM, -1, false, verbose,
+                         &len);
+    if ((0 == res) && (len > 3)) {
+        len -= 4;
+        len = (len < (obuff_len - 1)) ? len : (obuff_len - 1);
+        if (len > 0) {
+            /* replace non-printable characters (except NULL) with space */
+            for (k = 0; k < len; ++k) {
+                c = b[4 + k];
+                if (c)
+                    obuff[k] = isprint(c) ? c : ' ';
+                else
+                    break;
+            }
+            obuff[k] = '\0';
+            res = 0;
+            goto fini;
+        } else {
+            if (verbose > 2)
+                pr2serr("%s: bad sn VPD page\n", __func__);
+            res = SG_LIB_CAT_MALFORMED;
+        }
+    } else {
+        if (verbose > 2)
+            pr2serr("%s: no supported VPDs page\n", __func__);
+        res = SG_LIB_CAT_MALFORMED;
+    }
+fini:
+    if (free_b)
+        free(free_b);
+    return res;
+}
+
+
+/* Process a standard INQUIRY data format (response).
+ * Returns 0 if successful */
+static int
+std_inq_process(int sg_fd, struct opts_t * op, sgj_opaque_p jop, int off)
+{
+    int res, len, rlen, act_len;
+    int vb, resid;
+    char buff[48];
+
+    if (sg_fd < 0) {    /* assume --inhex=FD usage */
+        std_inq_decode(op, jop, off);
+        return 0;
+    }
+    rlen = (op->maxlen > 0) ? op->maxlen : SAFE_STD_INQ_RESP_LEN;
+    vb = op->verbose;
+    res = sg_ll_inquiry_v2(sg_fd, false, 0, rsp_buff, rlen, DEF_PT_TIMEOUT,
+                           &resid, false, vb);
+    if (0 == res) {
+        if ((vb > 4) && ((rlen - resid) > 0)) {
+            pr2serr("Safe (36 byte) Inquiry response:\n");
+            hex2stderr(rsp_buff, rlen - resid, 0);
+        }
+        len = rsp_buff[4] + 5;
+        if ((len > SAFE_STD_INQ_RESP_LEN) && (len < 256) &&
+            (0 == op->maxlen)) {
+            rlen = len;
+            memset(rsp_buff, 0, rlen);
+            if (sg_ll_inquiry_v2(sg_fd, false, 0, rsp_buff, rlen,
+                                 DEF_PT_TIMEOUT, &resid, true, vb)) {
+                pr2serr("second INQUIRY (%d byte) failed\n", len);
+                return SG_LIB_CAT_OTHER;
+            }
+            if (len != (rsp_buff[4] + 5)) {
+                pr2serr("strange, consecutive INQUIRYs yield different "
+                        "'additional lengths'\n");
+                len = rsp_buff[4] + 5;
+            }
+        }
+        if (op->maxlen > 0)
+            act_len = rlen;
+        else
+            act_len = (rlen < len) ? rlen : len;
+        /* don't use more than HBA's resid says was transferred from LU */
+        if (act_len > (rlen - resid))
+            act_len = rlen - resid;
+        if (act_len < SAFE_STD_INQ_RESP_LEN)
+            rsp_buff[act_len] = '\0';
+        if ((! op->do_only) && (! op->do_export) && (0 == op->maxlen)) {
+            if (fetch_unit_serial_num(sg_fd, usn_buff, sizeof(usn_buff), vb))
+                usn_buff[0] = '\0';
+        }
+        op->maxlen = act_len;
+        std_inq_decode(op, jop, 0);
+        return 0;
+    } else if (res < 0) { /* could be an ATA device */
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+        /* Try an ATA Identify Device command */
+        res = try_ata_identify(sg_fd, op->do_hex, op->do_raw, vb);
+        if (0 != res) {
+            pr2serr("SCSI INQUIRY, NVMe Identify and fetching ATA "
+                    "information failed on %s\n", op->device_name);
+            return (res < 0) ? SG_LIB_CAT_OTHER : res;
+        }
+#else
+        pr2serr("SCSI INQUIRY failed on %s, res=%d\n",
+                op->device_name, res);
+        return res;
+#endif
+    } else {
+        char b[80];
+
+        if (vb > 0) {
+            pr2serr("    inquiry: failed requesting %d byte response: ", rlen);
+            if (resid && (vb > 1))
+                snprintf(buff, sizeof(buff), " [resid=%d]", resid);
+            else
+                buff[0] = '\0';
+            sg_get_category_sense_str(res, sizeof(b), b, vb);
+            pr2serr("%s%s\n", b, buff);
+        }
+        return res;
+    }
+    return 0;
+}
+
+#ifdef SG_SCSI_STRINGS
+/* Returns 0 if successful */
+static int
+cmddt_process(int sg_fd, const struct opts_t * op)
+{
+    int k, j, num, len, pdt, reserved_cmddt, support_num, res;
+    char op_name[128];
+
+    memset(rsp_buff, 0, rsp_buff_sz);
+    if (op->do_cmddt > 1) {
+        printf("Supported command list:\n");
+        for (k = 0; k < 256; ++k) {
+            res = sg_ll_inquiry(sg_fd, true /* cmddt */, false, k, rsp_buff,
+                                DEF_ALLOC_LEN, true, op->verbose);
+            if (0 == res) {
+                pdt = rsp_buff[0] & PDT_MASK;
+                support_num = rsp_buff[1] & 7;
+                reserved_cmddt = rsp_buff[4];
+                if ((3 == support_num) || (5 == support_num)) {
+                    num = rsp_buff[5];
+                    for (j = 0; j < num; ++j)
+                        printf(" %.2x", (int)rsp_buff[6 + j]);
+                    if (5 == support_num)
+                        printf("  [vendor specific manner (5)]");
+                    sg_get_opcode_name((uint8_t)k, pdt,
+                                       sizeof(op_name) - 1, op_name);
+                    op_name[sizeof(op_name) - 1] = '\0';
+                    printf("  %s\n", op_name);
+                } else if ((4 == support_num) || (6 == support_num))
+                    printf("  opcode=0x%.2x vendor specific (%d)\n",
+                           k, support_num);
+                else if ((0 == support_num) && (reserved_cmddt > 0)) {
+                    printf("  opcode=0x%.2x ignored cmddt bit, "
+                           "given standard INQUIRY response, stop\n", k);
+                    break;
+                }
+            } else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+                break;
+            else {
+                pr2serr("CmdDt INQUIRY on opcode=0x%.2x: failed\n", k);
+                break;
+            }
+        }
+    }
+    else {
+        res = sg_ll_inquiry(sg_fd, true /* cmddt */, false, op->vpd_pn,
+                            rsp_buff, DEF_ALLOC_LEN, true, op->verbose);
+        if (0 == res) {
+            pdt = rsp_buff[0] & PDT_MASK;
+            if (! op->do_raw) {
+                printf("CmdDt INQUIRY, opcode=0x%.2x:  [", op->vpd_pn);
+                sg_get_opcode_name((uint8_t)op->vpd_pn, pdt,
+                                   sizeof(op_name) - 1, op_name);
+                op_name[sizeof(op_name) - 1] = '\0';
+                printf("%s]\n", op_name);
+            }
+            len = rsp_buff[5] + 6;
+            reserved_cmddt = rsp_buff[4];
+            if (op->do_hex)
+                hex2stdout(rsp_buff, len, no_ascii_4hex(op));
+            else if (op->do_raw)
+                dStrRaw((const char *)rsp_buff, len);
+            else {
+                bool prnt_cmd = false;
+                const char * desc_p;
+
+                support_num = rsp_buff[1] & 7;
+                num = rsp_buff[5];
+                switch (support_num) {
+                case 0:
+                    if (0 == reserved_cmddt)
+                        desc_p = "no data available";
+                    else
+                        desc_p = "ignored cmddt bit, standard INQUIRY "
+                                 "response";
+                    break;
+                case 1: desc_p = "not supported"; break;
+                case 2: desc_p = "reserved (2)"; break;
+                case 3: desc_p = "supported as per standard";
+                        prnt_cmd = true;
+                        break;
+                case 4: desc_p = "vendor specific (4)"; break;
+                case 5: desc_p = "supported in vendor specific way";
+                        prnt_cmd = true;
+                        break;
+                case 6: desc_p = "vendor specific (6)"; break;
+                case 7: desc_p = "reserved (7)"; break;
+                }
+                if (prnt_cmd) {
+                    printf("  Support field: %s [", desc_p);
+                    for (j = 0; j < num; ++j)
+                        printf(" %.2x", (int)rsp_buff[6 + j]);
+                    printf(" ]\n");
+                } else
+                    printf("  Support field: %s\n", desc_p);
+            }
+        } else if (SG_LIB_CAT_ILLEGAL_REQ != res) {
+            if (! op->do_raw) {
+                printf("CmdDt INQUIRY, opcode=0x%.2x:  [", op->vpd_pn);
+                sg_get_opcode_name((uint8_t)op->vpd_pn, 0,
+                                   sizeof(op_name) - 1, op_name);
+                op_name[sizeof(op_name) - 1] = '\0';
+                printf("%s]\n", op_name);
+            }
+            pr2serr("CmdDt INQUIRY on opcode=0x%.2x: failed\n", op->vpd_pn);
+        }
+    }
+    return res;
+}
+
+#else /* SG_SCSI_STRINGS */
+
+/* Returns 0. */
+static int
+cmddt_process(int sg_fd, const struct opts_t * op)
+{
+    if (sg_fd) { }      /* suppress warning */
+    if (op) { }         /* suppress warning */
+    pr2serr("'--cmddt' not implemented, use sg_opcodes\n");
+    return 0;
+}
+
+#endif /* SG_SCSI_STRINGS */
+
+
+/* Returns 0 if successful */
+static int
+vpd_mainly_hex(int sg_fd, struct opts_t * op, sgj_opaque_p jap, int off)
+{
+    bool as_json;
+    bool json_o_hr;
+    int res, len, n;
+    char b[128];
+    sgj_state * jsp = &op->json_st;
+    const char * cp;
+    uint8_t * rp;
+
+    as_json = jsp->pr_as_json;
+    json_o_hr = as_json && jsp->pr_out_hr;
+    rp = rsp_buff + off;
+    if ((! op->do_raw) && (op->do_hex < 3)) {
+        if (op->do_hex)
+            printf("VPD INQUIRY, page code=0x%.2x:\n", op->vpd_pn);
+        else
+            sgj_pr_hr(jsp, "VPD INQUIRY, page code=0x%.2x:\n", op->vpd_pn);
+    }
+    if (sg_fd < 0) {
+        len = sg_get_unaligned_be16(rp + 2) + 4;
+        res = 0;
+    } else {
+        memset(rp, 0, DEF_ALLOC_LEN);
+        res = vpd_fetch_page(sg_fd, rp, op->vpd_pn, op->maxlen,
+                             op->do_quiet, op->verbose, &len);
+    }
+    if (0 == res) {
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            int pdt = pdt = rp[0] & PDT_MASK;
+
+            if (0 == op->vpd_pn)
+                decode_supported_vpd_4inq(rp, len, op, jap);
+            else {
+                if (op->verbose) {
+                    cp = sg_get_pdt_str(pdt, sizeof(b), b);
+                    if (op->do_hex)
+                        printf("   [PQual=%d  Peripheral device type: %s]\n",
+                               (rp[0] & 0xe0) >> 5, cp);
+                    else
+                        sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device "
+                                  "type: %s]\n", (rp[0] & 0xe0) >> 5, cp);
+                }
+                if (json_o_hr && (0 == op->do_hex) && (len > 0) &&
+                    (len < UINT16_MAX)) {
+                    char * p;
+
+                    n = len * 4;
+                    p = malloc(n);
+                    if (p) {
+                        n = hex2str(rp, len, NULL, 1, n - 1, p);
+                        sgj_js_str_out(jsp, p, n);
+                    }
+                } else
+                    hex2stdout(rp, len, no_ascii_4hex(op));
+            }
+        }
+    } else {
+        if (SG_LIB_CAT_ILLEGAL_REQ == res)
+            pr2serr("    inquiry: field in cdb illegal (page not "
+                    "supported)\n");
+        else {
+            sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+            pr2serr("    inquiry: %s\n", b);
+        }
+    }
+    return res;
+}
+
+static int
+recurse_vpd_decode(struct opts_t * op, sgj_opaque_p jop, int off)
+{
+    return vpd_decode(-1, op, jop, off);
+}
+
+/* Returns 0 if successful */
+static int
+vpd_decode(int sg_fd, struct opts_t * op, sgj_opaque_p jop, int off)
+{
+    bool bad = false;
+    bool qt = op->do_quiet;
+    int len, pdt, pn, vb /*, pqual */;
+    int res = 0;
+    sgj_state * jsp = &op->json_st;
+    bool as_json = jsp->pr_as_json;
+    sgj_opaque_p jo2p = NULL;
+    sgj_opaque_p jap = NULL;
+    const char * np;
+    const char * ep = "";
+    uint8_t * rp;
+
+    rp = rsp_buff + off;
+    vb = op->verbose;
+    if ((off > 0) && (VPD_NOPE_WANT_STD_INQ != op->vpd_pn))
+        pn = rp[1];
+    else
+        pn = op->vpd_pn;
+    if (sg_fd != -1 && !op->do_force && pn != VPD_SUPPORTED_VPDS) {
+        res = vpd_fetch_page(sg_fd, rp, VPD_SUPPORTED_VPDS, op->maxlen,
+                             qt, vb, &len);
+        if (res)
+            goto out;
+        if (! vpd_page_is_supported(rp, len, pn, vb)) {
+            if (vb)
+                pr2serr("Given VPD page not in supported list, use --force "
+                        "to override this check\n");
+            res = sg_convert_errno(EDOM); /* was SG_LIB_CAT_ILLEGAL_REQ */
+            goto out;
+        }
+    }
+    switch (pn) {
+    case VPD_SUPPORTED_VPDS:            /* 0x0  ["sv"] */
+        np = "Supported VPD pages VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else if (op->do_hex)
+            hex2stdout(rp, len, no_ascii_4hex(op));
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "supported_vpd_page_list");
+            }
+            decode_supported_vpd_4inq(rp, len, op, jap);
+        }
+        break;
+    case VPD_UNIT_SERIAL_NUM:           /* 0x80  ["sn"] */
+        np = "Unit serial number VPD page";
+        if (! op->do_raw && ! op->do_export && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else if (op->do_hex)
+            hex2stdout(rp, len, no_ascii_4hex(op));
+        else {
+            char obuff[DEF_ALLOC_LEN];
+            int k, m;
+
+            memset(obuff, 0, sizeof(obuff));
+            len -= 4;
+            if (len >= (int)sizeof(obuff))
+                len = sizeof(obuff) - 1;
+            memcpy(obuff, rp + 4, len);
+            if (op->do_export) {
+                k = encode_whitespaces((uint8_t *)obuff, len);
+                if (k > 0) {
+                    printf("SCSI_IDENT_SERIAL=");
+                    /* udev-conforming character encoding */
+                    for (m = 0; m < k; ++m) {
+                        if ((obuff[m] >= '0' && obuff[m] <= '9') ||
+                            (obuff[m] >= 'A' && obuff[m] <= 'Z') ||
+                            (obuff[m] >= 'a' && obuff[m] <= 'z') ||
+                            strchr("#+-.:=@_", obuff[m]) != NULL)
+                            printf("%c", obuff[m]);
+                        else
+                            printf("\\x%02x", obuff[m]);
+                    }
+                    printf("\n");
+                }
+            } else {
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                k = encode_unicode((uint8_t *)obuff, len);
+                if (k > 0) {
+                    sgj_pr_hr(jsp, "  Unit serial number: %s\n", obuff);
+                    sgj_js_nv_s(jsp, jo2p, "unit_serial_number", obuff);
+                }
+            }
+        }
+        break;
+    case VPD_DEVICE_ID:         /* 0x83  ["di"] */
+        np = "Device Identification VPD page";
+        if (! op->do_raw && ! op->do_export && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else if (op->do_hex > 2)
+            hex2stdout(rp, len, -1);
+        else if (op->do_export && (! as_json))
+            export_dev_ids(rp + 4, len - 4, op->verbose);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "designation_descriptor_list");
+            }
+            decode_id_vpd(rp, len, op, jap);
+        }
+        break;
+    case VPD_SOFTW_INF_ID:      /* 0x84  ["sii"] */
+        np = "Software interface identification VPD page";
+        if (! op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "software_interface_identifier_list");
+            }
+            decode_softw_inf_id(rp, len, op, jap);
+        }
+        break;
+    case VPD_MAN_NET_ADDR:    /* 0x85 ["mna"] */
+        np = "Management network addresses page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            // pdt = rp[0] & PDT_MASK;
+            // pdt_str = sg_get_pdt_str(pdt, sizeof(d), d);
+            // pqual = (rp[0] & 0xe0) >> 5;
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "network_services_descriptor_list");
+            }
+            decode_net_man_vpd(rp, len, op, jap);
+        }
+        break;
+    case VPD_EXT_INQ:           /* 0x86  ["ei"] */
+        np = "Extended INQUIRY data";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s page\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            bool protect = false;
+
+            op->protect_not_sure = false;
+            if ((sg_fd >= 0) && (! op->do_force)) {
+                struct sg_simple_inquiry_resp sir;
+
+                res = sg_simple_inquiry(sg_fd, &sir, false, vb);
+                if (res) {
+                    if (op->verbose)
+                        pr2serr("%s: sg_simple_inquiry() failed, res=%d\n",
+                                __func__, res);
+                    op->protect_not_sure = true;
+                } else
+                    protect = !!(sir.byte_5 & 0x1); /* SPC-3 and later */
+            } else
+                op->protect_not_sure = true;
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            decode_x_inq_vpd(rp, len, protect, op, jo2p);
+        }
+        break;
+    case VPD_MODE_PG_POLICY:            /*  0x87  ["mpp"] */
+        np = "Mode page policy";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "mode_page_policy_descriptor_list");
+            }
+            decode_mode_policy_vpd(rp, len, op, jap);
+        }
+        break;
+    case VPD_SCSI_PORTS:        /* 0x88  ["sp"] */
+        np = "SCSI Ports VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "scsi_ports_descriptor_list");
+            }
+            decode_scsi_ports_vpd_4inq(rp, len, op, jap);
+        }
+        break;
+    case VPD_ATA_INFO:          /* 0x89  ["ai"] */
+        np = "ATA information VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        /* format output for 'hdparm --Istdin' with '-rr' or '-HHH' */
+        if ((2 == op->do_raw) || (3 == op->do_hex))
+            dWordHex((const unsigned short *)(rp + 60), 256, -2,
+                     sg_is_big_endian());
+        else if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            else
+                op->do_long = true;
+            decode_ata_info_vpd(rp, len, op, jo2p);
+        }
+        break;
+    case VPD_POWER_CONDITION:   /* 0x8a   ["pc"] */
+        np = "Power condition VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            decode_power_condition(rp, len, op, jo2p);
+        }
+        break;
+    case VPD_DEVICE_CONSTITUENTS:       /* 0x8b  ["dc"] */
+        np = "Device constituents page VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "constituent_descriptor_list");
+            }
+            decode_dev_constit_vpd(rp, len, op, jap, recurse_vpd_decode);
+        }
+        break;
+    case VPD_CFA_PROFILE_INFO:    /* 0x8c ["cfa"] */
+        np = "CFA profile information VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "%s:\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (op->do_raw)
+                dStrRaw((const char *)rp, len);
+            else {
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                      "cfa_profile_descriptor_list");
+                }
+                decode_cga_profile_vpd(rp, len, op, jap);
+            }
+        }
+        break;
+    case VPD_POWER_CONSUMPTION:   /* 0x8d   ["psm"] */
+        np = "Power consumption VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "power_consumption_descriptor_list");
+            }
+            decode_power_consumption(rp, len, op, jap);
+        }
+        break;
+    case VPD_3PARTY_COPY:       /* 0x8f  ["tpc"] */
+        np = "Third party copy VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "third_party_copy_descriptor_list");
+            }
+            decode_3party_copy_vpd(rp, len, op, jap);
+        }
+        break;
+    case VPD_PROTO_LU:          /* 0x90  ["pslu"] */
+        np = "Protocol specific logical unit information VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "logical_unit_information_descriptor_list");
+            }
+            decode_proto_lu_vpd(rp, len, op, jap);
+        }
+        break;
+    case VPD_PROTO_PORT:        /* 0x91  ["pspo"] */
+        np = "Protocol specific port information VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "port_information_descriptor_list");
+            }
+            decode_proto_port_vpd(rp, len, op, jap);
+        }
+        break;
+    case VPD_SCSI_FEATURE_SETS:         /* 0x92  ["sfs"] */
+        np = "SCSI Feature sets VPD page";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s\n", np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p,
+                                  "feature_set_code_list");
+            }
+            decode_feature_sets_vpd(rp, len, op, jap);
+        }
+        break;
+    case 0xb0:  /* VPD pages in B0h to BFh range depend on pdt */
+        np = NULL;
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool bl = false;
+            bool sad = false;
+            bool oi = false;
+
+            ep = "";
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Block limits VPD page";
+                ep = "(SBC)";
+                bl = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "Sequential-access device capabilities VPD page";
+                ep = "(SSC)";
+                sad = true;
+                break;
+            case PDT_OSD:
+                np = "OSD information VPD page";
+                ep = "(OSD)";
+                oi = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            if (bl)
+                decode_block_limits_vpd(rp, len, op, jo2p);
+            else if (sad || oi)
+                decode_b0_vpd(rp, len, op, jop);
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb0\n");
+        break;
+    case 0xb1:  /* VPD pages in B0h to BFh range depend on pdt */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool bdc = false;
+            static const char * masn =
+                        "Manufactured-assigned serial number VPD page";
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Block device characteristics VPD page";
+                ep = "(SBC)";
+                bdc = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = masn;
+                ep = "(SSC)";
+                break;
+            case PDT_OSD:
+                np = "Security token VPD page";
+                ep = "(OSD)";
+                break;
+            case PDT_ADC:
+                np = masn;
+                ep = "(ADC)";
+                break;
+            default:
+                np = NULL;
+                printf("VPD INQUIRY: page=0x%x, pdt=0x%x\n", 0xb1, pdt);
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            if (bdc)
+                decode_block_dev_ch_vpd(rp, len, op, jo2p);
+            else
+                decode_b1_vpd(rp, len, op, jo2p);
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb1\n");
+        break;
+    case 0xb2:  /* VPD pages in B0h to BFh range depend on pdt */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool lbpv = false;
+            bool tas = false;
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Logical block provisioning VPD page";
+                ep = "(SBC)";
+                lbpv = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "TapeAlert supported flags VPD page";
+                ep = "(SSC)";
+                tas = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            if (lbpv)
+                return decode_block_lb_prov_vpd(rp, len, op, jo2p);
+            else if (tas)
+                decode_tapealert_supported_vpd(rp, len, op, jo2p);
+            else
+                return vpd_mainly_hex(sg_fd, op, NULL, off);
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb2\n");
+        break;
+    case 0xb3:
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool ref = false;
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Referrals VPD page";
+                ep = "(SBC)";
+                ref = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            if (ref)
+                decode_referrals_vpd(rp, len, op, jo2p);
+            else
+                decode_b3_vpd(rp, len, op, jo2p);
+            return 0;
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb3\n");
+        break;
+    case 0xb4:
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool sbl = false;
+            bool dtde = false;
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Supported block lengths and protection types VPD page";
+                ep = "(SBC)";
+                sbl = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "Device transfer data element VPD page";
+                ep = "(SSC)";
+                dtde = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            if (sbl) {
+                if (as_json)
+                    jap = sgj_named_subarray_r(jsp, jo2p, "logical_block_"
+                            "length_and_protection_types_descriptor_list");
+                decode_sup_block_lens_vpd(rp, len, op, jap);
+            } else if (dtde) {
+                if (! jsp->pr_as_json)
+                    hex2stdout(rp + 4, len - 4, 1);
+                sgj_js_nv_hex_bytes(jsp, jop, "device_transfer_data_element",
+                                    rp + 4, len - 4);
+            } else
+                return vpd_mainly_hex(sg_fd, op, NULL, off);
+            return 0;
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb4\n");
+        break;
+    case 0xb5:
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool bdce = false;
+            bool lbp = false;
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Block device characteristics VPD page";
+                ep = "(SBC)";
+                bdce = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "Logical block protection VPD page";
+                ep = "(SSC)";
+                lbp = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            if (bdce)
+                decode_block_dev_char_ext_vpd(rp, len, op, jo2p);
+            else if (lbp) {     /* VPD_LB_PROTECTION  0xb5 ["lbpro"] (SSC) */
+                if (as_json)
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                     "logical_block_protection_method_descriptor_list");
+                decode_lb_protection_vpd(rp, len, op, jap);
+            } else
+                return vpd_mainly_hex(sg_fd, op, NULL, off);
+            return 0;
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb5\n");
+        break;
+    case 0xb6:
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool zbdch = false;
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Zoned block device characteristics VPD page";
+                ep = "(SBC, ZBC)";
+                zbdch = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            if (zbdch)
+                decode_zbdch_vpd(rp, len, op, jo2p);
+            else
+                return vpd_mainly_hex(sg_fd, op, NULL, off);
+            return 0;
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb6\n");
+        break;
+    case 0xb7:
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool ble = false;
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Block limits extension VPD page";
+                ep = "(SBC)";
+                ble = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            if (ble)
+                decode_block_limits_ext_vpd(rp, len, op, jo2p);
+            else
+                return vpd_mainly_hex(sg_fd, op, NULL, off);
+            return 0;
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb7\n");
+        break;
+    case 0xb8:
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool fp = false;
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Format presets VPD page";
+                ep = "(SBC)";
+                fp = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p, "format_preset_"
+                            "descriptor_list");
+            }
+            if (fp)
+                decode_format_presets_vpd(rp, len, op, jap);
+            else
+                return vpd_mainly_hex(sg_fd, op, NULL, off);
+            return 0;
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb8\n");
+        break;
+    case 0xb9:
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool cpr = false;
+
+            if (op->do_raw) {
+                dStrRaw((const char *)rp, len);
+                break;
+            }
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Concurrent positioning LBAs VPD page";
+                ep = "(SBC)";
+                cpr = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (op->do_hex < 3) {
+                if (NULL == np)
+                    sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+                else
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            }
+            if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                jap = sgj_named_subarray_r(jsp, jo2p, "lba_range_"
+                                           "descriptor_list");
+            }
+            if (cpr)
+                decode_con_pos_range_vpd(rp, len, op, jap);
+            else
+                return vpd_mainly_hex(sg_fd, op, NULL, off);
+            return 0;
+        } else if (! op->do_raw)
+            pr2serr("VPD INQUIRY: page=0xb8\n");
+        break;
+    /* Vendor specific VPD pages (>= 0xc0) */
+    case VPD_UPR_EMC:   /* 0xc0 */
+        np = "Unit path report VPD page";
+        ep = "(EMC)";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+        res = vpd_fetch_page(sg_fd, rp, pn, -1, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            decode_upr_vpd_c0_emc(rp, len, op, jo2p);
+        }
+        break;
+    case VPD_RDAC_VERS:         /* 0xc2 */
+        np = "Software Version VPD page";
+        ep = "(RDAC)";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+        res = vpd_fetch_page(sg_fd, rp, pn, -1, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            decode_rdac_vpd_c2(rp, len, op, jo2p);
+        }
+        break;
+    case VPD_RDAC_VAC:          /* 0xc9 */
+        np = "Volume access control VPD page";
+        ep = "(RDAC)";
+        if (!op->do_raw && (op->do_hex < 3))
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+        res = vpd_fetch_page(sg_fd, rp, pn, -1, qt, vb, &len);
+        if (res)
+            break;
+        if (op->do_raw)
+            dStrRaw((const char *)rp, len);
+        else {
+            if (as_json)
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+            decode_rdac_vpd_c9(rp, len, op, jo2p);
+        }
+        break;
+    case SG_NVME_VPD_NICR:          /* 0xde */
+        np = "NVMe Identify Controller Response VPD page";
+        /* NVMe: Identify Controller data structure (CNS 01h) */
+        ep = "(sg3_utils)";
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (res) {
+            sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+            break;
+        }
+        if (op->do_raw) {
+            dStrRaw((const char *)rp, len);
+            break;
+        }
+        pdt = rp[0] & PDT_MASK;
+        if (op->do_hex < 3) {
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else
+                sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+        }
+        if (len < 16) {
+            pr2serr("%s expected to be > 15 bytes long (got: %d)\n", ep, len);
+            break;
+        } else {
+            int n = len - 16;
+
+            if (n > 4096) {
+                pr2serr("NVMe Identify response expected to be <= 4096 "
+                        "bytes (got: %d)\n", n);
+                break;
+            }
+            if (op->do_hex)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (as_json) {
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                sgj_js_nv_hex_bytes(jsp, jo2p, "response_bytes", rp + 16, n);
+            } else
+                hex2stdout(rp + 16, n, 1);
+        }
+        break;
+    default:
+        bad = true;
+        break;
+    }
+    if (bad) {
+        if ((pn > 0) && (pn < 0x80)) {
+            if (!op->do_raw && (op->do_hex < 3))
+                printf("VPD INQUIRY: ASCII information page, FRU code=0x%x\n",
+                       pn);
+            res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+            if (0 == res) {
+                if (op->do_raw)
+                    dStrRaw((const char *)rp, len);
+                else
+                    decode_ascii_inf(rp, len, op);
+            }
+        } else {
+            if (op->do_hex < 3)
+                pr2serr(" Only hex output supported.\n");
+            return vpd_mainly_hex(sg_fd, op, NULL, off);
+        }
+    }
+out:
+    if (res) {
+        char b[80];
+
+        if (SG_LIB_CAT_ILLEGAL_REQ == res)
+            pr2serr("    inquiry: field in cdb illegal (page not "
+                    "supported)\n");
+        else {
+            sg_get_category_sense_str(res, sizeof(b), b, vb);
+            pr2serr("    inquiry: %s\n", b);
+        }
+    }
+    return res;
+}
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+
+static void
+nvme_hex_raw(const uint8_t * b, int b_len, const struct opts_t * op)
+{
+    if (op->do_raw)
+        dStrRaw((const char *)b, b_len);
+    else if (op->do_hex) {
+        if (op->do_hex < 3) {
+            printf("data_in buffer:\n");
+            hex2stdout(b, b_len, (2 == op->do_hex));
+        } else
+            hex2stdout(b, b_len, -1);
+    }
+}
+
+static const char * rperf[] = {"Best", "Better", "Good", "Degraded"};
+
+static void
+show_nvme_id_ns(const uint8_t * dinp, int do_long)
+{
+    bool got_eui_128 = false;
+    uint32_t u, k, off, num_lbaf, flbas, flba_info, md_size, lb_size;
+    uint64_t ns_sz, eui_64;
+
+    num_lbaf = dinp[25] + 1;  /* spec says this is "0's based value" */
+    flbas = dinp[26] & 0xf;   /* index of active LBA format (for this ns) */
+    ns_sz = sg_get_unaligned_le64(dinp + 0);
+    eui_64 = sg_get_unaligned_be64(dinp + 120);  /* N.B. big endian */
+    if (! sg_all_zeros(dinp + 104, 16))
+        got_eui_128 = true;
+    printf("    Namespace size/capacity: %" PRIu64 "/%" PRIu64
+           " blocks\n", ns_sz, sg_get_unaligned_le64(dinp + 8));
+    printf("    Namespace utilization: %" PRIu64 " blocks\n",
+           sg_get_unaligned_le64(dinp + 16));
+    if (got_eui_128) {          /* N.B. big endian */
+        printf("    NGUID: 0x%02x", dinp[104]);
+        for (k = 1; k < 16; ++k)
+            printf("%02x", dinp[104 + k]);
+        printf("\n");
+    } else if (do_long)
+        printf("    NGUID: 0x0\n");
+    if (eui_64)
+        printf("    EUI-64: 0x%" PRIx64 "\n", eui_64); /* N.B. big endian */
+    printf("    Number of LBA formats: %u\n", num_lbaf);
+    printf("    Index LBA size: %u\n", flbas);
+    for (k = 0, off = 128; k < num_lbaf; ++k, off += 4) {
+        printf("    LBA format %u support:", k);
+        if (k == flbas)
+            printf(" <-- active\n");
+        else
+            printf("\n");
+        flba_info = sg_get_unaligned_le32(dinp + off);
+        md_size = flba_info & 0xffff;
+        lb_size = flba_info >> 16 & 0xff;
+        if (lb_size > 31) {
+            pr2serr("%s: logical block size exponent of %u implies a LB "
+                    "size larger than 4 billion bytes, ignore\n", __func__,
+                    lb_size);
+            continue;
+        }
+        lb_size = 1U << lb_size;
+        ns_sz *= lb_size;
+        ns_sz /= 500*1000*1000;
+        if (ns_sz & 0x1)
+            ns_sz = (ns_sz / 2) + 1;
+        else
+            ns_sz = ns_sz / 2;
+        u = (flba_info >> 24) & 0x3;
+        printf("      Logical block size: %u bytes\n", lb_size);
+        printf("      Approximate namespace size: %" PRIu64 " GB\n", ns_sz);
+        printf("      Metadata size: %u bytes\n", md_size);
+        printf("      Relative performance: %s [0x%x]\n", rperf[u], u);
+    }
+}
+
+/* Send Identify(CNS=0, nsid) and decode the Identify namespace response */
+static int
+nvme_id_namespace(struct sg_pt_base * ptvp, uint32_t nsid,
+                  struct sg_nvme_passthru_cmd * id_cmdp, uint8_t * id_dinp,
+                  int id_din_len, const struct opts_t * op)
+{
+    int ret = 0;
+    int vb = op->verbose;
+    uint8_t resp[16];
+
+    clear_scsi_pt_obj(ptvp);
+    id_cmdp->nsid = nsid;
+    id_cmdp->cdw10 = 0x0;       /* CNS=0x0 Identify NS (CNTID=0) */
+    id_cmdp->cdw11 = 0x0;       /* NVMSETID=0 (only valid when CNS=0x4) */
+    id_cmdp->cdw14 = 0x0;       /* UUID index (assume not supported) */
+    set_scsi_pt_data_in(ptvp, id_dinp, id_din_len);
+    set_scsi_pt_sense(ptvp, resp, sizeof(resp));
+    set_scsi_pt_cdb(ptvp, (const uint8_t *)id_cmdp, sizeof(*id_cmdp));
+    ret = do_scsi_pt(ptvp, -1, 0 /* timeout (def: 1 min) */, vb);
+    if (vb > 2)
+        pr2serr("%s: do_scsi_pt() result is %d\n", __func__, ret);
+    if (ret) {
+        if (SCSI_PT_DO_BAD_PARAMS == ret)
+            ret = SG_LIB_SYNTAX_ERROR;
+        else if (SCSI_PT_DO_TIMEOUT == ret)
+            ret = SG_LIB_CAT_TIMEOUT;
+        else if (ret < 0)
+            ret = sg_convert_errno(-ret);
+        return ret;
+    }
+    if (op->do_hex || op->do_raw) {
+        nvme_hex_raw(id_dinp, id_din_len, op);
+        return 0;
+    }
+    show_nvme_id_ns(id_dinp, op->do_long);
+    return 0;
+}
+
+static void
+show_nvme_id_ctrl(const uint8_t *dinp, const char *dev_name, int do_long)
+{
+    bool got_fguid;
+    uint8_t ver_min, ver_ter, mtds;
+    uint16_t ver_maj, oacs, oncs;
+    uint32_t k, ver, max_nsid, npss, j, n, m;
+    uint64_t sz1, sz2;
+    const uint8_t * up;
+
+    max_nsid = sg_get_unaligned_le32(dinp + 516); /* NN */
+    printf("Identify controller for %s:\n", dev_name);
+    printf("  Model number: %.40s\n", (const char *)(dinp + 24));
+    printf("  Serial number: %.20s\n", (const char *)(dinp + 4));
+    printf("  Firmware revision: %.8s\n", (const char *)(dinp + 64));
+    ver = sg_get_unaligned_le32(dinp + 80);
+    ver_maj = (ver >> 16);
+    ver_min = (ver >> 8) & 0xff;
+    ver_ter = (ver & 0xff);
+    printf("  Version: %u.%u", ver_maj, ver_min);
+    if ((ver_maj > 1) || ((1 == ver_maj) && (ver_min > 2)) ||
+        ((1 == ver_maj) && (2 == ver_min) && (ver_ter > 0)))
+        printf(".%u\n", ver_ter);
+    else
+        printf("\n");
+    oacs = sg_get_unaligned_le16(dinp + 256);
+    if (0x1ff & oacs) {
+        printf("  Optional admin command support:\n");
+        if (0x200 & oacs)
+            printf("    Get LBA status\n");     /* NVMe 1.4 */
+        if (0x100 & oacs)
+            printf("    Doorbell buffer config\n");
+        if (0x80 & oacs)
+            printf("    Virtualization management\n");
+        if (0x40 & oacs)
+            printf("    NVMe-MI send and NVMe-MI receive\n");
+        if (0x20 & oacs)
+            printf("    Directive send and directive receive\n");
+        if (0x10 & oacs)
+            printf("    Device self-test\n");
+        if (0x8 & oacs)
+            printf("    Namespace management and attachment\n");
+        if (0x4 & oacs)
+            printf("    Firmware download and commit\n");
+        if (0x2 & oacs)
+            printf("    Format NVM\n");
+        if (0x1 & oacs)
+            printf("    Security send and receive\n");
+    } else
+        printf("  No optional admin command support\n");
+    oncs = sg_get_unaligned_le16(dinp + 256);
+    if (0x7f & oncs) {
+        printf("  Optional NVM command support:\n");
+        if (0x80 & oncs)
+            printf("    Verify\n");     /* NVMe 1.4 */
+        if (0x40 & oncs)
+            printf("    Timestamp feature\n");
+        if (0x20 & oncs)
+            printf("    Reservations\n");
+        if (0x10 & oncs)
+            printf("    Save and Select fields non-zero\n");
+        if (0x8 & oncs)
+            printf("    Write zeroes\n");
+        if (0x4 & oncs)
+            printf("    Dataset management\n");
+        if (0x2 & oncs)
+            printf("    Write uncorrectable\n");
+        if (0x1 & oncs)
+            printf("    Compare\n");
+    } else
+        printf("  No optional NVM command support\n");
+    printf("  PCI vendor ID VID/SSVID: 0x%x/0x%x\n",
+           sg_get_unaligned_le16(dinp + 0),
+           sg_get_unaligned_le16(dinp + 2));
+    printf("  IEEE OUI Identifier: 0x%x\n",  /* this has been renamed AOI */
+           sg_get_unaligned_le24(dinp + 73));
+    got_fguid = ! sg_all_zeros(dinp + 112, 16);
+    if (got_fguid) {
+        printf("  FGUID: 0x%02x", dinp[112]);
+        for (k = 1; k < 16; ++k)
+            printf("%02x", dinp[112 + k]);
+        printf("\n");
+    } else if (do_long)
+        printf("  FGUID: 0x0\n");
+    printf("  Controller ID: 0x%x\n", sg_get_unaligned_le16(dinp + 78));
+    if (do_long) {      /* Bytes 240 to 255 are reserved for NVME-MI */
+        printf("  NVMe Management Interface [MI] settings:\n");
+        printf("    Enclosure: %d [NVMEE]\n", !! (0x2 & dinp[253]));
+        printf("    NVMe Storage device: %d [NVMESD]\n",
+               !! (0x1 & dinp[253]));
+        printf("    Management endpoint capabilities, over a PCIe port: %d "
+               "[PCIEME]\n",
+               !! (0x2 & dinp[255]));
+        printf("    Management endpoint capabilities, over a SMBus/I2C port: "
+               "%d [SMBUSME]\n", !! (0x1 & dinp[255]));
+    }
+    printf("  Number of namespaces: %u\n", max_nsid);
+    sz1 = sg_get_unaligned_le64(dinp + 280);  /* lower 64 bits */
+    sz2 = sg_get_unaligned_le64(dinp + 288);  /* upper 64 bits */
+    if (sz2)
+        printf("  Total NVM capacity: huge ...\n");
+    else if (sz1)
+        printf("  Total NVM capacity: %" PRIu64 " bytes\n", sz1);
+    mtds = dinp[77];
+    printf("  Maximum data transfer size: ");
+    if (mtds)
+        printf("%u pages\n", 1U << mtds);
+    else
+        printf("<unlimited>\n");
+
+    if (do_long) {
+        const char * const non_op = "does not process I/O";
+        const char * const operat = "processes I/O";
+        const char * cp;
+
+        printf("  Total NVM capacity: 0 bytes\n");
+        npss = dinp[263] + 1;
+        up = dinp + 2048;
+        for (k = 0; k < npss; ++k, up += 32) {
+            n = sg_get_unaligned_le16(up + 0);
+            n *= (0x1 & up[3]) ? 1 : 100;    /* unit: 100 microWatts */
+            j = n / 10;                      /* unit: 1 milliWatts */
+            m = j % 1000;
+            j /= 1000;
+            cp = (0x2 & up[3]) ? non_op : operat;
+            printf("  Power state %u: Max power: ", k);
+            if (0 == j) {
+                m = n % 10;
+                n /= 10;
+                printf("%u.%u milliWatts, %s\n", n, m, cp);
+            } else
+                printf("%u.%03u Watts, %s\n", j, m, cp);
+            n = sg_get_unaligned_le32(up + 4);
+            if (0 == n)
+                printf("    [ENLAT], ");
+            else
+                printf("    ENLAT=%u, ", n);
+            n = sg_get_unaligned_le32(up + 8);
+            if (0 == n)
+                printf("[EXLAT], ");
+            else
+                printf("EXLAT=%u, ", n);
+            n = 0x1f & up[12];
+            printf("RRT=%u, ", n);
+            n = 0x1f & up[13];
+            printf("RRL=%u, ", n);
+            n = 0x1f & up[14];
+            printf("RWT=%u, ", n);
+            n = 0x1f & up[15];
+            printf("RWL=%u\n", n);
+        }
+    }
+}
+
+/* Send a NVMe Identify(CNS=1) and decode Controller info. If the
+ * device name includes a namespace indication (e.g. /dev/nvme0ns1) then
+ * an Identify namespace command is sent to that namespace (e.g. 1). If the
+ * device name does not contain a namespace indication (e.g. /dev/nvme0)
+ * and --only is not given then nvme_id_namespace() is sent for each
+ * namespace in the controller. Namespaces number sequentially starting at
+ * 1 . The CNS (Controller or Namespace Structure) field is CDW10 7:0, was
+ * only bit 0 in NVMe 1.0 and bits 1:0 in NVMe 1.1, thereafter 8 bits. */
+static int
+do_nvme_identify_ctrl(int pt_fd, const struct opts_t * op)
+{
+    int ret = 0;
+    int vb = op->verbose;
+    uint32_t k, nsid, max_nsid;
+    struct sg_pt_base * ptvp;
+    struct sg_nvme_passthru_cmd identify_cmd;
+    struct sg_nvme_passthru_cmd * id_cmdp = &identify_cmd;
+    uint8_t * id_dinp = NULL;
+    uint8_t * free_id_dinp = NULL;
+    const uint32_t pg_sz = sg_get_page_size();
+    uint8_t resp[16];
+
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+    ptvp = construct_scsi_pt_obj_with_fd(pt_fd, vb);
+    if (NULL == ptvp) {
+        pr2serr("%s: memory problem\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    memset(id_cmdp, 0, sizeof(*id_cmdp));
+    id_cmdp->opcode = 0x6;
+    nsid = get_pt_nvme_nsid(ptvp);
+    id_cmdp->cdw10 = 0x1;       /* CNS=0x1 --> Identify controller */
+    /* id_cmdp->nsid is a "don't care" when CNS=1, so leave as 0 */
+    id_dinp = sg_memalign(pg_sz, pg_sz, &free_id_dinp, false);
+    if (NULL == id_dinp) {
+        pr2serr("%s: sg_memalign problem\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    set_scsi_pt_data_in(ptvp, id_dinp, pg_sz);
+    set_scsi_pt_cdb(ptvp, (const uint8_t *)id_cmdp, sizeof(*id_cmdp));
+    set_scsi_pt_sense(ptvp, resp, sizeof(resp));
+    ret = do_scsi_pt(ptvp, -1, 0 /* timeout (def: 1 min) */, vb);
+    if (vb > 2)
+        pr2serr("%s: do_scsi_pt result is %d\n", __func__, ret);
+    if (ret) {
+        if (SCSI_PT_DO_BAD_PARAMS == ret)
+            ret = SG_LIB_SYNTAX_ERROR;
+        else if (SCSI_PT_DO_TIMEOUT == ret)
+            ret = SG_LIB_CAT_TIMEOUT;
+        else if (ret < 0)
+            ret = sg_convert_errno(-ret);
+        goto err_out;
+    }
+    max_nsid = sg_get_unaligned_le32(id_dinp + 516); /* NN */
+    if (op->do_raw || op->do_hex) {
+        if (op->do_only || (SG_NVME_CTL_NSID == nsid ) ||
+            (SG_NVME_BROADCAST_NSID == nsid)) {
+            nvme_hex_raw(id_dinp, pg_sz, op);
+            goto fini;
+        }
+        goto skip1;
+    }
+    show_nvme_id_ctrl(id_dinp, op->device_name, op->do_long);
+skip1:
+    if (op->do_only)
+        goto fini;
+    if (nsid > 0) {
+        if (! (op->do_raw || (op->do_hex > 2))) {
+            printf("  Namespace %u (deduced from device name):\n", nsid);
+            if (nsid > max_nsid)
+                pr2serr("NSID from device (%u) should not exceed number of "
+                        "namespaces (%u)\n", nsid, max_nsid);
+        }
+        ret = nvme_id_namespace(ptvp, nsid, id_cmdp, id_dinp, pg_sz, op);
+        if (ret)
+            goto err_out;
+
+    } else {        /* nsid=0 so char device; loop over all namespaces */
+        for (k = 1; k <= max_nsid; ++k) {
+            if ((! op->do_raw) || (op->do_hex < 3))
+                printf("  Namespace %u (of %u):\n", k, max_nsid);
+            ret = nvme_id_namespace(ptvp, k, id_cmdp, id_dinp, pg_sz, op);
+            if (ret)
+                goto err_out;
+            if (op->do_raw || op->do_hex)
+                goto fini;
+        }
+    }
+fini:
+    ret = 0;
+err_out:
+    destruct_scsi_pt_obj(ptvp);
+    free(free_id_dinp);
+    return ret;
+}
+#endif          /* (HAVE_NVME && (! IGNORE_NVME)) */
+
+
+int
+main(int argc, char * argv[])
+{
+    bool as_json;
+    int res, n, err;
+    int sg_fd = -1;
+    int ret = 0;
+    int subvalue = 0;
+    int inhex_len = 0;
+    int inraw_len = 0;
+    const char * cp;
+    const struct svpd_values_name_t * vnp;
+    sgj_state * jsp;
+    sgj_opaque_p jop = NULL;
+    struct opts_t opts SG_C_CPP_ZERO_INIT;
+    struct opts_t * op;
+
+    op = &opts;
+    op->invoker = SG_VPD_INV_SG_INQ;
+    op->vpd_pn = -1;
+    op->vend_prod_num = -1;
+    op->page_pdt = -1;
+    op->do_block = -1;         /* use default for OS */
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return SG_LIB_SYNTAX_ERROR;
+    if (op->do_help) {
+        usage_for(op);
+        if (op->do_help > 1) {
+            pr2serr("\n>>> Available VPD page abbreviations:\n");
+            enumerate_vpds();
+        }
+        return 0;
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+    jsp = &op->json_st;
+    as_json = jsp->pr_as_json;
+    if (op->page_str) {
+        if (op->vpd_pn >= 0) {
+            pr2serr("Given '-p' option and another option that "
+                    "implies a page\n");
+            return SG_LIB_CONTRADICT;
+        }
+        if ('-' == op->page_str[0])
+            op->vpd_pn = VPD_NOPE_WANT_STD_INQ;
+        else if (isalpha((uint8_t)op->page_str[0])) {
+            vnp = sdp_find_vpd_by_acron(op->page_str);
+            if (NULL == vnp) {
+#ifdef SG_SCSI_STRINGS
+                if (op->opt_new)
+                    pr2serr("abbreviation %s given to '--page=' "
+                            "not recognized\n", op->page_str);
+                else
+                    pr2serr("abbreviation %s given to '-p=' "
+                            "not recognized\n", op->page_str);
+#else
+                pr2serr("abbreviation %s given to '--page=' "
+                        "not recognized\n", op->page_str);
+#endif
+                pr2serr(">>> Available abbreviations:\n");
+                enumerate_vpds();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            // if ((1 != op->do_hex) && (0 == op->do_raw))
+            if (0 == op->do_raw)
+                op->do_decode = true;
+            op->vpd_pn = vnp->value;
+            subvalue = vnp->subvalue;
+            op->page_pdt = vnp->pdt;
+        } else {
+            cp = strchr(op->page_str, ',');
+            if (cp && op->vend_prod) {
+                pr2serr("the --page=pg,vp and the --vendor=vp forms overlap, "
+                        "choose one or the other\n");
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto err_out;
+            }
+            op->vpd_pn = sg_get_num_nomult(op->page_str);
+            if ((op->vpd_pn < 0) || (op->vpd_pn > 255)) {
+                pr2serr("Bad page code value after '-p' option\n");
+                printf("Available standard VPD pages:\n");
+                enumerate_vpds(/* 1, 1 */);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto err_out;
+            }
+            if (cp) {
+                if (isdigit((uint8_t)*(cp + 1)))
+                    op->vend_prod_num = sg_get_num_nomult(cp + 1);
+                else
+                    op->vend_prod_num = svpd_find_vp_num_by_acron(cp + 1);
+                if ((op->vend_prod_num < 0) || (op->vend_prod_num > 255)) {
+                    pr2serr("Bad vendor/product acronym after comma in '-p' "
+                            "option\n");
+                    if (op->vend_prod_num < 0)
+                        svpd_enumerate_vendor(-1);
+                    ret = SG_LIB_SYNTAX_ERROR;
+                    goto err_out;
+                }
+                subvalue = op->vend_prod_num;
+            } else if (op->vend_prod) {
+                if (isdigit((uint8_t)op->vend_prod[0]))
+                    op->vend_prod_num = sg_get_num_nomult(op->vend_prod);
+                else
+                    op->vend_prod_num =
+                        svpd_find_vp_num_by_acron(op->vend_prod);
+                if ((op->vend_prod_num < 0) || (op->vend_prod_num > 255)) {
+                    pr2serr("Bad vendor/product acronym after '--vendor=' "
+                            "option\n");
+                    svpd_enumerate_vendor(-1);
+                    ret = SG_LIB_SYNTAX_ERROR;
+                    goto err_out;
+                }
+                subvalue = op->vend_prod_num;
+            }
+        }
+        if (op->verbose > 3)
+           pr2serr("'--page=' matched pn=%d [0x%x], subvalue=%d\n",
+                   op->vpd_pn, op->vpd_pn, subvalue);
+#if 0
+        else {
+#ifdef SG_SCSI_STRINGS
+            if (op->opt_new) {
+                n = sg_get_num(op->page_str);
+                if ((n < 0) || (n > 255)) {
+                    pr2serr("Bad argument to '--page=', "
+                            "expecting 0 to 255 inclusive\n");
+                    usage_for(op);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                if ((1 != op->do_hex) && (0 == op->do_raw))
+                    op->do_decode = true;
+            } else {
+                int num;
+                unsigned int u;
+
+                num = sscanf(op->page_str, "%x", &u);
+                if ((1 != num) || (u > 255)) {
+                    pr2serr("Inappropriate value after '-o=' "
+                            "or '-p=' option\n");
+                    usage_for(op);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                n = u;
+            }
+#else
+            n = sg_get_num(op->page_str);
+            if ((n < 0) || (n > 255)) {
+                pr2serr("Bad argument to '--page=', "
+                        "expecting 0 to 255 inclusive\n");
+                usage_for(op);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((1 != op->do_hex) && (0 == op->do_raw))
+                op->do_decode = true;
+#endif /* SG_SCSI_STRINGS */
+            op->vpd_pn = n;
+        }
+#endif
+    } else if (op->vend_prod) {
+        if (isdigit((uint8_t)op->vend_prod[0]))
+            op->vend_prod_num = sg_get_num_nomult(op->vend_prod);
+        else
+            op->vend_prod_num = svpd_find_vp_num_by_acron(op->vend_prod);
+        if ((op->vend_prod_num < 0) || (op->vend_prod_num > 255)) {
+            pr2serr("Bad vendor/product acronym after '--vendor=' "
+                    "option\n");
+            svpd_enumerate_vendor(-1);
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto err_out;
+        }
+        subvalue = op->vend_prod_num;
+    }
+    if (as_json)
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+
+    rsp_buff = sg_memalign(rsp_buff_sz, 0 /* page align */, &free_rsp_buff,
+                           false);
+    if (NULL == rsp_buff) {
+        pr2serr("Unable to allocate %d bytes on heap\n", rsp_buff_sz);
+        return sg_convert_errno(ENOMEM);
+    }
+    if (op->sinq_inraw_fn) {
+        if (op->do_cmddt) {
+            pr2serr("Don't support --cmddt with --sinq-inraw= option\n");
+            ret = SG_LIB_CONTRADICT;
+            goto err_out;
+        }
+        if ((ret = sg_f2hex_arr(op->sinq_inraw_fn, true, false, rsp_buff,
+                                &inraw_len, rsp_buff_sz))) {
+            goto err_out;
+        }
+        if (inraw_len < 36) {
+            pr2serr("Unable to read 36 or more bytes from %s\n",
+                    op->sinq_inraw_fn);
+            ret = SG_LIB_FILE_ERROR;
+            goto err_out;
+        }
+        memcpy(op->std_inq_a,  rsp_buff, 36);
+        op->std_inq_a_valid = true;
+    }
+    if (op->inhex_fn) {
+        if (op->device_name) {
+            pr2serr("Cannot have both a DEVICE and --inhex= option\n");
+            ret = SG_LIB_CONTRADICT;
+            goto err_out;
+        }
+        if (op->do_cmddt) {
+            pr2serr("Don't support --cmddt with --inhex= option\n");
+            ret = SG_LIB_CONTRADICT;
+            goto err_out;
+        }
+        err = sg_f2hex_arr(op->inhex_fn, !!op->do_raw, false, rsp_buff,
+                           &inhex_len, rsp_buff_sz);
+        if (err) {
+            if (err < 0)
+                err = sg_convert_errno(-err);
+            ret = err;
+            goto err_out;
+        }
+        op->do_raw = 0;         /* don't want raw on output with --inhex= */
+        if (-1 == op->vpd_pn) {       /* may be able to deduce VPD page */
+            if (op->page_pdt < 0)
+                op->page_pdt = PDT_MASK & rsp_buff[0];
+            if ((0x2 == (0xf & rsp_buff[3])) && (rsp_buff[2] > 2)) {
+                if (op->verbose)
+                    pr2serr("Guessing from --inhex= this is a standard "
+                            "INQUIRY\n");
+            } else if (rsp_buff[2] <= 2) {
+                /*
+                 * Removable devices have the RMB bit set, which would
+                 * present itself as vpd page 0x80 output if we're not
+                 * careful
+                 *
+                 * Serial number must be right-aligned ASCII data in
+                 * bytes 5-7; standard INQUIRY will have flags here.
+                 */
+                if (rsp_buff[1] == 0x80 &&
+                    (rsp_buff[5] < 0x20 || rsp_buff[5] > 0x80 ||
+                     rsp_buff[6] < 0x20 || rsp_buff[6] > 0x80 ||
+                     rsp_buff[7] < 0x20 || rsp_buff[7] > 0x80)) {
+                    if (op->verbose)
+                        pr2serr("Guessing from --inhex= this is a "
+                                "standard INQUIRY\n");
+                } else {
+                    if (op->verbose)
+                        pr2serr("Guessing from --inhex= this is VPD "
+                                "page 0x%x\n", rsp_buff[1]);
+                    op->vpd_pn = rsp_buff[1];
+                    op->do_vpd = true;
+                    if ((1 != op->do_hex) && (0 == op->do_raw))
+                        op->do_decode = true;
+                }
+            } else {
+                if (op->verbose)
+                    pr2serr("page number unclear from --inhex, hope it's a "
+                            "standard INQUIRY\n");
+            }
+        } else
+            op->do_vpd = true;
+        if (op->do_vpd) {   /* Allow for multiple VPD pages from 'sg_vpd -a' */
+            op->maxlen = inhex_len;
+            ret = svpd_inhex_decode_all(op, jop);
+            goto fini2;
+        }
+    } else if (0 == op->device_name) {
+        pr2serr("No DEVICE argument given\n\n");
+        usage_for(op);
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto err_out;
+    }
+    if (VPD_NOPE_WANT_STD_INQ == op->vpd_pn)
+        op->vpd_pn = -1;  /* now past guessing, set to normal indication */
+
+    if (op->do_export) {
+        if (op->vpd_pn != -1) {
+            if (op->vpd_pn != VPD_DEVICE_ID &&
+                op->vpd_pn != VPD_UNIT_SERIAL_NUM) {
+                pr2serr("Option '--export' only supported for VPD pages 0x80 "
+                        "and 0x83\n");
+                usage_for(op);
+                ret = SG_LIB_CONTRADICT;
+                goto err_out;
+            }
+            op->do_decode = true;
+            op->do_vpd = true;
+        }
+    }
+
+    if ((0 == op->do_cmddt) && (op->vpd_pn >= 0) && op->page_given)
+        op->do_vpd = true;
+
+    if (op->do_raw && op->do_hex) {
+        pr2serr("Can't do hex and raw at the same time\n");
+        usage_for(op);
+        ret = SG_LIB_CONTRADICT;
+        goto err_out;
+    }
+    if (op->do_vpd && op->do_cmddt) {
+#ifdef SG_SCSI_STRINGS
+        if (op->opt_new)
+            pr2serr("Can't use '--cmddt' with VPD pages\n");
+        else
+            pr2serr("Can't have both '-e' and '-c' (or '-cl')\n");
+#else
+        pr2serr("Can't use '--cmddt' with VPD pages\n");
+#endif
+        usage_for(op);
+        ret = SG_LIB_CONTRADICT;
+        goto err_out;
+    }
+    if (((op->do_vpd || op->do_cmddt)) && (op->vpd_pn < 0))
+        op->vpd_pn = 0;
+    if (op->num_pages > 1) {
+        pr2serr("Can only fetch one page (VPD or Cmd) at a time\n");
+        usage_for(op);
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto err_out;
+    }
+    if (op->do_descriptors) {
+        if ((op->maxlen > 0) && (op->maxlen < 60)) {
+            pr2serr("version descriptors need INQUIRY response "
+                    "length >= 60 bytes\n");
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto err_out;
+        }
+        if (op->do_vpd || op->do_cmddt) {
+            pr2serr("version descriptors require standard INQUIRY\n");
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto err_out;
+        }
+    }
+    if (op->num_pages && op->do_ata) {
+        pr2serr("Can't use '-A' with an explicit decode VPD page option\n");
+        ret = SG_LIB_CONTRADICT;
+        goto err_out;
+    }
+
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto err_out;
+        }
+    }
+    if (op->inhex_fn) {
+        if (op->do_vpd) {
+            if (op->do_decode)
+                ret = vpd_decode(-1, op, jop, 0);
+            else
+                ret = vpd_mainly_hex(-1, op, NULL, 0);
+            goto err_out;
+        }
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+        else if (op->do_ata) {
+            prepare_ata_identify(op, inhex_len);
+            ret = 0;
+            goto err_out;
+        }
+#endif
+        else {
+            op->maxlen = inhex_len;
+            ret = std_inq_process(-1, op, jop, 0);
+            goto err_out;
+        }
+    }
+
+#if defined(O_NONBLOCK) && defined(O_RDONLY)
+    if (op->do_block >= 0) {
+        n = O_RDONLY | (op->do_block ? 0 : O_NONBLOCK);
+        if ((sg_fd = sg_cmds_open_flags(op->device_name, n,
+                                        op->verbose)) < 0) {
+            pr2serr("sg_inq: error opening file: %s: %s\n",
+                    op->device_name, safe_strerror(-sg_fd));
+            ret = sg_convert_errno(-sg_fd);
+            if (ret < 0)
+                ret = SG_LIB_FILE_ERROR;
+            goto err_out;
+        }
+
+    } else {
+        if ((sg_fd = sg_cmds_open_device(op->device_name, true /* ro */,
+                                         op->verbose)) < 0) {
+            pr2serr("sg_inq: error opening file: %s: %s\n",
+                    op->device_name, safe_strerror(-sg_fd));
+            ret = sg_convert_errno(-sg_fd);
+            if (ret < 0)
+                ret = SG_LIB_FILE_ERROR;
+            goto err_out;
+        }
+    }
+#else
+    if ((sg_fd = sg_cmds_open_device(op->device_name, true /* ro */,
+                                     op->verbose)) < 0) {
+        pr2serr("sg_inq: error opening file: %s: %s\n",
+                op->device_name, safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        if (ret < 0)
+            ret = SG_LIB_FILE_ERROR;
+        goto err_out;
+    }
+#endif
+    memset(rsp_buff, 0, rsp_buff_sz);
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+    n = check_pt_file_handle(sg_fd, op->device_name, op->verbose);
+    if (op->verbose > 1)
+        pr2serr("check_pt_file_handle()-->%d, page_given: %s\n", n,
+                (op->page_given ? "yes" : "no"));
+    if (n > 2) {   /* NVMe char or NVMe block */
+        op->possible_nvme = true;
+        if (! op->page_given) {
+            ret = do_nvme_identify_ctrl(sg_fd, op);
+            goto fini2;
+        }
+    }
+#endif
+
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+    if (op->do_ata) {
+        res = try_ata_identify(sg_fd, op->do_hex, op->do_raw,
+                               op->verbose);
+        if (0 != res) {
+            pr2serr("fetching ATA information failed on %s\n",
+                    op->device_name);
+            ret = SG_LIB_CAT_OTHER;
+        } else
+            ret = 0;
+        goto fini3;
+    }
+#endif
+
+    if ((! op->do_cmddt) && (! op->do_vpd)) {
+        /* So it's a standard INQUIRY, try ATA IDENTIFY if that fails */
+        ret = std_inq_process(sg_fd, op, jop, 0);
+        if (ret)
+            goto err_out;
+    } else if (op->do_cmddt) {
+        if (op->vpd_pn < 0)
+            op->vpd_pn = 0;
+        ret = cmddt_process(sg_fd, op);
+        if (ret)
+            goto err_out;
+    } else if (op->do_vpd) {
+        if (op->do_decode) {
+            ret = vpd_decode(sg_fd, op, jop, 0);
+            if (ret)
+                goto err_out;
+        } else {
+            ret = vpd_mainly_hex(sg_fd, op, NULL, 0);
+            if (ret)
+                goto err_out;
+        }
+    }
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+fini2:
+#endif
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+fini3:
+#endif
+
+err_out:
+    if (free_rsp_buff)
+        free(free_rsp_buff);
+    if ((0 == op->verbose) && (! op->do_export)) {
+        if (! sg_if_can2stderr("sg_inq failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    res = (sg_fd >= 0) ? sg_cmds_close_device(sg_fd) : 0;
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+    ret = (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+    if (as_json) {
+        if (0 == op->do_hex)
+            sgj_js2file(jsp, NULL, ret, stdout);
+        sgj_finish(jsp);
+    }
+    return ret;
+}
+
+
+#if defined(SG_LIB_LINUX) && defined(SG_SCSI_STRINGS) && \
+    defined(HDIO_GET_IDENTITY)
+/* Following code permits ATA IDENTIFY commands to be performed on
+   ATA non "Packet Interface" devices (e.g. ATA disks).
+   GPL-ed code borrowed from smartmontools (smartmontools.sf.net).
+   Copyright (C) 2002-4 Bruce Allen
+                <smartmontools-support@lists.sourceforge.net>
+ */
+#ifndef ATA_IDENTIFY_DEVICE
+#define ATA_IDENTIFY_DEVICE 0xec
+#define ATA_IDENTIFY_PACKET_DEVICE 0xa1
+#endif
+#ifndef HDIO_DRIVE_CMD
+#define HDIO_DRIVE_CMD    0x031f
+#endif
+
+/* Needed parts of the ATA DRIVE IDENTIFY Structure. Those labeled
+ * word* are NOT used.
+ */
+struct ata_identify_device {
+  unsigned short words000_009[10];
+  uint8_t  serial_no[20];
+  unsigned short words020_022[3];
+  uint8_t  fw_rev[8];
+  uint8_t  model[40];
+  unsigned short words047_079[33];
+  unsigned short major_rev_num;
+  unsigned short minor_rev_num;
+  unsigned short command_set_1;
+  unsigned short command_set_2;
+  unsigned short command_set_extension;
+  unsigned short cfs_enable_1;
+  unsigned short word086;
+  unsigned short csf_default;
+  unsigned short words088_255[168];
+};
+
+#define ATA_IDENTIFY_BUFF_SZ  sizeof(struct ata_identify_device)
+#define HDIO_DRIVE_CMD_OFFSET 4
+
+static int
+ata_command_interface(int device, char *data, bool * atapi_flag, int verbose)
+{
+    int err;
+    uint8_t buff[ATA_IDENTIFY_BUFF_SZ + HDIO_DRIVE_CMD_OFFSET];
+    unsigned short get_ident[256];
+
+    if (atapi_flag)
+        *atapi_flag = false;
+    memset(buff, 0, sizeof(buff));
+    if (ioctl(device, HDIO_GET_IDENTITY, &get_ident) < 0) {
+        err = errno;
+        if (ENOTTY == err) {
+            if (verbose > 1)
+                pr2serr("HDIO_GET_IDENTITY failed with ENOTTY, "
+                        "try HDIO_DRIVE_CMD ioctl ...\n");
+            buff[0] = ATA_IDENTIFY_DEVICE;
+            buff[3] = 1;
+            if (ioctl(device, HDIO_DRIVE_CMD, buff) < 0) {
+                if (verbose)
+                    pr2serr("HDIO_DRIVE_CMD(ATA_IDENTIFY_DEVICE) "
+                            "ioctl failed:\n\t%s [%d]\n",
+                            safe_strerror(err), err);
+                return sg_convert_errno(err);
+            }
+            memcpy(data, buff + HDIO_DRIVE_CMD_OFFSET, ATA_IDENTIFY_BUFF_SZ);
+            return 0;
+        } else {
+            if (verbose)
+                pr2serr("HDIO_GET_IDENTITY ioctl failed:\n"
+                        "\t%s [%d]\n", safe_strerror(err), err);
+            return sg_convert_errno(err);
+        }
+    } else if (verbose > 1)
+        pr2serr("HDIO_GET_IDENTITY succeeded\n");
+    if (0x2 == ((get_ident[0] >> 14) &0x3)) {   /* ATAPI device */
+        if (verbose > 1)
+            pr2serr("assume ATAPI device from HDIO_GET_IDENTITY response\n");
+        memset(buff, 0, sizeof(buff));
+        buff[0] = ATA_IDENTIFY_PACKET_DEVICE;
+        buff[3] = 1;
+        if (ioctl(device, HDIO_DRIVE_CMD, buff) < 0) {
+            err = errno;
+            if (verbose)
+                pr2serr("HDIO_DRIVE_CMD(ATA_IDENTIFY_PACKET_DEVICE) ioctl "
+                        "failed:\n\t%s [%d]\n", safe_strerror(err), err);
+            buff[0] = ATA_IDENTIFY_DEVICE;
+            buff[3] = 1;
+            if (ioctl(device, HDIO_DRIVE_CMD, buff) < 0) {
+                err = errno;
+                if (verbose)
+                    pr2serr("HDIO_DRIVE_CMD(ATA_IDENTIFY_DEVICE) ioctl "
+                            "failed:\n\t%s [%d]\n", safe_strerror(err), err);
+                return sg_convert_errno(err);
+            }
+        } else if (atapi_flag) {
+            *atapi_flag = true;
+            if (verbose > 1)
+                pr2serr("HDIO_DRIVE_CMD(ATA_IDENTIFY_DEVICE) succeeded\n");
+        }
+    } else {    /* assume non-packet device */
+        buff[0] = ATA_IDENTIFY_DEVICE;
+        buff[3] = 1;
+        if (ioctl(device, HDIO_DRIVE_CMD, buff) < 0) {
+            err = errno;
+            if (verbose)
+                pr2serr("HDIO_DRIVE_CMD(ATA_IDENTIFY_DEVICE) ioctl failed:"
+                        "\n\t%s [%d]\n", safe_strerror(err), err);
+            return sg_convert_errno(err);
+        } else if (verbose > 1)
+            pr2serr("HDIO_DRIVE_CMD(ATA_IDENTIFY_DEVICE) succeeded\n");
+    }
+    /* if the command returns data, copy it back */
+    memcpy(data, buff + HDIO_DRIVE_CMD_OFFSET, ATA_IDENTIFY_BUFF_SZ);
+    return 0;
+}
+
+static void
+show_ata_identify(const struct ata_identify_device * aidp, bool atapi,
+                  int vb)
+{
+    int res;
+    char model[64];
+    char serial[64];
+    char firm[64];
+
+    printf("%s device: model, serial number and firmware revision:\n",
+           (atapi ? "ATAPI" : "ATA"));
+    res = sg_ata_get_chars((const unsigned short *)aidp->model,
+                           0, 20, sg_is_big_endian(), model);
+    model[res] = '\0';
+    res = sg_ata_get_chars((const unsigned short *)aidp->serial_no,
+                           0, 10, sg_is_big_endian(), serial);
+    serial[res] = '\0';
+    res = sg_ata_get_chars((const unsigned short *)aidp->fw_rev,
+                           0, 4, sg_is_big_endian(), firm);
+    firm[res] = '\0';
+    printf("  %s %s %s\n", model, serial, firm);
+    if (vb) {
+        if (atapi)
+            printf("ATA IDENTIFY PACKET DEVICE response "
+                   "(256 words):\n");
+        else
+            printf("ATA IDENTIFY DEVICE response (256 words):\n");
+        dWordHex((const unsigned short *)aidp, 256, 0,
+                 sg_is_big_endian());
+    }
+}
+
+static void
+prepare_ata_identify(const struct opts_t * op, int inhex_len)
+{
+    int n = inhex_len;
+    struct ata_identify_device ata_ident;
+
+    if (n < 16) {
+        pr2serr("%s: got only %d bytes, give up\n", __func__, n);
+        return;
+    } else if (n < 512)
+        pr2serr("%s: expect 512 bytes or more, got %d, continue\n", __func__,
+                n);
+    else if (n > 512)
+        n = 512;
+    memset(&ata_ident, 0, sizeof(ata_ident));
+    memcpy(&ata_ident, rsp_buff, n);
+    show_ata_identify(&ata_ident, false, op->verbose);
+}
+
+/* Returns 0 if successful, else errno of error */
+static int
+try_ata_identify(int ata_fd, int do_hex, int do_raw, int verbose)
+{
+    bool atapi;
+    int res;
+    struct ata_identify_device ata_ident;
+
+    memset(&ata_ident, 0, sizeof(ata_ident));
+    res = ata_command_interface(ata_fd, (char *)&ata_ident, &atapi, verbose);
+    if (res)
+        return res;
+    if ((2 == do_raw) || (3 == do_hex))
+        dWordHex((const unsigned short *)&ata_ident, 256, -2,
+                 sg_is_big_endian());
+    else if (do_raw)
+        dStrRaw((const char *)&ata_ident, 512);
+    else {
+        if (do_hex) {
+            if (atapi)
+                printf("ATA IDENTIFY PACKET DEVICE response ");
+            else
+                printf("ATA IDENTIFY DEVICE response ");
+            if (do_hex > 1) {
+                printf("(512 bytes):\n");
+                hex2stdout((const uint8_t *)&ata_ident, 512, 0);
+            } else {
+                printf("(256 words):\n");
+                dWordHex((const unsigned short *)&ata_ident, 256, 0,
+                         sg_is_big_endian());
+            }
+        } else
+            show_ata_identify(&ata_ident, atapi, verbose);
+    }
+    return 0;
+}
+#endif
+
+/* structure defined in sg_lib_data.h */
+extern struct sg_lib_simple_value_name_t sg_version_descriptor_arr[];
+
+
+static const char *
+find_version_descriptor_str(int value)
+{
+    int k;
+    const struct sg_lib_simple_value_name_t * vdp;
+
+    for (k = 0; ((vdp = sg_version_descriptor_arr + k) && vdp->name); ++k) {
+        if (value == vdp->value)
+            return vdp->name;
+        if (value < vdp->value)
+            break;
+    }
+    return NULL;
+}
diff --git a/src/sg_inq_data.c b/src/sg_inq_data.c
new file mode 100644
index 0000000..f01bc5f
--- /dev/null
+++ b/src/sg_inq_data.c
@@ -0,0 +1,555 @@
+/*
+ * A utility program originally written for the Linux OS SCSI subsystem.
+ *     Copyright (C) 2000-2022 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This is an auxiliary file holding data tables for the sg_inq utility.
+ * It is mainly based on the SCSI SPC-6 document at https://www.t10.org .
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+
+/* Assume index is less than 16 */
+const char * sg_ansi_version_arr[16] =
+{
+    "no conformance claimed",
+    "SCSI-1",           /* obsolete, ANSI X3.131-1986 */
+    "SCSI-2",           /* obsolete, ANSI X3.131-1994 */
+    "SPC",              /* withdrawn, ANSI INCITS 301-1997 */
+    "SPC-2",            /* ANSI INCITS 351-2001, ISO/IEC 14776-452 */
+    "SPC-3",            /* ANSI INCITS 408-2005, ISO/IEC 14776-453 */
+    "SPC-4",            /* ANSI INCITS 513-2015 */
+    "SPC-5",            /* ANSI INCITS 502-2020 */
+    "ecma=1, [8h]",
+    "ecma=1, [9h]",
+    "ecma=1, [Ah]",
+    "ecma=1, [Bh]",
+    "reserved [Ch]",
+    "reserved [Dh]",
+    "reserved [Eh]",
+    "reserved [Fh]",
+};
+
+/* table from SPC-5 revision 16 [sorted numerically (from Annex E.9)] */
+/* Can also be obtained from : https://www.t10.org/lists/stds.txt 20170114 */
+/* Corrected against spc5r21 on 20190312 */
+
+#ifdef SG_SCSI_STRINGS
+
+struct sg_lib_simple_value_name_t sg_version_descriptor_arr[] = {
+    {0x0, "Version Descriptor not supported or No standard identified"},
+    {0x20, "SAM (no version claimed)"},
+    {0x3b, "SAM T10/0994-D revision 18"},
+    {0x3c, "SAM ANSI INCITS 270-1996"},
+    {0x40, "SAM-2 (no version claimed)"},
+    {0x54, "SAM-2 T10/1157-D revision 23"},
+    {0x55, "SAM-2 T10/1157-D revision 24"},
+    {0x5c, "SAM-2 ANSI INCITS 366-2003"},
+    {0x5e, "SAM-2 ISO/IEC 14776-412"},
+    {0x60, "SAM-3 (no version claimed)"},
+    {0x62, "SAM-3 T10/1561-D revision 7"},
+    {0x75, "SAM-3 T10/1561-D revision 13"},
+    {0x76, "SAM-3 T10/1561-D revision 14"},
+    {0x77, "SAM-3 ANSI INCITS 402-2005"},
+    {0x80, "SAM-4 (no version claimed)"},
+    {0x87, "SAM-4 T10/1683-D revision 13"},
+    {0x8b, "SAM-4 T10/1683-D revision 14"},
+    {0x90, "SAM-4 ANSI INCITS 447-2008"},
+    {0x92, "SAM-4 ISO/IEC 14776-414"},
+    {0xa0, "SAM-5 (no version claimed)"},
+    {0xa2, "SAM-5 T10/2104-D revision 4"},
+    {0xa4, "SAM-5 T10/2104-D revision 20"},
+    {0xa6, "SAM-5 T10/2104-D revision 21"},
+    {0xa8, "SAM-5 ANSI INCITS 515-2016"},
+    {0xc0, "SAM-6 (no version claimed)"},
+    {0x120, "SPC (no version claimed)"},
+    {0x13b, "SPC T10/0995-D revision 11a"},
+    {0x13c, "SPC ANSI INCITS 301-1997"},
+    {0x140, "MMC (no version claimed)"},
+    {0x15b, "MMC T10/1048-D revision 10a"},
+    {0x15c, "MMC ANSI INCITS 304-1997"},
+    {0x160, "SCC (no version claimed)"},
+    {0x17b, "SCC T10/1047-D revision 06c"},
+    {0x17c, "SCC ANSI INCITS 276-1997"},
+    {0x180, "SBC (no version claimed)"},
+    {0x19b, "SBC T10/0996-D revision 08c"},
+    {0x19c, "SBC ANSI INCITS 306-1998"},
+    {0x1a0, "SMC (no version claimed)"},
+    {0x1bb, "SMC T10/0999-D revision 10a"},
+    {0x1bc, "SMC ANSI INCITS 314-1998"},
+    {0x1be, "SMC ISO/IEC 14776-351"},
+    {0x1c0, "SES (no version claimed)"},
+    {0x1db, "SES T10/1212-D revision 08b"},
+    {0x1dc, "SES ANSI INCITS 305-1998"},
+    {0x1dd, "SES T10/1212-D revision 08b w/ Amendment ANSI "
+            "INCITS.305/AM1:2000"},
+    {0x1de, "SES ANSI INCITS 305-1998 w/ Amendment ANSI "
+            "INCITS.305/AM1:2000"},
+    {0x1e0, "SCC-2 (no version claimed}"},
+    {0x1fb, "SCC-2 T10/1125-D revision 04"},
+    {0x1fc, "SCC-2 ANSI INCITS 318-1998"},
+    {0x200, "SSC (no version claimed)"},
+    {0x201, "SSC T10/0997-D revision 17"},
+    {0x207, "SSC T10/0997-D revision 22"},
+    {0x21c, "SSC ANSI INCITS 335-2000"},
+    {0x220, "RBC (no version claimed)"},
+    {0x238, "RBC T10/1240-D revision 10a"},
+    {0x23c, "RBC ANSI INCITS 330-2000"},
+    {0x240, "MMC-2 (no version claimed)"},
+    {0x255, "MMC-2 T10/1228-D revision 11"},
+    {0x25b, "MMC-2 T10/1228-D revision 11a"},
+    {0x25c, "MMC-2 ANSI INCITS 333-2000"},
+    {0x260, "SPC-2 (no version claimed)"},
+    {0x267, "SPC-2 T10/1236-D revision 12"},
+    {0x269, "SPC-2 T10/1236-D revision 18"},
+    {0x275, "SPC-2 T10/1236-D revision 19"},
+    {0x276, "SPC-2 T10/1236-D revision 20"},
+    {0x277, "SPC-2 ANSI INCITS 351-2001"},
+    {0x278, "SPC-2 ISO/IEC 14776-452"},
+    {0x280, "OCRW (no version claimed)"},
+    {0x29e, "OCRW ISO/IEC 14776-381"},
+    {0x2a0, "MMC-3 (no version claimed)"},
+    {0x2b5, "MMC-3 T10/1363-D revision 9"},
+    {0x2b6, "MMC-3 T10/1363-D revision 10g"},
+    {0x2b8, "MMC-3 ANSI INCITS 360-2002"},
+    {0x2e0, "SMC-2 (no version claimed)"},
+    {0x2f5, "SMC-2 T10/1383-D revision 5"},
+    {0x2fc, "SMC-2 T10/1383-D revision 6"},
+    {0x2fd, "SMC-2 T10/1383-D revision 7"},
+    {0x2fe, "SMC-2 ANSI INCITS 382-2004"},
+    {0x300, "SPC-3 (no version claimed)"},
+    {0x301, "SPC-3 T10/1416-D revision 7"},
+    {0x307, "SPC-3 T10/1416-D revision 21"},
+    {0x30f, "SPC-3 T10/1416-D revision 22"},
+    {0x312, "SPC-3 T10/1416-D revision 23"},
+    {0x314, "SPC-3 ANSI INCITS 408-2005"},
+    {0x316, "SPC-3 ISO/IEC 14776-453"},
+    {0x320, "SBC-2 (no version claimed)"},
+    {0x322, "SBC-2 T10/1417-D revision 5a"},
+    {0x324, "SBC-2 T10/1417-D revision 15"},
+    {0x33b, "SBC-2 T10/1417-D revision 16"},
+    {0x33d, "SBC-2 ANSI INCITS 405-2005"},
+    {0x33e, "SBC-2 ISO/IEC 14776-322"},
+    {0x340, "OSD (no version claimed)"},
+    {0x341, "OSD T10/1355-D revision 0"},
+    {0x342, "OSD T10/1355-D revision 7a"},
+    {0x343, "OSD T10/1355-D revision 8"},
+    {0x344, "OSD T10/1355-D revision 9"},
+    {0x355, "OSD T10/1355-D revision 10"},
+    {0x356, "OSD ANSI INCITS 400-2004"},
+    {0x360, "SSC-2 (no version claimed)"},
+    {0x374, "SSC-2 T10/1434-D revision 7"},
+    {0x375, "SSC-2 T10/1434-D revision 9"},
+    {0x37d, "SSC-2 ANSI INCITS 380-2003"},
+    {0x380, "BCC (no version claimed)"},
+    {0x3a0, "MMC-4 (no version claimed)"},
+    {0x3b0, "MMC-4 T10/1545-D revision 5"},     /* dropped in spc4r09 */
+    {0x3b1, "MMC-4 T10/1545-D revision 5a"},
+    {0x3bd, "MMC-4 T10/1545-D revision 3"},
+    {0x3be, "MMC-4 T10/1545-D revision 3d"},
+    {0x3bf, "MMC-4 ANSI INCITS 401-2005"},
+    {0x3c0, "ADC (no version claimed)"},
+    {0x3d5, "ADC T10/1558-D revision 6"},
+    {0x3d6, "ADC T10/1558-D revision 7"},
+    {0x3d7, "ADC ANSI INCITS 403-2005"},
+    {0x3e0, "SES-2 (no version claimed)"},
+    {0x3e1, "SES-2 T10/1559-D revision 16"},
+    {0x3e7, "SES-2 T10/1559-D revision 19"},
+    {0x3eb, "SES-2 T10/1559-D revision 20"},
+    {0x3f0, "SES-2 ANSI INCITS 448-2008"},
+    {0x3f2, "SES-2 ISO/IEC 14776-372"},
+    {0x400, "SSC-3 (no version claimed)"},
+    {0x403, "SSC-3 T10/1611-D revision 04a"},
+    {0x407, "SSC-3 T10/1611-D revision 05"},
+    {0x409, "SSC-3 ANSI INCITS 467-2011"},
+    {0x40b, "SSC-3 ISO/IEC 14776-333:2013"},
+    {0x420, "MMC-5 (no version claimed)"},
+    {0x42f, "MMC-5 T10/1675-D revision 03"},
+    {0x431, "MMC-5 T10/1675-D revision 03b"},
+    {0x432, "MMC-5 T10/1675-D revision 04"},
+    {0x434, "MMC-5 ANSI INCITS 430-2007"},
+    {0x440, "OSD-2 (no version claimed)"},
+    {0x444, "OSD-2 T10/1729-D revision 4"},
+    {0x446, "OSD-2 T10/1729-D revision 5"},
+    {0x448, "OSD-2 ANSI INCITS 458-2011"},
+    {0x460, "SPC-4 (no version claimed)"},
+    {0x461, "SPC-4 T10/BSR INCITS 513 revision 16"},
+    {0x462, "SPC-4 T10/BSR INCITS 513 revision 18"},
+    {0x463, "SPC-4 T10/BSR INCITS 513 revision 23"},
+    {0x466, "SPC-4 T10/BSR INCITS 513 revision 36"},
+    {0x468, "SPC-4 T10/BSR INCITS 513 revision 37"},
+    {0x469, "SPC-4 T10/BSR INCITS 513 revision 37a"},
+    {0x46c, "SPC-4 ANSI INCITS 513-2015"},
+    {0x480, "SMC-3 (no version claimed)"},
+    {0x482, "SMC-3 T10/1730-D revision 15"},
+    {0x484, "SMC-3 T10/1730-D revision 16"},
+    {0x486, "SMC-3 ANSI INCITS 484-2012"},
+    {0x4a0, "ADC-2 (no version claimed)"},
+    {0x4a7, "ADC-2 T10/1741-D revision 7"},
+    {0x4aa, "ADC-2 T10/1741-D revision 8"},
+    {0x4ac, "ADC-2 ANSI INCITS 441-2008"},
+    {0x4c0, "SBC-3 (no version claimed)"},
+    {0x4c3, "SBC-3 T10/BSR INCITS 514 revision 35"},
+    {0x4c5, "SBC-3 T10/BSR INCITS 514 revision 36"},
+    {0x4c8, "SBC-3 ANSI INCITS 514-2014"},
+    {0x4e0, "MMC-6 (no version claimed)"},
+    {0x4e3, "MMC-6 T10/1836-D revision 2b"},
+    {0x4e5, "MMC-6 T10/1836-D revision 02g"},
+    {0x4e6, "MMC-6 ANSI INCITS 468-2010"},
+    {0x4e7, "MMC-6 ANSI INCITS 468-2010 + MMC-6/AM1 ANSI INCITS "
+            "468-2010/AM 1"},
+    {0x500, "ADC-3 (no version claimed)"},
+    {0x502, "ADC-3 T10/1895-D revision 04"},
+    {0x504, "ADC-3 T10/1895-D revision 05"},
+    {0x506, "ADC-3 T10/1895-D revision 05a"},
+    {0x50a, "ADC-3 ANSI INCITS 497-2012"},
+    {0x520, "SSC-4 (no version claimed)"},
+    {0x523, "SSC-4 T10/BSR INCITS 516 revision 2"},
+    {0x525, "SSC-4 T10/BSR INCITS 516 revision 3"},
+    {0x527, "SSC-4 SSC-4 ANSI INCITS 516-2013"},
+    {0x560, "OSD-3 (no version claimed)"},
+    {0x580, "SES-3 (no version claimed)"},
+    {0x582, "SES-3 T10/BSR INCITS 518 revision 13"},
+    {0x584, "SES-3 T10/BSR INCITS 518 revision 14"},
+    {0x5a0, "SSC-5 (no version claimed)"},
+    {0x5c0, "SPC-5 (no version claimed)"},
+/* SPC-5 is now a standard [ANSI INCITS 502-2020] but no version code */
+/* SPC-6 is now up to draft 06 but still no version code */
+    {0x5e0, "SFSC (no version claimed)"},
+    {0x5e3, "SFSC BSR INCITS 501 revision 01"},
+    {0x5e5, "SFSC BSR INCITS 501 revision 02"},
+    {0x5e8, "SFSC ANSI INCITS 501-2016"},
+    {0x600, "SBC-4 (no version claimed)"},
+    {0x620, "ZBC (no version claimed)"},
+    {0x622, "ZBC BSR INCITS 536 revision 02"},
+    {0x624, "ZBC BSR INCITS 536 revision 05"},
+    {0x640, "ADC-4 (no version claimed)"},
+    {0x660, "ZBC-2 (no version claimed)"},
+    {0x680, "SES-4 (no version claimed)"},
+    {0x820, "SSA-TL2 (no version claimed)"},
+    {0x83b, "SSA-TL2 T10/1147-D revision 05b"},
+    {0x83c, "SSA-TL2 ANSI INCITS 308-1998"},
+    {0x840, "SSA-TL1 (no version claimed)"},
+    {0x85b, "SSA-TL1 T10/0989-D revision 10b"},
+    {0x85c, "SSA-TL1 ANSI INCITS 295-1996"},
+    {0x860, "SSA-S3P (no version claimed)"},
+    {0x87b, "SSA-S3P T10/1051-D revision 05b"},
+    {0x87c, "SSA-S3P ANSI INCITS 309-1998"},
+    {0x880, "SSA-S2P (no version claimed)"},
+    {0x89b, "SSA-S2P T10/1121-D revision 07b"},
+    {0x89c, "SSA-S2P ANSI INCITS 294-1996"},
+    {0x8a0, "SIP (no version claimed)"},
+    {0x8bb, "SIP T10/0856-D revision 10"},
+    {0x8bc, "SIP ANSI INCITS 292-1997"},
+    {0x8c0, "FCP (no version claimed)"},
+    {0x8db, "FCP T10/0856-D revision 12"},
+    {0x8dc, "FCP ANSI INCITS 269-1996"},
+    {0x8e0, "SBP-2 (no version claimed)"},
+    {0x8fb, "SBP-2 T10/1155-D revision 04"},
+    {0x8fc, "SBP-2 ANSI INCITS 325-1999"},
+    {0x900, "FCP-2 (no version claimed)"},
+    {0x901, "FCP-2 T10/1144-D revision 4"},
+    {0x915, "FCP-2 T10/1144-D revision 7"},
+    {0x916, "FCP-2 T10/1144-D revision 7a"},
+    {0x917, "FCP-2 ANSI INCITS 350-2003"},
+    {0x918, "FCP-2 T10/1144-D revision 8"},
+    {0x920, "SST (no version claimed)"},
+    {0x935, "SST T10/1380-D revision 8b"},
+    {0x940, "SRP (no version claimed)"},
+    {0x954, "SRP T10/1415-D revision 10"},
+    {0x955, "SRP T10/1415-D revision 16a"},
+    {0x95c, "SRP ANSI INCITS 365-2002"},
+    {0x960, "iSCSI (no version claimed)"},
+    {0x961, "iSCSI RFC 7143"},
+    {0x962, "iSCSI RFC 7144"},
+    /* 0x960 up to 0x97f for iSCSI use */
+    {0x980, "SBP-3 (no version claimed)"},
+    {0x982, "SBP-3 T10/1467-D revision 1f"},
+    {0x994, "SBP-3 T10/1467-D revision 3"},
+    {0x99a, "SBP-3 T10/1467-D revision 4"},
+    {0x99b, "SBP-3 T10/1467-D revision 5"},
+    {0x99c, "SBP-3 ANSI INCITS 375-2004"},
+    {0x9a0, "SRP-2 (no version claimed)"},
+    {0x9c0, "ADP (no version claimed)"},
+    {0x9e0, "ADT (no version claimed)"},
+    {0x9f9, "ADT T10/1557-D revision 11"},
+    {0x9fa, "ADT T10/1557-D revision 14"},
+    {0x9fd, "ADT ANSI INCITS 406-2005"},
+    {0xa00, "FCP-3 (no version claimed)"},
+    {0xa07, "FCP-3 T10/1560-D revision 3f"},
+    {0xa0f, "FCP-3 T10/1560-D revision 4"},
+    {0xa11, "FCP-3 ANSI INCITS 416-2006"},
+    {0xa1c, "FCP-3 ISO/IEC 14776-223"},
+    {0xa20, "ADT-2 (no version claimed)"},
+    {0xa22, "ADT-2 T10/1742-D revision 06"},
+    {0xa27, "ADT-2 T10/1742-D revision 08"},
+    {0xa28, "ADT-2 T10/1742-D revision 09"},
+    {0xa2b, "ADT-2 ANSI INCITS 472-2011"},
+    {0xa40, "FCP-4 (no version claimed)"},
+    {0xa42, "FCP-4 T10/1828-D revision 01"},
+    {0xa44, "FCP-4 T10/1828-D revision 02"},
+    {0xa45, "FCP-4 T10/1828-D revision 02b"},
+    {0xa46, "FCP-4 ANSI INCITS 481-2012"},
+    {0xa60, "ADT-3 (no version claimed)"},
+    {0xaa0, "SPI (no version claimed)"},
+    {0xab9, "SPI T10/0855-D revision 15a"},
+    {0xaba, "SPI ANSI INCITS 253-1995"},
+    {0xabb, "SPI T10/0855-D revision 15a with SPI Amnd revision 3a"},
+    {0xabc, "SPI ANSI INCITS 253-1995 with SPI Amnd ANSI INCITS "
+            "253/AM1:1998"},
+    {0xac0, "Fast-20 (no version claimed)"},
+    {0xadb, "Fast-20 T10/1071-D revision 06"},
+    {0xadc, "Fast-20 ANSI INCITS 277-1996"},
+    {0xae0, "SPI-2 (no version claimed)"},
+    {0xafb, "SPI-2 T10/1142-D revision 20b"},
+    {0xafc, "SPI-2 ANSI INCITS 302-1999"},
+    {0xb00, "SPI-3 (no version claimed)"},
+    {0xb18, "SPI-3 T10/1302-D revision 10"},
+    {0xb19, "SPI-3 T10/1302-D revision 13a"},
+    {0xb1a, "SPI-3 T10/1302-D revision 14"},
+    {0xb1c, "SPI-3 ANSI INCITS 336-2000"},
+    {0xb20, "EPI (no version claimed)"},
+    {0xb3b, "EPI T10/1134-D revision 16"},
+    {0xb3c, "EPI ANSI INCITS TR-23 1999"},
+    {0xb40, "SPI-4 (no version claimed)"},
+    {0xb54, "SPI-4 T10/1365-D revision 7"},
+    {0xb55, "SPI-4 T10/1365-D revision 9"},
+    {0xb56, "SPI-4 ANSI INCITS 362-2002"},
+    {0xb59, "SPI-4 T10/1365-D revision 10"},
+    {0xb60, "SPI-5 (no version claimed)"},
+    {0xb79, "SPI-5 T10/1525-D revision 3"},
+    {0xb7a, "SPI-5 T10/1525-D revision 5"},
+    {0xb7b, "SPI-5 T10/1525-D revision 6"},
+    {0xb7c, "SPI-5 ANSI INCITS 367-2004"},
+    {0xbe0, "SAS (no version claimed)"},
+    {0xbe1, "SAS T10/1562-D revision 01"},
+    {0xbf5, "SAS T10/1562-D revision 03"},
+    {0xbfa, "SAS T10/1562-D revision 04"},
+    {0xbfb, "SAS T10/1562-D revision 04"},
+    {0xbfc, "SAS T10/1562-D revision 05"},
+    {0xbfd, "SAS ANSI INCITS 376-2003"},
+    {0xc00, "SAS-1.1 (no version claimed)"},
+    {0xc07, "SAS-1.1 T10/1602-D revision 9"},
+    {0xc0f, "SAS-1.1 T10/1602-D revision 10"},
+    {0xc11, "SAS-1.1 ANSI INCITS 417-2006"},
+    {0xc12, "SAS-1.1 ISO/IEC 14776-151"},
+    {0xc20, "SAS-2 (no version claimed)"},
+    {0xc23, "SAS-2 T10/1760-D revision 14"},
+    {0xc27, "SAS-2 T10/1760-D revision 15"},
+    {0xc28, "SAS-2 T10/1760-D revision 16"},
+    {0xc2a, "SAS-2 ANSI INCITS 457-2010"},
+    {0xc40, "SAS-2.1 (no version claimed)"},
+    {0xc48, "SAS-2.1 T10/2125-D revision 04"},
+    {0xc4a, "SAS-2.1 T10/2125-D revision 06"},
+    {0xc4b, "SAS-2.1 T10/2125-D revision 07"},
+    {0xc4e, "SAS-2.1 ANSI INCITS 478-2011"},
+    {0xc4f, "SAS-2.1 ANSI INCITS 478-2011 w/ Amnd 1 ANSI INCITS "
+            "478/AM1-2014"},
+    {0xc52, "SAS-2.1 ISO/IEC 14776-153"},
+    {0xc60, "SAS-3 (no version claimed)"},
+    {0xc63, "SAS-3 T10/BSR INCITS 519 revision 05a"},
+    {0xc65, "SAS-3 T10/BSR INCITS 519 revision 06"},
+    {0xc68, "SAS-3 ANSI INCITS 519-2014"},
+    {0xc80, "SAS-4 (no version claimed)"},
+    {0xc82, "SAS-4 T10/BSR INCITS 534 revision 08a"},
+    {0xd20, "FC-PH (no version claimed)"},
+    {0xd3b, "FC-PH ANSI INCITS 230-1994"},
+    {0xd3c, "FC-PH ANSI INCITS 230-1994 with Amnd 1 ANSI INCITS "
+            "230/AM1:1996"},
+    {0xd40, "FC-AL (no version claimed)"},
+    {0xd5c, "FC-AL ANSI INCITS 272-1996"},
+    {0xd60, "FC-AL-2 (no version claimed)"},
+    {0xd61, "FC-AL-2 T11/1133-D revision 7.0"},
+    {0xd63, "FC-AL-2 ANSI INCITS 332-1999 with AM1-2003 & AM2-2006"},
+    {0xd64, "FC-AL-2 ANSI INCITS 332-1999 with Amnd 2 AM2-2006"},
+    {0xd65, "FC-AL-2 ISO/IEC 14165-122 with AM1 & AM2"},
+    {0xd7c, "FC-AL-2 ANSI INCITS 332-1999"},
+    {0xd7d, "FC-AL-2 ANSI INCITS 332-1999 with Amnd 1 AM1:2002"},
+    {0xd80, "FC-PH-3 (no version claimed)"},
+    {0xd9c, "FC-PH-3 ANSI INCITS 303-1998"},
+    {0xda0, "FC-FS (no version claimed)"},
+    {0xdb7, "FC-FS T11/1331-D revision 1.2"},
+    {0xdb8, "FC-FS T11/1331-D revision 1.7"},
+    {0xdbc, "FC-FS ANSI INCITS 373-2003"},
+    {0xdbd, "FC-FS ISO/IEC 14165-251"},
+    {0xdc0, "FC-PI (no version claimed)"},
+    {0xddc, "FC-PI ANSI INCITS 352-2002"},
+    {0xde0, "FC-PI-2 (no version claimed)"},
+    {0xde2, "FC-PI-2 T11/1506-D revision 5.0"},
+    {0xde4, "FC-PI-2 ANSI INCITS 404-2006"},
+    {0xe00, "FC-FS-2 (no version claimed)"},
+    {0xe02, "FC-FS-2 ANSI INCITS 242-2007"},
+    {0xe03, "FC-FS-2 ANSI INCITS 242-2007 with AM1 ANSI INCITS 242/AM1-2007"},
+    {0xe20, "FC-LS (no version claimed)"},
+    {0xe21, "FC-LS T11/1620-D revision 1.62"},
+    {0xe29, "FC-LS ANSI INCITS 433-2007"},
+    {0xe40, "FC-SP (no version claimed)"},
+    {0xe42, "FC-SP T11/1570-D revision 1.6"},
+    {0xe45, "FC-SP ANSI INCITS 426-2007"},
+    {0xe60, "FC-PI-3 (no version claimed)"},
+    {0xe62, "FC-PI-3 T11/1625-D revision 2.0"},
+    {0xe68, "FC-PI-3 T11/1625-D revision 2.1"},
+    {0xe6a, "FC-PI-3 T11/1625-D revision 4.0"},
+    {0xe6e, "FC-PI-3 ANSI INCITS 460-2011"},
+    {0xe80, "FC-PI-4 (no version claimed)"},
+    {0xe82, "FC-PI-4 T11/1647-D revision 8.0"},
+    {0xe88, "FC-PI-4 ANSI INCITS 450 -2009"},
+    {0xea0, "FC 10GFC (no version claimed)"},
+    {0xea2, "FC 10GFC ANSI INCITS 364-2003"},
+    {0xea3, "FC 10GFC ISO/IEC 14165-116"},
+    {0xea5, "FC 10GFC ISO/IEC 14165-116 with AM1"},
+    {0xea6, "FC 10GFC ANSI INCITS 364-2003 with AM1 ANSI INCITS 364/AM1-2007"},
+    {0xec0, "FC-SP-2 (no version claimed)"},
+    {0xee0, "FC-FS-3 (no version claimed)"},
+    {0xee2, "FC-FS-3 T11/1861-D revision 0.9"},
+    {0xee7, "FC-FS-3 T11/1861-D revision 1.0"},
+    {0xee9, "FC-FS-3 T11/1861-D revision 1.10"},
+    {0xeeb, "FC-FS-3 ANSI INCITS 470-2011"},
+    {0xf00, "FC-LS-2 (no version claimed)"},
+    {0xf03, "FC-LS-2 T11/2103-D revision 2.11"},
+    {0xf05, "FC-LS-2 T11/2103-D revision 2.21"},
+    {0xf07, "FC-LS-2 ANSI INCITS 477-2011"},
+    {0xf20, "FC-PI-5 (no version claimed)"},
+    {0xf27, "FC-PI-5 T11/2118-D revision 2.00"},
+    {0xf28, "FC-PI-5 T11/2118-D revision 3.00"},
+    {0xf2a, "FC-PI-5 T11/2118-D revision 6.00"},
+    {0xf2b, "FC-PI-5 T11/2118-D revision 6.10"},
+    {0xf2e, "FC-PI-5 ANSI INCITS 479-2011"},
+    {0xf40, "FC-PI-6 (no version claimed)"},
+    {0xf60, "FC-FS-4 (no version claimed)"},
+    {0xf80, "FC-LS-3 (no version claimed)"},
+    {0x12a0, "FC-SCM (no version claimed)"},
+    {0x12a3, "FC-SCM T11/1824DT revision 1.0"},
+    {0x12a5, "FC-SCM T11/1824DT revision 1.1"},
+    {0x12a7, "FC-SCM T11/1824DT revision 1.4"},
+    {0x12aa, "FC-SCM INCITS TR-47 2012"},
+    {0x12c0, "FC-DA-2 (no version claimed)"},
+    {0x12c3, "FC-DA-2 T11/1870DT revision 1.04"},
+    {0x12c5, "FC-DA-2 T11/1870DT revision 1.06"},
+    {0x12c9, "FC-DA-2 INCITS TR-49 2012"},
+    {0x12e0, "FC-DA (no version claimed)"},
+    {0x12e2, "FC-DA T11/1513-DT revision 3.1"},
+    {0x12e8, "FC-DA ANSI INCITS TR-36 2004"},
+    {0x12e9, "FC-DA ISO/IEC 14165-341"},
+    {0x1300, "FC-Tape (no version claimed)"},
+    {0x1301, "FC-Tape T11/1315-D revision 1.16"},
+    {0x131b, "FC-Tape T11/1315-D revision 1.17"},
+    {0x131c, "FC-Tape ANSI INCITS TR-24 1999"},
+    {0x1320, "FC-FLA (no version claimed)"},
+    {0x133b, "FC-FLA T11/1235-D revision 7"},
+    {0x133c, "FC-FLA ANSI INCITS TR-20 1998"},
+    {0x1340, "FC-PLDA (no version claimed)"},
+    {0x135b, "FC-PLDA T11/1162-D revision 2.1"},
+    {0x135c, "FC-PLDA ANSI INCITS TR-19 1998"},
+    {0x1360, "SSA-PH2 (no version claimed)"},
+    {0x137b, "SSA-PH2 T10/1145-D revision 09c"},
+    {0x137c, "SSA-PH2 ANSI INCITS 293-1996"},
+    {0x1380, "SSA-PH3 (no version claimed)"},
+    {0x139b, "SSA-PH3 T10/1146-D revision 05b"},
+    {0x139c, "SSA-PH3 ANSI INCITS 307-1998"},
+    {0x14a0, "IEEE 1394 (no version claimed)"},
+    {0x14bd, "ANSI IEEE 1394:1995"},
+    {0x14c0, "IEEE 1394a (no version claimed)"},
+    {0x14e0, "IEEE 1394b (no version claimed)"},
+    {0x15e0, "ATA/ATAPI-6 (no version claimed)"},
+    {0x15fd, "ATA/ATAPI-6 ANSI INCITS 361-2002"},
+    {0x1600, "ATA/ATAPI-7 (no version claimed)"},
+    {0x1602, "ATA/ATAPI-7 T13/1532-D revision 3"},
+    {0x161c, "ATA/ATAPI-7 ANSI INCITS 397-2005"},
+    {0x161e, "ATA/ATAPI-7 ISO/IEC 24739"},
+    {0x1620, "ATA/ATAPI-8 ATA-AAM Architecture model (no version claimed)"},
+    {0x1621, "ATA/ATAPI-8 ATA-PT Parallel transport (no version claimed)"},
+    {0x1622, "ATA/ATAPI-8 ATA-AST Serial transport (no version claimed)"},
+    {0x1623, "ATA/ATAPI-8 ATA-ACS ATA/ATAPI command set (no version "
+             "claimed)"},
+    {0x1628, "ATA/ATAPI-8 ATA-AAM ANSI INCITS 451-2008"},
+    {0x162a, "ATA/ATAPI-8 ATA8-ACS ANSI INCITS 452-2009 w/ Amendment 1"},
+    {0x1728, "Universal Serial Bus Specification, Revision 1.1"},
+    {0x1729, "Universal Serial Bus Specification, Revision 2.0"},
+    {0x1730, "USB Mass Storage Class Bulk-Only Transport, Revision 1.0"},
+    {0x1740, "UAS (no version claimed)"},    /* USB attached SCSI */
+    {0x1743, "UAS T10/2095-D revision 02"},
+    {0x1747, "UAS T10/2095-D revision 04"},
+    {0x1748, "UAS ANSI INCITS 471-2010"},
+    {0x1749, "UAS ISO/IEC 14776-251:2014"},
+    {0x1761, "ACS-2 (no version claimed)"},
+    {0x1762, "ACS-2 ANSI INCITS 482-2013"},
+    {0x1765, "ACS-3 INCITS 522-2014"},
+    {0x1767, "ACS-4 INCITS 529-2018"},
+    {0x1780, "UAS-2 (no version claimed)"},
+    {0x1ea0, "SAT (no version claimed)"},
+    {0x1ea7, "SAT T10/1711-D rev 8"},
+    {0x1eab, "SAT T10/1711-D rev 9"},
+    {0x1ead, "SAT ANSI INCITS 431-2007"},
+    {0x1ec0, "SAT-2 (no version claimed)"},
+    {0x1ec4, "SAT-2 T10/1826-D revision 06"},
+    {0x1ec8, "SAT-2 T10/1826-D revision 09"},
+    {0x1eca, "SAT-2 ANSI INCITS 465-2010"},
+    {0x1ee0, "SAT-3 (no version claimed)"},
+    {0x1ee2, "SAT-3 T10/BSR INCITS 517 revision 4"},
+    {0x1ee4, "SAT-3 T10/BSR INCITS 517 revision 7"},
+    {0x1ee8, "SAT-3 ANSI INCITS 517-2015"},
+    {0x1f00, "SAT-4 (no version claimed)"},
+    {0x1f02, "SAT-4 T10/BSR INCITS 491 revision 5"},
+    {0x1f04, "SAT-4 T10/BSR INCITS 491 revision 6"},
+    {0x20a0, "SPL (no version claimed)"},
+    {0x20a3, "SPL T10/2124-D revision 6a"},
+    {0x20a5, "SPL T10/2124-D revision 7"},
+    {0x20a7, "SPL ANSI INCITS 476-2011"},
+    {0x20a8, "SPL ANSI INCITS 476-2011 + SPL AM1 INCITS 476/AM1 2012"},
+    {0x20aa, "SPL ISO/IEC 14776-261:2012"},
+    {0x20c0, "SPL-2 (no version claimed)"},
+    {0x20c2, "SPL-2 T10/BSR INCITS 505 revision 4"},
+    {0x20c4, "SPL-2 T10/BSR INCITS 505 revision 5"},
+    {0x20c8, "SPL-2 ANSI INCITS 505-2013"},
+    {0x20e0, "SPL-3 (no version claimed)"},
+    {0x20e4, "SPL-3 T10/BSR INCITS 492 revision 6"},
+    {0x20e6, "SPL-3 T10/BSR INCITS 492 revision 7"},
+    {0x20e8, "SPL-3 ANSI INCITS 492-2015"},
+    {0x2100, "SPL-4 (no version claimed)"},
+    {0x2102, "SPL-4 T10/BSR INCITS 538 revision 08a"},
+    {0x2104, "SPL-4 T10/BSR INCITS 538 revision 10"},
+    {0x2105, "SPL-4 T10/BSR INCITS 538 revision 11"},
+    {0x2120, "SPL-5 (no version claimed)"},
+    {0x21e0, "SOP (no version claimed)"},
+    {0x21e4, "SOP T10/BSR INCITS 489 revision 4"},
+    {0x21e6, "SOP T10/BSR INCITS 489 revision 5"},
+    {0x21e8, "SOP ANSI INCITS 489-2014"},
+    {0x2200, "PQI (no version claimed)"},
+    {0x2204, "PQI T10/BSR INCITS 490 revision 6"},
+    {0x2206, "PQI T10/BSR INCITS 490 revision 7"},
+    {0x2208, "PQI ANSI INCITS 490-2014"},
+    {0x2220, "SOP-2 (no draft published)"},
+    {0x2240, "PQI-2 (no version claimed)"},
+    {0x2242, "PQI-2 T10/BSR INCITS 507 revision 01"},
+    {0x2244, "PQI-2 PQI-2 ANSI INCITS 507-2016"},
+    {0xffc0, "IEEE 1667 (no version claimed)"},
+    {0xffc1, "IEEE 1667-2006"},
+    {0xffc2, "IEEE 1667-2009"},
+    {0xffc3, "IEEE 1667-2015"},
+    {0xffc4, "IEEE 1667-2018"},
+    {0xffff, NULL},     /* sentinel, leave at end */
+};
+
+#else
+
+struct sg_lib_simple_value_name_t sg_version_descriptor_arr[] = {
+    {0xffff, NULL},     /* sentinel, leave at end */
+};
+
+#endif
diff --git a/src/sg_logs.c b/src/sg_logs.c
new file mode 100644
index 0000000..ce6a7e9
--- /dev/null
+++ b/src/sg_logs.c
@@ -0,0 +1,9156 @@
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *  Copyright (C) 2000-2022 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program outputs information provided by a SCSI LOG SENSE command
+ * and in some cases issues a LOG SELECT command.
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <errno.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_lib_names.h"
+#include "sg_cmds_basic.h"
+#ifdef SG_LIB_WIN32
+#include "sg_pt.h"      /* needed for scsi_pt_win32_direct() */
+#endif
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "2.08 20221112";    /* spc6r06 + sbc5r03 */
+
+#define MY_NAME "sg_logs"
+
+#define MX_ALLOC_LEN (0xfffc)
+#define MX_INLEN_ALLOC_LEN (0x800000)
+#define DEF_INLEN_ALLOC_LEN (0x40000)
+#define SHORT_RESP_LEN 128
+
+#define SUPP_PAGES_LPAGE 0x0
+#define BUFF_OVER_UNDER_LPAGE 0x1
+#define WRITE_ERR_LPAGE 0x2
+#define READ_ERR_LPAGE 0x3
+#define READ_REV_ERR_LPAGE 0x4
+#define VERIFY_ERR_LPAGE 0x5
+#define NON_MEDIUM_LPAGE 0x6
+#define LAST_N_ERR_LPAGE 0x7
+#define FORMAT_STATUS_LPAGE 0x8
+#define LAST_N_DEFERRED_LPAGE 0xb
+#define LB_PROV_LPAGE 0xc
+#define TEMPERATURE_LPAGE 0xd
+#define START_STOP_LPAGE 0xe
+#define APP_CLIENT_LPAGE 0xf
+#define SELF_TEST_LPAGE 0x10
+#define SOLID_STATE_MEDIA_LPAGE 0x11
+#define REQ_RECOVERY_LPAGE 0x13
+#define DEVICE_STATS_LPAGE 0x14
+#define BACKGROUND_SCAN_LPAGE 0x15
+#define SAT_ATA_RESULTS_LPAGE 0x16
+#define PROTO_SPECIFIC_LPAGE 0x18
+#define STATS_LPAGE 0x19
+#define PCT_LPAGE 0x1a
+#define TAPE_ALERT_LPAGE 0x2e
+#define IE_LPAGE 0x2f
+#define NOT_SPG_SUBPG 0x0                       /* any page: no subpages */
+#define SUPP_SPGS_SUBPG 0xff                    /* all subpages of ... */
+#define PENDING_DEFECTS_SUBPG 0x1               /* page 0x15 */
+#define BACKGROUND_OP_SUBPG 0x2                 /* page 0x15 */
+#define CACHE_STATS_SUBPG 0x20                  /* page 0x19 */
+#define CMD_DUR_LIMITS_SUBPG 0x21               /* page 0x19 */
+#define ENV_REPORTING_SUBPG 0x1                 /* page 0xd */
+#define UTILIZATION_SUBPG 0x1                   /* page 0xe */
+#define ENV_LIMITS_SUBPG 0x2                    /* page 0xd */
+#define LPS_MISALIGNMENT_SUBPG 0x3              /* page 0x15 */
+#define ZONED_BLOCK_DEV_STATS_SUBPG 0x1         /* page 0x14 */
+#define LAST_N_INQUIRY_DATA_CH_SUBPG 0x1        /* page 0xb */
+#define LAST_N_MODE_PG_DATA_CH_SUBPG 0x2        /* page 0xb */
+
+/* Vendor product numbers/identifiers */
+#define VP_NONE   (-1)
+#define VP_SEAG   0
+#define VP_HITA   1
+#define VP_TOSH   2
+#define VP_LTO5   3
+#define VP_LTO6   4
+#define VP_ALL    99
+
+#define MVP_OFFSET 8
+
+/* Vendor product masks
+ * MVP_STD OR-ed with MVP_<vendor> is a T10 defined lpage with vendor
+ * specific parameter codes (e.g. Information Exceptions lpage [0x2f]) */
+#define MVP_STD    (1 << (MVP_OFFSET - 1))
+#define MVP_SEAG   (1 << (VP_SEAG + MVP_OFFSET))
+#define MVP_HITA   (1 << (VP_HITA + MVP_OFFSET))
+#define MVP_TOSH   (1 << (VP_TOSH + MVP_OFFSET))
+#define MVP_LTO5   (1 << (VP_LTO5 + MVP_OFFSET))
+#define MVP_LTO6   (1 << (VP_LTO6 + MVP_OFFSET))
+
+#define OVP_LTO    (MVP_LTO5 | MVP_LTO6)
+#define OVP_ALL    (~0)
+
+
+#define PCB_STR_LEN 128
+
+#define LOG_SENSE_PROBE_ALLOC_LEN 4
+#define LOG_SENSE_DEF_TIMEOUT 64        /* seconds */
+
+static uint8_t * rsp_buff;
+static uint8_t * free_rsp_buff;
+static int rsp_buff_sz = MX_ALLOC_LEN + 4;
+static const int parr_sz = 4096;
+
+static const char * const unknown_s = "unknown";
+static const char * const not_avail = "not available";
+static const char * const param_c = "Parameter code";
+static const char * const param_c_sn = "parameter_code";
+static const char * const as_s_s = "as_string";
+static const char * const rstrict_s = "restricted";
+static const char * const rsv_s = "reserved";
+static const char * const vend_spec = "vendor specific";
+static const char * const not_rep = "not reported";
+static const char * const in_hex = "in hex";
+static const char * const s_key = "sense key";
+
+static struct option long_options[] = {
+        {"All", no_argument, 0, 'A'},   /* equivalent to '-aa' */
+        {"ALL", no_argument, 0, 'A'},   /* equivalent to '-aa' */
+        {"all", no_argument, 0, 'a'},
+        {"brief", no_argument, 0, 'b'},
+        {"control", required_argument, 0, 'c'},
+        {"enumerate", no_argument, 0, 'e'},
+        {"exclude", no_argument, 0, 'E'},
+        {"filter", required_argument, 0, 'f'},
+        {"full", no_argument, 0, 'F'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"in", required_argument, 0, 'i'},
+        {"inhex", required_argument, 0, 'i'},
+        {"json", optional_argument, 0, 'j'},
+        {"list", no_argument, 0, 'l'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"name", no_argument, 0, 'n'},
+        {"new", no_argument, 0, 'N'},
+        {"no_inq", no_argument, 0, 'x'},
+        {"no-inq", no_argument, 0, 'x'},
+        {"old", no_argument, 0, 'O'},
+        {"page", required_argument, 0, 'p'},
+        {"paramp", required_argument, 0, 'P'},
+        {"pcb", no_argument, 0, 'q'},
+        {"ppc", no_argument, 0, 'Q'},
+        {"pdt", required_argument, 0, 'D'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'X'},
+        {"reset", no_argument, 0, 'R'},
+        {"sp", no_argument, 0, 's'},
+        {"select", no_argument, 0, 'S'},
+        {"temperature", no_argument, 0, 't'},
+        {"transport", no_argument, 0, 'T'},
+        {"undefined", no_argument, 0, 'u'},
+        {"vendor", required_argument, 0, 'M'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool do_full;
+    bool do_name;
+    bool do_pcb;
+    bool do_ppc;
+    bool do_raw;
+    bool do_pcreset;
+    bool do_select;
+    bool do_sp;
+    bool do_temperature;
+    bool do_transport;
+    bool exclude_vendor;
+    bool filter_given;
+    bool maxlen_given;
+    bool o_readonly;
+    bool opt_new;
+    bool verbose_given;
+    bool version_given;
+    int do_all;
+    int do_brief;
+    int do_enumerate;
+    int do_help;
+    int do_hex;
+    int do_list;
+    int dstrhex_no_ascii;       /* value for dStrHex() no_ascii argument */
+    int hex2str_oformat;        /* value for hex2str() oformat argument */
+    int vend_prod_num;  /* one of the VP_* constants or -1 (def) */
+    int deduced_vpn;    /* deduced vendor_prod_num; from INQUIRY, etc */
+    int verbose;
+    int filter;
+    int page_control;
+    int maxlen;
+    int pg_code;
+    int subpg_code;
+    int paramp;
+    int no_inq;
+    int dev_pdt;        /* from device or --pdt=DT */
+    int decod_subpg_code;
+    int undefined_hex;  /* hex format of undefined/unrecognized fields */
+    const char * device_name;
+    const char * in_fn;
+    const char * pg_arg;
+    const char * vend_prod;
+    const struct log_elem * lep;
+    sgj_state json_st;
+};
+
+
+struct log_elem {
+    int pg_code;
+    int subpg_code;     /* only unless subpg_high>0 then this is only */
+    int subpg_high;     /* when >0 this is high end of subpage range */
+    int pdt;            /* -1 for all */
+    int flags;          /* bit mask; or-ed with MVP_* constants */
+    const char * name;
+    const char * acron;
+    bool (*show_pagep)(const uint8_t * resp, int len,
+                       struct opts_t * op, sgj_opaque_p jop);
+                        /* Returns true if done */
+};
+
+struct vp_name_t {
+    int vend_prod_num;       /* vendor/product identifier */
+    const char * acron;
+    const char * name;
+    const char * t10_vendorp;
+    const char * t10_productp;
+};
+
+static const char * ls_s = "log_sense: ";
+
+static bool show_supported_pgs_page(const uint8_t * resp, int len,
+                                    struct opts_t * op, sgj_opaque_p jop);
+static bool show_supported_pgs_sub_page(const uint8_t * resp, int len,
+                                        struct opts_t * op, sgj_opaque_p jop);
+static bool show_buffer_over_under_run_page(const uint8_t * resp, int len,
+                                            struct opts_t * op,
+                                            sgj_opaque_p jop);
+static bool show_error_counter_page(const uint8_t * resp, int len,
+                                    struct opts_t * op, sgj_opaque_p jop);
+static bool show_non_medium_error_page(const uint8_t * resp, int len,
+                                       struct opts_t * op, sgj_opaque_p jop);
+static bool show_last_n_error_page(const uint8_t * resp, int len,
+                                   struct opts_t * op, sgj_opaque_p jop);
+static bool show_format_status_page(const uint8_t * resp, int len,
+                                    struct opts_t * op, sgj_opaque_p jop);
+static bool show_last_n_deferred_error_page(const uint8_t * resp, int len,
+                                            struct opts_t * op,
+                                            sgj_opaque_p jop);
+static bool show_last_n_inq_data_ch_page(const uint8_t * resp, int len,
+                                         struct opts_t * op,
+                                         sgj_opaque_p jop);
+static bool show_last_n_mode_pg_data_ch_page(const uint8_t * resp, int len,
+                                             struct opts_t * op,
+                                             sgj_opaque_p jop);
+static bool show_lb_provisioning_page(const uint8_t * resp, int len,
+                                      struct opts_t * op, sgj_opaque_p jop);
+static bool show_sequential_access_page(const uint8_t * resp, int len,
+                                        struct opts_t * op, sgj_opaque_p jop);
+static bool show_temperature_page(const uint8_t * resp, int len,
+                                  struct opts_t * op, sgj_opaque_p jop);
+static bool show_start_stop_page(const uint8_t * resp, int len,
+                                 struct opts_t * op, sgj_opaque_p jop);
+static bool show_utilization_page(const uint8_t * resp, int len,
+                                  struct opts_t * op, sgj_opaque_p jop);
+static bool show_app_client_page(const uint8_t * resp, int len,
+                                 struct opts_t * op, sgj_opaque_p jop);
+static bool show_self_test_page(const uint8_t * resp, int len,
+                                struct opts_t * op, sgj_opaque_p jop);
+static bool show_solid_state_media_page(const uint8_t * resp, int len,
+                                        struct opts_t * op, sgj_opaque_p jop);
+static bool show_device_stats_page(const uint8_t * resp, int len,
+                                   struct opts_t * op, sgj_opaque_p jop);
+static bool show_media_stats_page(const uint8_t * resp, int len,
+                                  struct opts_t * op, sgj_opaque_p jop);
+static bool show_dt_device_status_page(const uint8_t * resp, int len,
+                                       struct opts_t * op, sgj_opaque_p jop);
+static bool show_tapealert_response_page(const uint8_t * resp, int len,
+                                         struct opts_t * op,
+                                         sgj_opaque_p jop);
+static bool show_requested_recovery_page(const uint8_t * resp, int len,
+                                         struct opts_t * op,
+                                         sgj_opaque_p jop);
+static bool show_background_scan_results_page(const uint8_t * resp, int len,
+                                              struct opts_t * op,
+                                              sgj_opaque_p jop);
+static bool show_zoned_block_dev_stats(const uint8_t * resp, int len,
+                                       struct opts_t * op, sgj_opaque_p jop);
+static bool show_pending_defects_page(const uint8_t * resp, int len,
+                                      struct opts_t * op, sgj_opaque_p jop);
+static bool show_background_op_page(const uint8_t * resp, int len,
+                                    struct opts_t * op, sgj_opaque_p jop);
+static bool show_lps_misalignment_page(const uint8_t * resp, int len,
+                                       struct opts_t * op, sgj_opaque_p jop);
+static bool show_element_stats_page(const uint8_t * resp, int len,
+                                    struct opts_t * op, sgj_opaque_p jop);
+static bool show_service_buffer_info_page(const uint8_t * resp, int len,
+                                          struct opts_t * op,
+                                          sgj_opaque_p jop);
+static bool show_ata_pt_results_page(const uint8_t * resp, int len,
+                                     struct opts_t * op, sgj_opaque_p jop);
+static bool show_tape_diag_data_page(const uint8_t * resp, int len,
+                                     struct opts_t * op, sgj_opaque_p jop);
+static bool show_mchanger_diag_data_page(const uint8_t * resp, int len,
+                                         struct opts_t * op,
+                                         sgj_opaque_p jop);
+static bool show_non_volatile_cache_page(const uint8_t * resp, int len,
+                                         struct opts_t * op,
+                                         sgj_opaque_p jop);
+static bool show_volume_stats_pages(const uint8_t * resp, int len,
+                                    struct opts_t * op, sgj_opaque_p jop);
+static bool show_protocol_specific_port_page(const uint8_t * resp, int len,
+                                             struct opts_t * op,
+                                             sgj_opaque_p jop);
+static bool show_stats_perform_pages(const uint8_t * resp, int len,
+                                     struct opts_t * op, sgj_opaque_p jop);
+static bool show_cache_stats_page(const uint8_t * resp, int len,
+                                  struct opts_t * op, sgj_opaque_p jop);
+static bool show_power_condition_transitions_page(const uint8_t * resp,
+                                 int len, struct opts_t * op,
+                                                  sgj_opaque_p jop);
+static bool show_environmental_reporting_page(const uint8_t * resp, int len,
+                                              struct opts_t * op,
+                                              sgj_opaque_p jop);
+static bool show_environmental_limits_page(const uint8_t * resp, int len,
+                                           struct opts_t * op,
+                                           sgj_opaque_p jop);
+static bool show_cmd_dur_limits_page(const uint8_t * resp, int len,
+                                     struct opts_t * op, sgj_opaque_p jop);
+static bool show_data_compression_page(const uint8_t * resp, int len,
+                                       struct opts_t * op, sgj_opaque_p jop);
+static bool show_tape_alert_ssc_page(const uint8_t * resp, int len,
+                                     struct opts_t * op, sgj_opaque_p jop);
+static bool show_ie_page(const uint8_t * resp, int len,
+                         struct opts_t * op, sgj_opaque_p jop);
+static bool show_tape_usage_page(const uint8_t * resp, int len,
+                                 struct opts_t * op, sgj_opaque_p jop);
+static bool show_tape_capacity_page(const uint8_t * resp, int len,
+                                     struct opts_t * op, sgj_opaque_p jop);
+static bool show_seagate_cache_page(const uint8_t * resp, int len,
+                                    struct opts_t * op, sgj_opaque_p jop);
+static bool show_seagate_factory_page(const uint8_t * resp, int len,
+                                      struct opts_t * op, sgj_opaque_p jop);
+static bool show_hgst_perf_page(const uint8_t * resp, int len,
+                                struct opts_t * op, sgj_opaque_p jop);
+static bool show_hgst_misc_page(const uint8_t * resp, int len,
+                                struct opts_t * op, sgj_opaque_p jop);
+
+/* elements in page_number/subpage_number order */
+static struct log_elem log_arr[] = {
+    {SUPP_PAGES_LPAGE, 0, 0, -1, MVP_STD, "Supported log pages", "sp",
+     show_supported_pgs_page},          /* 0, 0 */
+    {SUPP_PAGES_LPAGE, SUPP_SPGS_SUBPG, 0, -1, MVP_STD, "Supported log pages "
+     "and subpages", "ssp", show_supported_pgs_sub_page}, /* 0, 0xff */
+    {BUFF_OVER_UNDER_LPAGE, 0, 0, -1, MVP_STD, "Buffer over-run/under-run",
+     "bou", show_buffer_over_under_run_page},  /* 0x1, 0x0 */
+    {WRITE_ERR_LPAGE, 0, 0, -1, MVP_STD, "Write error counters", "we",
+     show_error_counter_page},          /* 0x2, 0x0 */
+    {READ_ERR_LPAGE, 0, 0, -1, MVP_STD, "Read error counters", "re",
+     show_error_counter_page},          /* 0x3, 0x0 */
+    {READ_REV_ERR_LPAGE, 0, 0, -1, MVP_STD, "Read reverse error counters",
+     "rre", show_error_counter_page},          /* 0x4, 0x0 */
+    {VERIFY_ERR_LPAGE, 0, 0, -1, MVP_STD, "Verify error counters", "ve",
+     show_error_counter_page},          /* 0x5, 0x0 */
+    {NON_MEDIUM_LPAGE, 0, 0, -1, MVP_STD, "Non medium", "nm",
+     show_non_medium_error_page},       /* 0x6, 0x0 */
+    {LAST_N_ERR_LPAGE, 0, 0, -1, MVP_STD, "Last n error", "lne",
+     show_last_n_error_page},           /* 0x7, 0x0 */
+    {FORMAT_STATUS_LPAGE, 0, 0, 0, MVP_STD, "Format status", "fs",
+     show_format_status_page},          /* 0x8, 0x0  SBC */
+    {LAST_N_DEFERRED_LPAGE, 0, 0, -1, MVP_STD, "Last n deferred error", "lnd",
+     show_last_n_deferred_error_page},  /* 0xb, 0x0 */
+    {LAST_N_DEFERRED_LPAGE, LAST_N_INQUIRY_DATA_CH_SUBPG, 0, -1, MVP_STD,
+     "Last n inquiry data changed", "lnic",
+     show_last_n_inq_data_ch_page},     /* 0xb, 0x1 */
+    {LAST_N_DEFERRED_LPAGE, LAST_N_MODE_PG_DATA_CH_SUBPG, 0, -1, MVP_STD,
+     "Last n mode page data changed", "lnmc",
+     show_last_n_mode_pg_data_ch_page}, /* 0xb, 0x2 */
+    {LB_PROV_LPAGE, 0, 0, 0, MVP_STD, "Logical block provisioning", "lbp",
+     show_lb_provisioning_page},        /* 0xc, 0x0  SBC */
+    {0xc, 0, 0, PDT_TAPE, MVP_STD, "Sequential access device", "sad",
+     show_sequential_access_page},      /* 0xc, 0x0  SSC */
+    {TEMPERATURE_LPAGE, 0, 0, -1, MVP_STD, "Temperature", "temp",
+     show_temperature_page},            /* 0xd, 0x0 */
+    {TEMPERATURE_LPAGE, ENV_REPORTING_SUBPG, 0, -1, MVP_STD,  /* 0xd, 0x1 */
+     "Environmental reporting", "enr", show_environmental_reporting_page},
+    {TEMPERATURE_LPAGE, ENV_LIMITS_SUBPG, 0, -1, MVP_STD,     /* 0xd, 0x2 */
+     "Environmental limits", "enl", show_environmental_limits_page},
+    {START_STOP_LPAGE, 0, 0, -1, MVP_STD, "Start-stop cycle counter", "sscc",
+     show_start_stop_page},             /* 0xe, 0x0 */
+    {START_STOP_LPAGE, UTILIZATION_SUBPG, 0, 0, MVP_STD, "Utilization",
+     "util", show_utilization_page},    /* 0xe, 0x1 SBC */    /* sbc4r04 */
+    {APP_CLIENT_LPAGE, 0, 0, -1, MVP_STD, "Application client", "ac",
+     show_app_client_page},             /* 0xf, 0x0 */
+    {SELF_TEST_LPAGE, 0, 0, -1, MVP_STD, "Self test results", "str",
+     show_self_test_page},              /* 0x10, 0x0 */
+    {SOLID_STATE_MEDIA_LPAGE, 0, 0, 0, MVP_STD, "Solid state media", "ssm",
+     show_solid_state_media_page},      /* 0x11, 0x0  SBC */
+    {0x11, 0, 0, PDT_TAPE, MVP_STD, "DT Device status", "dtds",
+     show_dt_device_status_page},       /* 0x11, 0x0  SSC,ADC */
+    {0x12, 0, 0, PDT_TAPE, MVP_STD, "Tape alert response", "tar",
+     show_tapealert_response_page},      /* 0x12, 0x0  SSC,ADC */
+    {REQ_RECOVERY_LPAGE, 0, 0, PDT_TAPE, MVP_STD, "Requested recovery", "rr",
+     show_requested_recovery_page},     /* 0x13, 0x0  SSC,ADC */
+    {DEVICE_STATS_LPAGE, 0, 0, PDT_TAPE, MVP_STD, "Device statistics", "ds",
+     show_device_stats_page},           /* 0x14, 0x0  SSC,ADC */
+    {DEVICE_STATS_LPAGE, 0, 0, PDT_MCHANGER, MVP_STD,   /* 0x14, 0x0  SMC */
+     "Media changer statistics", "mcs", show_media_stats_page},
+    {DEVICE_STATS_LPAGE, ZONED_BLOCK_DEV_STATS_SUBPG,   /* 0x14,0x1 zbc2r01 */
+     0, 0, MVP_STD, "Zoned block device statistics", "zbds",
+     show_zoned_block_dev_stats},
+    {BACKGROUND_SCAN_LPAGE, 0, 0, 0, MVP_STD, "Background scan results",
+     "bsr", show_background_scan_results_page}, /* 0x15, 0x0  SBC */
+    {BACKGROUND_SCAN_LPAGE, BACKGROUND_OP_SUBPG, 0, 0, MVP_STD,
+     "Background operation", "bop", show_background_op_page},
+                                        /* 0x15, 0x2  SBC */
+    {BACKGROUND_SCAN_LPAGE, LPS_MISALIGNMENT_SUBPG, 0, 0, MVP_STD,
+     "LPS misalignment", "lps", show_lps_misalignment_page},
+                                        /* 0x15, 0x3  SBC-4 */
+    {0x15, 0, 0, PDT_MCHANGER, MVP_STD, "Element statistics", "els",
+     show_element_stats_page},          /* 0x15, 0x0  SMC */
+    {0x15, 0, 0, PDT_ADC, MVP_STD, "Service buffers information", "sbi",
+     show_service_buffer_info_page},    /* 0x15, 0x0  ADC */
+    {BACKGROUND_SCAN_LPAGE, PENDING_DEFECTS_SUBPG, 0, 0, MVP_STD,
+     "Pending defects", "pd", show_pending_defects_page}, /* 0x15, 0x1  SBC */
+    {SAT_ATA_RESULTS_LPAGE, 0, 0, 0, MVP_STD, "ATA pass-through results",
+     "aptr", show_ata_pt_results_page}, /* 0x16, 0x0  SAT */
+    {0x16, 0, 0, PDT_TAPE, MVP_STD, "Tape diagnostic data", "tdd",
+     show_tape_diag_data_page},         /* 0x16, 0x0  SSC */
+    {0x16, 0, 0, PDT_MCHANGER, MVP_STD, "Media changer diagnostic data",
+     "mcdd", show_mchanger_diag_data_page}, /* 0x16, 0x0  SMC */
+    {0x17, 0, 0, 0, MVP_STD, "Non volatile cache", "nvc",
+     show_non_volatile_cache_page},     /* 0x17, 0x0  SBC */
+    {0x17, 0, 0xf, PDT_TAPE, MVP_STD, "Volume statistics", "vs",
+     show_volume_stats_pages},          /* 0x17, 0x0...0xf  SSC */
+    {PROTO_SPECIFIC_LPAGE, 0, 0, -1, MVP_STD, "Protocol specific port",
+     "psp", show_protocol_specific_port_page},  /* 0x18, 0x0  */
+    {STATS_LPAGE, 0, 0, -1, MVP_STD, "General Statistics and Performance",
+     "gsp", show_stats_perform_pages},  /* 0x19, 0x0  */
+    {STATS_LPAGE, 0x1, 0x1f, -1, MVP_STD, "Group Statistics and Performance",
+     "grsp", show_stats_perform_pages}, /* 0x19, 0x1...0x1f  */
+    {STATS_LPAGE, CACHE_STATS_SUBPG, 0, -1, MVP_STD,    /* 0x19, 0x20  */
+     "Cache memory statistics", "cms", show_cache_stats_page},
+    {STATS_LPAGE, CMD_DUR_LIMITS_SUBPG, 0, -1, MVP_STD, /* 0x19, 0x21  */
+     "Command duration limits statistics", "cdl",
+     show_cmd_dur_limits_page /* spc6r01 */ },
+    {PCT_LPAGE, 0, 0, -1, MVP_STD, "Power condition transitions", "pct",
+     show_power_condition_transitions_page}, /* 0x1a, 0  */
+    {0x1b, 0, 0, PDT_TAPE, MVP_STD, "Data compression", "dc",
+     show_data_compression_page},       /* 0x1b, 0  SSC */
+    {0x2d, 0, 0, PDT_TAPE, MVP_STD, "Current service information", "csi",
+     NULL},                             /* 0x2d, 0  SSC */
+    {TAPE_ALERT_LPAGE, 0, 0, PDT_TAPE, MVP_STD, "Tape alert", "ta",
+     show_tape_alert_ssc_page},         /* 0x2e, 0  SSC */
+    {IE_LPAGE, 0, 0, -1, (MVP_STD | MVP_HITA),
+     "Informational exceptions", "ie", show_ie_page},       /* 0x2f, 0  */
+/* vendor specific */
+    {0x30, 0, 0, PDT_DISK, MVP_HITA, "Performance counters (Hitachi)",
+     "pc_hi", show_hgst_perf_page},     /* 0x30, 0  SBC */
+    {0x30, 0, 0, PDT_TAPE, OVP_LTO, "Tape usage (lto-5, 6)", "tu_",
+     show_tape_usage_page},             /* 0x30, 0  SSC */
+    {0x31, 0, 0, PDT_TAPE, OVP_LTO, "Tape capacity (lto-5, 6)",
+     "tc_", show_tape_capacity_page},   /* 0x31, 0  SSC */
+    {0x32, 0, 0, PDT_TAPE, MVP_LTO5, "Data compression (lto-5)",
+     "dc_", show_data_compression_page}, /* 0x32, 0  SSC; redirect to 0x1b */
+    {0x33, 0, 0, PDT_TAPE, MVP_LTO5, "Write errors (lto-5)", "we_",
+     NULL},                             /* 0x33, 0  SSC */
+    {0x34, 0, 0, PDT_TAPE, MVP_LTO5, "Read forward errors (lto-5)",
+     "rfe_", NULL},                             /* 0x34, 0  SSC */
+    {0x35, 0, 0, PDT_TAPE, OVP_LTO, "DT Device Error (lto-5, 6)",
+     "dtde_", NULL},                             /* 0x35, 0  SSC */
+    {0x37, 0, 0, PDT_DISK, MVP_SEAG, "Cache (seagate)", "c_se",
+     show_seagate_cache_page},          /* 0x37, 0  SBC */
+    {0x37, 0, 0, PDT_DISK, MVP_HITA, "Miscellaneous (hitachi)", "mi_hi",
+     show_hgst_misc_page},                             /* 0x37, 0  SBC */
+    {0x37, 0, 0, PDT_TAPE, MVP_LTO5, "Performance characteristics "
+     "(lto-5)", "pc_", NULL},                             /* 0x37, 0  SSC */
+    {0x38, 0, 0, PDT_TAPE, MVP_LTO5, "Blocks/bytes transferred "
+     "(lto-5)", "bbt_", NULL},                             /* 0x38, 0  SSC */
+    {0x39, 0, 0, PDT_TAPE, MVP_LTO5, "Host port 0 interface errors "
+     "(lto-5)", "hp0_", NULL},                             /* 0x39, 0  SSC */
+    {0x3a, 0, 0, PDT_TAPE, MVP_LTO5, "Drive control verification "
+     "(lto-5)", "dcv_", NULL},                             /* 0x3a, 0  SSC */
+    {0x3b, 0, 0, PDT_TAPE, MVP_LTO5, "Host port 1 interface errors "
+     "(lto-5)", "hp1_", NULL},                             /* 0x3b, 0  SSC */
+    {0x3c, 0, 0, PDT_TAPE, MVP_LTO5, "Drive usage information "
+     "(lto-5)", "dui_", NULL},                             /* 0x3c, 0  SSC */
+    {0x3d, 0, 0, PDT_TAPE, MVP_LTO5, "Subsystem statistics (lto-5)",
+     "ss_", NULL},                             /* 0x3d, 0  SSC */
+    {0x3e, 0, 0, PDT_DISK, MVP_SEAG, "Factory (seagate)", "f_se",
+     show_seagate_factory_page},        /* 0x3e, 0  SBC */
+    {0x3e, 0, 0, PDT_DISK, MVP_HITA, "Factory (hitachi)", "f_hi",
+     NULL},                             /* 0x3e, 0  SBC */
+    {0x3e, 0, 0, PDT_TAPE, OVP_LTO, "Device Status (lto-5, 6)",
+     "ds_", NULL},                             /* 0x3e, 0  SSC */
+
+    {-1, -1, -1, -1, 0, NULL, "zzzzz", NULL},           /* end sentinel */
+};
+
+/* Supported vendor product codes */
+/* Arrange in alphabetical order by acronym */
+static struct vp_name_t vp_arr[] = {
+    {VP_SEAG, "sea", "Seagate", "SEAGATE", NULL},
+    {VP_HITA, "hit", "Hitachi", "HGST", NULL},
+    {VP_HITA, "wdc", "WDC/Hitachi", "WDC", NULL},
+    {VP_TOSH, "tos", "Toshiba", "TOSHIBA", NULL},
+    {VP_LTO5, "lto5", "LTO-5 (tape drive consortium)", NULL, NULL},
+    {VP_LTO6, "lto6", "LTO-6 (tape drive consortium)", NULL, NULL},
+    {VP_ALL, "all", "enumerate all vendor specific", NULL, NULL},
+    {0, NULL, NULL, NULL, NULL},
+};
+
+static char t10_vendor_str[10];
+static char t10_product_str[18];
+
+#ifdef SG_LIB_WIN32
+static bool win32_spt_init_state = false;
+static bool win32_spt_curr_state = false;
+#endif
+
+
+static void
+usage(int hval)
+{
+    if (1 == hval) {
+        pr2serr(
+           "Usage: sg_logs [-ALL] [--all] [--brief] [--control=PC] "
+           "[--enumerate]\n"
+           "               [--exclude] [--filter=FL] [--full] [--help] "
+           "[--hex]\n"
+           "               [--in=FN] [--json[=JO]] [--list] [--maxlen=LEN] "
+           "[--name]\n"
+           "               [--no_inq] [--page=PG] [--paramp=PP] [--pcb] "
+           "[--ppc]\n"
+           "               [--pdt=DT] [--raw] [--readonly] [--reset] "
+           "[--select]\n"
+           "               [--sp] [--temperature] [--transport] "
+           "[--undefined]\n"
+           "               [--vendor=VP] [--verbose] [--version] DEVICE\n"
+           "  where the main options are:\n"
+           "    --ALL|-A        fetch and decode all log pages and "
+           "subpages\n"
+           "    --all|-a        fetch and decode all log pages, but not "
+           "subpages; use\n"
+           "                    twice to fetch and decode all log pages "
+           "and subpages\n"
+           "    --brief|-b      shorten the output of some log pages\n"
+           "    --enumerate|-e    enumerate known pages, ignore DEVICE. "
+           "Sort order,\n"
+           "                      '-e': all by acronym; '-ee': non-vendor "
+           "by acronym;\n"
+           "                      '-eee': all numerically; '-eeee': "
+           "non-v numerically\n"
+           "    --filter=FL|-f FL    FL is parameter code to display (def: "
+           "all);\n"
+           "                         with '-e' then FL>=0 enumerate that "
+           "pdt + spc\n"
+           "                         FL=-1 all (default), FL=-2 spc only\n"
+           "    --full|-F       drill down in application client log page\n"
+           "    --help|-h       print usage message then exit. Use twice "
+           "for more help\n"
+           "    --hex|-H        output response in hex (default: decode if "
+           "known)\n"
+           "    --in=FN|-i FN    FN is a filename containing a log page "
+           "in ASCII hex\n"
+           "                     or binary if --raw also given. --inhex=FN "
+           "also accepted\n"
+           "    --json[=JO]|-j[JO]    output in JSON instead of human "
+            "readable\n"
+            "                          test. Use --json=? for JSON help\n"
+           "    --list|-l       list supported log pages; twice: list "
+           "supported log\n"
+           "                    pages and subpages page; thrice: merge of "
+           "both pages\n"
+           "    --page=PG|-p PG    PG is either log page acronym, PGN or "
+           "PGN,SPGN\n"
+           "                       where (S)PGN is a (sub) page number\n");
+        pr2serr(
+           "    --raw|-r        either output response in binary to stdout "
+           "or, if\n"
+           "                    '--in=FN' is given, FN is decoded as "
+           "binary\n"
+           "    --temperature|-t    decode temperature (log page 0xd or "
+           "0x2f)\n"
+           "    --transport|-T    decode transport (protocol specific port "
+           "0x18) page\n"
+           "    --vendor=VP|-M VP    vendor/product abbreviation [or "
+           "number]\n"
+           "    --verbose|-v    increase verbosity\n\n"
+           "Performs a SCSI LOG SENSE (or LOG SELECT) command and decodes "
+           "the response.\nIf only DEVICE is given then '-p sp' (supported "
+           "pages) is assumed. Use\n'-e' to see known pages and their "
+           "acronyms. For more help use '-hh'.\n");
+    } else if (hval > 1) {
+        pr2serr(
+           "  where sg_logs' lesser used options are:\n"
+           "    --control=PC|-c PC    page control(PC) (default: 1)\n"
+           "                          0: current threshold, 1: current "
+           "cumulative\n"
+           "                          2: default threshold, 3: default "
+           "cumulative\n"
+           "    --exclude|-E    exclude vendor specific pages and "
+           "parameters\n"
+           "    --list|-l       list supported log page names (equivalent to "
+           "'-p sp')\n"
+           "                    use twice to list supported log page and "
+           "subpage names\n"
+           "    --maxlen=LEN|-m LEN    max response length (def: 0 "
+           "-> everything)\n"
+           "                           when > 1 will request LEN bytes\n"
+           "    --name|-n       decode some pages into multiple name=value "
+           "lines\n"
+           "    --no_inq|-x     no initial INQUIRY output (twice: and no "
+           "INQUIRY call)\n"
+           "    --old|-O        use old interface (use as first option)\n"
+           "    --paramp=PP|-P PP    place PP in parameter pointer field in "
+           "cdb (def: 0)\n"
+           "    --pcb|-q        show parameter control bytes in decoded "
+           "output\n"
+           "    --ppc|-Q        set the Parameter Pointer Control (PPC) bit "
+           "(def: 0)\n"
+           "    --pdt=DT|-D DT    DT is peripheral device type to use with "
+           "'--in=FN'\n"
+           "                      or when '--no_inq' is used\n"
+           "    --readonly|-X    open DEVICE read-only (def: first "
+           "read-write then if\n"
+           "                     fails try open again read-only)\n"
+           "    --reset|-R      reset log parameters (takes PC and SP into "
+           "account)\n"
+           "                    (uses PCR bit in LOG SELECT)\n"
+           "    --select|-S     perform LOG SELECT (def: LOG SENSE)\n"
+           "    --sp|-s         set the Saving Parameters (SP) bit (def: "
+           "0)\n"
+           "    --undefined|-u    hex format for undefined/unrecognized "
+           "fields,\n"
+           "                      use one or more times; format as per "
+           "--hex\n"
+           "    --version|-V    output version string then exit\n\n"
+           "If DEVICE and --select are given, a LOG SELECT command will be "
+           "issued.\nIf DEVICE is not given and '--in=FN' is given then FN "
+           "will decoded as if\nit were a log page. The contents of FN "
+           "generated by either a prior\n'sg_logs -HHH ...' invocation or "
+           "by a text editor.\nLog pages defined in SPC are common "
+           "to all device types.\n");
+    }
+}
+
+static void
+usage_old()
+{
+    printf("Usage: sg_logs [-a] [-A] [-b] [-c=PC] [-D=DT] [-e] [-E] [-f=FL] "
+           "[-F]\n"
+           "               [-h] [-H] [-i=FN] [-l] [-L] [-m=LEN] [-M=VP] "
+           "[-n] [-p=PG]\n"
+           "               [-paramp=PP] [-pcb] [-ppc] [-r] [-select] [-sp] "
+           "[-t] [-T]\n"
+           "               [-u] [-v] [-V] [-x] [-X] [-?] DEVICE\n"
+           "  where:\n"
+           "    -a     fetch and decode all log pages\n"
+           "    -A     fetch and decode all log pages and subpages\n"
+           "    -b     shorten the output of some log pages\n"
+           "    -c=PC    page control(PC) (default: 1)\n"
+           "                  0: current threshold, 1: current cumulative\n"
+           "                  2: default threshold, 3: default cumulative\n"
+           "    -e     enumerate known log pages\n"
+           "    -D=DT    DT is peripheral device type to use with "
+           "'--in=FN'\n"
+           "    -E     exclude vendor specific pages and parameters\n"
+           "    -f=FL    filter match parameter code or pdt\n"
+           "    -F     drill down in application client log page\n"
+           "    -h     output in hex (default: decode if known)\n"
+           "    -H     output in hex (same as '-h')\n"
+           "    -i=FN    FN is a filename containing a log page "
+           "in ASCII hex.\n"
+           "    -l     list supported log page names (equivalent to "
+           "'-p=0')\n"
+           "    -L     list supported log page and subpages names "
+           "(equivalent to\n"
+           "           '-p=0,ff')\n"
+           "    -m=LEN   max response length (decimal) (def: 0 "
+           "-> everything)\n"
+           "    -M=VP    vendor/product abbreviation [or number]\n"
+           "    -n       decode some pages into multiple name=value "
+           "lines\n"
+           "    -N|--new    use new interface\n"
+           "    -p=PG    PG is an acronym (def: 'sp')\n"
+           "    -p=PGN    page code in hex (def: 0)\n"
+           "    -p=PGN,SPGN    page and subpage codes in hex, (defs: 0,0)\n"
+           "    -paramp=PP   (in hex) (def: 0)\n"
+           "    -pcb     show parameter control bytes in decoded "
+           "output\n");
+    printf("    -ppc     set the Parameter Pointer Control (PPC) bit "
+           "(def: 0)\n"
+           "    -r       reset log parameters (takes PC and SP into "
+           "account)\n"
+           "             (uses PCR bit in LOG SELECT)\n"
+           "    -select  perform LOG SELECT (def: LOG SENSE)\n"
+           "    -sp      set the Saving Parameters (SP) bit (def: 0)\n"
+           "    -t       outputs temperature log page (0xd)\n"
+           "    -T       outputs transport (protocol specific port) log "
+           "page (0x18)\n"
+           "    -u       hex format for undefined/unrecognized fields\n"
+           "    -v       increase verbosity\n"
+           "    -V       output version string\n"
+           "    -x       no initial INQUIRY output (twice: no INQUIRY call)\n"
+           "    -X       open DEVICE read-only (def: first read-write then "
+           "if fails\n"
+           "             try open again with read-only)\n"
+           "    -?       output this usage message\n\n"
+           "Performs a SCSI LOG SENSE (or LOG SELECT) command\n");
+}
+
+/* Return vendor product mask given vendor product number */
+static int
+get_vp_mask(int vpn)
+{
+    if (vpn < 0)
+        return 0;
+    else
+        return (vpn >= (32 - MVP_OFFSET)) ?  OVP_ALL :
+                                             (1 << (vpn + MVP_OFFSET));
+}
+
+static int
+asort_comp(const void * lp, const void * rp)
+{
+    const struct log_elem * const * lepp =
+                (const struct log_elem * const *)lp;
+    const struct log_elem * const * repp =
+                (const struct log_elem * const *)rp;
+
+    return strcmp((*lepp)->acron, (*repp)->acron);
+}
+
+static void
+enumerate_helper(const struct log_elem * lep, bool first,
+                 const struct opts_t * op)
+{
+    char b[80];
+    char bb[80];
+    const char * cp;
+    bool vendor_lpage = ! (MVP_STD & lep->flags);
+
+    if (first) {
+        if (1 == op->verbose) {
+            printf("acronym   pg[,spg]        name\n");
+            printf("===============================================\n");
+        } else if (2 == op->verbose) {
+            printf("acronym   pg[,spg]        pdt   name\n");
+            printf("===================================================\n");
+        }
+    }
+    if ((0 == (op->do_enumerate % 2)) && vendor_lpage)
+        return;     /* if do_enumerate is even then skip vendor pages */
+    else if ((! op->filter_given) || (-1 == op->filter))
+        ;           /* otherwise enumerate all lpages if no --filter= */
+    else if (-2 == op->filter) {   /* skip non-SPC pages */
+        if (lep->pdt >= 0)
+            return;
+    } else if (-10 == op->filter) {   /* skip non-disk like pages */
+        if (sg_lib_pdt_decay(lep->pdt) != 0)
+            return;
+    } else if (-11 == op->filter) {   /* skip tape like device pages */
+        if (sg_lib_pdt_decay(lep->pdt) != 1)
+            return;
+    } else if ((op->filter >= 0) && (op->filter <= 0x1f)) {
+        if ((lep->pdt >= 0) && (lep->pdt != op->filter) &&
+            (lep->pdt != sg_lib_pdt_decay(op->filter)))
+            return;
+    }
+    if (op->vend_prod_num >= 0) {
+        if (! (lep->flags & get_vp_mask(op->vend_prod_num)))
+            return;
+    }
+    if (op->deduced_vpn >= 0) {
+        if (! (lep->flags & get_vp_mask(op->deduced_vpn)))
+            return;
+    }
+    if (lep->subpg_high > 0)
+        snprintf(b, sizeof(b), "0x%x,0x%x->0x%x", lep->pg_code,
+                 lep->subpg_code, lep->subpg_high);
+    else if (lep->subpg_code > 0)
+        snprintf(b, sizeof(b), "0x%x,0x%x", lep->pg_code,
+                 lep->subpg_code);
+    else
+        snprintf(b, sizeof(b), "0x%x", lep->pg_code);
+    snprintf(bb, sizeof(bb), "%-16s", b);
+    cp = (op->verbose && (! lep->show_pagep)) ? " [hex only]" : "";
+    if (op->verbose > 1) {
+        if (lep->pdt < 0)
+            printf("  %-8s%s-     %s%s\n", lep->acron, bb, lep->name, cp);
+        else
+            printf("  %-8s%s0x%02x  %s%s\n", lep->acron, bb, lep->pdt,
+                   lep->name, cp);
+    } else
+        printf("  %-8s%s%s%s\n", lep->acron, bb, lep->name, cp);
+}
+
+static void
+enumerate_pages(const struct opts_t * op)
+{
+    int j;
+    struct log_elem * lep;
+    struct log_elem ** lep_arr;
+
+    if (op->do_enumerate < 3) { /* -e, -ee: sort by acronym */
+        int k;
+        struct log_elem ** lepp;
+
+        for (k = 0, lep = log_arr; lep->pg_code >=0; ++lep, ++k)
+            ;
+        ++k;
+        lep_arr = (struct log_elem **)calloc(k, sizeof(struct log_elem *));
+        if (NULL == lep_arr) {
+            pr2serr("%s: out of memory\n", __func__);
+            return;
+        }
+        for (k = 0, lep = log_arr; lep->pg_code >=0; ++lep, ++k)
+            lep_arr[k] = lep;
+        lep_arr[k++] = lep;     /* put sentinel on end */
+        qsort(lep_arr, k, sizeof(struct log_elem *), asort_comp);
+        printf("Known log pages in acronym order:\n");
+        for (lepp = lep_arr, j = 0; (*lepp)->pg_code >=0; ++lepp, ++j)
+            enumerate_helper(*lepp, (0 == j), op);
+        free(lep_arr);
+    } else {    /* -eee, -eeee numeric sort (as per table) */
+        printf("Known log pages in numerical order:\n");
+        for (lep = log_arr, j = 0; lep->pg_code >=0; ++lep, ++j)
+            enumerate_helper(lep, (0 == j), op);
+    }
+}
+
+static const struct log_elem *
+acron_search(const char * acron)
+{
+    const struct log_elem * lep;
+
+    for (lep = log_arr; lep->pg_code >=0; ++lep) {
+        if (0 == strcmp(acron, lep->acron))
+            return lep;
+    }
+    return NULL;
+}
+
+static int
+find_vpn_by_acron(const char * vp_ap)
+{
+    const struct vp_name_t * vpp;
+
+    for (vpp = vp_arr; vpp->acron; ++vpp) {
+        size_t k;
+        size_t len = strlen(vpp->acron);
+
+        for (k = 0; k < len; ++k) {
+            if (tolower((uint8_t)vp_ap[k]) != (uint8_t)vpp->acron[k])
+                break;
+        }
+        if (k < len)
+            continue;
+        return vpp->vend_prod_num;
+    }
+    return VP_NONE;
+}
+
+/* Find vendor product number using T10 VENDOR and PRODUCT ID fields in a
+   INQUIRY response. */
+static int
+find_vpn_by_inquiry(void)
+{
+    size_t len;
+    size_t t10_v_len = strlen(t10_vendor_str);
+    size_t t10_p_len = strlen(t10_product_str);
+    const struct vp_name_t * vpp;
+
+    if ((0 == t10_v_len) && (0 == t10_p_len))
+        return VP_NONE;
+    for (vpp = vp_arr; vpp->acron; ++vpp) {
+        bool matched = false;
+
+        if (vpp->t10_vendorp && (t10_v_len > 0)) {
+            len = strlen(vpp->t10_vendorp);
+            len = (len > t10_v_len) ? t10_v_len : len;
+            if (strncmp(vpp->t10_vendorp, t10_vendor_str, len))
+                continue;
+            matched = true;
+        }
+        if (vpp->t10_productp && (t10_p_len > 0)) {
+            len = strlen(vpp->t10_productp);
+            len = (len > t10_p_len) ? t10_p_len : len;
+            if (strncmp(vpp->t10_productp, t10_product_str, len))
+                continue;
+            matched = true;
+        }
+        if (matched)
+            return vpp->vend_prod_num;
+    }
+    return VP_NONE;
+}
+
+static void
+enumerate_vp(void)
+{
+    const struct vp_name_t * vpp;
+    bool seen = false;
+
+    for (vpp = vp_arr; vpp->acron; ++vpp) {
+        if (vpp->name) {
+            if (! seen) {
+                printf("\nVendor/product identifiers:\n");
+                seen = true;
+            }
+            printf("  %-10s %d      %s\n", vpp->acron,
+                   vpp->vend_prod_num, vpp->name);
+        }
+    }
+}
+
+static const struct log_elem *
+pg_subpg_pdt_search(int pg_code, int subpg_code, int pdt, int vpn)
+{
+    const struct log_elem * lep;
+    int d_pdt;
+    int vp_mask = get_vp_mask(vpn);
+
+    d_pdt = sg_lib_pdt_decay(pdt);
+    for (lep = log_arr; lep->pg_code >=0; ++lep) {
+        if (pg_code == lep->pg_code) {
+            if (subpg_code == lep->subpg_code) {
+                if ((MVP_STD & lep->flags) || (0 == vp_mask) ||
+                    (vp_mask & lep->flags))
+                    ;
+                else
+                    continue;
+                if ((lep->pdt < 0) || (pdt == lep->pdt) || (pdt < 0))
+                    return lep;
+                else if (d_pdt == lep->pdt)
+                    return lep;
+                else if (pdt == sg_lib_pdt_decay(lep->pdt))
+                    return lep;
+            } else if ((lep->subpg_high > 0) &&
+                     (subpg_code > lep->subpg_code) &&
+                     (subpg_code <= lep->subpg_high))
+                return lep;
+        }
+    }
+    return NULL;
+}
+
+static void
+js_snakenv_ihexstr_nex(sgj_state * jsp, sgj_opaque_p jop,
+                       const char * conv2sname, int64_t val_i,
+                       bool hex_as_well, const char * str_name,
+                       const char * val_s, const char * nex_s)
+{
+
+    if ((NULL == jsp) || (NULL == jop))
+        return;
+    if (sgj_is_snake_name(conv2sname))
+        sgj_js_nv_ihexstr_nex(jsp, jop, conv2sname, val_i, hex_as_well,
+                              str_name, val_s, nex_s);
+    else {
+        char b[128];
+
+        sgj_convert_to_snake_name(conv2sname, b, sizeof(b));
+        sgj_js_nv_ihexstr_nex(jsp, jop, b, val_i, hex_as_well, str_name,
+                              val_s, nex_s);
+    }
+}
+
+static void
+usage_for(int hval, const struct opts_t * op)
+{
+    if (op->opt_new)
+        usage(hval);
+    else
+        usage_old();
+}
+
+/* Processes command line options according to new option format. Returns
+ * 0 is ok, else SG_LIB_SYNTAX_ERROR is returned. */
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    while (1) {
+        int c, n;
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "aAbc:D:eEf:FhHi:j::lLm:M:nNOp:P:qQrRsStT"
+                        "uvVxX", long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            ++op->do_all;
+            break;
+        case 'A':
+            op->do_all += 2;
+            break;
+        case 'b':
+            ++op->do_brief;
+            break;
+        case 'c':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 3)) {
+                pr2serr("bad argument to '--control='\n");
+                usage(2);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->page_control = n;
+            break;
+        case 'D':
+            if (0 == memcmp("-1", optarg, 3))
+                op->dev_pdt = -1;
+            else {
+                n = sg_get_num(optarg);
+                if ((n < 0) || (n > 31)) {
+                    pr2serr("bad argument to '--pdt='\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->dev_pdt = n;
+            }
+            break;
+        case 'e':
+            ++op->do_enumerate;
+            break;
+        case 'E':
+            op->exclude_vendor = true;
+            break;
+        case 'f':
+            if ('-' == optarg[0]) {
+                n = sg_get_num(optarg + 1);
+                if ((n < 0) || (n > 0x30)) {
+                    pr2serr("bad negated argument to '--filter='\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->filter = -n;
+            } else {
+                n = sg_get_num(optarg);
+                if ((n < 0) || (n > 0xffff)) {
+                    pr2serr("bad argument to '--filter='\n");
+                    usage(1);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->filter = n;
+            }
+            op->filter_given = true;
+            break;
+        case 'F':
+            op->do_full = true;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'i':
+            op->in_fn = optarg;
+            break;
+        case 'j':
+           if (! sgj_init_state(&op->json_st, optarg)) {
+                int bad_char = op->json_st.first_bad_char;
+                char e[1500];
+
+                if (bad_char) {
+                    pr2serr("bad argument to --json= option, unrecognized "
+                            "character '%c'\n\n", bad_char);
+                }
+                sg_json_usage(0, e, sizeof(e));
+                pr2serr("%s", e);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'l':
+            ++op->do_list;
+            break;
+        case 'L':
+            op->do_list += 2;
+            break;
+        case 'm':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (1 == n)) {
+                pr2serr("bad argument to '--maxlen=', from 2 and up "
+                        "expected\n");
+                usage(2);
+                return SG_LIB_SYNTAX_ERROR;
+            } else if (n < 4) {
+                pr2serr("Warning: setting '--maxlen' to 4\n");
+                n = 4;
+            }
+            op->maxlen = n;
+            op->maxlen_given = true;
+            break;
+        case 'M':
+            if (op->vend_prod) {
+                pr2serr("only one '--vendor=' option permitted\n");
+                usage(2);
+                return SG_LIB_SYNTAX_ERROR;
+            } else
+                op->vend_prod = optarg;
+            break;
+        case 'n':
+            op->do_name = true;
+            break;
+        case 'N':
+            break;      /* ignore */
+        case 'O':
+            op->opt_new = false;
+            return 0;
+        case 'p':
+            op->pg_arg = optarg;
+            break;
+        case 'P':
+            n = sg_get_num(optarg);
+            if (n < 0) {
+                pr2serr("bad argument to '--paramp='\n");
+                usage(2);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->paramp = n;
+            break;
+        case 'q':
+            op->do_pcb = true;
+            break;
+        case 'Q':       /* N.B. PPC bit obsoleted in SPC-4 rev 18 */
+            op->do_ppc = true;
+            break;
+        case 'r':
+            op->do_raw = true;
+            break;
+        case 'R':
+            op->do_pcreset = true;
+            op->do_select = true;
+            break;
+        case 's':
+            op->do_sp = true;
+            break;
+        case 'S':
+            op->do_select = true;
+            break;
+        case 't':
+            op->do_temperature = true;
+            break;
+        case 'T':
+            op->do_transport = true;
+            break;
+        case 'u':
+            ++op->undefined_hex;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'x':
+            ++op->no_inq;
+            break;
+        case 'X':
+            op->o_readonly = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+/* Processes command line options according to old option format. Returns
+ * 0 is ok, else SG_LIB_SYNTAX_ERROR is returned. */
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool jmp_out;
+    int k, num, n;
+    unsigned int u, uu;
+    const char * cp;
+
+    for (k = 1; k < argc; ++k) {
+        int plen;
+
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case 'a':
+                    ++op->do_all;
+                    break;
+                case 'A':
+                    op->do_all += 2;
+                    break;
+                case 'b':
+                    ++op->do_brief;
+                    break;
+                case 'e':
+                    ++op->do_enumerate;
+                    break;
+                case 'E':
+                    op->exclude_vendor = true;
+                    break;
+                case 'F':
+                    op->do_full = true;
+                    break;
+                case 'h':
+                case 'H':
+                    ++op->do_hex;
+                    break;
+                case 'l':
+                    ++op->do_list;
+                    break;
+                case 'L':
+                    op->do_list += 2;
+                    break;
+                case 'n':
+                    op->do_name = true;
+                    break;
+                case 'N':
+                    op->opt_new = true;
+                    return 0;
+                case 'O':
+                    break;
+                case 'r':
+                    op->do_pcreset = true;
+                    op->do_select = true;
+                    break;
+                case 't':
+                    op->do_temperature = true;
+                    break;
+                case 'T':
+                    op->do_transport = true;
+                    break;
+                case 'u':
+                    ++op->undefined_hex;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                case 'x':
+                    ++op->no_inq;
+                    break;
+                case 'X':
+                    op->o_readonly = true;
+                    break;
+                case '?':
+                    ++op->do_help;
+                    break;
+                case '-':
+                    ++cp;
+                    jmp_out = true;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            if (0 == strncmp("c=", cp, 2)) {
+                num = sscanf(cp + 2, "%6x", &u);
+                if ((1 != num) || (u > 3)) {
+                    pr2serr("Bad page control after '-c=' option [0..3]\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->page_control = u;
+            } else if (0 == strncmp("D=", cp, 2)) {
+                n = sg_get_num(cp + 2);
+                if ((n < 0) || (n > 31)) {
+                    pr2serr("Bad argument after '-D=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->dev_pdt = n;
+            } else if (0 == strncmp("f=", cp, 2)) {
+                n = sg_get_num(cp + 2);
+                if ((n < 0) || (n > 0xffff)) {
+                    pr2serr("Bad argument after '-f=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->filter = n;
+                op->filter_given = true;
+            } else if (0 == strncmp("i=", cp, 2))
+                op->in_fn = cp + 2;
+            else if (0 == strncmp("m=", cp, 2)) {
+                num = sscanf(cp + 2, "%8d", &n);
+                if ((1 != num) || (n < 0)) {
+                    pr2serr("Bad maximum response length after '-m=' "
+                            "option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->maxlen_given = true;
+                op->maxlen = n;
+            } else if (0 == strncmp("M=", cp, 2)) {
+                if (op->vend_prod) {
+                    pr2serr("only one '-M=' option permitted\n");
+                    usage(2);
+                    return SG_LIB_SYNTAX_ERROR;
+                } else
+                    op->vend_prod = cp + 2;
+            } else if (0 == strncmp("p=", cp, 2)) {
+                const char * ccp = cp + 2;
+                const struct log_elem * lep;
+
+                if (isalpha((uint8_t)ccp[0])) {
+                    char * xp;
+                    char b[80];
+
+                    if (strlen(ccp) >= (sizeof(b) - 1)) {
+                        pr2serr("argument to '-p=' is too long\n");
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    strcpy(b, ccp);
+                    xp = (char *)strchr(b, ',');
+                    if (xp)
+                        *xp = '\0';
+                    lep = acron_search(b);
+                    if (NULL == lep) {
+                        pr2serr("bad argument to '--page=' no acronyn match "
+                                "to '%s'\n", b);
+                        pr2serr("  Try using '-e' or'-ee' to see available "
+                                "acronyns\n");
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    op->lep = lep;
+                    op->pg_code = lep->pg_code;
+                    if (xp) {
+                        n = sg_get_num_nomult(xp + 1);
+                        if ((n < 0) || (n > 255)) {
+                            pr2serr("Bad second value in argument to "
+                                    "'--page='\n");
+                            return SG_LIB_SYNTAX_ERROR;
+                        }
+                        op->subpg_code = n;
+                    } else
+                        op->subpg_code = lep->subpg_code;
+                } else {
+                    /* numeric arg: either 'pg_num' or 'pg_num,subpg_num' */
+                    if (NULL == strchr(cp + 2, ',')) {
+                        num = sscanf(cp + 2, "%6x", &u);
+                        if ((1 != num) || (u > 63)) {
+                            pr2serr("Bad page code value after '-p=' "
+                                    "option\n");
+                            usage_old();
+                            return SG_LIB_SYNTAX_ERROR;
+                        }
+                        op->pg_code = u;
+                    } else if (2 == sscanf(cp + 2, "%4x,%4x", &u, &uu)) {
+                        if (uu > 255) {
+                            pr2serr("Bad sub page code value after '-p=' "
+                                    "option\n");
+                            usage_old();
+                            return SG_LIB_SYNTAX_ERROR;
+                        }
+                        op->pg_code = u;
+                        op->subpg_code = uu;
+                    } else {
+                        pr2serr("Bad page code, subpage code sequence after "
+                                "'-p=' option\n");
+                        usage_old();
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                }
+            } else if (0 == strncmp("paramp=", cp, 7)) {
+                num = sscanf(cp + 7, "%8x", &u);
+                if ((1 != num) || (u > 0xffff)) {
+                    pr2serr("Bad parameter pointer after '-paramp=' "
+                            "option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->paramp = u;
+            } else if (0 == strncmp("pcb", cp, 3))
+                op->do_pcb = true;
+            else if (0 == strncmp("ppc", cp, 3))
+                op->do_ppc = true;
+            else if (0 == strncmp("select", cp, 6))
+                op->do_select = true;
+            else if (0 == strncmp("sp", cp, 2))
+                op->do_sp = true;
+            else if (0 == strncmp("old", cp, 3))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage_old();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == op->device_name)
+            op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not expecting: %s\n",
+                    op->device_name, cp);
+            usage_old();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+/* Process command line options. First check using new option format unless
+ * the SG3_UTILS_OLD_OPTS environment variable is defined which causes the
+ * old option format to be checked first. Both new and old format can be
+ * countermanded by a '-O' and '-N' options respectively. As soon as either
+ * of these options is detected (when processing the other format), processing
+ * stops and is restarted using the other format. Clear? */
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opt_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opt_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opt_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (0 == op->opt_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* Returns 'xp' with "unknown" if all bits set; otherwise decoded (big endian)
+ * number in 'xp'. Number rendered in decimal if pr_in_hex=false otherwise in
+ * hex with leading '0x' prepended. */
+static char *
+num_or_unknown(const uint8_t * xp, int num_bytes /* max is 8 */,
+               bool pr_in_hex, char * b, int blen)
+{
+    if (sg_all_ffs(xp, num_bytes))
+        snprintf(b, blen, "%s", unknown_s);
+    else {
+        uint64_t num = sg_get_unaligned_be(num_bytes, xp);
+
+        if (pr_in_hex)
+            snprintf(b, blen, "0x%" PRIx64, num);
+        else
+            snprintf(b, blen, "%" PRIu64, num);
+    }
+    return b;
+}
+
+/* Call LOG SENSE twice: the first time ask for 4 byte response to determine
+   actual length of response; then a second time requesting the
+   min(actual_len, mx_resp_len) bytes. If the calculated length for the
+   second fetch is odd then it is incremented (perhaps should be made modulo
+   4 in the future for SAS). Returns 0 if ok, SG_LIB_CAT_INVALID_OP for
+   log_sense not supported, SG_LIB_CAT_ILLEGAL_REQ for bad field in log sense
+   command, SG_LIB_CAT_NOT_READY, SG_LIB_CAT_UNIT_ATTENTION,
+   SG_LIB_CAT_ABORTED_COMMAND and -1 for other errors. */
+static int
+do_logs(int sg_fd, uint8_t * resp, int mx_resp_len,
+        const struct opts_t * op)
+{
+    int calc_len, request_len, res, resid, vb;
+
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+    if (! win32_spt_init_state) {
+        if (win32_spt_curr_state) {
+            if (mx_resp_len < 16384) {
+                scsi_pt_win32_direct(0);
+                win32_spt_curr_state = false;
+            }
+        } else {
+            if (mx_resp_len >= 16384) {
+                scsi_pt_win32_direct(SG_LIB_WIN32_DIRECT /* SPT direct */);
+                win32_spt_curr_state = true;
+            }
+        }
+    }
+#endif
+#endif
+    memset(resp, 0, mx_resp_len);
+    vb = op->verbose;
+    if (op->maxlen > 1)
+        request_len = mx_resp_len;
+    else {
+        request_len = LOG_SENSE_PROBE_ALLOC_LEN;
+        if ((res = sg_ll_log_sense_v2(sg_fd, op->do_ppc, op->do_sp,
+                                      op->page_control, op->pg_code,
+                                      op->subpg_code, op->paramp,
+                                      resp, request_len, LOG_SENSE_DEF_TIMEOUT,
+                                      &resid, true /* noisy */, vb)))
+            return res;
+        if (resid > 0) {
+            res = SG_LIB_WILD_RESID;
+            goto resid_err;
+        }
+        calc_len = sg_get_unaligned_be16(resp + 2) + 4;
+        if ((! op->do_raw) && (vb > 1)) {
+            pr2serr("  Log sense (find length) response:\n");
+            hex2stderr(resp, LOG_SENSE_PROBE_ALLOC_LEN, 1);
+            pr2serr("  hence calculated response length=%d\n", calc_len);
+        }
+        if (op->pg_code != (0x3f & resp[0])) {
+            if (vb)
+                pr2serr("Page code does not appear in first byte of "
+                        "response so it's suspect\n");
+            if (calc_len > 0x40) {
+                calc_len = 0x40;
+                if (vb)
+                    pr2serr("Trim response length to 64 bytes due to "
+                            "suspect response format\n");
+            }
+        }
+        /* Some HBAs don't like odd transfer lengths */
+        if (calc_len % 2)
+            calc_len += 1;
+        if (calc_len > mx_resp_len)
+            calc_len = mx_resp_len;
+        request_len = calc_len;
+    }
+    if ((res = sg_ll_log_sense_v2(sg_fd, op->do_ppc, op->do_sp,
+                                  op->page_control, op->pg_code,
+                                  op->subpg_code, op->paramp,
+                                  resp, request_len,
+                                  LOG_SENSE_DEF_TIMEOUT, &resid,
+                                  true /* noisy */, vb)))
+        return res;
+    if (resid > 0) {
+        request_len -= resid;
+        if (request_len < 4) {
+            request_len += resid;
+            res = SG_LIB_WILD_RESID;
+            goto resid_err;
+        }
+    }
+    if ((! op->do_raw) && (vb > 1)) {
+        pr2serr("  Log sense response:\n");
+        hex2stderr(resp, request_len, 1);
+    }
+    return 0;
+resid_err:
+    pr2serr("%s: request_len=%d, resid=%d, problems\n", __func__, request_len,
+            resid);
+    request_len -= resid;
+    if ((request_len > 0) && (! op->do_raw) && (vb > 1)) {
+        pr2serr("  Log sense (resid_err) response:\n");
+        hex2stderr(resp, request_len, 1);
+    }
+    return res;
+}
+
+sgj_opaque_p
+sg_log_js_hdr(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+              const uint8_t * log_hdrp)
+{
+    bool ds = !! (log_hdrp[0] & 0x80);
+    bool spf = !! (log_hdrp[0] & 0x40);
+    int pg = log_hdrp[0] & 0x3f;
+    int subpg = log_hdrp[1];
+    size_t nlen = strlen(name);
+    sgj_opaque_p jo2p;
+    char b[80];
+
+    if ((nlen < 4) || (0 != strcmp("age", name + nlen - 3))) {
+        memcpy(b, name, nlen);
+        memcpy(b + nlen, " log page", 10);
+        jo2p = sgj_snake_named_subobject_r(jsp, jop, b);
+    } else
+        jo2p = sgj_snake_named_subobject_r(jsp, jop, name);
+
+    sgj_js_nv_ihex_nex(jsp, jo2p, "ds", (int)ds, false, "Did not Save");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "spf", (int)spf, NULL, "SubPage Format");
+    sgj_js_nv_ihex(jsp, jo2p, "page_code", pg);
+    sgj_js_nv_ihex(jsp, jo2p, "subpage_code", subpg);
+    return jo2p;
+}
+
+
+
+/* DS made obsolete in spc4r03; TMC and ETC made obsolete in spc5r03. */
+static char *
+get_pcb_str(int pcb, char * outp, int maxoutlen)
+{
+    char buff[PCB_STR_LEN];
+    int n;
+
+    n = sprintf(buff, "du=%d [ds=%d] tsd=%d [etc=%d] ", ((pcb & 0x80) ? 1 : 0),
+                ((pcb & 0x40) ? 1 : 0), ((pcb & 0x20) ? 1 : 0),
+                ((pcb & 0x10) ? 1 : 0));
+    if (pcb & 0x10)
+        n += sprintf(buff + n, "[tmc=%d] ", ((pcb & 0xc) >> 2));
+#if 1
+    n += sprintf(buff + n, "format+linking=%d  [0x%.2x]", pcb & 3,
+                 pcb);
+#else
+    if (pcb & 0x1)
+        n += sprintf(buff + n, "lbin=%d ", ((pcb & 0x2) >> 1));
+    n += sprintf(buff + n, "lp=%d  [0x%.2x]", pcb & 0x1, pcb);
+#endif
+    if (outp && (n < maxoutlen)) {
+        memcpy(outp, buff, n);
+        outp[n] = '\0';
+    } else if (outp && (maxoutlen > 0))
+        outp[0] = '\0';
+    return outp;
+}
+
+static void
+js_pcb(sgj_state * jsp, sgj_opaque_p jop, int pcb)
+{
+    sgj_opaque_p jo2p = sgj_snake_named_subobject_r(jsp, jop,
+                                                    "parameter_control_byte");
+
+    sgj_js_nv_ihex_nex(jsp, jo2p, "du", (pcb & 0x80) ? 1 : 0, false,
+                       "Disable Update");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "ds", (pcb & 0x40) ? 1 : 0, false,
+                       "Disable Save [obsolete]");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "tsd", (pcb & 0x20) ? 1 : 0, false,
+                       "Target Save Disable");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "etc", (pcb & 0x10) ? 1 : 0, false,
+                       "Enable Threshold Comparison [obsolete]");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "tmc", (pcb & 0xc) >> 2, false,
+                       "Threshold Met Criteria [obsolete]");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "format_and_linking", pcb & 0x3, false,
+                       NULL);
+}
+
+/* SUPP_PAGES_LPAGE [0x0,0x0] <sp> */
+static bool
+show_supported_pgs_page(const uint8_t * resp, int len,
+                        struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, k;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p, jo3p;
+    sgj_opaque_p jap = NULL;
+    char b[64];
+    static const char * slpgs = "Supported log pages";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x0]:\n", slpgs);  /* introduced in: SPC-2 */
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if ((op->do_hex > 0) || op->do_raw) {
+        if (op->do_raw)
+            dStrRaw(resp, len);
+        else
+            hex2stdout(resp, len, op->dstrhex_no_ascii);
+        return true;
+    }
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, slpgs, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p, "supported_pages_list");
+    }
+
+    for (k = 0; k < num; ++k) {
+        int pg_code = bp[k] & 0x3f;
+        const struct log_elem * lep;
+
+        snprintf(b, sizeof(b) - 1, "  0x%02x        ", pg_code);
+        lep = pg_subpg_pdt_search(pg_code, 0, op->dev_pdt, -1);
+        if (lep) {
+            if (op->do_brief > 1)
+                sgj_pr_hr(jsp, "    %s\n", lep->name);
+            else if (op->do_brief)
+                sgj_pr_hr(jsp, "%s%s\n", b, lep->name);
+            else
+                sgj_pr_hr(jsp, "%s%s [%s]\n", b, lep->name, lep->acron);
+        } else
+            sgj_pr_hr(jsp, "%s\n", b);
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            sgj_js_nv_ihex(jsp, jo3p, "page_code", pg_code);
+            sgj_js_nv_s(jsp, jo3p, "name", lep ? lep->name : unknown_s);
+            sgj_js_nv_s(jsp, jo3p, "acronym", lep ? lep->acron : unknown_s);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        }
+    }
+    return true;
+}
+
+/* SUPP_PAGES_LPAGE,SUPP_SPGS_SUBPG [0x0,0xff] <ssp> or all subpages of a
+ * given page code: [<pg_code>,0xff] where <pg_code> > 0 */
+static bool
+show_supported_pgs_sub_page(const uint8_t * resp, int len,
+                            struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, k;
+    const uint8_t * bp;
+    const struct log_elem * lep = NULL;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p, jo3p;
+    sgj_opaque_p jap = NULL;
+    char b[64];
+    static const char * slpass = "Supported log pages and subpages";
+    static const char * sss = "Supported subpages";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (op->pg_code > 0)
+            sgj_pr_hr(jsp, "%s  [0x%x, 0xff]:\n", sss, op->pg_code);
+        else
+            sgj_pr_hr(jsp, "%s  [0x0, 0xff]:\n", sss);
+    }
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if ((op->do_hex > 0) || op->do_raw) {
+        if (op->do_raw)
+            dStrRaw(resp, len);
+        else
+            hex2stdout(resp, len, op->dstrhex_no_ascii);
+        return true;
+    }
+    if (jsp->pr_as_json) {
+        if (op->pg_code > 0) {
+            jo2p = sg_log_js_hdr(jsp, jop, sss, resp);
+            jap = sgj_named_subarray_r(jsp, jo2p,
+                                       "supported_subpage_descriptors");
+        } else {
+            jo2p = sg_log_js_hdr(jsp, jop, slpass, resp);
+            jap = sgj_named_subarray_r(jsp, jo2p,
+                               "supported_page_subpage_descriptors");
+        }
+    }
+
+    for (k = 0; k < num; k += 2) {
+        bool pr_name = true;
+        int pg_code = bp[k];
+        int subpg_code = bp[k + 1];
+
+        /* formerly ignored [pg, 0xff] when pg > 0, don't know why */
+        if (NOT_SPG_SUBPG == subpg_code)
+            snprintf(b, sizeof(b) - 1, "  0x%02x        ", pg_code);
+        else
+            snprintf(b, sizeof(b) - 1, "  0x%02x,0x%02x   ", pg_code,
+                     subpg_code);
+        if ((pg_code > 0) && (subpg_code == 0xff)) {
+            sgj_pr_hr(jsp, "%s\n", b);
+            pr_name = false;
+        } else {
+            lep = pg_subpg_pdt_search(pg_code, subpg_code, op->dev_pdt, -1);
+            if (lep) {
+                if (op->do_brief > 1)
+                    sgj_pr_hr(jsp, "    %s\n", lep->name);
+                else if (op->do_brief)
+                    sgj_pr_hr(jsp, "%s%s\n", b, lep->name);
+                else
+                    sgj_pr_hr(jsp, "%s%s [%s]\n", b, lep->name, lep->acron);
+            } else
+                sgj_pr_hr(jsp, "%s\n", b);
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            sgj_js_nv_ihex(jsp, jo3p, "page_code", pg_code);
+            sgj_js_nv_ihex(jsp, jo3p, "subpage_code", subpg_code);
+            if (pr_name) {
+                sgj_js_nv_s(jsp, jo3p, "name", lep ? lep->name : unknown_s);
+                sgj_js_nv_s(jsp, jo3p, "acronym", lep ? lep->acron :
+                                                        unknown_s);
+            }
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        }
+    }
+    return true;
+}
+
+/* BUFF_OVER_UNDER_LPAGE [0x1] <bou>  introduced: SPC-2 */
+static bool
+show_buffer_over_under_run_page(const uint8_t * resp, int len,
+                                struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    uint64_t count;
+    const uint8_t * bp;
+    const char * cp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    static const char * bourlp = "Buffer over-run/under-run log page";
+    static const char * orurc = "over_run_under_run_counter";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x1]\n", bourlp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, bourlp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                        "buffer_over_run_under_run_log_parameters");
+    }
+    while (num > 3) {
+        cp = NULL;
+        pl = bp[3] + 4;
+        count = (pl > 4) ? sg_get_unaligned_be(pl - 4, bp + 4) : 0;
+        pc = sg_get_unaligned_be16(bp + 0);
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        switch (pc) {
+        case 0x0:
+            cp = "under-run";
+            break;
+        case 0x1:
+            cp = "over-run";
+            break;
+        case 0x2:
+            cp = "service delivery subsystem busy, under-run";
+            break;
+        case 0x3:
+            cp = "service delivery subsystem busy, over-run";
+            break;
+        case 0x4:
+            cp = "transfer too slow, under-run";
+            break;
+        case 0x5:
+            cp = "transfer too slow, over-run";
+            break;
+        case 0x20:
+            cp = "command, under-run";
+            break;
+        case 0x21:
+            cp = "command, over-run";
+            break;
+        case 0x22:
+            cp = "command, service delivery subsystem busy, under-run";
+            break;
+        case 0x23:
+            cp = "command, service delivery subsystem busy, over-run";
+            break;
+        case 0x24:
+            cp = "command, transfer too slow, under-run";
+            break;
+        case 0x25:
+            cp = "command, transfer too slow, over-run";
+            break;
+        case 0x40:
+            cp = "I_T nexus, under-run";
+            break;
+        case 0x41:
+            cp = "I_T nexus, over-run";
+            break;
+        case 0x42:
+            cp = "I_T nexus, service delivery subsystem busy, under-run";
+            break;
+        case 0x43:
+            cp = "I_T nexus, service delivery subsystem busy, over-run";
+            break;
+        case 0x44:
+            cp = "I_T nexus, transfer too slow, under-run";
+            break;
+        case 0x45:
+            cp = "I_T nexus, transfer too slow, over-run";
+            break;
+        case 0x80:
+            cp = "time, under-run";
+            break;
+        case 0x81:
+            cp = "time, over-run";
+            break;
+        case 0x82:
+            cp = "time, service delivery subsystem busy, under-run";
+            break;
+        case 0x83:
+            cp = "time, service delivery subsystem busy, over-run";
+            break;
+        case 0x84:
+            cp = "time, transfer too slow, under-run";
+            break;
+        case 0x85:
+            cp = "time, transfer too slow, over-run";
+            break;
+        default:
+            pr2serr("  undefined %s [0x%x], count = %" PRIu64 "\n",
+                    param_c, pc, count);
+            break;
+        }
+            sgj_js_nv_ihex(jsp, jo3p, param_c_sn, pc);
+        sgj_pr_hr(jsp, "  %s=0x%x\n", param_c, pc);
+        if (cp) {
+            sgj_pr_hr(jsp, "    %s = %" PRIu64 "\n", cp, count);
+            js_snakenv_ihexstr_nex(jsp, jo3p, param_c, pc, true,
+                                   NULL, cp, NULL);
+            sgj_js_nv_ihex(jsp, jo3p, orurc, count);
+        } else
+            sgj_pr_hr(jsp, "    counter = %" PRIu64 "\n", count);
+
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* WRITE_ERR_LPAGE; READ_ERR_LPAGE; READ_REV_ERR_LPAGE; VERIFY_ERR_LPAGE */
+/* [0x2, 0x3, 0x4, 0x5] <we, re, rre, ve>  introduced: SPC-3 */
+static bool
+show_error_counter_page(const uint8_t * resp, int len,
+                        struct opts_t * op, sgj_opaque_p jop)
+{
+    bool skip_out = false;
+    bool evsm_output = false;
+    int n, num, pl, pc, pg_code;
+    uint64_t val;
+    const uint8_t * bp;
+    const char * pg_cp = NULL;
+    const char * par_cp = NULL;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[128] SG_C_CPP_ZERO_INIT;
+    char d[128];
+    char e[64];
+    static const char * wec = "Write error counter";
+    static const char * rec = "Read error counter";
+    static const char * rrec = "Read reverse error counter";
+    static const char * vec = "Verify error counter";
+
+    pg_code = resp[0] & 0x3f;
+    switch(pg_code) {
+    case WRITE_ERR_LPAGE:
+        pg_cp = wec;
+        break;
+    case READ_ERR_LPAGE:
+        pg_cp = rec;
+        break;
+    case READ_REV_ERR_LPAGE:
+        pg_cp = rrec;
+        break;
+    case VERIFY_ERR_LPAGE:
+        pg_cp = vec;
+        break;
+    default:
+        pr2serr("expecting error counter page, got page = 0x%x\n",
+                pg_code);
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s log page  [0x%x]\n", pg_cp, pg_code);
+    if (jsp->pr_as_json) {
+        n = strlen(pg_cp);
+        memcpy(b, pg_cp, n);
+        memcpy(b + n, " log", 4);
+        n = strlen(b);
+        memcpy(b + n, " page", 5);
+        jo2p = sg_log_js_hdr(jsp, jop, b, resp);
+        memcpy(b + n, " parameters", 11);
+        sgj_convert_to_snake_name(b, d, sizeof(d) - 1);
+        jap = sgj_named_subarray_r(jsp, jo2p, d);
+    }
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        par_cp = NULL;
+        switch (pc) {
+        case 0:
+            par_cp = "Errors corrected without substantial delay";
+            break;
+        case 1:
+            par_cp = "Errors corrected with possible delays";
+            break;
+        case 2:
+            par_cp = "Total rewrites or rereads";
+            break;
+        case 3:
+            par_cp = "Total errors corrected";
+            break;
+        case 4:
+            par_cp = "Total times correction algorithm processed";
+            break;
+        case 5:
+            par_cp = "Total bytes processed";
+            break;
+        case 6:
+            par_cp = "Total uncorrected errors";
+            break;
+        default:
+            if (op->exclude_vendor) {
+                skip_out = true;
+                if ((op->verbose > 0) && (0 == op->do_brief) &&
+                    (! evsm_output)) {
+                    evsm_output = true;
+                    pr2serr("  %s parameter(s) being ignored\n", vend_spec);
+                }
+            } else {
+                if (0x8009 == pc)
+                    par_cp = "Track following errors [Hitachi]";
+                else if (0x8015 == pc)
+                    par_cp = "Positioning errors [Hitachi]";
+                else {
+                    snprintf(e, sizeof(e), "Reserved or %s [0x%x]", vend_spec,
+                             pc);
+                    par_cp = e;
+                }
+            }
+            break;
+        }
+
+        if (skip_out)
+            skip_out = false;
+        else if (par_cp) {
+            val = sg_get_unaligned_be(pl - 4, bp + 4);
+            if (val > ((uint64_t)1 << 40))
+                snprintf(d, sizeof(d), "%" PRIu64 " [%" PRIu64 " TB]",
+                         val, (val / (1000UL * 1000 * 1000 * 1000)));
+            else if (val > ((uint64_t)1 << 30))
+                snprintf(d, sizeof(d), "%" PRIu64 " [%" PRIu64 " GB]",
+                         val, (val / (1000UL * 1000 * 1000)));
+            else
+                snprintf(d, sizeof(d), "%" PRIu64, val);
+            sgj_pr_hr(jsp, "  %s = %s\n", par_cp, d);
+            if (jsp->pr_as_json) {
+                js_snakenv_ihexstr_nex(jsp, jo3p, param_c, pc, true,
+                                       NULL, par_cp, NULL);
+                sgj_convert_to_snake_name(pg_cp, e, sizeof(e) - 1);
+                n = strlen(e);
+                memcpy(e + n, "_counter", 9); /* take trailing null */
+                sgj_js_nv_ihexstr(jsp, jo3p, e, val, as_s_s, d);
+            }
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* NON_MEDIUM_LPAGE [0x6] <nm>  introduced: SPC-2 */
+static bool
+show_non_medium_error_page(const uint8_t * resp, int len,
+                           struct opts_t * op, sgj_opaque_p jop)
+{
+    bool skip_out = false;
+    bool evsm_output = false;
+    int num, pl, pc;
+    uint64_t count;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[128] SG_C_CPP_ZERO_INIT;
+    static const char * nmelp = "Non-medium error log page";
+    static const char * nmec = "Non-medium error count";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x6]\n", nmelp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, nmelp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                        "non_medium_error_log_parameters");
+    }
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        switch (pc) {
+        case 0:
+            snprintf(b, sizeof(b), "%s", nmec);
+            break;
+        default:
+            if (pc <= 0x7fff)
+                snprintf(b, sizeof(b), "  Reserved [0x%x]", pc);
+            else {
+                if (op->exclude_vendor) {
+                    skip_out = true;
+                    if ((op->verbose > 0) && (0 == op->do_brief) &&
+                        (! evsm_output)) {
+                        evsm_output = true;
+                        pr2serr("  %s parameter(s) being ignored\n",
+                                vend_spec);
+                    }
+                } else
+                    snprintf(b, sizeof(b), "%s [0x%x]", vend_spec, pc);
+            }
+            break;
+        }
+        if (skip_out)
+            skip_out = false;
+        else {
+            count = sg_get_unaligned_be(pl - 4, bp + 4);
+            sgj_pr_hr(jsp, "  %s = %" PRIu64 "\n", b, count);
+            js_snakenv_ihexstr_nex(jsp, jo3p, param_c, pc, true,
+                                   NULL, b, NULL);
+            js_snakenv_ihexstr_nex(jsp, jo3p, nmec, count, true, NULL, NULL,
+                                   NULL);
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* PCT_LPAGE [0x1a] <pct>  introduced: SPC-4 */
+static bool
+show_power_condition_transitions_page(const uint8_t * resp, int len,
+                                      struct opts_t * op, sgj_opaque_p jop)
+{
+    bool partial;
+    int num, pl, pc;
+    uint64_t count;
+    const uint8_t * bp;
+    const char * cp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[128];
+    char bb[64];
+    static const char * pctlp = "Power condition transitions log page";
+    static const char * att = "Accumulated transitions to";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x1a]\n", pctlp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, pctlp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                        "power_condition_transition_log_parameters");
+    }
+
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        cp = NULL;
+        partial = true;
+        switch (pc) {
+        case 1:
+            cp = "active";
+            break;
+        case 2:
+            cp = "idle_a";
+            break;
+        case 3:
+            cp = "idle_b";
+            break;
+        case 4:
+            cp = "idle_c";
+            break;
+        case 8:
+            cp = "standby_z";
+            break;
+        case 9:
+            cp = "standby_y";
+            break;
+        default:
+            snprintf(bb, sizeof(bb), "Reserved [0x%x]", pc);
+            cp = bb;
+            partial = false;
+            break;
+        }
+        if (partial) {
+            snprintf(b, sizeof(b), "%s %s", att, cp);
+            cp = b;
+        }
+        count = sg_get_unaligned_be(pl - 4, bp + 4);
+        sgj_pr_hr(jsp, "  %s = %" PRIu64 "\n", cp, count);
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+        if (jsp->pr_as_json) {
+            js_snakenv_ihexstr_nex(jsp, jo3p, cp, count, true,
+                                   NULL, NULL, "saturating counter");
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        }
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+static char *
+temperature_str(int8_t t, bool reporting, char * b, int blen)
+{
+    if (-128 == t) {
+        if (reporting)
+            snprintf(b, blen, "%s", not_avail);
+        else
+            snprintf(b, blen, "no limit");
+    } else
+        snprintf(b, blen, "%d C", t);
+    return b;
+}
+
+static char *
+humidity_str(uint8_t h, bool reporting, char * b, int blen)
+{
+    if (255 == h) {
+        if (reporting)
+            snprintf(b, blen, "%s", not_avail);
+        else
+            snprintf(b, blen, "no limit");
+    } else if (h <= 100)
+        snprintf(b, blen, "%u %%", h);
+    else
+        snprintf(b, blen, "%s value [%u]", rsv_s, h);
+    return b;
+}
+
+/* ENV_REPORTING_SUBPG [0xd,0x1] <env> introduced: SPC-5 (rev 02). "mounted"
+ * changed to "other" in spc5r11 */
+static bool
+show_environmental_reporting_page(const uint8_t * resp, int len,
+                                  struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc, blen;
+    bool other_valid;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[32];
+    static const char * erlp = "Environmental reporting log page";
+    static const char * temp = "Temperature";
+    static const char * lmaxt = "Lifetime maximum temperature";
+    static const char * lmint = "Lifetime minimum temperature";
+    static const char * maxtspo = "Maximum temperature since power on";
+    static const char * mintspo = "Minimum temperature since power on";
+    static const char * maxot = "Maximum other temperature";
+    static const char * minot = "Minimum other temperature";
+    static const char * relhum = "Relative humidity";
+    static const char * lmaxrh = "Lifetime maximum relative humidity";
+    static const char * lminrh = "Lifetime minimum relative humidity";
+    static const char * maxrhspo = "Maximum relative humidity since power on";
+    static const char * minrhspo = "Minimum relative humidity since power on";
+    static const char * maxorh = "Maximum other relative humidity";
+    static const char * minorh = "Minimum other relative humidity";
+
+    blen = sizeof(b);
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0xd,0x1]\n", erlp);
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, erlp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                                   "environmental_reporting_log_parameters");
+    }
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+        other_valid = !!(bp[4] & 1);
+        if (pc < 0x100) {
+            if (pl < 12)  {
+                pr2serr("  <<expect parameter 0x%x to be at least 12 bytes "
+                        "long, got %d, skip>>\n", pc, pl);
+                goto inner;
+            }
+            sgj_pr_hr(jsp, "  %s=0x%x\n", param_c, pc);
+            sgj_js_nv_ihex(jsp, jo3p, param_c_sn, pc);
+            sgj_pr_hr(jsp, "    OTV=%d\n", (int)other_valid);
+            sgj_js_nv_ihex_nex(jsp, jo3p, "otv",  (int)other_valid,
+                               false, "Other Temperature Valid");
+
+            temperature_str(bp[5], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", temp, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, temp, bp[5], false,
+                                   NULL, b, "current [Celsius]");
+            temperature_str(bp[6], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lmaxt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lmaxt, bp[6], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[7], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lmint, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lmint, bp[7], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[8], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", maxtspo, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, maxtspo, bp[8], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[9], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", mintspo, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, mintspo, bp[9], false,
+                                   NULL, b, NULL);
+            if (other_valid) {
+                temperature_str(bp[10], true, b, blen);
+                sgj_pr_hr(jsp, "    %s: %s\n", maxot, b);
+                js_snakenv_ihexstr_nex(jsp, jo3p, maxot, bp[10], false,
+                                       NULL, b, NULL);
+                temperature_str(bp[11], true, b, blen);
+                sgj_pr_hr(jsp, "    %s: %s\n", minot, b);
+                js_snakenv_ihexstr_nex(jsp, jo3p, minot, bp[11], false,
+                                       NULL, b, NULL);
+            }
+        } else if (pc < 0x200) {
+            if (pl < 12)  {
+                pr2serr("  <<expect parameter 0x%x to be at least 12 bytes "
+                        "long, got %d, skip>>\n", pc, pl);
+                goto inner;
+            }
+            sgj_pr_hr(jsp, "  %s=0x%x\n", param_c, pc);
+            sgj_js_nv_ihex(jsp, jo3p, param_c_sn, pc);
+            sgj_pr_hr(jsp, "    ORHV=%d\n", (int)other_valid);
+            sgj_js_nv_ihex_nex(jsp, jo3p, "orhv",  (int)other_valid,
+                               false, "Other Relative Humidity Valid");
+
+            humidity_str(bp[5], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", relhum, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, relhum, bp[5], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[6], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lmaxrh, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lmaxrh, bp[6], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[7], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lminrh, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lminrh, bp[7], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[8], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", maxrhspo, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, maxrhspo, bp[8], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[9], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", minrhspo, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, minrhspo, bp[9], false,
+                                   NULL, b, NULL);
+            if (other_valid) {
+                humidity_str(bp[10], true, b, blen);
+                sgj_pr_hr(jsp, "    %s: %s\n", maxorh, b);
+                js_snakenv_ihexstr_nex(jsp, jo3p, maxorh, bp[10], false,
+                                       NULL, b, NULL);
+                humidity_str(bp[11], true, b, blen);
+                sgj_pr_hr(jsp, "    %s: %s\n", minorh, b);
+                js_snakenv_ihexstr_nex(jsp, jo3p, minorh, bp[11], false,
+                                       NULL, b, NULL);
+            }
+        } else
+            sgj_pr_hr(jsp, "  <<unexpected %s 0x%x\n", param_c, pc);
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+inner:
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* ENV_LIMITS_SUBPG [0xd,0x2] <enl> introduced: SPC-5 (rev 02) */
+static bool
+show_environmental_limits_page(const uint8_t * resp, int len,
+                               struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc, blen;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[32];
+    static const char * ellp = "Environmental limits log page";
+    static const char * hctlt = "High critical temperature limit trigger";
+    static const char * hctlr = "High critical temperature limit reset";
+    static const char * lctlr = "High critical temperature limit reset";
+    static const char * lctlt = "High critical temperature limit trigger";
+    static const char * hotlt = "High operating temperature limit trigger";
+    static const char * hotlr = "High operating temperature limit reset";
+    static const char * lotlr = "High operating temperature limit reset";
+    static const char * lotlt = "High operating temperature limit trigger";
+    static const char * hcrhlt =
+                "High critical relative humidity limit trigger";
+    static const char * hcrhlr =
+                "High critical relative humidity limit reset";
+    static const char * lcrhlr =
+                "High critical relative humidity limit reset";
+    static const char * lcrhlt =
+                "High critical relative humidity limit trigger";
+    static const char * horhlt =
+                "High operating relative humidity limit trigger";
+    static const char * horhlr =
+                "High operating relative humidity limit reset";
+    static const char * lorhlr =
+                "High operating relative humidity limit reset";
+    static const char * lorhlt =
+                "High operating relative humidity limit trigger";
+
+    blen = sizeof(b);
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0xd,0x2]\n", ellp);
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, ellp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                                   "environmental_limits_log_parameters");
+    }
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+        if (pc < 0x100) {
+            if (pl < 12)  {
+                pr2serr("  <<expect parameter 0x%x to be at least 12 bytes "
+                        "long, got %d, skip>>\n", pc, pl);
+                goto inner;
+            }
+            sgj_pr_hr(jsp, "  %s=0x%x\n", param_c, pc);
+            sgj_js_nv_ihex(jsp, jo3p, param_c_sn, pc);
+
+            temperature_str(bp[4], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", hctlt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, hctlt, bp[4], false,
+                                   NULL, b, "[Celsius]");
+            temperature_str(bp[5], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", hctlr, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, hctlr, bp[5], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[6], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lctlr, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lctlr, bp[6], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[7], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lctlt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lctlt, bp[7], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[8], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", hotlt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, hotlt, bp[8], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[9], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", hotlr, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, hotlr, bp[9], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[10], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lotlr, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lotlr, bp[10], false,
+                                   NULL, b, NULL);
+            temperature_str(bp[11], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lotlt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lotlt, bp[11], false,
+                                   NULL, b, NULL);
+        } else if (pc < 0x200) {
+            if (pl < 12)  {
+                pr2serr("  <<expect parameter 0x%x to be at least 12 bytes "
+                        "long, got %d, skip>>\n", pc, pl);
+                goto inner;
+            }
+            sgj_pr_hr(jsp, "  %s=0x%x\n", param_c, pc);
+            sgj_js_nv_ihex(jsp, jo3p, param_c_sn, pc);
+
+            humidity_str(bp[4], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", hcrhlt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, hcrhlt, bp[4], false,
+                                   NULL, b, "[percentage]");
+            humidity_str(bp[5], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", hcrhlr, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, hcrhlr, bp[5], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[6], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lcrhlr, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lcrhlr, bp[6], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[7], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lcrhlt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lcrhlt, bp[7], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[8], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", horhlt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, horhlt, bp[8], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[9], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", horhlr, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, horhlr, bp[9], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[10], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lorhlr, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lorhlr, bp[10], false,
+                                   NULL, b, NULL);
+            humidity_str(bp[11], true, b, blen);
+            sgj_pr_hr(jsp, "    %s: %s\n", lorhlt, b);
+            js_snakenv_ihexstr_nex(jsp, jo3p, lorhlt, bp[11], false,
+                                   NULL, b, NULL);
+        } else
+             sgj_pr_hr(jsp, "  <<unexpected %s 0x%x\n", param_c, pc);
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+inner:
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* CMD_DUR_LIMITS_SUBPG [0x19,0x21] <cdl>
+ * introduced: SPC-6 rev 1, significantly changed rev 6 */
+static bool
+show_cmd_dur_limits_page(const uint8_t * resp, int len,
+                         struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    uint32_t count, noitmc_v, noatmc_v, noitatmc_v, noc_v;
+    const uint8_t * bp;
+    const char * cp;
+    const char * thp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[144];
+    static const char * cdllp = "Command duration limits statistics log page";
+    static const char * t2cdld = "T2 command duration limit descriptor";
+    static const char * cdlt2amp = "CDL T2A mode page";
+    static const char * cdlt2bmp = "CDL T2B mode page";
+    static const char * first_7[] = {"First", "Second", "Third", "Fourth",
+                                     "Fifth", "Sixth", "Seventh"};
+    static const char * noitmc = "Number of inactive target miss commands";
+    static const char * noatmc = "Number of active target miss commands";
+    static const char * noitatmc =
+        "Number of inactive target and active target miss commands";
+    static const char * noc = "Number of commands";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x19,0x21]\n", cdllp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, cdllp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                        "command_duration_limits_statistcs_log_parameters");
+    }
+
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;         /* parameter length */
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        switch (pc) {
+        case 0x1:
+            /* spc6r06: table 349 name "Number of READ commands" seems to
+             * be wrong. Use what surrounding text and table 347 suggest */
+            cp = "Achievable latency target";
+            count =  sg_get_unaligned_be32(bp + 4);
+            sgj_pr_hr(jsp, "  %s = %" PRIu32 "\n", cp, count);
+            if (jsp->pr_as_json) {
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, cp);
+                js_snakenv_ihexstr_nex(jsp, jop, cp, count, true, NULL, NULL,
+                                       "unit: microsecond");
+            }
+            break;
+        case 0x11:
+        case 0x12:
+        case 0x13:
+        case 0x14:
+        case 0x15:
+        case 0x16:
+        case 0x17:
+            sgj_pr_hr(jsp, "  %s code 0x%x restricted\n", param_c, pc);
+            if (jsp->pr_as_json)
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, rstrict_s);
+            break;
+        case 0x21:
+        case 0x22:
+        case 0x23:
+        case 0x24:
+        case 0x25:
+        case 0x26:
+        case 0x27:
+            thp = first_7[pc - 0x21];
+            sgj_pr_hr(jsp, "  %s %s for %s [pc=0x%x]:\n", thp, t2cdld,
+                      cdlt2amp, pc);
+            noitmc_v = sg_get_unaligned_be32(bp + 4);
+            sgj_pr_hr(jsp, "    %s = %u\n", noitmc, noitmc_v);
+            noatmc_v = sg_get_unaligned_be32(bp + 8);
+            sgj_pr_hr(jsp, "    %s = %u\n", noatmc, noatmc_v);
+            noitatmc_v = sg_get_unaligned_be32(bp + 12);
+            sgj_pr_hr(jsp, "    %s = %u\n", noitatmc, noitatmc_v);
+            noc_v = sg_get_unaligned_be32(bp + 16);
+            sgj_pr_hr(jsp, "    %s = %u\n", noc, noc_v);
+            if (jsp->pr_as_json) {
+                snprintf(b, sizeof(b), "%s %s for %s", thp, t2cdld, cdlt2amp);
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, b);
+
+                js_snakenv_ihexstr_nex(jsp, jop, noitmc, noitmc_v, true, NULL,
+                                       NULL, NULL);
+                js_snakenv_ihexstr_nex(jsp, jop, noatmc, noatmc_v, true, NULL,
+                                       NULL, NULL);
+                js_snakenv_ihexstr_nex(jsp, jop, noitatmc, noitatmc_v, true,
+                                       NULL, NULL, NULL);
+                js_snakenv_ihexstr_nex(jsp, jop, noc, noc_v, true, NULL,
+                                       NULL, NULL);
+            }
+            break;
+        case 0x31:
+        case 0x32:
+        case 0x33:
+        case 0x34:
+        case 0x35:
+        case 0x36:
+        case 0x37:
+            sgj_pr_hr(jsp, "  %s 0x%x restricted\n", param_c, pc);
+            if (jsp->pr_as_json)
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, rstrict_s);
+            break;
+        case 0x41:
+        case 0x42:
+        case 0x43:
+        case 0x44:
+        case 0x45:
+        case 0x46:
+        case 0x47:
+            /* This short form introduced in draft spc6r06 */
+            thp = first_7[pc - 0x41];
+            sgj_pr_hr(jsp, "  %s %s for %s [pc=0x%x]:\n", thp, t2cdld,
+                      cdlt2bmp, pc);
+            noitmc_v = sg_get_unaligned_be32(bp + 4);
+            sgj_pr_hr(jsp, "    %s = %u\n", noitmc, noitmc_v);
+            noatmc_v = sg_get_unaligned_be32(bp + 8);
+            sgj_pr_hr(jsp, "    %s = %u\n", noatmc, noatmc_v);
+            noitatmc_v = sg_get_unaligned_be32(bp + 12);
+            sgj_pr_hr(jsp, "    %s = %u\n", noitatmc, noitatmc_v);
+            noc_v = sg_get_unaligned_be32(bp + 16);
+            sgj_pr_hr(jsp, "    %s = %u\n", noc, noc_v);
+            if (jsp->pr_as_json) {
+                snprintf(b, sizeof(b), "%s %s for %s", thp, t2cdld, cdlt2amp);
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, b);
+
+                js_snakenv_ihexstr_nex(jsp, jop, noitmc, noitmc_v, true, NULL,
+                                       NULL, NULL);
+                js_snakenv_ihexstr_nex(jsp, jop, noatmc, noatmc_v, true, NULL,
+                                       NULL, NULL);
+                js_snakenv_ihexstr_nex(jsp, jop, noitatmc, noitatmc_v, true,
+                                       NULL, NULL, NULL);
+                js_snakenv_ihexstr_nex(jsp, jop, noc, noc_v, true, NULL,
+                                       NULL, NULL);
+            }
+
+            break;
+        default:
+             sgj_pr_hr(jsp, "  <<unexpected %s 0x%x\n", param_c, pc);
+            break;
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Tape usage: Vendor specific (LTO-5 and LTO-6): 0x30 */
+static bool
+show_tape_usage_page(const uint8_t * resp, int len, struct opts_t * op,
+                     sgj_opaque_p jop)
+{
+    int k, num, extra;
+    unsigned int n;
+    uint64_t ull;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        pr2serr("badly formed tape usage page\n");
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Tape usage page  (LTO-5 and LTO-6 specific) [0x30]\n");
+    for (k = num; k > 0; k -= extra, bp += extra) {
+        int pc = sg_get_unaligned_be16(bp + 0);
+
+        extra = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, extra);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            break;
+        }
+        ull = n = 0;
+        switch (bp[3]) {
+        case 2:
+            n = sg_get_unaligned_be16(bp + 4);
+            break;
+        case 4:
+            n = sg_get_unaligned_be32(bp + 4);
+            break;
+        case 8:
+            ull = sg_get_unaligned_be64(bp + 4);
+            break;
+        }
+        switch (pc) {
+        case 0x01:
+            if (extra == 8)
+                printf("  Thread count: %u", n);
+            break;
+        case 0x02:
+            if (extra == 12)
+                printf("  Total data sets written: %" PRIu64, ull);
+            break;
+        case 0x03:
+            if (extra == 8)
+                printf("  Total write retries: %u", n);
+            break;
+        case 0x04:
+            if (extra == 6)
+                printf("  Total unrecovered write errors: %u", n);
+            break;
+        case 0x05:
+            if (extra == 6)
+                printf("  Total suspended writes: %u", n);
+            break;
+        case 0x06:
+            if (extra == 6)
+                printf("  Total fatal suspended writes: %u", n);
+            break;
+        case 0x07:
+            if (extra == 12)
+                printf("  Total data sets read: %" PRIu64, ull);
+            break;
+        case 0x08:
+            if (extra == 8)
+                printf("  Total read retries: %u", n);
+            break;
+        case 0x09:
+            if (extra == 6)
+                printf("  Total unrecovered read errors: %u", n);
+            break;
+        case 0x0a:
+            if (extra == 6)
+                printf("  Total suspended reads: %u", n);
+            break;
+        case 0x0b:
+            if (extra == 6)
+                printf("  Total fatal suspended reads: %u", n);
+            break;
+        default:
+            printf("  unknown %s = 0x%x, contents in hex:\n", param_c, pc);
+            hex2stdout(bp, extra, 1);
+            break;
+        }
+        printf("\n");
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* 0x30 */
+static bool
+show_hgst_perf_page(const uint8_t * resp, int len, struct opts_t * op,
+                    sgj_opaque_p jop)
+{
+    bool valid = false;
+    int num, pl;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("HGST/WDC performance counters page [0x30]\n");
+    num = len - 4;
+    if (num < 0x30) {
+        printf("HGST/WDC performance counters page too short (%d) < 48\n",
+               num);
+        return valid;
+    }
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        int pc = sg_get_unaligned_be16(bp + 0);
+
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        switch (pc) {
+        case 0:
+            valid = true;
+            printf("  Zero Seeks = %u\n", sg_get_unaligned_be16(bp + 4));
+            printf("  Seeks >= 2/3 = %u\n", sg_get_unaligned_be16(bp + 6));
+            printf("  Seeks >= 1/3 and < 2/3 = %u\n",
+                   sg_get_unaligned_be16(bp + 8));
+            printf("  Seeks >= 1/6 and < 1/3 = %u\n",
+                   sg_get_unaligned_be16(bp + 10));
+            printf("  Seeks >= 1/12 and < 1/6 = %u\n",
+                   sg_get_unaligned_be16(bp + 12));
+            printf("  Seeks > 0 and < 1/12 = %u\n",
+                   sg_get_unaligned_be16(bp + 14));
+            printf("  Overrun Counter = %u\n",
+                   sg_get_unaligned_be16(bp + 20));
+            printf("  Underrun Counter = %u\n",
+                   sg_get_unaligned_be16(bp + 22));
+            printf("  Device Cache Full Read Hits = %u\n",
+                   sg_get_unaligned_be32(bp + 24));
+            printf("  Device Cache Partial Read Hits = %u\n",
+                   sg_get_unaligned_be32(bp + 28));
+            printf("  Device Cache Write Hits = %u\n",
+                   sg_get_unaligned_be32(bp + 32));
+            printf("  Device Cache Fast Writes = %u\n",
+                   sg_get_unaligned_be32(bp + 36));
+            printf("  Device Cache Read Misses = %u\n",
+                   sg_get_unaligned_be32(bp + 40));
+            break;
+        default:
+            valid = false;
+            printf("  Unknown HGST/WDC %s = 0x%x", param_c, pc);
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return valid;
+}
+
+/* Tape capacity: vendor specific (LTO-5 and LTO-6 ?): 0x31 */
+static bool
+show_tape_capacity_page(const uint8_t * resp, int len,
+                        struct opts_t * op, sgj_opaque_p jop)
+{
+    int k, num, extra;
+    unsigned int n;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        pr2serr("badly formed tape capacity page\n");
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Tape capacity page  (LTO-5 and LTO-6 specific) [0x31]\n");
+    for (k = num; k > 0; k -= extra, bp += extra) {
+        int pc = sg_get_unaligned_be16(bp + 0);
+
+        extra = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, extra);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            break;
+        }
+        if (extra != 8)
+            continue;
+        n = sg_get_unaligned_be32(bp + 4);
+        switch (pc) {
+        case 0x01:
+            printf("  Main partition remaining capacity (in MiB): %u", n);
+            break;
+        case 0x02:
+            printf("  Alternate partition remaining capacity (in MiB): %u", n);
+            break;
+        case 0x03:
+            printf("  Main partition maximum capacity (in MiB): %u", n);
+            break;
+        case 0x04:
+            printf("  Alternate partition maximum capacity (in MiB): %u", n);
+            break;
+        default:
+            printf("  unknown %s = 0x%x, contents in hex:\n", param_c, pc);
+            hex2stdout(bp, extra, 1);
+            break;
+        }
+        printf("\n");
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* Data compression: originally vendor specific 0x32 (LTO-5), then
+ * ssc-4 standardizes it at 0x1b <dc> */
+static bool
+show_data_compression_page(const uint8_t * resp, int len,
+                           struct opts_t * op, sgj_opaque_p jop)
+{
+    int k, j, pl, num, extra, pc, pg_code;
+    uint64_t n;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    pg_code = resp[0] & 0x3f;
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        pr2serr("badly formed data compression page\n");
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (0x1b == pg_code)
+            printf("Data compression page  (ssc-4) [0x1b]\n");
+        else
+            printf("Data compression page  (LTO-5 specific) [0x%x]\n",
+                   pg_code);
+    }
+    for (k = num; k > 0; k -= extra, bp += extra) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3];
+        extra = pl + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, extra);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            break;
+        }
+        if ((0 == pl) || (pl > 8)) {
+            printf("badly formed data compression log parameter\n");
+            printf("  %s = 0x%x, contents in hex:\n", param_c, pc);
+            hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            goto skip_para;
+        }
+        /* variable length integer, max length 8 bytes */
+        for (j = 0, n = 0; j < pl; ++j) {
+            if (j > 0)
+                n <<= 8;
+            n |= bp[4 + j];
+        }
+        switch (pc) {
+        case 0x00:
+            printf("  Read compression ratio x100: %" PRIu64 , n);
+            break;
+        case 0x01:
+            printf("  Write compression ratio x100: %" PRIu64 , n);
+            break;
+        case 0x02:
+            printf("  Megabytes transferred to server: %" PRIu64 , n);
+            break;
+        case 0x03:
+            printf("  Bytes transferred to server: %" PRIu64 , n);
+            break;
+        case 0x04:
+            printf("  Megabytes read from tape: %" PRIu64 , n);
+            break;
+        case 0x05:
+            printf("  Bytes read from tape: %" PRIu64 , n);
+            break;
+        case 0x06:
+            printf("  Megabytes transferred from server: %" PRIu64 , n);
+            break;
+        case 0x07:
+            printf("  Bytes transferred from server: %" PRIu64 , n);
+            break;
+        case 0x08:
+            printf("  Megabytes written to tape: %" PRIu64 , n);
+            break;
+        case 0x09:
+            printf("  Bytes written to tape: %" PRIu64 , n);
+            break;
+        case 0x100:
+            printf("  Data compression enabled: 0x%" PRIx64, n);
+            break;
+        default:
+            printf("  unknown %s = 0x%x, contents in hex:\n", param_c, pc);
+            hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            break;
+        }
+skip_para:
+        printf("\n");
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* LAST_N_ERR_LPAGE [0x7] <lne>  introduced: SPC-2 */
+static bool
+show_last_n_error_page(const uint8_t * resp, int len,
+                       struct opts_t * op, sgj_opaque_p jop)
+{
+    int k, num, pl;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[256];
+    static const char * lneelp = "Last n error events log page";
+    static const char * eed = "error_event_data";
+
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        sgj_pr_hr(jsp, "No error events logged\n");
+        return true;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x7]\n", lneelp);
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, lneelp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p, "error_event_log_parameters");
+    }
+
+    for (k = num; k > 0; k -= pl, bp += pl) {
+        uint16_t pc;
+
+        if (k < 3) {
+            pr2serr("short %s\n", lneelp);
+            return false;
+        }
+        pl = bp[3] + 4;
+        pc = sg_get_unaligned_be16(bp + 0);
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+            sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, NULL);
+        }
+
+        sgj_pr_hr(jsp, "  Error event %u [0x%x]:\n", pc, pc);
+        if (pl > 4) {
+            if ((bp[2] & 0x1) && (bp[2] & 0x2)) {
+                sgj_pr_hr(jsp, "    [binary]:\n");
+                hex2str(bp + 4, pl - 4, "    ", op->hex2str_oformat,
+                        sizeof(b), b);
+                sgj_pr_hr(jsp, "%s\n", b);
+                if (jsp->pr_as_json)
+                    sgj_js_nv_hex_bytes(jsp, jo3p, eed, bp + 4, pl - 4);
+            } else if (0x01 == (bp[2] & 0x3)) {  /* ASCII */
+                sgj_pr_hr(jsp, "    %.*s\n", pl - 4, (const char *)(bp + 4));
+                if (jsp->pr_as_json)
+                    sgj_js_nv_s_len(jsp, jo3p, eed,
+                                    (const char *)(bp + 4), pl - 4);
+            } else {
+                sgj_pr_hr(jsp, "    [data counter?? (LP bit should be "
+                          "set)]:\n");
+                hex2str(bp + 4, pl - 4, "    ", op->hex2str_oformat,
+                        sizeof(b), b);
+                sgj_pr_hr(jsp, "%s\n", b);
+                if (jsp->pr_as_json)
+                    sgj_js_nv_hex_bytes(jsp, jo3p, eed, bp + 4, pl - 4);
+            }
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* LAST_N_DEFERRED_LPAGE [0xb] <lnd>  introduced: SPC-2 */
+static bool
+show_last_n_deferred_error_page(const uint8_t * resp, int len,
+                                struct opts_t * op, sgj_opaque_p jop)
+{
+    int k, n, num, pl;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p, jo4p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[512];
+    static const char * lndeoaelp =
+                "Last n deferred errors or asynchronous events log page";
+    static const char * deoae = "Deferred error or asynchronous event";
+    static const char * sd = "sense_data";
+
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        pr2serr("No deferred errors logged\n");
+        return true;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0xb]\n", lndeoaelp);
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, lndeoaelp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                "deferred_error_or_asynchronous_event_log_parameters");
+    }
+
+    for (k = num; k > 0; k -= pl, bp += pl) {
+        int pc;
+
+        if (k < 3) {
+            pr2serr("short %s\n", lndeoaelp);
+            return false;
+        }
+        pl = bp[3] + 4;
+        pc = sg_get_unaligned_be16(bp + 0);
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+            sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, deoae);
+        }
+        sgj_pr_hr(jsp, "  %s [0x%x]:\n", deoae, pc);
+        if (op->do_brief > 0) {
+            hex2stdout(bp + 4, pl - 4, op->dstrhex_no_ascii);
+            hex2str(bp + 4, pl - 4, "    ", op->hex2str_oformat,
+                    sizeof(b), b);
+            sgj_pr_hr(jsp, "%s\n", b);
+            if (jsp->pr_as_json)
+                sgj_js_nv_hex_bytes(jsp, jo3p, sd, bp + 4, pl - 4);
+        } else {
+
+            n = sg_get_sense_str("    ", bp + 4, pl - 4,  false, sizeof(b),
+                                 b);
+            sgj_pr_hr(jsp, "%.*s\n", n, b);
+            if (jsp->pr_as_json) {
+                jo4p = sgj_named_subobject_r(jsp, jo3p, sd);
+                sgj_js_sense(jsp, jo4p, bp + 4, pl - 4);
+            }
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+static const char * clgc = "Change list generation code";
+static const char * cgn = "Changed generation number";
+
+/* LAST_N_INQUIRY_DATA_CH_SUBPG [0xb,0x1] <lnic> introduced: SPC-5 (rev 17) */
+static bool
+show_last_n_inq_data_ch_page(const uint8_t * resp, int len,
+                             struct opts_t * op, sgj_opaque_p jop)
+{
+    bool vpd;
+    int j, num, pl, vpd_pg;
+    uint32_t k, n;
+    const uint8_t * bp;
+    const char * vpd_pg_name = NULL;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p, jo4p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    sgj_opaque_p ja2p;
+    char str[PCB_STR_LEN];
+    char b[128];
+    static const char * lnidclp = "Last n inquiry data changed log page";
+    static const char * idci = "Inquiry data changed indicator";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0xb,0x1]\n", lnidclp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, lnidclp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                                   "inquiry_data_changed_log_parameters");
+    }
+
+    while (num > 3) {
+        int pc = sg_get_unaligned_be16(bp + 0);
+
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+            sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                              0 == pc ? clgc : idci);
+        }
+        if (0 == pc) {
+            if (pl < 8)  {
+                pr2serr("  <<expect parameter 0x%x to be at least 8 bytes "
+                        "long, got %d, skip>>\n", pc, pl);
+                goto skip;
+            }
+            sgj_pr_hr(jsp, "  %s [pc=0x0]:\n", clgc);
+            for (j = 4, k = 1; j < pl; j +=4, ++k) {
+                n = sg_get_unaligned_be32(bp + j);
+                sgj_pr_hr(jsp, "    %s [0x%x]: %u\n", cgn, k, n);
+            }
+            if (jsp->pr_as_json) {
+                ja2p = sgj_named_subarray_r(jsp, jo3p,
+                                            "changed_generation_numbers");
+                for (j = 4, k = 1; j < pl; j +=4, ++k) {
+                    jo4p = sgj_new_unattached_object_r(jsp);
+                    n = sg_get_unaligned_be32(bp + j);
+                    js_snakenv_ihexstr_nex(jsp, jo4p, cgn, n, true, NULL,
+                                           NULL, NULL);
+                    sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo4p);
+                }
+            }
+        } else {        /* pc > 0x0 */
+            int m;
+            const int nn = sg_lib_names_mode_len;
+            struct sg_lib_simple_value_name_t * nvp = sg_lib_names_vpd_arr;
+
+            snprintf(b, sizeof(b), "  %s 0x%x, ", param_c, pc);
+            vpd = !! (1 & *(bp + 4));
+            vpd_pg = *(bp + 5);
+            if (vpd) {
+                for (m = 0; m < nn; ++m, ++nvp) {
+                    if (nvp->value == vpd_pg)
+                        break;
+                }
+                vpd_pg_name = (m < nn) ? nvp->name : NULL;
+            } else
+                vpd_pg_name = "Standard INQUIRY";
+
+            if (jsp->pr_as_json) {
+                sgj_js_nv_i(jsp, jo3p, "vpd", (int)vpd);
+                sgj_js_nv_ihex(jsp, jo3p, "changed_page_code", vpd_pg);
+                if (vpd_pg_name)
+                    sgj_js_nv_s(jsp, jo3p, "changed_page_name", vpd_pg_name);
+            }
+            if (vpd) {
+                sgj_pr_hr(jsp, "%sVPD page 0x%x changed\n", b, vpd_pg);
+                if (0 == op->do_brief) {
+                    if (vpd_pg_name)
+                        sgj_pr_hr(jsp, "    name: %s\n", vpd_pg_name);
+                }
+            } else
+                sgj_pr_hr(jsp, "%sStandard INQUIRY data changed\n", b);
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+skip:
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* LAST_N_MODE_PG_DATA_CH_SUBPG [0xb,0x2] <lnmc> introduced: SPC-5 (rev 17) */
+static bool
+show_last_n_mode_pg_data_ch_page(const uint8_t * resp, int len,
+                                 struct opts_t * op, sgj_opaque_p jop)
+{
+    bool spf;
+    int j, k, num, pl, pg_code, spg_code;
+    uint32_t n;
+    const uint8_t * bp;
+    const char * mode_pg_name = NULL;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p, jo4p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    sgj_opaque_p ja2p;
+    char str[PCB_STR_LEN];
+    char b[128];
+    static const char * lnmpdclp = "Last n mode page data changed log page";
+    static const char * mpdci = "Mode page data changed indicator";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0xb,0x2]\n", lnmpdclp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, lnmpdclp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                                   "mode_page_data_changed_log_parameters");
+    }
+
+    while (num > 3) {
+        int pc = sg_get_unaligned_be16(bp + 0);
+
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+            sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                              0 == pc ? clgc : mpdci);
+        }
+        if (0 == pc) {  /* Same as LAST_N_INQUIRY_DATA_CH_SUBPG [0xb,0x1] */
+            if (pl < 8)  {
+                pr2serr("  <<expect parameter 0x%x to be at least 8 bytes "
+                        "long, got %d, skip>>\n", pc, pl);
+                goto skip;
+            }
+            sgj_pr_hr(jsp, "  %s [pc=0x0]:\n", clgc);
+            for (j = 4, k = 1; j < pl; j +=4, ++k) {
+                n = sg_get_unaligned_be32(bp + j);
+                sgj_pr_hr(jsp, "    %s [0x%x]: %u\n", cgn, k, n);
+            }
+            if (jsp->pr_as_json) {
+                ja2p = sgj_named_subarray_r(jsp, jo3p,
+                                            "changed_generation_numbers");
+                for (j = 4, k = 1; j < pl; j +=4, ++k) {
+                    jo4p = sgj_new_unattached_object_r(jsp);
+                    n = sg_get_unaligned_be32(bp + j);
+                    js_snakenv_ihexstr_nex(jsp, jo4p, cgn, n, true, NULL,
+                                           NULL, NULL);
+                    sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo4p);
+                }
+            }
+        } else {        /* pc > 0x0 */
+            int k, val;
+            const int nn = sg_lib_names_mode_len;
+            struct sg_lib_simple_value_name_t * nmp = sg_lib_names_mode_arr;
+
+            snprintf(b, sizeof(b), "  %s 0x%x, ", param_c, pc);
+            spf = !! (0x40 & *(bp + 4));
+            pg_code = 0x3f & *(bp + 4);
+            spg_code = *(bp + 5);
+            if (spf)       /* SPF bit set */
+                sgj_pr_hr(jsp, "%smode page 0x%x,0%x changed\n", b, pg_code,
+                          spg_code);
+            else
+                sgj_pr_hr(jsp, "%smode page 0x%x changed\n", b, pg_code);
+
+            val = (pg_code << 8) | spg_code;
+            for (k = 0; k < nn; ++k, ++nmp) {
+                if (nmp->value == val)
+                    break;
+            }
+            mode_pg_name = (k < nn) ? nmp->name : NULL;
+            if ((0 == op->do_brief) && mode_pg_name)
+                sgj_pr_hr(jsp, "    name: %s\n", nmp->name);
+            if (jsp->pr_as_json) {
+                sgj_js_nv_i(jsp, jo3p, "spf", (int)spf);
+                sgj_js_nv_ihex(jsp, jo3p, "mode_page_code", pg_code);
+                sgj_js_nv_ihex(jsp, jo3p, "subpage_code", spg_code);
+                if (mode_pg_name)
+                    sgj_js_nv_s(jsp, jo3p, "mode_page_name", mode_pg_name);
+            }
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+skip:
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->filter_given)
+            break;
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+static const char * self_test_code[] = {
+    "default", "background short", "background extended", rsv_s,
+    "aborted background", "foreground short", "foreground extended",
+    rsv_s};
+
+static const char * self_test_result[] = {
+    "completed without error",
+    "aborted by SEND DIAGNOSTIC",
+    "aborted other than by SEND DIAGNOSTIC",
+    "unknown error, unable to complete",
+    "self test completed with failure in test segment (which one unknown)",
+    "first segment in self test failed",
+    "second segment in self test failed",
+    "another segment in self test failed",
+    rsv_s, rsv_s, rsv_s, rsv_s, rsv_s, rsv_s,
+    rsv_s,
+    "self test in progress"};
+
+/* SELF_TEST_LPAGE [0x10] <str>  introduced: SPC-3 */
+static bool
+show_self_test_page(const uint8_t * resp, int len, struct opts_t * op,
+                    sgj_opaque_p jop)
+{
+    bool addr_all_ffs;
+    int k, num, res, st_c;
+    unsigned int v;
+    uint32_t n;
+    uint64_t ull;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[80];
+    static const char * strlp = "Self-test results log page";
+    static const char * stc_s = "Self-test code";
+    static const char * str_s = "Self-test result";
+    static const char * stn_s = "Self-test number";
+    static const char * apoh = "Accumulated power on hours";
+
+    num = len - 4;
+    if (num < 0x190) {
+        pr2serr("short %s [length 0x%x rather than 0x190 bytes]\n", strlp,
+                 num);
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x10]\n", strlp);
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, strlp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                                   "self_test_results_log_parameters");
+    }
+
+    for (k = 0, bp = resp + 4; k < 20; ++k, bp += 20 ) {
+        int pc = sg_get_unaligned_be16(bp + 0);
+        int pl = bp[3] + 4;
+
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            break;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+            sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                              "Self-test results");
+        }
+        n = sg_get_unaligned_be16(bp + 6);
+        if ((0 == n) && (0 == bp[4])) {
+            if (jsp->pr_as_json)
+                sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+            break;
+        }
+        sgj_pr_hr(jsp, "  %s = %d, accumulated power-on hours = %d\n",
+                  param_c, pc, n);
+        st_c = (bp[4] >> 5) & 0x7;
+        sgj_pr_hr(jsp, "    %s: %s [%d]\n", stc_s, self_test_code[st_c],
+                  st_c);
+        res = bp[4] & 0xf;
+        sgj_pr_hr(jsp, "    %s: %s [%d]\n", str_s, self_test_result[res],
+                  res);
+        if (bp[5])
+            sgj_pr_hr(jsp, "    %s = %d\n", stn_s, (int)bp[5]);
+        ull = sg_get_unaligned_be64(bp + 8);
+
+        addr_all_ffs = sg_all_ffs(bp + 8, 8);
+        if (! addr_all_ffs) {
+            addr_all_ffs = false;
+            if ((res > 0) && ( res < 0xf))
+                sgj_pr_hr(jsp, "    address of first error = 0x%" PRIx64 "\n",
+                          ull);
+        }
+            addr_all_ffs = false;
+        v = bp[16] & 0xf;
+        if (v) {
+            if (op->do_brief)
+                sgj_pr_hr(jsp, "    %s = 0x%x , asc = 0x%x, ascq = 0x%x\n",
+                          s_key, v, bp[17], bp[18]);
+            else {
+                sgj_pr_hr(jsp, "    %s = 0x%x [%s]\n", s_key, v,
+                          sg_get_sense_key_str(v, sizeof(b), b));
+
+                sgj_pr_hr(jsp, "      asc = 0x%x, ascq = 0x%x [%s]\n",
+                          bp[17], bp[18], sg_get_asc_ascq_str(bp[17], bp[18],
+                          sizeof(b), b));
+            }
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2],
+                      str, sizeof(str)));
+        if (jsp->pr_as_json) {
+            js_snakenv_ihexstr_nex(jsp, jo3p, stc_s, st_c, true, NULL,
+                                   self_test_code[st_c], NULL);
+            js_snakenv_ihexstr_nex(jsp, jo3p, str_s, res, true, NULL,
+                                    self_test_result[res], NULL);
+            js_snakenv_ihexstr_nex(jsp, jo3p, stn_s, bp[5], false, NULL,
+                                   NULL, "segment number that failed");
+            js_snakenv_ihexstr_nex(jsp, jo3p, apoh, n, true, NULL,
+                   (0xffff == n ? "65535 hours or more" : NULL), NULL);
+            sgj_js_nv_ihexstr(jsp, jo3p, "address_of_first_failure", pc, NULL,
+                              addr_all_ffs ? "no errors detected" : NULL);
+            sgj_js_nv_ihexstr(jsp, jo3p, "sense_key", v, NULL,
+                              sg_get_sense_key_str(v, sizeof(b), b));
+            sgj_js_nv_ihexstr(jsp, jo3p, "additional_sense_code", bp[17],
+                              NULL, NULL);
+            sgj_js_nv_ihexstr(jsp, jo3p, "additional_sense_code_qualifier",
+                              bp[18], NULL, sg_get_asc_ascq_str(bp[17],
+                              bp[18], sizeof(b), b));
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        }
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* TEMPERATURE_LPAGE [0xd] <temp>  introduced: SPC-3
+ * N.B. The ENV_REPORTING_SUBPG [0xd,0x1] and the ENV_LIMITS_SUBPG [0xd,0x2]
+ * (both added SPC-5) are a superset of this page. */
+static bool
+show_temperature_page(const uint8_t * resp, int len, struct opts_t * op,
+                      sgj_opaque_p jop)
+{
+    int k, num, extra;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    static const char * tlp = "Temperature log page";
+    static const char * ctemp = "Current temperature";
+    static const char * rtemp = "Reference temperature";
+
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        pr2serr("badly formed Temperature page\n");
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (! op->do_temperature)
+            sgj_pr_hr(jsp, "%s  [0xd]\n", tlp);
+    }
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, tlp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p, "temperature_log_parameters");
+    }
+
+    for (k = num; k > 0; k -= extra, bp += extra) {
+        int pc;
+
+        if (k < 3) {
+            pr2serr("short Temperature page\n");
+            return true;
+        }
+        extra = bp[3] + 4;
+        pc = sg_get_unaligned_be16(bp + 0);
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, extra);
+            goto skip;
+        } else if (op->do_hex) {
+            hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            goto skip;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+            sgj_js_nv_ihex(jsp, jo3p, param_c_sn, pc);
+        }
+
+        switch (pc) {
+        case 0:
+            if ((extra > 5) && (k > 5)) {
+                if (0 == bp[5])
+                    sgj_pr_hr(jsp, "  %s = 0 C (or less)\n", ctemp);
+                else if (bp[5] < 0xff)
+                    sgj_pr_hr(jsp, "  %s = %d C\n", ctemp, bp[5]);
+                else
+                    sgj_pr_hr(jsp, "  %s = <%s>\n", ctemp, not_avail);
+                if (jsp->pr_as_json) {
+                    const char * cp = NULL;
+
+                    if (0 == bp[5])
+                        cp = "0 or less Celsius";
+                    else if (0xff == bp[5])
+                        cp = "temperature not available";
+                    js_snakenv_ihexstr_nex(jsp, jo3p, "temperature", bp[5],
+                                           false, NULL, cp,
+                                           "current [unit: celsius]");
+                }
+            }
+            break;
+        case 1:
+            if ((extra > 5) && (k > 5)) {
+                if (bp[5] < 0xff)
+                    sgj_pr_hr(jsp, "  %s = %d C\n", rtemp, bp[5]);
+                else
+                    sgj_pr_hr(jsp, "  %s = <%s>\n", rtemp, not_avail);
+                if (jsp->pr_as_json) {
+                    const char * cp;
+
+                    if (0 == bp[5])
+                        cp = "in C (or less)";
+                    else if (0xff == bp[5])
+                        cp = not_avail;
+                    else
+                        cp = "in C";
+                    sgj_js_nv_ihex_nex(jsp, jo3p, "reference_temperature",
+                                       bp[5], true, cp);
+                }
+            }
+            break;
+        default:
+            if (! op->do_temperature) {
+                sgj_pr_hr(jsp, "  unknown %s = 0x%x, contents in hex:\n",
+                          param_c, pc);
+                hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            } else {
+                if (jsp->pr_as_json)
+                    sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+                continue;
+            }
+            break;
+        }
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+skip:
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* START_STOP_LPAGE [0xe] <sscc>  introduced: SPC-3 */
+static bool
+show_start_stop_page(const uint8_t * resp, int len, struct opts_t * op,
+                     sgj_opaque_p jop)
+{
+    int k, num, extra;
+    uint32_t val;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[256];
+    static const char * sscclp = "Start-stop cycle counter log page";
+    static const char * dom = "Date of manufacture";
+    static const char * ad = "Accounting date";
+    static const char * sccodl = "Specified cycle count over device lifetime";
+    static const char * assc = "Accumulated start-stop cycles";
+    static const char * slucodl =
+                        "Specified load-unload count over device lifetime";
+    static const char * aluc = "Accumulated load-unload cycles";
+
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        pr2serr("badly formed Start-stop cycle counter page\n");
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0xe]\n", sscclp);
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, sscclp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                                   "start_stop_cycle_log_parameters");
+    }
+
+    for (k = num; k > 0; k -= extra, bp += extra) {
+        int pc;
+
+        if (k < 3) {
+            pr2serr("short %s\n", sscclp);
+            return false;
+        }
+        extra = bp[3] + 4;
+        pc = sg_get_unaligned_be16(bp + 0);
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, extra);
+            goto skip;
+        } else if (op->do_hex) {
+            hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            goto skip;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        switch (pc) {
+        case 1:
+            if (10 == extra) {
+                 sgj_pr_hr(jsp, "  %s, year: %.4s, week: %.2s\n", dom,
+                       bp + 4, bp + 8);
+                if (jsp->pr_as_json) {
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                      "Date of manufacture");
+                    sgj_js_nv_s_len(jsp, jo3p, "year_of_manufacture",
+                                    (const char *)(bp + 4), 4);
+                    sgj_js_nv_s_len(jsp, jo3p, "week_of_manufacture",
+                                    (const char *)(bp + 8), 2);
+                }
+            } else if (op->verbose) {
+                pr2serr("%s parameter length strange: %d\n", dom, extra - 4);
+                hex2stderr(bp, extra, 1);
+            }
+            break;
+        case 2:
+            if (10 == extra) {
+                sgj_pr_hr(jsp, "  %s, year: %.4s, week: %.2s\n", ad, bp + 4,
+                          bp + 8);
+                if (jsp->pr_as_json) {
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                      "Accounting date");
+                    sgj_js_nv_s_len(jsp, jo3p, "year_of_manufacture",
+                                    (const char *)(bp + 4), 4);
+                    sgj_js_nv_s_len(jsp, jo3p, "week_of_manufacture",
+                                    (const char *)(bp + 8), 2);
+                }
+            } else if (op->verbose) {
+                pr2serr("%s parameter length strange: %d\n", ad, extra - 4);
+                hex2stderr(bp, extra, 1);
+            }
+            break;
+        case 3:
+            if (extra > 7) {
+                val = sg_get_unaligned_be32(bp + 4);
+                sgj_pr_hr(jsp, "  %s = %u\n", sccodl, val);
+                if (jsp->pr_as_json) {
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                      sccodl);
+                    js_snakenv_ihexstr_nex(jsp, jo3p, sccodl, val, false,
+                                           NULL, NULL, NULL);
+                }
+            }
+            break;
+        case 4:
+            if (extra > 7) {
+                val = sg_get_unaligned_be32(bp + 4);
+                sgj_pr_hr(jsp, "  %s = %u\n", assc, val);
+                if (jsp->pr_as_json) {
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                      assc);
+                    js_snakenv_ihexstr_nex(jsp, jo3p, assc, val, false,
+                                           NULL, NULL, NULL);
+                }
+            }
+            break;
+        case 5:
+            if (extra > 7) {
+                val = sg_get_unaligned_be32(bp + 4);
+                sgj_pr_hr(jsp, "  %s = %u\n", slucodl, val);
+                if (jsp->pr_as_json) {
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                      slucodl);
+                    js_snakenv_ihexstr_nex(jsp, jo3p, slucodl, val, false,
+                                           NULL, NULL, NULL);
+                }
+            }
+            break;
+        case 6:
+            if (extra > 7) {
+                val = sg_get_unaligned_be32(bp + 4);
+                sgj_pr_hr(jsp, "  %s = %u\n", aluc, val);
+                if (jsp->pr_as_json) {
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, aluc);
+                    js_snakenv_ihexstr_nex(jsp, jo3p, aluc, val, false,
+                                           NULL, NULL, NULL);
+                }
+            }
+            break;
+        default:
+            sgj_pr_hr(jsp, "  unknown %s = 0x%x, contents in hex:\n",
+                      param_c, pc);
+            hex2str(bp, extra, "    ", op->hex2str_oformat, sizeof(b), b);
+            sgj_pr_hr(jsp, "%s\n", b);
+            if (jsp->pr_as_json) {
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, unknown_s);
+                sgj_js_nv_hex_bytes(jsp, jo3p, in_hex, bp, extra);
+            }
+            break;
+        }
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+skip:
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* APP_CLIENT_LPAGE [0xf] <ac>  introduced: SPC-3 */
+static bool
+show_app_client_page(const uint8_t * resp, int len, struct opts_t * op,
+                     sgj_opaque_p jop)
+{
+    int k, n, num, extra;
+    char * mp;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    static const char * aclp = "Application Client log page";
+    static const char * guac = "General Usage Application Client";
+
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        pr2serr("badly formed %s\n", aclp);
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (op->do_hex == 0)))
+        sgj_pr_hr(jsp, "%s  [0xf]\n", aclp);
+    if (jsp->pr_as_json)
+        jo2p = sg_log_js_hdr(jsp, jop, aclp, resp);
+    if ((0 == op->filter_given) && (! op->do_full)) {
+        if ((len > 128) && (0 == op->do_hex) && (0 == op->undefined_hex)) {
+            char d[256];
+
+            hex2str(resp, 64, "  ", op->hex2str_oformat, sizeof(d), d);
+            sgj_pr_hr(jsp, "%s", d);
+            sgj_pr_hr(jsp, "  .....  [truncated after 64 of %d bytes (use "
+                      "'-H' to see the rest)]\n", len);
+            if (jsp->pr_as_json) {
+                sgj_js_nv_ihex(jsp, jo2p, "actual_length", len);
+                sgj_js_nv_ihex(jsp, jo2p, "truncated_length", 64);
+                sgj_js_nv_hex_bytes(jsp, jo2p, in_hex, resp, 64);
+            }
+        } else {
+            n = len * 4 + 32;
+            mp = malloc(n);
+            if (mp) {
+                hex2str(resp, len, "  ", op->hex2str_oformat, n, mp);
+                sgj_pr_hr(jsp, "%s", mp);
+                if (jsp->pr_as_json) {
+                    sgj_js_nv_ihex(jsp, jo2p, "length", len);
+                    sgj_js_nv_hex_bytes(jsp, jo2p, in_hex, resp, len);
+                }
+                free(mp);
+            }
+        }
+        return true;
+    }
+    if (jsp->pr_as_json)
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                                   "application_client_log_parameters");
+
+    /* here if filter_given set or --full given */
+    for (k = num; k > 0; k -= extra, bp += extra) {
+        int pc;
+        char d[1024];
+
+        if (k < 3) {
+            pr2serr("short %s\n", aclp);
+            return true;
+        }
+        extra = bp[3] + 4;
+        pc = sg_get_unaligned_be16(bp + 0);
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, extra);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+        sgj_pr_hr(jsp, "  %s = %d [0x%x] %s\n", param_c, pc, pc,
+                  (pc <= 0xfff) ? guac : "");
+        hex2str(bp, extra, "    ", op->hex2str_oformat, sizeof(d), d);
+        sgj_pr_hr(jsp, "%s", d);
+        if (jsp->pr_as_json) {
+            sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                              (pc <= 0xfff) ? guac : NULL);
+            sgj_js_nv_hex_bytes(jsp, jo3p, in_hex, bp, extra);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        }
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* IE_LPAGE [0x2f] <ie> "Informational Exceptions"  introduced: SPC-3
+ * Previously known as "SMART Status and Temperature Reading" lpage.  */
+static bool
+show_ie_page(const uint8_t * resp, int len, struct opts_t * op,
+             sgj_opaque_p jop)
+{
+    bool skip = false;
+    int k, num, param_len;
+    const uint8_t * bp;
+    const char * cp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[512];
+    char bb[64];
+    bool full, decoded;
+    static const char * ielp = "Informational exceptions log page";
+    static const char * ieasc =
+                         "informational_exceptions_additional_sense_code";
+    static const char * ct = "Current temperature";
+    static const char * tt = "Threshold temperature";
+    static const char * mt = "Maximum temperature";
+    static const char * ce = "common extension";
+
+    full = ! op->do_temperature;
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (num < 4) {
+        pr2serr("badly formed %s\n", ielp);
+        return false;
+    }
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (full)
+            sgj_pr_hr(jsp, "%s  [0x2f]\n", ielp);
+    }
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, ielp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                           "informational_exceptions_log_parameters");
+    }
+
+    for (k = num; k > 0; k -= param_len, bp += param_len) {
+        int pc;
+
+        if (k < 3) {
+            pr2serr("short %s\n", ielp);
+            return false;
+        }
+        param_len = bp[3] + 4;
+        pc = sg_get_unaligned_be16(bp + 0);
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, param_len);
+            goto skip;
+        } else if (op->do_hex) {
+            hex2stdout(bp, param_len, op->dstrhex_no_ascii);
+            goto skip;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+        decoded = true;
+        cp = NULL;
+
+        switch (pc) {
+        case 0x0:
+            if (param_len > 5) {
+                bool na;
+                uint8_t t;
+
+                if (full) {
+                     sgj_pr_hr(jsp, "  IE asc = 0x%x, ascq = 0x%x\n", bp[4],
+                               bp[5]);
+                    if (bp[4] || bp[5])
+                        if(sg_get_asc_ascq_str(bp[4], bp[5], sizeof(b), b))
+                             sgj_pr_hr(jsp, "    [%s]\n", b);
+                    if (jsp->pr_as_json) {
+                        sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                          "Informational exceptions general");
+                        sgj_js_nv_ihexstr(jsp, jo3p, ieasc, bp[4], NULL,
+                                          NULL);
+                        snprintf(b, sizeof(b), "%s_qualifier", ieasc);
+                        sgj_js_nv_ihexstr(jsp, jo3p, b, bp[5], NULL,
+                                          sg_get_asc_ascq_str(bp[4], bp[5],
+                                          sizeof(bb), bb));
+                    }
+                }
+                if (param_len <= 6)
+                    break;
+                t = bp[6];
+                na = (0xff == t);
+                if (na)
+                    snprintf(b, sizeof(b), "%u C", t);
+                else
+                    snprintf(b, sizeof(b), "<%s>", unknown_s);
+                sgj_pr_hr(jsp, "    %s = %s\n", ct, b);
+                if (jsp->pr_as_json)
+                    js_snakenv_ihexstr_nex(jsp, jo3p, ct, t, true,
+                                           NULL, na ? unknown_s : NULL,
+                                           "[unit: celsius]");
+                if (param_len > 7) {
+                    t = bp[7];
+                    na = (0xff == t);
+                    if (na)
+                        snprintf(b, sizeof(b), "%u C", t);
+                    else
+                        snprintf(b, sizeof(b), "<%s>", unknown_s);
+                    sgj_pr_hr(jsp, "    %s = %s  [%s]\n", tt, b, ce);
+                    if (jsp->pr_as_json)
+                        js_snakenv_ihexstr_nex(jsp, jo3p, tt, t, true, NULL,
+                                               na ? unknown_s : NULL, ce);
+                    t = bp[8];
+                    if ((param_len > 8) && (t >= bp[6])) {
+                        na = (0xff == t);
+                        if (na)
+                            snprintf(b, sizeof(b), "%u C", t);
+                        else
+                            snprintf(b, sizeof(b), "<%s>", unknown_s);
+                        sgj_pr_hr(jsp, "    %s = %s  [%s]\n", mt, b, ce);
+                        if (jsp->pr_as_json)
+                            js_snakenv_ihexstr_nex(jsp, jo3p, mt, t, true,
+                                                   NULL,
+                                                   na ? unknown_s : NULL, ce);
+                    }
+                }
+            }
+            decoded = true;
+            break;
+        default:
+            if (op->do_brief > 0) {
+                cp = NULL;
+                skip = true;
+                break;
+            }
+            if (VP_HITA == op->vend_prod_num) {
+                switch (pc) {
+                case 0x1:
+                    cp = "Remaining reserve 1";
+                    break;
+                case 0x2:
+                    cp = "Remaining reserve XOR";
+                    break;
+                case 0x3:
+                    cp = "XOR depletion";
+                    break;
+                case 0x4:
+                    cp = "Volatile memory backup failure";
+                    break;
+                case 0x5:
+                    cp = "Wear indicator";
+                    break;
+                case 0x6:
+                    cp = "System area wear indicator";
+                    break;
+                case 0x7:
+                    cp = "Channel hangs";
+                    break;
+                case 0x8:
+                    cp = "Flash scan failure";
+                    break;
+                default:
+                    decoded = false;
+                    break;
+                }
+                if (cp) {
+                    sgj_pr_hr(jsp, "  %s:\n", cp);
+                    sgj_pr_hr(jsp, "    SMART sense_code=0x%x sense_qualifier"
+                              "=0x%x threshold=%d%% trip=%d\n", bp[4], bp[5],
+                              bp[6], bp[7]);
+                    if (jsp->pr_as_json) {
+                        sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                          cp);
+                        sgj_js_nv_ihex(jsp, jo3p, "smart_sense_code", bp[4]);
+                        sgj_js_nv_ihex(jsp, jo3p, "smart_sense_qualifier",
+                                       bp[5]);
+                        sgj_js_nv_ihex(jsp, jo3p, "smart_threshold", bp[6]);
+                        sgj_js_nv_ihex(jsp, jo3p, "smart_trip", bp[7]);
+                    }
+                }
+            } else {
+                decoded = false;
+                if (jsp->pr_as_json)
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                      unknown_s);
+            }
+            break;
+        }               /* end of switch statement */
+        if (skip)
+            skip = false;
+        else if ((! decoded) && full) {
+            hex2str(bp, param_len, "    ", op->hex2str_oformat, sizeof(b), b);
+            sgj_pr_hr(jsp, "  %s = 0x%x, contents in hex:\n%s", param_c, pc,
+                      b);
+        }
+
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+skip:
+        if (op->filter_given)
+            break;
+    }           /* end of for loop */
+    return true;
+}
+
+/* called for SAS port of PROTO_SPECIFIC_LPAGE [0x18] */
+static const char *
+show_sas_phy_event_info(int pes, unsigned int val, unsigned int thresh_val,
+                        char * b, int blen)
+{
+    int n = 0;
+    unsigned int u;
+    const char * cp = "";
+    static const char * pvdt = "Peak value detector threshold";
+
+    switch (pes) {
+    case 0:
+        cp = "No event";
+        snprintf(b, blen, "%s", cp);
+        break;
+    case 0x1:
+        cp = "Invalid word count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x2:
+        cp = "Running disparity error count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x3:
+        cp = "Loss of dword synchronization count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x4:
+        cp = "Phy reset problem count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x5:
+        cp = "Elasticity buffer overflow count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x6:
+        cp = "Received ERROR count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x7:
+        cp = "Invalid SPL packet count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x8:
+        cp = "Loss of SPL packet synchronization count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x20:
+        cp = "Received address frame error count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x21:
+        cp = "Transmitted abandon-class OPEN_REJECT count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x22:
+        cp =  "Received abandon-class OPEN_REJECT count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x23:
+        cp = "Transmitted retry-class OPEN_REJECT count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x24:
+        cp = "Received retry-class OPEN_REJECT count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x25:
+        cp = "Received AIP (WAITING ON PARTIAL) count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x26:
+        cp = "Received AIP (WAITING ON CONNECTION) count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x27:
+        cp = "Transmitted BREAK count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x28:
+        cp = "Received BREAK count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x29:
+        cp = "Break timeout count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x2a:
+        cp =  "Connection count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x2b:
+        cp = "Peak transmitted pathway blocked count";
+        n = sg_scnpr(b, blen, "%s: %u", cp, val & 0xff);
+        sg_scnpr(b + n, blen - n, "\t%s: %u", pvdt, thresh_val & 0xff);
+        break;
+    case 0x2c:
+        cp = "Peak transmitted arbitration wait time";
+        u = val & 0xffff;
+        if (u < 0x8000)
+            n = sg_scnpr(b, blen, "%s (us): %u", cp, u);
+        else
+            n = sg_scnpr(b, blen, "%s (ms): %u", cp, 33 + (u - 0x8000));
+        u = thresh_val & 0xffff;
+        if (u < 0x8000)
+            sg_scnpr(b + n, blen - n, "\t%s (us): %u", pvdt, u);
+        else
+            sg_scnpr(b + n, blen - n, "\t%s (ms): %u", pvdt,
+                     33 + (u - 0x8000));
+        break;
+    case 0x2d:
+        cp = "Peak arbitration time";
+        n = sg_scnpr(b, blen, "%s (us): %u", cp, val);
+        sg_scnpr(b + n, blen - n, "\t%s: %u", pvdt, thresh_val);
+        break;
+    case 0x2e:
+        cp = "Peak connection time";
+        n = sg_scnpr(b, blen, "%s (us): %u", cp, val);
+        sg_scnpr(b + n, blen - n, "\t%s: %u", pvdt, thresh_val);
+        break;
+    case 0x2f:
+        cp = "Persistent connection count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x40:
+        cp = "Transmitted SSP frame count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x41:
+        cp = "Received SSP frame count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x42:
+        cp = "Transmitted SSP frame error count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x43:
+        cp = "Received SSP frame error count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x44:
+        cp = "Transmitted CREDIT_BLOCKED count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x45:
+        cp = "Received CREDIT_BLOCKED count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x50:
+        cp = "Transmitted SATA frame count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x51:
+        cp = "Received SATA frame count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x52:
+        cp = "SATA flow control buffer overflow count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x60:
+        cp = "Transmitted SMP frame count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x61:
+        cp = "Received SMP frame count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    case 0x63:
+        cp = "Received SMP frame error count";
+        snprintf(b, blen, "%s: %u", cp, val);
+        break;
+    default:
+        cp = "";
+        snprintf(b, blen, "Unknown phy event source: %d, val=%u, "
+                 "thresh_val=%u", pes, val, thresh_val);
+        break;
+    }
+    return cp;
+}
+
+static const char * sas_link_rate_arr[16] = {
+    "phy enabled; unknown rate",
+    "phy disabled",
+    "phy enabled; speed negotiation failed",
+    "phy enabled; SATA spinup hold state",
+    "phy enabled; port selector",
+    "phy enabled; reset in progress",
+    "phy enabled; unsupported phy attached",
+    "reserved [0x7]",
+    "1.5 Gbps",                 /* 0x8 */
+    "3 Gbps",
+    "6 Gbps",
+    "12 Gbps",
+    "22.5 Gbps",
+    "reserved [0xd]",
+    "reserved [0xe]",
+    "reserved [0xf]",
+};
+
+static char *
+sas_negot_link_rate(int lrate, char * b, int blen)
+{
+    int mask = 0xf;
+
+    if (~mask & lrate)
+        snprintf(b, blen, "bad link_rate value=0x%x\n", lrate);
+    else
+        snprintf(b, blen, "%s", sas_link_rate_arr[lrate]);
+    return b;
+}
+
+/* helper for SAS port of PROTO_SPECIFIC_LPAGE [0x18] */
+static void
+show_sas_port_param(const uint8_t * bp, int param_len, struct opts_t * op,
+                    sgj_opaque_p jop)
+{
+    int j, m, nphys, t, spld_len, pi;
+    uint64_t ull;
+    unsigned int ui, ui2, ui3, ui4;
+    char * cp;
+    const char * ccp;
+    const char * cc2p;
+    const char * cc3p;
+    const char * cc4p;
+    const uint8_t * vcp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    sgj_opaque_p ja2p = NULL;
+    char b[160];
+    char s[80];
+    static char * rtpi = "Relative target port identifier";
+    static char * psplpfstp =
+                "Protocol Specific Port log parameter for SAS target port";
+    static char * at = "attached";
+    static char * ip = "initiator_port";
+    static char * tp = "target_port";
+    static char * pvdt = "peak_value_detector_threshold";
+    static const int sz = sizeof(s);
+    static const int blen = sizeof(b);
+
+    t = sg_get_unaligned_be16(bp + 0);
+    if (op->do_name)
+        sgj_pr_hr(jsp, " rel_target_port=%d\n", t);
+    else
+        sgj_pr_hr(jsp, " %s = %d\n", rtpi, t);
+    if (op->do_name)
+        sgj_pr_hr(jsp, "  gen_code=%d\n", bp[6]);
+    else
+        sgj_pr_hr(jsp, "  generation code = %d\n", bp[6]);
+    nphys = bp[7];
+    if (op->do_name)
+        sgj_pr_hr(jsp, "  num_phys=%d\n", nphys);
+    else
+        sgj_pr_hr(jsp, "  number of phys = %d\n", nphys);
+    if (jsp->pr_as_json) {
+        js_snakenv_ihexstr_nex(jsp, jop, param_c , t, true,
+                               NULL, psplpfstp, rtpi);
+        pi = 0xf & bp[4];
+        sgj_js_nv_ihexstr(jsp, jop, "protocol_identifier", pi, NULL,
+                          sg_get_trans_proto_str(pi, blen, b));
+        sgj_js_nv_ihex(jsp, jop, "generation_code", bp[6]);
+        sgj_js_nv_ihex(jsp, jop, "number_of_phys", bp[7]);
+        jap = sgj_named_subarray_r(jsp, jop, "sas_phy_log_descriptor_list");
+    }
+
+    for (j = 0, vcp = bp + 8; j < (param_len - 8);
+         vcp += spld_len, j += spld_len) {
+        if (jsp->pr_as_json) {
+            jo2p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo2p, vcp[2]);
+        }
+        if (op->do_name)
+            sgj_pr_hr(jsp, "    phy_id=%d\n", vcp[1]);
+        else
+            sgj_haj_vi(jsp, jo2p, 2, "phy identifier", SGJ_SEP_EQUAL_1_SPACE,
+                       vcp[1], true);
+        spld_len = vcp[3];
+        if (spld_len < 44)
+            spld_len = 48;      /* in SAS-1 and SAS-1.1 vcp[3]==0 */
+        else
+            spld_len += 4;
+        if (op->do_name) {
+            t = ((0x70 & vcp[4]) >> 4);
+            sgj_pr_hr(jsp, "      att_dev_type=%d\n", t);
+            sgj_pr_hr(jsp, "      att_iport_mask=0x%x\n", vcp[6]);
+            sgj_pr_hr(jsp, "      att_phy_id=%d\n", vcp[24]);
+            sgj_pr_hr(jsp, "      att_reason=0x%x\n", (vcp[4] & 0xf));
+            ull = sg_get_unaligned_be64(vcp + 16);
+            sgj_pr_hr(jsp, "      att_sas_addr=0x%" PRIx64 "\n", ull);
+            sgj_pr_hr(jsp, "      att_tport_mask=0x%x\n", vcp[7]);
+            ui = sg_get_unaligned_be32(vcp + 32);
+            sgj_pr_hr(jsp, "      inv_dwords=%u\n", ui);
+            ui = sg_get_unaligned_be32(vcp + 40);
+            sgj_pr_hr(jsp, "      loss_dword_sync=%u\n", ui);
+            sgj_pr_hr(jsp, "      neg_log_lrate=%d\n", 0xf & vcp[5]);
+            ui = sg_get_unaligned_be32(vcp + 44);
+            sgj_pr_hr(jsp, "      phy_reset_probs=%u\n", ui);
+            ui = sg_get_unaligned_be32(vcp + 36);
+            sgj_pr_hr(jsp, "      running_disparity=%u\n", ui);
+            sgj_pr_hr(jsp, "      reason=0x%x\n", (vcp[5] & 0xf0) >> 4);
+            ull = sg_get_unaligned_be64(vcp + 8);
+            sgj_pr_hr(jsp, "      sas_addr=0x%" PRIx64 "\n", ull);
+        } else {
+            t = ((0x70 & vcp[4]) >> 4);
+            /* attached SAS device type. In SAS-1.1 case 2 was an edge
+             * expander; in SAS-2 case 3 is marked as obsolete. */
+            switch (t) {
+            case 0: snprintf(s, sz, "no device %s", at); break;
+            case 1: snprintf(s, sz, "SAS or SATA device"); break;
+            case 2: snprintf(s, sz, "expander device"); break;
+            case 3: snprintf(s, sz, "expander device (fanout)"); break;
+            default: snprintf(s, sz, "%s [%d]", rsv_s, t); break;
+            }
+            /* the word 'SAS' in following added in spl4r01 */
+            sgj_pr_hr(jsp, "    %s SAS device type: %s\n", at, s);
+            if (jsp->pr_as_json)
+                sgj_js_nv_ihexstr(jsp, jo2p, "attached_sas_device_type", t,
+                                  NULL, s);
+            t = 0xf & vcp[4];
+            switch (t) {
+            case 0: snprintf(s, sz, "%s", unknown_s); break;
+            case 1: snprintf(s, sz, "power on"); break;
+            case 2: snprintf(s, sz, "hard reset"); break;
+            case 3: snprintf(s, sz, "SMP phy control function"); break;
+            case 4: snprintf(s, sz, "loss of dword synchronization"); break;
+            case 5: snprintf(s, sz, "mux mix up"); break;
+            case 6: snprintf(s, sz, "I_T nexus loss timeout for STP/SATA");
+                break;
+            case 7: snprintf(s, sz, "break timeout timer expired"); break;
+            case 8: snprintf(s, sz, "phy test function stopped"); break;
+            case 9: snprintf(s, sz, "expander device reduced functionality");
+                 break;
+            default: snprintf(s, sz, "%s [0x%x]", rsv_s, t); break;
+            }
+            sgj_pr_hr(jsp, "    %s reason: %s\n", at, s);
+            if (jsp->pr_as_json)
+                sgj_js_nv_ihexstr(jsp, jo2p, "attached_reason", t, NULL, s);
+            t = (vcp[5] & 0xf0) >> 4;
+            switch (t) {
+            case 0: snprintf(s, sz, "%s", unknown_s); break;
+            case 1: snprintf(s, sz, "power on"); break;
+            case 2: snprintf(s, sz, "hard reset"); break;
+            case 3: snprintf(s, sz, "SMP phy control function"); break;
+            case 4: snprintf(s, sz, "loss of dword synchronization"); break;
+            case 5: snprintf(s, sz, "mux mix up"); break;
+            case 6: snprintf(s, sz, "I_T nexus loss timeout for STP/SATA");
+                break;
+            case 7: snprintf(s, sz, "break timeout timer expired"); break;
+            case 8: snprintf(s, sz, "phy test function stopped"); break;
+            case 9: snprintf(s, sz, "expander device reduced functionality");
+                 break;
+            default: snprintf(s, sz, "%s [0x%x]", rsv_s, t); break;
+            }
+            sgj_pr_hr(jsp, "    reason: %s\n", s);
+            if (jsp->pr_as_json)
+                sgj_js_nv_ihexstr(jsp, jo2p, "reason", t, NULL, s);
+            t = (0xf & vcp[5]);
+            ccp = "negotiated logical link rate";
+            cc2p = sas_negot_link_rate(t, s, sz);
+            sgj_pr_hr(jsp, "    %s: %s\n", ccp, cc2p);
+            if (jsp->pr_as_json) {
+                sgj_convert_to_snake_name(ccp, b, blen);
+                sgj_js_nv_ihexstr(jsp, jo2p, b, t, NULL, cc2p);
+            }
+
+            sgj_pr_hr(jsp, "    %s initiator port: ssp=%d stp=%d smp=%d\n",
+                      at, !! (vcp[6] & 8), !! (vcp[6] & 4), !! (vcp[6] & 2));
+            if (jsp->pr_as_json) {
+                snprintf(b, blen, "%s_ssp_%s", at, ip);
+                sgj_js_nv_i(jsp, jo2p, b, !! (vcp[6] & 8));
+                snprintf(b, blen, "%s_stp_%s", at, ip);
+                sgj_js_nv_i(jsp, jo2p, b, !! (vcp[6] & 4));
+                snprintf(b, blen, "%s_smp_%s", at, ip);
+                sgj_js_nv_i(jsp, jo2p, b, !! (vcp[6] & 2));
+            }
+            sgj_pr_hr(jsp, "    %s target port: ssp=%d stp=%d smp=%d\n", at,
+                      !! (vcp[7] & 8), !! (vcp[7] & 4), !! (vcp[7] & 2));
+            if (jsp->pr_as_json) {
+                snprintf(b, blen, "%s_ssp_%s", at, tp);
+                sgj_js_nv_i(jsp, jo2p, b, !! (vcp[7] & 8));
+                snprintf(b, blen, "%s_stp_%s", at, tp);
+                sgj_js_nv_i(jsp, jo2p, b, !! (vcp[7] & 4));
+                snprintf(b, blen, "%s_smp_%s", at, tp);
+                sgj_js_nv_i(jsp, jo2p, b, !! (vcp[7] & 2));
+            }
+            ull = sg_get_unaligned_be64(vcp + 8);
+            sgj_pr_hr(jsp, "    SAS address = 0x%" PRIx64 "\n", ull);
+            if (jsp->pr_as_json)
+                sgj_js_nv_ihex(jsp, jo2p, "sas_address", ull);
+            ull = sg_get_unaligned_be64(vcp + 16);
+            sgj_pr_hr(jsp, "    %s SAS address = 0x%" PRIx64 "\n", at, ull);
+            if (jsp->pr_as_json)
+                sgj_js_nv_ihex(jsp, jo2p, "attached_sas_address", ull);
+            ccp = "attached phy identifier";
+            sgj_haj_vi(jsp, jo2p, 4, ccp, SGJ_SEP_EQUAL_1_SPACE, vcp[24],
+                       true);
+            ccp = "Invalid DWORD count";
+            ui = sg_get_unaligned_be32(vcp + 32);
+            cc2p = "Running disparity error count";
+            ui2 = sg_get_unaligned_be32(vcp + 36);
+            cc3p = "Loss of DWORD synchronization count";
+            ui3 = sg_get_unaligned_be32(vcp + 40);
+            cc4p = "Phy reset problem count";
+            ui4 = sg_get_unaligned_be32(vcp + 44);
+            if (jsp->pr_as_json) {
+                sgj_convert_to_snake_name(ccp, b, blen);
+                sgj_js_nv_ihex(jsp, jo2p, b, ui);
+                sgj_convert_to_snake_name(cc2p, b, blen);
+                sgj_js_nv_ihex(jsp, jo2p, b, ui2);
+                sgj_convert_to_snake_name(cc3p, b, blen);
+                sgj_js_nv_ihex(jsp, jo2p, b, ui3);
+                sgj_convert_to_snake_name(cc4p, b, blen);
+                sgj_js_nv_ihex(jsp, jo2p, b, ui4);
+            } else {
+                if (0 == op->do_brief) {
+                    sgj_pr_hr(jsp, "    %s = %u\n", ccp, ui);
+                    sgj_pr_hr(jsp, "    %s = %u\n", cc2p, ui2);
+                    sgj_pr_hr(jsp, "    %s = %u\n", cc3p, ui3);
+                    sgj_pr_hr(jsp, "    %s = %u\n", cc4p, ui4);
+                }
+            }
+        }
+        if (op->do_brief > 0)
+            goto skip;
+        if (spld_len > 51) {
+            int num_ped;
+            const uint8_t * xcp;
+
+            num_ped = vcp[51];
+            if (op->verbose > 1)
+                sgj_pr_hr(jsp, "    <<Phy event descriptors: %d, spld_len: "
+                          "%d, calc_ped: %d>>\n", num_ped, spld_len,
+                          (spld_len - 52) / 12);
+            if (num_ped > 0) {
+                if (op->do_name) {
+                    sgj_pr_hr(jsp, "      phy_event_desc_num=%d\n", num_ped);
+                    return;      /* don't decode at this stage */
+                } else
+                    sgj_pr_hr(jsp, "    Phy event descriptors:\n");
+            }
+            if (jsp->pr_as_json) {
+                sgj_js_nv_i(jsp, jo2p, "number_of_phy_event_descriptors",
+                            num_ped);
+                if (num_ped > 0)
+                    ja2p = sgj_named_subarray_r(jsp, jo2p,
+                                                "phy_event_descriptor_list");
+            }
+            xcp = vcp + 52;
+            for (m = 0; m < (num_ped * 12); m += 12, xcp += 12) {
+                int pes = xcp[3];
+                unsigned int pvdt_v;
+
+                if (jsp->pr_as_json)
+                    jo3p = sgj_new_unattached_object_r(jsp);
+                ui = sg_get_unaligned_be32(xcp + 4);
+                pvdt_v = sg_get_unaligned_be32(xcp + 8);
+                ccp = show_sas_phy_event_info(pes, ui, pvdt_v, b, blen);
+                if (0 == strlen(ccp)) {
+                    sgj_pr_hr(jsp, "      %s\n", b);    /* unknown pvdt_v */
+                    if (jsp->pr_as_json) {
+                        int n;
+
+                        snprintf(s, sz, "%s_pes_0x%x", unknown_s, pes);
+                        sgj_js_nv_ihex(jsp, jo3p, s, ui);
+                        n = strlen(s);
+                        sg_scnpr(s + n, sz - n, "_%s", "threshold");
+                        sgj_js_nv_ihex(jsp, jo3p, s, pvdt_v);
+                    }
+                } else {
+                    if (jsp->pr_as_json) {
+                        sgj_convert_to_snake_name(ccp, s, sz);
+                        sgj_js_nv_ihex(jsp, jo3p, s, ui);
+                        if (0x2b == pes)
+                            sgj_js_nv_ihex(jsp, jo3p, pvdt, pvdt_v);
+                        else if (0x2c == pes)
+                            sgj_js_nv_ihex(jsp, jo3p, pvdt, pvdt_v);
+                        else if (0x2d == pes)
+                            sgj_js_nv_ihex(jsp, jo3p, pvdt, pvdt_v);
+                        else if (0x2e == pes)
+                            sgj_js_nv_ihex(jsp, jo3p, pvdt, pvdt_v);
+                    } else {
+                        cp = strchr(b, '\t');
+                        if (cp) {
+                            *cp = '\0';
+                            sgj_pr_hr(jsp, "      %s\n", b);
+                            sgj_pr_hr(jsp, "      %s\n", cp + 1);
+                        } else
+                            sgj_pr_hr(jsp, "      %s\n", b);
+                    }
+                }
+                if (jsp->pr_as_json)
+                    sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+            }
+        } else if (op->verbose)
+           printf("    <<No phy event descriptors>>\n");
+skip:
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }   /* end of for loop over phys with this relative port */
+}
+
+/* PROTO_SPECIFIC_LPAGE [0x18] <psp> */
+static bool
+show_protocol_specific_port_page(const uint8_t * resp, int len,
+                                 struct opts_t * op, sgj_opaque_p jop)
+{
+    int k, num, pl, pid;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char b[128];
+    static const char * psplp = "Protocol specific port log page";
+    static const char * fss = "for SAS SSP";
+
+    num = len - 4;
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (op->do_name)
+            sgj_pr_hr(jsp, "log_page=0x%x\n", PROTO_SPECIFIC_LPAGE);
+        else
+            sgj_pr_hr(jsp, "%s  [0x18]\n", psplp);
+    }
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, psplp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                           "protocol_specific_port_log_parameter_list");
+    }
+
+    for (k = 0, bp = resp + 4; k < num; ) {
+        int pc = sg_get_unaligned_be16(bp + 0);
+
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto skip;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto skip;
+        }
+        pid = 0xf & bp[4];
+        if (6 != pid) {
+            pr2serr("Protocol identifier: %d, only support SAS (SPL) which "
+                    "is 6\n", pid);
+            return false;   /* only decode SAS log page */
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+        if ((0 == k) && (! op->do_name))
+             sgj_pr_hr(jsp, "%s %s  [0x18]\n", psplp, fss);
+        /* call helper */
+        show_sas_port_param(bp, pl, op, jo3p);
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if ((op->do_pcb) && (! op->do_name))
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], b,
+                      sizeof(b)));
+        if (op->filter_given)
+            break;
+skip:
+        k += pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Returns true if processed page, false otherwise */
+/* STATS_LPAGE [0x19], subpages: 0x0 to 0x1f <gsp,grsp>  introduced: SPC-4 */
+static bool
+show_stats_perform_pages(const uint8_t * resp, int len,
+                         struct opts_t * op, sgj_opaque_p jop)
+{
+    bool nam, spf;
+    int k, num, param_len, param_code, subpg_code, extra;
+    uint64_t ull;
+    const uint8_t * bp;
+    const char * ccp;
+    const char * pg_name;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    static const char * gsaplp =
+                "General statistics and performance log page";
+    static const char * gr_saplp =
+                "Group statistics and performance log page";
+
+// yyyyyyyyyyyyyyyyyy
+    nam = op->do_name;
+    num = len - 4;
+    bp = resp + 4;
+    spf = !!(resp[0] & 0x40);
+    subpg_code = spf ? resp[1] : NOT_SPG_SUBPG;
+    if (0 == subpg_code)
+        pg_name = gsaplp;
+    else if (subpg_code < 0x20)
+        pg_name = gr_saplp;
+    else
+        pg_name = "Unknown subpage";
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (nam) {
+             sgj_pr_hr(jsp, "log_page=0x%x\n", STATS_LPAGE);
+            if (subpg_code > 0)
+                sgj_pr_hr(jsp, "log_subpage=0x%x\n", subpg_code);
+        } else {
+            if (0 == subpg_code)
+                sgj_pr_hr(jsp, "%s  [0x19]\n", gsaplp);
+            else if (subpg_code < 0x20)
+                sgj_pr_hr(jsp, "%s (%d)  [0x19,0x%x]\n", gr_saplp, subpg_code,
+                          subpg_code);
+            else
+                sgj_pr_hr(jsp, "%s: %d  [0x19,0x%x]\n", pg_name, subpg_code,
+                          subpg_code);
+        }
+    }
+    if (jsp->pr_as_json)
+        jo2p = sg_log_js_hdr(jsp, jop, pg_name, resp);
+    if (subpg_code > 31)
+        return false;
+    if (jsp->pr_as_json)
+        jap = sgj_named_subarray_r(jsp, jo2p, 0 == subpg_code ?
+                        "general_statistics_and_performance_log_parameters" :
+                        "group_statistics_and_performance_log_parameters");
+    if (0 == subpg_code) { /* General statistics and performance log page */
+        if (num < 0x5c)
+            return false;
+        for (k = num; k > 0; k -= extra, bp += extra) {
+            unsigned int ui;
+
+            if (k < 3)
+                return false;
+            param_len = bp[3];
+            extra = param_len + 4;
+            param_code = sg_get_unaligned_be16(bp + 0);
+            if (op->filter_given) {
+                if (param_code != op->filter)
+                    continue;
+            }
+            if (op->do_raw) {
+                dStrRaw(bp, extra);
+                goto skip;
+            } else if (op->do_hex) {
+                hex2stdout(bp, extra, op->dstrhex_no_ascii);
+                goto skip;
+            }
+            if (jsp->pr_as_json) {
+                jo3p = sgj_new_unattached_object_r(jsp);
+                if (op->do_pcb)
+                    js_pcb(jsp, jo3p, bp[2]);
+            }
+
+            switch (param_code) {
+            case 1:     /* Statistics and performance log parameter */
+                ccp = nam ? "parameter_code=1" : "Statistics and performance "
+                        "log parameter";
+                sgj_pr_hr(jsp, "%s\n", ccp);
+                ull = sg_get_unaligned_be64(bp + 4);
+                ccp = nam ? "read_commands=" : "number of read commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 12);
+                ccp = nam ? "write_commands=" : "number of write commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 20);
+                ccp = nam ? "lb_received="
+                          : "number of logical blocks received = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 28);
+                ccp = nam ? "lb_transmitted="
+                          : "number of logical blocks transmitted = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 36);
+                ccp = nam ? "read_proc_intervals="
+                          : "read command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 44);
+                ccp = nam ? "write_proc_intervals="
+                          : "write command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 52);
+                ccp = nam ? "weight_rw_commands=" : "weighted number of "
+                                "read commands plus write commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 60);
+                ccp = nam ? "weight_rw_processing=" : "weighted read command "
+                                "processing plus write command processing = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                break;
+            case 2:     /* Idle time log parameter */
+                ccp = nam ? "parameter_code=2" : "Idle time log parameter";
+                sgj_pr_hr(jsp, "%s\n", ccp);
+                ull = sg_get_unaligned_be64(bp + 4);
+                ccp = nam ? "idle_time_intervals=" : "idle time "
+                                "intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                break;
+            case 3:     /* Time interval log parameter for general stats */
+                ccp = nam ? "parameter_code=3" : "Time interval log "
+                        "parameter for general stats";
+                sgj_pr_hr(jsp, "%s\n", ccp);
+                ui = sg_get_unaligned_be32(bp + 4);
+                ccp = nam ? "time_interval_neg_exp=" : "time interval "
+                                "negative exponent = ";
+                sgj_pr_hr(jsp, "  %s%u\n", ccp, ui);
+                ui = sg_get_unaligned_be32(bp + 8);
+                ccp = nam ? "time_interval_int=" : "time interval "
+                                "integer = ";
+                sgj_pr_hr(jsp, "  %s%u\n", ccp, ui);
+                break;
+            case 4:     /* FUA statistics and performance log parameter */
+                ccp = nam ? "parameter_code=4" : "Force unit access "
+                        "statistics and performance log parameter ";
+                sgj_pr_hr(jsp, "%s\n", ccp);
+                ull = sg_get_unaligned_be64(bp + 4);
+                ccp = nam ? "read_fua_commands=" : "number of read FUA "
+                                "commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 12);
+                ccp = nam ? "write_fua_commands=" : "number of write FUA "
+                                "commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 20);
+                ccp = nam ? "read_fua_nv_commands="
+                          : "number of read FUA_NV commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 28);
+                ccp = nam ? "write_fua_nv_commands="
+                          : "number of write FUA_NV commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 36);
+                ccp = nam ? "read_fua_proc_intervals="
+                          : "read FUA command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 44);
+                ccp = nam ? "write_fua_proc_intervals="
+                          : "write FUA command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 52);
+                ccp = nam ? "read_fua_nv_proc_intervals="
+                          : "read FUA_NV command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 60);
+                ccp = nam ? "write_fua_nv_proc_intervals="
+                          : "write FUA_NV command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                break;
+            case 6:     /* Time interval log parameter for cache stats */
+                ccp = nam ? "parameter_code=6" : "Time interval log "
+                        "parameter for cache stats";
+                sgj_pr_hr(jsp, "%s\n", ccp);
+                ull = sg_get_unaligned_be64(bp + 4);
+                ccp = nam ? "time_interval_neg_exp=" : "time interval "
+                                "negative exponent = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 8);
+                ccp = nam ? "time_interval_int=" : "time interval "
+                                "integer = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                break;
+            default:
+                if (nam) {
+                    sgj_pr_hr(jsp, "parameter_code=%d\n", param_code);
+                    sgj_pr_hr(jsp, "  unknown=1\n");
+                } else
+                    pr2serr("show_performance...  unknown %s %d\n", param_c,
+                            param_code);
+                if (op->verbose)
+                    hex2stderr(bp, extra, 1);
+                break;
+            }
+            if (jsp->pr_as_json)
+                sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+            if ((op->do_pcb) && (! op->do_name))
+                sgj_pr_hr(jsp, "    <%s>\n", get_pcb_str(bp[2], str,
+                          sizeof(str)));
+skip:
+            if (op->filter_given)
+                break;
+        }
+    } else {    /* Group statistics and performance (n) log page */
+        if (num < 0x34)
+            return false;
+        for (k = num; k > 0; k -= extra, bp += extra) {
+            if (k < 3)
+                return false;
+            param_len = bp[3];
+            extra = param_len + 4;
+            param_code = sg_get_unaligned_be16(bp + 0);
+            if (op->filter_given) {
+                if (param_code != op->filter)
+                    continue;
+            }
+            if (op->do_raw) {
+                dStrRaw(bp, extra);
+                goto skip2;
+            } else if (op->do_hex) {
+                hex2stdout(bp, extra, op->dstrhex_no_ascii);
+                goto skip2;
+            }
+            if (jsp->pr_as_json) {
+                jo3p = sgj_new_unattached_object_r(jsp);
+                if (op->do_pcb)
+                    js_pcb(jsp, jo3p, bp[2]);
+            }
+
+            switch (param_code) {
+            case 1:     /* Group n Statistics and performance log parameter */
+                if (nam)
+                    sgj_pr_hr(jsp, "parameter_code=1\n");
+                else
+                    sgj_pr_hr(jsp, "Group %d Statistics and performance log "
+                           "parameter\n", subpg_code);
+                ull = sg_get_unaligned_be64(bp + 4);
+                ccp = nam ? "gn_read_commands=" : "group n number of read "
+                                "commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 12);
+                ccp = nam ? "gn_write_commands=" : "group n number of write "
+                                "commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 20);
+                ccp = nam ? "gn_lb_received="
+                          : "group n number of logical blocks received = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 28);
+                ccp = nam ? "gn_lb_transmitted="
+                          : "group n number of logical blocks transmitted = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 36);
+                ccp = nam ? "gn_read_proc_intervals="
+                          : "group n read command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 44);
+                ccp = nam ? "gn_write_proc_intervals="
+                          : "group n write command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                break;
+            case 4: /* Group n FUA statistics and performance log parameter */
+                ccp = nam ? "parameter_code=4" : "Group n force unit access "
+                        "statistics and performance log parameter";
+                sgj_pr_hr(jsp, "%s\n", ccp);
+                ull = sg_get_unaligned_be64(bp + 4);
+                ccp = nam ? "gn_read_fua_commands="
+                          : "group n number of read FUA commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 12);
+                ccp = nam ? "gn_write_fua_commands="
+                          : "group n number of write FUA commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 20);
+                ccp = nam ? "gn_read_fua_nv_commands="
+                          : "group n number of read FUA_NV commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 28);
+                ccp = nam ? "gn_write_fua_nv_commands="
+                          : "group n number of write FUA_NV commands = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 36);
+                ccp = nam ? "gn_read_fua_proc_intervals="
+                          : "group n read FUA command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 44);
+                ccp = nam ? "gn_write_fua_proc_intervals=" : "group n write "
+                            "FUA command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 52);
+                ccp = nam ? "gn_read_fua_nv_proc_intervals=" : "group n "
+                            "read FUA_NV command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                ull = sg_get_unaligned_be64(bp + 60);
+                ccp = nam ? "gn_write_fua_nv_proc_intervals=" : "group n "
+                            "write FUA_NV command processing intervals = ";
+                sgj_pr_hr(jsp, "  %s%" PRIu64 "\n", ccp, ull);
+                break;
+            default:
+                if (nam) {
+                    sgj_pr_hr(jsp, "parameter_code=%d\n", param_code);
+                    sgj_pr_hr(jsp, "  unknown=1\n");
+                } else
+                    pr2serr("show_performance...  unknown %s %d\n", param_c,
+                            param_code);
+                if (op->verbose)
+                    hex2stderr(bp, extra, 1);
+                break;
+            }
+            if (jsp->pr_as_json)
+                sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+            if ((op->do_pcb) && (! op->do_name))
+                sgj_pr_hr(jsp, "    <%s>\n", get_pcb_str(bp[2], str,
+                          sizeof(str)));
+skip2:
+            if (op->filter_given)
+                break;
+        }
+    }
+    return true;
+}
+
+/* Returns true if processed page, false otherwise */
+/* STATS_LPAGE [0x19], CACHE_STATS_SUBPG [0x20] <cms>  introduced: SPC-4 */
+static bool
+show_cache_stats_page(const uint8_t * resp, int len, struct opts_t * op,
+                      sgj_opaque_p jop)
+{
+    int k, num, subpg_code, extra;
+    bool nam, spf;
+    unsigned int ui;
+    const uint8_t * bp;
+    const char * ccp;
+    uint64_t ull;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    nam = op->do_name;
+    num = len - 4;
+    bp = resp + 4;
+    if (num < 4) {
+        pr2serr("badly formed Cache memory statistics page\n");
+        return false;
+    }
+    spf = !!(resp[0] & 0x40);
+    subpg_code = spf ? resp[1] : NOT_SPG_SUBPG;
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (nam) {
+            printf("log_page=0x%x\n", STATS_LPAGE);
+            if (subpg_code > 0)
+                printf("log_subpage=0x%x\n", subpg_code);
+        } else
+            printf("Cache memory statistics page  [0x19,0x20]\n");
+    }
+
+    for (k = num; k > 0; k -= extra, bp += extra) {
+        int pc;
+
+        if (k < 3) {
+            pr2serr("short Cache memory statistics page\n");
+            return false;
+        }
+        if (8 != bp[3]) {
+            printf("Cache memory statistics page parameter length not "
+                   "8\n");
+            return false;
+        }
+        extra = bp[3] + 4;
+        pc = sg_get_unaligned_be16(bp + 0);
+        if (op->filter_given) {
+            if (pc != op->filter)
+                continue;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, extra);
+            goto skip;
+        } else if (op->do_hex) {
+            hex2stdout(bp, extra, op->dstrhex_no_ascii);
+            goto skip;
+        }
+        switch (pc) {
+        case 1:     /* Read cache memory hits log parameter */
+            ccp = nam ? "parameter_code=1" :
+                        "Read cache memory hits log parameter";
+            printf("%s\n", ccp);
+            ull = sg_get_unaligned_be64(bp + 4);
+            ccp = nam ? "read_cache_memory_hits=" :
+                        "read cache memory hits = ";
+            printf("  %s%" PRIu64 "\n", ccp, ull);
+            break;
+        case 2:     /* Reads to cache memory log parameter */
+            ccp = nam ? "parameter_code=2" :
+                        "Reads to cache memory log parameter";
+            printf("%s\n", ccp);
+            ull = sg_get_unaligned_be64(bp + 4);
+            ccp = nam ? "reads_to_cache_memory=" :
+                        "reads to cache memory = ";
+            printf("  %s%" PRIu64 "\n", ccp, ull);
+            break;
+        case 3:     /* Write cache memory hits log parameter */
+            ccp = nam ? "parameter_code=3" :
+                        "Write cache memory hits log parameter";
+            printf("%s\n", ccp);
+            ull = sg_get_unaligned_be64(bp + 4);
+            ccp = nam ? "write_cache_memory_hits=" :
+                        "write cache memory hits = ";
+            printf("  %s%" PRIu64 "\n", ccp, ull);
+            break;
+        case 4:     /* Writes from cache memory log parameter */
+            ccp = nam ? "parameter_code=4" :
+                        "Writes from cache memory log parameter";
+            printf("%s\n", ccp);
+            ull = sg_get_unaligned_be64(bp + 4);
+            ccp = nam ? "writes_from_cache_memory=" :
+                        "writes from cache memory = ";
+            printf("  %s%" PRIu64 "\n", ccp, ull);
+            break;
+        case 5:     /* Time from last hard reset log parameter */
+            ccp = nam ? "parameter_code=5" :
+                        "Time from last hard reset log parameter";
+            printf("%s\n", ccp);
+            ull = sg_get_unaligned_be64(bp + 4);
+            ccp = nam ? "time_from_last_hard_reset=" :
+                        "time from last hard reset = ";
+            printf("  %s%" PRIu64 "\n", ccp, ull);
+            break;
+        case 6:     /* Time interval log parameter for cache stats */
+            ccp = nam ? "parameter_code=6" :
+                        "Time interval log parameter";
+            printf("%s\n", ccp);
+            ui = sg_get_unaligned_be32(bp + 4);
+            ccp = nam ? "time_interval_neg_exp=" : "time interval "
+                            "negative exponent = ";
+            printf("  %s%u\n", ccp, ui);
+            ui = sg_get_unaligned_be32(bp + 8);
+            ccp = nam ? "time_interval_int=" : "time interval "
+                            "integer = ";
+            printf("  %s%u\n", ccp, ui);
+            break;
+        default:
+            if (nam) {
+                printf("parameter_code=%d\n", pc);
+                printf("  unknown=1\n");
+            } else
+                pr2serr("show_performance...  unknown %s %d\n", param_c,
+                        pc);
+            if (op->verbose)
+                hex2stderr(bp, extra, 1);
+            break;
+        }
+        if ((op->do_pcb) && (! op->do_name))
+            printf("    <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+skip:
+        if (op->filter_given)
+            break;
+    }
+    return true;
+}
+
+/* FORMAT_STATUS_LPAGE [0x8] <fs>  introduced: SBC-2 */
+static bool
+show_format_status_page(const uint8_t * resp, int len,
+                        struct opts_t * op, sgj_opaque_p jop)
+{
+    bool is_count, is_not_avail;
+    int k, num, pl, pc;
+    uint64_t ull;
+    const char * cp = "";
+    const uint8_t * bp;
+    const uint8_t * xp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[512];
+    static const char * fslp = "Format status log page";
+    static const char * fso = "Format status out";
+    static const char * fso_sn = "format_status_out";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x8]\n", fslp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, fslp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p, "format_status_log_parameters");
+    }
+
+
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+        is_count = true;
+
+        switch (pc) {
+        case 0:
+            is_not_avail = false;
+            if (pl < 5)
+                sgj_pr_hr(jsp, "  %s: <empty>\n", fso);
+            else {
+                if (sg_all_ffs(bp + 4, pl - 4)) {
+                    sgj_pr_hr(jsp, "  %s: <%s>\n", fso, not_avail);
+                    is_not_avail = true;
+                } else {
+                    hex2str(bp + 4, pl - 4, "    ", op->hex2str_oformat,
+                            sizeof(b), b);
+                    sgj_pr_hr(jsp, "  %s:\n%s", fso, b);
+
+
+                }
+            }
+            if (jsp->pr_as_json) {
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, fso);
+                if (is_not_avail)
+                    sgj_js_nv_ihexstr(jsp, jo3p, fso_sn, 0, NULL, not_avail);
+                else
+                    sgj_js_nv_hex_bytes(jsp, jo3p,  fso_sn, bp + 4, pl - 4);
+            }
+            is_count = false;
+            break;
+        case 1:
+            cp = "Grown defects during certification";
+            break;
+        case 2:
+            cp = "Total blocks reassigned during format";
+            break;
+        case 3:
+            cp = "Total new blocks reassigned";
+            break;
+        case 4:
+            cp = "Power on minutes since format";
+            break;
+        default:
+            sgj_pr_hr(jsp, "  Unknown Format %s = 0x%x\n", param_c, pc);
+            is_count = false;
+            hex2fp(bp, pl, "    ", op->hex2str_oformat, stdout);
+            if (jsp->pr_as_json) {
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, unknown_s);
+                sgj_js_nv_hex_bytes(jsp, jo3p,  in_hex, bp, pl);
+            }
+            break;
+        }
+        if (is_count) {
+            k = pl - 4;
+            xp = bp + 4;
+            is_not_avail = false;
+            ull = 0;
+            if (sg_all_ffs(xp, k)) {
+                sgj_pr_hr(jsp, "  %s: <%s>\n", cp, not_avail);
+                is_not_avail = true;
+            } else {
+                if (k > (int)sizeof(ull)) {
+                    xp += (k - sizeof(ull));
+                    k = sizeof(ull);
+                }
+                ull = sg_get_unaligned_be(k, xp);
+                sgj_pr_hr(jsp, "  %s = %" PRIu64 "\n", cp, ull);
+            }
+            if (jsp->pr_as_json) {
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, cp);
+                sgj_convert_to_snake_name(cp, b, sizeof(b));
+                if (is_not_avail)
+                    sgj_js_nv_ihexstr(jsp, jo3p, b, 0, NULL, not_avail);
+                else
+                    sgj_js_nv_ihex(jsp, jo3p, b, ull);
+            }
+        }
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if ((op->do_pcb) && (! op->do_name))
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Non-volatile cache page [0x17] <nvc>  introduced: SBC-2
+ * Standard vacillates between "non-volatile" and "nonvolatile" */
+static bool
+show_non_volatile_cache_page(const uint8_t * resp, int len,
+                             struct opts_t * op, sgj_opaque_p jop)
+{
+    int j, num, pl, pc;
+    const char * cp;
+    const char * c2p;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[96];
+    static const char * nvclp = "Non-volatile cache log page";
+    static const char * ziinv = "0 (i.e. it is now volatile)";
+    static const char * indef = "indefinite";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+         sgj_pr_hr(jsp, "%s  [0x17]\n", nvclp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, nvclp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                                   "nonvolatile_cache_log_parameters");
+    }
+
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        cp = NULL;
+        switch (pc) {
+        case 0:
+            cp = "Remaining nonvolatile time";
+            c2p = NULL;
+            j = sg_get_unaligned_be24(bp + 5);
+            switch (j) {
+            case 0:
+                c2p = ziinv;
+                sgj_pr_hr(jsp, "  %s: %s\n", cp, c2p);
+                break;
+            case 1:
+                c2p = unknown_s;
+                sgj_pr_hr(jsp, "  %s: <%s>\n", cp, c2p);
+                break;
+            case 0xffffff:
+                c2p = indef;
+                sgj_pr_hr(jsp, "  %s: <%s>\n", cp, c2p);
+                break;
+            default:
+                snprintf(b, sizeof(b), "%d minutes [%d:%d]", j, (j / 60),
+                         (j % 60));
+                c2p = b;
+                sgj_pr_hr(jsp, "  %s: %s\n", cp, c2p);
+                break;
+            }
+            break;
+        case 1:
+            cp = "Maximum non-volatile time";
+            c2p = NULL;
+            j = sg_get_unaligned_be24(bp + 5);
+            switch (j) {
+            case 0:
+                c2p = ziinv;
+                sgj_pr_hr(jsp, "  %s: %s\n", cp, c2p);
+                break;
+            case 1:
+                c2p = rsv_s;
+                sgj_pr_hr(jsp, "  %s: <%s>\n", cp, c2p);
+                break;
+            case 0xffffff:
+                c2p = indef;
+                sgj_pr_hr(jsp, "  %s: <%s>\n", cp, c2p);
+                break;
+            default:
+                snprintf(b, sizeof(b), "%d minutes [%d:%d]", j, (j / 60),
+                         (j % 60));
+                c2p = b;
+                sgj_pr_hr(jsp, "  %s: %s\n", cp, c2p);
+                break;
+            }
+            break;
+        default:
+            sgj_pr_hr(jsp, "  Unknown %s = 0x%x\n", param_c, pc);
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json) {
+            sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                              cp ? cp : unknown_s);
+            if (cp)
+                js_snakenv_ihexstr_nex(jsp, jo3p, cp , j, true,
+                                       NULL, c2p, NULL);
+            else if (pl > 4)
+                sgj_js_nv_hex_bytes(jsp, jo3p, in_hex, bp + 4, pl - 4);
+
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        }
+        if ((op->do_pcb) && (! op->do_name))
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* LB_PROV_LPAGE [0xc] <lbp> introduced: SBC-3 */
+static bool
+show_lb_provisioning_page(const uint8_t * resp, int len,
+                          struct opts_t * op, sgj_opaque_p jop)
+{
+    bool evsm_output = false;
+    int num, pl, pc;
+    const uint8_t * bp;
+    const char * cp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Logical block provisioning page  [0xc]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x1:
+            cp = "  Available LBA mapping threshold";
+            break;
+        case 0x2:
+            cp = "  Used LBA mapping threshold";
+            break;
+        case 0x3:
+            cp = "  Available provisioning resource percentage";
+            break;
+        case 0x100:
+            cp = "  De-duplicated LBA";
+            break;
+        case 0x101:
+            cp = "  Compressed LBA";
+            break;
+        case 0x102:
+            cp = "  Total efficiency LBA";
+            break;
+        default:
+            cp = NULL;
+            break;
+        }
+        if (cp) {
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    pr2serr("\n    truncated by response length, expected at "
+                            "least 8 bytes\n");
+                else
+                    pr2serr("\n    parameter length >= 8 expected, got %d\n",
+                            pl);
+                break;
+            }
+            if (0x3 == pc)      /* resource percentage log parameter */
+                printf("  %s: %u %%\n", cp, sg_get_unaligned_be16(bp + 4));
+            else                /* resource count log parameters */
+                printf("  %s resource count: %u\n", cp,
+                       sg_get_unaligned_be32(bp + 4));
+            if (pl > 8) {
+                switch (bp[8] & 0x3) {      /* SCOPE field */
+                case 0: cp = not_rep; break;
+                case 1: cp = "dedicated to lu"; break;
+                case 2: cp = "not dedicated to lu"; break;
+                case 3: cp = rsv_s; break;
+                }
+                printf("    Scope: %s\n", cp);
+            }
+        } else if ((pc >= 0xfff0) && (pc <= 0xffff)) {
+            if (op->exclude_vendor) {
+                if ((op->verbose > 0) && (0 == op->do_brief) &&
+                    (! evsm_output)) {
+                    evsm_output = true;
+                    printf("  %s parameter(s) being ignored\n", vend_spec);
+                }
+            } else {
+                printf("  %s [0x%x]:", vend_spec, pc);
+                hex2stdout(bp, ((pl < num) ? pl : num), op->dstrhex_no_ascii);
+            }
+        } else {
+            printf("  Reserved [%s=0x%x]:\n", param_c_sn, pc);
+            hex2stdout(bp, ((pl < num) ? pl : num), op->dstrhex_no_ascii);
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* UTILIZATION_SUBPG [0xe,0x1] <util>  introduced: SBC-4 */
+static bool
+show_utilization_page(const uint8_t * resp, int len, struct opts_t * op,
+                      sgj_opaque_p jop)
+{
+    int num, pl, pc, k;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Utilization page  [0xe,0x1]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x0:
+            printf("  Workload utilization:");
+            if ((pl < 6) || (num < 6)) {
+                if (num < 6)
+                    pr2serr("\n    truncated by response length, expected "
+                            "at least 6 bytes\n");
+                else
+                    pr2serr("\n    parameter length >= 6 expected, got %d\n",
+                            pl);
+                break;
+            }
+            k = sg_get_unaligned_be16(bp + 4);
+            printf(" %d.%02d %%\n", k / 100, k % 100);
+            break;
+        case 0x1:
+            printf("  Utilization usage rate based on date and time:");
+            if ((pl < 6) || (num < 6)) {
+                if (num < 6)
+                    pr2serr("\n    truncated by response length, expected "
+                            "at least 6 bytes\n");
+                else
+                    pr2serr("\n    parameter length >= 6 expected, got %d\n",
+                            pl);
+                break;
+            }
+            printf(" %d %%\n", bp[4]);
+            break;
+        default:
+            printf("  Reserved [parameter_code=0x%x]:\n", pc);
+            hex2stdout(bp, ((pl < num) ? pl : num), op->dstrhex_no_ascii);
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* SOLID_STATE_MEDIA_LPAGE [0x11] <ssm>  introduced: SBC-3 */
+static bool
+show_solid_state_media_page(const uint8_t * resp, int len,
+                            struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    static const char * ssmlp = "Solid state media log page";
+    static const char * puei = "Percentage used endurance indicator";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x11]\n", ssmlp);
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, ssmlp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p,
+                           "solid_state_media_log_parameters");
+    }
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        switch (pc) {
+        case 0x1:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    pr2serr("\n    truncated by response length, expected "
+                            "at least 8 bytes\n");
+                else
+                    pr2serr("\n    parameter length >= 8 expected, got %d\n",
+                            pl);
+                break;
+            }
+            sgj_pr_hr(jsp, "  %s: %u %%\n", puei, bp[7]);
+            if (jsp->pr_as_json) {
+                js_snakenv_ihexstr_nex(jsp, jo3p, param_c, pc, true,
+                                       NULL, puei, NULL);
+                js_snakenv_ihexstr_nex(jsp, jo3p, puei, bp[7], false,
+                                       NULL, NULL, NULL);
+            }
+            break;
+        default:
+            printf("  Reserved [parameter_code=0x%x]:\n", pc);
+            hex2stdout(bp, ((pl < num) ? pl : num), op->dstrhex_no_ascii);
+            break;
+        }
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+static const char * dt_dev_activity[] = {
+    "No DT device activity",
+    "Cleaning operation in progress",
+    "Volume is being loaded",
+    "Volume is being unloaded",
+    "Other medium activity",
+    "Reading from medium",
+    "Writing to medium",
+    "Locating medium",
+    "Rewinding medium", /* 8 */
+    "Erasing volume",
+    "Formatting volume",
+    "Calibrating",
+    "Other DT device activity",
+    "Microcode update in progress",
+    "Reading encrypted from medium",
+    "Writing encrypted to medium",
+    "Diagnostic operation in progress", /* 10 */
+};
+
+/* DT device status [0x11] <dtds> (ssc, adc) */
+static bool
+show_dt_device_status_page(const uint8_t * resp, int len,
+                           struct opts_t * op, sgj_opaque_p jop)
+{
+    bool evsm_output = false;
+    int num, pl, pc, j;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+    char b[512];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("DT device status page (ssc-3, adc-3) [0x11]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x0:
+            printf("  Very high frequency data:\n");
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    pr2serr("    truncated by response length, expected at "
+                            "least 8 bytes\n");
+                else
+                    pr2serr("    parameter length >= 8 expected, got %d\n",
+                            pl);
+                break;
+            }
+            printf("  PAMR=%d HUI=%d MACC=%d CMPR=%d ", !!(0x80 & bp[4]),
+                   !!(0x40 & bp[4]), !!(0x20 & bp[4]), !!(0x10 & bp[4]));
+            printf("WRTP=%d CRQST=%d CRQRD=%d DINIT=%d\n", !!(0x8 & bp[4]),
+                   !!(0x4 & bp[4]), !!(0x2 & bp[4]), !!(0x1 & bp[4]));
+            printf("  INXTN=%d RAA=%d MPRSNT=%d ", !!(0x80 & bp[5]),
+                   !!(0x20 & bp[5]), !!(0x10 & bp[5]));
+            printf("MSTD=%d MTHRD=%d MOUNTED=%d\n",
+                   !!(0x4 & bp[5]), !!(0x2 & bp[5]), !!(0x1 & bp[5]));
+            printf("  DT device activity: ");
+            j = bp[6];
+            if (j < (int)SG_ARRAY_SIZE(dt_dev_activity))
+                printf("%s\n", dt_dev_activity[j]);
+            else if (j < 0x80)
+                printf("Reserved [0x%x]\n", j);
+            else
+                printf("%s [0x%x]\n", vend_spec, j);
+            printf("  VS=%d TDDEC=%d EPP=%d ", !!(0x80 & bp[7]),
+                   !!(0x20 & bp[7]), !!(0x10 & bp[7]));
+            printf("ESR=%d RRQST=%d INTFC=%d TAFC=%d\n", !!(0x8 & bp[7]),
+                   !!(0x4 & bp[7]), !!(0x2 & bp[7]), !!(0x1 & bp[7]));
+            break;
+        case 0x1:
+            printf("  Very high frequency polling delay: ");
+            if ((pl < 6) || (num < 6)) {
+                if (num < 6)
+                    pr2serr("\n    truncated by response length, expected at "
+                            "least 6 bytes\n");
+                else
+                    pr2serr("\n    parameter length >= 6 expected, got %d\n",
+                            pl);
+                break;
+            }
+            printf(" %d milliseconds\n", sg_get_unaligned_be16(bp + 4));
+            break;
+        case 0x2:
+            printf("   DT device ADC data encryption control status (hex "
+                   "only now):\n");
+            if ((pl < 12) || (num < 12)) {
+                if (num < 12)
+                    pr2serr("    truncated by response length, expected at "
+                            "least 12 bytes\n");
+                else
+                    pr2serr("    parameter length >= 12 expected, got %d\n",
+                            pl);
+                break;
+            }
+            hex2fp(bp + 4, 8, "      ", op->hex2str_oformat, stdout);
+            break;
+        case 0x3:
+            printf("   Key management error data (hex only now):\n");
+            if ((pl < 16) || (num < 16)) {
+                if (num < 16)
+                    pr2serr("    truncated by response length, expected at "
+                            "least 16 bytes\n");
+                else
+                    pr2serr("    parameter length >= 16 expected, got %d\n",
+                            pl);
+                break;
+            }
+            hex2fp(bp + 4, 12, "      ", op->hex2str_oformat, stdout);
+            break;
+        default:
+            if ((pc >= 0x101) && (pc <= 0x1ff)) {
+                printf("  Primary port %d status:\n", pc - 0x100);
+                if (12 == bp[3]) { /* if length of desc is 12, assume SAS */
+                    printf("    SAS: negotiated physical link rate: %s\n",
+                           sas_negot_link_rate((0xf & (bp[4] >> 4)), b,
+                                               sizeof(b)));
+                    printf("    signal=%d, pic=%d, ", !!(0x2 & bp[4]),
+                           !!(0x1 & bp[4]));
+                    printf("hashed SAS addr: 0x%u\n",
+                            sg_get_unaligned_be24(bp + 5));
+                    printf("    SAS addr: 0x%" PRIx64 "\n",
+                            sg_get_unaligned_be64(bp + 8));
+                } else {
+                    printf("    non-SAS transport, in hex:\n");
+                    hex2fp(bp + 4, ((pl < num) ? pl : num) - 4, "      ",
+                           op->hex2str_oformat, stdout);
+                }
+            } else if (pc >= 0x8000) {
+                if (op->exclude_vendor) {
+                    if ((op->verbose > 0) && (0 == op->do_brief) &&
+                        (! evsm_output)) {
+                        evsm_output = true;
+                        printf("  %s parameter(s) being ignored\n",
+                               vend_spec);
+                    }
+                } else {
+                    printf("  %s [%s=0x%x]:\n", vend_spec, param_c_sn, pc);
+                    hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                           op->hex2str_oformat, stdout);
+                }
+            } else {
+                printf("  Reserved [%s=0x%x]:\n", param_c_sn, pc);
+                hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                        op->hex2str_oformat, stdout);
+            }
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* TapeAlert response [0x12] <tar> (adc,ssc) */
+static bool
+show_tapealert_response_page(const uint8_t * resp, int len,
+                             struct opts_t * op, sgj_opaque_p jop)
+{
+    bool evsm_output = false;
+    int num, pl, pc, k, mod, div;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("TapeAlert response page (ssc-3, adc-3) [0x12]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x0:
+            if (pl < 12) {
+
+            }
+            for (k = 1; k < 0x41; ++k) {
+                mod = ((k - 1) % 8);
+                div = (k - 1) / 8;
+                if (0 == mod) {
+                    if (div > 0)
+                        printf("\n");
+                    printf("  Flag%02Xh: %d", k, !! (bp[4 + div] & 0x80));
+                } else
+                    printf("  %02Xh: %d", k,
+                           !! (bp[4 + div] & (1 << (7 - mod))));
+            }
+            printf("\n");
+            break;
+        default:
+            if (pc <= 0x8000) {
+                printf("  Reserved [parameter_code=0x%x]:\n", pc);
+                hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                        op->hex2str_oformat, stdout);
+            } else {
+                if (op->exclude_vendor) {
+                    if ((op->verbose > 0) && (0 == op->do_brief) &&
+                        (! evsm_output)) {
+                        evsm_output = true;
+                        printf("  %s parameter(s) being ignored\n",
+                               vend_spec);
+                    }
+                } else {
+                    printf("  %s [%s=0x%x]:\n", vend_spec, param_c_sn, pc);
+                    hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                           op->hex2str_oformat, stdout);
+                }
+            }
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+#define NUM_REQ_REC_ARR_ELEMS 16
+static const char * req_rec_arr[NUM_REQ_REC_ARR_ELEMS] = {
+    "Recovery not requested",
+    "Recovery requested, no recovery procedure defined",
+    "Instruct operator to push volume",
+    "Instruct operator to remove and re-insert volume",
+    "Issue UNLOAD command. Instruct operator to remove and re-insert volume",
+    "Instruct operator to power cycle target device",
+    "Issue LOAD command",
+    "Issue UNLOAD command",
+    "Issue LOGICAL UNIT RESET task management function",        /* 0x8 */
+    "No recovery procedure defined. Contact service organization",
+    "Issue UNLOAD command. Instruct operator to remove and quarantine "
+        "volume",
+    "Instruct operator to not insert a volume. Contact service organization",
+    "Issue UNLOAD command. Instruct operator to remove volume. Contact "
+        "service organization",
+    "Request creation of target device error log",
+    "Retrieve a target device error log",
+    "Modify configuration to all microcode update and instruct operator to "
+        "re-insert volume",     /* 0xf */
+};
+
+/* REQ_RECOVERY_LPAGE Requested recovery [0x13] <rr> (ssc) */
+static bool
+show_requested_recovery_page(const uint8_t * resp, int len,
+                             struct opts_t * op, sgj_opaque_p jop)
+{
+    bool evsm_output = false;
+    int num, pl, pc, j, k;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Requested recovery page (ssc-3) [0x13]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x0:
+            printf("  Recovery procedures:\n");
+            for (k = 4; k < pl; ++ k) {
+                j = bp[k];
+                if (j < NUM_REQ_REC_ARR_ELEMS)
+                    printf("    %s\n", req_rec_arr[j]);
+                else if (j < 0x80)
+                    printf("    Reserved [0x%x]\n", j);
+                else
+                    printf("    Vendor specific [0x%x]\n", j);
+            }
+            break;
+        default:
+            if (pc <= 0x8000) {
+                printf("  Reserved [parameter_code=0x%x]:\n", pc);
+                hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                        op->hex2str_oformat, stdout);
+            } else {
+                if (op->exclude_vendor) {
+                    if ((op->verbose > 0) && (0 == op->do_brief) &&
+                        (! evsm_output)) {
+                        evsm_output = true;
+                        printf("  Vendor specific parameter(s) being "
+                               "ignored\n");
+                    }
+                } else {
+                    printf("  Vendor specific [parameter_code=0x%x]:\n", pc);
+                    hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                            op->hex2str_oformat, stdout);
+                }
+            }
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* SAT_ATA_RESULTS_LPAGE (SAT-2) [0x16] <aptr> */
+static bool
+show_ata_pt_results_page(const uint8_t * resp, int len,
+                         struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    const uint8_t * bp;
+    const uint8_t * dp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("ATA pass-through results page (sat-2) [0x16]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        if ((pc < 0xf) && (pl > 17)) {
+            int extend, count;
+
+            printf("  Log_index=0x%x (parameter_code=0x%x)\n", pc + 1, pc);
+            dp = bp + 4;       /* dp is start of ATA Return descriptor
+                                 * which is 14 bytes long */
+            extend = dp[2] & 1;
+            count = dp[5] + (extend ? (dp[4] << 8) : 0);
+            printf("    extend=%d  error=0x%x count=0x%x\n", extend,
+                   dp[3], count);
+            if (extend)
+                printf("    lba=0x%02x%02x%02x%02x%02x%02x\n", dp[10], dp[8],
+                       dp[6], dp[11], dp[9], dp[7]);
+            else
+                printf("    lba=0x%02x%02x%02x\n", dp[11], dp[9], dp[7]);
+            printf("    device=0x%x  status=0x%x\n", dp[12], dp[13]);
+        } else if (pl > 17) {
+            printf("  Reserved [parameter_code=0x%x]:\n", pc);
+            hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                   op->hex2str_oformat, stdout);
+        } else {
+            printf("  short parameter length: %d [parameter_code=0x%x]:\n",
+                   pl, pc);
+            hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                   op->hex2str_oformat, stdout);
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+static const char * bms_status[] = {
+    "no background scans active",
+    "background medium scan is active",
+    "background pre-scan is active",
+    "background scan halted due to fatal error",
+    "background scan halted due to a vendor specific pattern of error",
+    "background scan halted due to medium formatted without P-List",
+    "background scan halted - vendor specific cause",
+    "background scan halted due to temperature out of range",
+    ("background scan enabled, none active (waiting for BMS interval timer "
+        "to expire)"),  /* clang warns about this, add parens to quieten */
+    "background scan halted - scan results list full",
+    "background scan halted - pre-scan time limit timer expired" /* 10 */,
+};
+
+static const char * reassign_status[] = {
+    "Reserved [0x0]",
+    "Reassignment pending receipt of Reassign or Write command",
+    "Logical block successfully reassigned by device server",
+    "Reserved [0x3]",
+    "Reassignment by device server failed",
+    "Logical block recovered by device server via rewrite",
+    "Logical block reassigned by application client, has valid data",
+    "Logical block reassigned by application client, contains no valid data",
+    "Logical block unsuccessfully reassigned by application client", /* 8 */
+};
+
+/* Background scan results [0x15,0] <bsr> for disk  introduced: SBC-3 */
+static bool
+show_background_scan_results_page(const uint8_t * resp, int len,
+                                  struct opts_t * op, sgj_opaque_p jop)
+{
+    bool skip_out = false;
+    bool evsm_output = false;
+    bool ok;
+    int j, m, n, num, pl, pc;
+    const uint8_t * bp;
+    double d;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    sgj_opaque_p jo3p = NULL;
+    sgj_opaque_p jap = NULL;
+    char str[PCB_STR_LEN];
+    char b[144];
+    char e[80];
+    static const int blen = sizeof(b);
+    static const int elen = sizeof(e);
+    static const char * bsrlp = "Background scan results log page";
+    static const char * bss = "Background scan status";
+    static const char * bms = "Background medium scan";
+    static const char * bsr = "Background scan results";
+    static const char * bs = "background scan";
+    static const char * ms = "Medium scan";
+    static const char * apom = "Accumulated power on minutes";
+    static const char * rs = "Reassign status";
+
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        sgj_pr_hr(jsp, "%s  [0x15]\n", bsrlp);
+    num = len - 4;
+    bp = &resp[0] + 4;
+    if (jsp->pr_as_json) {
+        jo2p = sg_log_js_hdr(jsp, jop, bsrlp, resp);
+        jap = sgj_named_subarray_r(jsp, jo2p, "background_scan_parameters");
+    }
+
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        if (jsp->pr_as_json) {
+            jo3p = sgj_new_unattached_object_r(jsp);
+            if (op->do_pcb)
+                js_pcb(jsp, jo3p, bp[2]);
+        }
+
+        switch (pc) {
+        case 0:
+            sgj_pr_hr(jsp, "  Status parameters:\n");
+            if (jsp->pr_as_json)
+                sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, bss);
+            if ((pl < 16) || (num < 16)) {
+                if (num < 16)
+                    pr2serr("    truncated by response length, expected at "
+                            "least 16 bytes\n");
+                else
+                    pr2serr("    parameter length >= 16 expected, got %d\n",
+                            pl);
+                break;
+            }
+            sg_scnpr(b, blen, "    %s: ", apom);
+            j = sg_get_unaligned_be32(bp + 4);
+            sgj_pr_hr(jsp, "%s%d [h:m  %d:%d]\n", b, j, (j / 60), (j % 60));
+            if (jsp->pr_as_json)
+                js_snakenv_ihexstr_nex(jsp, jo3p, apom, j, false, NULL, NULL,
+                                       NULL);
+            sg_scnpr(b, blen, "    Status: ");
+            j = bp[9];
+            ok = (j < (int)SG_ARRAY_SIZE(bms_status));
+            if (ok)
+                sgj_pr_hr(jsp, "%s%s\n", b, bms_status[j]);
+            else
+                sgj_pr_hr(jsp, "%sunknown [0x%x] %s value\n", b, j, bss);
+            if (jsp->pr_as_json)
+                js_snakenv_ihexstr_nex(jsp, jo3p, bss, j, true, NULL,
+                                       ok ? bms_status[j] : unknown_s, NULL);
+            j = sg_get_unaligned_be16(bp + 10);
+            snprintf(b, blen, "Number of %ss performed", bs);
+            sgj_pr_hr(jsp, "    %s: %d\n", b, j);
+            if (jsp->pr_as_json)
+                js_snakenv_ihexstr_nex(jsp, jo3p, b, j, true, NULL, NULL,
+                                       NULL);
+            j = sg_get_unaligned_be16(bp + 12);
+            snprintf(b, blen, "%s progress", bms);
+            d = (100.0 * j / 65536.0);
+#ifdef SG_LIB_MINGW
+            snprintf(e, elen, "%g %%", d);
+#else
+            snprintf(e, elen, "%.2f %%", d);
+#endif
+            sgj_pr_hr(jsp, "    %s: %s\n", b, e);
+            if (jsp->pr_as_json)
+                js_snakenv_ihexstr_nex(jsp, jo3p, b, j, true, NULL, e,
+                                       NULL);
+            j = sg_get_unaligned_be16(bp + 14);
+            snprintf(b, blen, "Number of %ss performed", bms);
+
+            ok = (j > 0);
+            if (ok)
+                sgj_pr_hr(jsp, "    %s: %d\n", b, j);
+            else
+                sgj_pr_hr(jsp, "    %s: 0 [%s]\n", b, not_rep);
+            if (jsp->pr_as_json)
+                js_snakenv_ihexstr_nex(jsp, jo3p, b, j, true, NULL,
+                                       ok ? NULL : not_rep, NULL);
+            break;
+        default:
+            if (pc > 0x800) {
+                if (jsp->pr_as_json)
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL,
+                                      (pc >= 0x8000) ? vend_spec : NULL);
+                if ((pc >= 0x8000) && (pc <= 0xafff)) {
+                    if (op->exclude_vendor) {
+                        skip_out = true;
+                        if ((op->verbose > 0) && (0 == op->do_brief) &&
+                            (! evsm_output)) {
+                            evsm_output = true;
+                            sgj_pr_hr(jsp, "  %s parameter(s) being "
+                                      "ignored\n", vend_spec);
+                        }
+                    } else
+                        sgj_pr_hr(jsp, "  %s parameter # %d [0x%x], %s\n",
+                                  ms, pc, pc, vend_spec);
+                } else
+                    sgj_pr_hr(jsp, "  %s parameter # %d [0x%x], %s\n", ms, pc,
+                              pc, rsv_s);
+                if (skip_out)
+                    skip_out = false;
+                else
+                    hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                           op->hex2str_oformat, stdout);
+                break;
+            } else {
+                sgj_pr_hr(jsp, "  %s parameter # %d [0x%x]\n", ms, pc, pc);
+                if (jsp->pr_as_json)
+                    sgj_js_nv_ihexstr(jsp, jo3p, param_c_sn, pc, NULL, bsr);
+            }
+            if ((pl < 24) || (num < 24)) {
+                if (num < 24)
+                    pr2serr("    truncated by response length, expected at "
+                            "least 24 bytes\n");
+                else
+                    pr2serr("    parameter length >= 24 expected, got %d\n",
+                            pl);
+                break;
+            }
+            j = sg_get_unaligned_be32(bp + 4);
+            n = (j % 60);
+            sgj_pr_hr(jsp, "    %s when error detected: %d [%d:%d]\n", apom,
+                      j, (j / 60), n);
+            if (jsp->pr_as_json) {
+                snprintf(b, blen, "%d hours, %d minute%s", (j / 60), n,
+                         n != 1 ? "s" : "");
+                js_snakenv_ihexstr_nex(jsp, jo3p, apom, j, true, NULL, b,
+                                       "when error detected [unit: minute]");
+            }
+            j = (bp[8] >> 4) & 0xf;
+            ok = (j < (int)SG_ARRAY_SIZE(reassign_status));
+            if (ok)
+                sgj_pr_hr(jsp, "    %s: %s\n", rs, reassign_status[j]);
+            else
+                sgj_pr_hr(jsp, "    %s: %s [0x%x]\n", rs, rsv_s, j);
+            if (jsp->pr_as_json)
+                js_snakenv_ihexstr_nex(jsp, jo3p, rs, j, true, NULL,
+                                       ok ? reassign_status[j] : NULL, NULL);
+            n = 0xf & b[8];
+            sgj_pr_hr(jsp, "    %s: %s  [sk,asc,ascq: 0x%x,0x%x,0x%x]\n",
+                      s_key, sg_get_sense_key_str(n, blen, b), n, bp[9],
+                                                  bp[10]);
+            if (bp[9] || bp[10])
+                sgj_pr_hr(jsp, "      %s\n",
+                          sg_get_asc_ascq_str(bp[9], bp[10], blen, b));
+            if (jsp->pr_as_json) {
+                sgj_js_nv_ihexstr(jsp, jo3p, "sense_key", n, NULL,
+                                  sg_get_sense_key_str(n, blen, b));
+                sgj_js_nv_ihexstr(jsp, jo3p, "additional_sense_code", bp[9],
+                                  NULL, NULL);
+                sgj_js_nv_ihexstr(jsp, jo3p, "additional_sense_code_qualifier",
+                                  bp[10], NULL, sg_get_asc_ascq_str(bp[9],
+                                  bp[10], blen, b));
+            }
+            if (op->verbose) {
+                n = sg_scnpr(b, blen, "    vendor bytes [11 -> 15]: ");
+                for (m = 0; m < 5; ++m)
+                    n += sg_scnpr(b + n, blen - n, "0x%02x ", bp[11 + m]);
+                 sgj_pr_hr(jsp, "%s\n", b);
+            }
+            n = sg_scnpr(b, blen, "    LBA (associated with medium error): "
+                         "0x");
+            if (sg_all_zeros(bp + 16, 8))
+                sgj_pr_hr(jsp, "%s0\n", b);
+            else {
+                for (m = 0; m < 8; ++m)
+                    n += sg_scnpr(b + n, blen - n, "%02x", bp[16 + m]);
+                sgj_pr_hr(jsp, "%s\n", b);
+            }
+            if (jsp->pr_as_json)
+                js_snakenv_ihexstr_nex(jsp, jo3p, "logical_block_address",
+                                       sg_get_unaligned_be64(bp + 16), true,
+                                       NULL, NULL, "of medium error");
+            break;
+        }               /* end of switch statement block */
+        if (jsp->pr_as_json)
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo3p);
+        if (op->do_pcb)
+            sgj_pr_hr(jsp, "        <%s>\n", get_pcb_str(bp[2], str,
+                      sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* ZONED_BLOCK_DEV_STATS_SUBPG [0x14,0x1] <zbds>  introduced: zbc2r01 */
+static bool
+show_zoned_block_dev_stats(const uint8_t * resp, int len,
+                           struct opts_t * op, sgj_opaque_p jop)
+{
+    bool trunc, bad_pl;
+    int num, pl, pc;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Zoned block device statistics page  [0x14,0x1]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        trunc = false;
+        bad_pl = false;
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (4 == pl)    /* DC HC560 has empty descriptors */
+            goto skip;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x0:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Maximum open zones: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x1:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Maximum explicitly open zones: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x2:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Maximum implicitly open zones: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x3:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Minimum empty zones: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x4:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Maximum non-sequential zones: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x5:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Zones emptied: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x6:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Suboptimal write commands: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x7:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Commands exceeding optimal limit: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x8:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Failed explicit opens: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0x9:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Read rule violations: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0xa:
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Write rule violations: %" PRIu32 "\n",
+                       sg_get_unaligned_be32(bp + 8));
+            break;
+        case 0xb:       /* added zbc2r04 */
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    trunc = true;
+                else
+                    bad_pl = true;
+            } else
+                printf("  Maximum implicitly open or before required zones: "
+                       "%" PRIu32 "\n", sg_get_unaligned_be32(bp + 8));
+            break;
+        default:
+            printf("  Reserved [parameter_code=0x%x]:\n", pc);
+            hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                   op->hex2str_oformat, stdout);
+            break;
+        }
+        if (trunc)
+            pr2serr("    truncated by response length, expected at least "
+                    "8 bytes\n");
+        if (bad_pl)
+            pr2serr("    parameter length >= 8 expected, got %d\n", pl);
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* PENDING_DEFECTS_SUBPG [0x15,0x1] <pd>  introduced: SBC-4 */
+static bool
+show_pending_defects_page(const uint8_t * resp, int len,
+                          struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    uint32_t count;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Pending defects page  [0x15,0x1]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x0:
+            printf("  Pending defect count: ");
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    pr2serr("\n    truncated by response length, expected "
+                            "at least 8 bytes\n");
+                else
+                    pr2serr("\n    parameter length >= 8 expected, got %d\n",
+                            pl);
+                break;
+            }
+            count = sg_get_unaligned_be32(bp + 4);
+            if (0 == count) {
+                printf("0\n");
+                break;
+            }
+            printf("%3u  |     LBA            Accumulated power_on\n", count);
+            printf("-----------------------------|---------------");
+            printf("-----------hours---------\n");
+            break;
+        default:
+            printf("  Pending defect %4d:  ", pc);
+            if ((pl < 16) || (num < 16)) {
+                if (num < 16)
+                    pr2serr("\n    truncated by response length, expected "
+                            "at least 16 bytes\n");
+                else
+                    pr2serr("\n    parameter length >= 16 expected, got %d\n",
+                            pl);
+                break;
+            }
+            printf("        0x%-16" PRIx64 "      %5u\n",
+                   sg_get_unaligned_be64(bp + 8),
+                   sg_get_unaligned_be32(bp + 4));
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* BACKGROUND_OP_SUBPG [0x15,0x2] <bop>  introduced: SBC-4 rev 7 */
+static bool
+show_background_op_page(const uint8_t * resp, int len,
+                        struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Background operation page  [0x15,0x2]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x0:
+            printf("  Background operation:");
+            if ((pl < 8) || (num < 8)) {
+                if (num < 8)
+                    pr2serr("\n    truncated by response length, expected "
+                            "at least 8 bytes\n");
+                else
+                    pr2serr("\n    parameter length >= 8 expected, got %d\n",
+                            pl);
+                break;
+            }
+            printf(" BO_STATUS=%d\n", bp[4]);
+            break;
+        default:
+            printf("  Reserved [parameter_code=0x%x]:\n", pc);
+            hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                   op->hex2str_oformat, stdout);
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* LPS misalignment page [0x15,0x3] <lps>  introduced: SBC-4 rev 10
+   LPS: "Long Physical Sector" a term from an ATA feature set */
+static bool
+show_lps_misalignment_page(const uint8_t * resp, int len,
+                           struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("LPS misalignment page  [0x15,0x3]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0x0:
+            printf("  LPS misalignment count: ");
+            if (4 == bp[3])
+                printf("max lpsm: %" PRIu16 ", count=%" PRIu16 "\n",
+                       sg_get_unaligned_be16(bp + 4),
+                       sg_get_unaligned_be16(bp + 6));
+            else
+                printf("<unexpected pc=0 parameter length=%d>\n", bp[4]);
+            break;
+        default:
+            if (pc <= 0xf000) {         /* parameter codes 0x1 to 0xf000 */
+                if (8 == bp[3])
+                    printf("  LBA of misaligned block: 0x%" PRIx64 "\n",
+                           sg_get_unaligned_be64(bp + 4));
+                else
+                    printf("<unexpected pc=0x%x parameter length=%d>\n",
+                           pc, bp[4]);
+            } else {
+                printf("<unexpected pc=0x%x>\n", pc);
+                hex2fp(bp, ((pl < num) ? pl : num), "    ",
+                       op->hex2str_oformat, stdout);
+            }
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Service buffer information [0x15] <sbi> (adc) */
+static bool
+show_service_buffer_info_page(const uint8_t * resp, int len,
+                              struct opts_t * op, sgj_opaque_p jop)
+{
+    bool evsm_output = false;
+    int num, pl, pc;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Service buffer information page (adc-3) [0x15]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        if (pc < 0x100) {
+            printf("  Service buffer identifier: 0x%x\n", pc);
+            printf("    Buffer id: 0x%x, tu=%d, nmp=%d, nmm=%d, "
+                   "offline=%d\n", bp[4], !!(0x10 & bp[5]),
+                   !!(0x8 & bp[5]), !!(0x4 & bp[5]), !!(0x2 & bp[5]));
+            printf("    pd=%d, code_set: %s, Service buffer title:\n",
+                   !!(0x1 & bp[5]), sg_get_desig_code_set_str(0xf & bp[6]));
+            printf("      %.*s\n", pl - 8, bp + 8);
+        } else if (pc < 0x8000) {
+            printf("  parameter_code=0x%x, Reserved, parameter in hex:\n",
+                   pc);
+            hex2fp(bp + 4, pl - 4, "    ", op->hex2str_oformat, stdout);
+        } else {
+            if (op->exclude_vendor) {
+                if ((op->verbose > 0) && (0 == op->do_brief) &&
+                    (! evsm_output)) {
+                    evsm_output = true;
+                    printf("  Vendor specific parameter(s) being "
+                           "ignored\n");
+                }
+            } else {
+                printf("  parameter_code=0x%x, Vendor-specific, parameter in "
+                       "hex:\n", pc);
+                hex2fp(bp + 4, pl - 4, "    ", op->hex2str_oformat, stdout);
+            }
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Sequential access device page [0xc] <sad> for tape */
+static bool
+show_sequential_access_page(const uint8_t * resp, int len,
+                            struct opts_t * op, sgj_opaque_p jop)
+{
+    bool evsm_output = false;
+    int num, pl, pc;
+    const uint8_t * bp;
+    uint64_t ull, gbytes;
+    bool all_set;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Sequential access device page (ssc-3)\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        ull = sg_get_unaligned_be(pl - 4, bp + 4);
+        all_set = sg_all_ffs(bp + 4, pl - 4);
+        gbytes = ull / 1000000000;
+        switch (pc) {
+        case 0:
+            printf("  Data bytes received with WRITE commands: %" PRIu64
+                   " GB", gbytes);
+            if (op->verbose)
+                printf(" [%" PRIu64 " bytes]", ull);
+            printf("\n");
+            break;
+        case 1:
+            printf("  Data bytes written to media by WRITE commands: %" PRIu64
+                   " GB", gbytes);
+            if (op->verbose)
+                printf(" [%" PRIu64 " bytes]", ull);
+            printf("\n");
+            break;
+        case 2:
+            printf("  Data bytes read from media by READ commands: %" PRIu64
+                   " GB", gbytes);
+            if (op->verbose)
+                printf(" [%" PRIu64 " bytes]", ull);
+            printf("\n");
+            break;
+        case 3:
+            printf("  Data bytes transferred by READ commands: %" PRIu64
+                   " GB", gbytes);
+            if (op->verbose)
+                printf(" [%" PRIu64 " bytes]", ull);
+            printf("\n");
+            break;
+        case 4:
+            if (! all_set)
+                printf("  Native capacity from BOP to EOD: %" PRIu64 " MB\n",
+                       ull);
+            break;
+        case 5:
+            if (! all_set)
+                printf("  Native capacity from BOP to EW of current "
+                       "partition: %" PRIu64 " MB\n", ull);
+            break;
+        case 6:
+            if (! all_set)
+                printf("  Minimum native capacity from EW to EOP of current "
+                       "partition: %" PRIu64 " MB\n", ull);
+            break;
+        case 7:
+            if (! all_set)
+                printf("  Native capacity from BOP to current position: %"
+                       PRIu64 " MB\n", ull);
+            break;
+        case 8:
+            if (! all_set)
+                printf("  Maximum native capacity in device object buffer: %"
+                       PRIu64 " MB\n", ull);
+            break;
+        case 0x100:
+            if (ull > 0)
+                printf("  Cleaning action required\n");
+            else
+                printf("  Cleaning action not required (or completed)\n");
+            if (op->verbose)
+                printf("    cleaning value: %" PRIu64 "\n", ull);
+            break;
+        default:
+            if (pc >= 0x8000) {
+                if (op->exclude_vendor) {
+                    if ((op->verbose > 0) && (0 == op->do_brief) &&
+                        (! evsm_output)) {
+                        evsm_output = true;
+                        printf("  Vendor specific parameter(s) being "
+                               "ignored\n");
+                    }
+                } else
+                    printf("  Vendor specific parameter [0x%x] value: %"
+                           PRIu64 "\n", pc, ull);
+            } else
+                printf("  Reserved parameter [0x%x] value: %" PRIu64 "\n",
+                       pc, ull);
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Device statistics 0x14 <ds> for tape and ADC */
+static bool
+show_device_stats_page(const uint8_t * resp, int len,
+                       struct opts_t * op, sgj_opaque_p jop)
+{
+    bool evsm_output = false;
+    int num, pl, pc;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Device statistics page (ssc-3 and adc)\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+             goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+             goto filter_chk;
+        }
+        if (pc < 0x1000) {
+            bool vl_num = true;
+
+            switch (pc) {
+            case 0:
+                printf("  Lifetime media loads:");
+                break;
+            case 1:
+                printf("  Lifetime cleaning operations:");
+                break;
+            case 2:
+                printf("  Lifetime power on hours:");
+                break;
+            case 3:
+                printf("  Lifetime media motion (head) hours:");
+                break;
+            case 4:
+                printf("  Lifetime metres of tape processed:");
+                break;
+            case 5:
+                printf("  Lifetime media motion (head) hours when "
+                       "incompatible media last loaded:");
+                break;
+            case 6:
+                printf("  Lifetime power on hours when last temperature "
+                       "condition occurred:");
+                break;
+            case 7:
+                printf("  Lifetime power on hours when last power "
+                       "consumption condition occurred:");
+                break;
+            case 8:
+                printf("  Media motion (head) hours since last successful "
+                       "cleaning operation:");
+                break;
+            case 9:
+                printf("  Media motion (head) hours since 2nd to last "
+                       "successful cleaning:");
+                break;
+            case 0xa:
+                printf("  Media motion (head) hours since 3rd to last "
+                       "successful cleaning:");
+                break;
+            case 0xb:
+                printf("  Lifetime power on hours when last operator "
+                       "initiated forced reset\n    and/or emergency "
+                       "eject occurred:");
+                break;
+            case 0xc:
+                printf("  Lifetime power cycles:");
+                break;
+            case 0xd:
+                printf("  Volume loads since last parameter reset:");
+                break;
+            case 0xe:
+                printf("  Hard write errors:");
+                break;
+            case 0xf:
+                printf("  Hard read errors:");
+                break;
+            case 0x10:
+                printf("  Duty cycle sample time (ms):");
+                break;
+            case 0x11:
+                printf("  Read duty cycle:");
+                break;
+            case 0x12:
+                printf("  Write duty cycle:");
+                break;
+            case 0x13:
+                printf("  Activity duty cycle:");
+                break;
+            case 0x14:
+                printf("  Volume not present duty cycle:");
+                break;
+            case 0x15:
+                printf("  Ready duty cycle:");
+                break;
+            case 0x16:
+                printf("  MBs transferred from app client in duty cycle "
+                       "sample time:");
+                break;
+            case 0x17:
+                printf("  MBs transferred to app client in duty cycle "
+                       "sample time:");
+                break;
+            case 0x40:
+                printf("  Drive manufacturer's serial number:");
+                break;
+            case 0x41:
+                printf("  Drive serial number:");
+                break;
+            case 0x42:          /* added ssc5r02b */
+                vl_num = false;
+                printf("  Manufacturing date (yyyymmdd): %.*s\n", pl - 4,
+                       bp + 4);
+                break;
+            case 0x43:          /* added ssc5r02b */
+                vl_num = false;
+                printf("  Manufacturing date (yyyyww): %.*s\n", pl - 4,
+                       bp + 4);
+                break;
+            case 0x80:
+                printf("  Medium removal prevented:");
+                break;
+            case 0x81:
+                printf("  Maximum recommended mechanism temperature "
+                       "exceeded:");
+                break;
+            default:
+                vl_num = false;
+                printf("  Reserved %s [0x%x] data in hex:\n", param_c, pc);
+                hex2fp(bp + 4, pl - 4, "    ", op->hex2str_oformat, stdout);
+                break;
+            }
+            if (vl_num)
+                printf(" %" PRIu64 "\n", sg_get_unaligned_be(pl - 4, bp + 4));
+        } else {        /* parameter_code >= 0x1000 */
+            int k;
+            const uint8_t * p = bp + 4;
+
+            switch (pc) {
+            case 0x1000:
+                printf("  Media motion (head) hours for each medium type:\n");
+                for (k = 0; ((pl - 4) - k) >= 8; k += 8, p += 8)
+                    printf("    [%d] Density code: %u, Medium type: 0x%x, "
+                           "hours: %u\n", ((k / 8) + 1), p[2], p[3],
+                           sg_get_unaligned_be32(p + 4));
+                break;
+            default:
+                if (pc >= 0x8000) {
+                    if (op->exclude_vendor) {
+                        if ((op->verbose > 0) && (0 == op->do_brief) &&
+                            (! evsm_output)) {
+                            evsm_output = true;
+                            printf("  Vendor specific parameter(s) being "
+                                   "ignored\n");
+                        }
+                    } else
+                        printf("  Vendor specific parameter [0x%x], dump in "
+                               "hex:\n", pc);
+                } else {
+                    printf("  Reserved parameter [0x%x], dump in hex:\n", pc);
+                    hex2fp(bp + 4, pl - 4, "    ", op->hex2str_oformat,
+                           stdout);
+                }
+                break;
+            }
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Media changer statistics 0x14 <mcs> for media changer */
+static bool
+show_media_stats_page(const uint8_t * resp, int len, struct opts_t * op,
+                      sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    const uint8_t * bp;
+    uint64_t ull;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Media statistics page (smc-3)\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        ull = sg_get_unaligned_be(pl - 4, bp + 4);
+        switch (pc) {
+        case 0:
+            printf("  Number of moves: %" PRIu64 "\n", ull);
+            break;
+        case 1:
+            printf("  Number of picks: %" PRIu64 "\n", ull);
+            break;
+        case 2:
+            printf("  Number of pick retries: %" PRIu64 "\n", ull);
+            break;
+        case 3:
+            printf("  Number of places: %" PRIu64 "\n", ull);
+            break;
+        case 4:
+            printf("  Number of place retries: %" PRIu64 "\n", ull);
+            break;
+        case 5:
+            printf("  Number of volume tags read by volume "
+                   "tag reader: %" PRIu64 "\n", ull);
+            break;
+        case 6:
+            printf("  Number of invalid volume tags returned by "
+                   "volume tag reader: %" PRIu64 "\n", ull);
+            break;
+        case 7:
+            printf("  Number of library door opens: %" PRIu64 "\n", ull);
+            break;
+        case 8:
+            printf("  Number of import/export door opens: %" PRIu64 "\n",
+                   ull);
+            break;
+        case 9:
+            printf("  Number of physical inventory scans: %" PRIu64 "\n",
+                   ull);
+            break;
+        case 0xa:
+            printf("  Number of medium transport unrecovered errors: "
+                   "%" PRIu64 "\n", ull);
+            break;
+        case 0xb:
+            printf("  Number of medium transport recovered errors: "
+                   "%" PRIu64 "\n", ull);
+            break;
+        case 0xc:
+            printf("  Number of medium transport X axis translation "
+                   "unrecovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0xd:
+            printf("  Number of medium transport X axis translation "
+                   "recovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0xe:
+            printf("  Number of medium transport Y axis translation "
+                   "unrecovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0xf:
+            printf("  Number of medium transport Y axis translation "
+                   "recovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0x10:
+            printf("  Number of medium transport Z axis translation "
+                   "unrecovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0x11:
+            printf("  Number of medium transport Z axis translation "
+                   "recovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0x12:
+            printf("  Number of medium transport rotational translation "
+                   "unrecovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0x13:
+            printf("  Number of medium transport rotational translation "
+                   "recovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0x14:
+            printf("  Number of medium transport inversion translation "
+                   "unrecovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0x15:
+            printf("  Number of medium transport inversion translation "
+                   "recovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0x16:
+            printf("  Number of medium transport auxiliary translation "
+                   "unrecovered errors: %" PRIu64 "\n", ull);
+            break;
+        case 0x17:
+            printf("  Number of medium transport auxiliary translation "
+                   "recovered errors: %" PRIu64 "\n", ull);
+            break;
+        default:
+            printf("  Reserved parameter [0x%x] value: %" PRIu64 "\n",
+                   pc, ull);
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Element statistics page, 0x15 <els> for SMC */
+static bool
+show_element_stats_page(const uint8_t * resp, int len,
+                        struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    unsigned int v;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Element statistics page (smc-3) [0x15]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        printf("  Element address: %d\n", pc);
+        v = sg_get_unaligned_be32(bp + 4);
+        printf("    Number of places: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 8);
+        printf("    Number of place retries: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 12);
+        printf("    Number of picks: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 16);
+        printf("    Number of pick retries: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 20);
+        printf("    Number of determined volume identifiers: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 24);
+        printf("    Number of unreadable volume identifiers: %u\n", v);
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Tape diagnostic data [0x16] <tdd> for tape */
+static bool
+show_tape_diag_data_page(const uint8_t * resp, int len,
+                         struct opts_t * op, sgj_opaque_p jop)
+{
+    int k, n, num, pl, pc;
+    unsigned int v;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+    char b[512];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Tape diagnostics data page (ssc-3) [0x16]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        printf("  %s: %d\n", param_c, pc);
+        printf("    Density code: 0x%x\n", bp[6]);
+        printf("    Medium type: 0x%x\n", bp[7]);
+        v = sg_get_unaligned_be32(bp + 8);
+        printf("    Lifetime media motion hours: %u\n", v);
+        printf("    Repeat: %d\n", !!(bp[13] & 0x80));
+        v = bp[13] & 0xf;
+        printf("    Sense key: 0x%x [%s]\n", v,
+               sg_get_sense_key_str(v, sizeof(b), b));
+        printf("    Additional sense code: 0x%x\n", bp[14]);
+        printf("    Additional sense code qualifier: 0x%x\n", bp[15]);
+        if (bp[14] || bp[15])
+            printf("      [%s]\n", sg_get_asc_ascq_str(bp[14], bp[15],
+                   sizeof(b), b));
+        v = sg_get_unaligned_be32(bp + 16);
+        printf("    Vendor specific code qualifier: 0x%x\n", v);
+        v = sg_get_unaligned_be32(bp + 20);
+        printf("    Product revision level: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 24);
+        printf("    Hours since last clean: %u\n", v);
+        printf("    Operation code: 0x%x\n", bp[28]);
+        printf("    Service action: 0x%x\n", bp[29] & 0xf);
+        // Check Medium id number for all zeros
+        // ssc4r03.pdf does not define this field, why? xxxxxx
+        if (sg_all_zeros(bp + 32, 32))
+            printf("    Medium id number is 32 bytes of zero\n");
+        else {
+            hex2str(bp + 32, 32, "      ", 0 /* with ASCII */, sizeof(b), b);
+            printf("    Medium id number (in hex):\n%s", b);
+        }
+        printf("    Timestamp origin: 0x%x\n", bp[64] & 0xf);
+        // Check Timestamp for all zeros
+        if (sg_all_zeros(bp + 66, 6))
+            printf("    Timestamp is all zeros:\n");
+        else {
+            hex2str(bp + 66, 6, NULL, op->hex2str_oformat, sizeof(b), b);
+            printf("    Timestamp: %s", b);
+        }
+        if (pl > 72) {
+            n = pl - 72;
+            k = hex2str(bp + 72, n, "      ", op->hex2str_oformat,
+                        sizeof(b), b);
+            printf("    Vendor specific:\n");
+            printf("%s", b);
+            if (k >= (int)sizeof(b) - 1)
+                printf("      <truncated>\n");
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Media changer diagnostic data [0x16] <mcdd> for media changer */
+static bool
+show_mchanger_diag_data_page(const uint8_t * resp, int len,
+                             struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc;
+    unsigned int v;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+    char b[512];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Media changer diagnostics data page (smc-3) [0x16]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        printf("  %s: %d\n", param_c, pc);
+        printf("    Repeat: %d\n", !!(bp[5] & 0x80));
+        v = bp[5] & 0xf;
+        printf("    Sense key: 0x%x [%s]\n", v,
+               sg_get_sense_key_str(v, sizeof(b), b));
+        printf("    Additional sense code: 0x%x\n", bp[6]);
+        printf("    Additional sense code qualifier: 0x%x\n", bp[7]);
+        if (bp[6] || bp[7])
+            printf("      [%s]\n", sg_get_asc_ascq_str(bp[6], bp[7],
+                   sizeof(b), b));
+        v = sg_get_unaligned_be32(bp + 8);
+        printf("    Vendor specific code qualifier: 0x%x\n", v);
+        v = sg_get_unaligned_be32(bp + 12);
+        printf("    Product revision level: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 16);
+        printf("    Number of moves: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 20);
+        printf("    Number of pick: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 24);
+        printf("    Number of pick retries: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 28);
+        printf("    Number of places: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 32);
+        printf("    Number of place retries: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 36);
+        printf("    Number of determined volume identifiers: %u\n", v);
+        v = sg_get_unaligned_be32(bp + 40);
+        printf("    Number of unreadable volume identifiers: %u\n", v);
+        printf("    Operation code: 0x%x\n", bp[44]);
+        printf("    Service action: 0x%x\n", bp[45] & 0xf);
+        printf("    Media changer error type: 0x%x\n", bp[46]);
+        printf("    MTAV: %d\n", !!(bp[47] & 0x8));
+        printf("    IAV: %d\n", !!(bp[47] & 0x4));
+        printf("    LSAV: %d\n", !!(bp[47] & 0x2));
+        printf("    DAV: %d\n", !!(bp[47] & 0x1));
+        v = sg_get_unaligned_be16(bp + 48);
+        printf("    Medium transport address: 0x%x\n", v);
+        v = sg_get_unaligned_be16(bp + 50);
+        printf("    Initial address: 0x%x\n", v);
+        v = sg_get_unaligned_be16(bp + 52);
+        printf("    Last successful address: 0x%x\n", v);
+        v = sg_get_unaligned_be16(bp + 54);
+        printf("    Destination address: 0x%x\n", v);
+        if (pl > 91) {
+            printf("    Volume tag information:\n");
+            hex2fp(bp + 56, 36, "    ", op->hex2str_oformat, stdout);
+        }
+        if (pl > 99) {
+            printf("    Timestamp origin: 0x%x\n", bp[92] & 0xf);
+            printf("    Timestamp:\n");
+            hex2fp(bp + 94, 6, "    ", op->hex2str_oformat, stdout);
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* Helper for show_volume_stats_pages() */
+static void
+volume_stats_partition(const uint8_t * xp, int len, bool pr_in_hex)
+{
+    uint64_t ull;
+
+    while (len > 3) {
+        bool all_ffs, ffs_last_fe;
+        int dl, pn;
+
+        dl = xp[0] + 1;
+        if (dl < 3)
+            return;
+        pn = sg_get_unaligned_be16(xp + 2);
+        ffs_last_fe = false;
+        all_ffs = false;
+        if (sg_all_ffs(xp + 4, dl - 3)) {
+            switch (xp[4 + dl - 3]) {
+            case 0xff:
+                all_ffs = true;
+                break;
+            case 0xfe:
+                ffs_last_fe = true;
+                break;
+            default:
+                break;
+            }
+        }
+        if (! (all_ffs || ffs_last_fe)) {
+            ull = sg_get_unaligned_be(dl - 4, xp + 4);
+            if (pr_in_hex)
+                printf("    partition number: %d, partition record data "
+                       "counter: 0x%" PRIx64 "\n", pn, ull);
+            else
+                printf("    partition number: %d, partition record data "
+                       "counter: %" PRIu64 "\n", pn, ull);
+        } else if (all_ffs)
+            printf("    partition number: %d, partition record data "
+                   "counter is all 0xFFs\n", pn);
+        else    /* ffs_last_fe is true */
+            printf("    partition number: %d, partition record data "
+                   "counter is all 0xFFs apart\n    from a trailing "
+                   "0xFE\n", pn);
+        xp += dl;
+        len -= dl;
+    }
+}
+
+/* Helper for show_volume_stats_pages() */
+static void
+volume_stats_history(const uint8_t * xp, int len)
+{
+    while (len > 3) {
+        int dl, mhi;
+
+        dl = xp[0] + 1;
+        if (dl < 4)
+            return;
+        mhi = sg_get_unaligned_be16(xp + 2);
+        if (dl < 12)
+            printf("    index: %d\n", mhi);
+        else if (12 == dl)
+            printf("    index: %d, vendor: %.8s\n", mhi, xp + 4);
+        else
+            printf("    index: %d, vendor: %.8s, unit serial number: %.*s\n",
+                   mhi, xp + 4, dl - 12, xp + 12);
+        xp += dl;
+        len -= dl;
+    }
+}
+
+/* Volume Statistics log page and subpages (ssc-4) [0x17, 0x0-0xf] <vs> */
+static bool
+show_volume_stats_pages(const uint8_t * resp, int len,
+                       struct opts_t * op, sgj_opaque_p jop)
+{
+    bool skip_out = false;
+    bool evsm_output = false;
+    int num, pl, pc, subpg_code;
+    bool spf;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+    char b[512];
+
+if (jop) { };
+    spf = !!(resp[0] & 0x40);
+    subpg_code = spf ? resp[1] : NOT_SPG_SUBPG;
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (subpg_code < 0x10)
+            printf("Volume statistics page (ssc-4), subpage=%d\n",
+                   subpg_code);
+        else {
+            printf("Volume statistics page (ssc-4), subpage=%d; Reserved, "
+                   "skip\n", subpg_code);
+            return false;
+        }
+    }
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+
+        switch (pc) {
+        case 0:
+            printf("  Page valid: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 1:
+            printf("  Thread count: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 2:
+            printf("  Total data sets written: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 3:
+            printf("  Total write retries: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 4:
+            printf("  Total unrecovered write errors: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 5:
+            printf("  Total suspended writes: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 6:
+            printf("  Total fatal suspended writes: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 7:
+            printf("  Total data sets read: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 8:
+            printf("  Total read retries: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 9:
+            printf("  Total unrecovered read errors: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0xa:
+            printf("  Total suspended reads: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0xb:
+            printf("  Total fatal suspended reads: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0xc:
+            printf("  Last mount unrecovered write errors: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0xd:
+            printf("  Last mount unrecovered read errors: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0xe:
+            printf("  Last mount megabytes written: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0xf:
+            printf("  Last mount megabytes read: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x10:
+            printf("  Lifetime megabytes written: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x11:
+            printf("  Lifetime megabytes read: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x12:
+            printf("  Last load write compression ratio: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x13:
+            printf("  Last load read compression ratio: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x14:
+            printf("  Medium mount time: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x15:
+            printf("  Medium ready time: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x16:
+            printf("  Total native capacity [MB]: %s\n",
+                   num_or_unknown(bp + 4, pl - 4, false, b, sizeof(b)));
+            break;
+        case 0x17:
+            printf("  Total used native capacity [MB]: %s\n",
+                   num_or_unknown(bp + 4, pl - 4, false, b, sizeof(b)));
+            break;
+        case 0x1a:
+            printf("  Volume stop writes of forward wraps: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x1b:
+            printf("  Volume stop writes of backward wraps: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x40:
+            printf("  Volume serial number: %.*s\n", pl - 4, bp + 4);
+            break;
+        case 0x41:
+            printf("  Tape lot identifier: %.*s\n", pl - 4, bp + 4);
+            break;
+        case 0x42:
+            printf("  Volume barcode: %.*s\n", pl - 4, bp + 4);
+            break;
+        case 0x43:
+            printf("  Volume manufacturer: %.*s\n", pl - 4, bp + 4);
+            break;
+        case 0x44:
+            printf("  Volume license code: %.*s\n", pl - 4, bp + 4);
+            break;
+        case 0x45:
+            printf("  Volume personality: %.*s\n", pl - 4, bp + 4);
+            break;
+        case 0x80:
+            printf("  Write protect: %s\n",
+                   num_or_unknown(bp + 4, pl - 4, false, b, sizeof(b)));
+            break;
+        case 0x81:
+            printf("  WORM: %s\n",
+                   num_or_unknown(bp + 4, pl - 4, false, b, sizeof(b)));
+            break;
+        case 0x82:
+            printf("  Maximum recommended tape path temperature exceeded: "
+                   "%s\n", num_or_unknown(bp + 4, pl - 4, false, b,
+                                          sizeof(b)));
+            break;
+        case 0x100:
+            printf("  Volume write mounts: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x101:
+            printf("  Beginning of medium passes: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x102:
+            printf("  Middle of medium passes: %" PRIu64 "\n",
+                   sg_get_unaligned_be(pl - 4, bp + 4));
+            break;
+        case 0x200:
+            printf("  Logical position of first encrypted logical object:\n");
+            volume_stats_partition(bp + 4, pl - 4, true);
+            break;
+        case 0x201:
+            printf("  Logical position of first unencrypted logical object "
+                   "after first\n  encrypted logical object:\n");
+            volume_stats_partition(bp + 4, pl - 4, true);
+            break;
+        case 0x202:
+            printf("  Native capacity partition(s) [MB]:\n");
+            volume_stats_partition(bp + 4, pl - 4, false);
+            break;
+        case 0x203:
+            printf("  Used native capacity partition(s) [MB]:\n");
+            volume_stats_partition(bp + 4, pl - 4, false);
+            break;
+        case 0x204:
+            printf("  Remaining native capacity partition(s) [MB]:\n");
+            volume_stats_partition(bp + 4, pl - 4, false);
+            break;
+        case 0x300:
+            printf("  Mount history:\n");
+            volume_stats_history(bp + 4, pl - 4);
+            break;
+
+        default:
+            if (pc >= 0xf000) {
+                    if (op->exclude_vendor) {
+                    skip_out = true;
+                    if ((op->verbose > 0) && (0 == op->do_brief) &&
+                        (! evsm_output)) {
+                        evsm_output = true;
+                        printf("  Vendor specific parameter(s) being "
+                               "ignored\n");
+                    }
+                } else
+                    printf("  Vendor specific %s (0x%x), payload in hex\n",
+                           param_c, pc);
+            } else
+                printf("  Reserved %s (0x%x), payload in hex\n", param_c, pc);
+            if (skip_out)
+                skip_out = false;
+            else
+                hex2fp(bp + 4, pl - 4, "    ", op->hex2str_oformat, stdout);
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* TAPE_ALERT_LPAGE [0x2e] <ta> */
+static bool
+show_tape_alert_ssc_page(const uint8_t * resp, int len,
+                         struct opts_t * op, sgj_opaque_p jop)
+{
+    int num, pl, pc, flag;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    /* N.B. the Tape alert log page for smc-3 is different */
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Tape alert page (ssc-3) [0x2e]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        flag = bp[4] & 1;
+        if (op->verbose && (0 == op->do_brief) && flag)
+            printf("  >>>> ");
+        if ((0 == op->do_brief) || op->verbose || flag) {
+            if (NULL == sg_lib_tapealert_strs[0])
+                printf("  No string available for code 0x%x, flag: %d\n",
+                       pc, flag);
+            else if (pc <= 0x40)
+                printf("  %s: %d\n", sg_lib_tapealert_strs[pc], flag);
+            else
+                printf("  Reserved %s 0x%x, flag: %d\n", param_c, pc, flag);
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* 0x37 */
+static bool
+show_seagate_cache_page(const uint8_t * resp, int len,
+                        struct opts_t * op, sgj_opaque_p jop)
+{
+    bool skip = false;
+    int num, pl, pc;
+    int bsti = 0;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex))) {
+        if (resp[1] > 0) {
+            printf("Suspicious page 0x37, SPF=0 but subpage=0x%x\n", resp[1]);
+            if (op->verbose)
+                printf("... try vendor=wdc\n");
+            if (op->do_brief > 0)
+                return true;
+        } else
+            printf("Seagate cache page [0x37]\n");
+    }
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0:
+            ++bsti;
+            if (bsti < 2)
+                printf("  Blocks sent to initiator");
+            else
+                skip = true;
+            break;
+        case 1:
+            printf("  Blocks received from initiator");
+            break;
+        case 2:
+            printf("  Blocks read from cache and sent to initiator");
+            break;
+        case 3:
+            printf("  Number of read and write commands whose size "
+                   "<= segment size");
+            break;
+        case 4:
+            printf("  Number of read and write commands whose size "
+                   "> segment size");
+            break;
+        default:
+            printf("  Unknown Seagate %s = 0x%x", param_c, pc);
+            break;
+        }
+        if (skip)
+            skip = false;
+        else {
+            printf(" = %" PRIu64 "", sg_get_unaligned_be(pl - 4, bp + 4));
+            printf("\n");
+            if (op->do_pcb)
+                printf("        <%s>\n", get_pcb_str(bp[2], str,
+                       sizeof(str)));
+        }
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+/* 0x37 */
+static bool
+show_hgst_misc_page(const uint8_t * resp, int len, struct opts_t * op,
+                    sgj_opaque_p jop)
+{
+    bool valid = false;
+    int num, pl, pc;
+    const uint8_t * bp;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("HGST/WDC miscellaneous page [0x37, 0x%x]\n",
+               op->decod_subpg_code);
+    num = len - 4;
+    if (num < 0x30) {
+        printf("HGST/WDC miscellaneous page too short (%d) < 48\n", num);
+        return valid;
+    }
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        switch (pc) {
+        case 0:
+            valid = true;
+            printf("  Power on hours = %u\n", sg_get_unaligned_be32(bp + 4));
+            printf("  Total Bytes Read = %" PRIu64 "\n",
+                   sg_get_unaligned_be64(bp + 8));
+            printf("  Total Bytes Written = %" PRIu64 "\n",
+                   sg_get_unaligned_be64(bp + 16));
+            printf("  Max Drive Temp (Celsius) = %u\n", bp[24]);
+            printf("  GList Size = %u\n", sg_get_unaligned_be16(bp + 25));
+            printf("  Number of Information Exceptions = %u\n", bp[27]);
+            printf("  MED EXC = %u\n", !! (0x80 & bp[28]));
+            printf("  HDW EXC = %u\n", !! (0x40 & bp[28]));
+            printf("  Total Read Commands = %" PRIu64 "\n",
+                   sg_get_unaligned_be64(bp + 29));
+            printf("  Total Write Commands = %" PRIu64 "\n",
+                   sg_get_unaligned_be64(bp + 37));
+            printf("  Flash Correction Count = %u\n",
+                   sg_get_unaligned_be16(bp + 46));
+            break;
+        default:
+            valid = false;
+            printf("  Unknown HGST/WDC %s = 0x%x", param_c, pc);
+            break;
+        }
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return valid;
+}
+
+/* 0x3e */
+static bool
+show_seagate_factory_page(const uint8_t * resp, int len,
+                          struct opts_t * op, sgj_opaque_p jop)
+{
+    bool valid = false;
+    int num, pl, pc;
+    const uint8_t * bp;
+    uint64_t ull;
+    char str[PCB_STR_LEN];
+
+if (jop) { };
+    if (op->verbose || ((! op->do_raw) && (0 == op->do_hex)))
+        printf("Seagate/Hitachi factory page [0x3e]\n");
+    num = len - 4;
+    bp = &resp[0] + 4;
+    while (num > 3) {
+        pc = sg_get_unaligned_be16(bp + 0);
+        pl = bp[3] + 4;
+        if (op->filter_given) {
+            if (pc != op->filter)
+                goto skip;
+        }
+        if (op->do_raw) {
+            dStrRaw(bp, pl);
+            goto filter_chk;
+        } else if (op->do_hex) {
+            hex2stdout(bp, pl, op->dstrhex_no_ascii);
+            goto filter_chk;
+        }
+        valid = true;
+        switch (pc) {
+        case 0:
+            printf("  number of hours powered up");
+            break;
+        case 8:
+            printf("  number of minutes until next internal SMART test");
+            break;
+        default:
+            valid = false;
+            printf("  Unknown Seagate/Hitachi %s = 0x%x", param_c, pc);
+            break;
+        }
+        if (valid) {
+            ull = sg_get_unaligned_be(pl - 4, bp + 4);
+            if (0 == pc)
+                printf(" = %.2f", ((double)ull) / 60.0 );
+            else
+                printf(" = %" PRIu64 "", ull);
+        }
+        printf("\n");
+        if (op->do_pcb)
+            printf("        <%s>\n", get_pcb_str(bp[2], str, sizeof(str)));
+filter_chk:
+        if (op->filter_given)
+            break;
+skip:
+        num -= pl;
+        bp += pl;
+    }
+    return true;
+}
+
+static void
+decode_page_contents(const uint8_t * resp, int len, struct opts_t * op,
+                     sgj_opaque_p jop)
+{
+    int pg_code, subpg_code, vpn;
+    bool spf;
+    bool done = false;
+    const struct log_elem * lep;
+
+    if (len < 3) {
+        pr2serr("%s: response has bad length: %d\n", __func__, len);
+        return;
+    }
+    spf = !!(resp[0] & 0x40);
+    pg_code = resp[0] & 0x3f;
+    if ((VP_HITA == op->vend_prod_num) && (pg_code >= 0x30))
+        subpg_code = resp[1];   /* Hitachi don't set SPF on VS pages */
+    else
+        subpg_code = spf ? resp[1] : NOT_SPG_SUBPG;
+    op->decod_subpg_code = subpg_code;
+    if ((SUPP_SPGS_SUBPG == subpg_code) && (SUPP_PAGES_LPAGE != pg_code)) {
+        done = show_supported_pgs_sub_page(resp, len, op, jop);
+        if (done)
+            return;
+    }
+    vpn = (op->vend_prod_num >= 0) ? op->vend_prod_num : op->deduced_vpn;
+    lep = pg_subpg_pdt_search(pg_code, subpg_code, op->dev_pdt, vpn);
+
+    /* Below is the indirect function call to all the show_* functions */
+    if (lep && lep->show_pagep)
+        done = (*lep->show_pagep)(resp, len, op, jop);
+
+    if (! done) {
+        if (0 == op->do_hex) {
+            static const char * unable_s = "Unable to decode page = 0x";
+
+            if (subpg_code > 0)
+                printf("%s%x, subpage = 0x%x, here is hex:\n", unable_s,
+                       pg_code, subpg_code);
+            else
+                printf("%s%x, here is hex:\n", unable_s, pg_code);
+        }
+        if ((len > 128) && (0 == op->do_hex)) {
+            hex2fp(resp, 64, "  ", op->hex2str_oformat, stdout);
+            printf("  .....  [truncated after 64 of %d bytes (use '-H' to "
+                   "see the rest)]\n", len);
+        } else {
+            if (0 == op->do_hex)
+                hex2fp(resp, len, "  ", op->hex2str_oformat, stdout);
+            else
+                hex2stdout(resp, len, op->dstrhex_no_ascii);
+        }
+    }
+}
+
+/* Tries to fetch the TEMPERATURE_LPAGE [0xd] page first. If that fails
+ * tries to get the Informational Exceptions (IE_LPAGE) page. */
+static int
+fetchTemperature(int sg_fd, uint8_t * resp, int max_len, struct opts_t * op,
+                 sgj_opaque_p jop)
+{
+    int len;
+    int res = 0;
+
+    op->pg_code = TEMPERATURE_LPAGE;
+    op->subpg_code = NOT_SPG_SUBPG;
+    res = do_logs(sg_fd, resp, max_len, op);
+    if (0 == res) {
+        len = sg_get_unaligned_be16(resp + 2) + 4;
+        if (op->do_raw)
+            dStrRaw(resp, len);
+        else if (op->do_hex)
+            hex2stdout(resp, len, op->dstrhex_no_ascii);
+        else
+            show_temperature_page(resp, len, op, jop);
+    } else if (SG_LIB_CAT_NOT_READY == res)
+        pr2serr("Device not ready\n");
+    else {
+        op->pg_code = IE_LPAGE;
+        res = do_logs(sg_fd, resp, max_len, op);
+        if (0 == res) {
+            len = sg_get_unaligned_be16(resp + 2) + 4;
+            if (op->do_raw)
+                dStrRaw(resp, len);
+            else if (op->do_hex)
+                hex2stdout(resp, len, op->dstrhex_no_ascii);
+            else
+                show_ie_page(resp, len, op, jop);
+        } else
+            pr2serr("Unable to find temperature in either Temperature or "
+                    "IE log page\n");
+    }
+    sg_cmds_close_device(sg_fd);
+    return (res >= 0) ? res : SG_LIB_CAT_OTHER;
+}
+
+/* Returns 0 if successful else SG_LIB_SYNTAX_ERROR. */
+static int
+decode_pg_arg(struct opts_t * op)
+{
+    int nn;
+    const struct log_elem * lep;
+    char * cp;
+
+    if (isalpha((uint8_t)op->pg_arg[0])) {
+        char b[80];
+
+        if (strlen(op->pg_arg) >= (sizeof(b) - 1)) {
+            pr2serr("argument to '--page=' is too long\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        strcpy(b, op->pg_arg);
+        cp = (char *)strchr(b, ',');
+        if (cp)
+            *cp = '\0';
+        lep = acron_search(b);
+        if (NULL == lep) {
+            pr2serr("bad argument to '--page=' no acronyn match to "
+                    "'%s'\n", b);
+            pr2serr("  Try using '-e' or'-ee' to see available "
+                    "acronyns\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        op->lep = lep;
+        op->pg_code = lep->pg_code;
+        if (cp) {
+            nn = sg_get_num_nomult(cp + 1);
+            if ((nn < 0) || (nn > 255)) {
+                pr2serr("Bad second value in argument to "
+                        "'--page='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->subpg_code = nn;
+        } else
+            op->subpg_code = lep->subpg_code;
+    } else { /* numeric arg: either 'pg_num' or 'pg_num,subpg_num' */
+        int n;
+
+        cp = (char *)strchr(op->pg_arg, ',');
+        n = sg_get_num_nomult(op->pg_arg);
+        if ((n < 0) || (n > 63)) {
+            pr2serr("Bad argument to '--page='\n");
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if (cp) {
+            nn = sg_get_num_nomult(cp + 1);
+            if ((nn < 0) || (nn > 255)) {
+                pr2serr("Bad second value in argument to "
+                        "'--page='\n");
+                usage(1);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else
+            nn = 0;
+        op->pg_code = n;
+        op->subpg_code = nn;
+    }
+    return 0;
+}
+
+/* Since the Supported subpages page is sitting in the rsp_buff which is
+ * MX_ALLOC_LEN bytes long (~ 64 KB) then move it (from rsp_buff+0 to
+ * rsp_buff+pg_len-1) to the top end of that buffer. Then there is room
+ * to merge supp_pgs_rsp with the supported subpages with the result back
+ * at the bottom of rsp_buff. The new length of the merged subpages page
+ * (excluding its 4 byte header) is returned.
+ * Assumes both pages are in ascending order (as required by SPC-4). */
+static int
+merge_both_supported(const uint8_t * supp_pgs_p, int su_p_pg_len, int pg_len)
+{
+    uint8_t pg;
+    int k, kp, ks;
+    int max_blen = (2 * su_p_pg_len) + pg_len;
+    uint8_t * m_buff = rsp_buff + (rsp_buff_sz - pg_len);
+    uint8_t * r_buff = rsp_buff + 4;
+
+    if (pg_len > 0)
+        memmove(m_buff, rsp_buff + 4, pg_len);
+    for (k = 0, kp = 0, ks = 0; k < max_blen; k += 2) {
+        if (kp < su_p_pg_len)
+            pg = supp_pgs_p[kp];
+        else
+            pg = 0xff;
+        if (ks < pg_len) {
+            if (m_buff[ks] < pg) {
+                r_buff[k] = m_buff[ks];
+                r_buff[k + 1] = m_buff[ks + 1];
+                ks += 2;
+            } else if ((m_buff[ks] == pg) && (m_buff[ks + 1] == 0)) {
+                r_buff[k] = m_buff[ks];
+                r_buff[k + 1] = m_buff[ks + 1];
+                ks += 2;
+                ++kp;
+            } else {
+                r_buff[k] = pg;
+                r_buff[k + 1] = 0;
+                ++kp;
+            }
+        } else {
+            if (0xff == pg)
+                break;
+            r_buff[k] = pg;
+            r_buff[k + 1] = 0;
+            ++kp;
+        }
+    }
+    sg_put_unaligned_be16(k, rsp_buff + 2);
+    return k;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool as_json;
+    int k, nn, pg_len, res, vb;
+    int resp_len = 0;
+    int su_p_pg_len = 0;
+    int in_len = -1;
+    int sg_fd = -1;
+    int ret = 0;
+    uint8_t * parr;
+    uint8_t * free_parr = NULL;
+    struct opts_t * op;
+    sgj_state * jsp;
+    sgj_opaque_p jop = NULL;
+    struct sg_simple_inquiry_resp inq_out;
+    struct opts_t opts SG_C_CPP_ZERO_INIT;
+    uint8_t supp_pgs_rsp[256];
+    char b[128];
+    static const int blen = sizeof(b);
+
+    op = &opts;
+    /* N.B. some disks only give data for current cumulative */
+    op->page_control = 1;
+    op->dev_pdt = -1;
+    op->vend_prod_num = VP_NONE;
+    op->deduced_vpn = VP_NONE;
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return SG_LIB_SYNTAX_ERROR;
+    if (op->do_help) {
+        usage_for(op->do_help, op);
+        return 0;
+    }
+    jsp = &op->json_st;
+    as_json = jsp->pr_as_json;
+    if (as_json) {
+        if (op->do_name) {
+            pr2serr(">>> The --json option is superior to the --name "
+                    "option.\n");
+            pr2serr(">>> Ignoring the --name option.\n");
+            op->do_name = false;
+        }
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+    if (op->do_hex > 0) {
+        if (op->do_hex > 2) {
+            op->dstrhex_no_ascii = -1;
+            op->hex2str_oformat = 1;
+        } else {
+            op->dstrhex_no_ascii = (1 == op->do_hex);
+            op->hex2str_oformat = (1 == op->do_hex);
+        }
+    } else {
+        if (op->undefined_hex > 0) {
+            if (op->undefined_hex > 2) {
+                op->dstrhex_no_ascii = -1;
+                op->hex2str_oformat = 1;
+            } else {
+                op->dstrhex_no_ascii = (1 == op->undefined_hex);
+                op->hex2str_oformat = (1 == op->undefined_hex);
+            }
+        } else {       /* default when no --hex nor --undefined */
+            op->dstrhex_no_ascii = -1;
+            op->hex2str_oformat = 1;
+        }
+    }
+    vb = op->verbose;
+    if (op->vend_prod) {
+        if (0 == memcmp("-1", op->vend_prod,3))
+            k = VP_NONE;
+        else if (isdigit((uint8_t)op->vend_prod[0]))
+            k = sg_get_num_nomult(op->vend_prod);
+        else
+            k = find_vpn_by_acron(op->vend_prod);
+        op->vend_prod_num = k;
+        if (VP_ALL == k)
+            ;
+        else if ((k < 0) || (k > (32 - MVP_OFFSET))) {
+            pr2serr("Bad vendor/product acronym after '--vendor=' "
+                    " ('-M ') option\n");
+            enumerate_vp();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (op->do_enumerate > 0) {
+        if (op->device_name && vb)
+            pr2serr("Warning: device: %s is being ignored\n",
+                    op->device_name);
+        enumerate_pages(op);
+        return 0;
+    }
+    if (op->in_fn) {
+        if (op->maxlen_given) {
+            if (op->maxlen > MX_INLEN_ALLOC_LEN) {
+                pr2serr("bad argument to '--maxlen=' when --in= given, from "
+                        "2 to %d (inclusive) expected\n", MX_INLEN_ALLOC_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            rsp_buff_sz = op->maxlen;
+        } else
+            rsp_buff_sz = DEF_INLEN_ALLOC_LEN;
+    } else {
+        if (op->maxlen_given) {
+            if (op->maxlen > MX_ALLOC_LEN) {
+                pr2serr("bad argument to '--maxlen=', from 2 to 65535 "
+                        "(inclusive) expected\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            rsp_buff_sz = op->maxlen;
+        }
+    }
+    rsp_buff = sg_memalign(rsp_buff_sz, 0 /* page aligned */, &free_rsp_buff,
+                           false);
+    if (NULL == rsp_buff) {
+        pr2serr("Unable to allocate %d bytes on the heap\n", rsp_buff_sz);
+        ret = sg_convert_errno(ENOMEM);
+        goto err_out;
+    }
+    if (NULL == op->device_name) {
+        if (op->in_fn) {
+            bool found = false;
+            bool r_spf = false;
+            uint16_t u;
+            int pg_code, subpg_code, pdt, n;
+            const struct log_elem * lep;
+            const uint8_t * bp;
+
+            if ((ret = sg_f2hex_arr(op->in_fn, op->do_raw, false, rsp_buff,
+                                    &in_len, rsp_buff_sz)))
+                goto err_out;
+            if (vb > 2)
+                pr2serr("Read %d [0x%x] bytes of user supplied data\n",
+                        in_len, in_len);
+            if (op->do_raw)
+                op->do_raw = false;    /* can interfere on decode */
+            if (in_len < 4) {
+                pr2serr("--in=%s only decoded %d bytes (needs 4 at least)\n",
+                        op->in_fn, in_len);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto err_out;
+            }
+            if (op->pg_arg) {
+                char b[144];
+                char * cp;
+
+                strcpy(b, op->pg_arg);
+                cp = (char *)strchr(b, ',');
+                if (cp)
+                    *cp = '\0';
+                lep = acron_search(b);
+                if (NULL == lep) {
+                    pr2serr("bad argument to '--page=' no acronyn match to "
+                            "'%s'\n", b);
+                    pr2serr("  Try using '-e' or'-ee' to see available "
+                            "acronyns\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->lep = lep;
+                op->pg_code = lep->pg_code;
+                op->subpg_code = lep->subpg_code;
+                if (op->subpg_code > 0)
+                    r_spf = true;
+            }
+
+            for (bp = rsp_buff, k = 0; k < in_len; bp += n, k += n) {
+                bool spf = !! (bp[0] & 0x40);
+
+                pg_code = bp[0] & 0x3f;
+                subpg_code = spf ? bp[1] : NOT_SPG_SUBPG;
+                u = sg_get_unaligned_be16(bp + 2);
+                n = u + 4;
+                if (n > (in_len - k)) {
+                    pr2serr("bytes decoded remaining (%d) less than lpage "
+                            "length (%d), try decoding anyway\n", in_len - k,
+                            n);
+                    n = in_len - k;
+                }
+                if (op->pg_arg) {
+                    if ((NOT_SPG_SUBPG == op->subpg_code) && spf) {
+                        continue;
+                    } else if ((! spf) && (! r_spf)) {
+                        if (pg_code != op->pg_code)
+                            continue;
+                    } else if ((SUPP_SPGS_SUBPG == op->subpg_code) &&
+                             (SUPP_PAGES_LPAGE != op->pg_code)) {
+                        if (pg_code != op->pg_code)
+                            continue;
+                    } else if ((SUPP_SPGS_SUBPG != op->subpg_code) &&
+                               (SUPP_PAGES_LPAGE == op->pg_code)) {
+                        if (subpg_code != op->subpg_code)
+                            continue;
+                    } else if ((SUPP_SPGS_SUBPG != op->subpg_code) &&
+                               (SUPP_PAGES_LPAGE != op->pg_code)) {
+                        if ((pg_code != op->pg_code) ||
+                            (subpg_code != op->subpg_code))
+                            continue;
+                    }
+                }
+                if (op->exclude_vendor && (pg_code >= 0x30))
+                    continue;
+                found = true;
+                if (op->do_hex > 2) {
+                     hex2fp(bp, n, NULL, op->hex2str_oformat, stdout);
+                     continue;
+                }
+                pdt = op->dev_pdt;
+                lep = pg_subpg_pdt_search(pg_code, subpg_code, pdt,
+                                          op->vend_prod_num);
+                if (lep) {
+                    /* Below is the indirect function call to all the
+                     * show_* functions */
+                    if (lep->show_pagep)
+                        (*lep->show_pagep)(bp, n, op, jop);
+                    else
+                        sgj_pr_hr(jsp, "Unable to decode %s [%s]\n",
+                                  lep->name, lep->acron);
+                } else {
+                    nn = sg_scnpr(b, blen, "Unable to decode page=0x%x",
+                                  pg_code);
+                    if (subpg_code > 0)
+                        sg_scnpr(b + nn, blen - nn, ", subpage=0x%x",
+                                 subpg_code);
+                    if (pdt >= 0)
+                        sg_scnpr(b + nn, blen - nn, ", pdt=0x%x\n", pdt);
+                    sgj_pr_hr(jsp, "%s\n", b);
+                }
+            }           /* end of page/subpage search loop */
+            if (op->pg_arg && (! found)) {
+                nn = sg_scnpr(b, blen, "Unable to find page=0x%x",
+                              op->pg_code);
+                if (op->subpg_code > 0)
+                    sg_scnpr(b + nn, blen - nn, ", subpage=0x%x",
+                             op->subpg_code);
+                sgj_pr_hr(jsp, "%s\n", b);
+                if (jsp->pr_as_json)
+                    sgj_js_nv_i(jsp, jop, "page_not_found", 1);
+            }
+            ret = 0;
+            goto err_out;
+        }
+        if (op->pg_arg) {         /* do this for 'sg_logs -p xxx' */
+            ret = decode_pg_arg(op);
+            if (ret)
+                goto err_out;
+        }
+        pr2serr("No DEVICE argument given\n\n");
+        usage_for(1, op);
+        ret = SG_LIB_FILE_ERROR;
+        goto err_out;
+    }
+    if (op->do_select) {
+        if (op->do_temperature) {
+            pr2serr("--select cannot be used with --temperature\n");
+            ret = SG_LIB_CONTRADICT;
+            goto err_out;
+        }
+        if (op->do_transport) {
+            pr2serr("--select cannot be used with --transport\n");
+            ret = SG_LIB_CONTRADICT;
+            goto err_out;
+        }
+    } else if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto err_out;
+        }
+    }
+    if (op->do_all) {
+        if (op->do_select) {
+            pr2serr("--all conflicts with --select\n");
+            ret = SG_LIB_CONTRADICT;
+            goto err_out;
+        }
+    }
+    if (op->in_fn) {
+        if (! op->do_select) {
+            pr2serr("--in=FN can only be used with --select when DEVICE "
+                    "given\n");
+            ret = SG_LIB_CONTRADICT;
+            goto err_out;
+        }
+        if ((ret = sg_f2hex_arr(op->in_fn, op->do_raw, false, rsp_buff,
+                                &in_len, rsp_buff_sz)))
+            goto err_out;
+        if (vb > 2)
+            pr2serr("Read %d [0x%x] bytes of user supplied data\n", in_len,
+                    in_len);
+    }
+    if (op->pg_arg) {
+        if (op->do_all) {
+            if (0 == op->do_brief)
+                pr2serr(">>> warning: --page=%s ignored when --all given\n",
+                        op->pg_arg);
+        } else {
+            ret = decode_pg_arg(op);
+            if (ret)
+                goto err_out;
+        }
+    }
+
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+    win32_spt_init_state = !! scsi_pt_win32_spt_state();
+    if (vb > 4)
+        pr2serr("Initial win32 SPT interface state: %s\n",
+                win32_spt_init_state ? "direct" : "indirect");
+#endif
+#endif
+    sg_fd = sg_cmds_open_device(op->device_name, op->o_readonly, vb);
+    if ((sg_fd < 0) && (! op->o_readonly))
+        sg_fd = sg_cmds_open_device(op->device_name, true /* ro */, vb);
+    if (sg_fd < 0) {
+        pr2serr("error opening file: %s: %s \n", op->device_name,
+                safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+    if (op->do_list || op->do_all) {
+        op->pg_code = SUPP_PAGES_LPAGE;
+        if ((op->do_list > 1) || (op->do_all > 1))
+            op->subpg_code = SUPP_SPGS_SUBPG;
+    }
+    if (op->do_transport) {
+        if ((op->pg_code > 0) || (op->subpg_code > 0) ||
+            op->do_temperature) {
+            pr2serr("'-T' should not be mixed with options implying other "
+                    "pages\n");
+            ret = SG_LIB_FILE_ERROR;
+            goto err_out;
+        }
+        op->pg_code = PROTO_SPECIFIC_LPAGE;
+    }
+
+    memset(&inq_out, 0, sizeof(inq_out));
+    if (op->no_inq < 2) {
+        if (sg_simple_inquiry(sg_fd, &inq_out, true, vb)) {
+            pr2serr("%s doesn't respond to a SCSI INQUIRY\n",
+                    op->device_name);
+            ret = SG_LIB_CAT_OTHER;
+            goto err_out;
+        }
+        op->dev_pdt = inq_out.peripheral_type;
+        if ((! op->do_raw) && (0 == op->do_hex) && (! op->do_name) &&
+            (0 == op->no_inq) && (0 == op->do_brief))
+            sgj_pr_hr(jsp, "    %.8s  %.16s  %.4s\n", inq_out.vendor,
+                      inq_out.product, inq_out.revision);
+        memcpy(t10_vendor_str, inq_out.vendor, 8);
+        memcpy(t10_product_str, inq_out.product, 16);
+        if (VP_NONE == op->vend_prod_num)
+            op->deduced_vpn = find_vpn_by_inquiry();
+    }
+
+    if (op->do_temperature) {
+        ret = fetchTemperature(sg_fd, rsp_buff, SHORT_RESP_LEN, op, jop);
+        goto err_out;
+    }
+    if (op->do_select) {
+        k = sg_ll_log_select(sg_fd, op->do_pcreset, op->do_sp,
+                             op->page_control, op->pg_code, op->subpg_code,
+                             rsp_buff, ((in_len > 0) ? in_len : 0), true, vb);
+        if (k) {
+            if (SG_LIB_CAT_NOT_READY == k)
+                pr2serr("log_select: device not ready\n");
+            else if (SG_LIB_CAT_ILLEGAL_REQ == k)
+                pr2serr("log_select: field in cdb illegal\n");
+            else if (SG_LIB_CAT_INVALID_OP == k)
+                pr2serr("log_select: not supported\n");
+            else if (SG_LIB_CAT_UNIT_ATTENTION == k)
+                pr2serr("log_select: unit attention\n");
+            else if (SG_LIB_CAT_ABORTED_COMMAND == k)
+                pr2serr("log_select: aborted command\n");
+            else
+                pr2serr("log_select: failed (%d), try '-v' for more "
+                        "information\n", k);
+        }
+        ret = (k >= 0) ?  k : SG_LIB_CAT_OTHER;
+        goto err_out;
+    }
+    if (op->do_list > 2) {
+        const int supp_pgs_blen = sizeof(supp_pgs_rsp);
+
+        op->subpg_code = NOT_SPG_SUBPG;
+        res = do_logs(sg_fd, supp_pgs_rsp, supp_pgs_blen, op);
+        if (res != 0)
+            goto bad;
+        su_p_pg_len = sg_get_unaligned_be16(supp_pgs_rsp + 2);
+        if ((su_p_pg_len + 4) > supp_pgs_blen) {
+            pr2serr("Supported log pages log page is too long [%d], exit\n",
+                    su_p_pg_len);
+            res = SG_LIB_CAT_OTHER;
+            goto bad;
+        }
+        op->subpg_code = SUPP_SPGS_SUBPG;
+    }
+    resp_len = (op->maxlen > 0) ? op->maxlen : MX_ALLOC_LEN;
+    res = do_logs(sg_fd, rsp_buff, resp_len, op);
+    if (0 == res) {
+        pg_len = sg_get_unaligned_be16(rsp_buff + 2);
+        if ((pg_len + 4) > resp_len) {
+            pr2serr("Only fetched %d bytes of response (available: %d "
+                    "bytes)\n    truncate output\n",
+                   resp_len, pg_len + 4);
+            pg_len = resp_len - 4;
+        }
+        goto good;
+    }
+bad:
+    if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("%snot supported\n", ls_s);
+    else if (SG_LIB_CAT_NOT_READY == res)
+        pr2serr("%sdevice not ready\n", ls_s);
+    else if (SG_LIB_CAT_ILLEGAL_REQ == res) {
+        if ((op->do_list > 2) && (SUPP_SPGS_SUBPG == op->subpg_code)) {
+            rsp_buff[0] = 0x40;
+            rsp_buff[1] = SUPP_SPGS_SUBPG;
+            pg_len = 0;
+            res = 0;
+            if (op->verbose)
+                pr2serr("%sfield in cdb illegal in [0,0xff], "
+                        "continue with merge\n", ls_s);
+            goto good;
+        } else
+            pr2serr("%sfield in cdb illegal\n", ls_s);
+    } else if (SG_LIB_CAT_UNIT_ATTENTION == res)
+        pr2serr("%sunit attention\n", ls_s);
+    else if (SG_LIB_CAT_ABORTED_COMMAND == res)
+        pr2serr("%saborted command\n", ls_s);
+    else if (SG_LIB_TRANSPORT_ERROR == res)
+        pr2serr("%stransport error\n", ls_s);
+    else
+        pr2serr("%sother error [%d]\n", ls_s, res);
+    ret = res;
+    goto err_out;
+
+good:
+    if (op->do_list > 2)
+        pg_len = merge_both_supported(supp_pgs_rsp + 4, su_p_pg_len, pg_len);
+
+    if (0 == op->do_all) {
+        if (op->filter_given) {
+            if (op->do_hex > 2)
+                hex2stdout(rsp_buff, pg_len + 4, op->dstrhex_no_ascii);
+            else
+                decode_page_contents(rsp_buff, pg_len + 4, op, jop);
+        } else if (op->do_raw)
+            dStrRaw(rsp_buff, pg_len + 4);
+        else if (op->do_hex > 1)
+            hex2stdout(rsp_buff, pg_len + 4, op->dstrhex_no_ascii);
+        else if (pg_len > 1) {
+            if (op->do_hex) {
+                if (rsp_buff[0] & 0x40)
+                    printf("Log page code=0x%x,0x%x, DS=%d, SPF=1, "
+                           "page_len=0x%x\n", rsp_buff[0] & 0x3f, rsp_buff[1],
+                           !!(rsp_buff[0] & 0x80), pg_len);
+                else
+                    printf("Log page code=0x%x, DS=%d, SPF=0, page_len=0x%x\n",
+                           rsp_buff[0] & 0x3f, !!(rsp_buff[0] & 0x80), pg_len);
+                hex2stdout(rsp_buff, pg_len + 4, op->dstrhex_no_ascii);
+            }
+            else
+                decode_page_contents(rsp_buff, pg_len + 4, op, jop);
+        }
+    }
+    ret = res;
+
+    if (op->do_all && (pg_len > 1)) {
+        int my_len = pg_len;
+        bool spf;
+
+        parr = sg_memalign(parr_sz, 0, &free_parr, false);
+        if (NULL == parr) {
+            pr2serr("Unable to allocate heap for parr\n");
+            ret = sg_convert_errno(ENOMEM);
+            goto err_out;
+        }
+        spf = !!(rsp_buff[0] & 0x40);
+        if (my_len > parr_sz) {
+            pr2serr("Unexpectedly large page_len=%d, trim to %d\n", my_len,
+                    parr_sz);
+            my_len = parr_sz;
+        }
+        memcpy(parr, rsp_buff + 4, my_len);
+        for (k = 0; k < my_len; ++k) {
+            op->pg_code = parr[k] & 0x3f;
+            if (spf)
+                op->subpg_code = parr[++k];
+            else
+                op->subpg_code = NOT_SPG_SUBPG;
+
+            /* Some devices include [pg_code, 0xff] for all pg_code > 0 */
+            if ((op->pg_code > 0) && (SUPP_SPGS_SUBPG == op->subpg_code))
+                continue;       /* skip since no new information */
+            if ((op->pg_code >= 0x30) && op->exclude_vendor)
+                continue;
+            if (! op->do_raw)
+                sgj_pr_hr(jsp, "\n");
+            res = do_logs(sg_fd, rsp_buff, resp_len, op);
+            if (0 == res) {
+                pg_len = sg_get_unaligned_be16(rsp_buff + 2);
+                if ((pg_len + 4) > resp_len) {
+                    pr2serr("Only fetched %d bytes of response, truncate "
+                            "output\n", resp_len);
+                    pg_len = resp_len - 4;
+                }
+                if (op->do_raw && (! op->filter_given))
+                    dStrRaw(rsp_buff, pg_len + 4);
+                else if (op->do_hex > 4)
+                    decode_page_contents(rsp_buff, pg_len + 4, op, jop);
+                else if (op->do_hex > 1)
+                    hex2stdout(rsp_buff, pg_len + 4, op->dstrhex_no_ascii);
+                else if (1 == op->do_hex) {
+                    if (0 == op->do_brief) {
+                        if (rsp_buff[0] & 0x40)
+                            printf("Log page code=0x%x,0x%x, DS=%d, SPF=1, "
+                                   "page_len=0x%x\n", rsp_buff[0] & 0x3f,
+                                   rsp_buff[1], !!(rsp_buff[0] & 0x80),
+                                   pg_len);
+                        else
+                            printf("Log page code=0x%x, DS=%d, SPF=0, "
+                                   "page_len=0x%x\n", rsp_buff[0] & 0x3f,
+                                   !!(rsp_buff[0] & 0x80), pg_len);
+                    }
+                    hex2stdout(rsp_buff, pg_len + 4, op->dstrhex_no_ascii);
+                }
+                else
+                    decode_page_contents(rsp_buff, pg_len + 4, op, jop);
+            } else if (SG_LIB_CAT_INVALID_OP == res)
+                pr2serr("%spage=0x%x,0x%x not supported\n", ls_s,
+                        op->pg_code, op->subpg_code);
+            else if (SG_LIB_CAT_NOT_READY == res)
+                pr2serr("%sdevice not ready\n", ls_s);
+            else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+                pr2serr("%sfield in cdb illegal [page=0x%x,0x%x]\n", ls_s,
+                        op->pg_code, op->subpg_code);
+            else if (SG_LIB_CAT_UNIT_ATTENTION == res)
+                pr2serr("%sunit attention\n", ls_s);
+            else if (SG_LIB_CAT_ABORTED_COMMAND == res)
+                pr2serr("%saborted command\n", ls_s);
+            else
+                pr2serr("%sfailed, try '-v' for more information\n", ls_s);
+        }
+    }
+err_out:
+    if (free_rsp_buff)
+        free(free_rsp_buff);
+    if (free_parr)
+        free(free_parr);
+    if (sg_fd >= 0)
+        sg_cmds_close_device(sg_fd);
+    if (0 == vb) {
+        if (! sg_if_can2stderr("sg_logs failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    if (as_json) {
+        if (0 == op->do_hex)
+            sgj_js2file(jsp, NULL, ret, stdout);
+        sgj_finish(jsp);
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_luns.c b/src/sg_luns.c
new file mode 100644
index 0000000..15d367c
--- /dev/null
+++ b/src/sg_luns.c
@@ -0,0 +1,722 @@
+/*
+ * Copyright (c) 2004-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI REPORT LUNS command to the given SCSI device
+ * and decodes the response.
+ */
+
+static const char * version_str = "1.48 20210804";      /* spc6r05 */
+
+#define MAX_RLUNS_BUFF_LEN (1024 * 1024)
+#define DEF_RLUNS_BUFF_LEN (1024 * 8)
+
+
+static struct option long_options[] = {
+        {"decode", no_argument, 0, 'd'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+#ifdef SG_LIB_LINUX
+        {"linux", no_argument, 0, 'l'},
+#endif
+        {"lu_cong", no_argument, 0, 'L'},
+        {"lu-cong", no_argument, 0, 'L'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"quiet", no_argument, 0, 'q'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"select", required_argument, 0, 's'},
+        {"test", required_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+#ifdef SG_LIB_LINUX
+    pr2serr("Usage: sg_luns    [--decode] [--help] [--hex] [--linux] "
+            "[--lu_cong]\n"
+            "                  [--maxlen=LEN] [--quiet] [--raw] "
+            "[--readonly]\n"
+            "                  [--select=SR] [--verbose] [--version] "
+            "DEVICE\n");
+#else
+    pr2serr("Usage: sg_luns    [--decode] [--help] [--hex] [--lu_cong] "
+            "[--maxlen=LEN]\n"
+            "                  [--quiet] [--raw] [--readonly] "
+            "[--select=SR]\n"
+            "                  [--verbose] [--version] DEVICE\n");
+#endif
+    pr2serr("     or\n"
+            "       sg_luns    --test=ALUN [--decode] [--hex] [--lu_cong] "
+            "[--verbose]\n"
+            "  where:\n"
+            "    --decode|-d        decode all luns into component parts\n"
+            "    --help|-h          print out usage message\n"
+            "    --hex|-H           output response in hexadecimal; used "
+            "twice\n"
+            "                       shows decoded values in hex\n");
+#ifdef SG_LIB_LINUX
+    pr2serr("    --linux|-l         show Linux integer lun after T10 "
+            "representation\n");
+#endif
+    pr2serr("    --lu_cong|-L       decode as if LU_CONG is set; used "
+            "twice:\n"
+            "                       decode as if LU_CONG is clear\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 0 -> %d bytes)\n"
+            "    --quiet|-q         output only ASCII hex lun values\n"
+            "    --raw|-r           output response in binary\n"
+            "    --readonly|-R      open DEVICE read-only (def: read-write)\n"
+            "    --select=SR|-s SR    select report SR (def: 0)\n"
+            "                          0 -> luns apart from 'well "
+            "known' lus\n"
+            "                          1 -> only 'well known' "
+            "logical unit numbers\n"
+            "                          2 -> all luns\n"
+            "                          0x10 -> administrative luns\n"
+            "                          0x11 -> admin luns + "
+            "non-conglomerate luns\n"
+            "                          0x12 -> admin lun + its "
+            "subsidiary luns\n"
+            "    --test=ALUN|-t ALUN    decode ALUN and ignore most other "
+            "options\n"
+            "                           and DEVICE (apart from '-H')\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Performs a SCSI REPORT LUNS command or decodes the given ALUN. "
+            "When SR is\n0x10 or 0x11 DEVICE must be LUN 0 or REPORT LUNS "
+            "well known logical unit;\nwhen SR is 0x12 DEVICE must be an "
+            "administrative logical unit. When the\n--test=ALUN option is "
+            "given, decodes ALUN rather than sending a REPORT\nLUNS "
+            "command.\n", DEF_RLUNS_BUFF_LEN );
+}
+
+/* Decoded according to SAM-5 rev 10. Note that one draft: BCC rev 0,
+ * defines its own "bridge addressing method" in place of the SAM-3
+ * "logical addressing method".  */
+static void
+decode_lun(const char * leadin, const uint8_t * lunp, bool lu_cong,
+           int do_hex, int verbose)
+{
+    bool next_level, admin_lu_cong;
+    int k, x, a_method, bus_id, target, lun, len_fld, e_a_method;
+    uint64_t ull;
+    char l_leadin[128];
+    char b[256];
+
+    if (0xff == lunp[0]) {
+        printf("%sLogical unit _not_ specified\n", leadin);
+        return;
+    }
+    admin_lu_cong = lu_cong;
+    memset(l_leadin, 0, sizeof(l_leadin));
+    for (k = 0; k < 4; ++k, lunp += 2) {
+        next_level = false;
+        strncpy(l_leadin, leadin, sizeof(l_leadin) - 3);
+        if (k > 0) {
+            if (lu_cong) {
+                admin_lu_cong = false;
+                if ((0 == lunp[0]) && (0 == lunp[1])) {
+                    printf("%s>>>> Administrative LU\n", l_leadin);
+                    if (do_hex || verbose)
+                         printf("        since Subsidiary element is "
+                                "0x0000\n");
+                    break;
+                } else
+                    printf("%s>>Subsidiary element:\n", l_leadin);
+            } else
+                printf("%s>>%s level addressing:\n", l_leadin, ((1 == k) ?
+                         "Second" : ((2 == k) ? "Third" : "Fourth")));
+            strcat(l_leadin, "  ");
+        } else if (lu_cong) {
+            printf("%s>>Administrative element:\n", l_leadin);
+            strcat(l_leadin, "  ");
+        }
+        a_method = (lunp[0] >> 6) & 0x3;
+        switch (a_method) {
+        case 0:         /* peripheral device addressing method */
+            if (lu_cong) {
+                snprintf(b, sizeof(b), "%sSimple lu addressing: ",
+                         l_leadin);
+                x = 0x3fff & sg_get_unaligned_be16(lunp + 0);
+                if (do_hex)
+                    printf("%s0x%04x\n", b, x);
+                else
+                    printf("%s%d\n", b, x);
+                if (admin_lu_cong)
+                    next_level = true;
+            } else {
+                bus_id = lunp[0] & 0x3f;
+                snprintf(b, sizeof(b), "%sPeripheral device addressing: ",
+                         l_leadin);
+                if ((0 == bus_id) && (0 == verbose)) {
+                    if (do_hex)
+                        printf("%slun=0x%02x\n", b, lunp[1]);
+                    else
+                        printf("%slun=%d\n", b, lunp[1]);
+                } else {
+                    if (do_hex)
+                        printf("%sbus_id=0x%02x, %s=0x%02x\n", b, bus_id,
+                               (bus_id ? "target" : "lun"), lunp[1]);
+                    else
+                        printf("%sbus_id=%d, %s=%d\n", b, bus_id,
+                               (bus_id ? "target" : "lun"), lunp[1]);
+                }
+                if (bus_id)
+                    next_level = true;
+            }
+            break;
+        case 1:         /* flat space addressing method */
+            lun = 0x3fff & sg_get_unaligned_be16(lunp + 0);
+            if (lu_cong) {
+                printf("%sSince LU_CONG=1, unexpected Flat space "
+                       "addressing: lun=0x%04x\n", l_leadin, lun);
+                break;
+            }
+            if (do_hex)
+                printf("%sFlat space addressing: lun=0x%04x\n", l_leadin,
+                       lun);
+            else
+                printf("%sFlat space addressing: lun=%d\n", l_leadin, lun);
+            break;
+        case 2:         /* logical unit addressing method */
+            target = (lunp[0] & 0x3f);
+            bus_id = (lunp[1] >> 5) & 0x7;
+            lun = lunp[1] & 0x1f;
+            if (lu_cong) {
+                printf("%sSince LU_CONG=1, unexpected lu addressing: "
+                       "bus_id=0x%x, target=0x%02x, lun=0x%02x\n", l_leadin,
+                       bus_id, target, lun);
+                break;
+            }
+            if (do_hex)
+                printf("%sLogical unit addressing: bus_id=0x%x, "
+                       "target=0x%02x, lun=0x%02x\n", l_leadin, bus_id,
+                       target, lun);
+            else
+                printf("%sLogical unit addressing: bus_id=%d, target=%d, "
+                       "lun=%d\n", l_leadin, bus_id, target, lun);
+            break;
+        case 3:         /* extended logical unit + flat space addressing */
+            len_fld = (lunp[0] & 0x30) >> 4;
+            e_a_method = lunp[0] & 0xf;
+            x = lunp[1];
+            if ((0 == len_fld) && (1 == e_a_method)) {
+                snprintf(b, sizeof(b), "well known logical unit");
+                switch (x) {
+                case 1:
+                    printf("%sREPORT LUNS %s\n", l_leadin, b);
+                    break;
+                case 2:         /* obsolete in spc5r01 */
+                    printf("%sACCESS CONTROLS %s\n", l_leadin, b);
+                    break;
+                case 3:
+                    printf("%sTARGET LOG PAGES %s\n", l_leadin, b);
+                    break;
+                case 4:
+                    printf("%sSECURITY PROTOCOL %s\n", l_leadin, b);
+                    break;
+                case 5:
+                    printf("%sMANAGEMENT PROTOCOL %s\n", l_leadin, b);
+                    break;
+                case 6:
+                    printf("%sTARGET COMMANDS %s\n", l_leadin, b);
+                    break;
+                default:
+                    if (do_hex)
+                        printf("%s%s 0x%02x\n", l_leadin, b, x);
+                    else
+                        printf("%s%s %d\n", l_leadin, b, x);
+                    break;
+                }
+            } else if ((1 == len_fld) && (2 == e_a_method)) {
+                x = sg_get_unaligned_be24(lunp + 1);
+                if (do_hex)
+                    printf("%sExtended flat space addressing: lun=0x%06x\n",
+                           l_leadin, x);
+                else
+                    printf("%sExtended flat space addressing: lun=%d\n",
+                           l_leadin, x);
+            } else if ((2 == len_fld) && (2 == e_a_method)) {
+                ull = sg_get_unaligned_be(5, lunp + 1);
+                if (do_hex)
+                    printf("%sLong extended flat space addressing: "
+                           "lun=0x%010" PRIx64 "\n", l_leadin, ull);
+                else
+                    printf("%sLong extended flat space addressing: "
+                           "lun=%" PRIu64 "\n", l_leadin, ull);
+            } else if ((3 == len_fld) && (0xf == e_a_method))
+                printf("%sLogical unit _not_ specified addressing\n",
+                       l_leadin);
+            else {
+                if (len_fld < 2) {
+                    if (1 == len_fld)
+                        x = sg_get_unaligned_be24(lunp + 1);
+                    if (do_hex)
+                        printf("%sExtended logical unit addressing: "
+                               "length=%d, e.a. method=%d, value=0x%06x\n",
+                               l_leadin, len_fld, e_a_method, x);
+                    else
+                        printf("%sExtended logical unit addressing: "
+                               "length=%d, e.a. method=%d, value=%d\n",
+                               l_leadin, len_fld, e_a_method, x);
+                } else {
+                    ull = sg_get_unaligned_be(((2 == len_fld) ? 5 : 7),
+                                              lunp + 1);
+                    if (do_hex) {
+                        printf("%sExtended logical unit addressing: "
+                               "length=%d, e. a. method=%d, ", l_leadin,
+                               len_fld, e_a_method);
+                        if (5 == len_fld)
+                                printf("value=0x%010" PRIx64 "\n", ull);
+                        else
+                                printf("value=0x%014" PRIx64 "\n", ull);
+                    } else
+                        printf("%sExtended logical unit addressing: "
+                               "length=%d, e. a. method=%d, value=%" PRIu64
+                               "\n", l_leadin, len_fld, e_a_method, ull);
+                }
+            }
+            break;
+        }
+        if (next_level)
+            continue;
+        if ((2 == a_method) && (k < 3) && (lunp[2] || lunp[3]))
+            printf("%s<<unexpected data at next level, continue>>\n",
+                   l_leadin);
+        break;
+    }
+}
+
+#ifdef SG_LIB_LINUX
+static void
+linux2t10_lun(uint64_t linux_lun, uint8_t t10_lun[])
+{
+    int k;
+
+    for (k = 0; k < 8; k += 2, linux_lun >>= 16)
+        sg_put_unaligned_be16((uint16_t)linux_lun, t10_lun + k);
+}
+
+static uint64_t
+t10_2linux_lun(const uint8_t t10_lun[])
+{
+    int k;
+    const uint8_t * cp;
+    uint64_t res;
+
+    res = sg_get_unaligned_be16(t10_lun + 6);
+    for (cp = t10_lun + 4, k = 0; k < 3; ++k, cp -= 2)
+        res = (res << 16) + sg_get_unaligned_be16(cp);
+    return res;
+}
+#endif  /* SG_LIB_LINUX */
+
+
+static void
+dStrRaw(const char * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+int
+main(int argc, char * argv[])
+{
+#ifdef SG_LIB_LINUX
+    bool do_linux = false;
+#endif
+    bool do_quiet = false;
+    bool do_raw = false;
+    bool lu_cong_arg_given = false;
+    bool o_readonly = false;
+#ifdef SG_LIB_LINUX
+    bool test_linux_in = false;
+    bool test_linux_out = false;
+#endif
+    bool trunc;
+    bool verbose_given = false;
+    bool version_given = false;
+    int sg_fd, k, m, off, res, c, list_len, len_cap, luns;
+    int decode_arg = 0;
+    int do_hex = 0;
+    int lu_cong_arg = 0;
+    int maxlen = 0;
+    int ret = 0;
+    int select_rep = 0;
+    int verbose = 0;
+    unsigned int h;
+    const char * test_arg = NULL;
+    const char * device_name = NULL;
+    const char * cp;
+    uint8_t * reportLunsBuff = NULL;
+    uint8_t * free_reportLunsBuff = NULL;
+    uint8_t lun_arr[8];
+    struct sg_simple_inquiry_resp sir;
+
+    while (1) {
+        int option_index = 0;
+
+#ifdef SG_LIB_LINUX
+        c = getopt_long(argc, argv, "dhHlLm:qrRs:t:vV", long_options,
+                        &option_index);
+#else
+        c = getopt_long(argc, argv, "dhHLm:qrRs:t:vV", long_options,
+                        &option_index);
+#endif
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'd':
+            ++decode_arg;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+#ifdef SG_LIB_LINUX
+        case 'l':
+            do_linux = false;
+            break;
+#endif
+        case 'L':
+            ++lu_cong_arg;
+            lu_cong_arg_given = true;
+            break;
+        case 'm':
+            maxlen = sg_get_num(optarg);
+            if ((maxlen < 0) || (maxlen > MAX_RLUNS_BUFF_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or less\n",
+                        MAX_RLUNS_BUFF_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            } else if (maxlen < 4) {
+                pr2serr("Warning: setting '--maxlen' to 4\n");
+                maxlen = 4;
+            }
+            break;
+        case 'q':
+            do_quiet = true;
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 's':
+           select_rep = sg_get_num(optarg);
+           if ((select_rep < 0) || (select_rep > 255)) {
+                pr2serr("bad argument to '--select', expect 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 't':
+            test_arg = optarg;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (test_arg) {
+        memset(lun_arr, 0, sizeof(lun_arr));
+        cp = test_arg;
+        /* check for leading 'L' */
+#ifdef SG_LIB_LINUX
+        if ('L' == toupper(cp[0])) {
+            uint64_t ull;
+
+            if (('0' == cp[1]) && ('X' == toupper((uint8_t)cp[2])))
+                k = sscanf(cp + 3, " %" SCNx64, &ull);
+            else
+                k = sscanf(cp + 1, " %" SCNu64, &ull);
+            if (1 != k) {
+                pr2serr("Unable to read Linux style LUN integer given to "
+                        "--test=\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            linux2t10_lun(ull, lun_arr);
+            test_linux_in = true;
+        } else
+#endif
+        {
+            /* Check if trailing 'L' */
+#ifdef SG_LIB_LINUX
+            m = strlen(cp);    /* must be at least 1 char in test_arg */
+            if ('L' == toupper(cp[m - 1]))
+                test_linux_out = true;
+#endif
+            if (('0' == cp[0]) && ('X' == toupper(cp[1])))
+                cp += 2;
+            if (strchr(cp, ' ') || strchr(cp, '\t') || strchr(cp, '-')) {
+                for (k = 0; k < 8; ++k, cp += 2) {
+                    c = *cp;
+                    if ('\0' == c)
+                        break;
+                    else if (! isxdigit(c))
+                        ++cp;
+                    if (1 != sscanf(cp, "%2x", &h))
+                        break;
+                    lun_arr[k] = h & 0xff;
+                }
+            } else {
+                for (k = 0; k < 8; ++k, cp += 2) {
+                if (1 != sscanf(cp, "%2x", &h))
+                        break;
+                    lun_arr[k] = h & 0xff;
+                }
+            }
+            if (0 == k) {
+                pr2serr("expected a hex number, optionally prefixed by "
+                        "'0x'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+#ifdef SG_LIB_LINUX
+        if (verbose || test_linux_in || decode_arg)
+#else
+        if (verbose || decode_arg)
+#endif
+        {
+            if (decode_arg > 1) {
+                printf("64 bit LUN in T10 (hex, dashed) format: ");
+                for (k = 0; k < 8; k += 2)
+                    printf("%c%02x%02x", (k ? '-' : ' '), lun_arr[k],
+                           lun_arr[k + 1]);
+            } else {
+                printf("64 bit LUN in T10 preferred (hex) format: ");
+                for (k = 0; k < 8; ++k)
+                    printf(" %02x", lun_arr[k]);
+            }
+            printf("\n");
+        }
+#ifdef SG_LIB_LINUX
+        if (test_linux_out) {
+            if (do_hex > 1)
+                printf("Linux 'word flipped' integer LUN representation: "
+                       "0x%016" PRIx64 "\n", t10_2linux_lun(lun_arr));
+            else if (do_hex)
+                printf("Linux 'word flipped' integer LUN representation: 0x%"
+                       PRIx64 "\n", t10_2linux_lun(lun_arr));
+            else
+                printf("Linux 'word flipped' integer LUN representation: %"
+                       PRIu64 "\n", t10_2linux_lun(lun_arr));
+        }
+#endif
+        printf("Decoded LUN:\n");
+        decode_lun("  ", lun_arr, (lu_cong_arg % 2), do_hex, verbose);
+        return 0;
+    }
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose);
+    if (sg_fd < 0) {
+        int err = -sg_fd;
+
+        pr2serr("open error: %s: %s\n", device_name, safe_strerror(err));
+        if ((! o_readonly) && ((err == EACCES) || (err == EROFS)))
+            pr2serr("Perhaps try again with --readonly option or with root "
+                    "permissions\n");
+        return sg_convert_errno(-sg_fd);
+    }
+    if (decode_arg && (! lu_cong_arg_given)) {
+        if (verbose > 1)
+            pr2serr("in order to decode LUN and since --lu_cong not given, "
+                    "do standard\nINQUIRY to find LU_CONG bit\n");
+        /* check if LU_CONG set in standard INQUIRY response */
+        res = sg_simple_inquiry(sg_fd, &sir, false, verbose);
+        ret = res;
+        if (res) {
+            pr2serr("fetching standard INQUIRY response failed\n");
+            goto the_end;
+        }
+        lu_cong_arg = !!(0x40 & sir.byte_1);
+        if (verbose && lu_cong_arg)
+            pr2serr("LU_CONG bit set in standard INQUIRY response\n");
+    }
+
+    if (0 == maxlen)
+        maxlen = DEF_RLUNS_BUFF_LEN;
+    reportLunsBuff = (uint8_t *)sg_memalign(maxlen, 0, &free_reportLunsBuff,
+                                            verbose > 3);
+    if (NULL == reportLunsBuff) {
+        pr2serr("unable to sg_memalign %d bytes\n", maxlen);
+        return sg_convert_errno(ENOMEM);
+    }
+    trunc = false;
+
+    res = sg_ll_report_luns(sg_fd, select_rep, reportLunsBuff, maxlen, true,
+                            verbose);
+    ret = res;
+    if (0 == res) {
+        list_len = sg_get_unaligned_be32(reportLunsBuff + 0);
+        len_cap = list_len + 8;
+        if (len_cap > maxlen)
+            len_cap = maxlen;
+        if (do_raw) {
+            dStrRaw((const char *)reportLunsBuff, len_cap);
+            goto the_end;
+        }
+        if (1 == do_hex) {
+            hex2stdout(reportLunsBuff, len_cap, 1);
+            goto the_end;
+        }
+        luns = (list_len / 8);
+        if (! do_quiet)
+            printf("Lun list length = %d which imples %d lun entr%s\n",
+                   list_len, luns, ((1 == luns) ? "y" : "ies"));
+        if ((list_len + 8) > maxlen) {
+            luns = ((maxlen - 8) / 8);
+            trunc = true;
+            pr2serr("  <<too many luns for internal buffer, will show %d "
+                    "lun%s>>\n", luns, ((1 == luns) ? "" : "s"));
+        }
+        if (verbose > 1) {
+            pr2serr("\nOutput response in hex\n");
+            hex2stderr(reportLunsBuff, (trunc ? maxlen : list_len + 8), 1);
+        }
+        for (k = 0, off = 8; k < luns; ++k, off += 8) {
+            if (! do_quiet) {
+                if (0 == k)
+                    printf("Report luns [select_report=0x%x]:\n", select_rep);
+                printf("    ");
+            }
+            for (m = 0; m < 8; ++m)
+                printf("%02x", reportLunsBuff[off + m]);
+#ifdef SG_LIB_LINUX
+            if (do_linux) {
+                uint64_t lin_lun;
+
+                lin_lun = t10_2linux_lun(reportLunsBuff + off);
+                if (do_hex > 1)
+                    printf("    [0x%" PRIx64 "]", lin_lun);
+                else
+                    printf("    [%" PRIu64 "]", lin_lun);
+            }
+#endif
+            printf("\n");
+            if (decode_arg)
+                decode_lun("      ", reportLunsBuff + off,
+                           (bool)(lu_cong_arg % 2), do_hex, verbose);
+        }
+    } else if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("Report Luns command not supported (support mandatory in "
+                "SPC-3)\n");
+    else if (SG_LIB_CAT_ABORTED_COMMAND == res)
+        pr2serr("Report Luns, aborted command\n");
+    else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+        pr2serr("Report Luns command has bad field in cdb\n");
+    else {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Report Luns command: %s\n", b);
+    }
+
+the_end:
+    if (free_reportLunsBuff)
+        free(free_reportLunsBuff);
+    res = sg_cmds_close_device(sg_fd);
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            return sg_convert_errno(-res);
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_luns failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_map.c b/src/sg_map.c
new file mode 100644
index 0000000..ebf2c2e
--- /dev/null
+++ b/src/sg_map.c
@@ -0,0 +1,508 @@
+/*
+ * Utility program for the Linux OS SCSI generic ("sg") device driver.
+ *     Copyright (C) 2000-2017 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+
+   This shows the mapping from "sg" devices to other scsi devices
+   (i.e. sd, scd or st) if any.
+
+   Note: This program requires sg version 2 or better.
+
+   Version 0.19 20041203
+
+   Version 1.02 20050511
+        - allow for sparse disk name with up to 3 letter SCSI
+          disk device node names (e.g. /dev/sdaaa)
+          [Nate Dailey < Nate dot Dailey at stratus dot com >]
+*/
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <dirent.h>
+#include <libgen.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_io_linux.h"
+
+
+static const char * version_str = "1.12 20171010";
+
+static const char * devfs_id = "/dev/.devfsd";
+
+#define NUMERIC_SCAN_DEF true /* set to false to make alpha scan default */
+
+#define INQUIRY_RESP_INITIAL_LEN 36
+#define MAX_SG_DEVS 4096
+#define PRESENT_ARRAY_SIZE MAX_SG_DEVS
+
+static const char * sysfs_sg_dir = "/sys/class/scsi_generic";
+static char gen_index_arr[PRESENT_ARRAY_SIZE];
+static int has_sysfs_sg = 0;
+
+
+typedef struct my_map_info
+{
+    int active;
+    int lin_dev_type;
+    int oth_dev_num;
+    struct sg_scsi_id sg_dat;
+    char vendor[8];
+    char product[16];
+    char revision[4];
+} my_map_info_t;
+
+
+#define MAX_SD_DEVS (26 + 26*26 + 26*26*26) /* sdX, sdXX, sdXXX */
+                 /* (26 + 676 + 17576) = 18278 */
+#define MAX_SR_DEVS 128
+#define MAX_ST_DEVS 128
+#define MAX_OSST_DEVS 128
+#define MAX_ERRORS 5
+
+static my_map_info_t map_arr[MAX_SG_DEVS];
+
+#define LIN_DEV_TYPE_UNKNOWN 0
+#define LIN_DEV_TYPE_SD 1
+#define LIN_DEV_TYPE_SR 2
+#define LIN_DEV_TYPE_ST 3
+#define LIN_DEV_TYPE_SCD 4
+#define LIN_DEV_TYPE_OSST 5
+
+
+typedef struct my_scsi_idlun {
+/* why can't userland see this structure ??? */
+    int dev_id;
+    int host_unique_id;
+} My_scsi_idlun;
+
+
+#define EBUFF_SZ 256
+static char ebuff[EBUFF_SZ];
+
+static void scan_dev_type(const char * leadin, int max_dev, bool do_numeric,
+                          int lin_dev_type, int last_sg_ind);
+
+static void usage()
+{
+    printf("Usage: sg_map [-a] [-h] [-i] [-n] [-sd] [-scd or -sr] [-st] "
+           "[-V] [-x]\n");
+    printf("  where:\n");
+    printf("    -a      do alphabetic scan (ie sga, sgb, sgc)\n");
+    printf("    -h or -?    show this usage message then exit\n");
+    printf("    -i      also show device INQUIRY strings\n");
+    printf("    -n      do numeric scan (i.e. sg0, sg1, sg2) "
+           "(default)\n");
+    printf("    -sd     show mapping to disks\n");
+    printf("    -scd    show mapping to cdroms (look for /dev/scd<n>\n");
+    printf("    -sr     show mapping to cdroms (look for /dev/sr<n>\n");
+    printf("    -st     show mapping to tapes (st and osst devices)\n");
+    printf("    -V      print version string then exit\n");
+    printf("    -x      also show bus,chan,id,lun and type\n\n");
+    printf("If no '-s*' arguments given then show all mappings. This "
+           "utility\nis DEPRECATED, do not use in Linux 2.6 series or "
+           "later.\n");
+}
+
+static int scandir_select(const struct dirent * s)
+{
+    int k;
+
+    if (1 == sscanf(s->d_name, "sg%d", &k)) {
+        if ((k >= 0) && (k < PRESENT_ARRAY_SIZE)) {
+            gen_index_arr[k] = 1;
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int sysfs_sg_scan(const char * dir_name)
+{
+    struct dirent ** namelist;
+    int num, k;
+
+    num = scandir(dir_name, &namelist, scandir_select, NULL);
+    if (num < 0)
+        return -errno;
+    for (k = 0; k < num; ++k)
+        free(namelist[k]);
+    free(namelist);
+    return num;
+}
+
+static void make_dev_name(char * fname, const char * leadin, int k,
+                          bool do_numeric)
+{
+    char buff[64];
+    int  ones,tens,hundreds; /* for lack of a better name */
+    int  buff_idx;
+
+    strcpy(fname, leadin ? leadin : "/dev/sg");
+    if (do_numeric) {
+        sprintf(buff, "%d", k);
+        strcat(fname, buff);
+    }
+    else if (k >= (26 + 26*26 + 26*26*26)) {
+        strcat(fname, "xxxx");
+    }
+    else {
+        ones = k % 26;
+
+        if ((k - 26) >= 0)
+            tens = ((k-26)/26) % 26;
+        else tens = -1;
+
+        if ((k - (26 + 26*26)) >= 0)
+             hundreds = ((k - (26 + 26*26))/(26*26)) % 26;
+        else hundreds = -1;
+
+        buff_idx = 0;
+        if (hundreds >= 0) buff[buff_idx++] = 'a' + (char)hundreds;
+        if (tens >= 0) buff[buff_idx++] = 'a' + (char)tens;
+        buff[buff_idx++] = 'a' + (char)ones;
+        buff[buff_idx] = '\0';
+        strcat(fname, buff);
+    }
+}
+
+
+int main(int argc, char * argv[])
+{
+    bool do_all_s = true;
+    bool do_extra = false;
+    bool do_inquiry = false;
+    bool do_numeric = NUMERIC_SCAN_DEF;
+    bool do_osst = false;
+    bool do_scd = false;
+    bool do_sd = false;
+    bool do_sr = false;
+    bool do_st = false;
+    bool eacces_err = false;
+    int sg_fd, res, k;
+    int num_errors = 0;
+    int num_silent = 0;
+    int last_sg_ind = -1;
+    char fname[64];
+    struct stat a_stat;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == strcmp("-n", argv[k]))
+            do_numeric = true;
+        else if (0 == strcmp("-a", argv[k]))
+            do_numeric = false;
+        else if (0 == strcmp("-x", argv[k]))
+            do_extra = true;
+        else if (0 == strcmp("-i", argv[k]))
+            do_inquiry = true;
+        else if (0 == strcmp("-sd", argv[k])) {
+            do_sd = true;
+            do_all_s = false;
+        } else if (0 == strcmp("-st", argv[k])) {
+            do_st = true;
+            do_osst = true;
+            do_all_s = false;
+        } else if (0 == strcmp("-sr", argv[k])) {
+            do_sr = true;
+            do_all_s = false;
+        } else if (0 == strcmp("-scd", argv[k])) {
+            do_scd = true;
+            do_all_s = false;
+        } else if (0 == strcmp("-V", argv[k])) {
+            fprintf(stderr, "Version string: %s\n", version_str);
+            exit(0);
+        } else if ((0 == strcmp("-?", argv[k])) ||
+                   (0 == strncmp("-h", argv[k], 2))) {
+            printf(
+            "Show mapping from sg devices to other scsi device names\n\n");
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        } else if (*argv[k] == '-') {
+            printf("Unknown switch: %s\n", argv[k]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        } else if (*argv[k] != '-') {
+            printf("Unknown argument\n");
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    if ((stat(sysfs_sg_dir, &a_stat) >= 0) && (S_ISDIR(a_stat.st_mode)))
+        has_sysfs_sg = sysfs_sg_scan(sysfs_sg_dir);
+
+    if (stat(devfs_id, &a_stat) == 0)
+        printf("# Note: the devfs pseudo file system is present\n");
+
+    for (k = 0, res = 0; (k < MAX_SG_DEVS) && (num_errors < MAX_ERRORS);
+         ++k, res = (sg_fd >= 0) ? close(sg_fd) : 0) {
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ, "Error closing %s ", fname);
+            perror("sg_map: close error");
+            return SG_LIB_FILE_ERROR;
+        }
+        if (has_sysfs_sg) {
+           if (0 == gen_index_arr[k]) {
+                sg_fd = -1;
+                continue;
+            }
+            make_dev_name(fname, "/dev/sg", k, true);
+        } else
+            make_dev_name(fname, "/dev/sg", k, do_numeric);
+
+        sg_fd = open(fname, O_RDONLY | O_NONBLOCK);
+        if (sg_fd < 0) {
+            if (EBUSY == errno) {
+                map_arr[k].active = -2;
+                continue;
+            }
+            else if ((ENODEV == errno) || (ENOENT == errno) ||
+                     (ENXIO == errno)) {
+                ++num_errors;
+                ++num_silent;
+                map_arr[k].active = -1;
+                continue;
+            }
+            else {
+                if (EACCES == errno)
+                    eacces_err = true;
+                snprintf(ebuff, EBUFF_SZ, "Error opening %s ", fname);
+                perror(ebuff);
+                ++num_errors;
+                continue;
+            }
+        }
+        res = ioctl(sg_fd, SG_GET_SCSI_ID, &map_arr[k].sg_dat);
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                     "device %s failed on sg ioctl, skip", fname);
+            perror(ebuff);
+            ++num_errors;
+            continue;
+        }
+        if (do_inquiry) {
+            char buff[INQUIRY_RESP_INITIAL_LEN];
+
+            if (0 == sg_ll_inquiry(sg_fd, false, false, 0, buff, sizeof(buff),
+                                   true, 0)) {
+                memcpy(map_arr[k].vendor, &buff[8], 8);
+                memcpy(map_arr[k].product, &buff[16], 16);
+                memcpy(map_arr[k].revision, &buff[32], 4);
+            }
+        }
+        map_arr[k].active = 1;
+        map_arr[k].oth_dev_num = -1;
+        last_sg_ind = k;
+    }
+    if ((num_errors >= MAX_ERRORS) && (num_silent < num_errors)) {
+        printf("Stopping because there are too many error\n");
+        if (eacces_err)
+            printf("    root access may be required\n");
+        return SG_LIB_FILE_ERROR;
+    }
+    if (last_sg_ind < 0) {
+        printf("Stopping because no sg devices found\n");
+    }
+
+    if (do_all_s || do_sd)
+        scan_dev_type("/dev/sd", MAX_SD_DEVS, 0, LIN_DEV_TYPE_SD, last_sg_ind);
+    if (do_all_s || do_sr)
+        scan_dev_type("/dev/sr", MAX_SR_DEVS, 1, LIN_DEV_TYPE_SR, last_sg_ind);
+    if (do_all_s || do_scd)
+        scan_dev_type("/dev/scd", MAX_SR_DEVS, 1, LIN_DEV_TYPE_SCD,
+                      last_sg_ind);
+    if (do_all_s || do_st)
+        scan_dev_type("/dev/nst", MAX_ST_DEVS, 1, LIN_DEV_TYPE_ST,
+                      last_sg_ind);
+    if (do_all_s || do_osst)
+        scan_dev_type("/dev/osst", MAX_OSST_DEVS, 1, LIN_DEV_TYPE_OSST,
+                      last_sg_ind);
+
+    for (k = 0; k <= last_sg_ind; ++k) {
+        if (has_sysfs_sg) {
+           if (0 == gen_index_arr[k]) {
+                continue;
+            }
+            make_dev_name(fname, "/dev/sg", k, true);
+        } else
+            make_dev_name(fname, "/dev/sg", k, do_numeric);
+        printf("%s", fname);
+        switch (map_arr[k].active)
+        {
+        case -2:
+            printf(do_extra ? "  -2 -2 -2 -2  -2" : "  busy");
+            break;
+        case -1:
+            printf(do_extra ? "  -1 -1 -1 -1  -1" : "  not present");
+            break;
+        case 0:
+            printf(do_extra ? "  -3 -3 -3 -3  -3" : "  some error");
+            break;
+        case 1:
+            if (do_extra)
+                printf("  %d %d %d %d  %d", map_arr[k].sg_dat.host_no,
+                       map_arr[k].sg_dat.channel, map_arr[k].sg_dat.scsi_id,
+                       map_arr[k].sg_dat.lun, map_arr[k].sg_dat.scsi_type);
+            switch (map_arr[k].lin_dev_type)
+            {
+            case LIN_DEV_TYPE_SD:
+                make_dev_name(fname, "/dev/sd" , map_arr[k].oth_dev_num, 0);
+                printf("  %s", fname);
+                break;
+            case LIN_DEV_TYPE_ST:
+                make_dev_name(fname, "/dev/nst" , map_arr[k].oth_dev_num, 1);
+                printf("  %s", fname);
+                break;
+            case LIN_DEV_TYPE_OSST:
+                make_dev_name(fname, "/dev/osst" , map_arr[k].oth_dev_num, 1);
+                printf("  %s", fname);
+                break;
+            case LIN_DEV_TYPE_SR:
+                make_dev_name(fname, "/dev/sr" , map_arr[k].oth_dev_num, 1);
+                printf("  %s", fname);
+                break;
+            case LIN_DEV_TYPE_SCD:
+                make_dev_name(fname, "/dev/scd" , map_arr[k].oth_dev_num, 1);
+                printf("  %s", fname);
+                break;
+            default:
+                break;
+            }
+            if (do_inquiry)
+                printf("  %.8s  %.16s  %.4s", map_arr[k].vendor,
+                       map_arr[k].product, map_arr[k].revision);
+            break;
+        default:
+            printf("  bad logic\n");
+            break;
+        }
+        printf("\n");
+    }
+    return 0;
+}
+
+static int find_dev_in_sg_arr(My_scsi_idlun * my_idlun, int host_no,
+                              int last_sg_ind)
+{
+    int k;
+    struct sg_scsi_id * sidp;
+
+    for (k = 0; k <= last_sg_ind; ++k) {
+        sidp = &(map_arr[k].sg_dat);
+        if ((host_no == sidp->host_no) &&
+            ((my_idlun->dev_id & 0xff) == sidp->scsi_id) &&
+            (((my_idlun->dev_id >> 8) & 0xff) == sidp->lun) &&
+            (((my_idlun->dev_id >> 16) & 0xff) == sidp->channel))
+            return k;
+    }
+    return -1;
+}
+
+static void scan_dev_type(const char * leadin, int max_dev, bool do_numeric,
+                          int lin_dev_type, int last_sg_ind)
+{
+    int k, res, ind, sg_fd = 0;
+    int num_errors = 0;
+    int num_silent = 0;
+    int host_no = -1;
+    My_scsi_idlun my_idlun;
+    char fname[64];
+
+    for (k = 0, res = 0; (k < max_dev)  && (num_errors < MAX_ERRORS);
+         ++k, res = (sg_fd >= 0) ? close(sg_fd) : 0) {
+
+/* ignore close() errors */
+#if 0
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ, "Error closing %s ", fname);
+            perror("sg_map: close error");
+#ifndef IGN_CLOSE_ERR
+            return;
+#else
+            ++num_errors;
+            sg_fd = 0;
+#endif
+        }
+#endif
+        make_dev_name(fname, leadin, k, do_numeric);
+#ifdef DEBUG
+        printf ("Trying %s: ", fname);
+#endif
+
+        sg_fd = open(fname, O_RDONLY | O_NONBLOCK);
+        if (sg_fd < 0) {
+#ifdef DEBUG
+            printf ("ERROR %i\n", errno);
+#endif
+            if (EBUSY == errno) {
+                printf("Device %s is busy\n", fname);
+                ++num_errors;
+            } else if ((ENODEV == errno) || (ENXIO == errno)) {
+                ++num_errors;
+                ++num_silent;
+            } else if (ENOENT != errno) { /* ignore ENOENT for sparse names */
+                snprintf(ebuff, EBUFF_SZ, "Error opening %s ", fname);
+                perror(ebuff);
+                ++num_errors;
+            }
+            continue;
+        }
+
+        res = ioctl(sg_fd, SCSI_IOCTL_GET_IDLUN, &my_idlun);
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                     "device %s failed on scsi ioctl(idlun), skip", fname);
+            perror(ebuff);
+            ++num_errors;
+#ifdef DEBUG
+            printf ("Couldn't get IDLUN!\n");
+#endif
+            continue;
+        }
+        res = ioctl(sg_fd, SCSI_IOCTL_GET_BUS_NUMBER, &host_no);
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                 "device %s failed on scsi ioctl(bus_number), skip", fname);
+            perror(ebuff);
+            ++num_errors;
+#ifdef DEBUG
+            printf ("Couldn't get BUS!\n");
+#endif
+            continue;
+        }
+#ifdef DEBUG
+        printf ("%i(%x) %i %i %i %i\n", host_no, my_idlun.host_unique_id,
+                (my_idlun.dev_id>>24)&0xff, (my_idlun.dev_id>>16)&0xff,
+                (my_idlun.dev_id>>8)&0xff, my_idlun.dev_id&0xff);
+#endif
+        ind = find_dev_in_sg_arr(&my_idlun, host_no, last_sg_ind);
+        if (ind >= 0) {
+            map_arr[ind].oth_dev_num = k;
+            map_arr[ind].lin_dev_type = lin_dev_type;
+        }
+        else
+            printf("Strange, could not find device %s mapped to sg device??\n",
+                   fname);
+    }
+}
diff --git a/src/sg_map26.c b/src/sg_map26.c
new file mode 100644
index 0000000..2ea8d69
--- /dev/null
+++ b/src/sg_map26.c
@@ -0,0 +1,1285 @@
+/*
+ * Copyright (c) 2005-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/* A utility program for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program maps a primary SCSI device node name to the corresponding
+ * SCSI generic device node name (or vice versa). Targets Linux
+ * kernel 2.6, 3 and 4 series. Sysfs device names can also be mapped.
+ */
+
+/* #define _XOPEN_SOURCE 500 */
+/* needed to see DT_REG and friends when compiled with: c99 pedantic */
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <getopt.h>
+#include <dirent.h>
+#include <libgen.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>      /* new location for major + minor */
+#ifndef major
+#include <sys/types.h>
+#endif
+#include <linux/major.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+
+static const char * version_str = "1.19 20220117";
+
+#define ME "sg_map26: "
+
+#define NT_NO_MATCH 0
+#define NT_SD 1
+#define NT_SR 2
+#define NT_HD 3
+#define NT_ST 4
+#define NT_OSST 5
+#define NT_SG 6
+#define NT_CH 7
+#define NT_REG 8
+#define NT_DIR 9
+
+#define NAME_LEN_MAX 256
+#define D_NAME_LEN_MAX 520
+
+#ifndef SCSI_CHANGER_MAJOR
+#define SCSI_CHANGER_MAJOR 86
+#endif
+#ifndef OSST_MAJOR
+#define OSST_MAJOR 206
+#endif
+
+/* scandir() and stat() categories */
+#define FT_OTHER 0
+#define FT_REGULAR 1
+#define FT_BLOCK 2
+#define FT_CHAR 3
+#define FT_DIR 4
+
+/* older major.h headers may not have these */
+#ifndef SCSI_DISK8_MAJOR
+#define SCSI_DISK8_MAJOR        128
+#define SCSI_DISK9_MAJOR        129
+#define SCSI_DISK10_MAJOR       130
+#define SCSI_DISK11_MAJOR       131
+#define SCSI_DISK12_MAJOR       132
+#define SCSI_DISK13_MAJOR       133
+#define SCSI_DISK14_MAJOR       134
+#define SCSI_DISK15_MAJOR       135
+#endif
+
+/* st minor decodes from Kai Makisara 20081008 */
+#define ST_NBR_MODE_BITS 2
+#define ST_MODE_SHIFT (7 - ST_NBR_MODE_BITS)
+#define TAPE_NR(minor) ( (((minor) & ~255) >> (ST_NBR_MODE_BITS + 1)) | \
+    ((minor) & ~(UINT_MAX << ST_MODE_SHIFT)) )
+
+static const char * sys_sg_dir = "/sys/class/scsi_generic/";
+static const char * sys_sd_dir = "/sys/block/";
+static const char * sys_sr_dir = "/sys/block/";
+static const char * sys_hd_dir = "/sys/block/";
+static const char * sys_st_dir = "/sys/class/scsi_tape/";
+static const char * sys_sch_dir = "/sys/class/scsi_changer/";
+static const char * sys_osst_dir = "/sys/class/onstream_tape/";
+static const char * def_dev_dir = "/dev";
+
+
+static struct option long_options[] = {
+        {"dev_dir", required_argument, 0, 'd'},
+        {"given_is", required_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"result", required_argument, 0, 'r'},
+        {"symlink", no_argument, 0, 's'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static const char * nt_names[] = {
+        "No matching",
+        "disk",
+        "cd/dvd",
+        "hd",
+        "tape",
+        "tape (osst)",
+        "generic (sg)",
+        "changer",
+        "regular file",
+        "directory",
+};
+
+#if defined(__GNUC__) || defined(__clang__)
+static int pr2serr(const char * fmt, ...)
+        __attribute__ ((format (printf, 1, 2)));
+#else
+static int pr2serr(const char * fmt, ...);
+#endif
+
+
+static int
+pr2serr(const char * fmt, ...)
+{
+    va_list args;
+    int n;
+
+    va_start(args, fmt);
+    n = vfprintf(stderr, fmt, args);
+    va_end(args);
+    return n;
+}
+
+static void
+usage()
+{
+        pr2serr("Usage: sg_map26 [--dev_dir=DIR] [--given_is=0...1] [--help] "
+                "[--result=0...3]\n"
+                "                [--symlink] [--verbose] [--version] "
+                "DEVICE\n"
+                "  where:\n"
+                "    --dev_dir=DIR | -d DIR    search in DIR for "
+                "resulting special\n"
+                "                            (def: directory of DEVICE "
+                "or '/dev')\n"
+                "    --given_is=0...1 | -g 0...1    variety of given "
+                "DEVICE\n"
+                "                                   0->block or char special "
+                "(or symlink to)\n"
+                "                                   1->sysfs device, 'dev' or "
+                "parent\n"
+                "    --help | -h       print out usage message\n"
+                "    --result=0...3 | -r 0...3    variety of file(s) to "
+                "find\n"
+                "                                 0->mapped block or char "
+                "special(def)\n"
+                "                                 1->mapped sysfs path\n"
+                "                                 2->matching block or "
+                "char special\n"
+                "                                 3->matching sysfs "
+                "path\n"
+                "    --symlink | -s    symlinks to special included in "
+                "result\n"
+                "    --verbose | -v    increase verbosity of output\n"
+                "    --version | -V    print version string and exit\n\n"
+                "Maps SCSI device node to corresponding generic node (and "
+                "vv)\n"
+                );
+}
+
+
+/* ssafe_strerror() contributed by Clayton Weaver <cgweav at email dot com>
+   Allows for situation in which strerror() is given a wild value (or the
+   C library is incomplete) and returns NULL. Still not thread safe.
+ */
+
+static char safe_errbuf[64] = {'u', 'n', 'k', 'n', 'o', 'w', 'n', ' ',
+                               'e', 'r', 'r', 'n', 'o', ':', ' ', 0};
+
+static char *
+ssafe_strerror(int errnum)
+{
+        size_t len;
+        char * errstr;
+
+        errstr = strerror(errnum);
+        if (NULL == errstr) {
+                len = strlen(safe_errbuf);
+                snprintf(safe_errbuf + len, sizeof(safe_errbuf) - len, "%i",
+                         errnum);
+                safe_errbuf[sizeof(safe_errbuf) - 1] = '\0';  /* bombproof */
+                return safe_errbuf;
+        }
+        return errstr;
+}
+
+static int
+nt_typ_from_filename(const char * filename, int * majj, int * minn)
+{
+        struct stat st;
+        int ma, mi;
+
+        if (stat(filename, &st) < 0)
+                return -errno;
+        ma = major(st.st_rdev);
+        mi = minor(st.st_rdev);
+        if (majj)
+                *majj = ma;
+        if (minn)
+                *minn = mi;
+        if (S_ISCHR(st.st_mode)) {
+                switch(ma) {
+                case OSST_MAJOR:
+                        return NT_OSST;
+                case SCSI_GENERIC_MAJOR:
+                        return NT_SG;
+                case SCSI_TAPE_MAJOR:
+                        return NT_ST;
+                case SCSI_CHANGER_MAJOR:
+                        return NT_CH;
+                default:
+                        return NT_NO_MATCH;
+                }
+        } else if (S_ISBLK(st.st_mode)) {
+                switch(ma) {
+                case SCSI_DISK0_MAJOR: case SCSI_DISK1_MAJOR:
+                case SCSI_DISK2_MAJOR: case SCSI_DISK3_MAJOR:
+                case SCSI_DISK4_MAJOR: case SCSI_DISK5_MAJOR:
+                case SCSI_DISK6_MAJOR: case SCSI_DISK7_MAJOR:
+                case SCSI_DISK8_MAJOR: case SCSI_DISK9_MAJOR:
+                case SCSI_DISK10_MAJOR: case SCSI_DISK11_MAJOR:
+                case SCSI_DISK12_MAJOR: case SCSI_DISK13_MAJOR:
+                case SCSI_DISK14_MAJOR: case SCSI_DISK15_MAJOR:
+                        return NT_SD;
+                case SCSI_CDROM_MAJOR:
+                        return NT_SR;
+                case IDE0_MAJOR: case IDE1_MAJOR:
+                case IDE2_MAJOR: case IDE3_MAJOR:
+                case IDE4_MAJOR: case IDE5_MAJOR:
+                case IDE6_MAJOR: case IDE7_MAJOR:
+                case IDE8_MAJOR: case IDE9_MAJOR:
+                        return NT_HD;
+                default:
+                        return NT_NO_MATCH;
+                }
+        } else if (S_ISREG(st.st_mode))
+                return NT_REG;
+        else if (S_ISDIR(st.st_mode))
+                return NT_DIR;
+        return NT_NO_MATCH;
+}
+
+static int
+nt_typ_from_major(int ma)
+{
+        switch(ma) {
+        case SCSI_DISK0_MAJOR: case SCSI_DISK1_MAJOR:
+        case SCSI_DISK2_MAJOR: case SCSI_DISK3_MAJOR:
+        case SCSI_DISK4_MAJOR: case SCSI_DISK5_MAJOR:
+        case SCSI_DISK6_MAJOR: case SCSI_DISK7_MAJOR:
+        case SCSI_DISK8_MAJOR: case SCSI_DISK9_MAJOR:
+        case SCSI_DISK10_MAJOR: case SCSI_DISK11_MAJOR:
+        case SCSI_DISK12_MAJOR: case SCSI_DISK13_MAJOR:
+        case SCSI_DISK14_MAJOR: case SCSI_DISK15_MAJOR:
+                return NT_SD;
+        case SCSI_CDROM_MAJOR:
+                return NT_SR;
+        case IDE0_MAJOR: case IDE1_MAJOR:
+        case IDE2_MAJOR: case IDE3_MAJOR:
+        case IDE4_MAJOR: case IDE5_MAJOR:
+        case IDE6_MAJOR: case IDE7_MAJOR:
+        case IDE8_MAJOR: case IDE9_MAJOR:
+                return NT_HD;
+        case OSST_MAJOR:
+                return NT_OSST;
+        case SCSI_GENERIC_MAJOR:
+                return NT_SG;
+        case SCSI_TAPE_MAJOR:
+                return NT_ST;
+        case SCSI_CHANGER_MAJOR:
+                return NT_CH;
+        default:
+                return NT_NO_MATCH;
+        }
+        return NT_NO_MATCH;
+}
+
+
+struct node_match_item {
+        bool follow_symlink;
+        int file_type;
+        int majj;
+        int minn;
+        char dir_name[D_NAME_LEN_MAX];
+};
+
+static struct node_match_item nd_match;
+
+static int
+nd_match_scandir_select(const struct dirent * s)
+{
+        bool symlnk = false;
+        struct stat st;
+        char name[D_NAME_LEN_MAX];
+
+        switch (s->d_type) {
+        case DT_BLK:
+                if (FT_BLOCK != nd_match.file_type)
+                        return 0;
+                break;
+        case DT_CHR:
+                if (FT_CHAR != nd_match.file_type)
+                        return 0;
+                break;
+        case DT_DIR:
+                return (FT_DIR == nd_match.file_type) ? 1 : 0;
+        case DT_REG:
+                return (FT_REGULAR == nd_match.file_type) ? 1 : 0;
+        case DT_LNK:    /* follow symlinks */
+                if (! nd_match.follow_symlink)
+                        return 0;
+                symlnk = true;
+                break;
+        default:
+                return 0;
+        }
+        if ((! symlnk) && (-1 == nd_match.majj) && (-1 == nd_match.minn))
+                return 1;
+        snprintf(name, sizeof(name), "%.*s/%.*s", NAME_LEN_MAX,
+                 nd_match.dir_name, NAME_LEN_MAX, s->d_name);
+        memset(&st, 0, sizeof(st));
+        if (stat(name, &st) < 0)
+                return 0;
+        if (symlnk) {
+                if (S_ISCHR(st.st_mode)) {
+                        if (FT_CHAR != nd_match.file_type)
+                                return 0;
+                } else if (S_ISBLK(st.st_mode)) {
+                        if (FT_BLOCK != nd_match.file_type)
+                                return 0;
+                } else
+                        return 0;
+        }
+        return (((-1 == nd_match.majj) ||
+                 ((unsigned)major(st.st_rdev) == (unsigned)nd_match.majj)) &&
+                ((-1 == nd_match.minn) ||
+                 ((unsigned)minor(st.st_rdev) == (unsigned)nd_match.minn)))
+               ? 1 : 0;
+}
+
+static int
+list_matching_nodes(const char * dir_name, int file_type, int majj, int minn,
+                    bool follow_symlink, int verbose)
+{
+        struct dirent ** namelist;
+        int num, k;
+
+        strncpy(nd_match.dir_name, dir_name, D_NAME_LEN_MAX - 1);
+        nd_match.file_type = file_type;
+        nd_match.majj = majj;
+        nd_match.minn = minn;
+        nd_match.follow_symlink = follow_symlink;
+        num = scandir(dir_name, &namelist, nd_match_scandir_select, NULL);
+        if (num < 0) {
+                if (verbose)
+                        pr2serr("scandir: %s %s\n", dir_name,
+                                ssafe_strerror(errno));
+                return -errno;
+        }
+        for (k = 0; k < num; ++k) {
+                printf("%s/%s\n", dir_name, namelist[k]->d_name);
+                free(namelist[k]);
+        }
+        free(namelist);
+        return num;
+}
+
+struct sg_item_t {
+        char name[NAME_LEN_MAX + 2];
+        int ft;
+        int nt;
+        int d_type;
+};
+
+static struct sg_item_t for_first;
+
+static int
+first_scandir_select(const struct dirent * s)
+{
+        if (FT_OTHER != for_first.ft)
+                return 0;
+        if ((DT_LNK != s->d_type) &&
+            ((DT_DIR != s->d_type) || ('.' == s->d_name[0])))
+                return 0;
+        strncpy(for_first.name, s->d_name, NAME_LEN_MAX);
+        for_first.ft = FT_CHAR;  /* dummy */
+        for_first.d_type =  s->d_type;
+        return 1;
+}
+
+/* scan for directory entry that is either a symlink or a directory */
+static int
+scan_for_first(const char * dir_name, int verbose)
+{
+        char name[NAME_LEN_MAX];
+        struct dirent ** namelist;
+        int num, k;
+
+        for_first.ft = FT_OTHER;
+        num = scandir(dir_name, &namelist, first_scandir_select, NULL);
+        if (num < 0) {
+                if (verbose > 0) {
+                        snprintf(name, NAME_LEN_MAX, "scandir: %s", dir_name);
+                        perror(name);
+                }
+                return -1;
+        }
+        for (k = 0; k < num; ++k)
+                free(namelist[k]);
+        free(namelist);
+        return num;
+}
+
+static struct sg_item_t from_sg;
+
+static int
+from_sg_scandir_select(const struct dirent * s)
+{
+        int len;
+
+        if (FT_OTHER != from_sg.ft)
+                return 0;
+        if ((DT_LNK != s->d_type) &&
+            ((DT_DIR != s->d_type) || ('.' == s->d_name[0])))
+                return 0;
+        from_sg.d_type = s->d_type;
+        if (0 == strncmp("scsi_changer", s->d_name, 12)) {
+                strncpy(from_sg.name, s->d_name, NAME_LEN_MAX);
+                from_sg.ft = FT_CHAR;
+                from_sg.nt = NT_CH;
+                return 1;
+        } else if (0 == strncmp("block", s->d_name, 5)) {
+                strncpy(from_sg.name, s->d_name, NAME_LEN_MAX);
+                from_sg.ft = FT_BLOCK;
+                return 1;
+        } else if (0 == strcmp("tape", s->d_name)) {
+                strcpy(from_sg.name, s->d_name);
+                from_sg.ft = FT_CHAR;
+                from_sg.nt = NT_ST;
+                return 1;
+        } else if (0 == strncmp("scsi_tape:st", s->d_name, 12)) {
+                len = strlen(s->d_name);
+                if (isdigit(s->d_name[len - 1])) {
+                        /* want 'st<num>' symlink only */
+                        strcpy(from_sg.name, s->d_name);
+                        from_sg.ft = FT_CHAR;
+                        from_sg.nt = NT_ST;
+                        return 1;
+                } else
+                        return 0;
+        } else if (0 == strncmp("onstream_tape:os", s->d_name, 16)) {
+                strcpy(from_sg.name, s->d_name);
+                from_sg.ft = FT_CHAR;
+                from_sg.nt = NT_OSST;
+                return 1;
+        } else
+                return 0;
+}
+
+static int
+from_sg_scan(const char * dir_name, int verbose)
+{
+        struct dirent ** namelist;
+        int num, k;
+
+        from_sg.ft = FT_OTHER;
+        from_sg.nt = NT_NO_MATCH;
+        num = scandir(dir_name, &namelist, from_sg_scandir_select, NULL);
+        if (num < 0) {
+                if (verbose)
+                        pr2serr("scandir: %s %s\n", dir_name,
+                                ssafe_strerror(errno));
+                return -errno;
+        }
+        if (verbose) {
+                for (k = 0; k < num; ++k)
+                        pr2serr("    %s/%s\n", dir_name,
+                                namelist[k]->d_name);
+        }
+        for (k = 0; k < num; ++k)
+                free(namelist[k]);
+        free(namelist);
+        return num;
+}
+
+static struct sg_item_t to_sg;
+
+static int
+to_sg_scandir_select(const struct dirent * s)
+{
+        if (FT_OTHER != to_sg.ft)
+                return 0;
+        if (DT_LNK != s->d_type)
+                return 0;
+        if (0 == strncmp("scsi_generic", s->d_name, 12)) {
+                strncpy(to_sg.name, s->d_name, NAME_LEN_MAX);
+                to_sg.ft = FT_CHAR;
+                to_sg.nt = NT_SG;
+                return 1;
+        } else
+                return 0;
+}
+
+static int
+to_sg_scan(const char * dir_name)
+{
+        struct dirent ** namelist;
+        int num, k;
+
+        to_sg.ft = FT_OTHER;
+        to_sg.nt = NT_NO_MATCH;
+        num = scandir(dir_name, &namelist, to_sg_scandir_select, NULL);
+        if (num < 0)
+                return -errno;
+        for (k = 0; k < num; ++k)
+                free(namelist[k]);
+        free(namelist);
+        return num;
+}
+
+/* Return 1 if directory, else 0 */
+static int
+if_directory_chdir(const char * dir_name, const char * base_name)
+{
+        char buff[D_NAME_LEN_MAX];
+        struct stat a_stat;
+
+        strcpy(buff, dir_name);
+        strcat(buff, "/");
+        strcat(buff, base_name);
+        if (stat(buff, &a_stat) < 0)
+                return 0;
+        if (S_ISDIR(a_stat.st_mode)) {
+                if (chdir(buff) < 0)
+                        return 0;
+                return 1;
+        }
+        return 0;
+}
+
+/* Return 1 if directory, else 0 */
+static int
+if_directory_ch2generic(const char * dir_name)
+{
+        char buff[NAME_LEN_MAX];
+        struct stat a_stat;
+        const char * old_name = "generic";
+
+        strcpy(buff, dir_name);
+        strcat(buff, "/");
+        strcat(buff, old_name);
+        if ((stat(buff, &a_stat) >= 0) && S_ISDIR(a_stat.st_mode)) {
+                if (chdir(buff) < 0)
+                        return 0;
+                return 1;
+        }
+        /* No "generic", so now look for "scsi_generic:sg<n>" */
+        if (1 != to_sg_scan(dir_name))
+                return 0;
+        strcpy(buff, dir_name);
+        strcat(buff, "/");
+        strcat(buff, to_sg.name);
+        if (stat(buff, &a_stat) < 0)
+                return 0;
+        if (S_ISDIR(a_stat.st_mode)) {
+                if (chdir(buff) < 0)
+                        return 0;
+                return 1;
+        }
+        return 0;
+}
+
+/* Return 1 if found, else 0 if problems */
+static int
+get_value(const char * dir_name, const char * base_name, char * value,
+          int max_value_len)
+{
+        char buff[D_NAME_LEN_MAX];
+        FILE * f;
+        int len;
+
+        if ((NULL == dir_name) && (NULL == base_name))
+                return 0;
+        if (dir_name) {
+                strcpy(buff, dir_name);
+                if (base_name && (strlen(base_name) > 0)) {
+                        strcat(buff, "/");
+                        strcat(buff, base_name);
+                }
+        } else
+                strcpy(buff, base_name);
+        if (NULL == (f = fopen(buff, "r"))) {
+                return 0;
+        }
+        if (NULL == fgets(value, max_value_len, f)) {
+                fclose(f);
+                return 0;
+        }
+        len = strlen(value);
+        if ((len > 0) && (value[len - 1] == '\n'))
+                value[len - 1] = '\0';
+        fclose(f);
+        return 1;
+}
+
+static int
+map_hd(const char * device_dir, int ma, int mi, int result,
+       bool follow_symlink, int verbose)
+{
+        char c, num;
+
+        if (2 == result) {
+                num = list_matching_nodes(device_dir, FT_BLOCK,
+                                          ma, mi, follow_symlink,
+                                          verbose);
+                return (num > 0) ? 0 : 1;
+        }
+        switch (ma) {
+        case IDE0_MAJOR: c = 'a'; break;
+        case IDE1_MAJOR: c = 'c'; break;
+        case IDE2_MAJOR: c = 'e'; break;
+        case IDE3_MAJOR: c = 'g'; break;
+        case IDE4_MAJOR: c = 'i'; break;
+        case IDE5_MAJOR: c = 'k'; break;
+        case IDE6_MAJOR: c = 'm'; break;
+        case IDE7_MAJOR: c = 'o'; break;
+        case IDE8_MAJOR: c = 'q'; break;
+        case IDE9_MAJOR: c = 's'; break;
+        default: c = '?'; break;
+        }
+        if (mi > 63)
+                ++c;
+        printf("%shd%c\n", sys_hd_dir, c);
+        return 0;
+}
+
+static int
+map_sd(const char * device_name, const char * device_dir, int ma, int mi,
+       int result, bool follow_symlink, int verbose)
+{
+        int index, m_mi, m_ma, num;
+        char value[D_NAME_LEN_MAX];
+        char name[D_NAME_LEN_MAX];
+
+        if (2 == result) {
+                num = list_matching_nodes(device_dir, FT_BLOCK, ma, mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        }
+        if (SCSI_DISK0_MAJOR == ma)
+                index = mi / 16;
+        else if (ma >= SCSI_DISK8_MAJOR)
+                index = (mi / 16) + 128 +
+                        ((ma - SCSI_DISK8_MAJOR) * 16);
+        else
+                index = (mi / 16) + 16 +
+                        ((ma - SCSI_DISK1_MAJOR) * 16);
+        if (index < 26)
+                snprintf(name, sizeof(name), "%ssd%c",
+                         sys_sd_dir, 'a' + index % 26);
+        else if (index < (26 + 1) * 26)
+                snprintf(name, sizeof(name), "%ssd%c%c",
+                         sys_sd_dir,
+                         'a' + index / 26 - 1,'a' + index % 26);
+        else {
+                const unsigned int m1 = (index / 26 - 1) / 26 - 1;
+                const unsigned int m2 = (index / 26 - 1) % 26;
+                const unsigned int m3 =  index % 26;
+
+                snprintf(name, sizeof(name), "%ssd%c%c%c",
+                         sys_sd_dir, 'a' + m1, 'a' + m2, 'a' + m3);
+        }
+        if (3 == result) {
+                printf("%s\n", name);
+                return 0;
+        }
+        if (! get_value(name, "dev", value, sizeof(value))) {
+                pr2serr("Couldn't find sysfs match for device: %s\n",
+                        device_name);
+                return 1;
+        }
+        if (verbose)
+                pr2serr("sysfs sd dev: %s\n", value);
+        if (! if_directory_chdir(name, "device")) {
+                pr2serr("sysfs problem with device: %s\n", device_name);
+                return 1;
+        }
+        if (if_directory_ch2generic(".")) {
+                if (1 == result) {
+                        if (NULL == getcwd(value, sizeof(value)))
+                                value[0] = '\0';
+                        printf("%s\n", value);
+                        return 0;
+                }
+                if (! get_value(".", "dev", value, sizeof(value))) {
+                        pr2serr("Couldn't find sysfs generic dev\n");
+                        return 1;
+                }
+                if (verbose)
+                        printf("matching dev: %s\n", value);
+                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
+                        pr2serr("Couldn't decode mapped dev\n");
+                        return 1;
+                }
+                num = list_matching_nodes(device_dir, FT_CHAR, m_ma, m_mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        } else {
+                pr2serr("sd device: %s does not match any SCSI generic "
+                        "device\n", device_name);
+                pr2serr("    perhaps sg module is not loaded\n");
+                return 1;
+        }
+}
+
+static int
+map_sr(const char * device_name, const char * device_dir, int ma, int mi,
+       int result, bool follow_symlink, int verbose)
+{
+        int m_mi, m_ma, num;
+        char value[D_NAME_LEN_MAX];
+        char name[D_NAME_LEN_MAX];
+
+        if (2 == result) {
+                num = list_matching_nodes(device_dir, FT_BLOCK, ma, mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        }
+        snprintf(name, sizeof(name), "%ssr%d", sys_sr_dir, mi);
+        if (3 == result) {
+                printf("%s\n", name);
+                return 0;
+        }
+        if (! get_value(name, "dev", value, sizeof(value))) {
+                pr2serr("Couldn't find sysfs match for device: %s\n",
+                        device_name);
+                return 1;
+        }
+        if (verbose)
+                pr2serr("sysfs sr dev: %s\n", value);
+        if (! if_directory_chdir(name, "device")) {
+                pr2serr("sysfs problem with device: %s\n", device_name);
+                return 1;
+        }
+        if (if_directory_ch2generic(".")) {
+                if (1 == result) {
+                        if (NULL == getcwd(value, sizeof(value)))
+                                value[0] = '\0';
+                        printf("%s\n", value);
+                        return 0;
+                }
+                if (! get_value(".", "dev", value, sizeof(value))) {
+                        pr2serr("Couldn't find sysfs generic dev\n");
+                        return 1;
+                }
+                if (verbose)
+                        printf("matching dev: %s\n", value);
+                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
+                        pr2serr("Couldn't decode mapped dev\n");
+                        return 1;
+                }
+                num = list_matching_nodes(device_dir, FT_BLOCK, m_ma, m_mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        } else {
+                pr2serr("sr device: %s does not match any SCSI generic "
+                        "device\n", device_name);
+                pr2serr("    perhaps sg module is not loaded\n");
+                return 1;
+        }
+}
+
+static int
+map_st(const char * device_name, const char * device_dir, int ma, int mi,
+       int result, bool follow_symlink, int verbose)
+{
+        int m_mi, m_ma, num;
+        char value[D_NAME_LEN_MAX];
+        char name[D_NAME_LEN_MAX];
+
+        if (2 == result) {
+                num = list_matching_nodes(device_dir, FT_CHAR, ma, mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        }
+        snprintf(name, sizeof(name), "%sst%d", sys_st_dir,
+                 TAPE_NR(mi));
+        if (3 == result) {
+                printf("%s\n", name);
+                return 0;
+        }
+        if (! get_value(name, "dev", value, sizeof(value))) {
+                pr2serr("Couldn't find sysfs match for device: %s\n",
+                        device_name);
+                return 1;
+        }
+        if (verbose)
+                pr2serr("sysfs st dev: %s\n", value);
+        if (! if_directory_chdir(name, "device")) {
+                pr2serr("sysfs problem with device: %s\n", device_name);
+                return 1;
+        }
+        if (if_directory_ch2generic(".")) {
+                if (1 == result) {
+                        if (NULL == getcwd(value, sizeof(value)))
+                                value[0] = '\0';
+                        printf("%s\n", value);
+                        return 0;
+                }
+                if (! get_value(".", "dev", value, sizeof(value))) {
+                        pr2serr("Couldn't find sysfs generic dev\n");
+                        return 1;
+                }
+                if (verbose)
+                        printf("matching dev: %s\n", value);
+                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
+                        pr2serr("Couldn't decode mapped dev\n");
+                        return 1;
+                }
+                num = list_matching_nodes(device_dir, FT_CHAR, m_ma, m_mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        } else {
+                pr2serr("st device: %s does not match any SCSI generic "
+                        "device\n", device_name);
+                pr2serr("    perhaps sg module is not loaded\n");
+                return 1;
+        }
+}
+
+static int
+map_osst(const char * device_name, const char * device_dir, int ma, int mi,
+         int result, bool follow_symlink, int verbose)
+{
+        int m_mi, m_ma, num;
+        char value[D_NAME_LEN_MAX];
+        char name[D_NAME_LEN_MAX];
+
+        if (2 == result) {
+                num = list_matching_nodes(device_dir, FT_CHAR, ma, mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        }
+        snprintf(name, sizeof(name), "%sosst%d", sys_osst_dir,
+                 TAPE_NR(mi));
+        if (3 == result) {
+                printf("%s\n", name);
+                return 0;
+        }
+        if (! get_value(name, "dev", value, sizeof(value))) {
+                pr2serr("Couldn't find sysfs match for device: %s\n",
+                        device_name);
+                return 1;
+        }
+        if (verbose)
+                pr2serr("sysfs osst dev: %s\n", value);
+        if (! if_directory_chdir(name, "device")) {
+                pr2serr("sysfs problem with device: %s\n", device_name);
+                return 1;
+        }
+        if (if_directory_ch2generic(".")) {
+                if (1 == result) {
+                        if (NULL == getcwd(value, sizeof(value)))
+                                value[0] = '\0';
+                        printf("%s\n", value);
+                        return 0;
+                }
+                if (! get_value(".", "dev", value, sizeof(value))) {
+                        pr2serr("Couldn't find sysfs generic dev\n");
+                        return 1;
+                }
+                if (verbose)
+                        printf("matching dev: %s\n", value);
+                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
+                        pr2serr("Couldn't decode mapped dev\n");
+                        return 1;
+                }
+                num = list_matching_nodes(device_dir, FT_CHAR, m_ma, m_mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        } else {
+                pr2serr("osst device: %s does not match any SCSI generic "
+                        "device\n", device_name);
+                pr2serr("    perhaps sg module is not loaded\n");
+                return 1;
+        }
+}
+
+static int
+map_ch(const char * device_name, const char * device_dir, int ma, int mi,
+       int result, bool follow_symlink, int verbose)
+{
+        int m_mi, m_ma, num;
+        char value[D_NAME_LEN_MAX];
+        char name[D_NAME_LEN_MAX];
+
+        if (2 == result) {
+                num = list_matching_nodes(device_dir, FT_CHAR, ma, mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        }
+        snprintf(name, sizeof(name), "%ssch%d", sys_sch_dir, mi);
+        if (3 == result) {
+                printf("%s\n", name);
+                return 0;
+        }
+        if (! get_value(name, "dev", value, sizeof(value))) {
+                pr2serr("Couldn't find sysfs match for device: %s\n",
+                        device_name);
+                return 1;
+        }
+        if (verbose)
+                pr2serr("sysfs sch dev: %s\n", value);
+        if (! if_directory_chdir(name, "device")) {
+                pr2serr("sysfs problem with device: %s\n", device_name);
+                return 1;
+        }
+        if (if_directory_ch2generic(".")) {
+                if (1 == result) {
+                        if (NULL == getcwd(value, sizeof(value)))
+                                value[0] = '\0';
+                        printf("%s\n", value);
+                        return 0;
+                }
+                if (! get_value(".", "dev", value, sizeof(value))) {
+                        pr2serr("Couldn't find sysfs generic dev\n");
+                        return 1;
+                }
+                if (verbose)
+                        printf("matching dev: %s\n", value);
+                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
+                        pr2serr("Couldn't decode mapped dev\n");
+                        return 1;
+                }
+                num = list_matching_nodes(device_dir, FT_CHAR, m_ma, m_mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        } else {
+                pr2serr("sch device: %s does not match any SCSI generic "
+                        "device\n", device_name);
+                pr2serr("    perhaps sg module is not loaded\n");
+                return 1;
+        }
+}
+
+static int
+map_sg(const char * device_name, const char * device_dir, int ma, int mi,
+       int result, bool follow_symlink, int verbose)
+{
+        int m_mi, m_ma, num;
+        char value[D_NAME_LEN_MAX];
+        char name[D_NAME_LEN_MAX];
+
+        if (2 == result) {
+                num = list_matching_nodes(device_dir, FT_CHAR, ma, mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        }
+        snprintf(name, sizeof(name), "%ssg%d", sys_sg_dir, mi);
+        if (3 == result) {
+                printf("%s\n", name);
+                return 0;
+        }
+        if (! get_value(name, "dev", value, sizeof(value))) {
+                pr2serr("Couldn't find sysfs match for device: %s\n",
+                        device_name);
+                return 1;
+        }
+        if (verbose)
+                pr2serr("sysfs sg dev: %s\n", value);
+        if (! if_directory_chdir(name, "device")) {
+                pr2serr("sysfs problem with device: %s\n", device_name);
+                return 1;
+        }
+        if ((1 == from_sg_scan(".", verbose)) &&
+            (if_directory_chdir(".", from_sg.name))) {
+                if (DT_DIR == from_sg.d_type) {
+                        if ((1 == scan_for_first(".", verbose)) &&
+                            (if_directory_chdir(".", for_first.name))) {
+                                ;
+                        } else {
+                                pr2serr("unexpected scan_for_first error\n");
+                        }
+                }
+                if (1 == result) {
+                        if (NULL == getcwd(value, sizeof(value)))
+                                value[0] = '\0';
+                        printf("%s\n", value);
+                        return 0;
+                }
+                if (! get_value(".", "dev", value, sizeof(value))) {
+                        pr2serr("Couldn't find sysfs block dev\n");
+                        return 1;
+                }
+                if (verbose)
+                        printf("matching dev: %s\n", value);
+                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
+                        pr2serr("Couldn't decode mapped dev\n");
+                        return 1;
+                }
+                num = list_matching_nodes(device_dir, from_sg.ft, m_ma, m_mi,
+                                          follow_symlink, verbose);
+                return (num > 0) ? 0 : 1;
+        } else {
+                pr2serr("sg device: %s does not match any other SCSI "
+                        "device\n", device_name);
+                return 1;
+        }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+        bool cont;
+        int c, num, tt, res;
+        int given_is = -1;
+        int result = 0;
+        int verbose = 0;
+        int ret = 1;
+        int ma, mi;
+        bool do_dev_dir = false;
+        bool follow_symlink = false;
+        char device_name[D_NAME_LEN_MAX];
+        char device_dir[D_NAME_LEN_MAX];
+        char value[D_NAME_LEN_MAX];
+
+        memset(device_name, 0, sizeof(device_name));
+        memset(device_dir, 0, sizeof(device_dir));
+        while (1) {
+                int option_index = 0;
+
+                c = getopt_long(argc, argv, "d:hg:r:svV", long_options,
+                                &option_index);
+                if (c == -1)
+                        break;
+
+                switch (c) {
+                case 'd':
+                        strncpy(device_dir, optarg, sizeof(device_dir) - 1);
+                        do_dev_dir = true;
+                        break;
+                case 'g':
+                        num = sscanf(optarg, "%d", &res);
+                        if ((1 == num) && ((0 == res) || (1 == res)))
+                                given_is = res;
+                        else {
+                                pr2serr("value for '--given_to=' must be 0 "
+                                        "or 1\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'h':
+                case '?':
+                        usage();
+                        return 0;
+                case 'r':
+                        num = sscanf(optarg, "%d", &res);
+                        if ((1 == num) && (res >= 0) && (res < 4))
+                                result = res;
+                        else {
+                                pr2serr("value for '--result=' must be "
+                                        "0..3\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 's':
+                        follow_symlink = true;
+                        break;
+                case 'v':
+                        ++verbose;
+                        break;
+                case 'V':
+                        pr2serr(ME "version: %s\n", version_str);
+                        return 0;
+                default:
+                        pr2serr("unrecognised option code 0x%x ??\n", c);
+                        usage();
+                        return SG_LIB_SYNTAX_ERROR;
+                }
+        }
+        if (optind < argc) {
+                if ('\0' == device_name[0]) {
+                        strncpy(device_name, argv[optind],
+                                sizeof(device_name) - 1);
+                        device_name[sizeof(device_name) - 1] = '\0';
+                        ++optind;
+                }
+                if (optind < argc) {
+                        for (; optind < argc; ++optind)
+                                pr2serr("Unexpected extra argument: %s\n",
+                                        argv[optind]);
+                        usage();
+                        return SG_LIB_SYNTAX_ERROR;
+                }
+        }
+
+        if (0 == device_name[0]) {
+                pr2serr("missing device name!\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+
+        ma = 0;
+        mi = 0;
+        if (do_dev_dir) {
+                if (if_directory_chdir(".", device_dir)) {
+                        if (getcwd(device_dir, sizeof(device_dir)))
+                                device_dir[sizeof(device_dir) - 1] = '\0';
+                        else
+                                device_dir[0] = '\0';
+                        if (verbose > 1)
+                                pr2serr("Absolute path to dev_dir: %s\n",
+                                        device_dir);
+                } else {
+                        pr2serr("dev_dir: %s invalid\n", device_dir);
+                        return SG_LIB_FILE_ERROR;
+                }
+        } else {
+                strcpy(device_dir, device_name);
+                dirname(device_dir);
+                if (0 == strcmp(device_dir, device_name)) {
+                        if (NULL == getcwd(device_dir, sizeof(device_dir)))
+                                device_dir[0] = '\0';
+                }
+        }
+        ret = nt_typ_from_filename(device_name, &ma, &mi);
+        if (ret < 0) {
+                pr2serr("stat failed on %s: %s\n", device_name,
+                        ssafe_strerror(-ret));
+                return SG_LIB_FILE_ERROR;
+        }
+        if (verbose)
+                pr2serr(" %s: %s device [maj=%d, min=%d]\n", device_name,
+                        nt_names[ret], ma, mi);
+        res = 0;
+        switch (ret) {
+        case NT_SD:
+        case NT_SR:
+        case NT_HD:
+                if (given_is > 0) {
+                        pr2serr("block special but '--given_is=' suggested "
+                                "sysfs device\n");
+                        return SG_LIB_FILE_ERROR;
+                }
+                break;
+        case NT_ST:
+        case NT_OSST:
+        case NT_CH:
+        case NT_SG:
+                if (given_is > 0) {
+                        pr2serr("character special but '--given_is=' "
+                                "suggested sysfs device\n");
+                        return SG_LIB_FILE_ERROR;
+                }
+                break;
+        case NT_REG:
+                if (0 == given_is) {
+                        pr2serr("regular file but '--given_is=' suggested "
+                                "block or char special\n");
+                        return SG_LIB_FILE_ERROR;
+                }
+                strcpy(device_dir, def_dev_dir);
+                break;
+        case NT_DIR:
+                if (0 == given_is) {
+                        pr2serr("directory but '--given_is=' suggested "
+                                "block or char special\n");
+                        return SG_LIB_FILE_ERROR;
+                }
+                strcpy(device_dir, def_dev_dir);
+                break;
+        default:
+                break;
+        }
+
+        tt = NT_NO_MATCH;
+        do {
+                cont = false;
+                switch (ret) {
+                case NT_NO_MATCH:
+                        res = 1;
+                        break;
+                case NT_SD:
+                        res = map_sd(device_name, device_dir, ma, mi, result,
+                                     follow_symlink, verbose);
+                        break;
+                case NT_SR:
+                        res = map_sr(device_name, device_dir, ma, mi, result,
+                                     follow_symlink, verbose);
+                        break;
+                case NT_HD:
+                        if (result < 2) {
+                                pr2serr("a hd device does not map to a sg "
+                                        "device\n");
+                                return SG_LIB_FILE_ERROR;
+                        }
+                        res = map_hd(device_dir, ma, mi, result,
+                                     follow_symlink, verbose);
+                        break;
+                case NT_ST:
+                        res = map_st(device_name, device_dir, ma, mi, result,
+                                     follow_symlink, verbose);
+                        break;
+                case NT_OSST:
+                        res = map_osst(device_name, device_dir, ma, mi,
+                                       result, follow_symlink, verbose);
+                        break;
+                case NT_CH:
+                        res = map_ch(device_name, device_dir, ma, mi, result,
+                                     follow_symlink, verbose);
+                        break;
+                case NT_SG:
+                        res = map_sg(device_name, device_dir, ma, mi, result,
+                                     follow_symlink, verbose);
+                        break;
+                case NT_REG:
+                        if (! get_value(NULL, device_name, value,
+                                        sizeof(value))) {
+                                pr2serr("Couldn't fetch value from: %s\n",
+                                        device_name);
+                                return SG_LIB_FILE_ERROR;
+                        }
+                        if (verbose)
+                                pr2serr("value: %s\n", value);
+                        if (2 != sscanf(value, "%d:%d", &ma, &mi)) {
+                                pr2serr("Couldn't decode value\n");
+                                return SG_LIB_FILE_ERROR;
+                        }
+                        tt = nt_typ_from_major(ma);
+                        cont = true;
+                        break;
+                case NT_DIR:
+                        if (! get_value(device_name, "dev", value,
+                                        sizeof(value))) {
+                                pr2serr("Couldn't fetch value from: %s/dev\n",
+                                        device_name);
+                                return SG_LIB_FILE_ERROR;
+                        }
+                        if (verbose)
+                                pr2serr("value: %s\n", value);
+                        if (2 != sscanf(value, "%d:%d", &ma, &mi)) {
+                                pr2serr("Couldn't decode value\n");
+                                return SG_LIB_FILE_ERROR;
+                        }
+                        tt = nt_typ_from_major(ma);
+                        cont = true;
+                        break;
+                default:
+                        break;
+                }
+                ret = tt;
+        } while (cont);
+        return res;
+}
diff --git a/src/sg_modes.c b/src/sg_modes.c
new file mode 100644
index 0000000..0be40ee
--- /dev/null
+++ b/src/sg_modes.c
@@ -0,0 +1,1579 @@
+/*
+ *  Copyright (C) 2000-2022 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ *  This program outputs information provided by a SCSI MODE SENSE command.
+ *  Does 10 byte MODE SENSE commands by default, Trent Piepho added a "-6"
+ *  switch for force 6 byte mode sense commands.
+ *  This utility cannot modify mode pages. See the sdparm utility for that.
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.75 20220202";
+
+#define DEF_ALLOC_LEN (1024 * 4)
+#define DEF_6_ALLOC_LEN 252
+#define UNLIKELY_ABOVE_LEN 512
+#define PG_CODE_ALL 0x3f
+#define PG_CODE_MASK 0x3f
+#define PG_CODE_MAX 0x3f
+#define SPG_CODE_ALL 0xff
+#define PROTO_SPECIFIC_1 0x18
+#define PROTO_SPECIFIC_2 0x19
+
+#define EBUFF_SZ 256
+
+
+struct opts_t {
+    bool do_dbd;
+    bool do_dbout;
+    bool do_examine;
+    bool do_flexible;
+    bool do_list;
+    bool do_llbaa;
+    bool do_six;
+    bool o_readwrite;
+    bool subpg_code_given;
+    bool opt_new;
+    bool verbose_given;
+    bool version_given;
+    int do_all;
+    int do_help;
+    int do_hex;
+    int maxlen;
+    int do_raw;
+    int verbose;
+    int page_control;
+    int pg_code;
+    int subpg_code;
+    const char * device_name;
+    const char * page_acron;
+};
+
+struct page_code_desc {
+    int page_code;
+    int subpage_code;
+    const char * acron;
+    const char * desc;
+};
+
+struct pc_desc_group {
+    struct page_code_desc * pcdp;
+    const char * group_name;
+};
+
+static struct option long_options[] = {
+        {"all", no_argument, 0, 'a'},
+        {"control", required_argument, 0, 'c'},
+        {"dbd", no_argument, 0, 'd'},
+        {"dbout", no_argument, 0, 'D'},
+        {"examine", no_argument, 0, 'e'},
+        {"flexible", no_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"list", no_argument, 0, 'l'},
+        {"llbaa", no_argument, 0, 'L'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"new", no_argument, 0, 'N'},
+        {"old", no_argument, 0, 'O'},
+        {"page", required_argument, 0, 'p'},
+        {"raw", no_argument, 0, 'r'},
+        {"read-write", no_argument, 0, 'w'},
+        {"read_write", no_argument, 0, 'w'},
+        {"readwrite", no_argument, 0, 'w'},
+        {"six", no_argument, 0, '6'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+/* Common to all SCSI devices (found in SPCx). In numerical order */
+static struct page_code_desc pc_desc_common[] = {
+    {0x0, 0x0, "ua", "Unit Attention condition [vendor specific format]"},
+    {0x2, 0x0, "dr", "Disconnect-Reconnect"},
+    {0x9, 0x0, "pd", "Peripheral device (obsolete)"},
+    {0xa, 0x0, "co", "Control"},
+    {0xa, 0x1, "coe", "Control extension"},
+    {0xa, 0x3, "cdla", "Command duration limit A"},
+    {0xa, 0x4, "cdlb", "Command duration limit B"},
+    {0xa, 0x7, "cdt2a", "Command duration limit T2A"},  /* spc6r01 */
+    {0xa, 0x8, "cdt2b", "Command duration limit T2B"},  /* spc6r01 */
+    {0x15, 0x0, "ext_", "Extended"},
+    {0x16, 0x0, "edts", "Extended device-type specific"},
+    {0x18, 0x0, "pslu", "Protocol specific lu"},
+    {0x19, 0x0, "pspo", "Protocol specific port"},
+    {0x1a, 0x0, "po", "Power condition"},
+    {0x1a, 0x1, "ps", "Power consumption"},
+    {0x1c, 0x0, "ie", "Informational exceptions control"},
+    {PG_CODE_ALL, 0x0, "asmp", "[yields all supported pages]"},
+    {PG_CODE_ALL, SPG_CODE_ALL,"asmsp",
+        "[yields all supported pages and subpages]"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_disk[] = {
+    {0x1, 0x0, "rw", "Read-Write error recovery"},
+    {0x3, 0x0, "fo", "Format (obsolete)"},
+    {0x4, 0x0, "rd", "Rigid disk geometry (obsolete)"},
+    {0x5, 0x0, "fd", "Flexible disk (obsolete)"},
+    {0x7, 0x0, "ve", "Verify error recovery"},
+    {0x8, 0x0, "ca", "Caching"},
+    {0xa, 0x2, "atag", "Application tag"},
+    {0xa, 0x5, "ioad", "IO advice hints grouping"}, /* added sbc4r06 */
+    {0xa, 0x6, "bop", "Background operation control"}, /* added sbc4r07 */
+    {0xa, 0xf1, "pat", "Parallel ATA control (SAT)"},
+    {0xa, 0xf2, "afc", "ATA feature control (SAT)"},   /* added 20-085r2 */
+    {0xb, 0x0, "mts", "Medium types supported (obsolete)"},
+    {0xc, 0x0, "not", "Notch and partition (obsolete)"},
+    {0xd, 0x0, "pco", "Power condition (obsolete, moved to 0x1a)"},
+    {0x10, 0x0, "xo", "XOR control"}, /* obsolete in sbc3r32 */
+    {0x1a, 0xf1, "apo", "ATA Power condition"},
+    {0x1c, 0x1, "bc", "Background control"},
+    {0x1c, 0x2, "lbp", "Logical block provisioning"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_tape[] = {
+    {0x1, 0x0, "rw", "Read-Write error recovery"},
+    {0xa, 0xf0, "cdp", "Control data protection"},
+    {0xf, 0x0, "dac", "Data Compression"},
+    {0x10, 0x0, "dc", "Device configuration"},
+    {0x10, 0x1, "dcs", "Device configuration extension"},
+    {0x11, 0x0, "mpa", "Medium Partition [1]"},
+    {0x12, 0x0, "mpa2", "Medium Partition [2]"},
+    {0x13, 0x0, "mpa3", "Medium Partition [3]"},
+    {0x14, 0x0, "mpar", "Medium Partition [4]"},
+    {0x1c, 0x0, "ie", "Informational exceptions control (tape version)"},
+    {0x1d, 0x0, "mco", "Medium configuration"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_cddvd[] = {
+    {0x1, 0x0, "rw", "Read-Write error recovery"},
+    {0x3, 0x0, "mrw", "Mount Rainer rewritable"},
+    {0x5, 0x0, "wp", "Write parameters"},
+    {0x7, 0x0, "ve", "Verify error recovery"},
+    {0x8, 0x0, "ca", "Caching"},
+    {0xd, 0x0, "cddp", "CD device parameters (obsolete)"},
+    {0xe, 0x0, "cda", "CD audio"},
+    {0x1a, 0x0, "po", "Power condition (mmc)"},
+    {0x1c, 0x0, "ffrc", "Fault/failure reporting control (mmc)"},
+    {0x1d, 0x0, "tp", "Timeout and protect"},
+    {0x2a, 0x0, "cms", "MM capabilities and mechanical status (obsolete)"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_smc[] = {
+    {0x1d, 0x0, "eaa", "Element address assignment"},
+    {0x1e, 0x0, "tgp", "Transport geometry parameters"},
+    {0x1f, 0x0, "dcs", "Device capabilities"},
+    {0x1f, 0x41, "edc", "Extended device capabilities"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_scc[] = {
+    {0x1b, 0x0, "sslm", "LUN mapping"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_ses[] = {
+    {0x14, 0x0, "esm", "Enclosure services management"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_rbc[] = {
+    {0x6, 0x0, "rbc", "RBC device parameters"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_adc[] = {
+    /* {0xe, 0x0, "ADC device configuration"}, */
+    {0xe, 0x1, "adtd", "Target device"},
+    {0xe, 0x2, "addp", "DT device primary port"},
+    {0xe, 0x3, "adlu", "Logical unit"},
+    {0xe, 0x4, "adts", "Target device serial number"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+
+/* Transport reated mode pages */
+static struct page_code_desc pc_desc_t_fcp[] = {
+    {0x18, 0x0, "pl", "LU control"},
+    {0x19, 0x0, "pp", "Port control"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_t_spi4[] = {
+    {0x18, 0x0, "luc", "LU control"},
+    {0x19, 0x0, "pp", "Port control short format"},
+    {0x19, 0x1, "mc", "Margin control"},
+    {0x19, 0x2, "stc", "Saved training configuration value"},
+    {0x19, 0x3, "ns", "Negotiated settings"},
+    {0x19, 0x4, "rtc", "Report transfer capabilities"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+/* SAS protocol layers now in SPL standards */
+static struct page_code_desc pc_desc_t_sas[] = {
+    {0x18, 0x0, "pslu", "Protocol specific logical unit (SPL)"},
+    {0x19, 0x0, "pspo", "Protocol specific port (SPL)"},
+    {0x19, 0x1, "pcd", "Phy control and discover (SPL)"},
+    {0x19, 0x2, "spc", "Shared port control (SPL)"},
+    {0x19, 0x3, "sep", "Enhanced phy control (SPL)"},
+    {0x19, 0x4, "oobm", "Out of band management control (SPL)"}, /* spl5r01 */
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_t_adc[] = {
+    {0xe, 0x1, "addt", "Target device"},
+    {0xe, 0x2, "addp", "DT device primary port"},
+    {0xe, 0x3, "adlu", "Logical unit"},
+    {0x18, 0x0, "pslu", "Protocol specific lu"},
+    {0x19, 0x0, "pspo", "Protocol specific port"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+static struct page_code_desc pc_desc_zbc[] = {
+    {0x1, 0x0, "rw", "Read-Write error recovery"},
+    {0x7, 0x0, "ve", "Verify error recovery"},
+    {0x8, 0x0, "ca", "Caching"},
+    {0xa, 0x2, "atag", "Application tag"},
+    {0xa, 0xf, "zbdct", "Zoned block device control"},  /* zbc2r04a */
+    {0x1c, 0x1, "bc", "Background control"},
+    {0x0, 0x0, NULL, NULL},
+};
+
+struct pc_desc_group pcd_gr_arr[] = {
+    {pc_desc_common, "common"},
+    {pc_desc_disk, "disk"},
+    {pc_desc_tape, "tape"},
+    {pc_desc_cddvd, "cd/dvd"},
+    {pc_desc_smc, "media changer"},
+    {pc_desc_scc, "scsi controller"},
+    {pc_desc_ses, "enclosure"},
+    {pc_desc_rbc, "reduced block"},
+    {pc_desc_adc, "adc"},
+    {pc_desc_zbc, "zbc"},
+    {pc_desc_t_fcp, "transport: FCP"},
+    {pc_desc_t_spi4, "transport: SPI"},
+    {pc_desc_t_sas, "transport: SAS"},
+    {pc_desc_t_adc, "transport: ADC"},
+
+    {NULL, NULL},
+};
+
+
+
+static void
+usage()
+{
+    printf("Usage: sg_modes [--all] [--control=PC] [--dbd] [--dbout] "
+           "[--examine]\n"
+           "                [--flexible] [--help] [--hex] [--list] "
+           "[--llbaa]\n"
+           "                [--maxlen=LEN] [--page=PG[,SPG]] [--raw] [-R] "
+           "[--readwrite]\n"
+           "                [--six] [--verbose] [--version] [DEVICE]\n"
+           "  where:\n"
+           "    --all|-a        get all mode pages supported by device\n"
+           "                    use twice to get all mode pages and subpages\n"
+           "    --control=PC|-c PC    page control (default: 0)\n"
+           "                       0: current, 1: changeable,\n"
+           "                       2: (manufacturer's) defaults, 3: saved\n"
+           "    --dbd|-d        disable block descriptors (DBD field in cdb)\n"
+           "    --dbout|-D      disable block descriptor output\n"
+           "    --examine|-e    examine pages # 0 through to 0x3e, note if "
+           "found\n"
+           "    --flexible|-f    be flexible, cope with MODE SENSE 6/10 "
+           "response mixup\n");
+    printf("    --help|-h       print usage message then exit\n"
+           "    --hex|-H        output full response in hex\n"
+           "                    use twice to output page number and header "
+           "in hex\n"
+           "    --list|-l       list common page codes for device peripheral "
+           "type,\n"
+           "                    if no device given then assume disk type\n"
+           "    --llbaa|-L      set Long LBA Accepted (LLBAA field in mode "
+           "sense (10) cdb)\n"
+           "    --maxlen=LEN|-m LEN    max response length (allocation "
+           "length in cdb)\n"
+           "                           (def: 0 -> 4096 or 252 (for MODE "
+           "SENSE 6) bytes)\n"
+           "    --page=PG|-p PG    page code to fetch (def: 63). May be "
+           "acronym\n"
+           "    --page=PG,SPG|-p PG,SPG\n"
+           "                       page code and subpage code to fetch "
+           "(defs: 63,0)\n"
+           "    --raw|-r        output response in binary to stdout\n"
+           "    -R              mode page response to stdout, a byte per "
+           "line in ASCII\n"
+           "                    hex (same result as '--raw --raw')\n"
+           "    --readwrite|-w    open DEVICE read-write (def: open "
+           "read-only)\n"
+           "    --six|-6|-s     use MODE SENSE(6), by default uses MODE "
+           "SENSE(10)\n"
+           "    --verbose|-v    increase verbosity\n"
+           "    --old|-O        use old interface (use as first option)\n"
+           "    --version|-V    output version string then exit\n\n"
+           "Performs a SCSI MODE SENSE (10 or 6) command. To access and "
+           "possibly change\nmode page fields see the sdparm utility.\n");
+}
+
+static void
+usage_old()
+{
+    printf("Usage:  sg_modes [-a] [-A] [-c=PC] [-d] [-D] [-e] [-f] [-h] "
+           "[-H] [-l] [-L]\n"
+           "                 [-m=LEN] [-p=PG[,SPG]] [-r] [-subp=SPG] [-v] "
+           "[-V] [-6]\n"
+           "                 [DEVICE]\n"
+           " where:\n"
+           "   -a    get all mode pages supported by device\n"
+           "   -A    get all mode pages and subpages supported by device\n"
+           "   -c=PC    page control (def: 0 [current],"
+           " 1 [changeable],\n"
+           "                               2 [default], 3 [saved])\n"
+           "   -d    disable block descriptors (DBD field in cdb)\n"
+           "   -D    disable block descriptor output\n"
+           "   -e    examine pages # 0 through to 0x3e, note if found\n"
+           "   -f    be flexible, cope with MODE SENSE 6/10 response "
+           "mixup\n");
+    printf("   -h    output page number and header in hex\n"
+           "   -H    output page number and header in hex (same as '-h')\n"
+           "   -l    list common page codes for device peripheral type,\n"
+           "         if no device given then assume disk type\n"
+           "   -L    set Long LBA Accepted (LLBAA field in mode sense "
+           "10 cdb)\n"
+           "   -m=LEN    max response length (allocation length in cdb)\n"
+           "             (def: 0 -> 4096 or 252 (for MODE SENSE 6) bytes)\n"
+           "   -p=PG     page code in hex (def: 3f). No acronym allowed\n"
+           "   -p=PG,SPG    both in hex, (defs: 3f,0)\n"
+           "   -r    mode page output to stdout, a byte per line in "
+           "ASCII hex\n"
+           "   -subp=SPG    sub page code in hex (def: 0)\n"
+           "   -v    verbose\n"
+           "   -V    output version string\n"
+           "   -6    Use MODE SENSE(6), by default uses MODE SENSE(10)\n"
+           "   -N|--new     use new interface\n"
+           "   -?    output this usage message\n\n"
+           "Performs a SCSI MODE SENSE (10 or 6) command\n");
+}
+
+static void
+enum_pc_desc(void)
+{
+    bool first = true;
+    const struct pc_desc_group * pcd_grp = pcd_gr_arr;
+    char b[128];
+
+    for ( ; pcd_grp->pcdp; ++pcd_grp) {
+        const struct page_code_desc * pcdp = pcd_grp->pcdp;
+
+        if (first)
+            first = false;
+        else
+            printf("\n");
+        printf("Mode pages group: %s:\n", pcd_grp->group_name);
+        for ( ; pcdp->acron; ++pcdp) {
+            if (pcdp->subpage_code > 0)
+                snprintf(b, sizeof(b), "[0x%x,0x%x]", pcdp->page_code,
+                         pcdp->subpage_code);
+            else
+                snprintf(b, sizeof(b), "[0x%x]", pcdp->page_code);
+            printf("  %s: %s  %s\n", pcdp->acron, pcdp->desc, b);
+        }
+    }
+}
+
+static const struct page_code_desc *
+find_pc_desc(const char * acron)
+{
+    const struct pc_desc_group * pcd_grp = pcd_gr_arr;
+
+    for ( ; pcd_grp->pcdp; ++pcd_grp) {
+        const struct page_code_desc * pcdp = pcd_grp->pcdp;
+
+        for ( ; pcdp->acron; ++pcdp) {
+            if (0 == strcmp(acron, pcdp->acron))
+                return pcdp;
+        }
+    }
+    return NULL;
+}
+
+static void
+usage_for(const struct opts_t * op)
+{
+    if (op->opt_new)
+        usage();
+    else
+        usage_old();
+}
+
+/* Processes command line options according to new option format. Returns
+ * 0 is ok, else SG_LIB_SYNTAX_ERROR is returned. */
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int c, n, nn;
+    char * cp;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "6aAc:dDefhHlLm:NOp:rRsvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case '6':
+            op->do_six = true;
+            break;
+        case 'a':
+            ++op->do_all;
+            break;
+        case 'A':
+            op->do_all += 2;
+            break;
+        case 'c':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 3)) {
+                pr2serr("bad argument to '--control='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->page_control = n;
+            break;
+        case 'd':
+            op->do_dbd = true;
+            break;
+        case 'D':
+            op->do_dbout = true;
+            break;
+        case 'e':
+            op->do_examine = true;
+            break;
+        case 'f':
+            op->do_flexible = true;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'l':
+            op->do_list = true;
+            break;
+        case 'L':
+            op->do_llbaa = true;
+            break;
+        case 'm':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 65535)) {
+                pr2serr("bad argument to '--maxlen='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((n > 0) && (n < 4)) {
+                pr2serr("Changing that '--maxlen=' value to 4\n");
+                n = 4;
+            }
+            op->maxlen = n;
+            break;
+        case 'N':
+            break;      /* ignore */
+        case 'O':
+            op->opt_new = false;
+            return 0;
+        case 'p':
+            if (isalpha((uint8_t)optarg[0])) {
+                const struct page_code_desc * pcdp;
+
+                op->page_acron = optarg;
+                if (0 == memcmp("xxx", optarg, 3)) {
+                    enum_pc_desc();
+                    return SG_LIB_OK_FALSE;     /* for quick exit */
+                }
+                pcdp = find_pc_desc(optarg);
+                if (pcdp) {
+                    if (pcdp->subpage_code > 0) {
+                        op->subpg_code = pcdp->subpage_code;
+                        op->subpg_code_given = true;
+                    }
+                    op->pg_code = pcdp->page_code;
+                } else {
+                    pr2serr(" Couldn't match acronym '%s', try '-p xxx' for "
+                            "list\n", optarg);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else {
+                cp = strchr(optarg, ',');
+                n = sg_get_num_nomult(optarg);
+                if ((n < 0) || (n > 63)) {
+                    pr2serr("Bad argument to '--page='\n");
+                    usage();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                if (cp) {
+                    nn = sg_get_num_nomult(cp + 1);
+                    if ((nn < 0) || (nn > 255)) {
+                        pr2serr("Bad second value in argument to "
+                                "'--page='\n");
+                        usage();
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    op->subpg_code = nn;
+                    op->subpg_code_given = true;
+                }
+                op->pg_code = n;
+            }
+            break;
+        case 'r':
+            ++op->do_raw;
+            break;
+        case 'R':
+            op->do_raw += 2;
+            break;
+        case 's':
+            op->do_six = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'w':
+            op->o_readwrite = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+/* Processes command line options according to old option format. Returns
+ * 0 is ok, else SG_LIB_SYNTAX_ERROR is returned. */
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool jmp_out;
+    int k, plen, num, n;
+    char pc1;
+    unsigned int u, uu;
+    const char * cp;
+
+    for (k = 1; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case '6':
+                    op->do_six = true;
+                    break;
+                case 'a':
+                    ++op->do_all;
+                    break;
+                case 'A':
+                    op->do_all += 2;
+                    break;
+                case 'd':
+                    op->do_dbd = true;
+                    break;
+                case 'D':
+                    op->do_dbout = true;
+                    break;
+                case 'e':
+                    op->do_examine = true;
+                    break;
+                case 'f':
+                    op->do_flexible = true;
+                    break;
+                case 'h':
+                case 'H':
+                    op->do_hex += 2;
+                    break;
+                case 'l':
+                    op->do_list = true;
+                    break;
+                case 'L':
+                    op->do_llbaa = true;
+                    break;
+                case 'N':
+                    op->opt_new = true;
+                    return 0;
+                case 'O':
+                    break;
+                case 'r':
+                    op->do_raw += 2;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                case '?':
+                    ++op->do_help;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            if (0 == strncmp("c=", cp, 2)) {
+                num = sscanf(cp + 2, "%x", &u);
+                if ((1 != num) || (u > 3)) {
+                    pr2serr("Bad page control after 'c=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->page_control = u;
+            } else if (0 == strncmp("m=", cp, 2)) {
+                num = sscanf(cp + 2, "%d", &n);
+                if ((1 != num) || (n < 0) || (n > 65535)) {
+                    pr2serr("Bad argument after 'm=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                if ((n > 0) && (n < 4)) {
+                    pr2serr("Changing that '-m=' value to 4\n");
+                    n = 4;
+                }
+                op->maxlen = n;
+            } else if (0 == strncmp("p=", cp, 2)) {
+                pc1 = *(cp + 2);
+                if (isalpha(pc1) && ((islower(pc1) && (pc1 > 'f')) ||
+                                     (isupper(pc1) && (pc1 > 'F')))) {
+                    pr2serr("Old format doesn't accept mode page acronyms: "
+                            "%s\n", cp + 2);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                if (NULL == strchr(cp + 2, ',')) {
+                    num = sscanf(cp + 2, "%x", &u);
+                    if ((1 != num) || (u > 63)) {
+                        pr2serr("Bad page code value after 'p=' option\n");
+                        usage_old();
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    op->pg_code = u;
+                } else if (2 == sscanf(cp + 2, "%x,%x", &u, &uu)) {
+                    if (uu > 255) {
+                        pr2serr("Bad subpage code value after 'p=' option\n");
+                        usage_old();
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    op->pg_code = u;
+                    op->subpg_code = uu;
+                    op->subpg_code_given = true;
+                } else {
+                    pr2serr("Bad page code, subpage code sequence after 'p=' "
+                            "option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else if (0 == strncmp("subp=", cp, 5)) {
+                num = sscanf(cp + 5, "%x", &u);
+                if ((1 != num) || (u > 255)) {
+                    pr2serr("Bad sub page code after 'subp=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->subpg_code = u;
+                op->subpg_code_given = true;
+                if (-1 == op->pg_code)
+                    op->pg_code = 0;
+            } else if (0 == strncmp("-old", cp, 4))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage_old();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == op->device_name)
+            op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not expecting: %s\n",
+                    op->device_name, cp);
+            usage_old();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+/* Process command line options. First check using new option format unless
+ * the SG3_UTILS_OLD_OPTS environment variable is defined which causes the
+ * old option format to be checked first. Both new and old format can be
+ * countermanded by a '-O' and '-N' options respectively. As soon as either
+ * of these options is detected (when processing the other format), processing
+ * stops and is restarted using the other format. Clear? */
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opt_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opt_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opt_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (! op->opt_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* Note to coverity: this function is safe as long as the page_code_desc
+ * objects pointed to by pcdp have a sentinel object at the end of each
+ * array. And they do by design.*/
+static int
+count_desc_elems(const struct page_code_desc * pcdp)
+{
+    int k;
+
+    for (k = 0; k < 1024; ++k, ++pcdp) {
+        if (NULL == pcdp->acron)
+            return k;
+    }
+    pr2serr("%s: sanity check trip, invalid pc_desc table\n", __func__);
+    return k;
+}
+
+/* Returns pointer to base of table for scsi_ptype or pointer to common
+ * table if scsi_ptype is -1. Yields numbers of elements in returned
+ * table via pointer sizep. If scsi_ptype not known then returns NULL
+ * with *sizep set to zero. */
+static struct page_code_desc *
+get_mpage_tbl_size(int scsi_ptype, int * sizep)
+{
+    switch (scsi_ptype)
+    {
+        case -1:        /* common list */
+            *sizep = count_desc_elems(pc_desc_common);
+            return &pc_desc_common[0];
+        case PDT_DISK:         /* disk (direct access) type devices */
+        case PDT_WO:
+        case PDT_OPTICAL:
+            *sizep = count_desc_elems(pc_desc_disk);
+            return &pc_desc_disk[0];
+        case PDT_TAPE:         /* tape devices */
+        case PDT_PRINTER:
+            *sizep = count_desc_elems(pc_desc_tape);
+            return &pc_desc_tape[0];
+        case PDT_MMC:         /* cd/dvd/bd devices */
+            *sizep = count_desc_elems(pc_desc_cddvd);
+            return &pc_desc_cddvd[0];
+        case PDT_MCHANGER:         /* medium changer devices */
+            *sizep = count_desc_elems(pc_desc_smc);
+            return &pc_desc_smc[0];
+        case PDT_SAC:       /* storage array devices */
+            *sizep = count_desc_elems(pc_desc_scc);
+            return &pc_desc_scc[0];
+        case PDT_SES:       /* enclosure services devices */
+            *sizep = count_desc_elems(pc_desc_ses);
+            return &pc_desc_ses[0];
+        case PDT_RBC:       /* simplified direct access device */
+            *sizep = count_desc_elems(pc_desc_rbc);
+            return &pc_desc_rbc[0];
+        case PDT_ADC:       /* automation device/interface */
+            *sizep = count_desc_elems(pc_desc_adc);
+            return &pc_desc_adc[0];
+        case PDT_ZBC:
+            *sizep = count_desc_elems(pc_desc_zbc);
+            return &pc_desc_zbc[0];
+    }
+    *sizep = 0;
+    return NULL;
+}
+
+
+static struct page_code_desc *
+get_mpage_trans_tbl_size(int t_proto, int * sizep)
+{
+    switch (t_proto)
+    {
+        case TPROTO_FCP:
+            *sizep = count_desc_elems(pc_desc_t_fcp);
+            return &pc_desc_t_fcp[0];
+        case TPROTO_SPI:
+            *sizep = count_desc_elems(pc_desc_t_spi4);
+            return &pc_desc_t_spi4[0];
+        case TPROTO_SAS:
+            *sizep = count_desc_elems(pc_desc_t_sas);
+            return &pc_desc_t_sas[0];
+        case TPROTO_ADT:
+            *sizep = count_desc_elems(pc_desc_t_adc);
+            return &pc_desc_t_adc[0];
+    }
+    *sizep = 0;
+    return NULL;
+}
+
+static const char *
+find_page_code_desc(int page_num, int subpage_num, int scsi_ptype,
+                    bool encserv, bool mchngr, int t_proto)
+{
+    int k, num, decayed_pdt;
+    const struct page_code_desc * pcdp;
+
+    if (t_proto >= 0) {
+        pcdp = get_mpage_trans_tbl_size(t_proto, &num);
+        if (pcdp) {
+            for (k = 0; k < num; ++k, ++pcdp) {
+                if ((page_num == pcdp->page_code) &&
+                    (subpage_num == pcdp->subpage_code))
+                    return pcdp->desc;
+                else if (page_num < pcdp->page_code)
+                    break;
+            }
+        }
+    }
+try_again:
+    pcdp = get_mpage_tbl_size(scsi_ptype, &num);
+    if (pcdp) {
+        for (k = 0; k < num; ++k, ++pcdp) {
+            if ((page_num == pcdp->page_code) &&
+                (subpage_num == pcdp->subpage_code))
+                return pcdp->desc;
+            else if (page_num < pcdp->page_code)
+                break;
+        }
+    }
+    decayed_pdt = sg_lib_pdt_decay(scsi_ptype);
+    if (decayed_pdt != scsi_ptype) {
+        scsi_ptype = decayed_pdt;
+        goto try_again;
+    }
+    if ((0xd != scsi_ptype) && encserv) {
+        /* check for attached enclosure services processor */
+        pcdp = get_mpage_tbl_size(0xd, &num);
+        if (pcdp) {
+            for (k = 0; k < num; ++k, ++pcdp) {
+                if ((page_num == pcdp->page_code) &&
+                    (subpage_num == pcdp->subpage_code))
+                    return pcdp->desc;
+                else if (page_num < pcdp->page_code)
+                    break;
+            }
+        }
+    }
+    if ((0x8 != scsi_ptype) && mchngr) {
+        /* check for attached medium changer device */
+        pcdp = get_mpage_tbl_size(0x8, &num);
+        if (pcdp) {
+            for (k = 0; k < num; ++k, ++pcdp) {
+                if ((page_num == pcdp->page_code) &&
+                    (subpage_num == pcdp->subpage_code))
+                    return pcdp->desc;
+                else if (page_num < pcdp->page_code)
+                    break;
+            }
+        }
+    }
+    pcdp = get_mpage_tbl_size(-1, &num);
+    for (k = 0; k < num; ++k, ++pcdp) {
+        if ((page_num == pcdp->page_code) &&
+            (subpage_num == pcdp->subpage_code))
+            return pcdp->desc;
+        else if (page_num < pcdp->page_code)
+            break;
+    }
+    return NULL;
+}
+
+static void
+list_page_codes(int scsi_ptype, bool encserv, bool mchngr, int t_proto)
+{
+    int num, num_ptype, pg, spg, c, d;
+    bool valid_transport;
+    const struct page_code_desc * dp;
+    const struct page_code_desc * pe_dp;
+    char b[64];
+
+    valid_transport = ((t_proto >= 0) && (t_proto <= 0xf));
+    printf("Page[,subpage]   Name\n");
+    printf("=====================\n");
+    dp = get_mpage_tbl_size(-1, &num);
+    pe_dp = get_mpage_tbl_size(scsi_ptype, &num_ptype);
+    while (1) {
+        pg = dp ? dp->page_code : PG_CODE_ALL + 1;
+        spg = dp ? dp->subpage_code : SPG_CODE_ALL;
+        c = (pg << 8) + spg;
+        pg = pe_dp ? pe_dp->page_code : PG_CODE_ALL + 1;
+        spg = pe_dp ? pe_dp->subpage_code : SPG_CODE_ALL;
+        d = (pg << 8) + spg;
+        if (valid_transport &&
+            ((PROTO_SPECIFIC_1 == c) || (PROTO_SPECIFIC_2 == c)))
+            dp = (--num <= 0) ? NULL : (dp + 1); /* skip protocol specific */
+        else if (c == d) {
+            if (pe_dp) {
+                if (pe_dp->subpage_code)
+                    printf(" 0x%02x,0x%02x    *  %s\n", pe_dp->page_code,
+                           pe_dp->subpage_code, pe_dp->desc);
+                else
+                    printf(" 0x%02x         *  %s\n", pe_dp->page_code,
+                           pe_dp->desc);
+                pe_dp = (--num_ptype <= 0) ? NULL : (pe_dp + 1);
+            }
+            if (dp)
+                dp = (--num <= 0) ? NULL : (dp + 1);
+        } else if (c < d) {
+            if (dp) {
+                if (dp->subpage_code)
+                    printf(" 0x%02x,0x%02x       %s\n", dp->page_code,
+                           dp->subpage_code, dp->desc);
+                else
+                    printf(" 0x%02x            %s\n", dp->page_code,
+                           dp->desc);
+                dp = (--num <= 0) ? NULL : (dp + 1);
+            }
+        } else {
+            if (pe_dp) {
+                if (pe_dp->subpage_code)
+                    printf(" 0x%02x,0x%02x       %s\n", pe_dp->page_code,
+                           pe_dp->subpage_code, pe_dp->desc);
+                else
+                    printf(" 0x%02x            %s\n", pe_dp->page_code,
+                           pe_dp->desc);
+                pe_dp = (--num_ptype <= 0) ? NULL : (pe_dp + 1);
+            }
+        }
+        if ((NULL == dp) && (NULL == pe_dp))
+            break;
+    }
+    if ((0xd != scsi_ptype) && encserv) {
+        /* check for attached enclosure services processor */
+        printf("\n    Attached enclosure services processor\n");
+        dp = get_mpage_tbl_size(0xd, &num);
+        while (dp) {
+            if (dp->subpage_code)
+                printf(" 0x%02x,0x%02x       %s\n", dp->page_code,
+                       dp->subpage_code, dp->desc);
+            else
+                printf(" 0x%02x            %s\n", dp->page_code,
+                       dp->desc);
+            dp = (--num <= 0) ? NULL : (dp + 1);
+        }
+    }
+    if ((0x8 != scsi_ptype) && mchngr) {
+        /* check for attached medium changer device */
+        printf("\n    Attached medium changer device\n");
+        dp = get_mpage_tbl_size(0x8, &num);
+        while (dp) {
+            if (dp->subpage_code)
+                printf(" 0x%02x,0x%02x       %s\n", dp->page_code,
+                       dp->subpage_code, dp->desc);
+            else
+                printf(" 0x%02x            %s\n", dp->page_code,
+                       dp->desc);
+            dp = (--num <= 0) ? NULL : (dp + 1);
+        }
+    }
+    if (valid_transport) {
+        printf("\n    Transport protocol: %s\n",
+               sg_get_trans_proto_str(t_proto, sizeof(b), b));
+        dp = get_mpage_trans_tbl_size(t_proto, &num);
+        while (dp) {
+            if (dp->subpage_code)
+                printf(" 0x%02x,0x%02x       %s\n", dp->page_code,
+                       dp->subpage_code, dp->desc);
+            else
+                printf(" 0x%02x            %s\n", dp->page_code,
+                       dp->desc);
+            dp = (--num <= 0) ? NULL : (dp + 1);
+        }
+    }
+}
+
+/* Returns 0 for ok, else error value */
+static int
+examine_pages(int sg_fd, int inq_pdt, bool encserv, bool mchngr,
+              const struct opts_t * op)
+{
+    bool header_printed;
+    int k, mresp_len, len, resid;
+    int res = 0;
+    const int mx_len = op->do_six ? DEF_6_ALLOC_LEN : DEF_ALLOC_LEN;
+    const char * cp;
+    uint8_t * rbuf;
+    uint8_t * free_rbuf = NULL;
+
+    rbuf = sg_memalign(mx_len, 0, &free_rbuf, false);
+    if (NULL == rbuf) {
+        pr2serr("%s: out of heap\n", __func__);
+        return sg_convert_errno(ENOMEM);
+    }
+    mresp_len = (op->do_raw || op->do_hex) ? mx_len : 4;
+    for (header_printed = false, k = 0; k < PG_CODE_MAX; ++k) {
+        resid = 0;
+        if (op->do_six) {
+            res = sg_ll_mode_sense6(sg_fd, 0, 0, k, 0, rbuf, mresp_len,
+                                    true, op->verbose);
+            if (SG_LIB_CAT_INVALID_OP == res) {
+                pr2serr(">>>>>> try again without the '-6' switch for a 10 "
+                        "byte MODE SENSE command\n");
+                goto out;
+            } else if (SG_LIB_CAT_NOT_READY == res) {
+                pr2serr("MODE SENSE (6) failed, device not ready\n");
+                goto out;
+            }
+        } else {
+            res = sg_ll_mode_sense10_v2(sg_fd, 0, 0, 0, k, 0, rbuf, mresp_len,
+                                        0, &resid, true, op->verbose);
+            if (SG_LIB_CAT_INVALID_OP == res) {
+                pr2serr(">>>>>> try again with a '-6' switch for a 6 byte "
+                        "MODE SENSE command\n");
+                goto out;
+            } else if (SG_LIB_CAT_NOT_READY == res) {
+                pr2serr("MODE SENSE (10) failed, device not ready\n");
+                goto out;
+            }
+        }
+        if (0 == res) {
+            len = sg_msense_calc_length(rbuf, mresp_len, op->do_six, NULL);
+            if (resid > 0) {
+                mresp_len -= resid;
+                if (mresp_len < 0) {
+                    pr2serr("%s: MS(10) resid=%d implies negative response "
+                            "length (%d)\n", __func__, resid, mresp_len);
+                    res = SG_LIB_WILD_RESID;
+                    goto out;
+                }
+            }
+            if (len > mresp_len)
+                len = mresp_len;
+            if (op->do_raw) {
+                dStrRaw(rbuf, len);
+                continue;
+            }
+            if (op->do_hex > 2) {
+                hex2stdout(rbuf, len, -1);
+                continue;
+            }
+            if (! header_printed) {
+                printf("Discovered mode pages:\n");
+                header_printed = true;
+            }
+            cp = find_page_code_desc(k, 0, inq_pdt, encserv, mchngr, -1);
+            if (cp)
+                printf("    %s\n", cp);
+            else
+                printf("    [0x%x]\n", k);
+            if (op->do_hex)
+                hex2stdout(rbuf, len, 1);
+        } else if (op->verbose) {
+            char b[80];
+
+            sg_get_category_sense_str(res, sizeof(b), b, op->verbose - 1);
+            pr2serr("MODE SENSE (%s) failed: %s\n", (op->do_six ? "6" : "10"),
+                    b);
+        }
+    }
+out:
+    if (free_rbuf)
+        free(free_rbuf);
+    return res;
+}
+
+static const char * pg_control_str_arr[] = {
+    "current",
+    "changeable",
+    "default",
+    "saved",
+};
+
+
+int
+main(int argc, char * argv[])
+{
+    bool resp_mode6, longlba, spf;
+    bool encserv = false;
+    bool mchngr = false;
+    uint8_t uc;
+    int k, num, len, res, md_len, bd_len, page_num, resid;
+    int density_code_off, t_proto, inq_pdt, num_ua_pages, vb;
+    int sg_fd = -1;
+    int ret = 0;
+    int rsp_buff_sz = DEF_ALLOC_LEN;
+    const char * descp;
+    struct opts_t * op;
+    uint8_t * rsp_buff = NULL;
+    uint8_t * free_rsp_buff = NULL;
+    uint8_t * bp;
+    const char * cdbLenStr;
+    struct sg_simple_inquiry_resp inq_out;
+    struct opts_t opts;
+    char b[80];
+    char ebuff[EBUFF_SZ];
+    char pdt_name[64];
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->pg_code = -1;
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return (SG_LIB_OK_FALSE == res) ? 0 : res;
+    if (op->do_help) {
+        usage_for(op);
+        return 0;
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+    vb = op->verbose;
+    if (vb && op->page_acron) {
+        pr2serr("page acronynm: '%s' maps to page_code=0x%x",
+                op->page_acron, op->pg_code);
+        if (op->subpg_code > 0)
+            pr2serr(", subpage_code=0x%x\n", op->subpg_code);
+        else
+            pr2serr("\n");
+    }
+
+    if (NULL == op->device_name) {
+        if (op->do_list) {
+            if ((op->pg_code < 0) || (op->pg_code > PG_CODE_MAX)) {
+                printf("    Assume peripheral device type: disk\n");
+                list_page_codes(0, false, false, -1);
+            } else {
+                printf("    peripheral device type: %s\n",
+                       sg_get_pdt_str(op->pg_code, sizeof(pdt_name),
+                                      pdt_name));
+                if (op->subpg_code_given)
+                    list_page_codes(op->pg_code, false, false,
+                                    op->subpg_code);
+                else
+                    list_page_codes(op->pg_code, false, false, -1);
+            }
+            return 0;
+        }
+        pr2serr("No DEVICE argument given\n\n");
+        usage_for(op);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (op->do_examine && (op->pg_code >= 0)) {
+        pr2serr("can't give '-e' and a page number\n");
+        return SG_LIB_CONTRADICT;
+    }
+
+    if (op->do_six && op->do_llbaa) {
+        pr2serr("LLBAA not defined for MODE SENSE 6, try without '-L'\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (op->maxlen > 0) {
+        if (op->do_six && (op->maxlen > 255)) {
+            pr2serr("For Mode Sense (6) maxlen cannot exceed 255\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        rsp_buff = sg_memalign(op->maxlen, 0, &free_rsp_buff, false);
+        rsp_buff_sz = op->maxlen;
+    } else {    /* maxlen == 0 */
+        rsp_buff = sg_memalign(rsp_buff_sz, 0, &free_rsp_buff, false);
+        if (op->do_six)
+            rsp_buff_sz = DEF_6_ALLOC_LEN;
+    }
+    if (NULL == rsp_buff) {     /* check for both sg_memalign()s */
+        pr2serr("Unable to allocate %d bytes on heap\n", rsp_buff_sz);
+        return sg_convert_errno(ENOMEM);
+    }
+    /* If no pages or list selected than treat as 'a' */
+    if (! ((op->pg_code >= 0) || op->do_all || op->do_list || op->do_examine))
+        op->do_all = 1;
+
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto fini;
+        }
+    }
+
+    if ((sg_fd = sg_cmds_open_device(op->device_name, ! op->o_readwrite,
+                                     vb)) < 0) {
+        pr2serr("error opening file: %s: %s\n", op->device_name,
+                safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    if ((res = sg_simple_inquiry(sg_fd, &inq_out, true, vb))) {
+        pr2serr("%s doesn't respond to a SCSI INQUIRY\n", op->device_name);
+        ret = (res > 0) ? res : sg_convert_errno(-res);
+        goto fini;
+    }
+    inq_pdt = inq_out.peripheral_type;
+    encserv = !! (0x40 & inq_out.byte_6);
+    mchngr = !! (0x8 & inq_out.byte_6);
+    if ((0 == op->do_raw) && (op->do_hex < 3))
+        printf("    %.8s  %.16s  %.4s   peripheral_type: %s [0x%x]\n",
+               inq_out.vendor, inq_out.product, inq_out.revision,
+               sg_get_pdt_str(inq_pdt, sizeof(pdt_name), pdt_name), inq_pdt);
+    if (op->do_list) {
+        if (op->subpg_code_given)
+            list_page_codes(inq_pdt, encserv, mchngr, op->subpg_code);
+        else
+            list_page_codes(inq_pdt, encserv, mchngr, -1);
+        goto fini;
+    }
+    if (op->do_examine) {
+        ret = examine_pages(sg_fd, inq_pdt, encserv, mchngr, op);
+        goto fini;
+    }
+    if (PG_CODE_ALL == op->pg_code) {
+        if (0 == op->do_all)
+            ++op->do_all;
+    } else if (op->do_all)
+        op->pg_code = PG_CODE_ALL;
+    if (op->do_all > 1)
+        op->subpg_code = SPG_CODE_ALL;
+
+    if (op->do_raw > 1) {
+        if (op->do_all) {
+            if (op->opt_new)
+                pr2serr("'-R' requires a specific (sub)page, not all\n");
+            else
+                pr2serr("'-r' requires a specific (sub)page, not all\n");
+            usage_for(op);
+            ret = SG_LIB_CONTRADICT;
+            goto fini;
+        }
+    }
+
+    resid = 0;
+    if (op->do_six) {
+        res = sg_ll_mode_sense6(sg_fd, op->do_dbd, op->page_control,
+                                op->pg_code, op->subpg_code, rsp_buff,
+                                rsp_buff_sz, true, vb);
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr(">>>>>> try again without the '-6' switch for a 10 byte "
+                    "MODE SENSE command\n");
+    } else {
+        res = sg_ll_mode_sense10_v2(sg_fd, op->do_llbaa, op->do_dbd,
+                                    op->page_control, op->pg_code,
+                                    op->subpg_code, rsp_buff, rsp_buff_sz,
+                                    0, &resid, true, vb);
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr(">>>>>> try again with a '-6' switch for a 6 byte MODE "
+                    "SENSE command\n");
+    }
+    if (SG_LIB_CAT_ILLEGAL_REQ == res) {
+        if (op->subpg_code > 0)
+            pr2serr("invalid field in cdb (perhaps subpages not "
+                    "supported)\n");
+        else if (op->page_control > 0)
+            pr2serr("invalid field in cdb (perhaps page control (PC) not "
+                    "supported)\n");
+        else
+            pr2serr("invalid field in cdb (perhaps page 0x%x not "
+                    "supported)\n", op->pg_code);
+    } else if (res) {
+        sg_get_category_sense_str(res, sizeof(b), b, vb);
+        pr2serr("%s\n", b);
+    }
+    ret = res;
+    if (0 == res) {
+        int medium_type, specific, headerlen;
+
+        ret = 0;
+        resp_mode6 = op->do_six;
+        if (op->do_flexible) {
+            num = rsp_buff[0];
+            if (op->do_six && (num < 3))
+                resp_mode6 = false;
+            if ((! op->do_six) && (num > 5)) {
+                if ((num > 11) && (0 == (num % 2)) && (0 == rsp_buff[4]) &&
+                    (0 == rsp_buff[5]) && (0 == rsp_buff[6])) {
+                    rsp_buff[1] = num;
+                    rsp_buff[0] = 0;
+                    pr2serr(">>> msense(10) but resp[0]=%d and not msense(6) "
+                            "response so fix length\n", num);
+                } else
+                    resp_mode6 = true;
+            }
+        }
+        cdbLenStr = resp_mode6 ? "6" : "10";
+        if (op->do_raw || (1 == op->do_hex) || (op->do_hex > 2))
+            ;
+        else {
+            if (resp_mode6 == op->do_six)
+                printf("Mode parameter header from MODE SENSE(%s):\n",
+                       cdbLenStr);
+            else
+                printf(" >>> Mode parameter header from MODE SENSE(%s),\n"
+                       "     decoded as %s byte response:\n",
+                       cdbLenStr, (resp_mode6 ? "6" : "10"));
+        }
+        rsp_buff_sz -= resid;
+        if (rsp_buff_sz < 0) {
+            pr2serr("MS(%s) resid=%d implies negative response length "
+                    "(%d)\n", cdbLenStr, resid, rsp_buff_sz);
+            ret = SG_LIB_WILD_RESID;
+            goto fini;
+        }
+        if (resp_mode6) {
+            if (rsp_buff_sz < 4) {
+                pr2serr("MS(6) resid=%d implies abridged header length "
+                        "(%d)\n", resid, rsp_buff_sz);
+                ret = SG_LIB_WILD_RESID;
+                goto fini;
+            }
+            headerlen = 4;
+            medium_type = rsp_buff[1];
+            specific = rsp_buff[2];
+            longlba = false;
+        } else {        /* MODE SENSE(10) with resid */
+            if (rsp_buff_sz < 8) {
+                pr2serr("MS(10) resid=%d implies abridged header length "
+                        "(%d)\n", resid, rsp_buff_sz);
+                ret = SG_LIB_WILD_RESID;
+                goto fini;
+            }
+            headerlen = 8;
+            medium_type = rsp_buff[2];
+            specific = rsp_buff[3];
+            longlba = !!(rsp_buff[4] & 1);
+        }
+        md_len = sg_msense_calc_length(rsp_buff, rsp_buff_sz, resp_mode6,
+                                       &bd_len);
+        if (md_len < 0) {
+            pr2serr("MS(%s): sg_msense_calc_length() failed\n", cdbLenStr);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto fini;
+        }
+        md_len = (md_len < rsp_buff_sz) ? md_len : rsp_buff_sz;
+        if ((bd_len + headerlen) > md_len) {
+            pr2serr("Invalid block descriptor length=%d, ignore\n", bd_len);
+            bd_len = 0;
+        }
+        if (op->do_raw || (op->do_hex > 2)) {
+            if (1 == op->do_raw)
+                dStrRaw(rsp_buff, md_len);
+            else if (op->do_raw > 1) {
+                bp = rsp_buff + bd_len + headerlen;
+                md_len -= bd_len + headerlen;
+                spf = !!(bp[0] & 0x40);
+                len = (spf ? (sg_get_unaligned_be16(bp + 2) + 4) :
+                             (bp[1] + 2));
+                len = (len < md_len) ? len : md_len;
+                for (k = 0; k < len; ++k)
+                    printf("%02x\n", bp[k]);
+            } else
+                hex2stdout(rsp_buff, md_len, -1);
+            goto fini;
+        }
+        if (1 == op->do_hex) {
+            hex2stdout(rsp_buff, md_len, 1);
+            goto fini;
+        } else if (op->do_hex > 1) {
+            hex2stdout(rsp_buff, headerlen, 1);
+            goto fini;
+        }
+        if ((PDT_DISK == inq_pdt) || (PDT_ZBC == inq_pdt))
+            printf("  Mode data length=%d, medium type=0x%.2x, WP=%d,"
+                   " DpoFua=%d, longlba=%d\n", md_len, medium_type,
+                   !!(specific & 0x80), !!(specific & 0x10), (int)longlba);
+        else
+            printf("  Mode data length=%d, medium type=0x%.2x, specific"
+                   " param=0x%.2x, longlba=%d\n", md_len, medium_type,
+                   specific, (int)longlba);
+        if (md_len > rsp_buff_sz) {
+            printf("Only fetched %d bytes of response, truncate output\n",
+                   rsp_buff_sz);
+            md_len = rsp_buff_sz;
+            if (bd_len + headerlen > rsp_buff_sz)
+                bd_len = rsp_buff_sz - headerlen;
+        }
+        if (! op->do_dbout) {
+            printf("  Block descriptor length=%d\n", bd_len);
+            if (bd_len > 0) {
+                len = 8;
+                density_code_off = 0;
+                num = bd_len;
+                if (longlba) {
+                    printf("> longlba direct access device block "
+                           "descriptors:\n");
+                    len = 16;
+                    density_code_off = 8;
+                }
+                else if ((PDT_DISK == inq_pdt) || (PDT_ZBC == inq_pdt)) {
+                    printf("> Direct access device block descriptors:\n");
+                    density_code_off = 4;
+                }
+                else
+                    printf("> General mode parameter block descriptors:\n");
+
+                bp = rsp_buff + headerlen;
+                while (num > 0) {
+                    printf("   Density code=0x%x\n",
+                           *(bp + density_code_off));
+                    hex2stdout(bp, len, 1);
+                    bp += len;
+                    num -= len;
+                }
+                printf("\n");
+            }
+        }
+        bp = rsp_buff + bd_len + headerlen;    /* start of mode page(s) */
+        md_len -= bd_len + headerlen;           /* length of mode page(s) */
+        num_ua_pages = 0;
+        for (k = 0; md_len > 0; ++k) { /* got mode page(s) */
+            if ((k > 0) && (! op->do_all) &&
+                (SPG_CODE_ALL != op->subpg_code)) {
+                pr2serr("Unexpectedly received extra mode page responses, "
+                        "ignore\n");
+                break;
+            }
+            uc = *bp;
+            spf = !!(uc & 0x40);
+            len = (spf ? (sg_get_unaligned_be16(bp + 2) + 4) : (bp[1] + 2));
+            page_num = bp[0] & PG_CODE_MASK;
+            if (0x0 == page_num) {
+                ++num_ua_pages;
+                if((num_ua_pages > 3) && (md_len > 0xa00)) {
+                    pr2serr(">>> Seen 3 unit attention pages (only one "
+                            "should be at end)\n     and mpage length=%d, "
+                            "looks malformed, try '-f' option\n", md_len);
+                    break;
+                }
+            }
+            if (op->do_hex) {
+                if (spf)
+                    printf(">> page_code=0x%x, subpage_code=0x%x, page_cont"
+                           "rol=%d\n", page_num, bp[1], op->page_control);
+                else
+                    printf(">> page_code=0x%x, page_control=%d\n", page_num,
+                           op->page_control);
+            } else {
+                descp = NULL;
+                if ((0x18 == page_num) || (0x19 == page_num)) {
+                    t_proto = (spf ? bp[5] : bp[2]) & 0xf;
+                    descp = find_page_code_desc(page_num, (spf ? bp[1] : 0),
+                                                inq_pdt, encserv, mchngr,
+                                                t_proto);
+                } else
+                    descp = find_page_code_desc(page_num, (spf ? bp[1] : 0),
+                                                inq_pdt, encserv, mchngr, -1);
+                if (NULL == descp) {
+                    if (spf)
+                        snprintf(ebuff, EBUFF_SZ, "0x%x, subpage_code: 0x%x",
+                                 page_num, bp[1]);
+                    else
+                        snprintf(ebuff, EBUFF_SZ, "0x%x", page_num);
+                }
+                if (descp)
+                    printf(">> %s, page_control: %s\n", descp,
+                           pg_control_str_arr[op->page_control]);
+                else
+                    printf(">> page_code: %s, page_control: %s\n", ebuff,
+                           pg_control_str_arr[op->page_control]);
+            }
+            num = (len > md_len) ? md_len : len;
+            if ((k > 0) && (num > UNLIKELY_ABOVE_LEN)) {
+                num = UNLIKELY_ABOVE_LEN;
+                pr2serr(">>> page length (%d) > %d bytes, unlikely, trim\n"
+                        "    Try '-f' option\n", len, num);
+            }
+            hex2stdout(bp, num , 1);
+            bp += len;
+            md_len -= len;
+        }
+    }
+
+fini:
+    if (sg_fd >= 0)
+        sg_cmds_close_device(sg_fd);
+    if (free_rsp_buff)
+        free(free_rsp_buff);
+    if (0 == vb) {
+        if (! sg_if_can2stderr("sg_modes failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_opcodes.c b/src/sg_opcodes.c
new file mode 100644
index 0000000..9a5e3b8
--- /dev/null
+++ b/src/sg_opcodes.c
@@ -0,0 +1,1500 @@
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *  Copyright (C) 2004-2022 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ *  This program outputs information provided by a SCSI REPORT SUPPORTED
+ *  OPERATION CODES [0xa3/0xc] (RSOC) and REPORT SUPPORTED TASK MANAGEMENT
+ *  FUNCTIONS [0xa3/0xd] (RSTMF) commands.
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+#include "sg_pt.h"
+
+static const char * version_str = "0.86 20221005";    /* spc6r06 */
+
+#define MY_NAME "sg_opcodes"
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_TIMEOUT_SECS 60
+
+#define SG_MAINTENANCE_IN 0xa3
+#define RSOC_SA     0xc
+#define RSTMF_SA    0xd
+#define RSOC_CMD_LEN 12
+#define RSTMF_CMD_LEN 12
+#define MX_ALLOC_LEN 8192
+
+#define NAME_BUFF_SZ 128
+
+#define SEAGATE_READ_UDS_DATA_CMD 0xf7  /* may start reporting vendor cmds */
+
+static int peri_dtype = -1; /* ugly but not easy to pass to alpha compare */
+static bool no_final_msg = false;
+
+static struct option long_options[] = {
+        {"alpha", no_argument, 0, 'a'},
+        {"compact", no_argument, 0, 'c'},
+        {"enumerate", no_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"inhex", required_argument, 0, 'i'},
+        {"in", required_argument, 0, 'i'},
+        {"json", optional_argument, 0, 'j'},
+        {"mask", no_argument, 0, 'm'},
+        {"mlu", no_argument, 0, 'M'},           /* added in spc5r20 */
+        {"no-inquiry", no_argument, 0, 'n'},
+        {"no_inquiry", no_argument, 0, 'n'},
+        {"new", no_argument, 0, 'N'},
+        {"opcode", required_argument, 0, 'o'},
+        {"old", no_argument, 0, 'O'},
+        {"pdt", required_argument, 0, 'p'},
+        {"raw", no_argument, 0, 'r'},
+        {"rctd", no_argument, 0, 'R'},
+        {"repd", no_argument, 0, 'q'},
+        {"sa", required_argument, 0, 's'},
+        {"tmf", no_argument, 0, 't'},
+        {"unsorted", no_argument, 0, 'u'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool do_alpha;
+    bool do_compact;
+    bool do_enumerate;
+    bool no_inquiry;
+    bool do_mask;
+    bool do_mlu;
+    bool do_raw;
+    bool do_rctd;       /* Return command timeout descriptor */
+    bool do_repd;
+    bool do_unsorted;
+    bool do_taskman;
+    bool opt_new;
+    bool verbose_given;
+    bool version_given;
+    int do_help;
+    int do_hex;
+    int opcode;
+    int servact;
+    int verbose;
+    const char * device_name;
+    const char * inhex_fn;
+    sgj_state json_st;
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage:  sg_opcodes [--alpha] [--compact] [--enumerate] "
+            "[--help] [--hex]\n"
+            "                   [--inhex=FN] [--json[=JO]] [--mask] [--mlu] "
+            "[--no-inquiry]\n"
+            "                   [--opcode=OP[,SA]] [--pdt=DT] [--raw] "
+            "[--rctd]\n"
+            "                   [--repd] [--sa=SA] [--tmf] [--unsorted] "
+            "[--verbose]\n"
+            "                   [--version] DEVICE\n"
+            "  where:\n"
+            "    --alpha|-a      output list of operation codes sorted "
+            "alphabetically\n"
+            "    --compact|-c    more compact output\n"
+            "    --enumerate|-e    use '--opcode=' and '--pdt=' to look up "
+            "name,\n"
+            "                      ignore DEVICE\n"
+            "    --help|-h       print usage message then exit\n"
+            "    --hex|-H        output response in hex, use -HHH for "
+            "hex\n"
+            "                    suitable for later use of --inhex= "
+            "option\n"
+            "    --inhex=FN|-i FN    contents of file FN treated as hex "
+            "and used\n"
+            "                        instead of DEVICE which is ignored\n"
+            "    --json[=JO]|-jJO    output in JSON instead of human "
+            "readable\n"
+            "                        test. Use --json=? for JSON help\n"
+            "    --mask|-m       show cdb usage data (a mask) when "
+            "all listed\n"
+            "    --mlu|-M        show MLU bit when all listed\n"
+            "    --no-inquiry|-n    don't output INQUIRY information\n"
+            "    --opcode=OP[,SA]|-o OP[,SA]    opcode (OP) and service "
+            "action (SA)\n"
+            "    --pdt=DT|-p DT    give peripheral device type for "
+            "'--no-inquiry'\n"
+            "                      '--enumerate'\n"
+            "    --raw|-r        output response in binary to stdout unless "
+            "--inhex=FN\n"
+            "                    is given then FN is parsed as binary "
+            "instead\n"
+            "    --rctd|-R       set RCTD (return command timeout "
+            "descriptor) bit\n"
+            "    --repd|-q       set Report Extended Parameter Data bit, "
+            "with --tmf\n"
+            "    --sa=SA|-s SA    service action in addition to opcode\n"
+            "    --tmf|-t        output list of supported task management "
+            "functions\n"
+            "    --unsorted|-u    output list of operation codes as is\n"
+            "                     (def: sort by opcode (then service "
+            "action))\n"
+            "    --verbose|-v    increase verbosity\n"
+            "    --old|-O        use old interface (use as first option)\n"
+            "    --version|-V    print version string then exit\n\n"
+            "Performs a SCSI REPORT SUPPORTED OPERATION CODES or a REPORT "
+            "SUPPORTED\nTASK MANAGEMENT FUNCTIONS command. All values are "
+            "in decimal by default,\nprefix with '0x' or add a trailing 'h' "
+            "for hex numbers.\n");
+}
+
+static void
+usage_old()
+{
+    pr2serr("Usage:  sg_opcodes [-a] [-c] [-e] [-H] [-j] [-m] [-M] [-n] "
+            "[-o=OP]\n"
+            "                   [-p=DT] [-q] [-r] [-R] [-s=SA] [-t] [-u] "
+            "[-v] [-V]\n"
+            "                   DEVICE\n"
+            "  where:\n"
+            "    -a    output list of operation codes sorted "
+            "alphabetically\n"
+            "    -c    more compact output\n"
+            "    -e    use '--opcode=' and '--pdt=' to look up name, "
+            "ignore DEVICE\n"
+            "    -H    print response in hex\n"
+            "    -j    print response in JSON\n"
+            "    -m    show cdb usage data (a mask) when all listed\n"
+            "    -M    show MLU bit when all listed\n"
+            "    -n    don't output INQUIRY information\n"
+            "    -o=OP    first byte of command to query (in hex)\n"
+            "    -p=DT    alternate source of pdt (normally obtained from "
+            "inquiry)\n"
+            "    -q    set REPD bit for tmf_s\n"
+            "    -r    output response in binary to stdout\n"
+            "    -R    set RCTD (return command timeout "
+            "descriptor) bit\n"
+            "    -s=SA    in addition to opcode (in hex)\n"
+            "    -t    output list of supported task management functions\n"
+            "    -u    output list of operation codes as is (unsorted)\n"
+            "    -v    verbose\n"
+            "    -V    output version string\n"
+            "    -N|--new   use new interface\n"
+            "    -?    output this usage message\n\n"
+            "Performs a SCSI REPORT SUPPORTED OPERATION CODES (or a REPORT "
+            "TASK MANAGEMENT\nFUNCTIONS) command\n");
+}
+
+static const char * const rsoc_s = "Report supported operation codes";
+
+static int
+do_rsoc(struct sg_pt_base * ptvp, bool rctd, int rep_opts, int rq_opcode,
+        int rq_servact, void * resp, int mx_resp_len, int * act_resp_lenp,
+        bool noisy, int verbose)
+{
+    int ret, res, sense_cat;
+    uint8_t rsoc_cdb[RSOC_CMD_LEN] = {SG_MAINTENANCE_IN, RSOC_SA, 0,
+                                              0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    if (rctd)
+        rsoc_cdb[2] |= 0x80;
+    if (rep_opts)
+        rsoc_cdb[2] |= (rep_opts & 0x7);
+    if (rq_opcode > 0)
+        rsoc_cdb[3] = (rq_opcode & 0xff);
+    if (rq_servact > 0)
+        sg_put_unaligned_be16((uint16_t)rq_servact, rsoc_cdb + 4);
+    if (act_resp_lenp)
+        *act_resp_lenp = 0;
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rsoc_cdb + 6);
+
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", rsoc_s,
+                sg_get_command_str(rsoc_cdb, RSOC_CMD_LEN, false,
+                                   sizeof(b), b));
+    }
+    clear_scsi_pt_obj(ptvp);
+    set_scsi_pt_cdb(ptvp, rsoc_cdb, sizeof(rsoc_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, -1, DEF_TIMEOUT_SECS, verbose);
+    ret = sg_cmds_process_resp(ptvp, rsoc_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        if (act_resp_lenp)
+            *act_resp_lenp = ret;
+        if ((verbose > 2) && (ret > 0)) {
+            pr2serr("%s response:\n", rsoc_s);
+            hex2stderr((const uint8_t *)resp, ret, 1);
+        }
+        ret = 0;
+    }
+    return ret;
+}
+
+static const char * const rstmf_s = "Report supported task management "
+                                    "functions";
+
+static int
+do_rstmf(struct sg_pt_base * ptvp, bool repd, void * resp, int mx_resp_len,
+         int * act_resp_lenp, bool noisy, int verbose)
+{
+    int ret, res, sense_cat;
+    uint8_t rstmf_cdb[RSTMF_CMD_LEN] = {SG_MAINTENANCE_IN, RSTMF_SA,
+                                       0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+
+    if (repd)
+        rstmf_cdb[2] = 0x80;
+    if (act_resp_lenp)
+        *act_resp_lenp = 0;
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rstmf_cdb + 6);
+
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", rstmf_s,
+                sg_get_command_str(rstmf_cdb, RSTMF_CMD_LEN, false,
+                                   sizeof(b), b));
+    }
+    clear_scsi_pt_obj(ptvp);
+    set_scsi_pt_cdb(ptvp, rstmf_cdb, sizeof(rstmf_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, -1, DEF_TIMEOUT_SECS, verbose);
+    ret = sg_cmds_process_resp(ptvp, rstmf_s, res, noisy, verbose,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        if (act_resp_lenp)
+            *act_resp_lenp = ret;
+        if ((verbose > 2) && (ret > 0)) {
+            pr2serr("%s response:\n", rstmf_s);
+            hex2stderr((const uint8_t *)resp, ret, 1);
+        }
+        ret = 0;
+    }
+    return ret;
+}
+
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int c, n;
+    char * cp;
+    char b[32];
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "acehHi:j::mMnNo:Op:qrRs:tuvV",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            op->do_alpha = true;
+            break;
+        case 'c':
+            op->do_compact = true;
+            break;
+        case 'e':
+            op->do_enumerate = true;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'i':
+            op->inhex_fn = optarg;
+            break;
+        case 'j':
+            if (! sgj_init_state(&op->json_st, optarg)) {
+                int bad_char = op->json_st.first_bad_char;
+                char e[1500];
+
+                if (bad_char) {
+                    pr2serr("bad argument to --json= option, unrecognized "
+                            "character '%c'\n\n", bad_char);
+                }
+                sg_json_usage(0, e, sizeof(e));
+                pr2serr("%s", e);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'm':
+            op->do_mask = true;
+            break;
+        case 'M':
+            op->do_mlu = true;
+            break;
+        case 'n':
+            op->no_inquiry = true;
+            break;
+        case 'N':
+            break;      /* ignore */
+        case 'o':
+            if (strlen(optarg) >= (sizeof(b) - 1)) {
+                pr2serr("argument to '--opcode' too long\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            cp = strchr(optarg, ',');
+            if (cp) {
+                memset(b, 0, sizeof(b));
+                strncpy(b, optarg, cp - optarg);
+                n = sg_get_num(b);
+                if ((n < 0) || (n > 255)) {
+                    pr2serr("bad OP argument to '--opcode'\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->opcode = n;
+                n = sg_get_num(cp + 1);
+                if ((n < 0) || (n > 0xffff)) {
+                    pr2serr("bad SA argument to '--opcode'\n");
+                    usage();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->servact = n;
+            } else {
+                n = sg_get_num(optarg);
+                if ((n < 0) || (n > 255)) {
+                    pr2serr("bad argument to '--opcode'\n");
+                    usage();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->opcode = n;
+            }
+            break;
+        case 'O':
+            op->opt_new = false;
+            return 0;
+        case 'p':
+            n = -2;
+            if (isdigit((uint8_t)optarg[0]))
+                n = sg_get_num(optarg);
+            else if ((2 == strlen(optarg)) && (0 == strcmp("-1", optarg)))
+                n = -1;
+            if ((n < -1) || (n > PDT_MAX)) {
+                pr2serr("bad argument to '--pdt=DT', expect -1 to 31\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            peri_dtype = n;
+            break;
+        case 'q':
+            op->do_repd = true;
+            break;
+        case 'r':
+            op->do_raw = true;
+            break;
+        case 'R':
+            op->do_rctd = true;
+            break;
+        case 's':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 0xffff)) {
+                pr2serr("bad argument to '--sa'\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->servact = n;
+            break;
+        case 't':
+            op->do_taskman = true;
+            break;
+        case 'u':
+            op->do_unsorted = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool jmp_out;
+    int k, plen, n, num;
+    const char * cp;
+
+    for (k = 1; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case 'a':
+                    op->do_alpha = true;
+                    break;
+                case 'c':
+                    op->do_compact = true;
+                    break;
+                case 'e':
+                    op->do_enumerate = true;
+                    break;
+                case 'H':
+                    ++op->do_hex;
+                    break;
+                case 'j':    /* don't accept argument with this old syntax */
+                    sgj_init_state(&op->json_st, NULL);
+                    break;
+                case 'm':
+                    op->do_mask = true;
+                    break;
+                case 'M':
+                    op->do_mlu = true;
+                    break;
+                case 'n':
+                    op->no_inquiry = true;
+                    break;
+                case 'N':
+                    op->opt_new = true;
+                    return 0;
+                case 'O':
+                    break;
+                case 'q':
+                    op->do_repd = true;
+                    break;
+                case 'r':
+                    op->do_raw = true;
+                    break;
+                case 'R':
+                    op->do_rctd = true;
+                    break;
+                case 't':
+                    op->do_taskman = true;
+                    break;
+                case 'u':
+                    op->do_unsorted = true;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                case 'h':
+                case '?':
+                    ++op->do_help;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            if (0 == strncmp("i=", cp, 2))
+                op->inhex_fn = cp + 2;
+            else if (0 == strncmp("o=", cp, 2)) {
+                num = sscanf(cp + 2, "%x", (unsigned int *)&n);
+                if ((1 != num) || (n > 255)) {
+                    pr2serr("Bad number after 'o=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->opcode = n;
+            } else if (0 == strncmp("p=", cp, 2)) {
+                num = sscanf(cp + 2, "%d", &n);
+                if ((1 != num) || (n > PDT_MAX) || (n < -1)) {
+                    pr2serr("Bad number after 'p=' option, expect -1 to "
+                            "31\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                peri_dtype = n;
+            } else if (0 == strncmp("s=", cp, 2)) {
+                num = sscanf(cp + 2, "%x", (unsigned int *)&n);
+                if (1 != num) {
+                    pr2serr("Bad number after 's=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->servact = n;
+            } else if (0 == strncmp("-old", cp, 4))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage_old();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (NULL == op->device_name)
+            op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not expecting: %s\n",
+                    op->device_name, cp);
+            usage_old();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opt_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opt_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opt_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (! op->opt_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+static void
+dStrRaw(const char * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* returns -1 when left < right, 0 when left == right, else returns 1 */
+static int
+opcode_num_compare(const void * left, const void * right)
+{
+    int l_serv_act = 0;
+    int r_serv_act = 0;
+    int l_opc, r_opc;
+    const uint8_t * ll = *(uint8_t **)left;
+    const uint8_t * rr = *(uint8_t **)right;
+
+    if (NULL == ll)
+        return -1;
+    if (NULL == rr)
+        return -1;
+    l_opc = ll[0];
+    if (ll[5] & 1)
+        l_serv_act = sg_get_unaligned_be16(ll + 2);
+    r_opc = rr[0];
+    if (rr[5] & 1)
+        r_serv_act = sg_get_unaligned_be16(rr + 2);
+    if (l_opc < r_opc)
+        return -1;
+    if (l_opc > r_opc)
+        return 1;
+    if (l_serv_act < r_serv_act)
+        return -1;
+    if (l_serv_act > r_serv_act)
+        return 1;
+    return 0;
+}
+
+/* returns -1 when left < right, 0 when left == right, else returns 1 */
+static int
+opcode_alpha_compare(const void * left, const void * right)
+{
+    const uint8_t * ll = *(uint8_t **)left;
+    const uint8_t * rr = *(uint8_t **)right;
+    int l_serv_act = 0;
+    int r_serv_act = 0;
+    char l_name_buff[NAME_BUFF_SZ];
+    char r_name_buff[NAME_BUFF_SZ];
+    int l_opc, r_opc;
+
+    if (NULL == ll)
+        return -1;
+    if (NULL == rr)
+        return -1;
+    l_opc = ll[0];
+    if (ll[5] & 1)
+        l_serv_act = sg_get_unaligned_be16(ll + 2);
+    l_name_buff[0] = '\0';
+    sg_get_opcode_sa_name(l_opc, l_serv_act, peri_dtype,
+                          NAME_BUFF_SZ, l_name_buff);
+    r_opc = rr[0];
+    if (rr[5] & 1)
+        r_serv_act = sg_get_unaligned_be16(rr + 2);
+    r_name_buff[0] = '\0';
+    sg_get_opcode_sa_name(r_opc, r_serv_act, peri_dtype,
+                          NAME_BUFF_SZ, r_name_buff);
+    return strncmp(l_name_buff, r_name_buff, NAME_BUFF_SZ);
+}
+
+/* For decoding a RSOC command's "All_commands" parameter data */
+static int
+list_all_codes(uint8_t * rsoc_buff, int rsoc_len, struct opts_t * op,
+               struct sg_pt_base * ptvp)
+{
+    bool sa_v;
+    int k, j, m, n, cd_len, serv_act, len, act_len, opcode, res;
+    uint8_t byt5;
+    unsigned int timeout;
+    uint8_t * bp;
+    uint8_t ** sort_arr = NULL;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jap = NULL;
+    sgj_opaque_p jop = NULL;
+    char name_buff[NAME_BUFF_SZ];
+    char sa_buff[8];
+    char b[192];
+    const int blen = sizeof(b);
+
+    cd_len = sg_get_unaligned_be32(rsoc_buff + 0);
+    if (cd_len > (rsoc_len - 4)) {
+        sgj_pr_hr(jsp, "sg_opcodes: command data length=%d, allocation=%d; "
+                   "truncate\n", cd_len, rsoc_len - 4);
+        cd_len = ((rsoc_len - 4) / 8) * 8;
+    }
+    if (0 == cd_len) {
+        sgj_pr_hr(jsp, "sg_opcodes: no commands to display\n");
+        return 0;
+    }
+    if (op->do_rctd) {  /* Return command timeout descriptor */
+        if (op->do_compact) {
+            sgj_pr_hr(jsp, "\nOpcode,sa  Nominal  Recommended  Name\n");
+            sgj_pr_hr(jsp,   "  (hex)    timeout  timeout(sec)     \n");
+            sgj_pr_hr(jsp, "-----------------------------------------------"
+                       "---------\n");
+        } else {
+            sgj_pr_hr(jsp, "\nOpcode  Service    CDB   Nominal  Recommended  "
+                      "Name\n");
+            sgj_pr_hr(jsp,   "(hex)   action(h)  size  timeout  timeout(sec) "
+                      "    \n");
+            sgj_pr_hr(jsp, "-------------------------------------------------"
+                      "---------------\n");
+        }
+    } else {            /* RCTD clear in cdb */
+        if (op->do_compact) {
+            sgj_pr_hr(jsp, "\nOpcode,sa  Name\n");
+            sgj_pr_hr(jsp,   "  (hex)        \n");
+            sgj_pr_hr(jsp, "---------------------------------------\n");
+        } else if (op->do_mlu) {
+            sgj_pr_hr(jsp, "\nOpcode  Service    CDB    MLU    Name\n");
+            sgj_pr_hr(jsp,   "(hex)   action(h)  size              \n");
+            sgj_pr_hr(jsp, "-------------------------------------------"
+                      "----\n");
+        } else {
+            sgj_pr_hr(jsp, "\nOpcode  Service    CDB  RWCDLP,  Name\n");
+            sgj_pr_hr(jsp,   "(hex)   action(h)  size   CDLP       \n");
+            sgj_pr_hr(jsp, "-------------------------------------------"
+                      "----\n");
+        }
+    }
+    /* SPC-4 does _not_ require any ordering of opcodes in the response */
+    if (! op->do_unsorted) {
+        sort_arr = (uint8_t **)calloc(cd_len, sizeof(uint8_t *));
+        if (NULL == sort_arr) {
+            pr2serr("sg_opcodes: no memory to sort operation codes, "
+                    "try '-u'\n");
+            return sg_convert_errno(ENOMEM);
+        }
+        memset(sort_arr, 0, cd_len * sizeof(uint8_t *));
+        bp = rsoc_buff + 4;
+        for (k = 0, j = 0; k < cd_len; ++j, k += len, bp += len) {
+            sort_arr[j] = bp;
+            len = (bp[5] & 0x2) ? 20 : 8;
+        }
+        qsort(sort_arr, j, sizeof(uint8_t *),
+              (op->do_alpha ? opcode_alpha_compare : opcode_num_compare));
+    }
+
+    jap = sgj_named_subarray_r(jsp, jsp->basep, "all_command_descriptor");
+    for (k = 0, j = 0; k < cd_len; ++j, k += len) {
+        jop = sgj_new_unattached_object_r(jsp);
+
+        bp = op->do_unsorted ? (rsoc_buff + 4 + k) : sort_arr[j];
+        byt5 = bp[5];
+        len = (byt5 & 0x2) ? 20 : 8;
+        opcode = bp[0];
+        sa_v = !!(byt5 & 1);    /* service action valid */
+        serv_act = 0;
+        name_buff[0] = '\0';
+        if (sa_v) {
+            serv_act = sg_get_unaligned_be16(bp + 2);
+            sg_get_opcode_sa_name(opcode, serv_act, peri_dtype, NAME_BUFF_SZ,
+                                  name_buff);
+            if (op->do_compact)
+                snprintf(sa_buff, sizeof(sa_buff), "%-4x", serv_act);
+            else
+                snprintf(sa_buff, sizeof(sa_buff), "%4x", serv_act);
+        } else {
+            sg_get_opcode_name(opcode, peri_dtype, NAME_BUFF_SZ, name_buff);
+            memset(sa_buff, ' ', sizeof(sa_buff));
+        }
+        if (op->do_rctd) {
+            n = 0;
+            if (byt5 & 0x2) {          /* CTDP set */
+                /* don't show CDLP because it makes line too long */
+                if (op->do_compact)
+                    n += sg_scnpr(b + n, blen - n, " %.2x%c%.4s", opcode,
+                                  (sa_v ? ',' : ' '), sa_buff);
+                else
+                    n += sg_scnpr(b + n, blen - n, " %.2x     %.4s       %3d",
+                                  opcode,
+                                  sa_buff, sg_get_unaligned_be16(bp + 6));
+                timeout = sg_get_unaligned_be32(bp + 12);
+                if (0 == timeout)
+                    n += sg_scnpr(b + n, blen - n, "         -");
+                else
+                    n += sg_scnpr(b + n, blen - n, "  %8u", timeout);
+                timeout = sg_get_unaligned_be32(bp + 16);
+                if (0 == timeout)
+                    n += sg_scnpr(b + n, blen - n, "          -");
+                else
+                    n += sg_scnpr(b + n, blen - n, "   %8u", timeout);
+                sgj_pr_hr(jsp, "%s    %s\n", b, name_buff);
+            } else                      /* CTDP clear */
+                if (op->do_compact)
+                    sgj_pr_hr(jsp, " %.2x%c%.4s                        %s\n",
+                              opcode, (sa_v ? ',' : ' '), sa_buff, name_buff);
+                else
+                    sgj_pr_hr(jsp, " %.2x     %.4s       %3d                 "
+                              "        %s\n", opcode, sa_buff,
+                              sg_get_unaligned_be16(bp + 6), name_buff);
+        } else {            /* RCTD clear in cdb */
+            /* before version 0.69 treated RWCDLP (1 bit) and CDLP (2 bits),
+             * as a 3 bit field, now break them out separately */
+            int rwcdlp = (byt5 >> 2) & 0x3;
+            int cdlp = !!(0x40 & byt5);
+
+            if (op->do_compact)
+                sgj_pr_hr(jsp, " %.2x%c%.4s   %s\n", bp[0],
+                          (sa_v ? ',' : ' '), sa_buff, name_buff);
+            else if (op->do_mlu)
+                sgj_pr_hr(jsp, " %.2x     %.4s       %3d   %3d     %s\n",
+                          bp[0], sa_buff, sg_get_unaligned_be16(bp + 6),
+                          ((byt5 >> 4) & 0x3), name_buff);
+            else
+                sgj_pr_hr(jsp, " %.2x     %.4s       %3d    %d,%d    %s\n",
+                          bp[0], sa_buff, sg_get_unaligned_be16(bp + 6),
+                          rwcdlp, cdlp, name_buff);
+        }
+        if (jsp->pr_as_json) {
+            snprintf(b, blen, "0x%x", opcode);
+            sgj_js_nv_s(jsp, jop, "operation_code", b);
+            if (sa_v) {
+                snprintf(b, blen, "0x%x", serv_act);
+                sgj_js_nv_s(jsp, jop, "service_action", b);
+            }
+            if (name_buff[0])
+                sgj_js_nv_s(jsp, jop, "name", name_buff);
+            sgj_js_nv_i(jsp, jop, "rwcdlp", (byt5 >> 6) & 0x1);
+            sgj_js_nv_i(jsp, jop, "mlu", (byt5 >> 4) & 0x3);
+            sgj_js_nv_i(jsp, jop, "cdlp", (byt5 >> 2) & 0x3);
+            sgj_js_nv_i(jsp, jop, "ctdp", (byt5 >> 1) & 0x1);
+            sgj_js_nv_i(jsp, jop, "servactv", byt5 & 0x1);
+            sgj_js_nv_i(jsp, jop, "cdb_length",
+                        sg_get_unaligned_be16(bp + 6));
+
+            sgj_js_nv_o(jsp, jap, NULL /* implies an array add */, jop);
+        }
+
+        if (op->do_mask && ptvp) {
+            int cdb_sz;
+            uint8_t d[64];
+
+            n = 0;
+            memset(d, 0, sizeof(d));
+            res = do_rsoc(ptvp, false, (sa_v ? 2 : 1), opcode, serv_act,
+                          d, sizeof(d), &act_len, true, op->verbose);
+            if (0 == res) {
+                int nn;
+
+                cdb_sz = sg_get_unaligned_be16(d + 2);
+                cdb_sz = (cdb_sz < act_len) ? cdb_sz : act_len;
+                if ((cdb_sz > 0) && (cdb_sz <= 80)) {
+                    if (op->do_compact)
+                        n += sg_scnpr(b + n, blen - n,
+                                      "             usage: ");
+                    else
+                        n += sg_scnpr(b + n, blen - n, "        cdb usage: ");
+                    nn = n;
+                    for (m = 0; (m < cdb_sz) && ((4 + m) < (int)sizeof(d));
+                         ++m)
+                        n += sg_scnpr(b + n, blen - n, "%.2x ", d[4 + m]);
+                    sgj_pr_hr(jsp, "%s\n", b);
+                    if (jsp->pr_as_json) {
+                        int l;
+                        char *b2p = b + nn;
+                        sgj_opaque_p jo2p = sgj_named_subobject_r(jsp, jop,
+                                         "one_command_descriptor");
+
+                        l = strlen(b2p);
+                        if ((l > 0) && (' ' == b2p[l - 1]))
+                            b2p[l - 1] = '\0';
+                        sgj_js_nv_i(jsp, jo2p, "cdb_size", cdb_sz);
+                        sgj_js_nv_s(jsp, jo2p, "cdb_usage_data", b2p);
+                    }
+                }
+            } else
+                goto err_out;
+        }
+    }
+    res = 0;
+err_out:
+    if (sort_arr)
+        free(sort_arr);
+    return res;
+}
+
+static void
+decode_cmd_timeout_desc(uint8_t * dp, int max_b_len, char * b,
+                        struct opts_t * op)
+{
+    int len;
+    unsigned int timeout;
+    sgj_state * jsp = &op->json_st;
+
+    if ((max_b_len < 2) || (NULL == dp))
+        return;
+    b[max_b_len - 1] = '\0';
+    --max_b_len;
+    len = sg_get_unaligned_be16(dp + 0);
+    if (10 != len) {
+        snprintf(b, max_b_len, "command timeout descriptor length %d "
+                 "(expect 10)", len);
+        return;
+    }
+    timeout = sg_get_unaligned_be32(dp + 4);
+    if (0 == timeout)
+        snprintf(b, max_b_len, "no nominal timeout, ");
+    else
+        snprintf(b, max_b_len, "nominal timeout: %u secs, ", timeout);
+    if (jsp->pr_as_json) {
+        sgj_js_nv_i(jsp, jsp->userp, "command_specific", dp[3]);
+        sgj_js_nv_i(jsp, jsp->userp, "nominal_command_processing_timeout",
+                    timeout);
+    }
+    len = strlen(b);
+    max_b_len -= len;
+    b += len;
+    timeout = sg_get_unaligned_be32(dp + 8);
+    if (0 == timeout)
+        snprintf(b, max_b_len, "no recommended timeout");
+    else
+        snprintf(b, max_b_len, "recommended timeout: %u secs", timeout);
+    if (jsp->pr_as_json)
+        sgj_js_nv_i(jsp, jsp->userp, "recommended_command_timeout", timeout);
+    return;
+}
+
+/* For decoding a RSOC command's "One_command" parameter data which includes
+ * cdb usage data. */
+static void
+list_one(uint8_t * rsoc_buff, int cd_len, int rep_opts,
+         struct opts_t * op)
+{
+    bool valid = false;
+    int k, mlu, cdlp, rwcdlp, support, ctdp;
+    int n = 0;
+    uint8_t * bp;
+    const char * cp;
+    const char * dlp;
+    const char * mlu_p;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jop = NULL;
+    char name_buff[NAME_BUFF_SZ];
+    char d[64];
+    char b[192];
+    const int blen = sizeof(b);
+
+
+    jop = sgj_named_subobject_r(jsp, jsp->basep, "one_command_descriptor");
+    n += sg_scnpr(b + n, blen - n, "\n  Opcode=0x%.2x", op->opcode);
+    if (rep_opts > 1)
+        n += sg_scnpr(b + n, blen - n, "  Service_action=0x%.4x", op->servact);
+    sgj_pr_hr(jsp, "%s\n", b);
+    sg_get_opcode_sa_name(((op->opcode > 0) ? op->opcode : 0),
+                          ((op->servact > 0) ? op->servact : 0),
+                          peri_dtype, NAME_BUFF_SZ, name_buff);
+    sgj_pr_hr(jsp, "  Command_name: %s\n", name_buff);
+    ctdp = !!(0x80 & rsoc_buff[1]);
+    support = rsoc_buff[1] & 7;
+    switch(support) {
+    case 0:
+        cp = "not currently available";
+        break;
+    case 1:
+        cp = "NOT supported";
+        break;
+    case 3:
+        cp = "supported [conforming to SCSI standard]";
+        valid = true;
+        break;
+    case 5:
+        cp = "supported [in a vendor specific manner]";
+        valid = true;
+        break;
+    default:
+        snprintf(name_buff, NAME_BUFF_SZ, "support reserved [0x%x]",
+                 rsoc_buff[1] & 7);
+        cp = name_buff;
+        break;
+    }
+    cdlp = 0x3 & (rsoc_buff[1] >> 3);
+    rwcdlp = rsoc_buff[0] & 1;
+    switch (cdlp) {
+    case 0:
+        if (rwcdlp)
+            dlp = "Reserved [RWCDLP=1, CDLP=0]";
+        else
+            dlp = "No command duration limit mode page";
+        break;
+    case 1:
+        if (rwcdlp)
+            dlp = "Command duration limit T2A mode page";
+        else
+            dlp = "Command duration limit A mode page";
+        break;
+    case 2:
+        if (rwcdlp)
+            dlp = "Command duration limit T2B mode page";
+        else
+            dlp = "Command duration limit B mode page";
+        break;
+    default:
+        dlp = "reserved [CDLP=3]";
+        break;
+    }
+    sgj_pr_hr(jsp, "  Command is %s\n", cp);
+    sgj_pr_hr(jsp, "  %s\n", dlp);
+    mlu = 0x3 & (rsoc_buff[1] >> 5);
+    switch (mlu) {
+    case 0:
+        mlu_p = "not reported";
+        break;
+    case 1:
+        mlu_p = "affects only this logical unit";
+        break;
+    case 2:
+        mlu_p = "affects more than 1, but not all LUs in this target";
+        break;
+    case 3:
+        mlu_p = "affects all LUs in this target";
+        break;
+    default:
+        snprintf(d, sizeof(d), "reserved [MLU=%d]", mlu);
+        mlu_p = d;
+        break;
+    }
+    sgj_pr_hr(jsp, "  Multiple Logical Units (MLU): %s\n", mlu_p);
+    if (valid) {
+        n = 0;
+        n += sg_scnpr(b + n, blen - n, "  Usage data: ");
+        bp = rsoc_buff + 4;
+        for (k = 0; k < cd_len; ++k)
+            n += sg_scnpr(b + n, blen - n, "%.2x ", bp[k]);
+        sgj_pr_hr(jsp, "%s\n", b);
+    }
+    if (jsp->pr_as_json) {
+        int l;
+
+        snprintf(b, blen, "0x%x", op->opcode);
+        sgj_js_nv_s(jsp, jop, "operation_code", b);
+        if (rep_opts > 1) {
+            snprintf(b, blen, "0x%x", op->servact);
+            sgj_js_nv_s(jsp, jop, "service_action", b);
+        }
+        sgj_js_nv_i(jsp, jop, "rwcdlp", rwcdlp);
+        sgj_js_nv_i(jsp, jop, "ctdp", ctdp);
+        sgj_js_nv_i(jsp, jop, "mlu", mlu);
+        sgj_js_nv_i(jsp, jop, "cdlp", cdlp);
+        sgj_js_nv_i(jsp, jop, "support", support);
+        sgj_js_nv_s(jsp, jop, "support_str", cp);
+        sgj_js_nv_i(jsp, jop, "cdb_size", cd_len);
+        n = 0;
+        for (k = 0; k < cd_len; ++k)
+            n += sg_scnpr(b + n, blen - n, "%.2x ", rsoc_buff[k + 4]);
+        l = strlen(b);
+        if ((l > 0) && (' ' == b[l - 1]))
+            b[l - 1] = '\0';
+        sgj_js_nv_s(jsp, jop, "cdb_usage_data", b);
+    }
+    if (ctdp) {
+        jsp->userp = sgj_named_subobject_r(jsp, jsp->basep,
+                                           "command_timeouts_descriptor");
+        bp = rsoc_buff + 4 + cd_len;
+        decode_cmd_timeout_desc(bp, NAME_BUFF_SZ, name_buff, op);
+        sgj_pr_hr(jsp, "  %s\n", name_buff);
+    }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool as_json;
+    int cd_len, res, len, act_len, rq_len, in_len, vb;
+    int rep_opts = 0;
+    int sg_fd = -1;
+    const char * cp;
+    struct opts_t * op;
+    const char * op_name;
+    uint8_t * rsoc_buff = NULL;
+    uint8_t * free_rsoc_buff = NULL;
+    struct sg_pt_base * ptvp = NULL;
+    sgj_state * jsp;
+    sgj_opaque_p jop = NULL;
+    char buff[48];
+    char b[80];
+    struct sg_simple_inquiry_resp inq_resp;
+    struct opts_t opts;
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->opcode = -1;
+    op->servact = -1;
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return SG_LIB_SYNTAX_ERROR;
+    if (op->do_help) {
+        if (op->opt_new)
+            usage();
+        else
+            usage_old();
+        return 0;
+    }
+    jsp = &op->json_st;
+    as_json = jsp->pr_as_json;
+    if (as_json) {
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        goto fini;
+    }
+    vb = op->verbose;
+    if (op->do_enumerate) {
+        char name_buff[NAME_BUFF_SZ];
+
+        if (op->do_taskman)
+            printf("enumerate not supported with task management "
+                   "functions\n");
+        else {  /* SCSI command */
+            if (op->opcode < 0)
+                op->opcode = 0;
+            if (op->servact < 0)
+                op->servact = 0;
+            if (peri_dtype < 0)
+                peri_dtype = 0;
+            printf("SCSI command:");
+            if (vb)
+                printf(" [opcode=0x%x, sa=0x%x, pdt=0x%x]\n", op->opcode,
+                       op->servact, peri_dtype);
+            else
+                printf("\n");
+            sg_get_opcode_sa_name(op->opcode, op->servact, peri_dtype,
+                                  NAME_BUFF_SZ, name_buff);
+            printf("  %s\n", name_buff);
+        }
+        goto fini;
+    } else if (op->inhex_fn) {
+        if (op->device_name) {
+            if (! as_json)
+                pr2serr("ignoring DEVICE, best to give DEVICE or "
+                        "--inhex=FN, but not both\n");
+            op->device_name = NULL;
+        }
+    } else if (NULL == op->device_name) {
+        pr2serr("No DEVICE argument given\n\n");
+        if (op->opt_new)
+            usage();
+        else
+            usage_old();
+        res = SG_LIB_SYNTAX_ERROR;
+        goto err_out;
+    }
+    if ((-1 != op->servact) && (-1 == op->opcode)) {
+        pr2serr("When '-s' is chosen, so must '-o' be chosen\n");
+        if (op->opt_new)
+            usage();
+        else
+            usage_old();
+        res = SG_LIB_CONTRADICT;
+        goto err_out;
+    }
+    if (op->do_unsorted && op->do_alpha)
+        pr2serr("warning: unsorted ('-u') and alpha ('-a') options chosen, "
+                "ignoring alpha\n");
+    if (op->do_taskman && ((-1 != op->opcode) || op->do_alpha ||
+        op->do_unsorted)) {
+        pr2serr("warning: task management functions ('-t') chosen so alpha "
+                "('-a'),\n          unsorted ('-u') and opcode ('-o') "
+                "options ignored\n");
+    }
+    op_name = op->do_taskman ? "Report supported task management functions" :
+              "Report supported operation codes";
+
+    rsoc_buff = (uint8_t *)sg_memalign(MX_ALLOC_LEN, 0, &free_rsoc_buff,
+                                       false);
+    if (NULL == rsoc_buff) {
+        pr2serr("Unable to allocate memory\n");
+        res = sg_convert_errno(ENOMEM);
+        no_final_msg = true;
+        goto err_out;
+    }
+
+    if (op->inhex_fn) {
+        if ((res = sg_f2hex_arr(op->inhex_fn, op->do_raw, false, rsoc_buff,
+                                &in_len, MX_ALLOC_LEN))) {
+            if (SG_LIB_LBA_OUT_OF_RANGE == res)
+                pr2serr("decode buffer [%d] not large enough??\n",
+                        MX_ALLOC_LEN);
+            goto err_out;
+        }
+        if (op->verbose > 2)
+            pr2serr("Read %d [0x%x] bytes of user supplied data\n",
+                    in_len, in_len);
+        if (op->do_raw)
+            op->do_raw = false;    /* can interfere on decode */
+        if (in_len < 4) {
+            pr2serr("--inhex=%s only decoded %d bytes (needs 4 at "
+                    "least)\n", op->inhex_fn, in_len);
+            res = SG_LIB_SYNTAX_ERROR;
+            goto err_out;
+        }
+        res = 0;
+        act_len = in_len;
+        goto start_response;
+    }
+    if (op->opcode < 0) {
+        /* Try to open read-only */
+        if ((sg_fd = scsi_pt_open_device(op->device_name, true, vb)) < 0) {
+            int err = -sg_fd;
+
+            if (op->verbose)
+                pr2serr("sg_opcodes: error opening file (ro): %s: %s\n",
+                        op->device_name, safe_strerror(err));
+#ifndef SG_LIB_WIN32
+            if (ENOENT == err) {
+                /* file or directory in the file's path doesn't exist, no
+                 * point in retrying with read-write flag */
+                res = sg_convert_errno(err);
+                goto err_out;
+            }
+#endif
+            goto open_rw;
+        }
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, op->verbose);
+        if (NULL == ptvp) {
+            pr2serr("Out of memory (ro)\n");
+            res = sg_convert_errno(ENOMEM);
+            no_final_msg = true;
+            goto err_out;
+        }
+        if (op->no_inquiry && (peri_dtype < 0))
+            pr2serr("--no-inquiry ignored because --pdt= not given\n");
+        if (op->no_inquiry && (peri_dtype >= 0))
+            ;
+        else if (0 == sg_simple_inquiry_pt(ptvp, &inq_resp, true, vb)) {
+            peri_dtype = inq_resp.peripheral_type;
+            if (! (as_json || op->do_raw || op->no_inquiry ||
+                   (op->do_hex > 2))) {
+                printf("  %.8s  %.16s  %.4s\n", inq_resp.vendor,
+                       inq_resp.product, inq_resp.revision);
+                cp = sg_get_pdt_str(peri_dtype, sizeof(buff), buff);
+                if (strlen(cp) > 0)
+                    printf("  Peripheral device type: %s\n", cp);
+                else
+                    printf("  Peripheral device type: 0x%x\n", peri_dtype);
+            }
+        } else {
+            pr2serr("sg_opcodes: %s doesn't respond to a SCSI INQUIRY\n",
+                    op->device_name);
+            res = SG_LIB_CAT_OTHER;
+            no_final_msg = true;
+            goto err_out;
+        }
+    }
+
+open_rw:                /* if not already open */
+    if (sg_fd < 0) {
+        sg_fd = scsi_pt_open_device(op->device_name, false /* RW */, vb);
+        if (sg_fd < 0) {
+            pr2serr("sg_opcodes: error opening file (rw): %s: %s\n",
+                    op->device_name, safe_strerror(-sg_fd));
+            res = sg_convert_errno(-sg_fd);
+            no_final_msg = true;
+            goto err_out;
+        }
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, op->verbose);
+        if (NULL == ptvp) {
+            pr2serr("Out of memory (rw)\n");
+            res = sg_convert_errno(ENOMEM);
+            no_final_msg = true;
+            goto err_out;
+        }
+    }
+    if (op->opcode >= 0)
+        rep_opts = ((op->servact >= 0) ? 2 : 1);
+    if (op->do_taskman) {
+        rq_len = (op->do_repd ? 16 : 4);
+        res = do_rstmf(ptvp, op->do_repd, rsoc_buff, rq_len, &act_len, true,
+                       vb);
+    } else {
+        rq_len = MX_ALLOC_LEN;
+        res = do_rsoc(ptvp, op->do_rctd, rep_opts, op->opcode, op->servact,
+                      rsoc_buff, rq_len, &act_len, true, vb);
+    }
+    if (res) {
+        sg_get_category_sense_str(res, sizeof(b), b, vb);
+        pr2serr("%s: %s\n", op_name, b);
+        no_final_msg = true;
+        if ((0 == op->servact) && (op->opcode >= 0))
+            pr2serr("    >> perhaps try again without a service action "
+                    "[SA] of 0\n");
+        goto err_out;
+    }
+    act_len = (rq_len < act_len) ? rq_len : act_len;
+
+start_response:
+    if (act_len < 4) {
+        pr2serr("Actual length of response [%d] is too small\n", act_len);
+        res = SG_LIB_CAT_OTHER;
+        no_final_msg = true;
+        goto err_out;
+    }
+    if (op->do_taskman) {
+        if (op->do_raw) {
+            dStrRaw((const char *)rsoc_buff, act_len);
+            goto fini;
+        }
+        if (op->do_hex) {
+            if (op->do_hex > 2)
+                hex2stdout(rsoc_buff, act_len, -1);
+            else  {
+                printf("\nTask Management Functions supported by device:\n");
+                if (2 == op->do_hex)
+                    hex2stdout(rsoc_buff, act_len, 0);
+                else
+                    hex2stdout(rsoc_buff, act_len, 1);
+            }
+            goto fini;
+        }
+        if (jsp->pr_as_json) {
+            sgj_js_nv_b(jsp, jop, "ats", rsoc_buff[0] & 0x80);
+            sgj_js_nv_b(jsp, jop, "atss", rsoc_buff[0] & 0x40);
+            sgj_js_nv_b(jsp, jop, "cacas", rsoc_buff[0] & 0x20);
+            sgj_js_nv_b(jsp, jop, "ctss", rsoc_buff[0] & 0x10);
+            sgj_js_nv_b(jsp, jop, "lurs", rsoc_buff[0] & 0x8);
+            sgj_js_nv_b(jsp, jop, "qts", rsoc_buff[0] & 0x4);
+            sgj_js_nv_b(jsp, jop, "trs", rsoc_buff[0] & 0x2);
+            sgj_js_nv_b(jsp, jop, "ws", rsoc_buff[0] & 0x1);
+            sgj_js_nv_b(jsp, jop, "qaes", rsoc_buff[1] & 0x4);
+            sgj_js_nv_b(jsp, jop, "qtss", rsoc_buff[1] & 0x2);
+            sgj_js_nv_b(jsp, jop, "itnrs", rsoc_buff[1] & 0x1);
+            if (! jsp->pr_out_hr)
+                goto fini;
+        }
+        sgj_pr_hr(jsp, "\nTask Management Functions supported by device:\n");
+        if (rsoc_buff[0] & 0x80)
+            sgj_pr_hr(jsp, "    Abort task\n");
+        if (rsoc_buff[0] & 0x40)
+            sgj_pr_hr(jsp, "    Abort task set\n");
+        if (rsoc_buff[0] & 0x20)
+            sgj_pr_hr(jsp, "    Clear ACA\n");
+        if (rsoc_buff[0] & 0x10)
+            sgj_pr_hr(jsp, "    Clear task set\n");
+        if (rsoc_buff[0] & 0x8)
+            sgj_pr_hr(jsp, "    Logical unit reset\n");
+        if (rsoc_buff[0] & 0x4)
+            sgj_pr_hr(jsp, "    Query task\n");
+        if (rsoc_buff[0] & 0x2)
+            sgj_pr_hr(jsp, "    Target reset (obsolete)\n");
+        if (rsoc_buff[0] & 0x1)
+            sgj_pr_hr(jsp, "    Wakeup (obsolete)\n");
+        if (rsoc_buff[1] & 0x4)
+            sgj_pr_hr(jsp, "    Query asynchronous event\n");
+        if (rsoc_buff[1] & 0x2)
+            sgj_pr_hr(jsp, "    Query task set\n");
+        if (rsoc_buff[1] & 0x1)
+            sgj_pr_hr(jsp, "    I_T nexus reset\n");
+        if (op->do_repd) {
+            if (rsoc_buff[3] < 0xc) {
+                pr2serr("when REPD given, byte 3 of response should be >= "
+                        "12\n");
+                res = SG_LIB_CAT_OTHER;
+                no_final_msg = true;
+                goto err_out;
+            } else
+                sgj_pr_hr(jsp, "  Extended parameter data:\n");
+            sgj_pr_hr(jsp, "    TMFTMOV=%d\n", !!(rsoc_buff[4] & 0x1));
+            sgj_pr_hr(jsp, "    ATTS=%d\n", !!(rsoc_buff[6] & 0x80));
+            sgj_pr_hr(jsp, "    ATSTS=%d\n", !!(rsoc_buff[6] & 0x40));
+            sgj_pr_hr(jsp, "    CACATS=%d\n", !!(rsoc_buff[6] & 0x20));
+            sgj_pr_hr(jsp, "    CTSTS=%d\n", !!(rsoc_buff[6] & 0x10));
+            sgj_pr_hr(jsp, "    LURTS=%d\n", !!(rsoc_buff[6] & 0x8));
+            sgj_pr_hr(jsp, "    QTTS=%d\n", !!(rsoc_buff[6] & 0x4));
+            sgj_pr_hr(jsp, "    QAETS=%d\n", !!(rsoc_buff[7] & 0x4));
+            sgj_pr_hr(jsp, "    QTSTS=%d\n", !!(rsoc_buff[7] & 0x2));
+            sgj_pr_hr(jsp, "    ITNRTS=%d\n", !!(rsoc_buff[7] & 0x1));
+            sgj_pr_hr(jsp, "    tmf long timeout: %u (100 ms units)\n",
+                      sg_get_unaligned_be32(rsoc_buff + 8));
+            sgj_pr_hr(jsp, "    tmf short timeout: %u (100 ms units)\n",
+                      sg_get_unaligned_be32(rsoc_buff + 12));
+        }
+    } else if (0 == rep_opts) {  /* list all supported operation codes */
+        len = sg_get_unaligned_be32(rsoc_buff + 0) + 4;
+        len = (len < act_len) ? len : act_len;
+        if (op->do_raw) {
+            dStrRaw((const char *)rsoc_buff, len);
+            goto fini;
+        }
+        if (op->do_hex) {
+            if (op->do_hex > 2)
+                hex2stdout(rsoc_buff, len, -1);
+            else if (2 == op->do_hex)
+                hex2stdout(rsoc_buff, len, 0);
+            else
+                hex2stdout(rsoc_buff, len, 1);
+            goto fini;
+        }
+        list_all_codes(rsoc_buff, len, op, ptvp);
+    } else {    /* asked about specific command */
+        cd_len = sg_get_unaligned_be16(rsoc_buff + 2);
+        len = cd_len + 4;
+        len = (len < act_len) ? len : act_len;
+        cd_len = (cd_len < act_len) ? cd_len : act_len;
+        if (op->do_raw) {
+            dStrRaw((const char *)rsoc_buff, len);
+            goto fini;
+        }
+        if (op->do_hex) {
+            if (op->do_hex > 2)
+                hex2stdout(rsoc_buff, len, -1);
+            else if (2 == op->do_hex)
+                hex2stdout(rsoc_buff, len, 0);
+            else
+                hex2stdout(rsoc_buff, len, 1);
+            goto fini;
+        }
+        list_one(rsoc_buff, cd_len, rep_opts, op);
+    }
+fini:
+    res = 0;
+
+err_out:
+    if (free_rsoc_buff)
+        free(free_rsoc_buff);
+    if (! op->inhex_fn) {
+        if (ptvp)
+            destruct_scsi_pt_obj(ptvp);
+        if (sg_fd >= 0)
+            scsi_pt_close_device(sg_fd);
+    }
+    if ((0 == op->verbose) && (! no_final_msg)) {
+        if (! sg_if_can2stderr("sg_opcodes failed: ", res))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    res = (res >= 0) ? res : SG_LIB_CAT_OTHER;
+    if (as_json) {
+        if (0 == op->do_hex)
+            sgj_js2file(jsp, NULL, res, stdout);
+        sgj_finish(jsp);
+    }
+    return res;
+}
diff --git a/src/sg_persist.c b/src/sg_persist.c
new file mode 100644
index 0000000..872f16e
--- /dev/null
+++ b/src/sg_persist.c
@@ -0,0 +1,1324 @@
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *  Copyright (C) 2004-2022 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ *  This program issues the SCSI PERSISTENT IN and OUT commands.
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "0.69 20220118";
+
+
+#define PRIN_RKEY_SA     0x0
+#define PRIN_RRES_SA     0x1
+#define PRIN_RCAP_SA     0x2
+#define PRIN_RFSTAT_SA   0x3
+#define PROUT_REG_SA     0x0
+#define PROUT_RES_SA     0x1
+#define PROUT_REL_SA     0x2
+#define PROUT_CLEAR_SA   0x3
+#define PROUT_PREE_SA    0x4
+#define PROUT_PREE_AB_SA 0x5
+#define PROUT_REG_IGN_SA 0x6
+#define PROUT_REG_MOVE_SA 0x7
+#define PROUT_REPL_LOST_SA 0x8
+#define MX_ALLOC_LEN 8192
+#define MX_TIDS 32
+#define MX_TID_LEN 256
+
+#define ME "sg_persist"
+
+#define SG_PERSIST_IN_RDONLY "SG_PERSIST_IN_RDONLY"
+
+struct opts_t {
+    bool inquiry;       /* set true by default (unlike most bools) */
+    bool param_alltgpt;
+    bool param_aptpl;
+    bool param_unreg;
+    bool pr_in;         /* true: PR_IN (def); false: PR_OUT */
+    bool readonly;
+    bool readwrite_force;/* set when '-yy' given. Ooverrides environment
+                            variable SG_PERSIST_IN_RDONLY and opens RW */
+    bool verbose_given;
+    bool version_given;
+    int hex;
+    int num_transportids;
+    int prin_sa;
+    int prout_sa;
+    int verbose;
+    uint32_t alloc_len;
+    uint32_t param_rtp;
+    uint32_t prout_type;
+    uint64_t param_rk;
+    uint64_t param_sark;
+    uint8_t transportid_arr[MX_TIDS * MX_TID_LEN];
+};
+
+
+static struct option long_options[] = {
+    {"alloc-length", required_argument, 0, 'l'},
+    {"alloc_length", required_argument, 0, 'l'},
+    {"clear", no_argument, 0, 'C'},
+    {"device", required_argument, 0, 'd'},
+    {"help", no_argument, 0, 'h'},
+    {"hex", no_argument, 0, 'H'},
+    {"in", no_argument, 0, 'i'},
+    {"maxlen", required_argument, 0, 'm'},
+    {"no-inquiry", no_argument, 0, 'n'},
+    {"no_inquiry", no_argument, 0, 'n'},
+    {"out", no_argument, 0, 'o'},
+    {"param-alltgpt", no_argument, 0, 'Y'},
+    {"param_alltgpt", no_argument, 0, 'Y'},
+    {"param-aptpl", no_argument, 0, 'Z'},
+    {"param_aptpl", no_argument, 0, 'Z'},
+    {"param-rk", required_argument, 0, 'K'},
+    {"param_rk", required_argument, 0, 'K'},
+    {"param-sark", required_argument, 0, 'S'},
+    {"param_sark", required_argument, 0, 'S'},
+    {"param-unreg", no_argument, 0, 'U'},
+    {"param_unreg", no_argument, 0, 'U'},
+    {"preempt", no_argument, 0, 'P'},
+    {"preempt-abort", no_argument, 0, 'A'},
+    {"preempt_abort", no_argument, 0, 'A'},
+    {"prout-type", required_argument, 0, 'T'},
+    {"prout_type", required_argument, 0, 'T'},
+    {"read-full-status", no_argument, 0, 's'},
+    {"read_full_status", no_argument, 0, 's'},
+    {"read-keys", no_argument, 0, 'k'},
+    {"read_keys", no_argument, 0, 'k'},
+    {"readonly", no_argument, 0, 'y'},
+    {"read-reservation", no_argument, 0, 'r'},
+    {"read_reservation", no_argument, 0, 'r'},
+    {"read-status", no_argument, 0, 's'},
+    {"read_status", no_argument, 0, 's'},
+    {"register", no_argument, 0, 'G'},
+    {"register-ignore", no_argument, 0, 'I'},
+    {"register_ignore", no_argument, 0, 'I'},
+    {"register-move", no_argument, 0, 'M'},
+    {"register_move", no_argument, 0, 'M'},
+    {"release", no_argument, 0, 'L'},
+    {"relative-target-port", required_argument, 0, 'Q'},
+    {"relative_target_port", required_argument, 0, 'Q'},
+    {"replace-lost", no_argument, 0, 'z'},
+    {"replace_lost", no_argument, 0, 'z'},
+    {"report-capabilities", no_argument, 0, 'c'},
+    {"report_capabilities", no_argument, 0, 'c'},
+    {"reserve", no_argument, 0, 'R'},
+    {"transport-id", required_argument, 0, 'X'},
+    {"transport_id", required_argument, 0, 'X'},
+    {"unreg", no_argument, 0, 'U'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {0, 0, 0, 0}
+};
+
+static const char * prin_sa_strs[] = {
+    "Read keys",
+    "Read reservation",
+    "Report capabilities",
+    "Read full status",
+    "[reserved 0x4]",
+    "[reserved 0x5]",
+    "[reserved 0x6]",
+    "[reserved 0x7]",
+};
+static const int num_prin_sa_strs = SG_ARRAY_SIZE(prin_sa_strs);
+
+static const char * prout_sa_strs[] = {
+    "Register",
+    "Reserve",
+    "Release",
+    "Clear",
+    "Preempt",
+    "Preempt and abort",
+    "Register and ignore existing key",
+    "Register and move",
+    "Replace lost reservation",
+    "[reserved 0x9]",
+};
+static const int num_prout_sa_strs = SG_ARRAY_SIZE(prout_sa_strs);
+
+static const char * pr_type_strs[] = {
+    "obsolete [0]",
+    "Write Exclusive",
+    "obsolete [2]",
+    "Exclusive Access",
+    "obsolete [4]",
+    "Write Exclusive, registrants only",
+    "Exclusive Access, registrants only",
+    "Write Exclusive, all registrants",
+    "Exclusive Access, all registrants",
+    "obsolete [9]", "obsolete [0xa]", "obsolete [0xb]", "obsolete [0xc]",
+    "obsolete [0xd]", "obsolete [0xe]", "obsolete [0xf]",
+};
+
+
+static void
+usage(int help)
+{
+    if (help < 2) {
+        pr2serr("Usage: sg_persist [OPTIONS] [DEVICE]\n"
+                "  where the main OPTIONS are:\n"
+                "    --clear|-C                 PR Out: Clear\n"
+                "    --help|-h                  print usage message, "
+                "twice for more\n"
+                "    --in|-i                    request PR In command "
+                "(default)\n"
+                "    --out|-o                   request PR Out command\n"
+                "    --param-rk=RK|-K RK        PR Out parameter reservation "
+                "key\n"
+                "                               (RK is in hex)\n"
+                "    --param-sark=SARK|-S SARK    PR Out parameter service "
+                "action\n"
+                "                                 reservation key (SARK is "
+                "in hex)\n"
+                "    --preempt|-P               PR Out: Preempt\n"
+                "    --preempt-abort|-A         PR Out: Preempt and Abort\n"
+                "    --prout-type=TYPE|-T TYPE    PR Out type field (see "
+                "'-hh')\n"
+                "    --read-full-status|-s      PR In: Read Full Status\n"
+                "    --read-keys|-k             PR In: Read Keys "
+                "(default)\n");
+        pr2serr("    --read-reservation|-r      PR In: Read Reservation\n"
+                "    --read-status|-s           PR In: Read Full Status\n"
+                "    --register|-G              PR Out: Register\n"
+                "    --register-ignore|-I       PR Out: Register and Ignore\n"
+                "    --register-move|-M         PR Out: Register and Move\n"
+                "                               for '--register-move'\n"
+                "    --release|-L               PR Out: Release\n"
+                "    --replace-lost|-x          PR Out: Replace Lost "
+                "Reservation\n"
+                "    --report-capabilities|-c   PR In: Report Capabilities\n"
+                "    --reserve|-R               PR Out: Reserve\n"
+                "    --unreg|-U                 optional with PR Out "
+                "Register and Move\n\n"
+                "Performs a SCSI PERSISTENT RESERVE (IN or OUT) command. "
+                "Invoking\n'sg_persist DEVICE' will do a PR In Read Keys "
+                "command. Use '-hh'\nfor more options and TYPE meanings.\n");
+    } else {
+        pr2serr("Usage: sg_persist [OPTIONS] [DEVICE]\n"
+                "  where the other OPTIONS are:\n"
+                "    --alloc-length=LEN|-l LEN    allocation length hex "
+                "value (used with\n"
+                "                                 PR In only) (default: 8192 "
+                "(2000 in hex))\n"
+                "    --device=DEVICE|-d DEVICE    supply DEVICE as an option "
+                "rather than\n"
+                "                                 an argument\n"
+                "    --hex|-H                   output response in hex (for "
+                "PR In commands)\n"
+                "    --maxlen=LEN|-m LEN        allocation length in "
+                "decimal, by default.\n"
+                "                               like --alloc-len= "
+                "(def: 8192, 8k, 2000h)\n"
+                "    --no-inquiry|-n            skip INQUIRY (default: do "
+                "INQUIRY)\n"
+                "    --param-alltgpt|-Y         PR Out parameter "
+                "'ALL_TG_PT'\n"
+                "    --param-aptpl|-Z           PR Out parameter 'APTPL'\n"
+                "    --readonly|-y              open DEVICE read-only (def: "
+                "read-write)\n"
+                "    --relative-target-port=RTPI|-Q RTPI    relative target "
+                "port "
+                "identifier\n"
+                "    --transport-id=TIDS|-X TIDS    one or more "
+                "TransportIDs can\n"
+                "                                   be given in several "
+                "forms\n"
+                "    --verbose|-v               output additional debug "
+                "information\n"
+                "    --version|-V               output version string\n\n"
+                "For the main options use '--help' or '-h' once.\n\n\n");
+        pr2serr("PR Out TYPE field value meanings:\n"
+                "  0:    obsolete (was 'read shared' in SPC)\n"
+                "  1:    write exclusive\n"
+                "  2:    obsolete (was 'read exclusive')\n"
+                "  3:    exclusive access\n"
+                "  4:    obsolete (was 'shared access')\n"
+                "  5:    write exclusive, registrants only\n"
+                "  6:    exclusive access, registrants only\n"
+                "  7:    write exclusive, all registrants\n"
+                "  8:    exclusive access, all registrants\n");
+    }
+}
+
+static int
+prin_work(int sg_fd, const struct opts_t * op)
+{
+    int k, j, num, add_len, add_desc_len;
+    int res = 0;
+    unsigned int pr_gen;
+    uint8_t * bp;
+    uint8_t * pr_buff = NULL;
+    uint8_t * free_pr_buff = NULL;
+
+    pr_buff = sg_memalign(op->alloc_len, 0 /* page aligned */, &free_pr_buff,
+                          false);
+    if (NULL == pr_buff) {
+        pr2serr("%s: unable to allocate %d bytes on heap\n", __func__,
+                op->alloc_len);
+        return sg_convert_errno(ENOMEM);
+    }
+    res = sg_ll_persistent_reserve_in(sg_fd, op->prin_sa, pr_buff,
+                                      op->alloc_len, true, op->verbose);
+    if (res) {
+        char b[64];
+        char bb[80];
+
+        if (op->prin_sa < num_prin_sa_strs)
+            snprintf(b, sizeof(b), "%s", prin_sa_strs[op->prin_sa]);
+        else
+            snprintf(b, sizeof(b), "service action=0x%x", op->prin_sa);
+
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("PR in (%s): command not supported\n", b);
+        else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+            pr2serr("PR in (%s): bad field in cdb or parameter list (perhaps "
+                    "unsupported service action)\n", b);
+        else {
+            sg_get_category_sense_str(res, sizeof(bb), bb, op->verbose);
+            pr2serr("PR in (%s): %s\n", b, bb);
+        }
+        goto fini;
+    }
+    if (PRIN_RCAP_SA == op->prin_sa) {
+        if (8 != pr_buff[1]) {
+            pr2serr("Unexpected response for PRIN Report Capabilities\n");
+            if (op->hex)
+                hex2stdout(pr_buff, pr_buff[1], 1);
+            res = SG_LIB_CAT_MALFORMED;
+            goto fini;
+        }
+        if (op->hex)
+            hex2stdout(pr_buff, 8, 1);
+        else {
+            printf("Report capabilities response:\n");
+            printf("  Replace Lost Reservation Capable(RLR_C): %d\n",
+                   !!(pr_buff[2] & 0x80));      /* added spc4r26 */
+            printf("  Compatible Reservation Handling(CRH): %d\n",
+                   !!(pr_buff[2] & 0x10));
+            printf("  Specify Initiator Ports Capable(SIP_C): %d\n",
+                   !!(pr_buff[2] & 0x8));
+            printf("  All Target Ports Capable(ATP_C): %d\n",
+                   !!(pr_buff[2] & 0x4));
+            printf("  Persist Through Power Loss Capable(PTPL_C): %d\n",
+                   !!(pr_buff[2] & 0x1));
+            printf("  Type Mask Valid(TMV): %d\n", !!(pr_buff[3] & 0x80));
+            printf("  Allow Commands: %d\n", (pr_buff[3] >> 4) & 0x7);
+            printf("  Persist Through Power Loss Active(PTPL_A): %d\n",
+                   !!(pr_buff[3] & 0x1));
+            if (pr_buff[3] & 0x80) {
+                printf("    Support indicated in Type mask:\n");
+                printf("      %s: %d\n", pr_type_strs[7],
+                       !!(pr_buff[4] & 0x80));  /* WR_EX_AR */
+                printf("      %s: %d\n", pr_type_strs[6],
+                       !!(pr_buff[4] & 0x40));  /* EX_AC_RO */
+                printf("      %s: %d\n", pr_type_strs[5],
+                       !!(pr_buff[4] & 0x20));  /* WR_EX_RO */
+                printf("      %s: %d\n", pr_type_strs[3],
+                       !!(pr_buff[4] & 0x8));   /* EX_AC */
+                printf("      %s: %d\n", pr_type_strs[1],
+                       !!(pr_buff[4] & 0x2));   /* WR_EX */
+                printf("      %s: %d\n", pr_type_strs[8],
+                       !!(pr_buff[5] & 0x1));   /* EX_AC_AR */
+            }
+        }
+    } else {
+        pr_gen =  sg_get_unaligned_be32(pr_buff + 0);
+        add_len = sg_get_unaligned_be32(pr_buff + 4);
+        if (op->hex) {
+            if (op->hex > 1)
+                hex2stdout(pr_buff, add_len + 8, ((2 == op->hex) ? 1 : -1));
+            else {
+                printf("  PR generation=0x%x, ", pr_gen);
+                if (add_len <= 0)
+                    printf("Additional length=%d\n", add_len);
+                if ((uint32_t)add_len > (op->alloc_len - 8)) {
+                    printf("Additional length too large=%d, truncate\n",
+                           add_len);
+                    hex2stdout((pr_buff + 8), op->alloc_len - 8, 1);
+                } else {
+                    printf("Additional length=%d\n", add_len);
+                    hex2stdout((pr_buff + 8), add_len, 1);
+                }
+            }
+        } else if (PRIN_RKEY_SA == op->prin_sa) {
+            printf("  PR generation=0x%x, ", pr_gen);
+            num = add_len / 8;
+            if (num > 0) {
+                if (1 == num)
+                    printf("1 registered reservation key follows:\n");
+                else
+                    printf("%d registered reservation keys follow:\n", num);
+                bp = pr_buff + 8;
+                for (k = 0; k < num; ++k, bp += 8)
+                    printf("    0x%" PRIx64 "\n",
+                           sg_get_unaligned_be64(bp + 0));
+            } else
+                printf("there are NO registered reservation keys\n");
+        } else if (PRIN_RRES_SA == op->prin_sa) {
+            printf("  PR generation=0x%x, ", pr_gen);
+            num = add_len / 16;
+            if (num > 0) {
+                printf("Reservation follows:\n");
+                bp = pr_buff + 8;
+                printf("    Key=0x%" PRIx64 "\n", sg_get_unaligned_be64(bp));
+                j = ((bp[13] >> 4) & 0xf);
+                if (0 == j)
+                    printf("    scope: LU_SCOPE, ");
+                else
+                    printf("    scope: %d ", j);
+                j = (bp[13] & 0xf);
+                printf(" type: %s\n", pr_type_strs[j]);
+            } else
+                printf("there is NO reservation held\n");
+        } else if (PRIN_RFSTAT_SA == op->prin_sa) {
+            printf("  PR generation=0x%x\n", pr_gen);
+            bp = pr_buff + 8;
+            if (0 == add_len) {
+                printf("  No full status descriptors\n");
+                if (op->verbose)
+                printf("  So there are no registered IT nexuses\n");
+            }
+            for (k = 0; k < add_len; k += num, bp += num) {
+                add_desc_len = sg_get_unaligned_be32(bp + 20);
+                num = 24 + add_desc_len;
+                printf("    Key=0x%" PRIx64 "\n", sg_get_unaligned_be64(bp));
+                if (bp[12] & 0x2)
+                    printf("      All target ports bit set\n");
+                else {
+                    printf("      All target ports bit clear\n");
+                    printf("      Relative port address: 0x%x\n",
+                           sg_get_unaligned_be16(bp + 18));
+                }
+                if (bp[12] & 0x1) {
+                    printf("      << Reservation holder >>\n");
+                    j = ((bp[13] >> 4) & 0xf);
+                    if (0 == j)
+                        printf("      scope: LU_SCOPE, ");
+                    else
+                        printf("      scope: %d ", j);
+                    j = (bp[13] & 0xf);
+                    printf(" type: %s\n", pr_type_strs[j]);
+                } else
+                    printf("      not reservation holder\n");
+                if (add_desc_len > 0) {
+                    char b[1024];
+
+                    printf("%s", sg_decode_transportid_str("      ", bp + 24,
+                                        add_desc_len, true, sizeof(b), b));
+                }
+            }
+        }
+    }
+fini:
+    if (free_pr_buff)
+        free(free_pr_buff);
+    return res;
+}
+
+/* Compact the 2 dimensional transportid_arr into a one dimensional
+ * array in place returning the length. */
+static int
+compact_transportid_array(struct opts_t * op)
+{
+    int k, off, protocol_id, len;
+    int compact_len = 0;
+    uint8_t * bp = op->transportid_arr;
+
+    for (k = 0, off = 0; ((k < op->num_transportids) && (k < MX_TIDS));
+         ++k, off += MX_TID_LEN) {
+        protocol_id = bp[off] & 0xf;
+        if (TPROTO_ISCSI == protocol_id) {
+            len = sg_get_unaligned_be16(bp + off + 2) + 4;
+            if (len < 24)
+                len = 24;
+            if (off > compact_len)
+                memmove(bp + compact_len, bp + off, len);
+            compact_len += len;
+
+        } else {
+            if (off > compact_len)
+                memmove(bp + compact_len, bp + off, 24);
+            compact_len += 24;
+        }
+    }
+    return compact_len;
+}
+
+static int
+prout_work(int sg_fd, struct opts_t * op)
+{
+    int len, t_arr_len;
+    int res = 0;
+    uint8_t * pr_buff = NULL;
+    uint8_t * free_pr_buff = NULL;
+    char b[64];
+    char bb[80];
+
+    t_arr_len = compact_transportid_array(op);
+    pr_buff = sg_memalign(op->alloc_len, 0 /* page aligned */, &free_pr_buff,
+                          false);
+    if (NULL == pr_buff) {
+        pr2serr("%s: unable to allocate %d bytes on heap\n", __func__,
+                op->alloc_len);
+        return sg_convert_errno(ENOMEM);
+    }
+    sg_put_unaligned_be64(op->param_rk, pr_buff + 0);
+    sg_put_unaligned_be64(op->param_sark, pr_buff + 8);
+    if (op->param_alltgpt)
+        pr_buff[20] |= 0x4;
+    if (op->param_aptpl)
+        pr_buff[20] |= 0x1;
+    len = 24;
+    if (t_arr_len > 0) {
+        pr_buff[20] |= 0x8;     /* set SPEC_I_PT bit */
+        memcpy(&pr_buff[28], op->transportid_arr, t_arr_len);
+        len += (t_arr_len + 4);
+        sg_put_unaligned_be32((uint32_t)t_arr_len, pr_buff + 24);
+    }
+    res = sg_ll_persistent_reserve_out(sg_fd, op->prout_sa, 0 /* rq_scope */,
+                                       op->prout_type, pr_buff, len, true,
+                                       op->verbose);
+    if (res || op->verbose) {
+        if (op->prout_sa < num_prout_sa_strs)
+            snprintf(b, sizeof(b), "%s", prout_sa_strs[op->prout_sa]);
+        else
+            snprintf(b, sizeof(b), "service action=0x%x", op->prout_sa);
+        if (res) {
+            if (SG_LIB_CAT_INVALID_OP == res)
+                pr2serr("PR out (%s): command not supported\n", b);
+            else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+                pr2serr("PR out (%s): bad field in cdb or parameter list "
+                        "(perhaps unsupported service action)\n", b);
+            else {
+                sg_get_category_sense_str(res, sizeof(bb), bb, op->verbose);
+                pr2serr("PR out (%s): %s\n", b, bb);
+            }
+            goto fini;
+        } else if (op->verbose)
+            pr2serr("PR out: command (%s) successful\n", b);
+    }
+fini:
+    if (free_pr_buff)
+        free(free_pr_buff);
+    return res;
+}
+
+static int
+prout_reg_move_work(int sg_fd, struct opts_t * op)
+{
+    int len, t_arr_len;
+    int res = 0;
+    uint8_t * pr_buff = NULL;
+    uint8_t * free_pr_buff = NULL;
+
+    t_arr_len = compact_transportid_array(op);
+    pr_buff = sg_memalign(op->alloc_len, 0 /* page aligned */, &free_pr_buff,
+                          false);
+    if (NULL == pr_buff) {
+        pr2serr("%s: unable to allocate %d bytes on heap\n", __func__,
+                op->alloc_len);
+        return sg_convert_errno(ENOMEM);
+    }
+    sg_put_unaligned_be64(op->param_rk, pr_buff + 0);
+    sg_put_unaligned_be64(op->param_sark, pr_buff + 8);
+    if (op->param_unreg)
+        pr_buff[17] |= 0x2;
+    if (op->param_aptpl)
+        pr_buff[17] |= 0x1;
+    sg_put_unaligned_be16(op->param_rtp, pr_buff + 18);
+    len = 24;
+    if (t_arr_len > 0) {
+        memcpy(&pr_buff[24], op->transportid_arr, t_arr_len);
+        len += t_arr_len;
+        sg_put_unaligned_be32((uint32_t)t_arr_len, pr_buff + 20);
+    }
+    res = sg_ll_persistent_reserve_out(sg_fd, PROUT_REG_MOVE_SA,
+                                       0 /* rq_scope */, op->prout_type,
+                                       pr_buff, len, true, op->verbose);
+    if (res) {
+       if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("PR out (register and move): command not supported\n");
+        else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+            pr2serr("PR out (register and move): bad field in cdb or "
+                    "parameter list (perhaps unsupported service action)\n");
+        else {
+            char bb[80];
+
+            sg_get_category_sense_str(res, sizeof(bb), bb, op->verbose);
+            pr2serr("PR out (register and move): %s\n", bb);
+        }
+        goto fini;
+    } else if (op->verbose)
+        pr2serr("PR out: 'register and move' command successful\n");
+fini:
+    if (free_pr_buff)
+        free(free_pr_buff);
+    return res;
+}
+
+/* Decode various symbolic forms of TransportIDs into SPC-4 format.
+ * Returns 1 if one found, else returns 0. */
+static int
+decode_sym_transportid(const char * lcp, uint8_t * tidp)
+{
+    int k, j, n, b, c, len, alen;
+    unsigned int ui;
+    const char * ecp;
+    const char * isip;
+
+    memset(tidp, 0, 24);
+    if ((0 == memcmp("sas,", lcp, 4)) || (0 == memcmp("SAS,", lcp, 4))) {
+        lcp += 4;
+        k = strspn(lcp, "0123456789aAbBcCdDeEfF");
+        if (16 != k) {
+            pr2serr("badly formed symbolic SAS TransportID: %s\n", lcp);
+            return 0;
+        }
+        tidp[0] = TPROTO_SAS;
+        for (k = 0, j = 0, b = 0; k < 16; ++k) {
+            c = lcp[k];
+            if (isdigit(c))
+                n = c - 0x30;
+            else if (isupper(c))
+                n = c - 0x37;
+            else
+                n = c - 0x57;
+            if (k & 1) {
+                tidp[4 + j] = b | n;
+                ++j;
+            } else
+                b = n << 4;
+        }
+        return 1;
+    } else if ((0 == memcmp("spi,", lcp, 4)) ||
+               (0 == memcmp("SPI,", lcp, 4))) {
+        lcp += 4;
+        if (2 != sscanf(lcp, "%d,%d", &b, &c)) {
+            pr2serr("badly formed symbolic SPI TransportID: %s\n", lcp);
+            return 0;
+        }
+        tidp[0] = TPROTO_SPI;
+        sg_put_unaligned_be16((uint16_t)b, tidp + 2);
+        sg_put_unaligned_be16((uint16_t)c, tidp + 6);
+        return 1;
+    } else if ((0 == memcmp("fcp,", lcp, 4)) ||
+               (0 == memcmp("FCP,", lcp, 4))) {
+        lcp += 4;
+        k = strspn(lcp, "0123456789aAbBcCdDeEfF");
+        if (16 != k) {
+            pr2serr("badly formed symbolic FCP TransportID: %s\n", lcp);
+            return 0;
+        }
+        tidp[0] = TPROTO_FCP;
+        for (k = 0, j = 0, b = 0; k < 16; ++k) {
+            c = lcp[k];
+            if (isdigit(c))
+                n = c - 0x30;
+            else if (isupper(c))
+                n = c - 0x37;
+            else
+                n = c - 0x57;
+            if (k & 1) {
+                tidp[8 + j] = b | n;
+                ++j;
+            } else
+                b = n << 4;
+        }
+        return 1;
+    } else if ((0 == memcmp("sbp,", lcp, 4)) ||
+               (0 == memcmp("SBP,", lcp, 4))) {
+        lcp += 4;
+        k = strspn(lcp, "0123456789aAbBcCdDeEfF");
+        if (16 != k) {
+            pr2serr("badly formed symbolic SBP TransportID: %s\n", lcp);
+            return 0;
+        }
+        tidp[0] = TPROTO_1394;
+        for (k = 0, j = 0, b = 0; k < 16; ++k) {
+            c = lcp[k];
+            if (isdigit(c))
+                n = c - 0x30;
+            else if (isupper(c))
+                n = c - 0x37;
+            else
+                n = c - 0x57;
+            if (k & 1) {
+                tidp[8 + j] = b | n;
+                ++j;
+            } else
+                b = n << 4;
+        }
+        return 1;
+    } else if ((0 == memcmp("srp,", lcp, 4)) ||
+               (0 == memcmp("SRP,", lcp, 4))) {
+        lcp += 4;
+        k = strspn(lcp, "0123456789aAbBcCdDeEfF");
+        if (16 != k) {
+            pr2serr("badly formed symbolic SRP TransportID: %s\n", lcp);
+            return 0;
+        }
+        tidp[0] = TPROTO_SRP;
+        for (k = 0, j = 0, b = 0; k < 32; ++k) {
+            c = lcp[k];
+            if (isdigit(c))
+                n = c - 0x30;
+            else if (isupper(c))
+                n = c - 0x37;
+            else
+                n = c - 0x57;
+            if (k & 1) {
+                tidp[8 + j] = b | n;
+                ++j;
+            } else
+                b = n << 4;
+        }
+        return 1;
+    } else if (0 == memcmp("iqn.", lcp, 4)) {
+        ecp = strpbrk(lcp, " \t");
+        isip = strstr(lcp, ",i,0x");
+        if (ecp && (isip > ecp))
+            isip = NULL;
+        len = ecp ? (ecp - lcp) : (int)strlen(lcp);
+        tidp[0] = TPROTO_ISCSI | (isip ? 0x40 : 0x0);
+        alen = len + 1; /* at least one trailing null */
+        if (alen < 20)
+            alen = 20;
+        else if (0 != (alen % 4))
+            alen = ((alen / 4) + 1) * 4;
+        if (alen > 241) { /* sam5r02.pdf A.2 (Annex) */
+            pr2serr("iSCSI name too long, alen=%d\n", alen);
+            return 0;
+        }
+        tidp[3] = alen & 0xff;
+        memcpy(tidp + 4, lcp, len);
+        return 1;
+    } else if ((0 == memcmp("sop,", lcp, 4)) ||
+               (0 == memcmp("SOP,", lcp, 4))) {
+        lcp += 4;
+        if (2 != sscanf(lcp, "%x", &ui)) {
+            pr2serr("badly formed symbolic SOP TransportID: %s\n", lcp);
+            return 0;
+        }
+        tidp[0] = TPROTO_SOP;
+        sg_put_unaligned_be16((uint16_t)ui, tidp + 2);
+        return 1;
+    }
+    pr2serr("unable to parse symbolic TransportID: %s\n", lcp);
+    return 0;
+}
+
+/* Read one or more TransportIDs from the given file or stdin. Reads from
+ * stdin when 'fnp' is NULL. Returns 0 if successful, 1 otherwise. */
+static int
+decode_file_tids(const char * fnp, struct opts_t * op)
+{
+    bool split_line;
+    int in_len, k, j, m;
+    int off = 0;
+    int num = 0;
+    unsigned int h;
+    FILE * fp = stdin;
+    const char * lcp;
+    uint8_t * tid_arr = op->transportid_arr;
+    char line[1024];
+    char carry_over[4];
+
+    if (fnp) {
+        fp = fopen(fnp, "r");
+        if (NULL == fp) {
+            pr2serr("%s: unable to open %s\n", __func__, fnp);
+            return 1;
+        }
+    }
+    carry_over[0] = 0;
+    for (j = 0, off = 0; j < 512; ++j) {
+        if (NULL == fgets(line, sizeof(line), fp))
+            break;
+        in_len = strlen(line);
+        if (in_len > 0) {
+            if ('\n' == line[in_len - 1]) {
+                --in_len;
+                line[in_len] = '\0';
+                split_line = false;
+            } else
+                split_line = true;
+        }
+        if (in_len < 1) {
+            carry_over[0] = 0;
+            continue;
+        }
+        if (carry_over[0]) {
+            if (isxdigit((uint8_t)line[0])) {
+                carry_over[1] = line[0];
+                carry_over[2] = '\0';
+                if (1 == sscanf(carry_over, "%x", &h))
+                    tid_arr[off - 1] = h;       /* back up and overwrite */
+                else {
+                    pr2serr("%s: carry_over error ['%s'] around line %d\n",
+                            __func__, carry_over, j + 1);
+                    goto bad;
+                }
+                lcp = line + 1;
+                --in_len;
+            } else
+                lcp = line;
+            carry_over[0] = 0;
+        } else
+            lcp = line;
+        m = strspn(lcp, " \t");
+        if (m == in_len)
+            continue;
+        lcp += m;
+        in_len -= m;
+        if ('#' == *lcp)
+            continue;
+        if (decode_sym_transportid(lcp, tid_arr + off))
+            goto my_cont_a;
+        k = strspn(lcp, "0123456789aAbBcCdDeEfF ,\t");
+        if ((k < in_len) && ('#' != lcp[k])) {
+            pr2serr("%s: syntax error at line %d, pos %d\n", __func__, j + 1,
+                    m + k + 1);
+            goto bad;
+        }
+        for (k = 0; k < 1024; ++k) {
+            if (1 == sscanf(lcp, "%x", &h)) {
+                if (h > 0xff) {
+                    pr2serr("%s: hex number larger than 0xff in line %d, pos "
+                            "%d\n", __func__, j + 1, (int)(lcp - line + 1));
+                    goto bad;
+                }
+                if (split_line && (1 == strlen(lcp))) {
+                    /* single trailing hex digit might be a split pair */
+                    carry_over[0] = *lcp;
+                }
+                if ((off + k) >= (int)sizeof(op->transportid_arr)) {
+                    pr2serr("%s: array length exceeded\n", __func__);
+                    goto bad;
+                }
+                op->transportid_arr[off + k] = h;/* keep code checker happy */
+                lcp = strpbrk(lcp, " ,\t");
+                if (NULL == lcp)
+                    break;
+                lcp += strspn(lcp, " ,\t");
+                if ('\0' == *lcp)
+                    break;
+            } else {
+                if ('#' == *lcp) {
+                    --k;
+                    break;
+                }
+                pr2serr("%s: error in line %d, at pos %d\n", __func__, j + 1,
+                        (int)(lcp - line + 1));
+                goto bad;
+            }
+        }
+my_cont_a:
+        off += MX_TID_LEN;
+        if (off >= (MX_TIDS * MX_TID_LEN)) {
+            pr2serr("%s: array length exceeded\n", __func__);
+            goto bad;
+        }
+        ++num;
+    }
+    op->num_transportids = num;
+   if (fnp)
+        fclose(fp);
+    return 0;
+
+bad:
+   if (fnp)
+        fclose(fp);
+   return 1;
+}
+
+/* Build transportid array which may contain one or more TransportIDs.
+ * A single TransportID can appear on the command line either as a list of
+ * comma (or single space) separated ASCII hex bytes, or in some transport
+ * protocol specific form (e.g. "sas,5000c50005b32001"). One or more
+ * TransportIDs may be given in a file (syntax: "file=<name>") or read from
+ * stdin in (when "-" is given). Fuller description in manpage of
+ * sg_persist(8). Returns 0 if successful, else 1 .
+ */
+static int
+build_transportid(const char * inp, struct opts_t * op)
+{
+    int in_len;
+    int k = 0;
+    unsigned int h;
+    const char * lcp;
+    uint8_t * tid_arr = op->transportid_arr;
+    char * cp;
+    char * c2p;
+
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len) {
+        op->num_transportids = 0;
+    }
+    if (('-' == inp[0]) ||
+        (0 == memcmp("file=", inp, 5)) ||
+        (0 == memcmp("FILE=", inp, 5))) {
+        if ('-' == inp[0])
+            lcp = NULL;         /* read from stdin */
+        else
+            lcp = inp + 5;      /* read from given file */
+        return decode_file_tids(lcp, op);
+    } else {        /* TransportID given directly on command line */
+        if (decode_sym_transportid(lcp, tid_arr))
+            goto my_cont_b;
+        k = strspn(inp, "0123456789aAbBcCdDeEfF, ");
+        if (in_len != k) {
+            pr2serr("%s: error at pos %d\n", __func__, k + 1);
+            return 1;
+        }
+        for (k = 0; k < (int)sizeof(op->transportid_arr); ++k) {
+            if (1 == sscanf(lcp, "%x", &h)) {
+                if (h > 0xff) {
+                    pr2serr("%s: hex number larger than 0xff at pos %d\n",
+                            __func__, (int)(lcp - inp + 1));
+                    return 1;
+                }
+                tid_arr[k] = h;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("%s: error at pos %d\n", __func__,
+                        (int)(lcp - inp + 1));
+                return 1;
+            }
+        }
+my_cont_b:
+        op->num_transportids = 1;
+        if (k >= (int)sizeof(op->transportid_arr)) {
+            pr2serr("%s: array length exceeded\n", __func__);
+            return 1;
+        }
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool got_maxlen, ok;
+    bool flagged = false;
+    bool want_prin = false;
+    bool want_prout = false;
+    int c, k, res;
+    int help = 0;
+    int num_prin_sa = 0;
+    int num_prout_sa = 0;
+    int num_prout_param = 0;
+    int peri_type = 0;
+    int sg_fd = -1;
+    int ret = 0;
+    const char * cp;
+    const char * device_name = NULL;
+    struct opts_t * op;
+    char buff[48];
+    struct opts_t opts;
+    struct sg_simple_inquiry_resp inq_resp;
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->pr_in = true;
+    op->prin_sa = -1;
+    op->prout_sa = -1;
+    op->inquiry = true;
+    op->alloc_len = MX_ALLOC_LEN;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv,
+                        "AcCd:GHhiIkK:l:Lm:MnoPQ:rRsS:T:UvVX:yYzZ",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'A':
+            op->prout_sa = PROUT_PREE_AB_SA;
+            ++num_prout_sa;
+            break;
+        case 'c':
+            op->prin_sa = PRIN_RCAP_SA;
+            ++num_prin_sa;
+            break;
+        case 'C':
+            op->prout_sa = PROUT_CLEAR_SA;
+            ++num_prout_sa;
+            break;
+        case 'd':
+            device_name = optarg;
+            break;
+        case 'G':
+            op->prout_sa = PROUT_REG_SA;
+            ++num_prout_sa;
+            break;
+        case 'h':
+            ++help;
+            break;
+        case 'H':
+            ++op->hex;
+            break;
+        case 'i':
+            want_prin = true;
+            break;
+        case 'I':
+            op->prout_sa = PROUT_REG_IGN_SA;
+            ++num_prout_sa;
+            break;
+        case 'k':
+            op->prin_sa = PRIN_RKEY_SA;
+            ++num_prin_sa;
+            break;
+        case 'K':
+            if (1 != sscanf(optarg, "%" SCNx64 "", &op->param_rk)) {
+                pr2serr("bad argument to '--param-rk'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            ++num_prout_param;
+            break;
+        case 'm':       /* --maxlen= and --alloc_length= are similar */
+        case 'l':
+            got_maxlen = ('m' == c);
+            cp =  (got_maxlen ? "maxlen" : "alloc-length");
+            if (got_maxlen) {
+                k = sg_get_num(optarg);
+                ok = (-1 != k);
+                op->alloc_len = (unsigned int)k;
+            } else
+                ok = (1 == sscanf(optarg, "%x", &op->alloc_len));
+            if (! ok) {
+                pr2serr("bad argument to '--%s'\n", cp);
+                return SG_LIB_SYNTAX_ERROR;
+            } else if (MX_ALLOC_LEN < op->alloc_len) {
+                pr2serr("'--%s' argument exceeds maximum value (%d)\n", cp,
+                        MX_ALLOC_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'L':
+            op->prout_sa = PROUT_REL_SA;
+            ++num_prout_sa;
+            break;
+        case 'M':
+            op->prout_sa = PROUT_REG_MOVE_SA;
+            ++num_prout_sa;
+            break;
+        case 'n':
+            op->inquiry = false;
+            break;
+        case 'o':
+            want_prout = true;
+            break;
+        case 'P':
+            op->prout_sa = PROUT_PREE_SA;
+            ++num_prout_sa;
+            break;
+        case 'Q':
+            if (1 != sscanf(optarg, "%x", &op->param_rtp)) {
+                pr2serr("bad argument to '--relative-target-port'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (op->param_rtp > 0xffff) {
+                pr2serr("argument to '--relative-target-port' 0 to ffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            ++num_prout_param;
+            break;
+        case 'r':
+            op->prin_sa = PRIN_RRES_SA;
+            ++num_prin_sa;
+            break;
+        case 'R':
+            op->prout_sa = PROUT_RES_SA;
+            ++num_prout_sa;
+            break;
+        case 's':
+            op->prin_sa = PRIN_RFSTAT_SA;
+            ++num_prin_sa;
+            break;
+        case 'S':
+            if (1 != sscanf(optarg, "%" SCNx64 "", &op->param_sark)) {
+                pr2serr("bad argument to '--param-sark'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            ++num_prout_param;
+            break;
+        case 'T':
+            if (1 != sscanf(optarg, "%x", &op->prout_type)) {
+                pr2serr("bad argument to '--prout-type'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            ++num_prout_param;
+            break;
+        case 'U':
+            op->param_unreg = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'X':
+            if (0 != build_transportid(optarg, op)) {
+                pr2serr("bad argument to '--transport-id'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            ++num_prout_param;
+            break;
+        case 'y':       /* differentiates -y, -yy and -yyy */
+            if (! op->readwrite_force) {
+                if (op->readonly) {
+                    op->readwrite_force = true;
+                    op->readonly = false;
+                } else
+                    op->readonly = true;
+            }
+            break;
+        case 'Y':
+            op->param_alltgpt = true;
+            ++num_prout_param;
+            break;
+        case 'z':
+            op->prout_sa = PROUT_REPL_LOST_SA;
+            ++num_prout_sa;
+            break;
+        case 'Z':
+            op->param_aptpl = true;
+            ++num_prout_param;
+            break;
+        case '?':
+            usage(1);
+            return 0;
+        default:
+            pr2serr("unrecognised switch code 0x%x ??\n", c);
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (help > 0) {
+        usage(help);
+        return 0;
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and "
+                "continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("No device name given\n");
+        usage(1);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (want_prout && want_prin) {
+        pr2serr("choose '--in' _or_ '--out' (not both)\n");
+        usage(1);
+        return SG_LIB_CONTRADICT;
+    } else if (want_prout) { /* syntax check on PROUT arguments */
+        op->pr_in = false;
+        if ((1 != num_prout_sa) || (0 != num_prin_sa)) {
+            pr2serr(">> For Persistent Reserve Out one and only one "
+                    "appropriate\n>> service action must be chosen (e.g. "
+                    "'--register')\n");
+            return SG_LIB_CONTRADICT;
+        }
+    } else { /* syntax check on PRIN arguments */
+        if (num_prout_sa > 0) {
+            pr2serr(">> When a service action for Persistent Reserve Out "
+                    "is chosen the\n>> '--out' option must be given (as a "
+                    "safeguard)\n");
+            return SG_LIB_CONTRADICT;
+        }
+        if (0 == num_prin_sa) {
+            pr2serr(">> No service action given; assume Persistent Reserve "
+                    "In command\n>> with Read Keys service action\n");
+            op->prin_sa = 0;
+            ++num_prin_sa;
+        } else if (num_prin_sa > 1)  {
+            pr2serr("Too many service actions given; choose one only\n");
+            usage(1);
+            return SG_LIB_CONTRADICT;
+        }
+    }
+    if ((op->param_unreg || op->param_rtp) &&
+        (PROUT_REG_MOVE_SA != op->prout_sa)) {
+        pr2serr("--unreg or --relative-target-port only useful with "
+                "--register-move\n");
+        usage(1);
+        return SG_LIB_CONTRADICT;
+    }
+    if ((PROUT_REG_MOVE_SA == op->prout_sa) &&
+        (1 != op->num_transportids)) {
+        pr2serr("with --register-move one (and only one) --transport-id "
+                "should be given\n");
+        usage(1);
+        return SG_LIB_CONTRADICT;
+    }
+    if (((PROUT_RES_SA == op->prout_sa) ||
+         (PROUT_REL_SA == op->prout_sa) ||
+         (PROUT_PREE_SA == op->prout_sa) ||
+         (PROUT_PREE_AB_SA == op->prout_sa)) &&
+        (0 == op->prout_type)) {
+        pr2serr("warning>>> --prout-type probably needs to be given\n");
+    }
+    if ((op->verbose > 2) && op->num_transportids) {
+        char b[1024];
+        uint8_t * bp;
+
+        pr2serr("number of tranport-ids decoded from command line (or "
+                "stdin): %d\n", op->num_transportids);
+        pr2serr("  Decode given transport-ids:\n");
+        for (k = 0; k < op->num_transportids; ++k) {
+            bp = op->transportid_arr + (MX_TID_LEN * k);
+            printf("%s", sg_decode_transportid_str("      ", bp, MX_TID_LEN,
+                                                   true, sizeof(b), b));
+        }
+    }
+
+    if (op->inquiry) {
+        if ((sg_fd = sg_cmds_open_device(device_name, true /* ro */,
+                                         op->verbose)) < 0) {
+            pr2serr("%s: error opening file (ro): %s: %s\n", ME,
+                    device_name, safe_strerror(-sg_fd));
+            ret = sg_convert_errno(-sg_fd);
+            flagged = true;
+            goto fini;
+        }
+        ret = sg_simple_inquiry(sg_fd, &inq_resp, true, op->verbose);
+        if (0 == ret) {
+            printf("  %.8s  %.16s  %.4s\n", inq_resp.vendor, inq_resp.product,
+                   inq_resp.revision);
+            peri_type = inq_resp.peripheral_type;
+            cp = sg_get_pdt_str(peri_type, sizeof(buff), buff);
+            if (strlen(cp) > 0)
+                printf("  Peripheral device type: %s\n", cp);
+            else
+                printf("  Peripheral device type: 0x%x\n", peri_type);
+        } else {
+            printf("%s: SCSI INQUIRY failed on %s", ME, device_name);
+            if (ret < 0) {
+                ret = -ret;
+                printf(": %s\n", safe_strerror(ret));
+                ret = sg_convert_errno(ret);
+            } else
+                printf("\n");
+            flagged = true;
+            goto fini;
+        }
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0)
+            pr2serr("%s: sg_cmds_close_device() failed res=%d\n", ME, res);
+    }
+
+    if (! op->readwrite_force) {
+        cp = getenv(SG_PERSIST_IN_RDONLY);
+        if (cp && op->pr_in)
+            op->readonly = true;  /* SG_PERSIST_IN_RDONLY overrides default
+                                     which is open(RW) */
+    } else
+        op->readonly = false;      /* '-yy' force open(RW) */
+    sg_fd = sg_cmds_open_device(device_name, op->readonly, op->verbose);
+    if (sg_fd < 0) {
+        pr2serr("%s: error opening file %s (r%s): %s\n", ME, device_name,
+                (op->readonly ? "o" : "w"), safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        flagged = true;
+        goto fini;
+    }
+
+    if (op->pr_in)
+        ret = prin_work(sg_fd, op);
+    else if (PROUT_REG_MOVE_SA == op->prout_sa)
+        ret = prout_reg_move_work(sg_fd, op);
+    else /* PROUT commands other than 'register and move' */
+        ret = prout_work(sg_fd, op);
+
+fini:
+    if (ret && (0 == op->verbose) && (! flagged)) {
+        if (! sg_if_can2stderr("sg_persist failed: ", ret))
+            pr2serr("Some error occurred [%d]\n", ret);
+    }
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_prevent.c b/src/sg_prevent.c
new file mode 100644
index 0000000..b2076c5
--- /dev/null
+++ b/src/sg_prevent.c
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2004-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ * This program issues the SCSI PREVENT ALLOW MEDIUM REMOVAL command to the
+ * given SCSI device.
+ */
+
+static const char * version_str = "1.13 20220826";
+
+#define ME "sg_prevent: "
+
+
+static struct option long_options[] = {
+    {"allow", no_argument, 0, 'a'},
+    {"help", no_argument, 0, 'h'},
+    {"prevent", required_argument, 0, 'p'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_prevent [--allow] [--help] [--prevent=PC] [--verbose] "
+            "[--version]\n"
+            "                  DEVICE\n"
+            "  where:\n"
+            "    --allow|-a            allow media removal\n"
+            "    --help|-h             print usage message then exit\n"
+            "    --prevent=PC|-p PC    prevent code value (def: 1 -> "
+            "prevent)\n"
+            "                            0 -> allow, 1 -> prevent\n"
+            "                            2 -> persistent allow, 3 -> "
+            "persistent prevent\n"
+            "    --verbose|-v          increase verbosity\n"
+            "    --version|-V          print version string and exit\n\n"
+            "Performs a SCSI PREVENT ALLOW MEDIUM REMOVAL command\n");
+
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool allow = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int sg_fd, res, c;
+    int prevent = -1;
+    int verbose = 0;
+    const char * device_name = NULL;
+    int ret = 0;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "ahp:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            allow = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'p':
+            prevent = sg_get_num(optarg);
+            if ((prevent < 0) || (prevent > 3)) {
+                pr2serr("bad argument to '--prevent'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (allow && (prevent >= 0)) {
+        pr2serr("can't give both '--allow' and '--prevent='\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    if (allow)
+        prevent = 0;
+    else if (prevent < 0)
+        prevent = 1;    /* default is to prevent, as utility name suggests */
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+    res = sg_ll_prevent_allow(sg_fd, prevent, true, verbose);
+    ret = res;
+    if (res) {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Prevent allow medium removal: %s\n", b);
+    }
+    res = sg_cmds_close_device(sg_fd);
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+fini:
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_prevent failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_raw.c b/src/sg_raw.c
new file mode 100644
index 0000000..a0254cd
--- /dev/null
+++ b/src/sg_raw.c
@@ -0,0 +1,849 @@
+/*
+ * A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ * Copyright (C) 2000-2022 Ingo van Lil <inguin@gmx.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program can be used to send raw SCSI commands (with an optional
+ * data phase) through a Generic SCSI interface.
+ */
+
+#define _XOPEN_SOURCE 600       /* clear up posix_memalign() warning */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_pt_nvme.h"
+#include "sg_pr2serr.h"
+#include "sg_unaligned.h"
+
+#define SG_RAW_VERSION "0.4.39 (2022-04-25)"
+
+#define DEFAULT_TIMEOUT 20
+#define MIN_SCSI_CDBSZ 6
+#define MAX_SCSI_CDBSZ 260
+#define MAX_SCSI_DXLEN (1024 * 1024)
+
+#define NVME_ADDR_DATA_IN  0xfffffffffffffffe
+#define NVME_ADDR_DATA_OUT 0xfffffffffffffffd
+#define NVME_DATA_LEN_DATA_IN  0xfffffffe
+#define NVME_DATA_LEN_DATA_OUT 0xfffffffd
+
+static struct option long_options[] = {
+    { "binary",  no_argument,       NULL, 'b' },
+    { "cmdfile", required_argument, NULL, 'c' },
+    { "cmdset",  required_argument, NULL, 'C' },
+    { "enumerate", no_argument,     NULL, 'e' },
+    { "help",    no_argument,       NULL, 'h' },
+    { "infile",  required_argument, NULL, 'i' },
+    { "skip",    required_argument, NULL, 'k' },
+    { "nosense", no_argument,       NULL, 'n' },
+    { "nvm",     no_argument,       NULL, 'N' },
+    { "outfile", required_argument, NULL, 'o' },
+    { "raw",     no_argument,       NULL, 'w' },
+    { "request", required_argument, NULL, 'r' },
+    { "readonly", no_argument,      NULL, 'R' },
+    { "scan",    required_argument, NULL, 'Q' },
+    { "send",    required_argument, NULL, 's' },
+    { "timeout", required_argument, NULL, 't' },
+    { "verbose", no_argument,       NULL, 'v' },
+    { "version", no_argument,       NULL, 'V' },
+    { 0, 0, 0, 0 }
+};
+
+struct opts_t {
+    bool cmdfile_given;
+    bool do_datain;
+    bool datain_binary;
+    bool do_dataout;
+    bool do_enumerate;
+    bool no_sense;
+    bool do_nvm;     /* the NVMe command set: NVM containing its READ+WRITE */
+    bool do_help;
+    bool verbose_given;
+    bool version_given;
+    int cdb_length;
+    int cmdset;
+    int datain_len;
+    int dataout_len;
+    int timeout;
+    int raw;
+    int readonly;
+    int scan_first;
+    int scan_last;
+    int verbose;
+    off_t dataout_offset;
+    uint8_t cdb[MAX_SCSI_CDBSZ];        /* might be NVMe command (64 byte) */
+    const char *cmd_file;
+    const char *datain_file;
+    const char *dataout_file;
+    char *device_name;
+};
+
+
+static void
+pr_version()
+{
+    pr2serr("sg_raw " SG_RAW_VERSION "\n"
+            "Copyright (C) 2007-2021 Ingo van Lil <inguin@gmx.de>\n"
+            "This is free software.  You may redistribute copies of it "
+            "under the terms of\n"
+            "the GNU General Public License "
+            "<https://www.gnu.org/licenses/gpl.html>.\n"
+            "There is NO WARRANTY, to the extent permitted by law.\n");
+}
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_raw [OPTION]* DEVICE [CDB0 CDB1 ...]\n"
+            "\n"
+            "Options:\n"
+            "  --binary|-b            Dump data in binary form, even when "
+            "writing to\n"
+            "                         stdout\n"
+            "  --cmdfile=CF|-c CF     CF is file containing command in hex "
+            "bytes\n"
+            "  --cmdset=CS|-C CS      CS is 0 (def) heuristic chooses "
+            "command set;\n"
+            "                         1: force SCSI; 2: force NVMe\n"
+            "  --enumerate|-e         Decodes cdb name then exits; requires "
+            "DEVICE but\n"
+            "                         ignores it\n"
+            "  --help|-h              Show this message and exit\n"
+            "  --infile=IFILE|-i IFILE    Read binary data to send (i.e. "
+            "data-out)\n"
+            "                             from IFILE (default: stdin)\n"
+            "  --nosense|-n           Don't display sense information\n"
+            "  --nvm|-N               command is for NVM command set (e.g. "
+            "Read);\n"
+            "                         default, if NVMe fd, Admin command "
+            "set\n"
+            "  --outfile=OFILE|-o OFILE    Write binary data from device "
+            "(i.e. data-in)\n"
+            "                              to OFILE (def: hexdump to "
+            "stdout)\n"
+            "  --raw|-w               interpret CF (command file) as "
+            "binary (def:\n"
+            "                         interpret as ASCII hex)\n"
+            "  --readonly|-R          Open DEVICE read-only (default: "
+            "read-write)\n"
+            "  --request=RLEN|-r RLEN    Request up to RLEN bytes of data "
+            "(data-in)\n"
+            "  --scan=FO,LO|-Q FO,LO    scan command set from FO (first "
+            "opcode)\n"
+            "                           to LO (last opcode) inclusive. Uses "
+            "given\n"
+            "                           command bytes, varying the opcode\n"
+            "  --send=SLEN|-s SLEN    Send SLEN bytes of data (data-out)\n"
+            "  --skip=KLEN|-k KLEN    Skip the first KLEN bytes when "
+            "reading\n"
+            "                         data to send (default: 0)\n"
+            "  --timeout=SECS|-t SECS    Timeout in seconds (default: 20)\n"
+            "  --verbose|-v           Increase verbosity\n"
+            "  --version|-V           Show version information and exit\n"
+            "\n"
+            "Between 6 and 260 command bytes (two hex digits each) can be "
+            "specified\nand will be sent to DEVICE. Lengths RLEN, SLEN and "
+            "KLEN are decimal by\ndefault. Bidirectional commands "
+            "accepted.\n\nSimple example: Perform INQUIRY on /dev/sg0:\n"
+            "  sg_raw -r 1k /dev/sg0 12 00 00 00 60 00\n");
+}
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char *argv[])
+{
+    while (1) {
+        int c, n;
+        const char * cp;
+
+        c = getopt_long(argc, argv, "bc:C:ehi:k:nNo:Q:r:Rs:t:vVw",
+                        long_options, NULL);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            op->datain_binary = true;
+            break;
+        case 'c':
+            op->cmd_file = optarg;
+            op->cmdfile_given = true;
+            break;
+        case 'C':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 2)) {
+                pr2serr("Invalid argument to --cmdset= expect 0, 1 or 2\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->cmdset = n;
+            break;
+        case 'e':
+            op->do_enumerate = true;
+            break;
+        case 'h':
+        case '?':
+            op->do_help = true;
+            return 0;
+        case 'i':
+            if (op->dataout_file) {
+                pr2serr("Too many '--infile=' options\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->dataout_file = optarg;
+            break;
+        case 'k':
+            n = sg_get_num(optarg);
+            if (n < 0) {
+                pr2serr("Invalid argument to '--skip'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->dataout_offset = n;
+            break;
+        case 'n':
+            op->no_sense = true;
+            break;
+        case 'N':
+            op->do_nvm = true;
+            break;
+        case 'o':
+            if (op->datain_file) {
+                pr2serr("Too many '--outfile=' options\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->datain_file = optarg;
+            break;
+        case 'Q':       /* --scan=FO,LO */
+            cp = strchr(optarg, ',');
+            if (NULL == cp) {
+                pr2serr("--scan= expects two numbers, comma separated\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 255)) {
+                pr2serr("Invalid first number to --scan= expect 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->scan_first = n;
+            n = sg_get_num(cp + 1);
+            if ((n < 0) || (n > 255)) {
+                pr2serr("Invalid second number to --scan= expect 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->scan_last = n;
+            if (op->scan_first >= n)
+                pr2serr("Warning: scan range degenerate, ignore\n");
+            break;
+        case 'r':
+            op->do_datain = true;
+            n = sg_get_num(optarg);
+            if (n < 0 || n > MAX_SCSI_DXLEN) {
+                pr2serr("Invalid argument to '--request'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->datain_len = n;
+            break;
+        case 'R':
+            ++op->readonly;
+            break;
+        case 's':
+            op->do_dataout = true;
+            n = sg_get_num(optarg);
+            if (n < 0 || n > MAX_SCSI_DXLEN) {
+                pr2serr("Invalid argument to '--send'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->dataout_len = n;
+            break;
+        case 't':
+            n = sg_get_num(optarg);
+            if (n < 0) {
+                pr2serr("Invalid argument to '--timeout'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->timeout = n;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'w':       /* -r and -R already in use, this is --raw */
+            ++op->raw;
+            break;
+        default:
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    if (op->version_given
+#ifdef DEBUG
+        && ! op->verbose_given
+#endif
+       )
+        return 0;
+
+    if (optind >= argc) {
+        pr2serr("No device specified\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    op->device_name = argv[optind];
+    ++optind;
+
+    while (optind < argc) {
+        char *opt = argv[optind++];
+        char *endptr;
+        int cmd = strtol(opt, &endptr, 16);
+
+        if (*opt == '\0' || *endptr != '\0' || cmd < 0x00 || cmd > 0xff) {
+            pr2serr("Invalid command byte '%s'\n", opt);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+
+        if (op->cdb_length >= MAX_SCSI_CDBSZ) {
+            pr2serr("CDB too long (max. %d bytes)\n", MAX_SCSI_CDBSZ);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        op->cdb[op->cdb_length] = cmd;
+        ++op->cdb_length;
+    }
+
+    if (op->cmdfile_given) {
+        int err;
+
+        err = sg_f2hex_arr(op->cmd_file, (op->raw > 0) /* as_binary */,
+                           false /* no_space */, op->cdb, &op->cdb_length,
+                           MAX_SCSI_CDBSZ);
+        if (err) {
+            pr2serr("Unable to parse: %s as %s\n", op->cmd_file,
+                    (op->raw > 0) ? "binary" : "hex");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if (op->verbose > 2) {
+            pr2serr("Read %d from %s . They are in hex:\n", op->cdb_length,
+                    op->cmd_file);
+            hex2stderr(op->cdb, op->cdb_length, -1);
+        }
+    }
+    if (op->cdb_length < MIN_SCSI_CDBSZ) {
+        pr2serr("CDB too short (min. %d bytes)\n", MIN_SCSI_CDBSZ);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (op->do_enumerate || (op->verbose > 1)) {
+        bool is_scsi_cdb = sg_is_scsi_cdb(op->cdb, op->cdb_length);
+        int sa;
+        char b[80];
+
+        if ((1 == op->cmdset) && !is_scsi_cdb) {
+            is_scsi_cdb = true;
+            if (op->verbose > 3)
+                printf(">>> overriding cmdset guess to SCSI\n");
+        }
+        if ((2 == op->cmdset) && is_scsi_cdb) {
+            is_scsi_cdb = false;
+            if (op->verbose > 3)
+                printf(">>> overriding cmdset guess to NVMe\n");
+        }
+        if (is_scsi_cdb) {
+            if (op->cdb_length > 16) {
+                sa = sg_get_unaligned_be16(op->cdb + 8);
+                if ((0x7f != op->cdb[0]) && (0x7e != op->cdb[0]))
+                    printf(">>> Unlikely to be SCSI CDB since all over 16 "
+                           "bytes long should\n>>> start with 0x7f or "
+                           "0x7e\n");
+            } else
+                sa = op->cdb[1] & 0x1f;
+            sg_get_opcode_sa_name(op->cdb[0], sa, 0, sizeof(b), b);
+            printf("Attempt to decode cdb name: %s\n", b);
+        } else
+            printf(">>> Seems to be NVMe %s command\n",
+                   sg_get_nvme_opcode_name(op->cdb[0], ! op->do_nvm,
+                                           sizeof(b), b));
+    }
+    return 0;
+}
+
+static int
+skip(int fd, off_t offset)
+{
+    int err;
+    off_t remain;
+    char buffer[512];
+
+    if (lseek(fd, offset, SEEK_SET) >= 0)
+        return 0;
+
+    // lseek failed; fall back to reading and discarding data
+    remain = offset;
+    while (remain > 0) {
+        ssize_t amount, done;
+        amount = (remain < (off_t)sizeof(buffer)) ? remain
+                                         : (off_t)sizeof(buffer);
+        done = read(fd, buffer, amount);
+        if (done < 0) {
+            err = errno;
+            perror("Error reading input data to skip");
+            return sg_convert_errno(err);
+        } else if (done == 0) {
+            pr2serr("EOF on input file/stream\n");
+            return SG_LIB_FILE_ERROR;
+        } else
+            remain -= done;
+    }
+    return 0;
+}
+
+static uint8_t *
+fetch_dataout(struct opts_t * op, uint8_t ** free_buf, int * errp)
+{
+    bool ok = false;
+    int fd, len, tot_len, boff, err;
+    uint8_t *buf = NULL;
+
+    *free_buf = NULL;
+    if (errp)
+        *errp = 0;
+    if (op->dataout_file) {
+        fd = open(op->dataout_file, O_RDONLY);
+        if (fd < 0) {
+            err = errno;
+            if (errp)
+                *errp = sg_convert_errno(err);
+            perror(op->dataout_file);
+            goto bail;
+        }
+    } else
+        fd = STDIN_FILENO;
+    if (sg_set_binary_mode(fd) < 0) {
+        err = errno;
+        if (errp)
+            *errp = err;
+        perror("sg_set_binary_mode");
+        goto bail;
+    }
+
+    if (op->dataout_offset > 0) {
+        err = skip(fd, op->dataout_offset);
+        if (err != 0) {
+            if (errp)
+                *errp = err;
+            goto bail;
+        }
+    }
+
+    tot_len = op->dataout_len;
+    buf = sg_memalign(tot_len, 0 /* page_size */, free_buf, false);
+    if (buf == NULL) {
+        pr2serr("sg_memalign: failed to get %d bytes of memory\n", tot_len);
+        if (errp)
+            *errp = sg_convert_errno(ENOMEM);
+        goto bail;
+    }
+
+    for (boff = 0; boff < tot_len; boff += len) {
+        len = read(fd, buf + boff , tot_len - boff);
+        if (len < 0) {
+            err = errno;
+            if (errp)
+                *errp = sg_convert_errno(err);
+            perror("Failed to read input data");
+            goto bail;
+        } else if (0 == len) {
+            if (errp)
+                *errp = SG_LIB_FILE_ERROR;
+            pr2serr("EOF on input file/stream at buffer offset %d\n", boff);
+            goto bail;
+        }
+    }
+    ok = true;
+
+bail:
+    if (fd >= 0 && fd != STDIN_FILENO)
+        close(fd);
+    if (! ok) {
+        if (*free_buf) {
+            free(*free_buf);
+            *free_buf = NULL;
+        }
+        return NULL;
+    }
+    return buf;
+}
+
+static int
+write_dataout(const char *filename, uint8_t *buf, int len)
+{
+    int ret = SG_LIB_FILE_ERROR;
+    int fd;
+
+    if ((filename == NULL) ||
+        ((1 == strlen(filename)) && ('-' == filename[0])))
+        fd = STDOUT_FILENO;
+    else {
+        fd = creat(filename, 0666);
+        if (fd < 0) {
+            ret = sg_convert_errno(errno);
+            perror(filename);
+            goto bail;
+        }
+    }
+    if (sg_set_binary_mode(fd) < 0) {
+        perror("sg_set_binary_mode");
+        goto bail;
+    }
+
+    if (write(fd, buf, len) != len) {
+        ret = sg_convert_errno(errno);
+        perror(filename ? filename : "stdout");
+        goto bail;
+    }
+
+    ret = 0;
+
+bail:
+    if (fd >= 0 && fd != STDOUT_FILENO)
+        close(fd);
+    return ret;
+}
+
+
+int
+main(int argc, char *argv[])
+{
+    bool is_scsi_cdb = true;
+    bool do_scan = false;
+    int ret = 0;
+    int err = 0;
+    int res_cat, status, s_len, k, ret2;
+    int sg_fd = -1;
+    uint16_t sct_sc;
+    uint32_t result;
+    struct sg_pt_base *ptvp = NULL;
+    uint8_t sense_buffer[32] SG_C_CPP_ZERO_INIT;
+    uint8_t * dinp = NULL;
+    uint8_t * doutp = NULL;
+    uint8_t * free_buf_out = NULL;
+    uint8_t * wrkBuf = NULL;
+    struct opts_t opts;
+    struct opts_t * op;
+    char b[128];
+    const int b_len = sizeof(b);
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->timeout = DEFAULT_TIMEOUT;
+    ret = parse_cmd_line(op, argc, argv);
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr_version();
+        goto done;
+    }
+
+    if (ret != 0) {
+        pr2serr("\n");  /* blank line before outputting usage */
+        usage();
+        goto done;
+    } else if (op->do_help) {
+        usage();
+        goto done;
+    } else if (op->do_enumerate)
+        goto done;
+
+    sg_fd = scsi_pt_open_device(op->device_name, op->readonly,
+                                op->verbose);
+    if (sg_fd < 0) {
+        pr2serr("%s: %s\n", op->device_name, safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto done;
+    }
+
+    ptvp = construct_scsi_pt_obj_with_fd(sg_fd, op->verbose);
+    if (ptvp == NULL) {
+        pr2serr("construct_scsi_pt_obj_with_fd() failed\n");
+        ret = SG_LIB_CAT_OTHER;
+        goto done;
+    }
+
+    if (op->scan_first < op->scan_last)
+        do_scan = true;
+
+and_again:
+    if (do_scan) {
+        op->cdb[0] = op->scan_first;
+        printf("Command bytes in hex:");
+        for (k = 0; k < op->cdb_length; ++k)
+            printf(" %02x", op->cdb[k]);
+        printf("\n");
+    }
+
+    is_scsi_cdb = sg_is_scsi_cdb(op->cdb, op->cdb_length);
+    if ((1 == op->cmdset) && !is_scsi_cdb)
+        is_scsi_cdb = true;
+    else if ((2 == op->cmdset) && is_scsi_cdb)
+        is_scsi_cdb = false;
+
+    if (op->do_dataout) {
+        uint32_t dout_len;
+
+        doutp = fetch_dataout(op, &free_buf_out, &err);
+        if (doutp == NULL) {
+            ret = err;
+            goto done;
+        }
+        dout_len = op->dataout_len;
+        if (op->verbose > 2)
+            pr2serr("dxfer_buffer_out=%p, length=%d\n",
+                    (void *)doutp, dout_len);
+        set_scsi_pt_data_out(ptvp, doutp, dout_len);
+        if (op->cmdfile_given) {
+            if (NVME_ADDR_DATA_OUT ==
+                sg_get_unaligned_le64(op->cdb + SG_NVME_PT_ADDR))
+                sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)doutp,
+                                      op->cdb + SG_NVME_PT_ADDR);
+            if (NVME_DATA_LEN_DATA_OUT ==
+                sg_get_unaligned_le32(op->cdb + SG_NVME_PT_DATA_LEN))
+                sg_put_unaligned_le32(dout_len,
+                                      op->cdb + SG_NVME_PT_DATA_LEN);
+        }
+    }
+    if (op->do_datain) {
+        uint32_t din_len = op->datain_len;
+
+        dinp = sg_memalign(din_len, 0 /* page_size */, &wrkBuf, false);
+        if (dinp == NULL) {
+            pr2serr("sg_memalign: failed to get %d bytes of memory\n",
+                    din_len);
+            ret = sg_convert_errno(ENOMEM);
+            goto done;
+        }
+        if (op->verbose > 2)
+            pr2serr("dxfer_buffer_in=%p, length=%d\n", (void *)dinp, din_len);
+        set_scsi_pt_data_in(ptvp, dinp, din_len);
+        if (op->cmdfile_given) {
+            if (NVME_ADDR_DATA_IN ==
+                sg_get_unaligned_le64(op->cdb + SG_NVME_PT_ADDR))
+                sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)dinp,
+                                      op->cdb + SG_NVME_PT_ADDR);
+            if (NVME_DATA_LEN_DATA_IN ==
+                sg_get_unaligned_le32(op->cdb + SG_NVME_PT_DATA_LEN))
+                sg_put_unaligned_le32(din_len,
+                                      op->cdb + SG_NVME_PT_DATA_LEN);
+        }
+    }
+    if (op->verbose) {
+        char d[128];
+
+        pr2serr("    %s to send: ", is_scsi_cdb ? "cdb" : "cmd");
+        if (is_scsi_cdb) {
+            pr2serr("%s\n", sg_get_command_str(op->cdb, op->cdb_length,
+                                               op->verbose > 1,
+                                               sizeof(d), d));
+        } else {        /* If not SCSI cdb then treat as NVMe command */
+            pr2serr("\n");
+            hex2stderr(op->cdb, op->cdb_length, -1);
+            if (op->verbose > 1)
+                pr2serr("  Command name: %s\n",
+                        sg_get_nvme_opcode_name(op->cdb[0], ! op->do_nvm,
+                                                b_len, b));
+        }
+    }
+    set_scsi_pt_cdb(ptvp, op->cdb, op->cdb_length);
+    if (op->verbose > 2)
+        pr2serr("sense_buffer=%p, length=%d\n", (void *)sense_buffer,
+                (int)sizeof(sense_buffer));
+    set_scsi_pt_sense(ptvp, sense_buffer, sizeof(sense_buffer));
+
+    if (op->do_nvm)
+        ret = do_nvm_pt(ptvp, 0, op->timeout, op->verbose);
+    else
+        ret = do_scsi_pt(ptvp, -1, op->timeout, op->verbose);
+    if (ret > 0) {
+        switch (ret) {
+        case SCSI_PT_DO_BAD_PARAMS:
+            pr2serr("do_scsi_pt: bad pass through setup\n");
+            ret = SG_LIB_CAT_OTHER;
+            break;
+        case SCSI_PT_DO_TIMEOUT:
+            pr2serr("do_scsi_pt: timeout\n");
+            ret = SG_LIB_CAT_TIMEOUT;
+            break;
+        case SCSI_PT_DO_NVME_STATUS:
+            sct_sc = (uint16_t)get_scsi_pt_status_response(ptvp);
+            pr2serr("NVMe Status: %s [0x%x]\n",
+                    sg_get_nvme_cmd_status_str(sct_sc, b_len, b), sct_sc);
+            if (op->verbose) {
+                result = get_pt_result(ptvp);
+                pr2serr("NVMe Result=0x%x\n", result);
+                s_len = get_scsi_pt_sense_len(ptvp);
+                if ((op->verbose > 1) && (s_len > 0)) {
+                    pr2serr("NVMe completion queue 4 DWords (as byte "
+                            "string):\n");
+                    hex2stderr(sense_buffer, s_len, -1);
+                }
+            }
+            break;
+        case SCSI_PT_DO_NOT_SUPPORTED:
+            pr2serr("do_scsi_pt: not supported\n");
+            ret = SG_LIB_CAT_TIMEOUT;
+            break;
+        default:
+            pr2serr("do_scsi_pt: unknown error: %d\n", ret);
+            ret = SG_LIB_CAT_OTHER;
+            break;
+        }
+        goto done;
+    } else if (ret < 0) {
+        k = -ret;
+        pr2serr("do_scsi_pt: %s\n", safe_strerror(k));
+        err = get_scsi_pt_os_err(ptvp);
+        if ((err != 0) && (err != k))
+            pr2serr("    ... or perhaps: %s\n", safe_strerror(err));
+        ret = sg_convert_errno(err);
+        goto done;
+    }
+
+    s_len = get_scsi_pt_sense_len(ptvp);
+    if (is_scsi_cdb) {
+        res_cat = get_scsi_pt_result_category(ptvp);
+        switch (res_cat) {
+        case SCSI_PT_RESULT_GOOD:
+            ret = 0;
+            break;
+        case SCSI_PT_RESULT_SENSE:
+            ret = sg_err_category_sense(sense_buffer, s_len);
+            break;
+        case SCSI_PT_RESULT_TRANSPORT_ERR:
+            get_scsi_pt_transport_err_str(ptvp, b_len, b);
+            pr2serr(">>> transport error: %s\n", b);
+            ret = SG_LIB_CAT_OTHER;
+            break;
+        case SCSI_PT_RESULT_OS_ERR:
+            get_scsi_pt_os_err_str(ptvp, b_len, b);
+            pr2serr(">>> os error: %s\n", b);
+            ret = SG_LIB_CAT_OTHER;
+            break;
+        default:
+            pr2serr(">>> unknown pass through result category (%d)\n",
+                    res_cat);
+            ret = SG_LIB_CAT_OTHER;
+            break;
+        }
+
+        status = get_scsi_pt_status_response(ptvp);
+        pr2serr("SCSI Status: ");
+        sg_print_scsi_status(status);
+        pr2serr("\n\n");
+        if ((SAM_STAT_CHECK_CONDITION == status) && (! op->no_sense)) {
+            if (0 == s_len)
+                pr2serr(">>> Strange: status is CHECK CONDITION but no Sense "
+                        "Information\n");
+            else {
+                pr2serr("Sense Information:\n");
+                sg_print_sense(NULL, sense_buffer, s_len, (op->verbose > 0));
+                pr2serr("\n");
+            }
+        }
+        if (SAM_STAT_RESERVATION_CONFLICT == status)
+            ret = SG_LIB_CAT_RES_CONFLICT;
+    } else {    /* NVMe command */
+        result = get_pt_result(ptvp);
+        pr2serr("NVMe Result=0x%x\n", result);
+        if (op->verbose && (s_len > 0)) {
+            pr2serr("NVMe completion queue 4 DWords (as byte string):\n");
+            hex2stderr(sense_buffer, s_len, -1);
+        }
+    }
+
+    if (op->do_datain) {
+        int data_len = op->datain_len - get_scsi_pt_resid(ptvp);
+
+        if (ret && !(SG_LIB_CAT_RECOVERED == ret ||
+                     SG_LIB_CAT_NO_SENSE == ret))
+            pr2serr("Error %d occurred, no data received\n", ret);
+        else if (data_len == 0) {
+            pr2serr("No data received\n");
+        } else {
+            if (op->datain_file == NULL && !op->datain_binary) {
+                pr2serr("Received %d bytes of data:\n", data_len);
+                hex2stderr(dinp, data_len, 0);
+            } else {
+                const char * cp = "stdout";
+
+                if (op->datain_file &&
+                    ! ((1 == strlen(op->datain_file)) &&
+                       ('-' == op->datain_file[0])))
+                    cp = op->datain_file;
+                pr2serr("Writing %d bytes of data to %s\n", data_len, cp);
+                ret2 = write_dataout(op->datain_file, dinp,
+                                     data_len);
+                if (0 != ret2) {
+                    if (0 == ret)
+                        ret = ret2;
+                    goto done;
+                }
+            }
+        }
+    }
+
+done:
+    if (do_scan) {
+        ++op->scan_first;
+        if (op->scan_first <= op->scan_last) {
+            clear_scsi_pt_obj(ptvp);
+            goto and_again;
+        }
+    }
+
+    if (op->verbose && is_scsi_cdb) {
+        sg_get_category_sense_str(ret, b_len, b, op->verbose - 1);
+        pr2serr("%s\n", b);
+    }
+    if (wrkBuf)
+        free(wrkBuf);
+    if (free_buf_out)
+        free(free_buf_out);
+    if (ptvp)
+        destruct_scsi_pt_obj(ptvp);
+    if (sg_fd >= 0)
+        scsi_pt_close_device(sg_fd);
+    return ret >= 0 ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_rbuf.c b/src/sg_rbuf.c
new file mode 100644
index 0000000..d37cc25
--- /dev/null
+++ b/src/sg_rbuf.c
@@ -0,0 +1,688 @@
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *  Copyright (C) 1999-2022 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program uses the SCSI command READ BUFFER on the given
+ * device, first to find out how big it is and then to read that
+ * buffer (data mode, buffer id 0).
+ */
+
+
+#define _XOPEN_SOURCE 600
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/time.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+#define RB_MODE_DESC 3
+#define RB_MODE_DATA 2
+#define RB_MODE_ECHO_DESC 0xb
+#define RB_MODE_ECHO_DATA 0xa
+#define RB_DESC_LEN 4
+#define RB_DEF_SIZE (200*1024*1024)
+#define RB_OPCODE 0x3C
+#define RB_CMD_LEN 10
+
+#ifndef SG_FLAG_MMAP_IO
+#define SG_FLAG_MMAP_IO 4
+#endif
+
+
+static const char * version_str = "5.09 20220425";
+
+static struct option long_options[] = {
+        {"buffer", required_argument, 0, 'b'},
+        {"dio", no_argument, 0, 'd'},
+        {"echo", no_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"mmap", no_argument, 0, 'm'},
+        {"new", no_argument, 0, 'N'},
+        {"old", no_argument, 0, 'O'},
+        {"quick", no_argument, 0, 'q'},
+        {"size", required_argument, 0, 's'},
+        {"time", no_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool do_dio;
+    bool do_echo;
+    bool do_mmap;
+    bool do_quick;
+    bool do_time;
+    bool verbose_given;
+    bool version_given;
+    bool opt_new;
+    int do_buffer;
+    int do_help;
+    int verbose;
+    int64_t do_size;
+    const char * device_name;
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_rbuf [--buffer=EACH] [--dio] [--echo] "
+            "[--help] [--mmap]\n"
+            "               [--quick] [--size=OVERALL] [--time] [--verbose] "
+            "[--version]\n"
+            "               SG_DEVICE\n");
+    pr2serr("  where:\n"
+            "    --buffer=EACH|-b EACH    buffer size to use (in bytes)\n"
+            "    --dio|-d        requests dio ('-q' overrides it)\n"
+            "    --echo|-e       use echo buffer (def: use data mode)\n"
+            "    --help|-h       print usage message then exit\n"
+            "    --mmap|-m       requests mmap-ed IO (overrides -q, -d)\n"
+            "    --quick|-q      quick, don't xfer to user space\n");
+    pr2serr("    --size=OVERALL|-s OVERALL    total size to read (in bytes)\n"
+            "                    default: 200 MiB\n"
+            "    --time|-t       time the data transfer\n"
+            "    --verbose|-v    increase verbosity (more debug)\n"
+            "    --old|-O        use old interface (use as first option)\n"
+            "    --version|-V    print version string then exit\n\n"
+            "Use SCSI READ BUFFER command (data or echo buffer mode, buffer "
+            "id 0)\nrepeatedly. This utility only works with Linux sg "
+            "devices.\n");
+}
+
+static void
+usage_old()
+{
+    printf("Usage: sg_rbuf [-b=EACH_KIB] [-d] [-m] [-q] [-s=OVERALL_MIB] "
+           "[-t] [-v] [-V]\n               SG_DEVICE\n");
+    printf("  where:\n");
+    printf("    -b=EACH_KIB    num is buffer size to use (in KiB)\n");
+    printf("    -d       requests dio ('-q' overrides it)\n");
+    printf("    -e       use echo buffer (def: use data mode)\n");
+    printf("    -m       requests mmap-ed IO (overrides -q, -d)\n");
+    printf("    -q       quick, don't xfer to user space\n");
+    printf("    -s=OVERALL_MIB    num is total size to read (in MiB) "
+           "(default: 200 MiB)\n");
+    printf("             maximum total size is 4000 MiB\n");
+    printf("    -t       time the data transfer\n");
+    printf("    -v       increase verbosity (more debug)\n");
+    printf("    -N|--new use new interface\n");
+    printf("    -V       print version string then exit\n\n");
+    printf("Use SCSI READ BUFFER command (data or echo buffer mode, buffer "
+           "id 0)\nrepeatedly. This utility only works with Linux sg "
+            "devices.\n");
+}
+
+static void
+usage_for(const struct opts_t * op)
+{
+    if (op->opt_new)
+        usage();
+    else
+        usage_old();
+}
+
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int c, n;
+    int64_t nn;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "b:dehmNOqs:tvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            n = sg_get_num(optarg);
+            if (n < 0) {
+                pr2serr("bad argument to '--buffer'\n");
+                usage_for(op);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_buffer = n;
+            break;
+        case 'd':
+            op->do_dio = true;
+            break;
+        case 'e':
+            op->do_echo = true;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'm':
+            op->do_mmap = true;
+            break;
+        case 'N':
+            break;      /* ignore */
+        case 'O':
+            op->opt_new = false;
+            return 0;
+        case 'q':
+            op->do_quick = true;
+            break;
+        case 's':
+           nn = sg_get_llnum(optarg);
+           if (nn < 0) {
+                pr2serr("bad argument to '--size'\n");
+                usage_for(op);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_size = nn;
+            break;
+        case 't':
+            op->do_time = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage_for(op);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage_for(op);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool jmp_out;
+    int k, plen, num;
+    int64_t nn;
+    const char * cp;
+
+    for (k = 1; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case 'd':
+                    op->do_dio = true;
+                    break;
+                case 'e':
+                    op->do_echo = true;
+                    break;
+                case 'h':
+                case '?':
+                    ++op->do_help;
+                    break;
+                case 'm':
+                    op->do_mmap = true;
+                    break;
+                case 'N':
+                    op->opt_new = true;
+                    return 0;
+                case 'O':
+                    break;
+                case 'q':
+                    op->do_quick = true;
+                    break;
+                case 't':
+                    op->do_time = true;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            if (0 == strncmp("b=", cp, 2)) {
+                num = sscanf(cp + 2, "%d", &op->do_buffer);
+                if ((1 != num) || (op->do_buffer <= 0)) {
+                    printf("Couldn't decode number after 'b=' option\n");
+                    usage_for(op);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->do_buffer *= 1024;
+            }
+            else if (0 == strncmp("s=", cp, 2)) {
+                nn = sg_get_llnum(optarg);
+                if (nn < 0) {
+                    printf("Couldn't decode number after 's=' option\n");
+                    usage_for(op);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->do_size = nn;
+                op->do_size *= 1024 * 1024;
+            } else if (0 == strncmp("-old", cp, 4))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage_for(op);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == op->device_name)
+            op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not expecting: %s\n",
+                    op->device_name, cp);
+            usage_for(op);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opt_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opt_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opt_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (! op->opt_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+#ifdef DEBUG
+    bool clear = true;
+#endif
+    bool dio_incomplete = false;
+    int sg_fd, res, err;
+    int buf_capacity = 0;
+    int buf_size = 0;
+    size_t psz;
+    unsigned int k, num;
+    int64_t total_size = RB_DEF_SIZE;
+    struct opts_t * op;
+    uint8_t * rbBuff = NULL;
+    void * rawp = NULL;
+    uint8_t sense_buffer[32] SG_C_CPP_ZERO_INIT;
+    uint8_t rb_cdb [RB_CMD_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr;
+    struct timeval start_tm, end_tm;
+    struct opts_t opts;
+
+#if defined(HAVE_SYSCONF) && defined(_SC_PAGESIZE)
+    psz = sysconf(_SC_PAGESIZE); /* POSIX.1 (was getpagesize()) */
+#else
+    psz = 4096;     /* give up, pick likely figure */
+#endif
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return SG_LIB_SYNTAX_ERROR;
+    if (op->do_help) {
+        usage_for(op);
+        return 0;
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == op->device_name) {
+        pr2serr("No DEVICE argument given\n\n");
+        usage_for(op);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (op->do_buffer > 0)
+        buf_size = op->do_buffer;
+    if (op->do_size > 0)
+        total_size = op->do_size;
+
+    sg_fd = open(op->device_name, O_RDONLY | O_NONBLOCK);
+    if (sg_fd < 0) {
+        err = errno;
+        perror("device open error");
+        return sg_convert_errno(err);
+    }
+    if (op->do_mmap) {
+        op->do_dio = false;
+        op->do_quick = false;
+    }
+    if (NULL == (rawp = malloc(512))) {
+        printf("out of memory (query)\n");
+        return SG_LIB_CAT_OTHER;
+    }
+    rbBuff = (uint8_t *)rawp;
+
+    rb_cdb[0] = RB_OPCODE;
+    rb_cdb[1] = op->do_echo ? RB_MODE_ECHO_DESC : RB_MODE_DESC;
+    rb_cdb[8] = RB_DESC_LEN;
+    memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(rb_cdb);
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = RB_DESC_LEN;
+    io_hdr.dxferp = rbBuff;
+    io_hdr.cmdp = rb_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 60000;     /* 60000 millisecs == 60 seconds */
+    if (op->verbose) {
+        char b[128];
+
+        pr2serr("    Read buffer (%sdescriptor) cdb: %s\n",
+                (op->do_echo ? "echo " : ""),
+                sg_get_command_str(rb_cdb, RB_CMD_LEN, false, sizeof(b), b));
+    }
+
+    /* do normal IO to find RB size (not dio or mmap-ed at this stage) */
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        perror("SG_IO READ BUFFER descriptor error");
+        if (rawp)
+            free(rawp);
+        return SG_LIB_CAT_OTHER;
+    }
+
+    if (op->verbose > 2)
+        pr2serr("      duration=%u ms\n", io_hdr.duration);
+    /* now for the error processing */
+    res = sg_err_category3(&io_hdr);
+    switch (res) {
+    case SG_LIB_CAT_RECOVERED:
+        sg_chk_n_print3("READ BUFFER descriptor, continuing", &io_hdr,
+                        op->verbose > 1);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+        __attribute__((fallthrough));
+        /* FALL THROUGH */
+#endif
+#endif
+    case SG_LIB_CAT_CLEAN:
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("READ BUFFER descriptor error", &io_hdr,
+                        op->verbose > 1);
+        if (rawp) free(rawp);
+        return (res >= 0) ? res : SG_LIB_CAT_OTHER;
+    }
+
+    if (op->do_echo) {
+        buf_capacity = 0x1fff & sg_get_unaligned_be16(rbBuff + 2);
+        printf("READ BUFFER reports: echo buffer capacity=%d\n",
+               buf_capacity);
+    } else {
+        buf_capacity = sg_get_unaligned_be24(rbBuff + 1);
+        printf("READ BUFFER reports: buffer capacity=%d, offset "
+               "boundary=%d\n", buf_capacity, (int)rbBuff[0]);
+    }
+
+    if (0 == buf_size)
+        buf_size = buf_capacity;
+    else if (buf_size > buf_capacity) {
+        printf("Requested buffer size=%d exceeds reported capacity=%d\n",
+               buf_size, buf_capacity);
+        if (rawp) free(rawp);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    if (rawp) {
+        free(rawp);
+        rawp = NULL;
+    }
+
+    if (! op->do_dio) {
+        k = buf_size;
+        if (op->do_mmap && (0 != (k % psz)))
+            k = ((k / psz) + 1) * psz;  /* round up to page size */
+        res = ioctl(sg_fd, SG_SET_RESERVED_SIZE, &k);
+        if (res < 0)
+            perror("SG_SET_RESERVED_SIZE error");
+    }
+
+    if (op->do_mmap) {
+        rbBuff = (uint8_t *)mmap(NULL, buf_size, PROT_READ, MAP_SHARED,
+                                       sg_fd, 0);
+        if (MAP_FAILED == rbBuff) {
+            if (ENOMEM == errno) {
+                pr2serr("mmap() out of memory, try a smaller buffer size "
+                        "than %d bytes\n", buf_size);
+                if (op->opt_new)
+                    pr2serr("    [with '--buffer=EACH' where EACH is in "
+                            "bytes]\n");
+                else
+                    pr2serr("    [with '-b=EACH' where EACH is in KiB]\n");
+            } else
+                perror("error using mmap()");
+            return SG_LIB_CAT_OTHER;
+        }
+    }
+    else { /* non mmap-ed IO */
+        rawp = (uint8_t *)malloc(buf_size + (op->do_dio ? psz : 0));
+        if (NULL == rawp) {
+            printf("out of memory (data)\n");
+            return SG_LIB_CAT_OTHER;
+        }
+        /* perhaps use posix_memalign() instead */
+        if (op->do_dio)    /* align to page boundary */
+            rbBuff= (uint8_t *)(((sg_uintptr_t)rawp + psz - 1) &
+                                      (~(psz - 1)));
+        else
+            rbBuff = (uint8_t *)rawp;
+    }
+
+    num = total_size / buf_size;
+    if (op->do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+    }
+    /* main data reading loop */
+    for (k = 0; k < num; ++k) {
+        memset(rb_cdb, 0, RB_CMD_LEN);
+        rb_cdb[0] = RB_OPCODE;
+        rb_cdb[1] = op->do_echo ? RB_MODE_ECHO_DATA : RB_MODE_DATA;
+        sg_put_unaligned_be24((uint32_t)buf_size, rb_cdb + 6);
+#ifdef DEBUG
+        memset(rbBuff, 0, buf_size);
+#endif
+
+        memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+        io_hdr.interface_id = 'S';
+        io_hdr.cmd_len = sizeof(rb_cdb);
+        io_hdr.mx_sb_len = sizeof(sense_buffer);
+        io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+        io_hdr.dxfer_len = buf_size;
+        if (! op->do_mmap)
+            io_hdr.dxferp = rbBuff;
+        io_hdr.cmdp = rb_cdb;
+        io_hdr.sbp = sense_buffer;
+        io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        io_hdr.pack_id = k;
+        if (op->do_mmap)
+            io_hdr.flags |= SG_FLAG_MMAP_IO;
+        else if (op->do_dio)
+            io_hdr.flags |= SG_FLAG_DIRECT_IO;
+        else if (op->do_quick)
+            io_hdr.flags |= SG_FLAG_NO_DXFER;
+        if (op->verbose > 1) {
+            char b[128];
+
+            pr2serr("    Read buffer (%sdata) cdb: %s\n",
+                    (op->do_echo ? "echo " : ""),
+                    sg_get_command_str(rb_cdb, RB_CMD_LEN, false,
+                                       sizeof(b), b));
+        }
+        if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+            if (ENOMEM == errno) {
+                pr2serr("SG_IO data: out of memory, try a smaller buffer "
+                        "size than %d bytes\n", buf_size);
+                if (op->opt_new)
+                    pr2serr("    [with '--buffer=EACH' where EACH is in "
+                            "bytes]\n");
+                else
+                    pr2serr("    [with '-b=EACH' where EACH is in KiB]\n");
+            } else
+                perror("SG_IO READ BUFFER data error");
+            if (rawp) free(rawp);
+            return SG_LIB_CAT_OTHER;
+        }
+
+        if (op->verbose > 2)
+            pr2serr("      duration=%u ms\n", io_hdr.duration);
+        /* now for the error processing */
+        res = sg_err_category3(&io_hdr);
+        switch (res) {
+        case SG_LIB_CAT_CLEAN:
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            sg_chk_n_print3("READ BUFFER data, continuing", &io_hdr,
+                            op->verbose > 1);
+            break;
+        default: /* won't bother decoding other categories */
+            sg_chk_n_print3("READ BUFFER data error", &io_hdr,
+                            op->verbose > 1);
+            if (rawp) free(rawp);
+            return (res >= 0) ? res : SG_LIB_CAT_OTHER;
+        }
+        if (op->do_dio &&
+            ((io_hdr.info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+            dio_incomplete = true;  /* flag that dio not done (completely) */
+
+#ifdef DEBUG
+        if (clear) {
+            int j;
+
+            for (j = 0; j < buf_size; ++j) {
+                if (rbBuff[j] != 0) {
+                    clear = false;
+                    break;
+                }
+            }
+        }
+#endif
+    }
+    if (op->do_time && (start_tm.tv_sec || start_tm.tv_usec)) {
+        struct timeval res_tm;
+        double a, b;
+
+        gettimeofday(&end_tm, NULL);
+        res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+        res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+        if (res_tm.tv_usec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_usec += 1000000;
+        }
+        a = res_tm.tv_sec;
+        a += (0.000001 * res_tm.tv_usec);
+        b = (double)buf_size * num;
+        printf("time to read data from buffer was %d.%06d secs",
+               (int)res_tm.tv_sec, (int)res_tm.tv_usec);
+        if (a > 0.00001) {
+            if (b > 511)
+                printf(", %.2f MB/sec", b / (a * 1000000.0));
+            printf(", %.2f IOPS", num / a);
+        }
+        printf("\n");
+    }
+    if (dio_incomplete)
+        printf(">> direct IO requested but not done\n");
+    printf("Read %" PRId64 " MiB (actual: %" PRId64 " bytes), buffer "
+           "size=%d KiB (%d bytes)\n", (total_size / (1024 * 1024)),
+           (int64_t)num * buf_size, buf_size / 1024, buf_size);
+
+    if (rawp) free(rawp);
+    res = close(sg_fd);
+    if (res < 0) {
+        err = errno;
+        perror("close error");
+        return sg_convert_errno(err);
+    }
+#ifdef DEBUG
+    if (clear)
+        printf("read buffer always zero\n");
+    else
+        printf("read buffer non-zero\n");
+#endif
+    return res;
+}
diff --git a/src/sg_rdac.c b/src/sg_rdac.c
new file mode 100644
index 0000000..b0d1cdc
--- /dev/null
+++ b/src/sg_rdac.c
@@ -0,0 +1,516 @@
+/*
+ * sg_rdac
+ *
+ * Retrieve / set RDAC options.
+ *
+ * Copyright (C) 2006-2018 Hannes Reinecke <hare@suse.de>
+ *
+ * Based on sg_modes.c and sg_emc_trespass.c; credits from there apply.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <string.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "1.17 20180512";
+
+uint8_t mode6_hdr[] = {
+    0x75, /* Length */
+    0, /* medium */
+    0, /* params */
+    8, /* Block descriptor length */
+};
+
+uint8_t mode10_hdr[] = {
+    0x01, 0x18, /* Length */
+    0, /* medium */
+    0, /* params */
+    0, 0, /* reserved */
+    0, 0, /* block descriptor length */
+};
+
+uint8_t block_descriptor[] = {
+    0, /* Density code */
+    0, 0, 0, /* Number of blocks */
+    0, /* Reserved */
+    0, 0x02, 0, /* 512 byte blocks */
+};
+
+struct rdac_page_common {
+    uint8_t  current_serial[16];
+    uint8_t  alternate_serial[16];
+    uint8_t  current_mode_msb;
+    uint8_t  current_mode_lsb;
+    uint8_t  alternate_mode_msb;
+    uint8_t  alternate_mode_lsb;
+    uint8_t  quiescence;
+    uint8_t  options;
+};
+
+struct rdac_legacy_page {
+    uint8_t  page_code;
+    uint8_t  page_length;
+    struct rdac_page_common attr;
+    uint8_t  lun_table[32];
+    uint8_t  lun_table_exp[32];
+    unsigned short reserved;
+};
+
+struct rdac_expanded_page {
+    uint8_t  page_code;
+    uint8_t  subpage_code;
+    uint8_t  page_length[2];
+    struct rdac_page_common attr;
+    uint8_t  lun_table[256];
+    uint8_t  reserved[2];
+};
+
+static int do_verbose = 0;
+
+static void dump_mode_page( uint8_t *page, int len )
+{
+        int i, k;
+
+        for (k = 0; k < len; k += 16) {
+
+                printf("%x:",k / 16);
+                for (i = 0; i < 16; i++) {
+                        printf(" %02x", page[k + i]);
+                        if (k + i >= len) {
+                                printf("\n");
+                                break;
+                        }
+                }
+                printf("\n");
+        }
+
+}
+
+#define MX_ALLOC_LEN (1024 * 4)
+#define RDAC_CONTROLLER_PAGE 0x2c
+#define RDAC_CONTROLLER_PAGE_LEN 0x68
+#define LEGACY_PAGE 0x00
+#define EXPANDED_LUN_SPACE_PAGE 0x01
+#define EXPANDED_LUN_SPACE_PAGE_LEN 0x128
+#define RDAC_FAIL_ALL_PATHS 0x1
+#define RDAC_FAIL_SELECTED_PATHS 0x2
+#define RDAC_FORCE_QUIESCENCE 0x2
+#define RDAC_QUIESCENCE_TIME 10
+
+static int fail_all_paths(int fd, bool use_6_byte)
+{
+        struct rdac_legacy_page *rdac_page;
+        struct rdac_expanded_page *rdac_page_exp;
+        struct rdac_page_common *rdac_common = NULL;
+        uint8_t fail_paths_pg[308];
+
+        int res;
+        char b[80];
+
+        memset(fail_paths_pg, 0, 308);
+        if (use_6_byte) {
+                memcpy(fail_paths_pg, mode6_hdr, 4);
+                memcpy(fail_paths_pg + 4, block_descriptor, 8);
+                rdac_page = (struct rdac_legacy_page *)(fail_paths_pg + 4 + 8);
+                rdac_page->page_code = RDAC_CONTROLLER_PAGE;
+                rdac_page->page_length = RDAC_CONTROLLER_PAGE_LEN;
+                rdac_common = &rdac_page->attr;
+        } else {
+                memcpy(fail_paths_pg, mode10_hdr, 8);
+                rdac_page_exp = (struct rdac_expanded_page *)
+                                (fail_paths_pg + 8);
+                rdac_page_exp->page_code = RDAC_CONTROLLER_PAGE | 0x40;
+                rdac_page_exp->subpage_code = 0x1;
+                sg_put_unaligned_be16(EXPANDED_LUN_SPACE_PAGE_LEN,
+                                      rdac_page_exp->page_length + 0);
+                rdac_common = &rdac_page_exp->attr;
+        }
+
+        rdac_common->current_mode_lsb =  RDAC_FAIL_ALL_PATHS;
+        rdac_common->quiescence = RDAC_QUIESCENCE_TIME;
+        rdac_common->options = RDAC_FORCE_QUIESCENCE;
+
+        if (use_6_byte) {
+                res = sg_ll_mode_select6(fd, 1 /* pf */, 0 /* sp */,
+                                        fail_paths_pg, 118,
+                                        true, (do_verbose ? 2 : 0));
+        } else {
+                res = sg_ll_mode_select10(fd, 1 /* pf */, 0 /* sp */,
+                                        fail_paths_pg, 308,
+                                        true, (do_verbose ? 2: 0));
+        }
+
+        switch (res) {
+        case 0:
+                if (do_verbose)
+                        pr2serr("fail paths successful\n");
+                break;
+        default:
+                sg_get_category_sense_str(res, sizeof(b), b, do_verbose);
+                pr2serr("fail paths failed: %s\n", b);
+                break;
+        }
+
+        return res;
+}
+
+static int fail_this_path(int fd, int lun, bool use_6_byte)
+{
+        int res;
+        struct rdac_legacy_page *rdac_page;
+        struct rdac_expanded_page *rdac_page_exp;
+        struct rdac_page_common *rdac_common = NULL;
+        uint8_t fail_paths_pg[308];
+        char b[80];
+
+        if (use_6_byte) {
+                if (lun > 31) {
+                        pr2serr("must use 10 byte cdb to fail luns over 31\n");
+                        return -1;
+                }
+        } else {        /* 10 byte cdb case */
+                if (lun > 255) {
+                        pr2serr("lun cannot exceed 255\n");
+                        return -1;
+                }
+        }
+
+        memset(fail_paths_pg, 0, 308);
+        if (use_6_byte) {
+                memcpy(fail_paths_pg, mode6_hdr, 4);
+                memcpy(fail_paths_pg + 4, block_descriptor, 8);
+                rdac_page = (struct rdac_legacy_page *)(fail_paths_pg + 4 + 8);
+                rdac_page->page_code = RDAC_CONTROLLER_PAGE;
+                rdac_page->page_length = RDAC_CONTROLLER_PAGE_LEN;
+                rdac_common = &rdac_page->attr;
+                memset(rdac_page->lun_table, 0x0, 32);
+                rdac_page->lun_table[lun] = 0x81;
+        } else {
+                memcpy(fail_paths_pg, mode10_hdr, 8);
+                rdac_page_exp = (struct rdac_expanded_page *)
+                                (fail_paths_pg + 8);
+                rdac_page_exp->page_code = RDAC_CONTROLLER_PAGE | 0x40;
+                rdac_page_exp->subpage_code = 0x1;
+                sg_put_unaligned_be16(EXPANDED_LUN_SPACE_PAGE_LEN,
+                                      rdac_page_exp->page_length + 0);
+                rdac_common = &rdac_page_exp->attr;
+                memset(rdac_page_exp->lun_table, 0x0, 256);
+                rdac_page_exp->lun_table[lun] = 0x81;
+        }
+
+        rdac_common->current_mode_lsb =  RDAC_FAIL_SELECTED_PATHS;
+        rdac_common->quiescence = RDAC_QUIESCENCE_TIME;
+        rdac_common->options = RDAC_FORCE_QUIESCENCE;
+
+        if (use_6_byte) {
+                res = sg_ll_mode_select6(fd, 1 /* pf */, 0 /* sp */,
+                                        fail_paths_pg, 118,
+                                        true, (do_verbose ? 2 : 0));
+        } else {
+                res = sg_ll_mode_select10(fd, 1 /* pf */, 0 /* sp */,
+                                        fail_paths_pg, 308,
+                                        true, (do_verbose ? 2: 0));
+        }
+
+        switch (res) {
+        case 0:
+                if (do_verbose)
+                        pr2serr("fail paths successful\n");
+                break;
+        default:
+                sg_get_category_sense_str(res, sizeof(b), b, do_verbose);
+                pr2serr("fail paths page (lun=%d) failed: %s\n", lun, b);
+                break;
+        }
+
+        return res;
+}
+
+static void print_rdac_mode(uint8_t *ptr, bool exp_subpg)
+{
+        int i, k, bd_len, lun_table_len;
+        uint8_t * lun_table = NULL;
+        struct rdac_legacy_page *legacy;
+        struct rdac_expanded_page *expanded;
+        struct rdac_page_common *rdac_ptr = NULL;
+
+        if (exp_subpg) {
+                bd_len = ptr[7];
+                expanded = (struct rdac_expanded_page *)(ptr + 8 + bd_len);
+                rdac_ptr = &expanded->attr;
+                lun_table = expanded->lun_table;
+                lun_table_len = 256;
+        } else {
+                bd_len = ptr[3];
+                legacy = (struct rdac_legacy_page *)(ptr + 4 + bd_len);
+                rdac_ptr = &legacy->attr;
+                lun_table = legacy->lun_table;
+                lun_table_len = 32;
+        }
+
+        printf("RDAC %s page\n", exp_subpg ? "Expanded" : "Legacy");
+        printf("  Controller serial: %s\n",
+               rdac_ptr->current_serial);
+        printf("  Alternate controller serial: %s\n",
+               rdac_ptr->alternate_serial);
+        printf("  RDAC mode (redundant processor): ");
+        switch (rdac_ptr->current_mode_msb) {
+        case 0x00:
+                printf("alternate controller not present; ");
+                break;
+        case 0x01:
+                printf("alternate controller present; ");
+                break;
+        default:
+                printf("(Unknown controller status 0x%x); ",
+                       rdac_ptr->current_mode_msb);
+                break;
+        }
+        switch (rdac_ptr->current_mode_lsb) {
+        case 0x0:
+                printf("inactive\n");
+                break;
+        case 0x1:
+                printf("active\n");
+                break;
+        case 0x2:
+                printf("Dual active mode\n");
+                break;
+        default:
+                printf("(Unknown mode 0x%x)\n",
+                       rdac_ptr->current_mode_lsb);
+        }
+
+        printf("  RDAC mode (alternate processor): ");
+        switch (rdac_ptr->alternate_mode_msb) {
+        case 0x00:
+                printf("alternate controller not present; ");
+                break;
+        case 0x01:
+                printf("alternate controller present; ");
+                break;
+        default:
+                printf("(Unknown status 0x%x); ",
+                       rdac_ptr->alternate_mode_msb);
+                break;
+        }
+        switch (rdac_ptr->alternate_mode_lsb) {
+        case 0x0:
+                printf("inactive\n");
+                break;
+        case 0x1:
+                printf("active\n");
+                break;
+        case 0x2:
+                printf("Dual active mode\n");
+                break;
+        case 0x3:
+                printf("Not present\n");
+                break;
+        case 0x4:
+                printf("held in reset\n");
+                break;
+        default:
+                printf("(Unknown mode 0x%x)\n",
+                       rdac_ptr->alternate_mode_lsb);
+        }
+        printf("  Quiescence timeout: %d\n", rdac_ptr->quiescence);
+        printf("  RDAC option 0x%x\n", rdac_ptr->options);
+        printf("    ALUA: %s\n", (rdac_ptr->options & 0x4 ? "Enabled" :
+                                                            "Disabled" ));
+        printf("    Force Quiescence: %s\n", (rdac_ptr->options & 0x2 ?
+                                              "Enabled" : "Disabled" ));
+        printf ("  LUN Table: (p = preferred, a = alternate, u = utm lun)\n");
+        printf("         0 1 2 3 4 5 6 7  8 9 a b c d e f\n");
+        for (k = 0; k < lun_table_len; k += 16) {
+                printf("    0x%x:",k / 16);
+                for (i = 0; i < 16; i++) {
+                        switch (lun_table[k + i]) {
+                        case 0x0:
+                                printf(" x");
+                                break;
+                        case 0x1:
+                                printf(" p");
+                                break;
+                        case 0x2:
+                                printf(" a");
+                                break;
+                        case 0x3:
+                                printf(" u");
+                                break;
+                        default:
+                                printf(" ?");
+                                break;
+                        }
+                        if (i == 7) {
+                                printf(" ");
+                        }
+                }
+                printf("\n");
+        }
+}
+
+static void usage()
+{
+    printf("Usage:  sg_rdac [-6] [-a] [-f=LUN] [-v] [-V] DEVICE\n"
+           "  where:\n"
+           "    -6        use 6 byte cdbs for mode sense/select\n"
+           "    -a        transfer all devices to the controller\n"
+           "              serving DEVICE.\n"
+           "    -f=LUN    transfer the device at LUN to the\n"
+           "              controller serving DEVICE\n"
+           "    -v        verbose\n"
+           "    -V        print version then exit\n\n"
+           " Display/Modify RDAC Redundant Controller Page 0x2c.\n"
+           " If [-a] or [-f] is not specified the current settings"
+           " are displayed.\n");
+}
+
+int main(int argc, char * argv[])
+{
+        bool fail_all = false;
+        bool fail_path = false;
+        bool use_6_byte = false;
+        int res, fd, k, resid, len, lun = -1;
+        int ret = 0;
+        char **argptr;
+        char * file_name = 0;
+        uint8_t rsp_buff[MX_ALLOC_LEN];
+
+        if (argc < 2) {
+                usage ();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+
+        for (k = 1; k < argc; ++k) {
+                argptr = argv + k;
+                if (!strcmp (*argptr, "-v"))
+                        ++do_verbose;
+                else if (!strncmp(*argptr, "-f=",3)) {
+                        fail_path = true;
+                        lun = strtoul(*argptr + 3, NULL, 0);
+                }
+                else if (!strcmp(*argptr, "-a")) {
+                        fail_all = true;
+                }
+                else if (!strcmp(*argptr, "-6")) {
+                        use_6_byte = true;
+                }
+                else if (!strcmp(*argptr, "-V")) {
+                        pr2serr("sg_rdac version: %s\n", version_str);
+                        return 0;
+                }
+                else if (*argv[k] == '-') {
+                        pr2serr("Unrecognized switch: %s\n", argv[k]);
+                        file_name = 0;
+                        break;
+                }
+                else if (0 == file_name)
+                        file_name = argv[k];
+                else {
+                        pr2serr("too many arguments\n");
+                        file_name = 0;
+                        break;
+                }
+        }
+        if (0 == file_name) {
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+
+        fd = sg_cmds_open_device(file_name, false /* rw */, do_verbose);
+        if (fd < 0) {
+                pr2serr("open error: %s: %s\n", file_name, safe_strerror(-fd));
+                usage();
+                ret = sg_convert_errno(-fd);
+                goto fini;
+        }
+
+        if (fail_all) {
+                res = fail_all_paths(fd, use_6_byte);
+        } else if (fail_path) {
+                res = fail_this_path(fd, lun, use_6_byte);
+        } else {
+                resid = 0;
+                if (use_6_byte)
+                        res = sg_ll_mode_sense6(fd, /* DBD */ false,
+                                                /* PC */ 0,
+                                                0x2c /* page */,
+                                                0 /*subpage */,
+                                                rsp_buff, 252,
+                                                true, do_verbose);
+                else
+                        res = sg_ll_mode_sense10_v2(fd, /* llbaa */ false,
+                                                    /* DBD */ false,
+                                                    /* page control */0,
+                                                    0x2c, 0x1 /* subpage */,
+                                                    rsp_buff, 308, 0, &resid,
+                                                    true, do_verbose);
+
+                if (! res) {
+                        len = sg_msense_calc_length(rsp_buff, 308, use_6_byte,
+                                                    NULL);
+                        if (resid > 0) {
+                                len = ((308 - resid) < len) ? (308 - resid) :
+                                                              len;
+                                if (len < 2)
+                                        pr2serr("MS(10) residual value (%d) "
+                                                "a worry\n", resid);
+                        }
+                        if (do_verbose && (len > 1))
+                                dump_mode_page(rsp_buff, len);
+                        print_rdac_mode(rsp_buff, ! use_6_byte);
+                } else {
+                        if (SG_LIB_CAT_INVALID_OP == res)
+                                pr2serr(">>>>>> try again without the '-6' "
+                                        "switch for a 10 byte MODE SENSE "
+                                        "command\n");
+                        else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+                                pr2serr("mode sense: invalid field in cdb "
+                                        "(perhaps subpages or page control "
+                                        "(PC) not supported)\n");
+                        else {
+                                char b[80];
+
+                                sg_get_category_sense_str(res, sizeof(b), b,
+                                                          do_verbose);
+                                pr2serr("mode sense failed: %s\n", b);
+                        }
+                }
+        }
+        ret = res;
+
+        res = sg_cmds_close_device(fd);
+        if (res < 0) {
+                pr2serr("close error: %s\n", safe_strerror(-res));
+                if (0 == ret)
+                        ret = sg_convert_errno(res);
+        }
+fini:
+        if (0 == do_verbose) {
+                if (! sg_if_can2stderr("sg_rdac failed: ", ret))
+                        pr2serr("Some error occurred, try again with '-v' "
+                                "or '-vv' for more information\n");
+        }
+        return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_read.c b/src/sg_read.c
new file mode 100644
index 0000000..abeb477
--- /dev/null
+++ b/src/sg_read.c
@@ -0,0 +1,931 @@
+/*
+ *  A utility program for the Linux OS SCSI generic ("sg") device driver.
+ *    Copyright (C) 2001 - 2022 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+
+   This program reads data from the given SCSI device (typically a disk
+   or cdrom) and discards that data. Its primary goal is to time
+   multiple reads all starting from the same logical address. Its interface
+   is a subset of another member of this package: sg_dd which is a
+   "dd" variant. The input file can be a scsi generic device, a block device,
+   or a seekable file. Streams such as stdin are not acceptable. The block
+   size ('bs') is assumed to be 512 if not given.
+
+   This version should compile with Linux sg drivers with version numbers
+   >= 30000 . For mmap-ed IO the sg version number >= 30122 .
+
+*/
+
+#define _XOPEN_SOURCE 600
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <signal.h>
+#include <ctype.h>
+#include <errno.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#ifndef major
+#include <sys/types.h>
+#endif
+#include <sys/mman.h>
+#include <sys/time.h>
+#include <linux/major.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "1.38 20220118";
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BLOCKS_PER_TRANSFER 128
+#define DEF_SCSI_CDBSZ 10
+#define MAX_SCSI_CDBSZ 16
+#define MAX_BPT_VALUE (1 << 24)         /* used for maximum bs as well */
+#define MAX_COUNT_SKIP_SEEK (1LL << 48) /* coverity wants upper bound */
+
+#define ME "sg_read: "
+
+#ifndef SG_FLAG_MMAP_IO
+#define SG_FLAG_MMAP_IO 4
+#endif
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_TIMEOUT 40000       /* 40,000 millisecs == 40 seconds */
+
+#ifndef RAW_MAJOR
+#define RAW_MAJOR 255   /*unlikely value */
+#endif
+
+#define FT_OTHER 1              /* filetype other than sg and ... */
+#define FT_SG 2                 /* filetype is sg char device */
+#define FT_RAW 4                /* filetype is raw char device */
+#define FT_BLOCK 8              /* filetype is block device */
+#define FT_ERROR 64             /* couldn't "stat" file */
+
+#define MIN_RESERVED_SIZE 8192
+
+static int sum_of_resids = 0;
+
+static int64_t dd_count = -1;
+static int64_t orig_count = 0;
+static int64_t in_full = 0;
+static int in_partial = 0;
+
+static int pack_id_count = 0;
+static int verbose = 0;
+
+static const char * sg_allow_dio = "/sys/module/sg/parameters/allow_dio";
+
+
+static void
+install_handler (int sig_num, void (*sig_handler) (int sig))
+{
+    struct sigaction sigact;
+
+    sigaction (sig_num, NULL, &sigact);
+    if (sigact.sa_handler != SIG_IGN) {
+        sigact.sa_handler = sig_handler;
+        sigemptyset (&sigact.sa_mask);
+        sigact.sa_flags = 0;
+        sigaction (sig_num, &sigact, NULL);
+    }
+}
+
+static void
+print_stats(int iters, const char * str)
+{
+    if (orig_count > 0) {
+        if (0 != dd_count)
+            pr2serr("  remaining block count=%" PRId64 "\n", dd_count);
+        pr2serr("%" PRId64 "+%d records in", in_full - in_partial,
+                in_partial);
+        if (iters > 0)
+            pr2serr(", %s commands issued: %d\n", (str ? str : ""), iters);
+        else
+            pr2serr("\n");
+    } else if (iters > 0)
+        pr2serr("%s commands issued: %d\n", (str ? str : ""), iters);
+}
+
+static void
+interrupt_handler(int sig)
+{
+    struct sigaction sigact;
+
+    sigact.sa_handler = SIG_DFL;
+    sigemptyset (&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigaction (sig, &sigact, NULL);
+    pr2serr("Interrupted by signal,");
+    print_stats(0, NULL);
+    kill (getpid (), sig);
+}
+
+static void
+siginfo_handler(int sig)
+{
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    pr2serr("Progress report, continuing ...\n");
+    print_stats(0, NULL);
+}
+
+static int
+dd_filetype(const char * filename)
+{
+    struct stat st;
+
+    if (stat(filename, &st) < 0)
+        return FT_ERROR;
+    if (S_ISCHR(st.st_mode)) {
+        if (RAW_MAJOR == major(st.st_rdev))
+            return FT_RAW;
+        else if (SCSI_GENERIC_MAJOR == major(st.st_rdev))
+            return FT_SG;
+    } else if (S_ISBLK(st.st_mode))
+        return FT_BLOCK;
+    return FT_OTHER;
+}
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_read  [blk_sgio=0|1] [bpt=BPT] [bs=BS] "
+            "[cdbsz=6|10|12|16]\n"
+            "                count=COUNT [dio=0|1] [dpo=0|1] [fua=0|1] "
+            "if=IFILE\n"
+            "                [mmap=0|1] [no_dfxer=0|1] [odir=0|1] "
+            "[skip=SKIP]\n"
+            "                [time=TI] [verbose=VERB] [--help] "
+            "[--verbose]\n"
+            "                [--version] "
+            "  where:\n"
+            "    blk_sgio 0->normal IO for block devices, 1->SCSI commands "
+            "via SG_IO\n"
+            "    bpt      is blocks_per_transfer (default is 128, or 64 KiB "
+            "for default BS)\n"
+            "             setting 'bpt=0' will do COUNT zero block SCSI "
+            "READs\n"
+            "    bs       must match sector size if IFILE accessed via SCSI "
+            "commands\n"
+            "             (def=512)\n"
+            "    cdbsz    size of SCSI READ command (default is 10)\n"
+            "    count    total bytes read will be BS*COUNT (if no "
+            "error)\n"
+            "             (if negative, do |COUNT| zero block SCSI READs)\n"
+            "    dio      1-> attempt direct IO on sg device, 0->indirect IO "
+            "(def)\n");
+    pr2serr("    dpo      1-> set disable page out (DPO) in SCSI READs\n"
+            "    fua      1-> set force unit access (FUA) in SCSI READs\n"
+            "    if       an sg, block or raw device, or a seekable file (not "
+            "stdin)\n"
+            "    mmap     1->perform mmap-ed IO on sg device, 0->indirect IO "
+            "(def)\n"
+            "    no_dxfer 1->DMA to kernel buffers only, not user space, "
+            "0->normal(def)\n"
+            "    odir     1->open block device O_DIRECT, 0->don't (def)\n"
+            "    skip     each transfer starts at this logical address "
+            "(def=0)\n"
+            "    time     0->do nothing(def), 1->time from 1st cmd, 2->time "
+            "from 2nd, ...\n"
+            "    verbose  increase level of verbosity (def: 0)\n"
+            "    --help|-h    print this usage message then exit\n"
+            "    --verbose|-v   increase level of verbosity (def: 0)\n"
+            "    --version|-V   print version number then exit\n\n"
+            "Issue SCSI READ commands, each starting from the same logical "
+            "block address\n");
+}
+
+static int
+sg_build_scsi_cdb(uint8_t * cdbp, int cdb_sz, unsigned int blocks,
+                  int64_t start_block, bool write_true, bool fua, bool dpo)
+{
+    int sz_ind;
+    int rd_opcode[] = {0x8, 0x28, 0xa8, 0x88};
+    int wr_opcode[] = {0xa, 0x2a, 0xaa, 0x8a};
+
+    memset(cdbp, 0, cdb_sz);
+    if (dpo)
+        cdbp[1] |= 0x10;
+    if (fua)
+        cdbp[1] |= 0x8;
+    switch (cdb_sz) {
+    case 6:
+        sz_ind = 0;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be24(0x1fffff & start_block, cdbp + 1);
+        cdbp[4] = (256 == blocks) ? 0 : (uint8_t)blocks;
+        if (blocks > 256) {
+            pr2serr(ME "for 6 byte commands, maximum number of blocks is "
+                    "256\n");
+            return 1;
+        }
+        if ((start_block + blocks - 1) & (~0x1fffff)) {
+            pr2serr(ME "for 6 byte commands, can't address blocks beyond "
+                    "%d\n", 0x1fffff);
+            return 1;
+        }
+        if (dpo || fua) {
+            pr2serr(ME "for 6 byte commands, neither dpo nor fua bits "
+                    "supported\n");
+            return 1;
+        }
+        break;
+    case 10:
+        sz_ind = 1;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be16((uint16_t)blocks, cdbp + 7);
+        if (blocks & (~0xffff)) {
+            pr2serr(ME "for 10 byte commands, maximum number of blocks is "
+                    "%d\n", 0xffff);
+            return 1;
+        }
+        break;
+    case 12:
+        sz_ind = 2;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 6);
+        break;
+    case 16:
+        sz_ind = 3;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be64(start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 10);
+        break;
+    default:
+        pr2serr(ME "expected cdb size of 6, 10, 12, or 16 but got %d\n",
+                cdb_sz);
+        return 1;
+    }
+    return 0;
+}
+
+/* -3 medium/hardware error, -2 -> not ready, 0 -> successful,
+   1 -> recoverable (ENOMEM), 2 -> try again (e.g. unit attention),
+   3 -> try again (e.g. aborted command), -1 -> other unrecoverable error */
+static int
+sg_bread(int sg_fd, uint8_t * buff, int blocks, int64_t from_block, int bs,
+         int cdbsz, bool fua, bool dpo, bool * diop, bool do_mmap,
+         bool no_dxfer)
+{
+    uint8_t rdCmd[MAX_SCSI_CDBSZ];
+    uint8_t senseBuff[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr;
+
+    if (sg_build_scsi_cdb(rdCmd, cdbsz, blocks, from_block, false, fua,
+                          dpo)) {
+        pr2serr(ME "bad cdb build, from_block=%" PRId64 ", blocks=%d\n",
+                from_block, blocks);
+        return -1;
+    }
+    memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = cdbsz;
+    io_hdr.cmdp = rdCmd;
+    if (blocks > 0) {
+        io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+        io_hdr.dxfer_len = bs * blocks;
+        /* next: shows dxferp unused during mmap-ed IO */
+        if (! do_mmap)
+            io_hdr.dxferp = buff;
+        if (diop && *diop)
+            io_hdr.flags |= SG_FLAG_DIRECT_IO;
+        else if (do_mmap)
+            io_hdr.flags |= SG_FLAG_MMAP_IO;
+        else if (no_dxfer)
+            io_hdr.flags |= SG_FLAG_NO_DXFER;
+    } else
+        io_hdr.dxfer_direction = SG_DXFER_NONE;
+    io_hdr.mx_sb_len = SENSE_BUFF_LEN;
+    io_hdr.sbp = senseBuff;
+    io_hdr.timeout = DEF_TIMEOUT;
+    io_hdr.pack_id = pack_id_count++;
+    if (verbose > 1) {
+        char b[128];
+
+        pr2serr("    READ cdb: %s\n",
+                sg_get_command_str(rdCmd, cdbsz, false, sizeof(b), b));
+    }
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        if (ENOMEM == errno)
+            return 1;
+        perror("reading (SG_IO) on sg device, error");
+        return -1;
+    }
+
+    if (verbose > 2)
+        pr2serr( "      duration=%u ms\n", io_hdr.duration);
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        if (verbose > 1)
+                sg_chk_n_print3("reading, continue", &io_hdr, true);
+        break;
+    case SG_LIB_CAT_UNIT_ATTENTION:
+        if (verbose)
+            sg_chk_n_print3("reading", &io_hdr, (verbose > 1));
+        return 2;
+    case SG_LIB_CAT_ABORTED_COMMAND:
+        if (verbose)
+            sg_chk_n_print3("reading", &io_hdr, (verbose > 1));
+        return 3;
+    case SG_LIB_CAT_NOT_READY:
+        if (verbose)
+            sg_chk_n_print3("reading", &io_hdr, (verbose > 1));
+        return -2;
+    case SG_LIB_CAT_MEDIUM_HARD:
+        if (verbose)
+            sg_chk_n_print3("reading", &io_hdr, (verbose > 1));
+        return -3;
+    default:
+        sg_chk_n_print3("reading", &io_hdr, !! verbose);
+        return -1;
+    }
+    if (blocks > 0) {
+        if (diop && *diop &&
+            ((io_hdr.info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+            *diop = 0;      /* flag that dio not done (completely) */
+        sum_of_resids += io_hdr.resid;
+    }
+    return 0;
+}
+
+/* Returns the number of times 'ch' is found in string 's' given the
+ * string's length. */
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+#define STR_SZ 1024
+#define INF_SZ 512
+#define EBUFF_SZ 768
+
+
+int
+main(int argc, char * argv[])
+{
+    bool count_given = false;
+    bool dio_tmp;
+    bool do_blk_sgio = false;
+    bool do_dio = false;
+    bool do_mmap = false;
+    bool do_odir = false;
+    bool dpo = false;
+    bool fua = false;
+    bool no_dxfer = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int bs = 0;
+    int bpt = DEF_BLOCKS_PER_TRANSFER;
+    int dio_incomplete = 0;
+    int do_time = 0;
+    int in_type = FT_OTHER;
+    int ret = 0;
+    int scsi_cdbsz = DEF_SCSI_CDBSZ;
+    int res, k, t, buf_sz, iters, infd, blocks, flags, blocks_per, err;
+    int n, keylen;
+    size_t psz;
+    int64_t skip = 0;
+    char * key;
+    char * buf;
+    uint8_t * wrkBuff = NULL;
+    uint8_t * wrkPos = NULL;
+    char inf[INF_SZ];
+    char outf[INF_SZ];
+    char str[STR_SZ];
+    char ebuff[EBUFF_SZ];
+    const char * read_str;
+    struct timeval start_tm, end_tm;
+
+#if defined(HAVE_SYSCONF) && defined(_SC_PAGESIZE)
+    psz = sysconf(_SC_PAGESIZE); /* POSIX.1 (was getpagesize()) */
+#else
+    psz = 4096;     /* give up, pick likely figure */
+#endif
+    inf[0] = '\0';
+
+    for (k = 1; k < argc; k++) {
+        if (argv[k]) {
+            strncpy(str, argv[k], STR_SZ);
+            str[STR_SZ - 1] = '\0';
+        } else
+            continue;
+        for (key = str, buf = key; (*buf && (*buf != '=')); )
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        keylen = strlen(key);
+        if (0 == strcmp(key,"blk_sgio"))
+            do_blk_sgio = !! sg_get_num(buf);
+        else if (0 == strcmp(key,"bpt")) {
+            bpt = sg_get_num(buf);
+            if ((bpt < 0) || (bpt > MAX_BPT_VALUE)) {
+                pr2serr( ME "bad argument to 'bpt'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"bs")) {
+            bs = sg_get_num(buf);
+            if ((bs < 0) || (bs > MAX_BPT_VALUE)) {
+                pr2serr( ME "bad argument to 'bs'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"cdbsz")) {
+            scsi_cdbsz = sg_get_num(buf);
+            if ((scsi_cdbsz < 0) || (scsi_cdbsz > 32)) {
+                pr2serr( ME "bad argument to 'cdbsz', expect 6, 10, 12, 16 "
+                        "or 32\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"count")) {
+            count_given = true;
+            if ('-' == *buf) {
+                dd_count = sg_get_llnum(buf + 1);
+                if ((dd_count < 0) || (dd_count > MAX_COUNT_SKIP_SEEK)) {
+                    pr2serr( ME "bad argument to 'count'\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                dd_count = - dd_count;
+            } else {
+                dd_count = sg_get_llnum(buf);
+                if ((dd_count < 0) || (dd_count > MAX_COUNT_SKIP_SEEK)) {
+                    pr2serr( ME "bad argument to 'count'\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+        } else if (0 == strcmp(key,"dio"))
+            do_dio = !! sg_get_num(buf);
+        else if (0 == strcmp(key,"dpo"))
+            dpo = !! sg_get_num(buf);
+        else if (0 == strcmp(key,"fua"))
+            fua = !! sg_get_num(buf);
+        else if (strcmp(key,"if") == 0) {
+            memcpy(inf, buf, INF_SZ - 1);
+            inf[INF_SZ - 1] = '\0';
+        } else if (0 == strcmp(key,"mmap"))
+            do_mmap = !! sg_get_num(buf);
+        else if (0 == strcmp(key,"no_dxfer"))
+            no_dxfer = !! sg_get_num(buf);
+        else if (0 == strcmp(key,"odir"))
+            do_odir = !! sg_get_num(buf);
+        else if (strcmp(key,"of") == 0) {
+            memcpy(outf, buf, INF_SZ - 1);
+            outf[INF_SZ - 1] = '\0';
+        } else if (0 == strcmp(key,"skip")) {
+            skip = sg_get_llnum(buf);
+            if ((skip < 0) || (skip > MAX_COUNT_SKIP_SEEK)) {
+                pr2serr( ME "bad argument to 'skip'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"time"))
+            do_time = sg_get_num(buf);
+        else if (0 == strncmp(key, "verb", 4)) {
+            verbose_given = true;
+            verbose = sg_get_num(buf);
+        } else if (0 == strncmp(key, "--help", 6)) {
+            usage();
+            return 0;
+        } else if (0 == strncmp(key, "--verb", 6)) {
+            verbose_given = true;
+            ++verbose;
+        } else if (0 == strncmp(key, "--vers", 6))
+            version_given = true;
+        else if ((keylen > 1) && ('-' == key[0]) && ('-' != key[1])) {
+            res = 0;
+            n = num_chs_in_str(key + 1, keylen - 1, 'h');
+            if (n > 0) {
+                usage();
+                return 0;
+            }
+            n = num_chs_in_str(key + 1, keylen - 1, 'v');
+            if (n > 0)
+                verbose_given = true;
+            verbose += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'V');
+            if (n > 0)
+                version_given = true;
+            res += n;
+            if (res < (keylen - 1)) {
+                pr2serr("Unrecognised short option in '%s', try '--help'\n",
+                        key);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else {
+            pr2serr( "Unrecognized argument '%s'\n", key);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr( ME ": %s\n", version_str);
+        return 0;
+    }
+
+    if (bs <= 0) {
+        bs = DEF_BLOCK_SIZE;
+        if ((dd_count > 0) && (bpt > 0))
+            pr2serr( "Assume default 'bs' (block size) of %d bytes\n", bs);
+    }
+    if (! count_given) {
+        pr2serr("'count' must be given\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (skip < 0) {
+        pr2serr("skip cannot be negative\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (bpt < 1) {
+        if (0 == bpt) {
+            if (dd_count > 0)
+                dd_count = - dd_count;
+        } else {
+            pr2serr("bpt must be greater than 0\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (do_dio && do_mmap) {
+        pr2serr("cannot select both dio and mmap\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (no_dxfer && (do_dio || do_mmap)) {
+        pr2serr("cannot select no_dxfer with dio or mmap\n");
+        return SG_LIB_CONTRADICT;
+    }
+
+    install_handler (SIGINT, interrupt_handler);
+    install_handler (SIGQUIT, interrupt_handler);
+    install_handler (SIGPIPE, interrupt_handler);
+    install_handler (SIGUSR1, siginfo_handler);
+
+    if (! inf[0]) {
+        pr2serr("must provide 'if=<filename>'\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (0 == strcmp("-", inf)) {
+        pr2serr("'-' (stdin) invalid as <filename>\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    in_type = dd_filetype(inf);
+    if (FT_ERROR == in_type) {
+        pr2serr("Unable to access: %s\n", inf);
+        return SG_LIB_FILE_ERROR;
+    } else if ((FT_BLOCK & in_type) && do_blk_sgio)
+        in_type |= FT_SG;
+
+    if (FT_SG & in_type) {
+        if ((dd_count < 0) && (6 == scsi_cdbsz)) {
+            pr2serr(ME "SCSI READ (6) can't do zero block reads\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        flags = O_RDWR;
+        if (do_odir)
+            flags |= O_DIRECT;
+        if ((infd = open(inf, flags)) < 0) {
+            flags = O_RDONLY;
+            if (do_odir)
+                flags |= O_DIRECT;
+            if ((infd = open(inf, flags)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ,
+                         ME "could not open %s for sg reading", inf);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+        }
+        if (verbose)
+            pr2serr("Opened %s for SG_IO with flags=0x%x\n", inf, flags);
+        if ((dd_count > 0) && (! (FT_BLOCK & in_type))) {
+            if (verbose > 2) {
+                if (ioctl(infd, SG_GET_RESERVED_SIZE, &t) >= 0)
+                    pr2serr("  SG_GET_RESERVED_SIZE yields: %d\n", t);
+            }
+            t = bs * bpt;
+            if ((do_mmap) && (0 != (t % psz)))
+                t = ((t / psz) + 1) * psz;    /* round up to next pagesize */
+            res = ioctl(infd, SG_SET_RESERVED_SIZE, &t);
+            if (res < 0)
+                perror(ME "SG_SET_RESERVED_SIZE error");
+            res = ioctl(infd, SG_GET_VERSION_NUM, &t);
+            if ((res < 0) || (t < 30000)) {
+                pr2serr(ME "sg driver prior to 3.x.y\n");
+                return SG_LIB_CAT_OTHER;
+            }
+            if (do_mmap && (t < 30122)) {
+                pr2serr(ME "mmap-ed IO needs a sg driver version >= 3.1.22\n");
+                return SG_LIB_CAT_OTHER;
+            }
+        }
+    } else {
+        if (do_mmap) {
+            pr2serr(ME "mmap-ed IO only support on sg devices\n");
+            return SG_LIB_CAT_OTHER;
+        }
+        if (dd_count < 0) {
+            pr2serr(ME "negative 'count' only supported with SCSI READs\n");
+            return SG_LIB_CAT_OTHER;
+        }
+        flags = O_RDONLY;
+        if (do_odir)
+            flags |= O_DIRECT;
+        if ((infd = open(inf, flags)) < 0) {
+            err = errno;
+            snprintf(ebuff,  EBUFF_SZ,
+                     ME "could not open %s for reading", inf);
+            perror(ebuff);
+            return sg_convert_errno(err);
+        }
+        if (verbose)
+            pr2serr("Opened %s for Unix reads with flags=0x%x\n", inf, flags);
+        if (skip > 0) {
+            off64_t offset = skip;
+
+            offset *= bs;       /* could exceed 32 bits here! */
+            if (lseek64(infd, offset, SEEK_SET) < 0) {
+                err = errno;
+                snprintf(ebuff,  EBUFF_SZ,
+                    ME "couldn't skip to required position on %s", inf);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+        }
+    }
+
+    if (0 == dd_count)
+        return 0;
+    orig_count = dd_count;
+
+    if (dd_count > 0) {
+        if (do_dio || do_odir || (FT_RAW & in_type)) {
+            wrkBuff = (uint8_t *)malloc(bs * bpt + psz);
+            if (0 == wrkBuff) {
+                pr2serr("Not enough user memory for aligned storage\n");
+                return SG_LIB_CAT_OTHER;
+            }
+            /* perhaps use posix_memalign() instead */
+            wrkPos = (uint8_t *)(((sg_uintptr_t)wrkBuff + psz - 1) &
+                                       (~(psz - 1)));
+        } else if (do_mmap) {
+            wrkPos = (uint8_t *)mmap(NULL, bs * bpt,
+                        PROT_READ | PROT_WRITE, MAP_SHARED, infd, 0);
+            if (MAP_FAILED == wrkPos) {
+                perror(ME "error from mmap()");
+                return SG_LIB_CAT_OTHER;
+            }
+        } else {
+            wrkBuff = (uint8_t *)malloc(bs * bpt);
+            if (0 == wrkBuff) {
+                pr2serr("Not enough user memory\n");
+                return SG_LIB_CAT_OTHER;
+            }
+            wrkPos = wrkBuff;
+        }
+    }
+
+    blocks_per = bpt;
+    start_tm.tv_sec = 0;   /* just in case start set condition not met */
+    start_tm.tv_usec = 0;
+
+    if (verbose && (dd_count < 0))
+        pr2serr("About to issue %" PRId64 " zero block SCSI READs\n",
+                0 - dd_count);
+
+    /* main loop */
+    for (iters = 0; dd_count != 0; ++iters) {
+        if ((do_time > 0) && (iters == (do_time - 1)))
+            gettimeofday(&start_tm, NULL);
+        if (dd_count < 0)
+            blocks = 0;
+        else
+            blocks = (dd_count > blocks_per) ? blocks_per : dd_count;
+        if (FT_SG & in_type) {
+            dio_tmp = do_dio;
+            res = sg_bread(infd, wrkPos, blocks, skip, bs, scsi_cdbsz,
+                           fua, dpo, &dio_tmp, do_mmap, no_dxfer);
+            if (1 == res) {     /* ENOMEM, find what's available+try that */
+                if (ioctl(infd, SG_GET_RESERVED_SIZE, &buf_sz) < 0) {
+                    perror("RESERVED_SIZE ioctls failed");
+                    break;
+                }
+                if (buf_sz < MIN_RESERVED_SIZE)
+                    buf_sz = MIN_RESERVED_SIZE;
+                blocks_per = (buf_sz + bs - 1) / bs;
+                blocks = blocks_per;
+                pr2serr("Reducing read to %d blocks per loop\n", blocks_per);
+                res = sg_bread(infd, wrkPos, blocks, skip, bs, scsi_cdbsz,
+                               fua, dpo, &dio_tmp, do_mmap, no_dxfer);
+            } else if (2 == res) {
+                pr2serr("Unit attention, try again (r)\n");
+                res = sg_bread(infd, wrkPos, blocks, skip, bs, scsi_cdbsz,
+                               fua, dpo, &dio_tmp, do_mmap, no_dxfer);
+            }
+            if (0 != res) {
+                switch (res) {
+                case -3:
+                    ret = SG_LIB_CAT_MEDIUM_HARD;
+                    pr2serr(ME "SCSI READ medium/hardware error\n");
+                    break;
+                case -2:
+                    ret = SG_LIB_CAT_NOT_READY;
+                    pr2serr(ME "device not ready\n");
+                    break;
+                case 2:
+                    ret = SG_LIB_CAT_UNIT_ATTENTION;
+                    pr2serr(ME "SCSI READ unit attention\n");
+                    break;
+                case 3:
+                    ret = SG_LIB_CAT_ABORTED_COMMAND;
+                    pr2serr(ME "SCSI READ aborted command\n");
+                    break;
+                default:
+                    ret = SG_LIB_CAT_OTHER;
+                    pr2serr(ME "SCSI READ failed\n");
+                    break;
+                }
+                break;
+            } else {
+                in_full += blocks;
+                if (do_dio && (0 == dio_tmp))
+                    dio_incomplete++;
+            }
+        } else {
+            if (iters > 0) { /* subsequent iteration reset skip position */
+                off64_t offset = skip;
+
+                offset *= bs;       /* could exceed 32 bits here! */
+                if (lseek64(infd, offset, SEEK_SET) < 0) {
+                    perror(ME "could not reset skip position");
+                    break;
+                }
+            }
+            while (((res = read(infd, wrkPos, blocks * bs)) < 0) &&
+                   (EINTR == errno))
+                ;
+            if (res < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "reading, skip=%" PRId64 " ",
+                         skip);
+                perror(ebuff);
+                break;
+            } else if (res < blocks * bs) {
+                pr2serr(ME "short read: wanted/got=%d/%d bytes, stop\n",
+                        blocks * bs, res);
+                blocks = res / bs;
+                if ((res % bs) > 0) {
+                    blocks++;
+                    in_partial++;
+                }
+                dd_count -= blocks;
+                in_full += blocks;
+                break;
+            }
+            in_full += blocks;
+        }
+        if (dd_count > 0)
+            dd_count -= blocks;
+        else if (dd_count < 0)
+            ++dd_count;
+    }
+    read_str = (FT_SG & in_type) ? "SCSI READ" : "read";
+    if (do_time > 0) {
+        gettimeofday(&end_tm, NULL);
+        if (start_tm.tv_sec || start_tm.tv_usec) {
+            struct timeval res_tm;
+            double a, b, c;
+
+            res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+            res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+            if (res_tm.tv_usec < 0) {
+                --res_tm.tv_sec;
+                res_tm.tv_usec += 1000000;
+            }
+            a = res_tm.tv_sec;
+            a += (0.000001 * res_tm.tv_usec);
+            if (orig_count > 0) {
+                b = (double)bs * (orig_count - dd_count);
+                if (do_time > 1)
+                    c = b - ((double)bs * ((do_time - 1.0) * bpt));
+                else
+                    c = 0.0;
+            } else {
+                b = 0.0;
+                c = 0.0;
+            }
+
+            if (1 == do_time) {
+                pr2serr("Time for all %s commands was %d.%06d secs", read_str,
+                        (int)res_tm.tv_sec, (int)res_tm.tv_usec);
+                if ((orig_count > 0) && (a > 0.00001) && (b > 511))
+                    pr2serr(", %.2f MB/sec\n", b / (a * 1000000.0));
+                else
+                    pr2serr("\n");
+            } else if (2 == do_time) {
+                pr2serr("Time from second %s command to end was %d.%06d secs",
+                        read_str, (int)res_tm.tv_sec,
+                        (int)res_tm.tv_usec);
+                if ((orig_count > 0) && (a > 0.00001) && (c > 511))
+                    pr2serr(", %.2f MB/sec\n", c / (a * 1000000.0));
+                else
+                    pr2serr("\n");
+            } else {
+                pr2serr("Time from start of %s command "
+                        "#%d to end was %d.%06d secs", read_str, do_time,
+                        (int)res_tm.tv_sec, (int)res_tm.tv_usec);
+                if ((orig_count > 0) && (a > 0.00001) && (c > 511))
+                    pr2serr(", %.2f MB/sec\n", c / (a * 1000000.0));
+                else
+                    pr2serr("\n");
+            }
+            if ((iters > 0) && (a > 0.00001))
+                pr2serr("Average number of %s commands per second was %.2f\n",
+                        read_str, (double)iters / a);
+        }
+    }
+
+    if (wrkBuff)
+        free(wrkBuff);
+
+    close(infd);
+    if (0 != dd_count) {
+        pr2serr("Some error occurred,");
+        if (0 == ret)
+            ret = SG_LIB_CAT_OTHER;
+    }
+    print_stats(iters, read_str);
+
+    if (dio_incomplete) {
+        int fd;
+        char c;
+
+        pr2serr(">> Direct IO requested but incomplete %d times\n",
+                dio_incomplete);
+        if ((fd = open(sg_allow_dio, O_RDONLY)) >= 0) {
+            if (1 == read(fd, &c, 1)) {
+                if ('0' == c)
+                    pr2serr(">>> %s set to '0' but should be set to '1' for "
+                            "direct IO\n", sg_allow_dio);
+            }
+            close(fd);
+        }
+    }
+    if (sum_of_resids)
+        pr2serr(">> Non-zero sum of residual counts=%d\n", sum_of_resids);
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_read_attr.c b/src/sg_read_attr.c
new file mode 100644
index 0000000..89ca26d
--- /dev/null
+++ b/src/sg_read_attr.c
@@ -0,0 +1,1004 @@
+/*
+ * Copyright (c) 2016-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <errno.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI READ ATTRIBUTE command to the given SCSI device
+ * and decodes the response. Based on spc5r08.pdf
+ */
+
+static const char * version_str = "1.16 20211114";
+
+#define MAX_RATTR_BUFF_LEN (1024 * 1024)
+#define DEF_RATTR_BUFF_LEN (1024 * 8)
+
+#define SG_READ_ATTRIBUTE_CMD 0x8c
+#define SG_READ_ATTRIBUTE_CMDLEN 16
+
+#define RA_ATTR_VAL_SA 0x0
+#define RA_ATTR_LIST_SA 0x1
+#define RA_LV_LIST_SA 0x2
+#define RA_PART_LIST_SA 0x3
+#define RA_SMC2_SA 0x4
+#define RA_SUP_ATTR_SA 0x5
+#define RA_HIGHEST_SA 0x5
+
+#define RA_FMT_BINARY 0x0
+#define RA_FMT_ASCII 0x1
+#define RA_FMT_TEXT 0x2         /* takes into account locale */
+#define RA_FMT_RES 0x3          /* reserved */
+
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+struct opts_t {
+    bool cache;
+    bool enumerate;
+    bool do_raw;
+    bool o_readonly;
+    bool verbose_given;
+    bool version_given;
+    int elem_addr;
+    int filter;
+    int fai;
+    int do_hex;
+    int lvn;
+    int maxlen;
+    int pn;
+    int quiet;
+    int sa;
+    int verbose;
+};
+
+struct acron_nv_t {
+    const char * acron;
+    const char * name;
+    int val;
+};
+
+struct attr_name_info_t {
+    int id;
+    const char * name;  /* tab ('\t') suggest line break */
+    int format;         /* RA_FMT_BINARY and friends, -1 --> unknown */
+    int len;            /* -1 --> not fixed (variable) */
+    int process;        /* 0 --> print decimal if binary, 1 --> print hex,
+                         * 2 --> further processing */
+};
+
+static struct option long_options[] = {
+    {"cache", no_argument, 0, 'c'},
+    {"enumerate", no_argument, 0, 'e'},
+    {"element", required_argument, 0, 'E'},   /* SMC-3 element address */
+    {"filter", required_argument, 0, 'f'},
+    {"first", required_argument, 0, 'F'},
+    {"help", no_argument, 0, 'h'},
+    {"hex", no_argument, 0, 'H'},
+    {"in", required_argument, 0, 'i'},
+    {"lvn", required_argument, 0, 'l'},
+    {"maxlen", required_argument, 0, 'm'},
+    {"partition", required_argument, 0, 'p'},
+    {"quiet", required_argument, 0, 'q'},
+    {"raw", no_argument, 0, 'r'},
+    {"readonly", no_argument, 0, 'R'},
+    {"sa", required_argument, 0, 's'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {0, 0, 0, 0},   /* sentinel */
+};
+
+static struct acron_nv_t sa_acron_arr[] = {
+    {"av", "attribute values", 0},
+    {"al", "attribute list", 1},
+    {"lvl", "logical volume list", 2},
+    {"pl", "partition list", 3},
+    {"smc", "SMC-2 should define this", 4},
+    {"sa", "supported attributes", 5},
+    {NULL, NULL, -1},           /* sentinel */
+};
+
+static struct attr_name_info_t attr_name_arr[] = {
+/* Device type attributes */
+    {0x0, "Remaining capacity in partition [MiB]", RA_FMT_BINARY, 8, 0},
+    {0x1, "Maximum capacity in partition [MiB]", RA_FMT_BINARY, 8, 0},
+    {0x2, "TapeAlert flags", RA_FMT_BINARY, 8, 0},   /* SSC-4 */
+    {0x3, "Load count", RA_FMT_BINARY, 8, 0},
+    {0x4, "MAM space remaining [B]", RA_FMT_BINARY, 8, 0},
+    {0x5, "Assigning organization", RA_FMT_ASCII, 8, 0}, /* SSC-4 */
+    {0x6, "Format density code", RA_FMT_BINARY, 1, 1},    /* SSC-4 */
+    {0x7, "Initialization count", RA_FMT_BINARY, 2, 0},
+    {0x8, "Volume identifier", RA_FMT_ASCII, 32, 0},
+    {0x9, "Volume change reference", RA_FMT_BINARY, -1, 1}, /* SSC-4 */
+    {0x20a, "Density vendor/serial number at last load", RA_FMT_ASCII, 40, 0},
+    {0x20b, "Density vendor/serial number at load-1", RA_FMT_ASCII, 40, 0},
+    {0x20c, "Density vendor/serial number at load-2", RA_FMT_ASCII, 40, 0},
+    {0x20d, "Density vendor/serial number at load-3", RA_FMT_ASCII, 40, 0},
+    {0x220, "Total MiB written in medium life", RA_FMT_BINARY, 8, 0},
+    {0x221, "Total MiB read in medium life", RA_FMT_BINARY, 8, 0},
+    {0x222, "Total MiB written in current/last load", RA_FMT_BINARY, 8, 0},
+    {0x223, "Total MiB read in current/last load", RA_FMT_BINARY, 8, 0},
+    {0x224, "Logical position of first encrypted block", RA_FMT_BINARY, 8, 2},
+    {0x225, "Logical position of first unencrypted block\tafter first "
+     "encrypted block", RA_FMT_BINARY, 8, 2},
+    {0x340, "Medium usage history", RA_FMT_BINARY, 90, 2},
+    {0x341, "Partition usage history", RA_FMT_BINARY, 60, 2},
+
+/* Medium type attributes */
+    {0x400, "Medium manufacturer", RA_FMT_ASCII, 8, 0},
+    {0x401, "Medium serial number", RA_FMT_ASCII, 32, 0},
+    {0x402, "Medium length [m]", RA_FMT_BINARY, 4, 0},      /* SSC-4 */
+    {0x403, "Medium width [0.1 mm]", RA_FMT_BINARY, 4, 0},  /* SSC-4 */
+    {0x404, "Assigning organization", RA_FMT_ASCII, 8, 0},  /* SSC-4 */
+    {0x405, "Medium density code", RA_FMT_BINARY, 1, 1},    /* SSC-4 */
+    {0x406, "Medium manufacture date", RA_FMT_ASCII, 8, 0},
+    {0x407, "MAM capacity [B]", RA_FMT_BINARY, 8, 0},
+    {0x408, "Medium type", RA_FMT_BINARY, 1, 1},
+    {0x409, "Medium type information", RA_FMT_BINARY, 2, 1},
+    {0x40a, "Numeric medium serial number", -1, -1, 1},
+
+/* Host type attributes */
+    {0x800, "Application vendor", RA_FMT_ASCII, 8, 0},
+    {0x801, "Application name", RA_FMT_ASCII, 32, 0},
+    {0x802, "Application version", RA_FMT_ASCII, 8, 0},
+    {0x803, "User medium text label", RA_FMT_TEXT, 160, 0},
+    {0x804, "Date and time last written", RA_FMT_ASCII, 12, 0},
+    {0x805, "Text localization identifier", RA_FMT_BINARY, 1, 0},
+    {0x806, "Barcode", RA_FMT_ASCII, 32, 0},
+    {0x807, "Owning host textual name", RA_FMT_TEXT, 80, 0},
+    {0x808, "Media pool", RA_FMT_TEXT, 160, 0},
+    {0x809, "Partition user text label", RA_FMT_ASCII, 16, 0},
+    {0x80a, "Load/unload at partition", RA_FMT_BINARY, 1, 0},
+    {0x80a, "Application format version", RA_FMT_ASCII, 16, 0},
+    {0x80c, "Volume coherency information", RA_FMT_BINARY, -1, 1},
+     /* SSC-5 */
+    {0x820, "Medium globally unique identifier", RA_FMT_BINARY, 36, 1},
+    {0x821, "Media pool globally unique identifier", RA_FMT_BINARY, 36, 1},
+
+    {-1, NULL, -1, -1, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_read_attr [--cache] [--element=EA] [--enumerate] "
+            "[--filter=FL]\n"
+            "                    [--first=FAI] [--help] [--hex] [--in=FN] "
+            "[--lvn=LVN]\n"
+            "                    [--maxlen=LEN] [--partition=PN] [--quiet] "
+            "[--raw]\n"
+            "                    [--readonly] [--sa=SA] [--verbose] "
+            "[--version]\n"
+            "                    DEVICE\n");
+    pr2serr("  where:\n"
+            "    --cache|-c         set CACHE bit in cdn (def: clear)\n"
+            "    --enumerate|-e     enumerate known attributes and service "
+            "actions\n"
+            "    --element=EA|-E EA    EA is placed in 'element address' "
+            "field in\n"
+            "                          cdb [SMC-3] (def: 0)\n"
+            "    --filter=FL|-f FL    FL is parameter code to match (def: "
+            "-1 -> all)\n"
+            "    --first=FAI|-F FAI    FAI is placed in 'first attribute "
+            "identifier'\n"
+            "                          field in cdb (def: 0)\n"
+            "    --help|-h          print out usage message\n"
+            "    --hex|-H           output response in hexadecimal; used "
+            "twice\n"
+            "                       shows decoded values in hex\n"
+            "    --in=FN|-i FN      FN is a filename containing attribute "
+            "values in\n"
+            "                       ASCII hex or binary if --raw also "
+            "given\n"
+            "    --lvn=LVN|-l LVN    logical volume number (LVN) (def:0)\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 0 -> 8192 bytes)\n"
+            "    --partition=PN|-p PN    partition number (PN) (def:0)\n"
+            "    --quiet|-q         reduce the amount of output, can use "
+            "more than once\n"
+            "    --raw|-r           output response in binary\n"
+            "    --readonly|-R      open DEVICE read-only (def: read-write)\n"
+            "    --sa=SA|-s SA      SA is service action (def: 0)\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Performs a SCSI READ ATTRIBUTE command. Even though it is "
+            "defined in\nSPC-3 and later it is typically used on tape "
+            "systems.\n");
+}
+
+/* Invokes a SCSI READ ATTRIBUTE command (SPC+SMC).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_read_attr(int sg_fd, void * resp, int * residp, bool noisy,
+                const struct opts_t * op)
+{
+    int ret, res, sense_cat;
+    uint8_t ra_cdb[SG_READ_ATTRIBUTE_CMDLEN] =
+          {SG_READ_ATTRIBUTE_CMD, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
+           0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    ra_cdb[1] = 0x1f & op->sa;
+    if (op->elem_addr)
+        sg_put_unaligned_be16(op->elem_addr, ra_cdb + 2);
+    if (op->lvn)
+        ra_cdb[5] = 0xff & op->lvn;
+    if (op->pn)
+        ra_cdb[7] = 0xff & op->pn;
+    if (op->fai)
+        sg_put_unaligned_be16(op->fai, ra_cdb + 8);
+    sg_put_unaligned_be32((uint32_t)op->maxlen, ra_cdb + 10);
+    if (op->cache)
+        ra_cdb[14] |= 0x1;
+    if (op->verbose) {
+        char b[128];
+
+        pr2serr("    Read attribute cdb: %s\n",
+                sg_get_command_str(ra_cdb, SG_READ_ATTRIBUTE_CMDLEN, false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", __func__);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, ra_cdb, sizeof(ra_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, op->maxlen);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, op->verbose);
+    ret = sg_cmds_process_resp(ptvp, "read attribute", res, noisy,
+                               op->verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    if (residp)
+        *residp = get_scsi_pt_resid(ptvp);
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+static void
+dStrRaw(const char * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+static int
+find_sa_acron(const char * cp)
+{
+    int k;
+    const struct acron_nv_t * anvp;
+    const char * mp;
+
+    for (anvp = sa_acron_arr; anvp->acron ; ++anvp) {
+        for (mp = cp, k = 0; *mp; ++mp, ++k) {
+            if (0 == anvp->acron[k])
+                return anvp->val;
+            if (tolower((uint8_t)*mp) != (uint8_t)anvp->acron[k])
+                break;
+        }
+        if ((0 == *mp) && (0 == anvp->acron[k]))
+            return anvp->val;
+    }
+    return -1;  /* not found */
+}
+
+const char * a_format[] = {
+    "binary",
+    "ascii",
+    "text",
+    "format[0x3]",
+};
+
+static void
+enum_attributes(void)
+{
+    const struct attr_name_info_t * anip;
+    const char * cp;
+    char b[32];
+
+    printf("Attribute ID\tLength\tFormat\tName\n");
+    printf("------------------------------------------\n");
+    for (anip = attr_name_arr; anip->name ; ++anip) {
+        if (anip->format < 0)
+            snprintf(b, sizeof(b), "unknown");
+        else
+            snprintf(b, sizeof(b), "%s", a_format[0x3 & anip->format]);
+        printf("  0x%04x:\t%d\t%s\t", anip->id, anip->len, b);
+        cp = strchr(anip->name, '\t');
+        if (cp ) {
+            printf("%.*s\n", (int)(cp - anip->name), anip->name);
+            printf("\t\t\t\t%s\n", cp + 1);
+        } else
+            printf("%s\n", anip->name);
+    }
+}
+
+static void
+enum_sa_acrons(void)
+{
+    const struct acron_nv_t * anvp;
+
+    printf("SA_value\tAcronym\tDescription\n");
+    printf("------------------------------------------\n");
+    for (anvp = sa_acron_arr; anvp->acron ; ++anvp)
+        printf("  %d:\t\t%s\t%s\n", anvp->val, anvp->acron, anvp->name);
+}
+
+/* Returns 1 if 'bp' all 0xff bytes, returns 2 is all 0xff bytes apart
+ * from last being 0xfe; otherwise returns 0. */
+static int
+all_ffs_or_last_fe(const uint8_t * bp, int len)
+{
+    for ( ; len > 0; ++bp, --len) {
+        if (*bp < 0xfe)
+            return 0;
+        if (0xfe == *bp)
+            return (1 == len) ? 2 : 0;
+
+    }
+    return 1;
+}
+
+static char *
+attr_id_lookup(unsigned int id, const struct attr_name_info_t ** anipp,
+               int blen, char * b)
+{
+    const struct attr_name_info_t * anip;
+
+    for (anip = attr_name_arr; anip->name; ++anip) {
+        if (id == (unsigned int)anip->id)
+            break;
+    }
+    if (anip->name) {
+        snprintf(b, blen, "%s", anip->name);
+        if (anipp)
+            *anipp = anip;
+        return b;
+    }
+    if (anipp)
+        *anipp = NULL;
+    if (id < 0x400)
+        snprintf(b, blen, "Unknown device attribute 0x%x", id);
+    else if (id < 0x800)
+        snprintf(b, blen, "Unknown medium attribute 0x%x", id);
+    else if (id < 0xc00)
+        snprintf(b, blen, "Unknown host attribute 0x%x", id);
+    else if (id < 0x1000)
+        snprintf(b, blen, "Vendor specific device attribute 0x%x", id);
+    else if (id < 0x1400)
+        snprintf(b, blen, "Vendor specific medium attribute 0x%x", id);
+    else if (id < 0x1800)
+        snprintf(b, blen, "Vendor specific host attribute 0x%x", id);
+    else
+        snprintf(b, blen, "Reserved attribute 0x%x", id);
+    return b;
+}
+
+static void
+decode_attr_list(const uint8_t * alp, int len, bool supported,
+                 const struct opts_t * op)
+{
+    int id;
+    char b[160];
+    char * cp;
+    char * c2p;
+    const char * leadin = supported ? "Supported a" : "A";
+
+    if (op->verbose)
+        printf("%sttribute list: [len=%d]\n", leadin, len);
+    else if (0 == op->quiet)
+        printf("%sttribute list:\n", leadin);
+    if (op->do_hex) {
+        hex2stdout(alp, len, 0);
+        return;
+    }
+    for ( ; len > 0; alp += 2, len -= 2) {
+        id = sg_get_unaligned_be16(alp + 0);
+        if ((op->filter >= 0) && (op->filter != id))
+            continue;
+        if (op->verbose)
+            printf("  0x%.4x:\t", id);
+        cp = attr_id_lookup(id, NULL, sizeof(b), b);
+        c2p = strchr(cp, '\t');
+        if (c2p) {
+            printf("  %.*s -\n", (int)(c2p - cp), cp);
+            if (op->verbose)
+                printf("\t\t      %s\n", c2p + 1);
+            else
+                printf("      %s\n", c2p + 1);
+        } else
+            printf("  %s\n", cp);
+    }
+}
+
+static void
+helper_full_attr(const uint8_t * alp, int len, int id,
+                 const struct attr_name_info_t * anip,
+                 const struct opts_t * op)
+{
+    int k;
+    const uint8_t * bp;
+
+    if (op->verbose)
+        printf("[r%c] ", (0x80 & alp[2]) ? 'o' : 'w');
+    if (op->verbose > 3)
+        pr2serr("%s: id=0x%x, len=%d, anip->format=%d, anip->len=%d\n",
+                __func__, id, len, anip->format, anip->len);
+    switch (id) {
+    case 0x224:         /* logical position of first encrypted block */
+        k = all_ffs_or_last_fe(alp + 5, len - 5);
+        if (1 == k)
+            printf("<unknown> [ff]\n");
+        else if (2 == k)
+            printf("<unknown [fe]>\n");
+        else {
+            if ((len - 5) <= 8)
+                printf("%" PRIx64, sg_get_unaligned_be(len - 5, alp + 5));
+            else {
+                printf("\n");
+                hex2stdout((alp + 5), len - 5, 0);
+            }
+        }
+        break;
+    case 0x225:         /* logical position of first unencrypted block
+                         * after first encrypted block */
+        k = all_ffs_or_last_fe(alp + 5, len - 5);
+        if (1 == k)
+            printf("<unknown> [ff]\n");
+        else if (2 == k)
+            printf("<unknown [fe]>\n");
+        else {
+            if ((len - 5) <= 8)
+                printf("%" PRIx64, sg_get_unaligned_be(len - 5, alp + 5));
+            else {
+                printf("\n");
+                hex2stdout(alp + 5, len - 5, 0);
+            }
+        }
+        break;
+    case 0x340:         /* Medium Usage history */
+        bp = alp + 5;
+        printf("\n");
+        if ((len - 5) < 90) {
+            pr2serr("%s: expected 90 bytes, got %d\n", __func__, len - 5);
+            break;
+        }
+        printf("    Current amount of data written [MiB]: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 0));
+        printf("    Current write retry count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 6));
+        printf("    Current amount of data read [MiB]: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 12));
+        printf("    Current read retry count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 18));
+        printf("    Previous amount of data written [MiB]: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 24));
+        printf("    Previous write retry count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 30));
+        printf("    Previous amount of data read [MiB]: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 36));
+        printf("    Previous read retry count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 42));
+        printf("    Total amount of data written [MiB]: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 48));
+        printf("    Total write retry count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 54));
+        printf("    Total amount of data read [MiB]: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 60));
+        printf("    Total read retry count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 66));
+        printf("    Load count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 72));
+        printf("    Total change partition count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 78));
+        printf("    Total partition initialization count: %" PRIu64 "\n",
+               sg_get_unaligned_be48(bp + 84));
+        break;
+    case 0x341:         /* Partition Usage history */
+        bp = alp + 5;
+        printf("\n");
+        if ((len - 5) < 60) {
+            pr2serr("%s: expected 60 bytes, got %d\n", __func__, len - 5);
+            break;
+        }
+        printf("    Current amount of data written [MiB]: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 0));
+        printf("    Current write retry count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 4));
+        printf("    Current amount of data read [MiB]: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 8));
+        printf("    Current read retry count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 12));
+        printf("    Previous amount of data written [MiB]: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 16));
+        printf("    Previous write retry count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 20));
+        printf("    Previous amount of data read [MiB]: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 24));
+        printf("    Previous read retry count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 28));
+        printf("    Total amount of data written [MiB]: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 32));
+        printf("    Total write retry count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 36));
+        printf("    Total amount of data read [MiB]: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 40));
+        printf("    Total read retry count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 44));
+        printf("    Load count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 48));
+        printf("    change partition count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 52));
+        printf("    partition initialization count: %" PRIu32 "\n",
+               sg_get_unaligned_be32(bp + 56));
+        break;
+    default:
+        pr2serr("%s: unknown attribute id: 0x%x\n", __func__, id);
+        printf("  In hex:\n");
+        hex2stdout(alp, len, 0);
+        break;
+    }
+}
+
+static void
+decode_attr_vals(const uint8_t * alp, int len, const struct opts_t * op)
+{
+    int bump, id, alen;
+    uint64_t ull;
+    char * cp;
+    char * c2p;
+    const struct attr_name_info_t * anip;
+    char b[160];
+
+    if (op->verbose)
+        printf("Attribute values: [len=%d]\n", len);
+    else if (op->filter < 0) {
+        if (0 == op->quiet)
+            printf("Attribute values:\n");
+        if (op->do_hex) {       /* only expect -HH to get through here */
+            hex2stdout(alp, len, 0);
+            return;
+        }
+    }
+    for ( ; len > 4; alp += bump, len -= bump) {
+        id = sg_get_unaligned_be16(alp + 0);
+        bump = sg_get_unaligned_be16(alp + 3) + 5;
+        alen = bump - 5;
+        if ((op->filter >= 0) && (op->filter != id)) {
+            if (id < op->filter)
+                continue;
+            else
+                break;  /* Assume array is ascending id order */
+        }
+        anip = NULL;
+        cp = attr_id_lookup(id, &anip, sizeof(b), b);
+        if (op->quiet < 2) {
+            c2p = strchr(cp, '\t');
+            if (c2p) {
+                printf("  %.*s -\n", (int)(c2p - cp), cp);
+                printf("      %s: ", c2p + 1);
+            } else
+                printf("  %s: ", cp);
+        }
+        if (op->verbose)
+            printf("[r%c] ", (0x80 & alp[2]) ? 'o' : 'w');
+        if (anip) {
+            if ((RA_FMT_BINARY == anip->format) && (bump <= 13)) {
+                ull = sg_get_unaligned_be(alen, alp + 5);
+                if (0 == anip->process)
+                    printf("%" PRIu64 "\n", ull);
+                else if (1 == anip->process)
+                    printf("0x%" PRIx64 "\n", ull);
+                else
+                    helper_full_attr(alp, bump, id, anip, op);
+                if (op->verbose) {
+                    if ((anip->len > 0) && (alen > 0) && (alen != anip->len))
+                        printf(" <<< T10 length (%d) differs from length in "
+                               "response (%d) >>>\n", anip->len, alen);
+                }
+            } else if (RA_FMT_BINARY == anip->format) {
+                if (2 == anip->process)
+                    helper_full_attr(alp, bump, id, anip, op);
+                else {
+                    printf("\n");
+                    hex2stdout(alp + 5, alen, 0);
+                }
+           } else {
+                if (2 == anip->process)
+                    helper_full_attr(alp, bump, id, anip, op);
+                else {
+                    printf("%.*s\n", alen, alp + 5);
+                    if (op->verbose) {
+                        if ((anip->len > 0) && (alen > 0) &&
+                            (alen != anip->len))
+                            printf(" <<< T10 length (%d) differs from length "
+                                   "in response (%d) >>>\n", anip->len, alen);
+                    }
+                }
+            }
+        } else {
+            if (op->verbose > 1)
+                printf("Attribute id lookup failed, in hex:\n");
+            else
+                printf("\n");
+            hex2stdout(alp + 5, alen, 0);
+        }
+    }
+    if (op->verbose && (len > 0) && (len <= 4))
+        pr2serr("warning: iterate of attributes should end a residual of "
+                "%d\n", len);
+}
+
+static void
+decode_all_sa_s(const uint8_t * rabp, int len, const struct opts_t * op)
+{
+    if (op->do_hex && (2 != op->do_hex)) {
+        hex2stdout(rabp, len, ((1 == op->do_hex) ? 1 : -1));
+        return;
+    }
+    switch (op->sa) {
+    case RA_ATTR_VAL_SA:
+        decode_attr_vals(rabp + 4, len - 4, op);
+        break;
+    case RA_ATTR_LIST_SA:
+        decode_attr_list(rabp + 4, len - 4, false, op);
+        break;
+    case RA_LV_LIST_SA:
+        if ((0 == op->quiet) || op->verbose)
+            printf("Logical volume list:\n");
+        if (len < 4) {
+            pr2serr(">>> response length unexpectedly short: %d bytes\n",
+                    len);
+            break;
+        }
+        printf("  First logical volume number: %u\n", rabp[2]);
+        printf("  Number of logical volumes available: %u\n", rabp[3]);
+        break;
+    case RA_PART_LIST_SA:
+        if ((0 == op->quiet) || op->verbose)
+            printf("Partition number list:\n");
+        if (len < 4) {
+            pr2serr(">>> response length unexpectedly short: %d bytes\n",
+                    len);
+            break;
+        }
+        printf("  First partition number: %u\n", rabp[2]);
+        printf("  Number of partitions available: %u\n", rabp[3]);
+        break;
+    case RA_SMC2_SA:
+        printf("Used by SMC-2, not information, output in hex:\n");
+        hex2stdout(rabp, len, 0);
+        break;
+    case RA_SUP_ATTR_SA:
+        decode_attr_list(rabp + 4, len - 4, true, op);
+        break;
+    default:
+        printf("Unrecognized service action [0x%x], response in hex:\n",
+               op->sa);
+        hex2stdout(rabp, len, 0);
+        break;
+    }
+}
+
+int
+main(int argc, char * argv[])
+{
+    int sg_fd, res, c, len, resid, rlen;
+    unsigned int ra_len;
+    int in_len = 0;
+    int ret = 0;
+    const char * device_name = NULL;
+    const char * fname = NULL;
+    uint8_t * rabp = NULL;
+    uint8_t * free_rabp = NULL;
+    struct opts_t opts;
+    struct opts_t * op;
+    char b[80];
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->filter = -1;
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "ceE:f:F:hHi:l:m:p:qrRs:vV",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            op->cache = true;
+            break;
+        case 'e':
+            op->enumerate = true;
+            break;
+        case 'E':
+           op->elem_addr = sg_get_num(optarg);
+           if ((op->elem_addr < 0) || (op->elem_addr > 65535)) {
+                pr2serr("bad argument to '--element=EA', expect 0 to 65535\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'f':
+           op->filter = sg_get_num(optarg);
+           if ((op->filter < -3) || (op->filter > 65535)) {
+                pr2serr("bad argument to '--filter=FL', expect -3 to "
+                        "65535\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'F':
+           op->fai = sg_get_num(optarg);
+           if ((op->fai < 0) || (op->fai > 65535)) {
+                pr2serr("bad argument to '--first=FAI', expect 0 to 65535\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'i':
+            fname = optarg;
+            break;
+        case 'l':
+           op->lvn = sg_get_num(optarg);
+           if ((op->lvn < 0) || (op->lvn > 255)) {
+                pr2serr("bad argument to '--lvn=LVN', expect 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'm':
+            op->maxlen = sg_get_num(optarg);
+            if ((op->maxlen < 0) || (op->maxlen > MAX_RATTR_BUFF_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or "
+                        "less\n", MAX_RATTR_BUFF_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'p':
+           op->pn = sg_get_num(optarg);
+           if ((op->pn < 0) || (op->pn > 255)) {
+                pr2serr("bad argument to '--pn=PN', expect 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'q':
+            ++op->quiet;
+            break;
+        case 'r':
+            op->do_raw = true;
+            break;
+        case 'R':
+            op->o_readonly = true;
+            break;
+        case 's':
+           if (isdigit((uint8_t)*optarg)) {
+               op->sa = sg_get_num(optarg);
+               if ((op->sa < 0) || (op->sa > 63)) {
+                    pr2serr("bad argument to '--sa=SA', expect 0 to 63\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else {
+                res = find_sa_acron(optarg);
+                if (res < 0) {
+                    enum_sa_acrons();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->sa = res;
+            }
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (op->enumerate) {
+        enum_attributes();
+        printf("\n");
+        enum_sa_acrons();
+        return 0;
+    }
+
+    if (fname && device_name) {
+        pr2serr("since '--in=FN' given, ignoring DEVICE\n");
+        device_name = NULL;
+    }
+
+    if (0 == op->maxlen)
+        op->maxlen = DEF_RATTR_BUFF_LEN;
+    rabp = (uint8_t *)sg_memalign(op->maxlen, 0, &free_rabp, op->verbose > 3);
+    if (NULL == rabp) {
+        pr2serr("unable to sg_memalign %d bytes\n", op->maxlen);
+        return sg_convert_errno(ENOMEM);
+    }
+
+    if (NULL == device_name) {
+        if (fname) {
+            if ((ret = sg_f2hex_arr(fname, op->do_raw, false /* no space */,
+                                    rabp, &in_len, op->maxlen)))
+                goto clean_up;
+            if (op->do_raw)
+                op->do_raw = false;    /* can interfere on decode */
+            if (in_len < 4) {
+                pr2serr("--in=%s only decoded %d bytes (needs 4 at least)\n",
+                        fname, in_len);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto clean_up;
+            }
+            decode_all_sa_s(rabp, in_len, op);
+            goto clean_up;
+        }
+        pr2serr("missing device name!\n");
+        usage();
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto clean_up;
+    }
+
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+                goto clean_up;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, op->o_readonly, op->verbose);
+    if (sg_fd < 0) {
+        pr2serr("open error: %s: %s\n", device_name,
+                safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto clean_up;
+    }
+
+    res = sg_ll_read_attr(sg_fd, rabp, &resid, op->verbose > 0, op);
+    ret = res;
+    if (0 == res) {
+        rlen = op->maxlen - resid;
+        if (rlen < 4) {
+            pr2serr("Response length (%d) too short\n", rlen);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto close_then_end;
+        }
+        if ((op->sa <= RA_HIGHEST_SA) && (op->sa != RA_SMC2_SA)) {
+            ra_len = ((RA_LV_LIST_SA == op->sa) ||
+                      (RA_PART_LIST_SA == op->sa)) ?
+                        (unsigned int)sg_get_unaligned_be16(rabp + 0) :
+                        sg_get_unaligned_be32(rabp + 0) + 2;
+            ra_len += 2;
+        } else
+            ra_len = rlen;
+        if ((int)ra_len > rlen) {
+            if (op->verbose)
+                pr2serr("ra_len available is %d, response length is %d\n",
+                        ra_len, rlen);
+            len = rlen;
+        } else
+            len = (int)ra_len;
+        if (op->do_raw) {
+            dStrRaw((const char *)rabp, len);
+            goto close_then_end;
+        }
+        decode_all_sa_s(rabp, len, op);
+    } else if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("Read attribute command not supported\n");
+    else {
+        sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+        pr2serr("Read attribute command: %s\n", b);
+    }
+
+close_then_end:
+    res = sg_cmds_close_device(sg_fd);
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+clean_up:
+    if (free_rabp)
+        free(free_rabp);
+    if (0 == op->verbose) {
+        if (! sg_if_can2stderr("sg_read_attr failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_read_block_limits.c b/src/sg_read_block_limits.c
new file mode 100644
index 0000000..4fc1fae
--- /dev/null
+++ b/src/sg_read_block_limits.c
@@ -0,0 +1,282 @@
+/*
+ * Copyright (c) 2009-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI READ BLOCK LIMITS command (SSC) to the given
+ * SCSI device.
+ */
+
+static const char * version_str = "1.09 20221101";
+
+#define DEF_READ_BLOCK_LIMITS_LEN 6
+#define MLIO_READ_BLOCK_LIMITS_LEN 20
+#define MAX_READ_BLOCK_LIMITS_LEN MLIO_READ_BLOCK_LIMITS_LEN
+
+static uint8_t readBlkLmtBuff[MAX_READ_BLOCK_LIMITS_LEN];
+
+
+static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"mloi", no_argument, 0, 'm'},  /* added in ssc4r02.pdf */
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_read_block_limits  [--help] [--hex] [--mloi] "
+            "[--raw]\n"
+            "                             [--readonly] [--verbose] "
+            "[--version]\n"
+            "                             DEVICE\n"
+            "  where:\n"
+            "    --help|-h          print out usage message\n"
+            "    --hex|-H           output response in hexadecimal\n"
+            "    --mloi|-m          output maximum logical object "
+            "identifier\n"
+            "    --raw|-r           output response in binary to stdout\n"
+            "    --readonly|-R      open DEVICE in read-only mode\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Performs a SCSI READ BLOCK LIMITS command and decode the "
+            "response\n"
+            );
+}
+
+static void
+dStrRaw(const char * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool do_mloi = false;
+    bool do_raw = false;
+    bool readonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int sg_fd, k, m, res, c, max_resp_len;
+    int resid = 0;
+    int actual_len = 0;
+    int do_hex = 0;
+    int verbose = 0;
+    int ret = 0;
+    uint32_t max_block_size;
+    uint64_t mloi;
+    uint16_t min_block_size;
+    uint8_t granularity;
+    const char * device_name = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "hHmrRvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'm':
+            do_mloi = true;
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            readonly = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("invalid option -%c ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        printf("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto the_end2;
+    }
+
+    max_resp_len = do_mloi ? MLIO_READ_BLOCK_LIMITS_LEN :
+                             DEF_READ_BLOCK_LIMITS_LEN;
+    memset(readBlkLmtBuff, 0x0, sizeof(readBlkLmtBuff));
+    res = sg_ll_read_block_limits_v2(sg_fd, do_mloi, readBlkLmtBuff,
+                                     max_resp_len, &resid, true, verbose);
+    ret = res;
+    if (0 == res) {
+        actual_len =  max_resp_len - resid;
+        if (do_hex) {
+            int fl = -1;
+
+            if (1 == do_hex)
+                fl = 1;
+            else if (2 == do_hex)
+                fl = 0;
+            hex2stdout(readBlkLmtBuff, actual_len, fl);
+            goto the_end;
+        } else if (do_raw) {
+            dStrRaw((const char *)readBlkLmtBuff, actual_len);
+            goto the_end;
+        }
+
+        if (do_mloi) {
+            if (actual_len < MLIO_READ_BLOCK_LIMITS_LEN) {
+                pr2serr("Expected at least %d bytes in response but only "
+                        "%d bytes\n", MLIO_READ_BLOCK_LIMITS_LEN, actual_len);
+                goto the_end;
+            }
+            printf("Read Block Limits (MLOI=1) results:\n");
+            mloi = sg_get_unaligned_be64(readBlkLmtBuff + 12);
+            printf("    Maximum logical block identifier: %" PRIu64 "\n",
+                   mloi);
+        } else {        /* MLOI=0 (only case before ssc4r02.pdf) */
+            if (actual_len < DEF_READ_BLOCK_LIMITS_LEN) {
+                pr2serr("Expected at least %d bytes in response but only "
+                        "%d bytes\n", DEF_READ_BLOCK_LIMITS_LEN, actual_len);
+                goto the_end;
+            }
+            max_block_size = sg_get_unaligned_be32(readBlkLmtBuff + 0);
+            // first byte contains granularity field
+            granularity = (max_block_size >> 24) & 0x1F;
+            max_block_size = max_block_size & 0xFFFFFF;
+            min_block_size = sg_get_unaligned_be16(readBlkLmtBuff + 4);
+            k = min_block_size / 1024;
+            printf("Read Block Limits results:\n");
+            printf("    Minimum block size: %u byte(s)",
+                   (unsigned int)min_block_size);
+            if (k != 0)
+                printf(", %d KB", k);
+            printf("\n");
+            k = max_block_size / 1024;
+            m = max_block_size / 1048576;
+            printf("    Maximum block size: %u byte(s)",
+                   (unsigned int)max_block_size);
+            if (k != 0)
+                printf(", %d KB", k);
+            if (m != 0)
+                printf(", %d MB", m);
+            printf("\n");
+            printf("    Granularity: %u",
+                   (unsigned int)granularity);
+            printf("\n");
+        }
+    } else {    /* error detected */
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Read block limits: %s\n", b);
+        if (0 == verbose)
+            pr2serr("    try '-v' option for more information\n");
+    }
+
+the_end:
+    res = sg_cmds_close_device(sg_fd);
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+the_end2:
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_read_block_limits failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_read_buffer.c b/src/sg_read_buffer.c
new file mode 100644
index 0000000..8dbd170
--- /dev/null
+++ b/src/sg_read_buffer.c
@@ -0,0 +1,844 @@
+/*
+ * Copyright (c) 2006-2022 Luben Tuikov and Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/*
+ * This utility issues the SCSI READ BUFFER(10 or 16) command to the given
+ * device.
+ */
+
+static const char * version_str = "1.35 20220217";      /* spc6r06 */
+
+#ifndef SG_READ_BUFFER_10_CMD
+#define SG_READ_BUFFER_10_CMD 0x3c
+#define SG_READ_BUFFER_10_CMDLEN 10
+#endif
+#ifndef SG_READ_BUFFER_16_CMD
+#define SG_READ_BUFFER_16_CMD 0x9b
+#define SG_READ_BUFFER_16_CMDLEN 16
+#endif
+
+#define MODE_HEADER_DATA        0
+#define MODE_VENDOR             1
+#define MODE_DATA               2
+#define MODE_DESCRIPTOR         3
+#define MODE_ECHO_BUFFER        0x0A
+#define MODE_ECHO_BDESC         0x0B
+#define MODE_READ_MICROCODE_ST  0x0F
+#define MODE_EN_EX_ECHO         0x1A
+#define MODE_ERR_HISTORY        0x1C
+
+#define MAX_DEF_INHEX_LEN 8192
+#define SENSE_BUFF_LEN  64      /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+#define DEF_RESPONSE_LEN 4      /* increased to 64 for MODE_ERR_HISTORY */
+
+
+static struct option long_options[] = {
+        {"16", no_argument, 0, 'L'},
+        {"eh_code", required_argument, 0, 'e'},
+        {"eh-code", required_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"id", required_argument, 0, 'i'},
+        {"inhex", required_argument, 0, 'I'},
+        {"length", required_argument, 0, 'l'},
+        {"long", no_argument, 0, 'L'},
+        {"mode", required_argument, 0, 'm'},
+        {"no_output", no_argument, 0, 'N'},
+        {"no-output", no_argument, 0, 'N'},
+        {"offset", required_argument, 0, 'o'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"specific", required_argument, 0, 'S'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},   /* sentinel */
+};
+
+struct opts_t {
+    bool do_long;
+    bool o_readonly;
+    bool do_raw;
+    bool eh_code_given;
+    bool no_output;
+    bool rb_id_given;
+    bool rb_len_given;
+    bool rb_mode_given;
+    bool verbose_given;
+    bool version_given;
+    int sg_fd;
+    int do_help;
+    int do_hex;
+    int eh_code;
+    int rb_id;
+    int rb_len;
+    int rb_mode;
+    int rb_mode_sp;
+    int verbose;
+    uint64_t rb_offset;
+    const char * device_name;
+    const char * inhex_name;
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_read_buffer [--16] [--eh_code=EHC] [--help] [--hex] "
+            "[--id=ID]\n"
+            "                      [--inhex=FN] [--length=LEN] [--long] "
+            "[--mode=MO]\n"
+            "                      [--no_output] [--offset=OFF] [--raw] "
+            "[--readonly]\n"
+            "                      [--specific=MS] [--verbose] [--version] "
+            "DEVICE\n"
+            "  where:\n"
+            "    --16|-L             issue READ BUFFER(16) (def: 10)\n"
+            "    --eh_code=EHC|-e EHC    same as '-m eh -i EHC' where "
+            "EHC is the\n"
+            "                            error history code\n"
+            "    --help|-h           print out usage message\n"
+            "    --hex|-H            print output in hex\n"
+            "    --id=ID|-i ID       buffer identifier (0 (default) to 255)\n"
+            "    --inhex=FN|-I FN    filename FN contains hex data to "
+            "decode\n"
+            "                        rather than DEVICE. If --raw given "
+            "then binary\n"
+            "    --length=LEN|-l LEN    length in bytes to read (def: 4, "
+            "64 for eh)\n"
+            "    --long|-L           issue READ BUFFER(16) (def: 10)\n"
+            "    --mode=MO|-m MO     read buffer mode, MO is number or "
+            "acronym (def: 0)\n"
+            "    --no_output|-N      perform the command then exit\n"
+            "    --offset=OFF|-o OFF    buffer offset (unit: bytes, def: 0)\n"
+            "    --raw|-r            output response in binary to stdout\n"
+            "    --readonly|-R       open DEVICE read-only (def: read-write)\n"
+            "    --specific=MS|-S MS    mode specific value; 3 bit field (0 "
+            "to 7)\n"
+            "    --verbose|-v        increase verbosity\n"
+            "    --version|-V        print version string and exit\n\n"
+            "Performs a SCSI READ BUFFER (10 or 16) command. Use '-m xxx' to "
+            "list\navailable modes. Some responses are decoded, others are "
+            "output in hex.\n"
+           );
+}
+
+
+static struct mode_s {
+        const char *mode_string;
+        int   mode;
+        const char *comment;
+} modes[] = {
+        { "hd",         MODE_HEADER_DATA, "combined header and data"},
+        { "vendor",     MODE_VENDOR,    "vendor specific"},
+        { "data",       MODE_DATA,      "data"},
+        { "desc",       MODE_DESCRIPTOR, "descriptor"},
+        { "echo",       MODE_ECHO_BUFFER, "read data from echo buffer "
+          "(spc-2)"},
+        { "echo_desc",  MODE_ECHO_BDESC, "echo buffer descriptor (spc-2)"},
+        { "rd_microc_st",  MODE_READ_MICROCODE_ST, "read microcode status "
+          "(spc-5)"},
+        { "en_ex",      MODE_EN_EX_ECHO,
+          "enable expander communications protocol and echo buffer (spc-3)"},
+        { "err_hist|eh",   MODE_ERR_HISTORY, "error history (spc-4)"},
+        { NULL,   999, NULL},   /* end sentinel */
+};
+
+
+static void
+print_modes(void)
+{
+    const struct mode_s *mp;
+
+    pr2serr("The modes parameter argument can be numeric (hex or decimal)\n"
+            "or symbolic:\n");
+    for (mp = modes; mp->mode_string; ++mp) {
+        pr2serr(" %2d (0x%02x)  %-16s%s\n", mp->mode, mp->mode,
+                mp->mode_string, mp->comment);
+    }
+}
+
+/* Invokes a SCSI READ BUFFER(10) command (spc5r02).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_read_buffer_10(void * resp, int * residp, bool noisy,
+                    const struct opts_t * op)
+{
+    int ret, res, sense_cat;
+    uint8_t rb10_cb[SG_READ_BUFFER_10_CMDLEN] =
+          {SG_READ_BUFFER_10_CMD, 0, 0, 0,  0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    rb10_cb[1] = (uint8_t)(op->rb_mode & 0x1f);
+    if (op->rb_mode_sp)
+        rb10_cb[1] |= (uint8_t)((op->rb_mode_sp & 0x7) << 5);
+    rb10_cb[2] = (uint8_t)op->rb_id;
+    sg_put_unaligned_be24(op->rb_offset, rb10_cb + 3);
+    sg_put_unaligned_be24(op->rb_len, rb10_cb + 6);
+    if (op->verbose) {
+        char b[128];
+
+        pr2serr("    Read buffer(10) cdb: %s\n",
+                sg_get_command_str(rb10_cb, SG_READ_BUFFER_10_CMDLEN, false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("Read buffer(10): out of memory\n");
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, rb10_cb, sizeof(rb10_cb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, op->rb_len);
+    res = do_scsi_pt(ptvp, op->sg_fd, DEF_PT_TIMEOUT, op->verbose);
+    ret = sg_cmds_process_resp(ptvp, "Read buffer(10)", res, noisy,
+                               op->verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        if ((op->verbose > 2) && (ret > 0)) {
+            pr2serr("    Read buffer(10): response%s\n",
+                    (ret > 256 ? ", first 256 bytes" : ""));
+            hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret), -1);
+        }
+        ret = 0;
+    }
+    if (residp)
+        *residp = get_scsi_pt_resid(ptvp);
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI READ BUFFER(16) command (spc5r02).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_read_buffer_16(void * resp, int * residp, bool noisy,
+                    const struct opts_t * op)
+{
+    int ret, res, sense_cat;
+    uint8_t rb16_cb[SG_READ_BUFFER_16_CMDLEN] =
+          {SG_READ_BUFFER_16_CMD, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
+           0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    rb16_cb[1] = (uint8_t)(op->rb_mode & 0x1f);
+    if (op->rb_mode_sp)
+        rb16_cb[1] |= (uint8_t)((op->rb_mode_sp & 0x7) << 5);
+    sg_put_unaligned_be64(op->rb_offset, rb16_cb + 2);
+    sg_put_unaligned_be32(op->rb_len, rb16_cb + 10);
+    rb16_cb[14] = (uint8_t)op->rb_id;
+    if (op->verbose) {
+        char b[128];
+
+        pr2serr("    Read buffer(16) cdb: %s\n",
+                sg_get_command_str(rb16_cb, SG_READ_BUFFER_16_CMDLEN, false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", __func__);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, rb16_cb, sizeof(rb16_cb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, op->rb_len);
+    res = do_scsi_pt(ptvp, op->sg_fd, DEF_PT_TIMEOUT, op->verbose);
+    ret = sg_cmds_process_resp(ptvp, "Read buffer(16)", res, noisy,
+                               op->verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        if ((op->verbose > 2) && (ret > 0)) {
+            pr2serr("    Read buffer(16): response%s\n",
+                    (ret > 256 ? ", first 256 bytes" : ""));
+            hex2stderr((const uint8_t *)resp, (ret > 256 ? 256 : ret), -1);
+        }
+        ret = 0;
+    }
+    if (residp)
+        *residp = get_scsi_pt_resid(ptvp);
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Microcode status: active, redundant and download */
+static const char * act_micro_st_arr[] = {
+    "Microcode status not reported",
+    "Activated microcode is valid",
+    "Activated microcode is not valid",
+    "Activated microcode is not a full microcode image",
+};
+
+static const char * red_micro_st_arr[] = {
+    "Redundant microcode status is not reported",
+    "At least one redundant microcode copy is valid",
+    "No redundant microcode copy is valid",
+    "Redundant microcode is not a full microcode image",
+};
+
+/* Major overlap between this SPC-4 table and SES-4r2 table 63 */
+struct sg_lib_simple_value_name_t down_micro_st_arr[] = {
+    {0x0, "No download microcode operation in progress"},
+    {0x1, "Download in progress, awaiting more"},               /* SES */
+    {0x2, "Download complete, updating storage"},               /* SES */
+    {0x3, "Updating storage with deferred microcode"},          /* SES */
+    {0x10, "Complete, no error, starting now"},                 /* SES */
+    {0x11, "Complete, no error, start after hard reset or power "
+           "cycle"},                                            /* SES */
+    {0x12, "Complete, no error, start after power cycle"},      /* SES */
+    {0x13, "Complete, no error, start after activate_mc, hard reset or "
+           "power cycle"},                                      /* SES */
+    {0x21, "Download in progress, awaiting more"},              /* SPC-6 */
+    {0x22, "Download complete, updating storage"},              /* SPC-6 */
+    {0x23, "Updating storage with deferred microcode"},         /* SPC-6 */
+    {0x30, "Deferred microcode download complete, no reports"}, /* SPC-6 */
+    {0x31, "Deferred download ok, await hard reset or power cycle"},
+    {0x32, "Deferred download ok, await power cycle"},          /* SPC-6 */
+    {0x33, "Deferred download ok, await any event"},            /* SPC-6 */
+    {0x34, "Deferred download ok, await Write buffer command"}, /* SPC-6 */
+    {0x35, "Deferred download ok, await any event, WB only this LU"},
+    {0x80, "Error, discarded, see additional status"},          /* SES */
+    {0x81, "Error, discarded, image error"},                    /* SES */
+    {0x82, "Timeout, discarded"},                               /* SES */
+    {0x83, "Internal error, need new microcode before reset"},  /* SES */
+    {0x84, "Internal error, need new microcode, reset safe"},   /* SES */
+    {0x85, "Unexpected activate_mc received"},                  /* SES */
+    {0x90, "Error, discarded, see additional status"},          /* SPC-6 */
+    {0x91, "Error, discarded, image error"},                    /* SPC-6 */
+    {0x92, "Timeout, discarded"},                               /* SPC-6 */
+    {0x93, "Internal error, need new microcode before reset"},  /* SPC-6 */
+    {0x94, "Internal error, need new microcode, reset safe"},   /* SPC-6 */
+    {0x95, "Unexpected activate_mc received, mcrocode discard"}, /* SPC-6 */
+    {0x1000, NULL},             /* End sentinel */
+};
+
+static void
+decode_microcode_status(const uint8_t * resp, const struct opts_t * op)
+{
+    int n;
+    uint32_t u;
+    const char * cp;
+    const struct sg_lib_simple_value_name_t * vnp;
+    char b[32];
+
+    if ((NULL == resp) || (op->rb_len < 1))
+        return;
+    n = resp[0];
+    if (n < (int)SG_ARRAY_SIZE(act_micro_st_arr))
+        cp = act_micro_st_arr[n];
+    else {
+        snprintf(b, sizeof(b), "unknown [0x%x]", n);
+        cp = b;
+    }
+    printf("Activated microcode status: %s\n", cp);
+
+    if (op->rb_len < 2)
+        return;
+    n = resp[1];
+    if (n < (int)SG_ARRAY_SIZE(red_micro_st_arr))
+        cp = red_micro_st_arr[n];
+    else {
+        snprintf(b, sizeof(b), "unknown [0x%x]", n);
+        cp = b;
+    }
+    printf("Redundant microcode status: %s\n", cp);
+
+    if (op->rb_len < 3)
+        return;
+    n = resp[2];
+    for (vnp = down_micro_st_arr, cp = NULL; vnp->name; ++vnp) {
+        if (vnp->value == n) {
+            cp = vnp->name;
+            break;
+        }
+    }
+    if (NULL == cp) {
+        snprintf(b, sizeof(b), "unknown [0x%x]", n);
+        cp = b;
+    }
+    printf("Download microcode status: %s\n", cp);
+
+    if (op->rb_len > 7) {
+        u = sg_get_unaligned_be32(resp + 4);
+        printf("Download microcode maximum size (bytes): %u [0x%x]\n", u, u);
+    }
+    if (op->rb_len > 15) {
+        u = sg_get_unaligned_be32(resp + 12);
+        printf("Download microcode expected buffer offset (bytes): %u "
+               "[0x%x]\n", u, u);
+    }
+}
+
+static void
+decode_error_history(const uint8_t * resp, const struct opts_t * op)
+{
+    static const char * eh_s = "Error history";
+    int k, num;
+    uint32_t dir_len;
+    const uint8_t * up;
+
+    if (op->rb_id < 0x4) {     /* eh directory variants */
+        if (op->rb_len < 8) {
+            pr2serr("%s response buffer too short [%d] to show directory "
+                    "header\n", eh_s, op->rb_len);
+            return;
+        }
+        printf("%s directory header:\n", eh_s);
+        printf("  T10 Vendor: %.8s\n", resp + 0);
+        printf("  Version: %u\n", resp[8]);
+        printf("  EHS_retrieved: %u\n", 0x3 & (resp[9] >> 3));
+        printf("  EHS_source: %u\n", 0x3 & (resp[9] >> 1));
+        printf("  CLR_SUP: %u\n", 0x1 & resp[9]);
+        if (op->rb_len < 32) {
+            pr2serr("%s response buffer too short [%d] to show directory "
+                    "length\n", eh_s, op->rb_len);
+            return;
+        }
+        dir_len = sg_get_unaligned_be16(resp + 30);
+        printf("  Directory length: %u\n", dir_len);
+        if ((unsigned)op->rb_len < (32 + dir_len)) {
+            pr2serr("%s directory entries truncated, try adding '-l %u' "
+                    "option\n", eh_s, 32 + dir_len);
+        }
+        num = (op->rb_len - 32) / 8;
+        for (k = 0, up = resp + 32; k < num; ++k, up += 8) {
+            if (k > 0)
+                printf("\n");
+            printf("   Supported buffer ID: 0x%x\n", up[0]);
+            printf("    Buffer format: 0x%x\n", up[1]);
+            printf("    Buffer source: 0x%x\n", 0xf & up[2]);
+            printf("    Maximum available length: 0x%x\n",
+                   sg_get_unaligned_be32(up + 4));
+        }
+    } else if ((op->rb_id >= 0x10) && (op->rb_id <= 0xef))
+        hex2stdout(resp, op->rb_len, (op->verbose > 1 ? 0 : 1));
+    else if (0xfe == op->rb_id)
+        pr2serr("clear %s I_T nexus [0x%x]\n", eh_s, op->rb_id);
+    else if (0xff == op->rb_id)
+        pr2serr("clear %s I_T nexus and release any snapshots [0x%x]\n",
+                eh_s, op->rb_id);
+    else
+        pr2serr("Reserved Buffer ID value [0x%x] for %s\n", op->rb_id, eh_s);
+
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+int
+main(int argc, char * argv[])
+{
+    int res, c, len, k;
+    int inhex_len = 0;
+    int resid = 0;
+    int ret = 0;
+    int64_t ll;
+    const char * cp = NULL;
+    uint8_t * resp = NULL;
+    uint8_t * free_resp = NULL;
+    const struct mode_s * mp;
+    struct opts_t opts SG_C_CPP_ZERO_INIT;
+    struct opts_t * op = &opts;
+
+    op->sg_fd = -1;
+    op->rb_len = DEF_RESPONSE_LEN;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "e:hHi:I:l:Lm:No:rRS:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'e':
+            if (op->rb_mode_given && (MODE_ERR_HISTORY != op->rb_mode)) {
+                pr2serr("mode incompatible with --eh_code= option\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->eh_code = sg_get_num(optarg);
+            if ((op->eh_code < 0) || (op->eh_code > 255)) {
+                pr2serr("argument to '--eh_code=' should be in the range 0 "
+                        "to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->rb_mode = MODE_ERR_HISTORY;
+            op->eh_code_given = true;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'i':
+            op->rb_id = sg_get_num(optarg);
+            if ((op->rb_id < 0) || (op->rb_id > 255)) {
+                pr2serr("argument to '--id=' should be in the range 0 to "
+                        "255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->rb_id_given = true;
+            break;
+        case 'I':
+            if (op->inhex_name) {
+                pr2serr("--inhex= option given more than once. Once only "
+                        "please\n");
+                return SG_LIB_SYNTAX_ERROR;
+            } else
+                op->inhex_name = optarg;
+            break;
+        case 'l':
+            op->rb_len = sg_get_num(optarg);
+            if (op->rb_len < 0) {
+                pr2serr("bad argument to '--length'\n");
+                return SG_LIB_SYNTAX_ERROR;
+             }
+             if (op->rb_len > 0xffffff) {
+                pr2serr("argument to '--length' must be <= 0xffffff\n");
+                return SG_LIB_SYNTAX_ERROR;
+             }
+             op->rb_len_given = true;
+             break;
+        case 'L':
+            op->do_long = true;
+            break;
+        case 'm':
+            if (NULL == optarg) {
+                pr2serr("bad argument to '--mode'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            } else if (isdigit((uint8_t)*optarg)) {
+                op->rb_mode = sg_get_num(optarg);
+                if ((op->rb_mode < 0) || (op->rb_mode > 31)) {
+                    pr2serr("argument to '--mode' should be in the range 0 "
+                            "to 31\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else {
+                len = strlen(optarg);
+                for (mp = modes; mp->mode_string; ++mp) {
+                    cp = strchr(mp->mode_string, '|');
+                    if (NULL == cp) {
+                        if (0 == strncmp(mp->mode_string, optarg, len)) {
+                            op->rb_mode = mp->mode;
+                            break;
+                        }
+                    } else {
+                        int f_len = cp - mp->mode_string;
+
+                        if ((f_len == len) &&
+                            (0 == memcmp(mp->mode_string, optarg, len))) {
+                            op->rb_mode = mp->mode;
+                            break;
+                        }
+                        if (0 == strncmp(cp + 1, optarg, len)) {
+                            op->rb_mode = mp->mode;
+                            break;
+                        }
+                    }
+                }
+                if (NULL == mp->mode_string) {
+                    print_modes();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            if (op->eh_code_given && (MODE_ERR_HISTORY != op->rb_mode)) {
+                pr2serr("mode incompatible with --eh_code= option\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->rb_mode_given = true;
+            break;
+        case 'N':
+            op->no_output = true;
+            break;
+        case 'o':
+           ll = sg_get_llnum(optarg);
+           if (ll < 0) {
+                pr2serr("bad argument to '--offset'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->rb_offset = ll;
+            break;
+        case 'r':
+            op->do_raw = true;
+            break;
+        case 'R':
+            op->o_readonly = true;
+            break;
+        case 'S':
+           op->rb_mode_sp = sg_get_num(optarg);
+           if ((op->rb_mode_sp < 0) || (op->rb_mode_sp > 7)) {
+                pr2serr("expected argument to '--specific' to be 0 to 7\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (op->do_help) {
+        if (op->do_help > 1) {
+            usage();
+            pr2serr("\n");
+            print_modes();
+        } else
+            usage();
+        return 0;
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+    if ((MODE_ERR_HISTORY == op->rb_mode) && (NULL == op->inhex_name)) {
+        if (! op->rb_len_given)
+            op->rb_len = 64;
+    }
+    if (op->eh_code_given) {
+        if (op->rb_id_given && (op->eh_code != op->rb_id)) {
+            pr2serr("Buffer ID incompatible with --eh_code= option\n");
+            return SG_LIB_CONTRADICT;
+        }
+        op->rb_id = op->eh_code;
+    }
+
+    if (op->device_name && op->inhex_name) {
+        pr2serr("Confused: both DEVICE (%s) and --inhex= option given. One "
+                "only please\n", op->device_name);
+                return SG_LIB_SYNTAX_ERROR;
+    } else if (op->inhex_name) {
+        op->rb_len = (op->rb_len > MAX_DEF_INHEX_LEN) ? op->rb_len :
+                                                        MAX_DEF_INHEX_LEN;
+        resp = (uint8_t *)sg_memalign(op->rb_len, 0, &free_resp, false);
+        ret = sg_f2hex_arr(op->inhex_name, op->do_raw, false, resp,
+                           &inhex_len, op->rb_len);
+        if (ret)
+            goto fini;
+        if (op->do_raw)
+            op->do_raw = false;     /* only used for input in this case */
+        op->rb_len = inhex_len;
+        resid = 0;
+        goto decode_result;
+    } else if (NULL == op->device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    len = op->rb_len ? op->rb_len : 8;
+    resp = (uint8_t *)sg_memalign(len, 0, &free_resp, false);
+    if (NULL == resp) {
+        pr2serr("unable to allocate %d bytes on the heap\n", len);
+        return SG_LIB_CAT_OTHER;
+    }
+
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto fini;
+        }
+    }
+
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+    if (op->verbose > 4)
+        pr2serr("Initial win32 SPT interface state: %s\n",
+                scsi_pt_win32_spt_state() ? "direct" : "indirect");
+    scsi_pt_win32_direct(SG_LIB_WIN32_DIRECT /* SPT pt interface */);
+#endif
+#endif
+
+    op->sg_fd = sg_cmds_open_device(op->device_name, op->o_readonly,
+                                    op->verbose);
+    if (op->sg_fd < 0) {
+        if (op->verbose)
+            pr2serr("open error: %s: %s\n", op->device_name,
+                    safe_strerror(-op->sg_fd));
+        ret = sg_convert_errno(-op->sg_fd);
+        goto fini;
+    }
+
+    if (op->do_long)
+        res = sg_ll_read_buffer_16(resp, &resid, true, op);
+    else if (op->rb_offset > 0xffffff) {
+        pr2serr("--offset value is too large for READ BUFFER(10), try "
+                "--16\n");
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto fini;
+    } else
+        res = sg_ll_read_buffer_10(resp, &resid, true, op);
+    if (0 != res) {
+        char b[80];
+
+        ret = res;
+        if (res > 0) {
+            sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+            pr2serr("Read buffer(%d) failed: %s\n",
+                    (op->do_long ? 16 : 10), b);
+        }
+        goto fini;
+    }
+    if (resid > 0)
+        op->rb_len -= resid;        /* got back less than requested */
+    if (op->no_output)
+        goto fini;
+decode_result:
+    if (op->rb_len > 0) {
+        if (op->do_raw)
+            dStrRaw(resp, op->rb_len);
+        else if (op->do_hex || (op->rb_len < 4)) {
+            k = (op->do_hex > 2) ? -1 : (2 - op->do_hex);
+            hex2stdout(resp, op->rb_len, k);
+        } else {
+            switch (op->rb_mode) {
+            case MODE_DESCRIPTOR:
+                k = sg_get_unaligned_be24(resp + 1);
+                printf("OFFSET BOUNDARY: %d, Buffer offset alignment: "
+                       "%d-byte\n", resp[0], (1 << resp[0]));
+                printf("BUFFER CAPACITY: %d (0x%x)\n", k, k);
+                break;
+            case MODE_ECHO_BDESC:
+                k = sg_get_unaligned_be16(resp + 2) & 0x1fff;
+                printf("EBOS:%d\n", resp[0] & 1 ? 1 : 0);
+                printf("Echo buffer capacity: %d (0x%x)\n", k, k);
+                break;
+            case MODE_READ_MICROCODE_ST:
+                decode_microcode_status(resp, op);
+                break;
+            case MODE_ERR_HISTORY:
+                decode_error_history(resp, op);
+                break;
+            default:
+                hex2stdout(resp, op->rb_len, (op->verbose > 1 ? 0 : 1));
+                break;
+            }
+        }
+    }
+
+fini:
+    if (free_resp)
+        free(free_resp);
+    if (op->sg_fd >= 0) {
+        res = sg_cmds_close_device(op->sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == op->verbose) {
+        if (! sg_if_can2stderr("sg_read_buffer failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_read_long.c b/src/sg_read_long.c
new file mode 100644
index 0000000..0a5e6a6
--- /dev/null
+++ b/src/sg_read_long.c
@@ -0,0 +1,325 @@
+/* A utility program for the Linux OS SCSI subsystem.
+ *  Copyright (C) 2004-2018 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program issues the SCSI command READ LONG to a given SCSI device.
+ * It sends the command with the logical block address passed as the lba
+ * argument, and the transfer length set to the xfer_len argument. the
+ * buffer to be written to the device filled with 0xff, this buffer includes
+ * the sector data and the ECC bytes.
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#include <errno.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.27 20180627";
+
+#define MAX_XFER_LEN 10000
+
+#define ME "sg_read_long: "
+
+#define EBUFF_SZ 512
+
+
+static struct option long_options[] = {
+        {"16", no_argument, 0, 'S'},
+        {"correct", no_argument, 0, 'c'},
+        {"help", no_argument, 0, 'h'},
+        {"lba", required_argument, 0, 'l'},
+        {"out", required_argument, 0, 'o'},
+        {"pblock", no_argument, 0, 'p'},
+        {"readonly", no_argument, 0, 'r'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"xfer_len", required_argument, 0, 'x'},
+        {"xfer-len", required_argument, 0, 'x'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_read_long [--16] [--correct] [--help] [--lba=LBA] "
+            "[--out=OF]\n"
+            "                    [--pblock] [--readonly] [--verbose] "
+            "[--version]\n"
+            "                    [--xfer_len=BTL] DEVICE\n"
+            "  where:\n"
+            "    --16|-S              do READ LONG(16) (default: "
+            "READ LONG(10))\n"
+            "    --correct|-c         use ECC to correct data "
+            "(default: don't)\n"
+            "    --help|-h            print out usage message\n"
+            "    --lba=LBA|-l LBA     logical block address"
+            " (default: 0)\n"
+            "    --out=OF|-o OF       output in binary to file named OF\n"
+            "    --pblock|-p          fetch physical block containing LBA\n"
+            "    --readonly|-r        open DEVICE read-only (def: open it "
+            "read-write)\n"
+            "    --verbose|-v         increase verbosity\n"
+            "    --version|-V         print version string and"
+            " exit\n"
+            "    --xfer_len=BTL|-x BTL    byte transfer length (< 10000)"
+            " default 520\n\n"
+            "Perform a SCSI READ LONG (10 or 16) command. Reads a single "
+            "block with\nassociated ECC data. The user data could be "
+            "encoded or encrypted.\n");
+}
+
+/* Returns 0 if successful */
+static int
+process_read_long(int sg_fd, bool do_16, bool pblock, bool correct,
+                  uint64_t llba, void * data_out, int xfer_len, int verbose)
+{
+    int offset, res;
+    const char * ten_or;
+    char b[80];
+
+    if (do_16)
+        res = sg_ll_read_long16(sg_fd, pblock, correct, llba, data_out,
+                                xfer_len, &offset, true, verbose);
+    else
+        res = sg_ll_read_long10(sg_fd, pblock, correct, (unsigned int)llba,
+                                data_out, xfer_len, &offset, true, verbose);
+    ten_or = do_16 ? "16" : "10";
+    switch (res) {
+    case 0:
+        break;
+    case SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO:
+        pr2serr("<<< device indicates 'xfer_len' should be %d >>>\n",
+                xfer_len - offset);
+        break;
+    default:
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("  SCSI READ LONG (%s): %s\n", ten_or, b);
+        break;
+    }
+    return res;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool correct = false;
+    bool do_16 = false;
+    bool pblock = false;
+    bool readonly = false;
+    bool got_stdout;
+    bool verbose_given = false;
+    bool version_given = false;
+    int outfd, res, c;
+    int sg_fd = -1;
+    int ret = 0;
+    int xfer_len = 520;
+    int verbose = 0;
+    uint64_t llba = 0;
+    int64_t ll;
+    uint8_t * readLongBuff = NULL;
+    uint8_t * rawp = NULL;
+    uint8_t * free_rawp = NULL;
+    const char * device_name = NULL;
+    char out_fname[256];
+    char ebuff[EBUFF_SZ];
+
+    memset(out_fname, 0, sizeof out_fname);
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "chl:o:prSvVx:", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            correct = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'l':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            llba = (uint64_t)ll;
+            break;
+        case 'o':
+            strncpy(out_fname, optarg, sizeof(out_fname) - 1);
+            break;
+        case 'p':
+            pblock = true;
+            break;
+        case 'r':
+            readonly = true;
+            break;
+        case 'S':
+            do_16 = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        case 'x':
+            xfer_len = sg_get_num(optarg);
+           if (-1 == xfer_len) {
+                pr2serr("bad argument to '--xfer_len'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (xfer_len >= MAX_XFER_LEN){
+        pr2serr("xfer_len (%d) is out of range ( < %d)\n", xfer_len,
+                MAX_XFER_LEN);
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    sg_fd = sg_cmds_open_device(device_name, readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    if (NULL == (rawp = (uint8_t *)sg_memalign(MAX_XFER_LEN, 0, &free_rawp,
+                                               false))) {
+        if (verbose)
+            pr2serr(ME "out of memory\n");
+        ret = sg_convert_errno(ENOMEM);
+        goto err_out;
+    }
+    readLongBuff = (uint8_t *)rawp;
+    memset(rawp, 0x0, MAX_XFER_LEN);
+
+    pr2serr(ME "issue read long (%s) to device %s\n    xfer_len=%d (0x%x), "
+            "lba=%" PRIu64 " (0x%" PRIx64 "), correct=%d\n",
+            (do_16 ? "16" : "10"), device_name, xfer_len, xfer_len, llba,
+            llba, (int)correct);
+
+    if ((ret = process_read_long(sg_fd, do_16, pblock, correct, llba,
+                                 readLongBuff, xfer_len, verbose)))
+        goto err_out;
+
+    if ('\0' == out_fname[0])
+        hex2stdout((const uint8_t *)rawp, xfer_len, 0);
+    else {
+        got_stdout = (0 == strcmp(out_fname, "-"));
+        if (got_stdout)
+            outfd = STDOUT_FILENO;
+        else {
+            if ((outfd = open(out_fname, O_WRONLY | O_CREAT | O_TRUNC,
+                              0666)) < 0) {
+                snprintf(ebuff, EBUFF_SZ,
+                         ME "could not open %s for writing", out_fname);
+                perror(ebuff);
+                goto err_out;
+            }
+        }
+        if (sg_set_binary_mode(outfd) < 0) {
+            perror("sg_set_binary_mode");
+            goto err_out;
+        }
+        res = write(outfd, readLongBuff, xfer_len);
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ, ME "couldn't write to %s", out_fname);
+            perror(ebuff);
+            goto err_out;
+        }
+        if (! got_stdout)
+            close(outfd);
+    }
+
+err_out:
+    if (free_rawp)
+        free(free_rawp);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_read_long failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_readcap.c b/src/sg_readcap.c
new file mode 100644
index 0000000..571f92e
--- /dev/null
+++ b/src/sg_readcap.c
@@ -0,0 +1,682 @@
+/* This code is does a SCSI READ CAPACITY command on the given device
+ * and outputs the result.
+ *
+ * Copyright (C) 1999 - 2020 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program was originally written with Linux 2.4 kernel series.
+ * It now builds for the Linux 2.6, 3 and 4 kernel series and various other
+ * operating systems.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "4.05 20200122";
+
+#define ME "sg_readcap: "
+
+#define RCAP_REPLY_LEN 8
+#define RCAP16_REPLY_LEN 32
+
+static struct option long_options[] = {
+    {"brief", no_argument, 0, 'b'},
+    {"help", no_argument, 0, 'h'},
+    {"hex", no_argument, 0, 'H'},
+    {"lba", required_argument, 0, 'L'},
+    {"long", no_argument, 0, 'l'},
+    {"16", no_argument, 0, 'l'},
+    {"new", no_argument, 0, 'N'},
+    {"old", no_argument, 0, 'O'},
+    {"pmi", no_argument, 0, 'p'},
+    {"raw", no_argument, 0, 'r'},
+    {"readonly", no_argument, 0, 'R'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {"zbc", no_argument, 0, 'z'},
+    {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool do_brief;
+    bool do_long;
+    bool do_pmi;
+    bool do_raw;
+    bool o_readonly;
+    bool do_zbc;
+    bool opt_new;
+    bool verbose_given;
+    bool version_given;
+    int do_help;
+    int do_hex;
+    int do_lba;
+    int verbose;
+    uint64_t llba;
+    const char * device_name;
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_readcap [--16] [--brief] [--help] [--hex] "
+            "[--lba=LBA] [--long]\n"
+            "                  [--pmi] [--raw] [--readonly] [--verbose] "
+            "[--version]\n"
+            "                  [--zbc] DEVICE\n"
+            "  where:\n"
+            "    --16            use READ CAPACITY (16) cdb (same as "
+            "--long)\n"
+            "    --brief|-b      brief, two hex numbers: number of blocks "
+            "and block size\n"
+            "    --help|-h       print this usage message and exit\n"
+            "    --hex|-H        output response in hexadecimal to stdout\n"
+            "    --lba=LBA|-L LBA    yields the last block prior to (head "
+            "movement) delay\n"
+            "                        after LBA [in decimal (def: 0) "
+            "valid with '--pmi']\n"
+            "    --long|-l       use READ CAPACITY (16) cdb (def: use "
+            "10 byte cdb)\n"
+            "    --pmi|-p        partial medium indicator (without this "
+            "option shows\n"
+            "                    total disk capacity) [made obsolete in "
+            "sbc3r26]\n"
+            "    --raw|-r        output response in binary to stdout\n"
+            "    --readonly|-R    open DEVICE read-only (def: RCAP(16) "
+            "read-write)\n"
+            "    --verbose|-v    increase verbosity\n"
+            "    --version|-V    print version string and exit\n"
+            "    --old|-O        use old interface (use as first option)\n"
+            "    --zbc|-z        show rc_basis ZBC field (implies --16)\n\n"
+            "Perform a SCSI READ CAPACITY (10 or 16) command\n");
+}
+
+static void
+usage_old()
+{
+    pr2serr("Usage:  sg_readcap [-16] [-b] [-h] [-H] [-lba=LBA] "
+            "[-pmi] [-r] [-R]\n"
+            "                   [-v] [-V] [-z] DEVICE\n"
+            "  where:\n"
+            "    -16    use READ CAPACITY (16) cdb (def: use "
+            "10 byte cdb)\n"
+            "    -b     brief, two hex numbers: number of blocks "
+            "and block size\n"
+            "    -h     print this usage message and exit\n"
+            "    -H     output response in hexadecimal to stdout\n"
+            "    -lba=LBA    yields the last block prior to (head "
+            "movement) delay\n"
+            "                after LBA [in hex (def: 0) "
+            "valid with -pmi]\n"
+            "    -pmi   partial medium indicator (without this option "
+            "shows total\n"
+            "           disk capacity)\n"
+            "    -r     output response in binary to stdout\n"
+            "    -R     open DEVICE read-only (def: RCAP(16) read-write)\n"
+            "    -v     increase verbosity\n"
+            "    -V     print version string and exit\n"
+            "    -N|--new   use new interface\n"
+            "    -z     show rc_basis ZBC field (implies -16)\n\n"
+            "Perform a SCSI READ CAPACITY (10 or 16) command\n");
+}
+
+static void
+usage_for(const struct opts_t * op)
+{
+    if (op->opt_new)
+        usage();
+    else
+        usage_old();
+}
+
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int c;
+    int a_one = 0;
+    int64_t nn;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "16bhHlL:NOprRvVz", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case '1':
+            ++a_one;
+            break;
+        case '6':
+            if (a_one)
+                op->do_long = true;
+            break;
+        case 'b':
+            op->do_brief = true;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'l':
+            op->do_long = true;
+            break;
+        case 'L':
+            nn = sg_get_llnum(optarg);
+            if (-1 == nn) {
+                pr2serr("bad argument to '--lba='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->llba = nn;
+            /* force READ_CAPACITY16 for large lbas */
+            if (op->llba > 0xfffffffeULL)
+                op->do_long = true;
+            ++op->do_lba;
+            break;
+        case 'N':
+            break;      /* ignore */
+        case 'O':
+            op->opt_new = false;
+            return 0;
+        case 'p':
+            op->do_pmi = true;
+            break;
+        case 'r':
+            op->do_raw = true;
+            break;
+        case 'R':
+            op->o_readonly = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'z':
+            op->do_zbc = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool jmp_out;
+    int k, plen, num;
+    const char * cp;
+    uint64_t uu;
+
+    for (k = 1; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case '1':
+                    if ('6' == *(cp + 1)) {
+                        op->do_long = true;
+                        ++cp;
+                        --plen;
+                    } else
+                        jmp_out = true;
+                    break;
+                case 'b':
+                    op->do_brief = true;
+                    break;
+                case 'h':
+                case '?':
+                    ++op->do_help;
+                    break;
+                case 'H':
+                    ++op->do_hex;
+                    break;
+                case 'N':
+                    op->opt_new = true;
+                    return 0;
+                case 'O':
+                    break;
+                case 'p':
+                    if (0 == strncmp("pmi", cp, 3)) {
+                        op->do_pmi = true;
+                        cp += 2;
+                        plen -= 2;
+                    } else
+                        jmp_out = true;
+                    break;
+                case 'r':
+                    op->do_raw = true;
+                    break;
+                case 'R':
+                    op->o_readonly = true;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                case 'z':
+                    op->do_zbc = true;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            if (0 == strncmp("lba=", cp, 4)) {
+                num = sscanf(cp + 4, "%" SCNx64 "", &uu);
+                if (1 != num) {
+                    printf("Bad value after 'lba=' option\n");
+                    usage();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                /* force READ_CAPACITY16 for large lbas */
+                if (uu > 0xfffffffeULL)
+                    op->do_long = true;
+                op->llba = uu;
+                ++op->do_lba;
+            } else if (0 == strncmp("-old", cp, 4))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == op->device_name)
+            op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not expecting: %s\n",
+                    op->device_name, cp);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opt_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opt_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opt_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (! op->opt_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+static const char *
+rc_basis_str(int rc_basis, char * b, int blen)
+{
+    switch (rc_basis) {
+    case 0:
+        snprintf(b, blen, "last contiguous that's not seq write required");
+        break;
+    case 1:
+        snprintf(b, blen, "last LBA on logical unit");
+        break;
+    default:
+        snprintf(b, blen, "reserved (0x%x)", rc_basis);
+        break;
+    }
+    return b;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool rw_0_flag;
+    int res, prot_en, p_type, lbppbe;
+    int sg_fd = -1;
+    int ret = 0;
+    uint32_t last_blk_addr, block_size;
+    uint64_t llast_blk_addr;
+    uint8_t * resp_buff;
+    uint8_t * free_resp_buff;
+    const int resp_buff_sz = RCAP16_REPLY_LEN;
+    char b[80];
+    struct opts_t opts;
+    struct opts_t * op;
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return res;
+    if (op->do_help) {
+        usage_for(op);
+        return 0;
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == op->device_name) {
+        pr2serr("No DEVICE argument given\n\n");
+        usage_for(op);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+    if (op->do_zbc) {
+        if (! op->do_long)
+            op->do_long = true;
+    }
+
+    resp_buff = sg_memalign(resp_buff_sz, 0, &free_resp_buff, false);
+    if (NULL == resp_buff) {
+        pr2serr("Unable to allocate %d bytes on heap\n", resp_buff_sz);
+        return sg_convert_errno(ENOMEM);
+    }
+    if ((! op->do_pmi) && (op->llba > 0)) {
+        pr2serr(ME "lba can only be non-zero when '--pmi' is set\n");
+        usage_for(op);
+        ret = SG_LIB_CONTRADICT;
+        goto fini;
+    }
+    if (op->do_long)
+        rw_0_flag = op->o_readonly;
+    else
+        rw_0_flag = true;  /* RCAP(10) has opened RO in past, so leave */
+    if ((sg_fd = sg_cmds_open_device(op->device_name, rw_0_flag,
+                                     op->verbose)) < 0) {
+        pr2serr(ME "error opening file: %s: %s\n", op->device_name,
+                safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    if (! op->do_long) {
+        res = sg_ll_readcap_10(sg_fd, op->do_pmi, (unsigned int)op->llba,
+                               resp_buff, RCAP_REPLY_LEN, true,
+                               op->verbose);
+        ret = res;
+        if (0 == res) {
+            if (op->do_hex || op->do_raw) {
+                if (op->do_raw)
+                    dStrRaw(resp_buff, RCAP_REPLY_LEN);
+                else if (op->do_hex > 2)
+                    hex2stdout(resp_buff, RCAP_REPLY_LEN, -1);
+                else
+                    hex2stdout(resp_buff, RCAP_REPLY_LEN, 1);
+                goto fini;
+            }
+            last_blk_addr = sg_get_unaligned_be32(resp_buff + 0);
+            if (0xffffffff != last_blk_addr) {
+                block_size = sg_get_unaligned_be32(resp_buff + 4);
+                if (op->do_brief) {
+                    printf("0x%" PRIx32 " 0x%" PRIx32 "\n",
+                           last_blk_addr + 1, block_size);
+                    goto fini;
+                }
+                printf("Read Capacity results:\n");
+                if (op->do_pmi)
+                    printf("   PMI mode: given lba=0x%" PRIx64 ", last lba "
+                           "before delay=0x%" PRIx32 "\n", op->llba,
+                           last_blk_addr);
+                else
+                    printf("   Last LBA=%" PRIu32 " (0x%" PRIx32 "), Number "
+                           "of logical blocks=%" PRIu32 "\n", last_blk_addr,
+                            last_blk_addr, last_blk_addr + 1);
+                printf("   Logical block length=%u bytes\n", block_size);
+                if (! op->do_pmi) {
+                    uint64_t total_sz = last_blk_addr + 1;
+                    double sz_mb, sz_gb;
+
+                    total_sz *= block_size;
+                    sz_mb = ((double)(last_blk_addr + 1) * block_size) /
+                            (double)(1048576);
+                    sz_gb = ((double)(last_blk_addr + 1) * block_size) /
+                            (double)(1000000000L);
+                    printf("Hence:\n");
+#ifdef SG_LIB_MINGW
+                    printf("   Device size: %" PRIu64 " bytes, %g MiB, %g "
+                           "GB", total_sz, sz_mb, sz_gb);
+#else
+                    printf("   Device size: %" PRIu64 " bytes, %.1f MiB, "
+                           "%.2f GB", total_sz, sz_mb, sz_gb);
+#endif
+                    if (sz_gb > 2000) {
+#ifdef SG_LIB_MINGW
+                        printf(", %g TB", sz_gb / 1000);
+#else
+                        printf(", %.2f TB", sz_gb / 1000);
+#endif
+                    }
+                    printf("\n");
+                }
+                goto fini;
+            } else {
+                printf("READ CAPACITY (10) indicates device capacity too "
+                       "large\n  now trying 16 byte cdb variant\n");
+                op->do_long = true;
+            }
+        } else if (SG_LIB_CAT_INVALID_OP == res) {
+            op->do_long = true;
+            sg_cmds_close_device(sg_fd);
+            if ((sg_fd = sg_cmds_open_device(op->device_name, op->o_readonly,
+                                             op->verbose)) < 0) {
+                pr2serr(ME "error re-opening file: %s (rw): %s\n",
+                        op->device_name, safe_strerror(-sg_fd));
+                ret = sg_convert_errno(-sg_fd);
+                goto fini;
+            }
+            if (op->verbose)
+                pr2serr("READ CAPACITY (10) not supported, trying READ "
+                        "CAPACITY (16)\n");
+        } else if (res) {
+            sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+            pr2serr("READ CAPACITY (10) failed: %s\n", b);
+        }
+    }
+    if (op->do_long) {
+        res = sg_ll_readcap_16(sg_fd, op->do_pmi, op->llba, resp_buff,
+                               RCAP16_REPLY_LEN, true, op->verbose);
+        ret = res;
+        if (0 == res) {
+            if (op->do_hex || op->do_raw) {
+                if (op->do_raw)
+                    dStrRaw(resp_buff, RCAP16_REPLY_LEN);
+                else if (op->do_hex > 2)
+                    hex2stdout(resp_buff, RCAP16_REPLY_LEN, -1);
+                else
+                    hex2stdout(resp_buff, RCAP16_REPLY_LEN, 1);
+                goto fini;
+            }
+            llast_blk_addr = sg_get_unaligned_be64(resp_buff + 0);
+            block_size = sg_get_unaligned_be32(resp_buff + 8);
+            if (op->do_brief) {
+                printf("0x%" PRIx64 " 0x%" PRIx32 "\n", llast_blk_addr + 1,
+                       block_size);
+                goto fini;
+            }
+            prot_en = !!(resp_buff[12] & 0x1);
+            p_type = ((resp_buff[12] >> 1) & 0x7);
+            printf("Read Capacity results:\n");
+            printf("   Protection: prot_en=%d, p_type=%d, p_i_exponent=%d",
+                   prot_en, p_type, ((resp_buff[13] >> 4) & 0xf));
+            if (prot_en)
+                printf(" [type %d protection]\n", p_type + 1);
+            else
+                printf("\n");
+            if (op->do_zbc) {
+                int rc_basis = (resp_buff[12] >> 4) & 0x3;
+
+                printf("   ZBC's rc_basis=%d [%s]\n", rc_basis,
+                       rc_basis_str(rc_basis, b, sizeof(b)));
+            }
+            printf("   Logical block provisioning: lbpme=%d, lbprz=%d\n",
+                   !!(resp_buff[14] & 0x80), !!(resp_buff[14] & 0x40));
+            if (op->do_pmi)
+                printf("   PMI mode: given lba=0x%" PRIx64 ", last lba "
+                       "before delay=0x%" PRIx64 "\n", op->llba,
+                       llast_blk_addr);
+            else
+                printf("   Last LBA=%" PRIu64 " (0x%" PRIx64 "), Number of "
+                       "logical blocks=%" PRIu64 "\n", llast_blk_addr,
+                       llast_blk_addr, llast_blk_addr + 1);
+            printf("   Logical block length=%" PRIu32 " bytes\n", block_size);
+            lbppbe = resp_buff[13] & 0xf;
+            printf("   Logical blocks per physical block exponent=%d",
+                   lbppbe);
+            if (lbppbe > 0)
+                printf(" [so physical block length=%u bytes]\n",
+                       block_size * (1 << lbppbe));
+            else
+                printf("\n");
+            printf("   Lowest aligned LBA=%d\n",
+                   ((resp_buff[14] & 0x3f) << 8) + resp_buff[15]);
+            if (! op->do_pmi) {
+                uint64_t total_sz = llast_blk_addr + 1;
+                double sz_mb, sz_gb;
+
+                total_sz *= block_size;
+                sz_mb = ((double)(llast_blk_addr + 1) * block_size) /
+                        (double)(1048576);
+                sz_gb = ((double)(llast_blk_addr + 1) * block_size) /
+                        (double)(1000000000L);
+                printf("Hence:\n");
+#ifdef SG_LIB_MINGW
+                printf("   Device size: %" PRIu64 " bytes, %g MiB, %g GB",
+                       total_sz, sz_mb, sz_gb);
+#else
+                printf("   Device size: %" PRIu64 " bytes, %.1f MiB, %.2f "
+                       "GB", total_sz, sz_mb, sz_gb);
+#endif
+                if (sz_gb > 2000) {
+#ifdef SG_LIB_MINGW
+                    printf(", %g TB", sz_gb / 1000);
+#else
+                    printf(", %.2f TB", sz_gb / 1000);
+#endif
+                }
+                printf("\n");
+            }
+            goto fini;
+        } else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+            pr2serr("bad field in READ CAPACITY (16) cdb including "
+                    "unsupported service action\n");
+        else if (res) {
+            sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+            pr2serr("READ CAPACITY (16) failed: %s\n", b);
+        }
+    }
+    if (op->do_brief)
+        printf("0x0 0x0\n");
+fini:
+    if (free_resp_buff)
+        free(free_resp_buff);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == op->verbose) {
+        if (! sg_if_can2stderr("sg_readcap failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_reassign.c b/src/sg_reassign.c
new file mode 100644
index 0000000..68668ec
--- /dev/null
+++ b/src/sg_reassign.c
@@ -0,0 +1,508 @@
+/*
+ * Copyright (c) 2005-2019 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#include <limits.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ * This utility invokes the REASSIGN BLOCKS SCSI command to reassign
+ * an existing (possibly damaged) lba on a direct access device (e.g.
+ * a disk) to a new physical location. The previous contents is
+ * recoverable then it is written to the remapped lba otherwise
+ * vendor specific data is written.
+ */
+
+static const char * version_str = "1.27 20191001";
+
+#define DEF_DEFECT_LIST_FORMAT 4        /* bytes from index */
+
+#define MAX_NUM_ADDR 1024
+
+#ifndef UINT32_MAX
+#define UINT32_MAX ((uint32_t)-1)
+#endif
+
+
+static struct option long_options[] = {
+        {"address", required_argument, 0, 'a'},
+        {"dummy", no_argument, 0, 'd'},
+        {"eight", required_argument, 0, 'e'},
+        {"grown", no_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"longlist", required_argument, 0, 'l'},
+        {"primary", no_argument, 0, 'p'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_reassign [--address=A,A...] [--dummy] [--eight=0|1] "
+            "[--grown]\n"
+            "                   [--help] [--hex] [--longlist=0|1] "
+            "[--primary] [--verbose]\n"
+            "                   [--version] DEVICE\n"
+            "  where:\n"
+            "    --address=A,A...|-a A,A...    comma separated logical block "
+            "addresses\n"
+            "                                  one or more, assumed to be "
+            "decimal\n"
+            "    --address=-|-a -    read stdin for logical block "
+            "addresses\n"
+            "    --dummy|-d          prepare but do not execute REASSIGN "
+            "BLOCKS command\n"
+            "    --eight=0|1\n"
+            "      -e 0|1            force eight byte (64 bit) lbas "
+            "when 1,\n"
+            "                        four byte (32 bit) lbas when 0 "
+            "(def)\n"
+            "    --grown|-g          fetch grown defect list length, "
+            "don't reassign\n"
+            "    --help|-h           print out usage message\n"
+            "    --hex|-H            print response in hex (for '-g' or "
+            "'-p')\n"
+            "    --longlist=0|1\n"
+            "       -l 0|1           use 4 byte list length when 1, safe to "
+            "ignore\n"
+            "                        (def: 0 (2 byte list length))\n"
+            "    --primary|-p        fetch primary defect list length, "
+            "don't reassign\n"
+            "    --verbose|-v        increase verbosity\n"
+            "    --version|-V        print version string and exit\n\n"
+            "Perform a SCSI REASSIGN BLOCKS command (or READ DEFECT LIST)\n");
+}
+
+/* Read numbers (up to 64 bits in size) from command line (comma (or
+ * (single) space) separated list) or from stdin (one per line, comma
+ * separated list or space separated list). Assumed decimal unless prefixed
+ * by '0x', '0X' or contains trailing 'h' or 'H' (which indicate hex).
+ * Returns 0 if ok, or error code. */
+static int
+build_lba_arr(const char * inp, uint64_t * lba_arr,
+              int * lba_arr_len, int max_arr_len)
+{
+    int in_len, k, j, m;
+    const char * lcp;
+    int64_t ll;
+    char * cp;
+    char * c2p;
+
+    if ((NULL == inp) || (NULL == lba_arr) ||
+        (NULL == lba_arr_len))
+        return SG_LIB_LOGIC_ERROR;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *lba_arr_len = 0;
+    if ('-' == inp[0]) {        /* read from stdin */
+        char line[1024];
+        int off = 0;
+
+        for (j = 0; j < 512; ++j) {
+            if (NULL == fgets(line, sizeof(line), stdin))
+                break;
+            // could improve with carry_over logic if sizeof(line) too small
+            in_len = strlen(line);
+            if (in_len > 0) {
+                if ('\n' == line[in_len - 1]) {
+                    --in_len;
+                    line[in_len] = '\0';
+                }
+            }
+            if (in_len < 1)
+                continue;
+            lcp = line;
+            m = strspn(lcp, " \t");
+            if (m == in_len)
+                continue;
+            lcp += m;
+            in_len -= m;
+            if ('#' == *lcp)
+                continue;
+            k = strspn(lcp, "0123456789aAbBcCdDeEfFhHxX ,\t");
+            if ((k < in_len) && ('#' != lcp[k])) {
+                pr2serr("%s: syntax error at line %d, pos %d\n", __func__,
+                        j + 1, m + k + 1);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            for (k = 0; k < 1024; ++k) {
+                ll = sg_get_llnum_nomult(lcp);
+                if (-1 != ll) {
+                    if ((off + k) >= max_arr_len) {
+                        pr2serr("%s: array length exceeded\n", __func__);
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    lba_arr[off + k] = (uint64_t)ll;
+                    lcp = strpbrk(lcp, " ,\t");
+                    if (NULL == lcp)
+                        break;
+                    lcp += strspn(lcp, " ,\t");
+                    if ('\0' == *lcp)
+                        break;
+                } else {
+                    if ('#' == *lcp) {
+                        --k;
+                        break;
+                    }
+                    pr2serr("%s: error in line %d, at pos %d\n", __func__,
+                            j + 1, (int)(lcp - line + 1));
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            off += (k + 1);
+        }
+        *lba_arr_len = off;
+    } else {        /* list of numbers (default decimal) on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfFhHxX, ");
+        if (in_len != k) {
+            pr2serr("%s: error at pos %d\n", __func__, k + 1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        for (k = 0; k < max_arr_len; ++k) {
+            ll = sg_get_llnum_nomult(lcp);
+            if (-1 != ll) {
+                lba_arr[k] = (uint64_t)ll;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("%s: error at pos %d\n", __func__,
+                        (int)(lcp - inp + 1));
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+        *lba_arr_len = k + 1;
+        if (k == max_arr_len) {
+            pr2serr("%s: array length exceeded\n", __func__);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool dummy = false;
+    bool eight = false;
+    bool eight_given = false;
+    bool got_addr = false;
+    bool longlist = false;
+    bool primary = false;
+    bool grown = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, num, k, j;
+    int sg_fd = -1;
+    int addr_arr_len = 0;
+    int do_hex = 0;
+    int verbose = 0;
+    const char * device_name = NULL;
+    uint64_t addr_arr[MAX_NUM_ADDR];
+    uint8_t param_arr[4 + (MAX_NUM_ADDR * 8)];
+    char b[80];
+    int param_len = 4;
+    int ret = 0;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "a:de:ghHl:pvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            memset(addr_arr, 0, sizeof(addr_arr));
+            if ((res = build_lba_arr(optarg, addr_arr, &addr_arr_len,
+                                     MAX_NUM_ADDR))) {
+                pr2serr("bad argument to '--address'\n");
+                return res;
+            }
+            got_addr = true;
+            break;
+        case 'd':
+            dummy = true;
+            break;
+        case 'e':
+            num = sscanf(optarg, "%d", &res);
+            if ((1 == num) && ((0 == res) || (1 == res)))
+                eight = !! res;
+            else {
+                pr2serr("value for '--eight=' must be 0 or 1\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            eight_given = true;
+            break;
+        case 'g':
+            grown = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'l':
+            num = sscanf(optarg, "%d", &res);
+            if ((1 == num) && ((0 == res) || (1 == res)))
+                longlist = !!res;
+            else {
+                pr2serr("value for '--longlist=' must be 0 or 1\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'p':
+            primary = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (grown || primary) {
+        if (got_addr) {
+            pr2serr("can't have '--address=' with '--grown' or '--primary'\n");
+            usage();
+            return SG_LIB_CONTRADICT;
+        }
+    } else if ((! got_addr) || (addr_arr_len < 1)) {
+        pr2serr("need at least one address (see '--address=')\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (got_addr) {
+        for (k = 0; k < addr_arr_len; ++k) {
+            if (addr_arr[k] >= UINT32_MAX) {
+                if (! eight_given) {
+                    eight = true;
+                    break;
+                } else if (! eight) {
+                    pr2serr("address number %d exceeds 32 bits so "
+                            "'--eight=0' invalid\n", k + 1);
+                    return SG_LIB_CONTRADICT;
+                }
+            }
+        }
+        if (! eight_given)
+            eight = false;
+
+        k = 4;
+        for (j = 0; j < addr_arr_len; ++j) {
+            if (eight) {
+                sg_put_unaligned_be64(addr_arr[j], param_arr + k);
+                k += 8;
+            } else {
+                sg_put_unaligned_be32((uint32_t)addr_arr[j], param_arr + k);
+                k += 4;
+            }
+        }
+        param_len = k;
+        k -= 4;
+        if (longlist)
+            sg_put_unaligned_be32((uint32_t)k, param_arr + 0);
+        else
+            sg_put_unaligned_be16((uint16_t)k, param_arr + 2);
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    if (got_addr) {
+        if (dummy) {
+            pr2serr(">>> dummy: REASSIGN BLOCKS not executed\n");
+            if (verbose) {
+                pr2serr("  Would have reassigned these blocks:\n");
+                for (j = 0; j < addr_arr_len; ++j)
+                    printf("    0x%" PRIx64 "\n", addr_arr[j]);
+            }
+            return 0;
+        }
+        res = sg_ll_reassign_blocks(sg_fd, eight, longlist, param_arr,
+                                    param_len, true, verbose);
+        ret = res;
+        if (res) {
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("REASSIGN BLOCKS: %s\n", b);
+            goto err_out;
+        }
+    } else /* if (grown || primary) */ {
+        int dl_format = DEF_DEFECT_LIST_FORMAT;
+        int div = 0;
+        int dl_len;
+        bool got_grown, got_primary;
+        const char * lstp;
+
+        param_len = 4;
+        memset(param_arr, 0, param_len);
+        res = sg_ll_read_defect10(sg_fd, primary, grown, dl_format,
+                                  param_arr, param_len, false, verbose);
+        ret = res;
+        if (res) {
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("READ DEFECT DATA(10): %s\n", b);
+            goto err_out;
+        }
+        if (do_hex) {
+            hex2stdout(param_arr, param_len, 1);
+            goto err_out;       /* ret is zero */
+        }
+        got_grown = !!(param_arr[1] & 0x8);
+        got_primary = !!(param_arr[1] & 0x10);
+        if (got_grown && got_primary)
+            lstp = "grown and primary defect lists";
+        else if (got_grown)
+            lstp = "grown defect list";
+        else if (got_primary)
+            lstp = "primary defect list";
+        else {
+            pr2serr("didn't get grown or primary list in response\n");
+            goto err_out;
+        }
+        if (verbose)
+            pr2serr("asked for defect list format %d, got %d\n", dl_format,
+                    (param_arr[1] & 0x7));
+        dl_format = (param_arr[1] & 0x7);
+        switch (dl_format) {    /* Defect list formats: */
+            case 0:     /* short block */
+                div = 4;
+                break;
+            case 1:     /* extended bytes from index */
+                div = 8;
+                break;
+            case 2:     /* extended physical sector */
+                div = 8;
+                break;
+            case 3:     /* long block */
+            case 4:     /* bytes from index */
+            case 5:     /* physical sector */
+                div = 8;
+                break;
+            case 6:     /* vendor specific */
+                if (verbose)
+                    pr2serr("defect list format: vendor specific\n");
+                break;
+            default:
+                pr2serr("defect list format %d unknown\n", dl_format);
+                break;
+        }
+        dl_len = sg_get_unaligned_be16(param_arr + 2);
+        if (0 == dl_len)
+            printf(">> Elements in %s: 0\n", lstp);
+        else {
+            if (0 == div)
+                printf(">> %s length=%d bytes [unknown number of elements]\n",
+                       lstp, dl_len);
+            else
+                printf(">> Elements in %s: %d\n", lstp,
+                       dl_len / div);
+        }
+    }
+
+err_out:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_reassign failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_referrals.c b/src/sg_referrals.c
new file mode 100644
index 0000000..35cf50e
--- /dev/null
+++ b/src/sg_referrals.c
@@ -0,0 +1,387 @@
+/*
+ * Copyright (c) 2010-2018 Hannes Reinecke.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/*
+ * A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI REPORT REFERRALS command to the given
+ * SCSI device.
+ */
+
+static const char * version_str = "1.13 20180628";    /* sbc4r10 */
+
+#define MAX_REFER_BUFF_LEN (1024 * 1024)
+#define DEF_REFER_BUFF_LEN 256
+
+#define TPGS_STATE_OPTIMIZED 0x0
+#define TPGS_STATE_NONOPTIMIZED 0x1
+#define TPGS_STATE_STANDBY 0x2
+#define TPGS_STATE_UNAVAILABLE 0x3
+#define TPGS_STATE_LB_DEPENDENT 0x4
+#define TPGS_STATE_OFFLINE 0xe          /* SPC-4 rev 9 */
+#define TPGS_STATE_TRANSITIONING 0xf
+
+static uint8_t referralBuff[DEF_REFER_BUFF_LEN];
+
+static const char *decode_tpgs_state(const int st)
+{
+    switch (st) {
+    case TPGS_STATE_OPTIMIZED:
+        return "active/optimized";
+        break;
+    case TPGS_STATE_NONOPTIMIZED:
+        return "active/non optimized";
+        break;
+    case TPGS_STATE_STANDBY:
+        return "standby";
+        break;
+    case TPGS_STATE_UNAVAILABLE:
+        return "unavailable";
+        break;
+    case TPGS_STATE_LB_DEPENDENT:
+        return "logical block dependent";
+        break;
+    case TPGS_STATE_OFFLINE:
+        return "offline";
+        break;
+    case TPGS_STATE_TRANSITIONING:
+        return "transitioning between states";
+        break;
+    default:
+        return "unknown";
+        break;
+    }
+}
+
+static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"lba", required_argument, 0, 'l'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"one-segment", no_argument, 0, 's'},
+        {"one_segment", no_argument, 0, 's'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_referrals  [--help] [--hex] [--lba=LBA] "
+            "[--maxlen=LEN]\n"
+            "                     [--one-segment] [--raw] [--readonly] "
+            "[--verbose]\n"
+            "                     [--version] DEVICE\n"
+            "  where:\n"
+            "    --help|-h         print out usage message\n"
+            "    --hex|-H          output in hexadecimal\n"
+            "    --lba=LBA|-l LBA    starting LBA (logical block address) "
+            "(def: 0)\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 0 -> %d bytes)\n",
+            DEF_REFER_BUFF_LEN );
+    pr2serr("    --one-segment|-s    return information about the specified "
+            "segment only\n"
+            "    --raw|-r          output in binary\n"
+            "    --verbose|-v      increase verbosity\n"
+            "    --version|-V      print version string and exit\n\n"
+            "Performs a SCSI REPORT REFERRALS command (SBC-3)\n"
+            );
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* Decodes given user data referral segment descriptor
+ * the number of blocks and returns the number of bytes processed,
+ * -1 for error.
+ */
+static int
+decode_referral_desc(const uint8_t * bp, int bytes)
+{
+    int j, n;
+    uint64_t first, last;
+
+    if (NULL == bp)
+        return -1;
+
+    if (bytes < 20)
+        return -1;
+
+    first = sg_get_unaligned_be64(bp + 4);
+    last = sg_get_unaligned_be64(bp + 12);
+
+    printf("    target port descriptors: %d\n", bp[3]);
+    printf("    user data segment: first lba %" PRIu64 ", last lba %"
+          PRIu64 "\n", first, last);
+    n = 20;
+    bytes -= n;
+    for (j = 0; j < bp[3]; j++) {
+        if (bytes < 4)
+            return -1;
+        printf("      target port descriptor %d:\n", j);
+        printf("        port group %x state (%s)\n",
+               sg_get_unaligned_be16(bp + n + 2),
+               decode_tpgs_state(bp[n] & 0xf));
+        n += 4;
+        bytes -= 4;
+    }
+    return n;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool do_one_segment = false;
+    bool o_readonly = false;
+    bool do_raw = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int k, res, c, rlen;
+    int sg_fd = -1;
+    int do_hex = 0;
+    int maxlen = DEF_REFER_BUFF_LEN;
+    int verbose = 0;
+    int desc = 0;
+    int ret = 0;
+    int64_t ll;
+    uint64_t lba = 0;
+    const char * device_name = NULL;
+    const uint8_t * bp;
+    uint8_t * referralBuffp = referralBuff;
+    uint8_t * free_referralBuffp = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "hHl:m:rRsvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'l':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            lba = (uint64_t)ll;
+            break;
+        case 'm':
+            maxlen = sg_get_num(optarg);
+            if ((maxlen < 0) || (maxlen > MAX_REFER_BUFF_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or less\n",
+                        MAX_REFER_BUFF_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 's':
+            do_one_segment = true;
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("No DEVICE argument given\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (maxlen > DEF_REFER_BUFF_LEN) {
+        referralBuffp = (uint8_t *)sg_memalign(maxlen, 0,
+                                               &free_referralBuffp,
+                                               verbose > 3);
+        if (NULL == referralBuffp) {
+            pr2serr("unable to allocate %d bytes on heap\n", maxlen);
+            return sg_convert_errno(ENOMEM);
+        }
+    }
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto free_buff;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto free_buff;
+    }
+
+    res = sg_ll_report_referrals(sg_fd, lba, do_one_segment, referralBuffp,
+                                 maxlen, true, verbose);
+    ret = res;
+    if (0 == res) {
+        if (maxlen >= 4)
+            /*
+             * This is strictly speaking incorrect. However, the
+             * spec reserved bytes 0 and 1, so some implementations
+             * might want to use them to increase the number of
+             * possible user segments.
+             * And maybe someone takes a pity and updates the spec ...
+             */
+            rlen = sg_get_unaligned_be32(referralBuffp + 0) + 4;
+        else
+            rlen = maxlen;
+        k = (rlen > maxlen) ? maxlen : rlen;
+        if (do_raw) {
+            dStrRaw(referralBuffp, k);
+            goto the_end;
+        }
+        if (do_hex) {
+            hex2stdout(referralBuffp, k, 1);
+            goto the_end;
+        }
+        if (maxlen < 4) {
+            if (verbose)
+                pr2serr("Exiting because allocation length (maxlen)  less "
+                        "than 4\n");
+            goto the_end;
+        }
+        if ((verbose > 1) || (verbose && (rlen > maxlen))) {
+            pr2serr("response length %d bytes\n", rlen);
+            if (rlen > maxlen)
+                pr2serr("  ... which is greater than maxlen (allocation "
+                        "length %d), truncation\n", maxlen);
+        }
+        if (rlen > maxlen)
+            rlen = maxlen;
+
+        bp = referralBuffp + 4;
+        k = 0;
+        printf("Report referrals:\n");
+        while (k < rlen - 4) {
+            printf("  descriptor %d:\n", desc);
+            res = decode_referral_desc(bp + k, rlen - 4 - k);
+            if (res < 0) {
+                pr2serr("bad user data segment referral descriptor\n");
+                break;
+            }
+            k += res;
+            desc++;
+        }
+    } else {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Report Referrals command failed: %s\n", b);
+    }
+
+the_end:
+    res = sg_cmds_close_device(sg_fd);
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+free_buff:
+    if (free_referralBuffp)
+        free(free_referralBuffp);
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_referrals failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_rem_rest_elem.c b/src/sg_rem_rest_elem.c
new file mode 100644
index 0000000..eae7979
--- /dev/null
+++ b/src/sg_rem_rest_elem.c
@@ -0,0 +1,331 @@
+/*
+ * Copyright (c) 2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ * This program issues one of the following SCSI commands:
+ *   - REMOVE ELEMENT AND TRUNCATE
+ *   - RESTORE ELEMENTS AND REBUILD
+ */
+
+static const char * version_str = "1.01 20221027";
+
+#define REMOVE_ELEM_SA 0x18
+#define RESTORE_ELEMS_SA 0x19
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+
+static struct option long_options[] = {
+        {"capacity", required_argument, 0, 'c'},
+        {"element", required_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"quick", no_argument, 0, 'q'},
+        {"remove", no_argument, 0, 'r'},
+        {"restore", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static const char * remove_cmd_s = "Remove element and truncate";
+static const char * restore_cmd_s = "Restore elements and rebuild";
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_rem_rest_elem  [--capacity=RC] [--element=EID] [--help] "
+            "[--quick]\n"
+            "                         [--remove] [--restore] [--verbose] "
+            "[--version]\n"
+            "                         DEVICE\n");
+    pr2serr("  where:\n"
+            "    --capacity=RC|-c RC    RC is requested capacity (unit: "
+            "block; def: 0)\n"
+            "    --element=EID|-e EID    EID is the element identifier to "
+            "remove;\n"
+            "                            default is 0 which is an invalid "
+            "EID\n"
+            "    --help|-h          print out usage message\n"
+            "    --quick|-q         bypass 15 second warn and wait\n"
+            "    --remove|-r        issue REMOVE ELEMENT AND TRUNCATE "
+            "command\n"
+            "    --restore|-R       issue RESTORE ELEMENTS AND REBUILD "
+            "command\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Performs a SCSI REMOVE ELEMENT AND TRUNCATE or RESTORE "
+            "ELEMENTS AND\nREBUILD command. Either the --remove or "
+            "--restore option needs to be given.\n");
+}
+
+/* Return of 0 -> success, various SG_LIB_CAT_* positive values or -1 ->
+ * other errors */
+static int
+sg_ll_rem_rest_elem(int sg_fd, int sa, uint64_t req_cap, uint32_t e_id,
+                    bool noisy, int verbose)
+{
+    int ret, res, sense_cat;
+    struct sg_pt_base * ptvp;
+    uint8_t sai16_cdb[16] =
+          {SG_SERVICE_ACTION_IN_16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+           0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    const char * cmd_name;
+
+    sai16_cdb[1] = 0x1f & sa;
+    if (REMOVE_ELEM_SA == sa) {
+        sg_put_unaligned_be64(req_cap, sai16_cdb + 2);
+        sg_put_unaligned_be32(e_id, sai16_cdb + 10);
+        cmd_name = remove_cmd_s;
+    } else
+        cmd_name = restore_cmd_s;
+    if (verbose) {
+        char d[128];
+
+        pr2serr("    %s cdb: %s\n", cmd_name,
+                sg_get_command_str(sai16_cdb, 16, false, sizeof(d), d));
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", cmd_name);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, sai16_cdb, sizeof(sai16_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cmd_name, res, noisy,
+                               verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool quick = false;
+    bool reat = false;
+    bool resar = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c;
+    int sg_fd = -1;
+    int verbose = 0;
+    int ret = 0;
+    int sa = 0;
+    uint32_t e_id = 0;
+    uint64_t req_cap = 0;
+    int64_t ll;
+    const char * device_name = NULL;
+    const char * cmd_name;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "c:e:hqrRvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("--capacity= expects a numeric argument\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            req_cap = (uint64_t)ll;
+            break;
+        case 'e':
+            ll = sg_get_llnum(optarg);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("bad argument to '--element=EID'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (0 == ll)
+                pr2serr("Warning: 0 is an invalid element identifier\n");
+            e_id = (uint64_t)ll;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'q':
+            quick = true;
+            break;
+        case 'r':
+            reat = true;
+            sa = REMOVE_ELEM_SA;
+            break;
+        case 'R':
+            resar = true;
+            sa = RESTORE_ELEMS_SA;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (1 != ((int)reat + (int)resar)) {
+        pr2serr("One, and only one, of these options needs to be given:\n"
+                "   --remove or --restore\n\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    cmd_name = reat ? remove_cmd_s : restore_cmd_s;
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        int err = -sg_fd;
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(err));
+        ret = sg_convert_errno(err);
+        goto fini;
+    }
+    if (! quick) {
+        int k;
+        char b[80] SG_C_CPP_ZERO_INIT;
+        char ch;
+
+        for (k = 0; k < (int)sizeof(b) - 1; ++k) {
+            ch = cmd_name[k];
+            if ('\0' == ch)
+                break;
+            else if (islower(ch))
+                b[k] = toupper(ch);
+            else
+                b[k] = ch;
+        }
+        sg_warn_and_wait(b, device_name, false);
+    }
+
+    res = sg_ll_rem_rest_elem(sg_fd, sa, req_cap, e_id, true, verbose);
+    ret = res;
+    if (res) {
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("%s command not supported\n", cmd_name);
+        else {
+            char b[80];
+
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("%s command: %s\n", cmd_name, b);
+        }
+    }
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_rem_rest_elem failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_rep_density.c b/src/sg_rep_density.c
new file mode 100644
index 0000000..afe6f29
--- /dev/null
+++ b/src/sg_rep_density.c
@@ -0,0 +1,478 @@
+/*
+ * Copyright (c) 2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI REPORT DENSITY SUPPORT command to the given
+ * SCSI (tape) device and outputs the response. Based on ssc5r06.pdf
+ */
+
+static const char * version_str = "1.00 20220120";
+
+#define MAX_RDS_BUFF_LEN (64 * 1024 - 1)
+#define DEF_RDS_BUFF_LEN 4096
+
+#define REPORT_DENSITY_SUPPORT_CMD 0x44
+#define REPORT_DENSITY_SUPPORT_CMDLEN 10
+
+#define RDS_DENSITY_DESC_LEN 52
+#define RDS_MEDIUM_T_DESC_LEN 56
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+static const char * rds_s = "Report density support";
+
+
+static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"in", required_argument, 0, 'i'},      /* silent, same as --inhex= */
+        {"inhex", required_argument, 0, 'i'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"media", no_argument, 0, 'M'}, /* Media field; byte 1, bit 0 */
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"typem", no_argument, 0, 't'}, /* Medium type field, byte 1, bit 1 */
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage(void)
+{
+    pr2serr("Usage: "
+            "sg_rep_density  [--help] [--hex] [--inhex=FN] [--maxlen=LEN] "
+            "[--media]\n"
+            "                   [--raw] [--readonly] [--typem] [--verbose] "
+            "[--version]\n"
+            "                   DEVICE\n");
+    pr2serr("  where:\n"
+            "    --help|-h          prints out this usage message\n"
+            "    --hex|-H           output response in hexadecimal "
+            "(default); used\n"
+            "                       twice: hex without addresses at start "
+            "of line\n"
+            "    --inhex=FN         decode contents of FN, ignore DEVICE\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 512 bytes)\n"
+            "    --media|-M         report on media in drive (def: report "
+            "on drive)\n"
+            "    --raw|-r           output response in binary\n"
+            "    --readonly|-R      open DEVICE read-only (def: read-write)\n"
+            "    --typem|-t         report medium types (def: density "
+            "codes)\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Sends a SCSI REPORT DENSITY SUPPORT command outputs the "
+            "response in\nASCII hexadecimal or binary. By default it reports "
+            "on density codes supported\nby the drive (LU).\n");
+}
+
+/* Invokes a SCSI REPORT PROVISIONING INITIALIZATION PATTERN command (SBC).
+ * Return of 0 -> success, various SG_LIB_CAT_* positive values or
+ * -1 -> other errors */
+static int
+sg_ll_report_density(int sg_fd, bool media, bool m_type, void * resp,
+                     int mx_resp_len, int * residp, bool noisy, int verbose)
+{
+    int ret, res, sense_cat;
+    uint8_t rds_cdb[REPORT_DENSITY_SUPPORT_CMDLEN] =
+          {REPORT_DENSITY_SUPPORT_CMD, 0, 0, 0,  0, 0, 0, 0,  0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (media)
+        rds_cdb[1] |= 0x1;
+    if (m_type)
+        rds_cdb[1] |= 0x2;
+    sg_put_unaligned_be16((uint16_t)mx_resp_len, rds_cdb + 7);
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", rds_s,
+                sg_get_command_str(rds_cdb, REPORT_DENSITY_SUPPORT_CMDLEN,
+                                   false, sizeof(b), b));
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", __func__);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, rds_cdb, sizeof(rds_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, rds_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    if (residp)
+        *residp = get_scsi_pt_resid(ptvp);
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+static void
+decode_medium_type(const uint8_t * up, int num_desc)
+{
+    int k, j, n, q;
+
+    for (k = 0; k < num_desc; ++k, up += RDS_MEDIUM_T_DESC_LEN) {
+        if (0 == k)
+            printf("Medium type descriptor%s\n", ((num_desc > 1) ? "s" : ""));
+        printf("  descriptor %d\n", k + 1);
+        printf("    Medium type: %u\n", up[0]);
+        n = up[4];
+        printf("    Number of density codes: %d\n", n);
+        if (n > 9)
+            n = 9;
+        for (j = 0; j < n; ++j) {
+            q = up[5 + j];
+            if (q > 0)
+                printf("      Primary density code: %d\n", q);
+        }
+        printf("    Media width: %u\n", sg_get_unaligned_be16(up + 14));
+        printf("    Medium length: %u\n", sg_get_unaligned_be16(up + 16));
+        printf("    Assigning organization: %.8s\n", (const char *)(up + 20));
+        printf("    Medium type name: %.8s\n", (const char *)(up + 28));
+        printf("    Description: %.20s\n", (const char *)(up + 36));
+    }
+}
+
+static void
+decode_density_code(const uint8_t * up, int num_desc)
+{
+    int k;
+
+    for (k = 0; k < num_desc; ++k, up += RDS_DENSITY_DESC_LEN) {
+        if (0 == k)
+            printf("Density support data block descriptor%s\n",
+                   ((num_desc > 1) ? "s" : ""));
+        printf("  descriptor %d\n", k + 1);
+        printf("    Primary density code: %u\n", up[0]);
+        printf("    Secondary density code: %u\n", up[1]);
+        printf("    WRT: %u\n", !!(0x80 & up[2]));
+        printf("    DUP: %u\n", !!(0x40 & up[2]));
+        printf("    DEFLT: %u\n", !!(0x20 & up[2]));
+        printf("    DLV: %u\n", !!(0x1 & up[2]));
+        printf("    Bits per mm: %u\n", sg_get_unaligned_be24(up + 5));
+        printf("    Media width: %u\n", sg_get_unaligned_be16(up + 8));
+        printf("    Tracks: %u\n", sg_get_unaligned_be16(up + 10));
+        printf("    Capacity: %u\n", sg_get_unaligned_be32(up + 12));
+        printf("    Assigning organization: %.8s\n", (const char *)(up + 16));
+        printf("    Density name: %.8s\n", (const char *)(up + 24));
+        printf("    Description: %.20s\n", (const char *)(up + 32));
+    }
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool do_raw = false;
+    bool media = false;
+    bool m_type = false;
+    bool no_final_msg = false;
+    bool o_readonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, rlen, desc_len, ads_len, num_desc;
+    int resid = 0;
+    int sg_fd = -1;
+    int do_help = 0;
+    int do_hex = 0;
+    int maxlen = 0;
+    int in_len = 0;
+    int ret = 0;
+    int verbose = 0;
+    const char * device_name = NULL;
+    const char * inhex_fn = NULL;
+    uint8_t * rdsBuff = NULL;
+    uint8_t * free_rds = NULL;
+    char b[80];
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "hHi:m:MrRtvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'h':
+        case '?':
+            ++do_help;
+            break;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'i':
+            inhex_fn = optarg;
+            break;
+        case 'm':
+            maxlen = sg_get_num(optarg);
+            if ((maxlen < 0) || (maxlen > MAX_RDS_BUFF_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or less\n",
+                        MAX_RDS_BUFF_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'M':
+            media = true;
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 't':
+            m_type = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (do_help) {
+        usage();
+        return 0;
+    }
+    if (device_name && inhex_fn) {
+        pr2serr("ignoring DEVICE, best to give DEVICE or --inhex=FN, but "
+                "not both\n");
+        device_name = NULL;
+    }
+    if (0 == maxlen)
+        maxlen = DEF_RDS_BUFF_LEN;
+    rdsBuff = (uint8_t *)sg_memalign(maxlen, 0, &free_rds, verbose > 3);
+    if (NULL == rdsBuff) {
+        pr2serr("unable to sg_memalign %d bytes\n", maxlen);
+        return sg_convert_errno(ENOMEM);
+    }
+    if (NULL == device_name) {
+        if (inhex_fn) {
+            if ((ret = sg_f2hex_arr(inhex_fn, do_raw, false, rdsBuff,
+                                    &in_len, maxlen))) {
+                if (SG_LIB_LBA_OUT_OF_RANGE == ret) {
+                    no_final_msg = true;
+                    pr2serr("... decode what we have, --maxlen=%d needs to "
+                            "be increased\n", maxlen);
+                } else
+                    goto the_end;
+            }
+            if (verbose > 2)
+                pr2serr("Read %d [0x%x] bytes of user supplied data\n",
+                        in_len, in_len);
+            if (do_raw)
+                do_raw = false;    /* otherwise interferes with decoding */
+            if (in_len < 4) {
+                pr2serr("--inhex=%s only decoded %d bytes (needs 4 at "
+                        "least)\n", inhex_fn, in_len);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto the_end;
+            }
+            res = 0;
+            maxlen = in_len;
+            goto start_response;
+        } else {
+            pr2serr("missing device name!\n\n");
+            usage();
+            ret = SG_LIB_FILE_ERROR;
+            no_final_msg = true;
+            goto the_end;
+        }
+    } else
+        in_len = 0;
+
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            no_final_msg = true;
+            goto the_end;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto the_end;
+    }
+
+    res = sg_ll_report_density(sg_fd, media, m_type, rdsBuff, maxlen, &resid,
+                               true, verbose);
+start_response:
+    ret = res;
+    if (0 == res) {
+        rlen = maxlen - resid;
+        if (rlen < 4) {
+            pr2serr("Response length (%d) too short\n", rlen);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto the_end;
+        }
+        if (do_raw) {
+            dStrRaw(rdsBuff, rlen);
+            goto the_end;
+        }
+        if (do_hex) {
+            if (2 != do_hex)
+                hex2stdout(rdsBuff, rlen, ((1 == do_hex) ? 1 : -1));
+            else
+                hex2stdout(rdsBuff, rlen, 0);
+            goto the_end;
+        }
+        desc_len = m_type ? RDS_MEDIUM_T_DESC_LEN : RDS_DENSITY_DESC_LEN;
+        ads_len = sg_get_unaligned_be16(rdsBuff + 0) + 2;
+        if (4 == ads_len)
+            goto the_end;
+        if (ads_len < 4) {
+            pr2serr("Badly formatted response, ads_len=%d\n", ads_len - 2);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto the_end;
+        }
+        if (ads_len > rlen) {
+            if (verbose)
+                pr2serr("Trimming response from %d to %d bytes\n", ads_len,
+                        rlen);
+            ads_len = rlen;
+            if (4 == ads_len)
+                goto the_end;
+        }
+        num_desc = (ads_len - 4) / desc_len;
+        if (0 != ((ads_len - 4) % desc_len)) {
+            if (verbose)
+                pr2serr("Truncating response to %d descriptors\n", num_desc);
+        }
+        if (m_type)
+            decode_medium_type(rdsBuff + 4, num_desc);
+        else
+            decode_density_code(rdsBuff + 4, num_desc);
+    } else if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("%s command not supported\n", rds_s);
+    else {
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("%s command: %s\n", rds_s, b);
+    }
+
+the_end:
+    if (free_rds)
+        free(free_rds);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if ((0 == verbose) && (! no_final_msg)) {
+        if (! sg_if_can2stderr("sg_rep_density failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_rep_pip.c b/src/sg_rep_pip.c
new file mode 100644
index 0000000..beadaa2
--- /dev/null
+++ b/src/sg_rep_pip.c
@@ -0,0 +1,335 @@
+/*
+ * Copyright (c) 2014-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI REPORT PROVISIONING INITIALIZATION PATTERN
+ * command to the given SCSI device and outputs the response. Based on
+ * sbc4r21.pdf
+ */
+
+static const char * version_str = "1.04 20220120";
+
+#define MAX_RPIP_BUFF_LEN (1024 * 1024)
+#define DEF_RPIP_BUFF_LEN 512
+
+#define SG_MAINT_IN_CMDLEN 12
+
+#define REPORT_PROVISIONING_INITIALIZATION_PATTERN_SA 0x1d
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+static const char * rpip_s = "Report provisioning initialization pattern";
+
+
+static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage(void)
+{
+    pr2serr("Usage: "
+            "sg_rep_pip  [--help] [--hex] [--maxlen=LEN] [--raw] "
+            "[--readonly]\n"
+            "                   [--verbose] [--version] DEVICE\n");
+    pr2serr("  where:\n"
+            "    --help|-h          prints out this usage message\n"
+            "    --hex|-H           output response in hexadecimal "
+            "(default); used\n"
+            "                       twice: hex without addresses at start "
+            "of line\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 512 bytes)\n"
+            "    --raw|-r           output response in binary\n"
+            "    --readonly|-R      open DEVICE read-only (def: read-write)\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Sends a SCSI REPORT PROVISIONING INITIALIZATION PATTERN "
+            "command and outputs\nthe response in ASCII hexadecimal or "
+            "binary.\n");
+}
+
+/* Invokes a SCSI REPORT PROVISIONING INITIALIZATION PATTERN command (SBC).
+ * Return of 0 -> success, various SG_LIB_CAT_* positive values or
+ * -1 -> other errors */
+static int
+sg_ll_report_pip(int sg_fd, void * resp, int mx_resp_len, int * residp,
+                 bool noisy, int verbose)
+{
+    int ret, res, sense_cat;
+    uint8_t rpip_cdb[SG_MAINT_IN_CMDLEN] =
+          {SG_MAINTENANCE_IN, REPORT_PROVISIONING_INITIALIZATION_PATTERN_SA,
+           0, 0,  0, 0, 0, 0,  0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rpip_cdb + 6);
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", rpip_s,
+                sg_get_command_str(rpip_cdb, SG_MAINT_IN_CMDLEN, false,
+                                   sizeof(b), b));
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", __func__);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, rpip_cdb, sizeof(rpip_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, rpip_s, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    if (residp)
+        *residp = get_scsi_pt_resid(ptvp);
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool do_raw = false;
+    bool o_readonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, resid, rlen;
+    int sg_fd = -1;
+    int do_help = 0;
+    int do_hex = 0;
+    int maxlen = 0;
+    int ret = 0;
+    int verbose = 0;
+    const char * device_name = NULL;
+    uint8_t * rpipBuff = NULL;
+    uint8_t * free_rpip = NULL;
+    char b[80];
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "hHm:rRvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'h':
+        case '?':
+            ++do_help;
+            break;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'm':
+            maxlen = sg_get_num(optarg);
+            if ((maxlen < 0) || (maxlen > MAX_RPIP_BUFF_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or less\n",
+                        MAX_RPIP_BUFF_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (do_help) {
+        usage();
+        return 0;
+    }
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto the_end;
+    }
+
+    if (0 == maxlen)
+        maxlen = DEF_RPIP_BUFF_LEN;
+    rpipBuff = (uint8_t *)sg_memalign(maxlen, 0, &free_rpip, verbose > 3);
+    if (NULL == rpipBuff) {
+        pr2serr("unable to sg_memalign %d bytes\n", maxlen);
+        return sg_convert_errno(ENOMEM);
+    }
+    res = sg_ll_report_pip(sg_fd, rpipBuff, maxlen, &resid, true, verbose);
+    ret = res;
+    if (0 == res) {
+        rlen = maxlen - resid;
+        if (rlen < 4) {
+            pr2serr("Response length (%d) too short\n", rlen);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto the_end;
+        }
+        if (do_raw) {
+            dStrRaw(rpipBuff, rlen);
+            goto the_end;
+        }
+        if (do_hex) {
+            if (2 != do_hex)
+                hex2stdout(rpipBuff, rlen, ((1 == do_hex) ? 1 : -1));
+            else
+                hex2stdout(rpipBuff, rlen, 0);
+            goto the_end;
+        } else {
+            printf("Printing response in hex:\n");
+            hex2stdout(rpipBuff, rlen, 1);
+            goto the_end;
+        }
+    } else if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("%s command not supported\n", rpip_s);
+    else {
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("%s command: %s\n", rpip_s, b);
+    }
+
+the_end:
+    if (free_rpip)
+        free(free_rpip);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_rep_pip failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_rep_zones.c b/src/sg_rep_zones.c
new file mode 100644
index 0000000..c0d19d3
--- /dev/null
+++ b/src/sg_rep_zones.c
@@ -0,0 +1,1525 @@
+/*
+ * Copyright (c) 2014-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <limits.h>
+#include <errno.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI REPORT ZONES, REPORT ZONE DOMAINS or REPORT
+ * REALMS command to the given SCSI device and decodes the response.
+ * Based on zbc2r12.pdf
+ */
+
+static const char * version_str = "1.42 20220807";
+
+#define MY_NAME "sg_rep_zones"
+
+#define WILD_RZONES_BUFF_LEN (1 << 28)
+#define MAX_RZONES_BUFF_LEN (2 * 1024 * 1024)
+#define DEF_RZONES_BUFF_LEN (1024 * 16)
+#define RCAP16_REPLY_LEN 32
+
+#define SG_ZONING_IN_CMDLEN 16
+#define REPORT_ZONES_DESC_LEN 64
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+/* Three zone service actions supported by this utility */
+enum zone_report_sa_e {
+    REPORT_ZONES_SA = 0x0,
+    REPORT_REALMS_SA = 0x6,
+    REPORT_ZONE_DOMAINS_SA = 0x7
+};
+
+struct opts_t {
+    bool do_brief;
+    bool do_force;
+    bool do_partial;
+    bool do_raw;
+    bool do_realms;
+    bool do_zdomains;
+    bool maxlen_given;
+    bool o_readonly;
+    bool statistics;
+    bool verbose_given;
+    bool version_given;
+    bool wp_only;
+    enum zone_report_sa_e serv_act;
+    int do_help;
+    int do_hex;
+    int do_num;
+    int find_zt;        /* negative values: find first not equal to */
+    int maxlen;
+    int reporting_opt;
+    int vb;
+    uint64_t st_lba;
+    const char * in_fn;
+    sgj_state json_st;
+};
+
+struct zt_num2abbrev_t {
+    int ztn;
+    const char * abbrev;
+};
+
+static struct option long_options[] = {
+        {"brief", no_argument, 0, 'b'}, /* only header and last descriptor */
+        {"domain", no_argument, 0, 'd'},
+        {"domains", no_argument, 0, 'd'},
+        {"force", no_argument, 0, 'f'},
+        {"find", required_argument, 0, 'F'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"in", required_argument, 0, 'i'},      /* silent, same as --inhex= */
+        {"inhex", required_argument, 0, 'i'},
+        {"json", optional_argument, 0, 'j'},
+        {"locator", required_argument, 0, 'l'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"num", required_argument, 0, 'n'},
+        {"partial", no_argument, 0, 'p'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"realm", no_argument, 0, 'e'},
+        {"realms", no_argument, 0, 'e'},
+        {"report", required_argument, 0, 'o'},
+        {"start", required_argument, 0, 's'},
+        {"statistics", no_argument, 0, 'S'},
+        {"stats", no_argument, 0, 'S'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"wp", no_argument, 0, 'w'},
+        {0, 0, 0, 0},
+};
+
+/* Zone types */
+static struct zt_num2abbrev_t zt_num2abbrev[] = {
+    {0, "none"},
+    {1, "c"},           /* conventionial */
+    {2, "swr"},         /* sequential write required */
+    {3, "swp"},         /* sequential write preferred */
+    {4, "sobr"},        /* sequential or before required */
+    {5, "g"},           /* gap */
+    {-1, NULL},         /* sentinel */
+};
+
+static const char * zn_dnum_s = "zone descriptor number: ";
+
+static const char * meaning_s = "meaning";
+
+
+static void
+prn_zone_type_abbrevs(void)
+{
+    const struct zt_num2abbrev_t * n2ap = zt_num2abbrev;
+    char b[32];
+
+    pr2serr("Zone type number\tAbbreviation\tName\n");
+    pr2serr("----------------\t------------\t----\n");
+    for ( ; n2ap->abbrev; ++n2ap) {
+        if (n2ap == zt_num2abbrev)
+            pr2serr("\t%d\t\t%s\t\t[reserved]\n",
+                    n2ap->ztn, n2ap->abbrev);
+        else
+            pr2serr("\t%d\t\t%s\t\t%s\n", n2ap->ztn, n2ap->abbrev,
+                    sg_get_zone_type_str(n2ap->ztn, sizeof(b), b));
+    }
+}
+
+static void
+usage(int h)
+{
+    if (h > 1) goto h_twoormore;
+    pr2serr("Usage: "
+            "sg_rep_zones  [--domain] [--find=ZT] [--force] [--help] "
+            "[--hex]\n"
+            "                     [--inhex=FN] [--json[=JO]] "
+            "[--locator=LBA]\n"
+            "                     [--maxlen=LEN] [--num=NUM] [--partial] "
+            "[--raw]\n"
+            "                     [--readonly] [--realm] [--report=OPT] "
+            "[--start=LBA]\n"
+            "                     [--statistics] [--verbose] [--version] "
+            "[--wp]\n"
+            "                     DEVICE\n");
+    pr2serr("  where:\n"
+            "    --domain|-d        sends a REPORT ZONE DOMAINS command\n"
+            "    --find=ZT|-F ZT    find first zone with ZT zone type, "
+            "starting at LBA\n"
+            "                       if first character of ZT is - or !, "
+            "find first\n"
+            "                       zone that is not ZT\n"
+            "    --force|-f         bypass some sanity checks when decoding "
+            "response\n"
+            "    --help|-h          print out usage message, use twice for "
+            "more help\n"
+            "    --hex|-H           output response in hexadecimal; used "
+            "twice\n"
+            "                       shows decoded values in hex\n"
+            "    --inhex=FN|-i FN    decode contents of FN, ignore DEVICE\n"
+            "    --json[=JO]|-j[JO]    output in JSON instead of human "
+            "readable text.\n"
+            "                          Use --json=? for JSON help\n"
+            "    --locator=LBA|-l LBA    similar to --start= option\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 0 -> 8192 bytes)\n"
+            "    --num=NUM|-n NUM    number of zones to output (def: 0 -> "
+            "all)\n"
+            "    --partial|-p       sets PARTIAL bit in cdb (def: 0 -> "
+            "zone list\n"
+            "                       length not altered by allocation length "
+            "in cdb)\n"
+            "    --raw|-r           output response in binary\n"
+            "    --readonly|-R      open DEVICE read-only (def: read-write)\n"
+            "    --realm|-e         sends a REPORT REALMS command\n"
+            "    --report=OPT|-o OP    reporting options (def: 0: all "
+            "zones)\n"
+            "    --start=LBA|-s LBA    report zones from the LBA (def: 0)\n"
+            "                          need not be a zone starting LBA\n"
+            "    --statistics|-S    gather statistics by reviewing zones\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n"
+            "    --wp|-w            output write pointer only\n\n"
+            "Sends a SCSI REPORT ZONES, REPORT ZONE DOMAINS or REPORT REALMS "
+            "command.\nBy default sends a REPORT ZONES command. Give help "
+            "option twice\n(e.g. '-hh') to see reporting options "
+            "enumerated.\n");
+    return;
+h_twoormore:
+    pr2serr("Reporting options for REPORT ZONES:\n"
+            "    0x0    list all zones\n"
+            "    0x1    list zones with a zone condition of EMPTY\n"
+            "    0x2    list zones with a zone condition of IMPLICITLY "
+            "OPENED\n"
+            "    0x3    list zones with a zone condition of EXPLICITLY "
+            "OPENED\n"
+            "    0x4    list zones with a zone condition of CLOSED\n"
+            "    0x5    list zones with a zone condition of FULL\n"
+            "    0x6    list zones with a zone condition of READ ONLY\n"
+            "    0x7    list zones with a zone condition of OFFLINE\n"
+            "    0x8    list zones with a zone condition of INACTIVE\n"
+            "    0x10   list zones with RWP Recommended set to true\n"
+            "    0x11   list zones with Non-sequential write resources "
+            "active set to true\n"
+            "    0x3e   list zones except those with zone type: GAP\n"
+            "    0x3f   list zones with a zone condition of NOT WRITE "
+            "POINTER\n\n");
+    pr2serr("Reporting options for REPORT ZONE DOMAINS:\n"
+            "    0x0    list all zone domains\n"
+            "    0x1    list all zone domains in which all zones are active\n"
+            "    0x2    list all zone domains that contain active zones\n"
+            "    0x3    list all zone domains that do not contain any active "
+            "zones\n\n");
+    pr2serr("Reporting options for REPORT REALMS:\n"
+            "    0x0    list all realms\n"
+            "    0x1    list all realms that contain active Sequential Or "
+            "Before Required zones\n"
+            "    0x2    list all realms that contain active Sequential Write "
+            "Required zones\n"
+            "    0x3    list all realms that contain active Sequential Write "
+            "Preferred zones\n");
+    pr2serr("\n");
+    prn_zone_type_abbrevs();
+}
+
+/* Invokes a SCSI REPORT ZONES, REPORT ZONE DOMAINS or REPORT REALMS command
+ * (see ZBC and ZBC-2).  Return of 0 -> success, various SG_LIB_CAT_* positive
+ * values or -1 -> other errors */
+static int
+sg_ll_report_zzz(int sg_fd, enum zone_report_sa_e serv_act, uint64_t zs_lba,
+                 bool partial, int report_opts, void * resp, int mx_resp_len,
+                 int * residp, bool noisy, int vb)
+{
+    int ret, res, sense_cat;
+    uint8_t rz_cdb[SG_ZONING_IN_CMDLEN] =
+          {SG_ZONING_IN, REPORT_ZONES_SA, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0,
+           0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    rz_cdb[1] = (uint8_t)serv_act;
+    sg_put_unaligned_be64(zs_lba, rz_cdb + 2);
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rz_cdb + 10);
+    rz_cdb[14] = report_opts & 0x3f;
+    if (partial)
+        rz_cdb[14] |= 0x80;
+    if (vb) {
+        char b[128];
+
+        pr2serr("    %s\n", sg_get_command_str(rz_cdb, SG_ZONING_IN_CMDLEN,
+                                               true, sizeof(b), b));
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", __func__);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, rz_cdb, sizeof(rz_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, vb);
+    ret = sg_cmds_process_resp(ptvp, "report zone/domain/realm", res, noisy,
+                               vb, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    if (residp)
+        *residp = get_scsi_pt_resid(ptvp);
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+static const char *
+zone_condition_str(int zc, char * b, int blen, int vb)
+{
+    const char * cp;
+
+    if (NULL == b)
+        return "zone_condition_str: NULL ptr)";
+    switch (zc) {
+    case 0:
+        cp = "Not write pointer";
+        break;
+    case 1:
+        cp = "Empty";
+        break;
+    case 2:
+        cp = "Implicitly opened";
+        break;
+    case 3:
+        cp = "Explicitly opened";
+        break;
+    case 4:
+        cp = "Closed";
+        break;
+    case 5:
+        cp = "Inactive";
+        break;
+    case 0xd:
+        cp = "Read only";
+        break;
+    case 0xe:
+        cp = "Full";
+        break;
+    case 0xf:
+        cp = "Offline";
+        break;
+    default:
+        cp = NULL;
+        break;
+    }
+    if (cp) {
+        if (vb)
+            snprintf(b, blen, "%s [0x%x]", cp, zc);
+        else
+            snprintf(b, blen, "%s", cp);
+    } else
+        snprintf(b, blen, "Reserved [0x%x]", zc);
+    return b;
+}
+
+static const char * same_desc_arr[16] = {
+    "zone type and length may differ in each descriptor",
+    "zone type and length same in each descriptor",
+    "zone type and length same apart from length in last descriptor",
+    "zone type for each descriptor may be different",
+    "Reserved [0x4]", "Reserved [0x5]", "Reserved [0x6]", "Reserved [0x7]",
+    "Reserved [0x8]", "Reserved [0x9]", "Reserved [0xa]", "Reserved [0xb]",
+    "Reserved [0xc]", "Reserved [0xd]", "Reserved [0xe]", "Reserved [0xf]",
+};
+
+static uint64_t
+prt_a_zn_desc(const uint8_t *bp, const struct opts_t * op,
+              sgj_state * jsp, sgj_opaque_p jop)
+{
+    uint8_t zt, zc;
+    uint64_t lba, len, wp;
+    char b[80];
+
+    jop = jop ? jop : jsp->basep;
+    zt = bp[0] & 0xf;
+    zc = (bp[1] >> 4) & 0xf;
+    sg_get_zone_type_str(zt, sizeof(b), b);
+    sgj_pr_hr(jsp, "   Zone type: %s\n", b);
+    sgj_js_nv_istr(jsp, jop, "zone_type", zt, meaning_s, b);
+    zone_condition_str(zc, b, sizeof(b), op->vb);
+    sgj_pr_hr(jsp, "   Zone condition: %s\n", b);
+    sgj_js_nv_istr(jsp, jop, "zone_condition", zc, meaning_s, b);
+    sgj_haj_vi(jsp, jop, 3, "PUEP", SGJ_SEP_COLON_1_SPACE,
+               !!(bp[1] & 0x4), false);
+    sgj_haj_vi(jsp, jop, 3, "NON_SEQ", SGJ_SEP_COLON_1_SPACE,
+               !!(bp[1] & 0x2), false);
+    sgj_haj_vi(jsp, jop, 3, "RESET", SGJ_SEP_COLON_1_SPACE,
+               !!(bp[1] & 0x1), false);
+    len = sg_get_unaligned_be64(bp + 8);
+    sgj_pr_hr(jsp, "   Zone Length: 0x%" PRIx64 "\n", len);
+    sgj_js_nv_ihex(jsp, jop, "zone_length", (int64_t)len);
+    lba = sg_get_unaligned_be64(bp + 16);
+    sgj_pr_hr(jsp, "   Zone start LBA: 0x%" PRIx64 "\n", lba);
+    sgj_js_nv_ihex(jsp, jop, "zone_start_lba", (int64_t)lba);
+    wp = sg_get_unaligned_be64(bp + 24);
+    if (sg_all_ffs((const uint8_t *)&wp, sizeof(wp)))
+        sgj_pr_hr(jsp, "   Write pointer LBA: -1\n");
+    else
+        sgj_pr_hr(jsp, "   Write pointer LBA: 0x%" PRIx64 "\n", wp);
+    sgj_js_nv_ihex(jsp, jop, "write_pointer_lba", (int64_t)wp);
+    return lba + len;
+}
+
+static int
+decode_rep_zones(const uint8_t * rzBuff, int act_len, uint32_t decod_len,
+                 const struct opts_t * op, sgj_state * jsp)
+{
+    bool as_json = jsp ? jsp->pr_as_json : false;
+    int k, same, num_zd;
+    uint64_t wp, ul, mx_lba;
+    sgj_opaque_p jop = jsp ? jsp->basep : NULL;
+    sgj_opaque_p jap = NULL;
+    const uint8_t * bp;
+
+    if ((uint32_t)act_len < decod_len) {
+        num_zd = (act_len >= 64) ? ((act_len - 64) / REPORT_ZONES_DESC_LEN)
+                                 : 0;
+        if (act_len == op->maxlen) {
+            if (op->maxlen_given)
+                pr2serr("decode length [%u bytes] may be constrained by "
+                        "given --maxlen value, try increasing\n", decod_len);
+            else
+                pr2serr("perhaps --maxlen=%u needs to be used\n", decod_len);
+        } else if (op->in_fn)
+            pr2serr("perhaps %s has been truncated\n", op->in_fn);
+    } else
+        num_zd = (decod_len - 64) / REPORT_ZONES_DESC_LEN;
+    same = rzBuff[4] & 0xf;
+    mx_lba = sg_get_unaligned_be64(rzBuff + 8);
+    if (op->wp_only) {
+        ;
+    } else if (op->do_hex) {
+        hex2stdout(rzBuff, 64, -1);
+        printf("\n");
+    } else {
+        uint64_t rzslbag = sg_get_unaligned_be64(rzBuff + 16);
+        static const char * rzslbag_s = "Reported zone starting LBA "
+                                        "granularity";
+
+        sgj_pr_hr(jsp, "  Same=%d: %s\n", same, same_desc_arr[same]);
+        sgj_js_nv_istr(jsp, jop, "same", same, meaning_s,
+                       same_desc_arr[same]);
+        sgj_pr_hr(jsp, "  Maximum LBA: 0x%" PRIx64 "\n\n", mx_lba);
+        sgj_js_nv_ihex(jsp, jop, "maximum_lba", mx_lba);
+        sgj_pr_hr(jsp, "  %s: 0x%" PRIx64 "\n\n", rzslbag_s, rzslbag);
+        sgj_js_nv_ihex(jsp, jop, rzslbag_s, rzslbag);
+    }
+    if (op->do_num > 0)
+            num_zd = (num_zd > op->do_num) ? op->do_num : num_zd;
+    if (((uint32_t)act_len < decod_len) &&
+        ((num_zd * REPORT_ZONES_DESC_LEN) + 64 > act_len)) {
+        pr2serr("Skip due to truncated response, try using --num= to a "
+                "value less than %d\n", num_zd);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    if (op->do_brief && (num_zd > 0)) {
+        bp = rzBuff + 64 + ((num_zd - 1) * REPORT_ZONES_DESC_LEN);
+        if (op->do_hex) {
+            if (op->wp_only)
+                hex2stdout(bp + 24, 8, -1);
+            else
+                hex2stdout(bp, 64, -1);
+            return 0;
+        }
+        sgj_pr_hr(jsp, "From last descriptor in this response:\n");
+        sgj_pr_hr(jsp, " %s%d\n", zn_dnum_s, num_zd - 1);
+        sgj_js_nv_i(jsp, jop, "zone_descriptor_index", num_zd - 1);
+        ul = prt_a_zn_desc(bp, op, jsp, jop);
+        if (ul > mx_lba)
+            sgj_pr_hr(jsp, "   >> This zone seems to be the last one\n");
+        else
+            sgj_pr_hr(jsp, "   >> Probable next Zone start LBA: 0x%" PRIx64
+                      "\n", ul);
+        return 0;
+    }
+    if (as_json)
+        jap = sgj_named_subarray_r(jsp, NULL, "zone_descriptors_list");
+    for (k = 0, bp = rzBuff + 64; k < num_zd;
+         ++k, bp += REPORT_ZONES_DESC_LEN) {
+        sgj_opaque_p jo2p;
+
+        if (! op->wp_only)
+             sgj_pr_hr(jsp, " %s%d\n", zn_dnum_s, k);
+        if (op->do_hex) {
+            hex2stdout(bp, 64, -1);
+            continue;
+        }
+        if (op->wp_only) {
+            if (op->do_hex)
+                hex2stdout(bp + 24, 8, -1);
+            else {
+                wp = sg_get_unaligned_be64(bp + 24);
+                if (sg_all_ffs((const uint8_t *)&wp, sizeof(wp)))
+                    sgj_pr_hr(jsp, "-1\n");
+                else
+                    sgj_pr_hr(jsp, "0x%" PRIx64 "\n", wp);
+                jo2p = sgj_new_unattached_object_r(jsp);
+                sgj_js_nv_ihex(jsp, jo2p, "write_pointer_lba", (int64_t)wp);
+                sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            }
+            continue;
+        }
+        jo2p = sgj_new_unattached_object_r(jsp);
+        prt_a_zn_desc(bp, op, jsp, jo2p);
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+    if ((op->do_num == 0) && (! op->wp_only) && (! op->do_hex)) {
+        if ((64 + (REPORT_ZONES_DESC_LEN * (uint32_t)num_zd)) < decod_len)
+            sgj_pr_hr(jsp, "\n>>> Beware: Zone list truncated, may need "
+                      "another call\n");
+    }
+    return 0;
+}
+
+static int
+decode_rep_realms(const uint8_t * rzBuff, int act_len,
+                  const struct opts_t * op, sgj_state * jsp)
+{
+    uint32_t k, realms_count, derived_realms_count, r_desc_len,
+             zdomains_count;
+    uint64_t nr_locator;
+    const uint8_t * bp;
+    sgj_opaque_p jop = jsp ? jsp->basep : NULL;
+    sgj_opaque_p jap = NULL;
+    sgj_opaque_p ja2p = NULL;
+
+    if (act_len < 12) {
+        pr2serr("need more than 12 bytes to decode, got %u\n", act_len);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    realms_count = sg_get_unaligned_be32(rzBuff + 4);
+    r_desc_len = sg_get_unaligned_be32(rzBuff + 8);
+    if (act_len < 20)
+        nr_locator = sg_get_unaligned_be64(rzBuff + 12);
+    else
+        nr_locator = 0;
+    sgj_haj_vi(jsp, jop, 0, "Realms_count", SGJ_SEP_EQUAL_NO_SPACE,
+               realms_count, true);
+    sgj_haj_vi(jsp, jop, 0, "Realms_descriptor_length",
+               SGJ_SEP_EQUAL_NO_SPACE, r_desc_len, true);
+    sgj_pr_hr(jsp, "Next_realm_locator=0x%" PRIx64 "\n", nr_locator);
+    sgj_js_nv_ihex(jsp, jop, "Next_realm_locator", nr_locator);
+    if ((realms_count < 1) || (act_len < (64 + 16)) || (r_desc_len < 16)) {
+        if (op->vb) {
+            pr2serr("%s: exiting early because ", __func__);
+            if (realms_count < 1)
+                pr2serr("realms_count is zero\n");
+            else if (r_desc_len < 16)
+                pr2serr("realms descriptor length less than 16\n");
+            else
+                pr2serr("actual_length (%u) too short\n", act_len);
+        }
+        return 0;
+    }
+    derived_realms_count = (act_len - 64) / r_desc_len;
+    if (derived_realms_count > realms_count) {
+        if (op->vb)
+            pr2serr("%s: derived_realms_count [%u] > realms_count [%u]\n",
+                    __func__, derived_realms_count, realms_count);
+    } else if (derived_realms_count < realms_count) {
+        if (op->vb)
+            pr2serr("%s: derived_realms_count [%u] < realms_count [%u], "
+                    "use former\n", __func__, derived_realms_count,
+                    realms_count);
+        realms_count = derived_realms_count;
+    }
+    zdomains_count = (r_desc_len - 16) / 16;
+
+    if (op->do_num > 0)
+            realms_count = (realms_count > (uint32_t)op->do_num) ?
+                                (uint32_t)op->do_num : realms_count;
+    jap = sgj_named_subarray_r(jsp, jop, "realm_descriptors_list");
+
+    for (k = 0, bp = rzBuff + 64; k < realms_count; ++k, bp += r_desc_len) {
+        uint32_t j;
+        uint16_t restrictions;
+        const uint8_t * zp;
+        sgj_opaque_p jo2p;
+
+        jo2p = sgj_new_unattached_object_r(jsp);
+        sgj_haj_vi(jsp, jo2p, 1, "Realms_id", SGJ_SEP_EQUAL_NO_SPACE,
+                   sg_get_unaligned_be32(bp + 0), true);
+        if (op->do_hex) {
+            hex2stdout(bp, r_desc_len, -1);
+            continue;
+        }
+        restrictions = sg_get_unaligned_be16(bp + 4);
+        sgj_pr_hr(jsp, "   realm_restrictions=0x%hu\n", restrictions);
+        sgj_js_nv_ihex(jsp, jo2p, "realm_restrictions", restrictions);
+        sgj_haj_vi(jsp, jo2p, 3, "active_zone_domain_id",
+                   SGJ_SEP_EQUAL_NO_SPACE, bp[7], true);
+
+        ja2p = sgj_named_subarray_r(jsp, jo2p,
+                                    "realm_start_end_descriptors_list");
+        for (j = 0, zp = bp + 16; j < zdomains_count; ++j, zp += 16) {
+            uint64_t lba;
+            sgj_opaque_p jo3p;
+
+            jo3p = sgj_new_unattached_object_r(jsp);
+            sgj_pr_hr(jsp, "   zone_domain=%u\n", j);
+            sgj_js_nv_i(jsp, jo3p, "corresponding_zone_domain_id", j);
+            lba = sg_get_unaligned_be64(zp + 0);
+            sgj_pr_hr(jsp, "     starting_lba=0x%" PRIx64 "\n", lba);
+            sgj_js_nv_ihex(jsp, jo3p, "realm_starting_lba", (int64_t)lba);
+            lba = sg_get_unaligned_be64(zp + 8);
+            sgj_pr_hr(jsp, "     ending_lba=0x%" PRIx64 "\n", lba);
+            sgj_js_nv_ihex(jsp, jo3p, "realm_ending_lba", (int64_t)lba);
+            sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+        }
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+    return 0;
+}
+
+static int
+decode_rep_zdomains(const uint8_t * rzBuff, int act_len,
+                   const struct opts_t * op, sgj_state * jsp)
+{
+    uint32_t k, zd_len, zd_ret_len, zdoms_sup, zdoms_rep, zd_rep_opts;
+    uint32_t num, der_zdoms;
+    uint64_t zd_locator;
+    sgj_opaque_p jop = jsp ? jsp->basep : NULL;
+    sgj_opaque_p jap = NULL;
+    const uint8_t * bp;
+
+    if (act_len < 12) {
+        pr2serr("need more than 12 bytes to decode, got %u\n", act_len);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    zd_len = sg_get_unaligned_be32(rzBuff + 0);
+    zd_ret_len = sg_get_unaligned_be32(rzBuff + 4);
+    zdoms_sup = rzBuff[8];
+    zdoms_rep = rzBuff[9];
+    zd_rep_opts = rzBuff[10];
+    if (act_len < 24)
+        zd_locator = sg_get_unaligned_be64(rzBuff + 16);
+    else
+        zd_locator = 0;
+    sgj_haj_vi(jsp, jop, 0, "Zone_domains_returned_list_length=",
+               SGJ_SEP_EQUAL_NO_SPACE, zd_ret_len, true);
+    sgj_haj_vi(jsp, jop, 0, "Zone_domains_supported",
+               SGJ_SEP_EQUAL_NO_SPACE, zdoms_sup, true);
+    sgj_haj_vi(jsp, jop, 0, "Zone_domains_reported",
+               SGJ_SEP_EQUAL_NO_SPACE, zdoms_rep, true);
+    sgj_pr_hr(jsp, "Reporting_options=0x%x\n", zd_rep_opts);
+    sgj_js_nv_ihex(jsp, jop, "Reporting_options", zd_rep_opts);
+    sgj_pr_hr(jsp, "Zone_domain_locator=0x%" PRIx64 "\n", zd_locator);
+    sgj_js_nv_ihex(jsp, jop, "Zone_domain_locator", zd_locator);
+
+    der_zdoms = zd_len / 96;
+    if (op->vb > 1)
+        pr2serr("Derived zdomains=%u\n", der_zdoms);
+    num = ((der_zdoms < zdoms_rep) ? der_zdoms : zdoms_rep) * 96;
+    jap = sgj_named_subarray_r(jsp, jop, "zone_domain_descriptors_list");
+
+    for (k = 0, bp = rzBuff + 64; k < num; k += 96, bp += 96) {
+        uint64_t lba;
+        sgj_opaque_p jo2p;
+
+        jo2p = sgj_new_unattached_object_r(jsp);
+        sgj_haj_vi(jsp, jo2p, 3, "zone_domain",
+                   SGJ_SEP_EQUAL_NO_SPACE, bp[0], true);
+        lba = sg_get_unaligned_be64(bp + 16);
+        sgj_pr_hr(jsp, "     zone_count=%" PRIu64 "\n", lba);
+        sgj_js_nv_ihex(jsp, jo2p, "zone_count", lba);
+        lba = sg_get_unaligned_be64(bp + 24);
+        sgj_pr_hr(jsp, "     starting_lba=0x%" PRIx64 "\n", lba);
+        sgj_js_nv_ihex(jsp, jo2p, "starting_lba", lba);
+        lba = sg_get_unaligned_be64(bp + 32);
+        sgj_pr_hr(jsp, "     ending_lba=0x%" PRIx64 "\n", lba);
+        sgj_js_nv_ihex(jsp, jo2p, "ending_lba", lba);
+        sgj_pr_hr(jsp, "     zone_domain_zone_type=0x%x\n", bp[40]);
+        sgj_js_nv_ihex(jsp, jo2p, "zone_domain_zone_type", bp[40]);
+        sgj_haj_vi(jsp, jo2p, 5, "VZDZT", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(0x2 & bp[42]), false);
+        sgj_haj_vi(jsp, jo2p, 5, "SRB", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(0x1 & bp[42]), false);
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+    return 0;
+}
+
+static int
+find_report_zones(int sg_fd, uint8_t * rzBuff, const char * cmd_name,
+                  struct opts_t * op, sgj_state * jsp)
+{
+    bool as_json = (jsp && (0 == op->do_hex)) ?  jsp->pr_as_json : false;
+    bool found = false;
+    uint8_t zt;
+    int k, res, resid, rlen, num_zd, num_rem;
+    uint32_t zn_dnum = 0;
+    uint64_t slba = op->st_lba;
+    uint64_t mx_lba = 0;
+    const uint8_t * bp = rzBuff;
+    char b[96];
+
+    num_rem = op->do_num ? op->do_num : INT_MAX;
+    for ( ; num_rem > 0; num_rem -= num_zd) {
+        resid = 0;
+        if (sg_fd >= 0) {
+            res = sg_ll_report_zzz(sg_fd, REPORT_ZONES_SA, slba,
+                                   true /* set partial */, op->reporting_opt,
+                                   rzBuff, op->maxlen, &resid, true, op->vb);
+            if (res) {
+                if (SG_LIB_CAT_INVALID_OP == res)
+                    pr2serr("%s: %s%u, %s command not supported\n", __func__,
+                            zn_dnum_s, zn_dnum, cmd_name);
+                else {
+                    sg_get_category_sense_str(res, sizeof(b), b, op->vb);
+                    pr2serr("%s: %s%u, %s command: %s\n", __func__,
+                            zn_dnum_s, zn_dnum, cmd_name, b);
+                }
+                break;
+            }
+        } else
+            res = 0;
+        rlen = op->maxlen - resid;
+        if (rlen <= 64)
+            break;
+        mx_lba = sg_get_unaligned_be64(rzBuff + 8);
+        num_zd = (rlen - 64) / REPORT_ZONES_DESC_LEN;
+        if (num_zd > num_rem)
+            num_zd = num_rem;
+        for (k = 0, bp = rzBuff + 64; k < num_zd;
+             ++k, bp += REPORT_ZONES_DESC_LEN, ++zn_dnum) {
+            zt = 0xf & bp[0];
+            if (op->find_zt > 0) {
+                if ((uint8_t)op->find_zt == zt )
+                    break;
+            } else if (op->find_zt < 0) {
+                if ((uint8_t)(-op->find_zt) != zt )
+                    break;
+            }
+            slba = sg_get_unaligned_be64(bp + 16) +
+                   sg_get_unaligned_be64(bp + 8);
+        }
+        if (k < num_zd) {
+            found = true;
+            break;
+        } else if ((slba > mx_lba) || (sg_fd < 0))
+            break;
+    }           /* end of outer for loop */
+    if (res == 0) {
+        sgj_opaque_p jo2p = as_json ?
+                sgj_named_subobject_r(jsp, NULL, "find_condition") : NULL;
+
+        if (found) {
+            if (op->do_hex) {
+                hex2stdout(rzBuff, 64, -1);
+                printf("\n");
+                hex2stdout(bp, 64, -1);
+            } else {
+                sgj_pr_hr(jsp, "Condition met at:\n");
+                sgj_pr_hr(jsp, " %s: %d\n", zn_dnum_s, zn_dnum);
+                sgj_js_nv_b(jsp, jo2p, "met", true);
+                sgj_js_nv_i(jsp, jo2p, "zone_descriptor_index", zn_dnum);
+                prt_a_zn_desc(bp, op, jsp, jo2p);
+            }
+        } else {
+            if (op->do_hex) {
+                memset(b, 0xff, 64);
+                hex2stdout((const uint8_t *)b, 64, -1);
+            } else {
+                sgj_js_nv_b(jsp, jo2p, "met", false);
+                sgj_js_nv_i(jsp, jo2p, "zone_descriptor_index", zn_dnum);
+                if (num_rem < 1)
+                    sgj_pr_hr(jsp, "Condition NOT met, checked %d zones; "
+                              "next %s%u\n", op->do_num, zn_dnum_s, zn_dnum);
+                else
+                    sgj_pr_hr(jsp, "Condition NOT met; next %s%u\n",
+                              zn_dnum_s, zn_dnum);
+            }
+        }
+    }
+    return res;
+}
+
+struct statistics_t {
+    uint32_t zt_conv_num;
+    uint32_t zt_swr_num;
+    uint32_t zt_swp_num;
+    uint32_t zt_sob_num;
+    uint32_t zt_gap_num;
+    uint32_t zt_unk_num;
+
+    uint32_t zc_nwp_num;
+    uint32_t zc_mt_num;
+    uint32_t zc_iop_num;
+    uint32_t zc_eop_num;
+    uint32_t zc_cl_num;
+    uint32_t zc_ina_num;
+    uint32_t zc_ro_num;
+    uint32_t zc_full_num;
+    uint32_t zc_off_num;
+    uint32_t zc_unk_num;
+
+    /* The following LBAs have 1 added to them, initialized to 0 */
+    uint64_t zt_swr_1st_lba1;
+    uint64_t zt_swp_1st_lba1;
+    uint64_t zt_sob_1st_lba1;
+    uint64_t zt_gap_1st_lba1;
+
+    uint64_t zc_nwp_1st_lba1;
+    uint64_t zc_mt_1st_lba1;
+    uint64_t zc_iop_1st_lba1;
+    uint64_t zc_eop_1st_lba1;
+    uint64_t zc_cl_1st_lba1;
+    uint64_t zc_ina_1st_lba1;
+    uint64_t zc_ro_1st_lba1;
+    uint64_t zc_full_1st_lba1;
+    uint64_t zc_off_1st_lba1;
+
+    uint64_t wp_max_lba1;       /* ... that isn't Zone start LBA */
+    uint64_t wp_blk_num;        /* sum of (zwp - zs_lba) */
+    uint64_t conv_blk_num;      /* sum of (z_blks) of zt=conv */
+};
+
+static int
+gather_statistics(int sg_fd, uint8_t * rzBuff, const char * cmd_name,
+                  struct opts_t * op)
+{
+    uint8_t zt, zc;
+    int k, res, resid, rlen, num_zd, num_rem;
+    uint32_t zn_dnum = 0;
+    uint64_t slba = op->st_lba;
+    uint64_t mx_lba = 0;
+    uint64_t zs_lba, zwp, z_blks;
+    const uint8_t * bp = rzBuff;
+    struct statistics_t st SG_C_CPP_ZERO_INIT;
+    char b[96];
+
+    if (op->serv_act != REPORT_ZONES_SA) {
+        pr2serr("%s: do not support statistics for %s yet\n", __func__,
+                cmd_name);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    num_rem = op->do_num ? op->do_num : INT_MAX;
+    for ( ; num_rem > 0; num_rem -= num_zd) {
+        resid = 0;
+        zs_lba = slba;
+        if (sg_fd >= 0) {
+            res = sg_ll_report_zzz(sg_fd, REPORT_ZONES_SA, slba,
+                                   true /* set partial */, op->reporting_opt,
+                                   rzBuff, op->maxlen, &resid, true, op->vb);
+            if (res) {
+                if (SG_LIB_CAT_INVALID_OP == res)
+                    pr2serr("%s: %s%u, %s command not supported\n", __func__,
+                            zn_dnum_s, zn_dnum, cmd_name);
+                else {
+                    sg_get_category_sense_str(res, sizeof(b), b, op->vb);
+                    pr2serr("%s: %s%u, %s command: %s\n", __func__,
+                            zn_dnum_s, zn_dnum, cmd_name, b);
+                }
+                break;
+            }
+        } else
+            res = 0;
+        rlen = op->maxlen - resid;
+        if (rlen <= 64) {
+            break;
+        }
+        mx_lba = sg_get_unaligned_be64(rzBuff + 8);
+        num_zd = (rlen - 64) / REPORT_ZONES_DESC_LEN;
+        if (num_zd > num_rem)
+            num_zd = num_rem;
+        for (k = 0, bp = rzBuff + 64; k < num_zd;
+             ++k, bp += REPORT_ZONES_DESC_LEN, ++zn_dnum) {
+            z_blks = sg_get_unaligned_be64(bp + 8);
+            zs_lba = sg_get_unaligned_be64(bp + 16);
+            zwp = sg_get_unaligned_be64(bp + 24);
+            zt = 0xf & bp[0];
+            switch (zt) {
+            case 1:     /* conventional */
+                ++st.zt_conv_num;
+                st.conv_blk_num += z_blks;
+                break;
+            case 2:     /* sequential write required */
+                ++st.zt_swr_num;
+                if (0 == st.zt_swr_1st_lba1)
+                    st.zt_swr_1st_lba1 = zs_lba + 1;
+                break;
+            case 3:     /* sequential write preferred */
+                ++st.zt_swp_num;
+                if (0 == st.zt_swp_1st_lba1)
+                    st.zt_swp_1st_lba1 = zs_lba + 1;
+                break;
+            case 4:     /* sequential or before (write) */
+                ++st.zt_sob_num;
+                if (0 == st.zt_sob_1st_lba1)
+                    st.zt_sob_1st_lba1 = zs_lba + 1;
+                break;
+            case 5:     /* gap */
+                ++st.zt_gap_num;
+                if (0 == st.zt_gap_1st_lba1)
+                    st.zt_gap_1st_lba1 = zs_lba + 1;
+                break;
+            default:
+                ++st.zt_unk_num;
+                break;
+            }
+            zc = (bp[1] >> 4) & 0xf;
+            switch (zc) {
+            case 0:     /* not write pointer (zone) */
+                ++st.zc_nwp_num;
+                if (0 == st.zc_nwp_1st_lba1)
+                    st.zc_nwp_1st_lba1 = zs_lba + 1;
+                break;
+            case 1:     /* empty */
+                ++st.zc_mt_num;
+                if (0 == st.zc_mt_1st_lba1)
+                    st.zc_mt_1st_lba1 = zs_lba + 1;
+                break;
+            case 2:     /* implicitly opened */
+                ++st.zc_iop_num;
+                if (0 == st.zc_iop_1st_lba1)
+                    st.zc_iop_1st_lba1 = zs_lba + 1;
+                if (zwp > zs_lba) {
+                    st.wp_max_lba1 = zwp + 1;
+                    st.wp_blk_num += zwp - zs_lba;
+                }
+                break;
+            case 3:     /* explicitly opened */
+                ++st.zc_eop_num;
+                if (0 == st.zc_eop_1st_lba1)
+                    st.zc_eop_1st_lba1 = zs_lba + 1;
+                if (zwp > zs_lba) {
+                    st.wp_max_lba1 = zwp + 1;
+                    st.wp_blk_num += zwp - zs_lba;
+                }
+                break;
+            case 4:     /* closed */
+                ++st.zc_cl_num;
+                if (0 == st.zc_cl_1st_lba1)
+                    st.zc_cl_1st_lba1 = zs_lba + 1;
+                if (zwp > zs_lba) {
+                    st.wp_max_lba1 = zwp + 1;
+                    st.wp_blk_num += zwp - zs_lba;
+                }
+                break;
+            case 5:     /* inactive */
+                ++st.zc_ina_num;
+                if (0 == st.zc_ina_1st_lba1)
+                    st.zc_ina_1st_lba1 = zs_lba + 1;
+                break;
+            case 0xd:   /* read-only */
+                ++st.zc_ro_num;
+                if (0 == st.zc_ro_1st_lba1)
+                    st.zc_ro_1st_lba1 = zs_lba + 1;
+                break;
+            case 0xe:   /* full */
+                ++st.zc_full_num;
+                if (0 == st.zc_full_1st_lba1)
+                    st.zc_full_1st_lba1 = zs_lba + 1;
+                st.wp_blk_num += z_blks;
+                break;
+            case 0xf:   /* offline */
+                ++st.zc_off_num;
+                if (0 == st.zc_off_1st_lba1)
+                    st.zc_off_1st_lba1 = zs_lba + 1;
+                break;
+            default:
+                ++st.zc_unk_num;
+                break;
+            }
+            slba = zs_lba + z_blks;
+        }       /* end of inner for loop */
+        if ((slba > mx_lba) || (sg_fd < 0))
+            break;
+    }           /* end of outer for loop */
+    printf("Number of conventional type zones: %u\n", st.zt_conv_num);
+    if (st.zt_swr_num > 0)
+        printf("Number of sequential write required type zones: %u\n",
+               st.zt_swr_num);
+    if (st.zt_swr_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zt_swr_1st_lba1 - 1);
+    if (st.zt_swp_num > 0)
+        printf("Number of sequential write preferred type zones: %u\n",
+               st.zt_swp_num);
+    if (st.zt_swp_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zt_swp_1st_lba1 - 1);
+    if (st.zt_sob_num > 0)
+        printf("Number of sequential or before type zones: %u\n",
+               st.zt_sob_num);
+    if (st.zt_sob_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zt_sob_1st_lba1 - 1);
+    if (st.zt_gap_num > 0)
+        printf("Number of gap type zones: %u\n", st.zt_gap_num);
+    if (st.zt_gap_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zt_gap_1st_lba1 - 1);
+    if (st.zt_unk_num > 0)
+        printf("Number of unknown type zones: %u\n", st.zt_unk_num);
+
+    printf("Number of 'not write pointer' condition zones: %u\n",
+           st.zc_nwp_num);
+    if (st.zc_nwp_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_nwp_1st_lba1 - 1);
+    printf("Number of empty condition zones: %u\n", st.zc_mt_num);
+    if (st.zc_mt_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_mt_1st_lba1 - 1);
+    if (st.zc_iop_num > 0)
+        printf("Number of implicitly open condition zones: %u\n",
+               st.zc_iop_num);
+    if (st.zc_iop_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_iop_1st_lba1 - 1);
+    if (st.zc_eop_num)
+        printf("Number of explicitly open condition zones: %u\n",
+               st.zc_eop_num);
+    if (st.zc_eop_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_eop_1st_lba1 - 1);
+    if (st.zc_cl_num)
+        printf("Number of closed condition zones: %u\n", st.zc_cl_num);
+    if (st.zc_cl_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_cl_1st_lba1 - 1);
+    if (st.zc_ina_num)
+        printf("Number of inactive condition zones: %u\n", st.zc_ina_num);
+    if (st.zc_ina_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_ina_1st_lba1 - 1);
+    if (st.zc_ro_num)
+        printf("Number of inactive condition zones: %u\n", st.zc_ro_num);
+    if (st.zc_ro_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_ro_1st_lba1 - 1);
+    if (st.zc_full_num)
+        printf("Number of full condition zones: %u\n", st.zc_full_num);
+    if (st.zc_full_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_full_1st_lba1 - 1);
+    if (st.zc_off_num)
+        printf("Number of offline condition zones: %u\n", st.zc_off_num);
+    if (st.zc_off_1st_lba1 > 0)
+        printf("    Lowest starting LBA: 0x%" PRIx64 "\n",
+              st.zc_off_1st_lba1 - 1);
+    if (st.zc_unk_num > 0)
+        printf("Number of unknown condition zones: %u\n", st.zc_unk_num);
+
+    if (st.wp_max_lba1 > 0)
+        printf("Highest active write pointer LBA: 0x%" PRIx64 "\n",
+              st.wp_max_lba1 - 1);
+    printf("Number of used blocks in write pointer zones: 0x%" PRIx64 "\n",
+           st.wp_blk_num);
+
+    if ((sg_fd >= 0) && (op->maxlen >= RCAP16_REPLY_LEN) &&
+        ((st.wp_blk_num > 0) || (st.conv_blk_num > 0))) {
+        uint32_t block_size = 0;
+        uint64_t total_sz;
+        double sz_mb, sz_gb;
+
+        res = sg_ll_readcap_16(sg_fd, false, 0, rzBuff,
+                               RCAP16_REPLY_LEN, true, op->vb);
+        if (SG_LIB_CAT_INVALID_OP == res) {
+            pr2serr("READ CAPACITY (16) cdb not supported\n");
+        } else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+            pr2serr("bad field in READ CAPACITY (16) cdb including "
+                    "unsupported service action\n");
+        else if (res) {
+            sg_get_category_sense_str(res, sizeof(b), b, op->vb);
+            pr2serr("READ CAPACITY (16) failed: %s\n", b);
+        } else
+            block_size = sg_get_unaligned_be32(rzBuff + 8);
+
+        if (st.wp_blk_num) {
+            total_sz = st.wp_blk_num * block_size;
+            sz_mb = (double)(total_sz) / (double)(1048576);
+            sz_gb = (double)(total_sz) / (double)(1000000000L);
+#ifdef SG_LIB_MINGW
+            printf("   associated size: %" PRIu64 " bytes, %g MiB, %g GB",
+                   total_sz, sz_mb, sz_gb);
+#else
+            printf("   associated size: %" PRIu64 " bytes, %.1f MiB, %.2f "
+                   "GB", total_sz, sz_mb, sz_gb);
+#endif
+            if (sz_gb > 2000) {
+#ifdef SG_LIB_MINGW
+                printf(", %g TB", sz_gb / 1000);
+#else
+                printf(", %.2f TB", sz_gb / 1000);
+#endif
+            }
+            printf("\n");
+        }
+        if (st.conv_blk_num) {
+            total_sz = st.conv_blk_num * block_size;
+            sz_mb = (double)(total_sz) / (double)(1048576);
+            sz_gb = (double)(total_sz) / (double)(1000000000L);
+            printf("Size of all conventional zones: ");
+#ifdef SG_LIB_MINGW
+            printf("%" PRIu64 " bytes, %g MiB, %g GB", total_sz, sz_mb,
+                   sz_gb);
+#else
+            printf("%" PRIu64 " bytes, %.1f MiB, %.2f GB", total_sz,
+                   sz_mb, sz_gb);
+#endif
+            if (sz_gb > 2000) {
+#ifdef SG_LIB_MINGW
+                printf(", %g TB", sz_gb / 1000);
+#else
+                printf(", %.2f TB", sz_gb / 1000);
+#endif
+            }
+            printf("\n");
+        }
+    }
+    return res;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool no_final_msg = false;
+    bool as_json;
+    int res, c, act_len, rlen, in_len, off;
+    int sg_fd = -1;
+    int resid = 0;
+    int ret = 0;
+    uint32_t decod_len;
+    int64_t ll;
+    const char * device_name = NULL;
+    uint8_t * rzBuff = NULL;
+    uint8_t * free_rzbp = NULL;
+    const char * cmd_name = "Report zones";
+    sgj_state * jsp;
+    sgj_opaque_p jop = NULL;
+    char b[80];
+    struct opts_t opts SG_C_CPP_ZERO_INIT;
+    struct opts_t * op = &opts;
+
+    op->serv_act = REPORT_ZONES_SA;
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "bdefF:hHi:j::l:m:n:o:prRs:SvVw",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            op->do_brief = true;
+            break;
+        case 'd':
+            op->do_zdomains = true;
+            op->serv_act = REPORT_ZONE_DOMAINS_SA;
+            break;
+        case 'e':
+            op->do_realms = true;
+            op->serv_act = REPORT_REALMS_SA;
+            break;
+        case 'f':
+            op->do_force = true;
+            break;
+        case 'F':
+            off = (('-' == *optarg) || ('!' == *optarg)) ? 1 : 0;
+            if (isdigit(*(optarg + off))) {
+                op->find_zt = sg_get_num_nomult(optarg + off);
+                if (op->find_zt < 0) {
+                    pr2serr("bad numeric argument to '--find='\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                if (off)
+                    op->find_zt = -op->find_zt; /* find first not equal */
+            } else {    /* check for abbreviation */
+                struct zt_num2abbrev_t * zn2ap = zt_num2abbrev;
+
+                for ( ; zn2ap->abbrev; ++zn2ap) {
+                    if (0 == strcmp(optarg + off, zn2ap->abbrev))
+                        break;
+                }
+                if (NULL == zn2ap->abbrev) {
+                    pr2serr("bad abbreviation argument to '--find='\n\n");
+                    prn_zone_type_abbrevs();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->find_zt = off ? -zn2ap->ztn : zn2ap->ztn;
+            }
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'H':
+            ++op->do_hex;
+            break;
+       case 'i':
+            op->in_fn = optarg;
+            break;
+       case 'j':
+            if (! sgj_init_state(&op->json_st, optarg)) {
+                int bad_char = op->json_st.first_bad_char;
+                char e[1500];
+
+                if (bad_char) {
+                    pr2serr("bad argument to --json= option, unrecognized "
+                            "character '%c'\n\n", bad_char);
+                }
+                sg_json_usage(0, e, sizeof(e));
+                pr2serr("%s", e);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        /* case 'l': is under case 's': */
+        case 'm':
+            op->maxlen = sg_get_num(optarg);
+            if ((op->maxlen < 0) || (op->maxlen > MAX_RZONES_BUFF_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or "
+                        "less\n", MAX_RZONES_BUFF_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->maxlen_given = true;
+            break;
+        case 'n':
+            op->do_num = sg_get_num(optarg);
+            if (op->do_num < 0) {
+                pr2serr("argument to '--num' should be zero or more\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'o':
+           op->reporting_opt = sg_get_num_nomult(optarg);
+           if ((op->reporting_opt < 0) || (op->reporting_opt > 63)) {
+                pr2serr("bad argument to '--report=OPT', expect 0 to "
+                        "63\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'p':
+            op->do_partial = true;
+            break;
+        case 'r':
+            op->do_raw = true;
+            break;
+        case 'R':
+            op->o_readonly = true;
+            break;
+        case 's':
+        case 'l':       /* --locator= and --start= are interchangeable */
+        if ((2 == strlen(optarg)) && (0 == memcmp("-1", optarg, 2))) {
+                op->st_lba = UINT64_MAX;
+                break;
+            }
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--start=LBA' or '--locator=LBA\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->st_lba = (uint64_t)ll;
+            break;
+        case 'S':
+            op->statistics = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->vb;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'w':
+            op->wp_only = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->vb = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->vb = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->vb);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (op->do_help) {
+        usage(op->do_help);
+        return 0;
+    }
+    as_json = op->json_st.pr_as_json;
+    jsp = &op->json_st;
+    if (as_json)
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+
+    if (op->do_zdomains && op->do_realms) {
+        pr2serr("Can't have both --domain and --realm\n");
+        return SG_LIB_SYNTAX_ERROR;
+    } else if (op->do_zdomains)
+        cmd_name = "Report zone domains";
+    else if (op->do_realms)
+        cmd_name = "Report realms";
+    if (as_json)
+        sgj_js_nv_s(jsp, jop, "scsi_command_name", cmd_name);
+    if ((op->serv_act != REPORT_ZONES_SA) && op->do_partial) {
+        pr2serr("Can only use --partial with REPORT ZONES\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (device_name && op->in_fn) {
+        pr2serr("ignoring DEVICE, best to give DEVICE or --inhex=FN, but "
+                "not both\n");
+        device_name = NULL;
+    }
+    if (0 == op->maxlen)
+        op->maxlen = DEF_RZONES_BUFF_LEN;
+    rzBuff = (uint8_t *)sg_memalign(op->maxlen, 0, &free_rzbp, op->vb > 3);
+    if (NULL == rzBuff) {
+        pr2serr("unable to sg_memalign %d bytes\n", op->maxlen);
+        return sg_convert_errno(ENOMEM);
+    }
+
+    if (NULL == device_name) {
+        if (op->in_fn) {
+            if ((ret = sg_f2hex_arr(op->in_fn, op->do_raw, false, rzBuff,
+                                    &in_len, op->maxlen))) {
+                if (SG_LIB_LBA_OUT_OF_RANGE == ret) {
+                    no_final_msg = true;
+                    pr2serr("... decode what we have, --maxlen=%d needs to "
+                            "be increased\n", op->maxlen);
+                } else
+                    goto the_end;
+            }
+            if (op->vb > 2)
+                pr2serr("Read %d [0x%x] bytes of user supplied data\n",
+                        in_len, in_len);
+            if (op->do_raw)
+                op->do_raw = false;    /* can interfere on decode */
+            if (in_len < 4) {
+                pr2serr("--inhex=%s only decoded %d bytes (needs 4 at "
+                        "least)\n", op->in_fn, in_len);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto the_end;
+            }
+            res = 0;
+            if (op->find_zt) {  /* so '-F none' will drop through */
+                op->maxlen = in_len;
+                ret = find_report_zones(sg_fd, rzBuff, cmd_name, op, jsp);
+                goto the_end;
+            } else if (op->statistics) {
+                op->maxlen = in_len;
+                ret = gather_statistics(sg_fd, rzBuff, cmd_name, op);
+                goto the_end;
+            }
+            goto start_response;
+        } else {
+            pr2serr("missing device name!\n\n");
+            usage(1);
+            ret = SG_LIB_FILE_ERROR;
+            no_final_msg = true;
+            goto the_end;
+        }
+    }
+
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto the_end;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, op->o_readonly, op->vb);
+    if (sg_fd < 0) {
+        if (op->vb)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto the_end;
+    }
+
+    if (op->find_zt) {  /* so '-F none' will drop through */
+        ret = find_report_zones(sg_fd, rzBuff, cmd_name, op, jsp);
+        goto the_end;
+    } else if (op->statistics) {
+        ret = gather_statistics(sg_fd, rzBuff, cmd_name, op);
+        goto the_end;
+    }
+    res = sg_ll_report_zzz(sg_fd, op->serv_act, op->st_lba, op->do_partial,
+                           op->reporting_opt, rzBuff, op->maxlen, &resid,
+                           true, op->vb);
+    ret = res;
+start_response:
+    if (0 == res) {
+        rlen = op->in_fn ? in_len : (op->maxlen - resid);
+        if (rlen < 4) {
+            pr2serr("Decoded response length (%d) too short\n", rlen);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto the_end;
+        }
+        decod_len = sg_get_unaligned_be32(rzBuff + 0) + 64;
+        if (decod_len > WILD_RZONES_BUFF_LEN) {
+            if (! op->do_force) {
+                pr2serr("decode length [%u bytes] seems wild, use --force "
+                        "override\n", decod_len);
+                ret = SG_LIB_CAT_MALFORMED;
+                goto the_end;
+            }
+        }
+        if (decod_len > (uint32_t)rlen) {
+            if ((REPORT_ZONES_SA == op->serv_act) && (! op->do_partial)) {
+                pr2serr("%u zones starting from LBA 0x%" PRIx64 " available "
+                        "but only %d zones returned\n",
+                        (decod_len - 64) / REPORT_ZONES_DESC_LEN, op->st_lba,
+                        (rlen - 64) / REPORT_ZONES_DESC_LEN);
+                decod_len = rlen;
+                act_len = rlen;
+            } else {
+                pr2serr("decoded response length is %u bytes, but system "
+                        "reports %d bytes received??\n", decod_len, rlen);
+                if (op->do_force)
+                    act_len = rlen;
+                else {
+                    pr2serr("Exiting, use --force to override\n");
+                    ret = SG_LIB_CAT_MALFORMED;
+                    goto the_end;
+                }
+            }
+        } else
+            act_len = decod_len;
+        if (op->do_raw) {
+            dStrRaw(rzBuff, act_len);
+            goto the_end;
+        }
+        if (op->do_hex && (2 != op->do_hex)) {
+            hex2stdout(rzBuff, act_len, ((1 == op->do_hex) ? 1 : -1));
+            goto the_end;
+        }
+        if (! op->wp_only && (! op->do_hex))
+            sgj_pr_hr(jsp, "%s response:\n", cmd_name);
+
+        if (act_len < 64) {
+            pr2serr("Zone length [%d] too short (perhaps after truncation\n)",
+                    act_len);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto the_end;
+        }
+        if (REPORT_ZONES_SA == op->serv_act)
+            ret = decode_rep_zones(rzBuff, act_len, decod_len, op, jsp);
+        else if (op->do_realms)
+            ret = decode_rep_realms(rzBuff, act_len, op, jsp);
+        else if (op->do_zdomains)
+            ret = decode_rep_zdomains(rzBuff, act_len, op, jsp);
+    } else if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("%s command not supported\n", cmd_name);
+    else {
+        sg_get_category_sense_str(res, sizeof(b), b, op->vb);
+        pr2serr("%s command: %s\n", cmd_name, b);
+    }
+
+the_end:
+    if (free_rzbp)
+        free(free_rzbp);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if ((0 == op->vb && (! no_final_msg))) {
+        if (! sg_if_can2stderr("sg_rep_zones failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    ret = (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+    if (as_json) {
+        if (0 == op->do_hex)
+            sgj_js2file(jsp, NULL, ret, stdout);
+        sgj_finish(jsp);
+    }
+    return ret;
+}
diff --git a/src/sg_requests.c b/src/sg_requests.c
new file mode 100644
index 0000000..2779cbd
--- /dev/null
+++ b/src/sg_requests.c
@@ -0,0 +1,543 @@
+/*
+ * Copyright (c) 2004-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/time.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pr2serr.h"
+#include "sg_pt.h"
+
+/* A utility program for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI command REQUEST SENSE to the given SCSI device.
+ */
+
+static const char * version_str = "1.40 20220607";
+
+#define MAX_REQS_RESP_LEN 255
+#define DEF_REQS_RESP_LEN 252
+
+#define SENSE_BUFF_LEN 96       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT 60       /* 60 seconds */
+
+#define REQUEST_SENSE_CMD 0x3
+#define REQUEST_SENSE_CMDLEN 6
+
+#define ME "sg_requests: "
+
+
+static struct option long_options[] = {
+        {"desc", no_argument, 0, 'd'},
+        {"error", no_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"num", required_argument, 0, 'n'},
+        {"number", required_argument, 0, 'n'},
+        {"progress", no_argument, 0, 'p'},
+        {"raw", no_argument, 0, 'r'},
+        {"status", no_argument, 0, 's'},
+        {"time", no_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_requests [--desc] [--error] [--help] [--hex] "
+            "[--maxlen=LEN]\n"
+            "                   [--num=NUM] [--number=NUM] [--progress] "
+            "[--raw]\n"
+            "                   [--status] [--time] [--verbose] "
+            "[--version] DEVICE\n"
+            "  where:\n"
+            "    --desc|-d         set flag for descriptor sense "
+            "format\n"
+            "    --error|-e        change opcode to 0xff; to measure "
+            "overhead\n"
+            "                      twice: skip ioctl call\n"
+            "    --help|-h         print out usage message\n"
+            "    --hex|-H          output in hexadecimal\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 0 -> 252 bytes)\n"
+            "    --num=NUM|-n NUM  number of REQUEST SENSE commands "
+            "to send (def: 1)\n"
+            "    --number=NUM      same action as '--num=NUM'\n"
+            "    --progress|-p     output a progress indication (percentage) "
+            "if available\n"
+            "    --raw|-r          output in binary (to stdout)\n"
+            "    --status|-s       set exit status from parameter data "
+            "(def: only set\n"
+            "                       exit status from autosense)\n"
+            "    --time|-t         time the transfer, calculate commands "
+            "per second\n"
+            "    --verbose|-v      increase verbosity\n"
+            "    --version|-V      print version string and exit\n\n"
+            "Performs a SCSI REQUEST SENSE command\n"
+            );
+
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+int
+main(int argc, char * argv[])
+{
+    int c, n, k, progress, rs, sense_cat, act_din_len;
+    int do_error = 0;
+    int err = 0;
+    int num_errs = 0;
+    int num_din_errs = 0;
+    int most_recent_skey = 0;
+    int sg_fd = -1;
+    int res = 0;
+    uint8_t rsBuff[MAX_REQS_RESP_LEN + 1];
+    bool desc = false;
+    bool do_progress = false;
+    bool do_raw = false;
+    bool do_status = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    bool not_raw_hex;
+    int num_rs = 1;
+    int do_hex = 0;
+    int maxlen = 0;
+    int verbose = 0;
+    const char * device_name = NULL;
+    int ret = 0;
+    struct sg_pt_base * ptvp = NULL;
+    char b[256];
+    uint8_t rs_cdb[REQUEST_SENSE_CMDLEN] =
+        {REQUEST_SENSE_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+#ifndef SG_LIB_MINGW
+    bool do_time = false;
+    struct timeval start_tm, end_tm;
+#endif
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "dehHm:n:prstvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'd':
+            desc = true;
+            break;
+        case 'e':
+            ++do_error;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'm':
+            maxlen = sg_get_num(optarg);
+            if ((maxlen < 0) || (maxlen > MAX_REQS_RESP_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or less\n",
+                        MAX_REQS_RESP_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'n':
+           num_rs = sg_get_num(optarg);
+           if (num_rs < 1) {
+                pr2serr("bad argument to '--num'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'p':
+            do_progress = true;
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 's':
+            do_status = true;
+            break;
+        case 't':
+#ifndef SG_LIB_MINGW
+            do_time = true;
+#endif
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (0 == maxlen)
+        maxlen = DEF_REQS_RESP_LEN;
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+    if (do_raw || do_hex) {
+        not_raw_hex = false;
+#ifdef SG_LIB_MINGW
+        bool prog_time = do_progress;
+#else
+        bool prog_time = do_progress || do_time;
+#endif
+
+        if (prog_time) {
+            pr2serr("With either --raw or --hex, --progress and --time "
+                    "contradict\n");
+            ret = SG_LIB_CONTRADICT;
+            goto finish;
+        }
+    } else
+        not_raw_hex = true;
+
+    sg_fd = sg_cmds_open_device(device_name, true /* ro */, verbose);
+    if (sg_fd < 0) {
+        if (not_raw_hex && verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto finish;
+    }
+    ptvp = construct_scsi_pt_obj_with_fd(sg_fd, verbose);
+    if ((NULL == ptvp) || ((err = get_scsi_pt_os_err(ptvp)))) {
+        if (not_raw_hex)
+            pr2serr("%s: unable to construct pt object\n", __func__);
+        ret = sg_convert_errno(err ? err : ENOMEM);
+        goto finish;
+    }
+    if (do_error)
+        rs_cdb[0] = 0xff;
+    if (desc)
+        rs_cdb[1] |= 0x1;
+    rs_cdb[4] = maxlen;
+    if (do_progress) {
+        for (k = 0; k < num_rs; ++k) {
+            act_din_len = 0;
+            if (k > 0)
+                sg_sleep_secs(30);
+            set_scsi_pt_cdb(ptvp, rs_cdb, sizeof(rs_cdb));
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+            memset(rsBuff, 0x0, sizeof(rsBuff));
+            set_scsi_pt_data_in(ptvp, rsBuff, sizeof(rsBuff));
+            set_scsi_pt_packet_id(ptvp, k + 1);
+            if (do_error > 1) {
+                ++num_errs;
+                n = 0;
+            } else {
+                if (verbose && (0 == k)) {
+                    char bb[128];
+
+                    pr2serr("    cdb: %s\n",
+                            sg_get_command_str(rs_cdb, REQUEST_SENSE_CMDLEN,
+                                               true, sizeof(bb), bb));
+                }
+                rs = do_scsi_pt(ptvp, -1, DEF_PT_TIMEOUT, verbose);
+                n = sg_cmds_process_resp(ptvp, "Request sense", rs, (0 == k),
+                                         verbose, &sense_cat);
+            }
+            if (-1 == n) {
+                if (get_scsi_pt_transport_err(ptvp))
+                    ret = SG_LIB_TRANSPORT_ERROR;
+                else
+                    ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+                goto finish;
+            } else if (-2 == n) {
+                switch (sense_cat) {
+                case SG_LIB_CAT_RECOVERED:
+                case SG_LIB_CAT_NO_SENSE:
+                    break;
+                case SG_LIB_CAT_NOT_READY:
+                    ++num_errs;
+                    if (1 ==  num_rs) {
+                        ret = sense_cat;
+                        printf("device not ready\n");
+                    }
+                    break;
+                case SG_LIB_CAT_UNIT_ATTENTION:
+                    ++num_errs;
+                    if (verbose) {
+                        pr2serr("Ignoring Unit attention (sense key)\n");
+                    }
+                    break;
+                default:
+                    ++num_errs;
+                    if (1 == num_rs) {
+                        ret = sense_cat;
+                        sg_get_category_sense_str(sense_cat, sizeof(b), b,
+                                                  verbose);
+                        printf("%s\n", b);
+                        break; // return k;
+                    }
+                    break;
+                }
+            }
+            if (n >= 0)
+                act_din_len = n;
+            if (ret)
+                goto finish;
+
+            if (verbose > 1) {
+                pr2serr("Parameter data in hex\n");
+                hex2stderr(rsBuff, act_din_len, 1);
+            }
+            progress = -1;
+            sg_get_sense_progress_fld(rsBuff, act_din_len, &progress);
+            if (progress < 0) {
+                ret = res;
+                if (verbose > 1)
+                     pr2serr("No progress indication found, iteration %d\n",
+                             k + 1);
+                /* N.B. exits first time there isn't a progress indication */
+                break;
+            } else
+                printf("Progress indication: %d.%02d%% done\n",
+                       (progress * 100) / 65536,
+                       ((progress * 100) % 65536) / 656);
+            partial_clear_scsi_pt_obj(ptvp);
+        }                               /* >>>>> end of for(num_rs) loop */
+        goto finish;
+    }
+
+#ifndef SG_LIB_MINGW
+    if (not_raw_hex && do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+    }
+#endif
+
+    rsBuff[0] = '\0';
+    rsBuff[7] = '\0';
+    for (k = 0; k < num_rs; ++k) {
+        act_din_len = 0;
+        ret = 0;
+        set_scsi_pt_cdb(ptvp, rs_cdb, sizeof(rs_cdb));
+        set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+        memset(rsBuff, 0x0, sizeof(rsBuff));
+        set_scsi_pt_data_in(ptvp, rsBuff, sizeof(rsBuff));
+        set_scsi_pt_packet_id(ptvp, k + 1);
+        if (do_error > 1) {
+            ++num_errs;
+            n = 0;
+        } else {
+            if (verbose && (0 == k)) {
+                char bb[128];
+
+                pr2serr("    cdb: %s\n",
+                        sg_get_command_str(rs_cdb, REQUEST_SENSE_CMDLEN,
+                                           true, sizeof(bb), bb));
+            }
+            rs = do_scsi_pt(ptvp, -1, DEF_PT_TIMEOUT, verbose);
+            n = sg_cmds_process_resp(ptvp, "Request sense", rs, (0 == k),
+                                     verbose, &sense_cat);
+        }
+        if (-1 == n) {
+            if (get_scsi_pt_transport_err(ptvp))
+                ret = SG_LIB_TRANSPORT_ERROR;
+            else
+                ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+            goto finish;
+        } else if (-2 == n) {
+            switch (sense_cat) {
+            case SG_LIB_CAT_RECOVERED:
+            case SG_LIB_CAT_NO_SENSE:
+                break;
+            case SG_LIB_CAT_NOT_READY:
+                ++num_errs;
+                if (1 ==  num_rs) {
+                    ret = sense_cat;
+                    printf("device not ready\n");
+                }
+                break;
+            case SG_LIB_CAT_UNIT_ATTENTION:
+                ++num_errs;
+                if (verbose) {
+                    pr2serr("Ignoring Unit attention (sense key)\n");
+                }
+                break;
+            default:
+                ++num_errs;
+                if (1 == num_rs) {
+                    ret = sense_cat;
+                    sg_get_category_sense_str(sense_cat, sizeof(b), b,
+                                              verbose);
+                    printf("%s\n", b);
+                    break; // return k;
+                }
+                break;
+            }
+        }
+        if (n >= 0)
+            act_din_len = n;
+
+        if (act_din_len > 7) {
+            struct sg_scsi_sense_hdr ssh;
+
+            if (sg_scsi_normalize_sense(rsBuff, act_din_len, &ssh)) {
+                if (ssh.sense_key > 0) {
+                    ++num_din_errs;
+                    most_recent_skey = ssh.sense_key;
+                }
+                if (not_raw_hex && ((1 == num_rs) || verbose)) {
+                    char bb[144];
+
+                    sg_get_sense_str(NULL, rsBuff, act_din_len,
+                                     false, sizeof(bb), bb);
+                    pr2serr("data-in decoded as sense:\n%s\n", bb);
+                }
+            }
+        }
+        partial_clear_scsi_pt_obj(ptvp);
+        if (ret)
+            goto finish;
+
+        if (act_din_len > 0) {
+            if (do_raw)
+                dStrRaw(rsBuff, act_din_len);
+            else if (do_hex)
+                hex2stdout(rsBuff, act_din_len, 1);
+        }
+    }                                   /* <<<<< end of for(num_rs) loop */
+    if ((0 == ret) && do_status) {
+        ret = sg_err_category_sense(rsBuff, act_din_len);
+        if (SG_LIB_CAT_NO_SENSE == ret) {
+            struct sg_scsi_sense_hdr ssh;
+
+            if (sg_scsi_normalize_sense(rsBuff, act_din_len, &ssh)) {
+                if ((0 == ssh.asc) && (0 == ssh.ascq))
+                    ret = 0;
+            }
+        }
+    }
+#ifndef SG_LIB_MINGW
+    if (not_raw_hex && do_time && (start_tm.tv_sec || start_tm.tv_usec)) {
+        struct timeval res_tm;
+        double den, num;
+
+        gettimeofday(&end_tm, NULL);
+        res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+        res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+        if (res_tm.tv_usec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_usec += 1000000;
+        }
+        den = res_tm.tv_sec;
+        den += (0.000001 * res_tm.tv_usec);
+        num = (double)num_rs;
+        printf("time to perform commands was %d.%06d secs",
+               (int)res_tm.tv_sec, (int)res_tm.tv_usec);
+        if (den > 0.00001)
+            printf("; %.2f operations/sec\n", num / den);
+        else
+            printf("\n");
+    }
+#endif
+
+finish:
+    if (not_raw_hex) {
+        if (num_errs > 0)
+            printf("Number of command errors detected: %d\n", num_errs);
+        if (num_din_errs > 0)
+            printf("Number of data-in errors detected: %d, most recent "
+                   "sense_key=%d\n", num_din_errs, most_recent_skey);
+    }
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            if (not_raw_hex)
+                pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (not_raw_hex && (0 == verbose)) {
+        if (! sg_if_can2stderr("sg_requests failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_reset.c b/src/sg_reset.c
new file mode 100644
index 0000000..739a7f2
--- /dev/null
+++ b/src/sg_reset.c
@@ -0,0 +1,314 @@
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *  Copyright (C) 1999-2022 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program send either device, bus or host resets to device,
+ * or bus or host associated with the given sg device. This is a Linux
+ * only utility (perhaps Android as well).
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_io_linux.h"
+
+
+#define ME "sg_reset: "
+
+static const char * version_str = "0.68 20220308";
+
+#ifndef SG_SCSI_RESET
+#define SG_SCSI_RESET 0x2284
+#endif
+
+#ifndef SG_SCSI_RESET_NOTHING
+#define SG_SCSI_RESET_NOTHING 0
+#define SG_SCSI_RESET_DEVICE 1
+#define SG_SCSI_RESET_BUS 2
+#define SG_SCSI_RESET_HOST 3
+#endif
+
+#ifndef SG_SCSI_RESET_TARGET
+#define SG_SCSI_RESET_TARGET 4
+#endif
+
+#ifndef SG_SCSI_RESET_NO_ESCALATE
+#define SG_SCSI_RESET_NO_ESCALATE 0x100
+#endif
+
+static struct option long_options[] = {
+        {"bus", no_argument, 0, 'b'},
+        {"device", no_argument, 0, 'd'},
+        {"help", no_argument, 0, 'z'},
+        {"host", no_argument, 0, 'H'},
+        {"no-esc", no_argument, 0, 'N'},
+        {"no_esc", no_argument, 0, 'N'},
+        {"no-escalate", no_argument, 0, 'N'},
+        {"target", no_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+#if defined(__GNUC__) || defined(__clang__)
+static int pr2serr(const char * fmt, ...)
+        __attribute__ ((format (printf, 1, 2)));
+#else
+static int pr2serr(const char * fmt, ...);
+#endif
+
+
+static int
+pr2serr(const char * fmt, ...)
+{
+    va_list args;
+    int n;
+
+    va_start(args, fmt);
+    n = vfprintf(stderr, fmt, args);
+    va_end(args);
+    return n;
+}
+
+static void
+usage(int compat_mode)
+{
+    pr2serr("Usage: sg_reset [--bus] [--device] [--help] [--host] [--no-esc] "
+            "[--no-escalate] [--target]\n"
+            "                [--verbose] [--version] DEVICE\n"
+            "  where:\n"
+            "    --bus|-b        SCSI bus reset (SPI concept), might be all "
+            "targets\n"
+            "    --device|-d     device (logical unit) reset\n");
+    if (compat_mode) {
+        pr2serr("    --help|-z       print usage information then exit\n"
+                "    --host|-h|-H    host (bus adapter: HBA) reset\n");
+    } else {
+        pr2serr("    --help|-h       print usage information then exit\n"
+                "    --host|-H       host (bus adapter: HBA) reset\n");
+    }
+    pr2serr("    --no-esc|-N     overrides default action and only does "
+            "reset requested\n"
+            "    --no-escalate   The same as --no-esc|-N\n"
+            "    --target|-t     target reset. The target holds the DEVICE "
+            "and perhaps\n"
+            "                    other LUs\n"
+            "    --verbose|-v    increase the level of verbosity\n"
+            "    --version|-V    print version number then exit\n\n"
+            "Use SG_SCSI_RESET ioctl to send a reset to the "
+            "host/bus/target/device\nalong the DEVICE path. The DEVICE "
+            "itself is known as a logical unit (LU)\nin SCSI terminology.\n"
+            "Be warned: if the '-N' option is not given then if '-d' "
+            "fails then a\ntarget reset ('-t') is instigated. And it "
+            "'-t' fails then a bus reset\n('-b') is instigated. And if "
+            "'-b' fails then a host reset ('h') is\ninstigated. It is "
+            "recommended to use '-N' to stop the reset escalation.\n"
+           );
+}
+
+
+int main(int argc, char * argv[])
+{
+    bool do_device_reset = false;
+    bool do_bus_reset = false;
+    bool do_host_reset = false;
+    bool no_escalate = false;
+    bool do_target_reset = false;
+    int c, sg_fd, res, k, hold_errno;
+    int verbose = 0;
+    char * device_name = NULL;
+    char * cp = NULL;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (NULL == cp)
+        cp = getenv("SG_RESET_OLD_OPTS");
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "bdhHNtvVz", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            do_bus_reset = true;
+            break;
+        case 'd':
+            do_device_reset = true;
+            break;
+        case 'h':
+            if (cp) {
+                do_host_reset = true;
+                break;
+            } else {
+                usage(!!cp);
+                return 0;
+            }
+        case 'H':
+            do_host_reset = true;
+            break;
+        case 'N':
+            no_escalate = true;
+            break;
+        case 't':
+            do_target_reset = true;
+            break;
+        case 'v':
+            ++verbose;
+            break;
+        case 'V':
+            pr2serr(ME "version: %s\n", version_str);
+            return 0;
+        case 'z':
+            usage(!!cp);
+            return 0;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage(!!cp);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage(!!cp);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (NULL == device_name) {
+        pr2serr("Missing DEVICE name. Use '--help' to see usage.\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (cp && (0 == verbose))
+        ++verbose;      // older behaviour was more verbose
+
+    if (((int)do_device_reset + (int)do_target_reset + (int)do_bus_reset +
+         (int)do_host_reset) > 1) {
+        pr2serr("Can only request one type of reset per invocation\n");
+        return 1;
+    }
+
+    sg_fd = open(device_name, O_RDWR | O_NONBLOCK);
+    if (sg_fd < 0) {
+        pr2serr(ME "open error: %s: ", device_name);
+        perror("");
+        return 1;
+    }
+
+    k = SG_SCSI_RESET_NOTHING;
+    if (do_device_reset) {
+        if (verbose)
+            printf(ME "starting device reset\n");
+        k = SG_SCSI_RESET_DEVICE;
+    }
+    else if (do_target_reset) {
+        if (verbose)
+            printf(ME "starting target reset\n");
+        k = SG_SCSI_RESET_TARGET;
+    }
+    else if (do_bus_reset) {
+        if (verbose)
+            printf(ME "starting bus reset\n");
+        k = SG_SCSI_RESET_BUS;
+    }
+    else if (do_host_reset) {
+        if (verbose)
+            printf(ME "starting host reset\n");
+        k = SG_SCSI_RESET_HOST;
+    }
+    if (no_escalate)
+        k += SG_SCSI_RESET_NO_ESCALATE;
+    if (verbose > 2)
+        pr2serr("    third argument to ioctl(SG_SCSI_RESET) is 0x%x\n", k);
+
+    res = ioctl(sg_fd, SG_SCSI_RESET, &k);
+    if (res < 0) {
+        hold_errno = errno;
+        switch (errno) {
+        case EBUSY:
+            pr2serr(ME "BUSY, may be resetting now\n");
+            break;
+        case ENODEV:
+            pr2serr(ME "'no device' error, may be temporary while device is "
+                    "resetting\n");
+            break;
+        case EAGAIN:
+            pr2serr(ME "try again later, may be resetting now\n");
+            break;
+        case EIO:
+            pr2serr(ME "reset (for value=0x%x) may not be available\n", k);
+            break;
+        case EPERM:
+        case EACCES:
+            pr2serr(ME "reset requires CAP_SYS_ADMIN (root) permission\n");
+            break;
+        case EINVAL:
+            pr2serr(ME "SG_SCSI_RESET not supported (for value=0x%x)\n", k);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        default:
+            perror(ME "SG_SCSI_RESET failed");
+            break;
+        }
+        if (verbose > 1)
+            pr2serr(ME "ioctl(SG_SCSI_RESET) returned %d, errno=%d\n", res,
+                    hold_errno);
+        close(sg_fd);
+        return 1;
+    }
+
+    if (no_escalate)
+        k -= SG_SCSI_RESET_NO_ESCALATE;
+    if (verbose) {
+        if (SG_SCSI_RESET_NOTHING == k)
+            printf(ME "did nothing, device is normal mode\n");
+        else if (SG_SCSI_RESET_DEVICE == k)
+            printf(ME "completed device %sreset\n", (no_escalate ?
+                    "" : "(or target or bus or host) "));
+        else if (SG_SCSI_RESET_TARGET == k)
+            printf(ME "completed target %sreset\n", (no_escalate ?
+                    "" : "(or bus or host) "));
+        else if (SG_SCSI_RESET_BUS == k)
+            printf(ME "completed bus %sreset\n", (no_escalate ?
+                    "" : "(or host) "));
+        else if (SG_SCSI_RESET_HOST == k)
+            printf(ME "completed host reset\n");
+    }
+
+    if (close(sg_fd) < 0) {
+        perror(ME "close error");
+        return 1;
+    }
+    return 0;
+}
diff --git a/src/sg_reset_wp.c b/src/sg_reset_wp.c
new file mode 100644
index 0000000..1580c84
--- /dev/null
+++ b/src/sg_reset_wp.c
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) 2014-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI RESET WRITE POINTER command to the given SCSI
+ * device. Based on zbc-r04c.pdf .
+ */
+
+static const char * version_str = "1.16 20211114";
+
+#define SG_ZONING_OUT_CMDLEN 16
+#define RESET_WRITE_POINTER_SA 0x4
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+
+static struct option long_options[] = {
+        {"all", no_argument, 0, 'a'},
+        {"count", required_argument, 0, 'C'},
+        {"help", no_argument, 0, 'h'},
+        {"reset-all", no_argument, 0, 'R'},
+        {"reset_all", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"zone", required_argument, 0, 'z'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_reset_wp  [--all] [--count=ZC] [--help] [--verbose]\n"
+            "                    [--version] [--zone=ID] DEVICE\n");
+    pr2serr("  where:\n"
+            "    --all|-a           sets the ALL flag in the cdb\n"
+            "    --count=ZC|-C ZC    set zone count field (def: 0)\n"
+            "    --help|-h          print out usage message\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n"
+            "    --zone=ID|-z ID    ID is the starting LBA of the zone "
+            "whose\n"
+            "                       write pointer is to be reset\n\n"
+            "Performs a SCSI RESET WRITE POINTER command. ID is decimal by "
+            "default,\nfor hex use a leading '0x' or a trailing 'h'. "
+            "Either the --zone=ID\nor --all option needs to be given.\n");
+}
+
+/* Invokes a SCSI RESET WRITE POINTER command (ZBC).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_reset_write_pointer(int sg_fd, uint64_t zid, uint16_t zc, bool all,
+                          bool noisy, int verbose)
+{
+    int ret, res, sense_cat;
+    uint8_t rwp_cdb[SG_ZONING_OUT_CMDLEN] = {SG_ZONING_OUT,
+         RESET_WRITE_POINTER_SA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be64(zid, rwp_cdb + 2);
+    sg_put_unaligned_be16(zc, rwp_cdb + 12);
+    if (all)
+        rwp_cdb[14] = 0x1;
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    Reset write pointer cdb: %s\n",
+                sg_get_command_str(rwp_cdb, SG_ZONING_OUT_CMDLEN, false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("Reset write pointer: out of memory\n");
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, rwp_cdb, sizeof(rwp_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, "reset write pointer", res, noisy,
+                               verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool all = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    bool zid_given = false;
+    int res, c, n;
+    int sg_fd = -1;
+    int ret = 0;
+    int verbose = 0;
+    uint16_t zc = 0;
+    uint64_t zid = 0;
+    int64_t ll;
+    const char * device_name = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "aC:hRvVz:", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+        case 'R':
+            all = true;
+            break;
+        case 'C':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 0xffff)) {
+                pr2serr("--count= expects an argument between 0 and 0xffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            zc = (uint16_t)n;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        case 'z':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--zone=ID'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            zid = (uint64_t)ll;
+            zid_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if ((! zid_given) && (! all)) {
+        pr2serr("either the --zone=ID or --all option is required\n\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        int err = -sg_fd;
+
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(err));
+        ret = sg_convert_errno(err);
+        goto fini;
+    }
+
+    res = sg_ll_reset_write_pointer(sg_fd, zid, zc, all, true, verbose);
+    ret = res;
+    if (res) {
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("Reset write pointer command not supported\n");
+        else {
+            char b[80];
+
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("Reset write pointer command: %s\n", b);
+        }
+    }
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_reset_wp failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_rmsn.c b/src/sg_rmsn.c
new file mode 100644
index 0000000..c413ea4
--- /dev/null
+++ b/src/sg_rmsn.c
@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2005-2018 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program was originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI command READ MEDIA SERIAL NUMBER
+ * to the given SCSI device.
+ */
+
+static const char * version_str = "1.18 20180628";
+
+#define SERIAL_NUM_SANITY_LEN (16 * 1024)
+
+
+static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void usage()
+{
+    pr2serr("Usage: sg_rmsn   [--help] [--raw] [--readonly] [--verbose] "
+            "[--version]\n"
+            "                 DEVICE\n"
+            "  where:\n"
+            "    --help|-h       print out usage message\n"
+            "    --raw|-r        output serial number to stdout "
+            "(potentially binary)\n"
+            "    --readonly|-R    open DEVICE read-only (def: open it "
+            "read-write)\n"
+            "    --verbose|-v    increase verbosity\n"
+            "    --version|-V    print version string and exit\n\n"
+            "Performs a SCSI READ MEDIA SERIAL NUMBER command\n");
+}
+
+int main(int argc, char * argv[])
+{
+    bool raw = false;
+    bool readonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, sn_len, n;
+    int sg_fd = -1;
+    int ret = 0;
+    int verbose = 0;
+    uint8_t rmsn_buff[4];
+    uint8_t * bp = NULL;
+    const char * device_name = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "hrRvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'r':
+            raw = true;
+            break;
+        case 'R':
+            readonly = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    memset(rmsn_buff, 0x0, sizeof(rmsn_buff));
+
+    res = sg_ll_read_media_serial_num(sg_fd, rmsn_buff, sizeof(rmsn_buff),
+                                      true, verbose);
+    ret = res;
+    if (0 == res) {
+        sn_len = sg_get_unaligned_be32(rmsn_buff + 0);
+        if (! raw)
+            printf("Reported serial number length = %d\n", sn_len);
+        if (0 == sn_len) {
+            pr2serr("    This implies the media has no serial number\n");
+            goto err_out;
+        }
+        if (sn_len > SERIAL_NUM_SANITY_LEN) {
+            pr2serr("    That length (%d) seems too long for a serial "
+                    "number\n", sn_len);
+            goto err_out;
+        }
+        sn_len += 4;
+        bp = (uint8_t *)malloc(sn_len);
+        if (NULL == bp) {
+            pr2serr("    Out of memory (ram)\n");
+            goto err_out;
+        }
+        res = sg_ll_read_media_serial_num(sg_fd, bp, sn_len, true, verbose);
+        if (0 == res) {
+            sn_len = sg_get_unaligned_be32(bp + 0);
+            if (raw) {
+                if (sn_len > 0) {
+                    n = fwrite(bp + 4, 1, sn_len, stdout);
+                    if (n) { ; }  /* unused, dummy to suppress warning */
+                }
+            } else {
+                printf("Serial number:\n");
+                if (sn_len > 0)
+                    hex2stdout(bp + 4, sn_len, 0);
+            }
+        }
+    }
+    if (res) {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Read Media Serial Number: %s\n", b);
+        if (0 == verbose)
+            pr2serr("    try '-v' for more information\n");
+    }
+
+err_out:
+    if (bp)
+        free(bp);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_rmsn failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_rtpg.c b/src/sg_rtpg.c
new file mode 100644
index 0000000..d83bf36
--- /dev/null
+++ b/src/sg_rtpg.c
@@ -0,0 +1,371 @@
+/*
+ * Copyright (c) 2004-2018 Christophe Varoqui and Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI command REPORT TARGET PORT GROUPS
+ * to the given SCSI device.
+ */
+
+static const char * version_str = "1.27 20180628";
+
+#define REPORT_TGT_GRP_BUFF_LEN 1024
+
+#define TPGS_STATE_OPTIMIZED 0x0
+#define TPGS_STATE_NONOPTIMIZED 0x1
+#define TPGS_STATE_STANDBY 0x2
+#define TPGS_STATE_UNAVAILABLE 0x3
+#define TPGS_STATE_LB_DEPENDENT 0x4
+#define TPGS_STATE_OFFLINE 0xe          /* SPC-4 rev 9 */
+#define TPGS_STATE_TRANSITIONING 0xf
+
+#define STATUS_CODE_NOSTATUS 0x0
+#define STATUS_CODE_CHANGED_BY_SET 0x1
+#define STATUS_CODE_CHANGED_BY_IMPLICIT 0x2
+
+static struct option long_options[] = {
+        {"decode", no_argument, 0, 'd'},
+        {"extended", no_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_rtpg   [--decode] [--extended] [--help] [--hex] "
+            "[--raw] [--readonly]\n"
+            "                 [--verbose] [--version] DEVICE\n"
+            "  where:\n"
+            "    --decode|-d        decode status and asym. access state\n"
+            "    --extended|-e      use extended header parameter data "
+            "format\n"
+            "    --help|-h          print out usage message\n"
+            "    --hex|-H           print out response in hex\n"
+            "    --raw|-r           output response in binary to stdout\n"
+            "    --readonly|-R      open DEVICE read-only (def: read-write)\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Performs a SCSI REPORT TARGET PORT GROUPS command\n");
+
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+static void
+decode_status(const int st)
+{
+    switch (st) {
+    case STATUS_CODE_NOSTATUS:
+        printf(" (no status available)");
+        break;
+    case STATUS_CODE_CHANGED_BY_SET:
+        printf(" (target port asym. state changed by SET TARGET PORT "
+               "GROUPS command)");
+        break;
+    case STATUS_CODE_CHANGED_BY_IMPLICIT:
+        printf(" (target port asym. state changed by implicit lu "
+               "behaviour)");
+        break;
+    default:
+        printf(" (unknown status code)");
+        break;
+    }
+}
+
+static void
+decode_tpgs_state(const int st)
+{
+    switch (st) {
+    case TPGS_STATE_OPTIMIZED:
+        printf(" (active/optimized)");
+        break;
+    case TPGS_STATE_NONOPTIMIZED:
+        printf(" (active/non optimized)");
+        break;
+    case TPGS_STATE_STANDBY:
+        printf(" (standby)");
+        break;
+    case TPGS_STATE_UNAVAILABLE:
+        printf(" (unavailable)");
+        break;
+    case TPGS_STATE_LB_DEPENDENT:
+        printf(" (logical block dependent)");
+        break;
+    case TPGS_STATE_OFFLINE:
+        printf(" (offline)");
+        break;
+    case TPGS_STATE_TRANSITIONING:
+        printf(" (transitioning between states)");
+        break;
+    default:
+        printf(" (unknown)");
+        break;
+    }
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool decode = false;
+    bool hex = false;
+    bool raw = false;
+    bool o_readonly = false;
+    bool extended = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int k, j, off, res, c, report_len, buff_len, tgt_port_count;
+    int sg_fd = -1;
+    int ret = 0;
+    int verbose = 0;
+    uint8_t * reportTgtGrpBuff = NULL;
+    uint8_t * bp;
+    const char * device_name = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "dehHrRvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'd':
+            decode = true;
+            break;
+        case 'e':
+             extended = true;
+             break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            hex = true;
+            break;
+        case 'r':
+            raw = true;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("Version: %s\n", version_str);
+        return 0;
+    }
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    buff_len = REPORT_TGT_GRP_BUFF_LEN;
+
+retry:
+    reportTgtGrpBuff = (uint8_t *)malloc(buff_len);
+    if (NULL == reportTgtGrpBuff) {
+        pr2serr("    Out of memory (ram)\n");
+        goto err_out;
+    }
+    memset(reportTgtGrpBuff, 0x0, buff_len);
+
+    res = sg_ll_report_tgt_prt_grp2(sg_fd, reportTgtGrpBuff,
+                                    buff_len,
+                                    extended, true, verbose);
+    ret = res;
+    if (0 == res) {
+        report_len = sg_get_unaligned_be32(reportTgtGrpBuff + 0) + 4;
+        if (report_len > buff_len) {
+            free(reportTgtGrpBuff);
+            buff_len = report_len;
+            goto retry;
+        }
+        if (raw) {
+            dStrRaw(reportTgtGrpBuff, report_len);
+            goto err_out;
+        }
+        if (verbose)
+            printf("Report list length = %d\n", report_len);
+        if (hex) {
+            if (verbose)
+                printf("\nOutput response in hex:\n");
+            hex2stdout(reportTgtGrpBuff, report_len, 1);
+            goto err_out;
+        }
+        printf("Report target port groups:\n");
+        bp = reportTgtGrpBuff + 4;
+        if (extended) {
+             if (0x10 != (bp[0] & 0x70)) {
+                  pr2serr("   <<invalid extended header format\n");
+                  goto err_out;
+             }
+             printf("  Implicit transition time: %d\n", bp[1]);
+             bp += 4;
+        }
+        for (k = bp - reportTgtGrpBuff; k < report_len;
+             k += off, bp += off) {
+
+            printf("  target port group id : 0x%x , Pref=%d, Rtpg_fmt=%d\n",
+                   sg_get_unaligned_be16(bp + 2), !!(bp[0] & 0x80),
+                   (bp[0] >> 4) & 0x07);
+            printf("    target port group asymmetric access state : ");
+            printf("0x%02x", bp[0] & 0x0f);
+            if (decode)
+                decode_tpgs_state(bp[0] & 0x0f);
+            printf("\n");
+
+            printf("    T_SUP : %d, ", !!(bp[1] & 0x80));
+            printf("O_SUP : %d, ", !!(bp[1] & 0x40));
+            printf("LBD_SUP : %d, ", !!(bp[1] & 0x10));
+            printf("U_SUP : %d, ", !!(bp[1] & 0x08));
+            printf("S_SUP : %d, ", !!(bp[1] & 0x04));
+            printf("AN_SUP : %d, ", !!(bp[1] & 0x02));
+            printf("AO_SUP : %d\n", !!(bp[1] & 0x01));
+
+            printf("    status code : ");
+            printf("0x%02x", bp[5]);
+            if (decode)
+                decode_status(bp[5]);
+            printf("\n");
+
+            printf("    vendor unique status : ");
+            printf("0x%02x\n", bp[6]);
+
+            printf("    target port count : ");
+            tgt_port_count = bp[7];
+            printf("%02x\n", tgt_port_count);
+
+            for (j = 0; j < tgt_port_count * 4; j += 4) {
+                if (0 == j)
+                    printf("    Relative target port ids:\n");
+                printf("      0x%02x\n",
+                       sg_get_unaligned_be16(bp + 8 + j + 2));
+            }
+            off = 8 + j;
+        }
+    } else if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("Report Target Port Groups command not supported\n");
+    else if (SG_LIB_CAT_ILLEGAL_REQ == res)
+        pr2serr("bad field in Report Target Port Groups cdb including "
+                "unsupported service action\n");
+    else {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Report Target Port Groups: %s\n", b);
+    }
+
+err_out:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (reportTgtGrpBuff)
+        free(reportTgtGrpBuff);
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_rtpg failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_safte.c b/src/sg_safte.c
new file mode 100644
index 0000000..588d47f
--- /dev/null
+++ b/src/sg_safte.c
@@ -0,0 +1,776 @@
+/*
+ * Copyright (c) 2004-2018 Hannes Reinecke and Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program for the Linux OS SCSI subsystem.
+ *
+ *  This program accesses a processor device which operates according
+ *  to the 'SCSI Accessed Fault-Tolerant Enclosures' (SAF-TE) spec.
+ */
+
+static const char * version_str = "0.33 20180628";
+
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs == 60 seconds */
+#define EBUFF_SZ 256
+
+#define RB_MODE_DESC 3
+#define RWB_MODE_DATA 2
+#define RWB_MODE_VENDOR 1
+#define RB_DESC_LEN 4
+
+#define SAFTE_CFG_FLAG_DOORLOCK 1
+#define SAFTE_CFG_FLAG_ALARM 2
+#define SAFTE_CFG_FLAG_CELSIUS 3
+
+struct safte_cfg_t {
+    int fans;
+    int psupplies;
+    int slots;
+    int temps;
+    int thermostats;
+    int vendor_specific;
+    int flags;
+};
+
+struct safte_cfg_t safte_cfg;
+
+static unsigned int buf_capacity = 64;
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* Buffer ID 0x0: Read Enclosure Configuration (mandatory) */
+static int
+read_safte_configuration(int sg_fd, uint8_t *rb_buff,
+                         unsigned int rb_len, int verbose)
+{
+    int res;
+
+    if (rb_len < buf_capacity) {
+        pr2serr("SCSI BUFFER size too small (%d/%d bytes)\n", rb_len,
+                buf_capacity);
+        return SG_LIB_CAT_ILLEGAL_REQ;
+    }
+
+    if (verbose > 1)
+        pr2serr("Use READ BUFFER,mode=vendor_specific,buff_id=0 to fetch "
+                "configuration\n");
+    res = sg_ll_read_buffer(sg_fd, RWB_MODE_VENDOR, 0, 0,
+                            rb_buff, rb_len, true, verbose);
+    if (res && res != SG_LIB_CAT_RECOVERED)
+        return res;
+
+    safte_cfg.fans = rb_buff[0];
+    safte_cfg.psupplies = rb_buff[1];
+    safte_cfg.slots = rb_buff[2];
+    safte_cfg.temps = rb_buff[4];
+    if (rb_buff[3])
+        safte_cfg.flags |= SAFTE_CFG_FLAG_DOORLOCK;
+    if (rb_buff[5])
+        safte_cfg.flags |= SAFTE_CFG_FLAG_ALARM;
+    if (rb_buff[6] & 0x80)
+        safte_cfg.flags |= SAFTE_CFG_FLAG_CELSIUS;
+
+    safte_cfg.thermostats = rb_buff[6] & 0x0f;
+    safte_cfg.vendor_specific = rb_buff[63];
+
+    return 0;
+}
+
+static int
+print_safte_configuration(void)
+{
+    printf("Enclosure Configuration:\n");
+    printf("\tNumber of Fans: %d\n", safte_cfg.fans);
+    printf("\tNumber of Power Supplies: %d\n", safte_cfg.psupplies);
+    printf("\tNumber of Device Slots: %d\n", safte_cfg.slots);
+    printf("\tNumber of Temperature Sensors: %d\n", safte_cfg.temps);
+    printf("\tNumber of Thermostats: %d\n", safte_cfg.thermostats);
+    printf("\tVendor unique bytes: %d\n", safte_cfg.vendor_specific);
+
+    return 0;
+}
+
+/* Buffer ID 0x01: Read Enclosure Status (mandatory) */
+static int
+do_safte_encl_status(int sg_fd, int do_hex, int do_raw, int verbose)
+{
+    int res, i, offset;
+    unsigned int rb_len;
+    uint8_t *rb_buff;
+
+    rb_len = safte_cfg.fans + safte_cfg.psupplies + safte_cfg.slots +
+        safte_cfg.temps + 5 + safte_cfg.vendor_specific;
+    rb_buff = (uint8_t *)malloc(rb_len);
+
+
+    if (verbose > 1)
+        pr2serr("Use READ BUFFER,mode=vendor_specific,buff_id=1 to read "
+                "enclosure status\n");
+    res = sg_ll_read_buffer(sg_fd, RWB_MODE_VENDOR, 1, 0,
+                            rb_buff, rb_len, false, verbose);
+    if (res && res != SG_LIB_CAT_RECOVERED)
+        return res;
+
+    if (do_raw > 1) {
+        dStrRaw(rb_buff, buf_capacity);
+        return 0;
+    }
+    if (do_hex > 1) {
+        hex2stdout(rb_buff, buf_capacity, 1);
+        return 0;
+    }
+    printf("Enclosure Status:\n");
+    offset = 0;
+    for (i = 0; i < safte_cfg.fans; i++) {
+        printf("\tFan %d status: ", i);
+        switch(rb_buff[i]) {
+            case 0:
+                printf("operational\n");
+                break;
+            case 1:
+                printf("malfunctioning\n");
+                break;
+            case 2:
+                printf("not installed\n");
+                break;
+            case 80:
+                printf("not reportable\n");
+                break;
+            default:
+                printf("unknown\n");
+                break;
+        }
+    }
+
+    offset += safte_cfg.fans;
+    for (i = 0; i < safte_cfg.psupplies; i++) {
+        printf("\tPower supply %d status: ", i);
+        switch(rb_buff[i + offset]) {
+            case 0:
+                printf("operational / on\n");
+                break;
+            case 1:
+                printf("operational / off\n");
+                break;
+            case 0x10:
+                printf("malfunctioning / on\n");
+                break;
+            case 0x11:
+                printf("malfunctioning / off\n");
+                break;
+            case 0x20:
+                printf("not present\n");
+                break;
+            case 0x21:
+                printf("present\n");
+                break;
+            case 0x80:
+                printf("not reportable\n");
+                break;
+            default:
+                printf("unknown\n");
+                break;
+        }
+    }
+
+    offset += safte_cfg.psupplies;
+    for (i = 0; i < safte_cfg.slots; i++) {
+        printf("\tDevice Slot %d: SCSI ID %d\n", i, rb_buff[i + offset]);
+    }
+
+    offset += safte_cfg.slots;
+    if (safte_cfg.flags & SAFTE_CFG_FLAG_DOORLOCK) {
+        switch(rb_buff[offset]) {
+            case 0x0:
+                printf("\tDoor lock status: locked\n");
+                break;
+            case 0x01:
+                printf("\tDoor lock status: unlocked\n");
+                break;
+            case 0x80:
+                printf("\tDoor lock status: not reportable\n");
+                break;
+        }
+    } else {
+        printf("\tDoor lock status: not installed\n");
+    }
+
+    offset++;
+    if (!(safte_cfg.flags & SAFTE_CFG_FLAG_ALARM)) {
+        printf("\tSpeaker status: not installed\n");
+    } else {
+        switch(rb_buff[offset]) {
+            case 0x0:
+                printf("\tSpeaker status: off\n");
+                break;
+            case 0x01:
+                printf("\tSpeaker status: on\n");
+                break;
+        }
+    }
+
+    offset++;
+    for (i = 0; i < safte_cfg.temps; i++) {
+        int temp = rb_buff[i + offset];
+        int is_celsius = !!(safte_cfg.flags & SAFTE_CFG_FLAG_CELSIUS);
+
+        if (! is_celsius)
+            temp -= 10;
+
+        printf("\tTemperature sensor %d: %d deg %c\n", i, temp,
+               is_celsius ? 'C' : 'F');
+    }
+
+    offset += safte_cfg.temps;
+    if (safte_cfg.thermostats) {
+        if (rb_buff[offset] & 0x80) {
+            printf("\tEnclosure Temperature alert status: abnormal\n");
+        } else {
+            printf("\tEnclosure Temperature alert status: normal\n");
+        }
+    }
+    return 0;
+}
+
+/* Buffer ID 0x02: Read Usage Statistics (optional) */
+static int
+do_safte_usage_statistics(int sg_fd, int do_hex, int do_raw, int verbose)
+{
+    int res;
+    unsigned int rb_len;
+    uint8_t *rb_buff;
+    unsigned int minutes;
+
+    rb_len = 16 + safte_cfg.vendor_specific;
+    rb_buff = (uint8_t *)malloc(rb_len);
+
+    if (verbose > 1)
+        pr2serr("Use READ BUFFER,mode=vendor_specific,buff_id=2 to read "
+                "usage statistics\n");
+    res = sg_ll_read_buffer(sg_fd, RWB_MODE_VENDOR, 2, 0,
+                            rb_buff, rb_len, false, verbose);
+    if (res) {
+        if (res == SG_LIB_CAT_ILLEGAL_REQ) {
+            printf("Usage Statistics:\n\tNot implemented\n");
+            return 0;
+        }
+        if (res != SG_LIB_CAT_RECOVERED) {
+            free(rb_buff);
+            return res;
+        }
+    }
+
+    if (do_raw > 1) {
+        dStrRaw(rb_buff, buf_capacity);
+        return 0;
+    }
+    if (do_hex > 1) {
+        hex2stdout(rb_buff, buf_capacity, 1);
+        return 0;
+    }
+    printf("Usage Statistics:\n");
+    minutes = sg_get_unaligned_be32(rb_buff + 0);
+    printf("\tPower on Minutes: %u\n", minutes);
+    minutes = sg_get_unaligned_be32(rb_buff + 4);
+    printf("\tPower on Cycles: %u\n", minutes);
+
+    free(rb_buff);
+    return 0;
+}
+
+/* Buffer ID 0x03: Read Device Insertions (optional) */
+static int
+do_safte_slot_insertions(int sg_fd, int do_hex, int do_raw, int verbose)
+{
+    int res, i;
+    unsigned int rb_len;
+    uint8_t *rb_buff, slot_status;
+
+    rb_len = safte_cfg.slots * 2;
+    rb_buff = (uint8_t *)malloc(rb_len);
+
+    if (verbose > 1)
+        pr2serr("Use READ BUFFER,mode=vendor_specific,buff_id=3 to read "
+                "device insertions\n");
+    res = sg_ll_read_buffer(sg_fd, RWB_MODE_VENDOR, 3, 0,
+                            rb_buff, rb_len, false, verbose);
+    if (res ) {
+        if (res == SG_LIB_CAT_ILLEGAL_REQ) {
+                printf("Slot insertions:\n\tNot implemented\n");
+                return 0;
+        }
+        if (res != SG_LIB_CAT_RECOVERED) {
+                free(rb_buff);
+                return res;
+        }
+    }
+
+    if (do_raw > 1) {
+        dStrRaw(rb_buff, buf_capacity);
+        return 0;
+    }
+    if (do_hex > 1) {
+        hex2stdout(rb_buff, buf_capacity, 1);
+        return 0;
+    }
+    printf("Slot insertions:\n");
+    for (i = 0; i < safte_cfg.slots; i++) {
+        slot_status = sg_get_unaligned_be16(rb_buff + (i * 2));
+        printf("\tSlot %d: %d insertions", i, slot_status);
+    }
+    free(rb_buff);
+    return 0;
+}
+
+/* Buffer ID 0x04: Read Device Slot Status (mandatory) */
+static int
+do_safte_slot_status(int sg_fd, int do_hex, int do_raw, int verbose)
+{
+    int res, i;
+    unsigned int rb_len;
+    uint8_t *rb_buff, slot_status;
+
+    rb_len = safte_cfg.slots * 4;
+    rb_buff = (uint8_t *)malloc(rb_len);
+
+    if (verbose > 1)
+        pr2serr("Use READ BUFFER,mode=vendor_specific,buff_id=4 to read "
+                "device slot status\n");
+    res = sg_ll_read_buffer(sg_fd, RWB_MODE_VENDOR, 4, 0,
+                            rb_buff, rb_len, false, verbose);
+    if (res && res != SG_LIB_CAT_RECOVERED) {
+        free(rb_buff);
+        return res;
+    }
+
+    if (do_raw > 1) {
+        dStrRaw(rb_buff, buf_capacity);
+        return 0;
+    }
+    if (do_hex > 1) {
+        hex2stdout(rb_buff, buf_capacity, 1);
+        return 0;
+    }
+    printf("Slot status:\n");
+    for (i = 0; i < safte_cfg.slots; i++) {
+        slot_status = rb_buff[i * 4 + 3];
+        printf("\tSlot %d: ", i);
+        if (slot_status & 0x7) {
+            if (slot_status & 0x1)
+                printf("inserted ");
+            if (slot_status & 0x2)
+                printf("ready ");
+            if (slot_status & 0x4)
+                printf("activated ");
+            printf("\n");
+        } else {
+            printf("empty\n");
+        }
+    }
+    free(rb_buff);
+    return 0;
+}
+
+/* Buffer ID 0x05: Read Global Flags (optional) */
+static int
+do_safte_global_flags(int sg_fd, int do_hex, int do_raw, int verbose)
+{
+    int res;
+    unsigned int rb_len;
+    uint8_t *rb_buff;
+
+    rb_len = 16;
+    rb_buff = (uint8_t *)malloc(rb_len);
+
+    if (verbose > 1)
+        pr2serr("Use READ BUFFER,mode=vendor_specific,buff_id=5 to read "
+                "global flags\n");
+    res = sg_ll_read_buffer(sg_fd, RWB_MODE_VENDOR, 5, 0,
+                            rb_buff, rb_len, false, verbose);
+    if (res ) {
+        if (res == SG_LIB_CAT_ILLEGAL_REQ) {
+                printf("Global Flags:\n\tNot implemented\n");
+                return 0;
+        }
+        if (res != SG_LIB_CAT_RECOVERED) {
+                free(rb_buff);
+                return res;
+        }
+    }
+
+    if (do_raw > 1) {
+        dStrRaw(rb_buff, buf_capacity);
+        return 0;
+    }
+    if (do_hex > 1) {
+        hex2stdout(rb_buff, buf_capacity, 1);
+        return 0;
+    }
+    printf("Global Flags:\n");
+    printf("\tAudible Alarm Control: %s\n",
+           rb_buff[0] & 0x1?"on":"off");
+    printf("\tGlobal Failure Indicator: %s\n",
+           rb_buff[0] & 0x2?"on":"off");
+    printf("\tGlobal Warning Indicator: %s\n",
+           rb_buff[0] & 0x4?"on":"off");
+    printf("\tEnclosure Power: %s\n",
+           rb_buff[0] & 0x8?"on":"off");
+    printf("\tCooling Failure: %s\n",
+           rb_buff[0] & 0x10?"yes":"no");
+    printf("\tPower Failure: %s\n",
+           rb_buff[0] & 0x20?"yes":"no");
+    printf("\tDrive Failure: %s\n",
+           rb_buff[0] & 0x40?"yes":"no");
+    printf("\tDrive Warning: %s\n",
+           rb_buff[0] & 0x80?"yes":"no");
+    printf("\tArray Failure: %s\n",
+           rb_buff[1] & 0x1?"yes":"no");
+    printf("\tArray Warning: %s\n",
+           rb_buff[0] & 0x2?"yes":"no");
+    printf("\tEnclosure Lock: %s\n",
+           rb_buff[0] & 0x4?"on":"off");
+    printf("\tEnclosure Identify: %s\n",
+           rb_buff[0] & 0x8?"on":"off");
+
+    free(rb_buff);
+    return 0;
+}
+
+static void
+usage()
+{
+    pr2serr("Usage:  sg_safte [--config] [--devstatus] [--encstatus] "
+            "[--flags] [--help]\n"
+            "                 [--hex] [--insertions] [--raw] [--usage] "
+            "[--verbose]\n"
+            "                 [--version] DEVICE\n"
+            "  where:\n"
+            "    --config|-c         output enclosure configuration\n"
+            "    --devstatus|-d      output device slot status\n"
+            "    --encstatus|-s      output enclosure status\n"
+            "    --flags|-f          output global flags\n"
+            "    --help|-h           output command usage message then "
+            "exit\n"
+            "    --hex|-H            output enclosure config in hex\n"
+            "    --insertions|-i     output insertion statistics\n"
+            "    --raw|-r            output enclosure config in binary "
+            "to stdout\n"
+            "    --usage|-u          output usage statistics\n"
+            "    --verbose|-v        increase verbosity\n"
+            "    --version|-v        output version then exit\n\n"
+            "Queries a SAF-TE processor device\n");
+}
+
+static struct option long_options[] = {
+    {"config", 0, 0, 'c'},
+    {"devstatus", 0, 0, 'd'},
+    {"encstatus", 0, 0, 's'},
+    {"flags", 0, 0, 'f'},
+    {"help", 0, 0, 'h'},
+    {"hex", 0, 0, 'H'},
+    {"insertions", 0, 0, 'i'},
+    {"raw", 0, 0, 'r'},
+    {"usage", 0, 0, 'u'},
+    {"verbose", 0, 0, 'v'},
+    {"version", 0, 0, 'V'},
+    {0, 0, 0, 0},
+};
+
+int
+main(int argc, char * argv[])
+{
+    bool do_insertions = false;
+    bool no_hex_raw;
+    bool verbose_given = false;
+    bool version_given = false;
+    int c, ret, peri_type;
+    int sg_fd = -1;
+    int res = SG_LIB_CAT_OTHER;
+    const char * device_name = NULL;
+    char ebuff[EBUFF_SZ];
+    uint8_t *rb_buff;
+    bool do_config = false;
+    bool do_status = false;
+    bool do_slots = false;
+    bool do_flags = false;
+    bool do_usage = false;
+    int do_hex = 0;
+    int do_raw = 0;
+    int verbose = 0;
+    const char * cp;
+    char buff[48];
+    char b[80];
+    struct sg_simple_inquiry_resp inq_resp;
+    const char op_name[] = "READ BUFFER";
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "cdfhHirsuvV?", long_options,
+                        &option_index);
+
+        if (c == -1)
+            break;
+
+        switch (c) {
+            case 'c':
+                do_config = true;
+                break;
+            case 'd':
+                do_slots = true;
+                break;
+            case 'f':
+                do_flags = true;
+                break;
+            case 'h':
+            case '?':
+                usage();
+                return 0;
+            case 'H':
+                ++do_hex;
+                break;
+            case 'i':
+                do_insertions = true;
+                break;
+            case 'r':
+                ++do_raw;
+                break;
+            case 's':
+                do_status = true;
+                break;
+            case 'u':
+                do_usage = true;
+                break;
+            case 'v':
+                verbose_given = true;
+                ++verbose;
+                break;
+            case 'V':
+                version_given = true;
+                break;
+            default:
+                pr2serr("unrecognised option code 0x%x ??\n", c);
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    if ((sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose))
+        < 0) {
+        if (verbose) {
+            snprintf(ebuff, EBUFF_SZ, "sg_safte: error opening file: %s (rw)",
+                     device_name);
+            perror(ebuff);
+        }
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+    no_hex_raw = ((0 == do_hex) && (0 == do_raw));
+
+    if (no_hex_raw) {
+        if (0 == sg_simple_inquiry(sg_fd, &inq_resp, true, verbose)) {
+            printf("  %.8s  %.16s  %.4s\n", inq_resp.vendor,
+                   inq_resp.product, inq_resp.revision);
+            peri_type = inq_resp.peripheral_type;
+            cp = sg_get_pdt_str(peri_type, sizeof(buff), buff);
+            if (strlen(cp) > 0)
+                printf("  Peripheral device type: %s\n", cp);
+            else
+                printf("  Peripheral device type: 0x%x\n", peri_type);
+        } else {
+            pr2serr("sg_safte: %s doesn't respond to a SCSI INQUIRY\n",
+                    device_name);
+            return SG_LIB_CAT_OTHER;
+        }
+    }
+
+    rb_buff = (uint8_t *)malloc(buf_capacity);
+    if (!rb_buff)
+        goto err_out;
+
+    memset(rb_buff, 0, buf_capacity);
+
+    res = read_safte_configuration(sg_fd, rb_buff, buf_capacity, verbose);
+    switch (res) {
+    case 0:
+    case SG_LIB_CAT_RECOVERED:
+        break;
+    default:
+        goto err_out;
+    }
+    if (1 == do_raw) {
+        dStrRaw(rb_buff, buf_capacity);
+        goto finish;
+    }
+    if (1 == do_hex) {
+        hex2stdout(rb_buff, buf_capacity, 1);
+        goto finish;
+    }
+
+    if (do_config && no_hex_raw)
+        print_safte_configuration();
+
+    if (do_status) {
+        res = do_safte_encl_status(sg_fd, do_hex, do_raw, verbose);
+        switch (res) {
+            case 0:
+            case SG_LIB_CAT_RECOVERED:
+                break;
+            default:
+                goto err_out;
+        }
+    }
+
+    if (do_usage) {
+        res = do_safte_usage_statistics(sg_fd, do_hex, do_raw, verbose);
+        switch (res) {
+            case 0:
+            case SG_LIB_CAT_RECOVERED:
+                break;
+            default:
+                goto err_out;
+        }
+    }
+
+    if (do_insertions) {
+        res = do_safte_slot_insertions(sg_fd, do_hex, do_raw, verbose);
+        switch (res) {
+            case 0:
+            case SG_LIB_CAT_RECOVERED:
+                break;
+            default:
+                goto err_out;
+        }
+    }
+
+    if (do_slots) {
+        res = do_safte_slot_status(sg_fd, do_hex, do_raw, verbose);
+        switch (res) {
+            case 0:
+            case SG_LIB_CAT_RECOVERED:
+                break;
+            default:
+                goto err_out;
+        }
+    }
+
+    if (do_flags) {
+        res = do_safte_global_flags(sg_fd, do_hex, do_raw, verbose);
+        switch (res) {
+            case 0:
+            case SG_LIB_CAT_RECOVERED:
+                break;
+            default:
+                goto err_out;
+        }
+    }
+finish:
+    res = 0;
+
+err_out:
+    switch (res) {
+    case 0:
+    case SG_LIB_CAT_RECOVERED:
+        break;
+    default:
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("%s failed: %s\n", op_name, b);
+        break;
+    }
+    ret = res;
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_safte failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_sanitize.c b/src/sg_sanitize.c
new file mode 100644
index 0000000..89108fe
--- /dev/null
+++ b/src/sg_sanitize.c
@@ -0,0 +1,792 @@
+/*
+ * Copyright (c) 2011-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.19 20220608";
+
+#define ME "sg_sanitize: "
+
+#define SANITIZE_OP 0x48
+#define SANITIZE_OP_LEN 10
+#define SANITIZE_SA_OVERWRITE 0x1
+#define SANITIZE_SA_BLOCK_ERASE 0x2
+#define SANITIZE_SA_CRYPTO_ERASE 0x3
+#define SANITIZE_SA_EXIT_FAIL_MODE 0x1f
+#define DEF_REQS_RESP_LEN 252
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define MAX_XFER_LEN 65535
+#define EBUFF_SZ 256
+
+#define SHORT_TIMEOUT 20   /* 20 seconds unless immed=0 ... */
+#define LONG_TIMEOUT (15 * 3600)       /* 15 hours ! */
+                /* Seagate ST32000444SS 2TB disk takes 9.5 hours to format */
+#define POLL_DURATION_SECS 60
+
+
+static struct option long_options[] = {
+    {"ause", no_argument, 0, 'A'},
+    {"block", no_argument, 0, 'B'},
+    {"count", required_argument, 0, 'c'},
+    {"crypto", no_argument, 0, 'C'},
+    {"desc", no_argument, 0, 'd'},
+    {"dry-run", no_argument, 0, 'D'},
+    {"dry_run", no_argument, 0, 'D'},
+    {"early", no_argument, 0, 'e'},
+    {"fail", no_argument, 0, 'F'},
+    {"help", no_argument, 0, 'h'},
+    {"invert", no_argument, 0, 'I'},
+    {"ipl", required_argument, 0, 'i'},
+    {"overwrite", no_argument, 0, 'O'},
+    {"pattern", required_argument, 0, 'p'},
+    {"quick", no_argument, 0, 'Q'},
+    {"test", required_argument, 0, 'T'},
+    {"timeout", required_argument, 0, 't'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {"wait", no_argument, 0, 'w'},
+    {"zero", no_argument, 0, 'z'},
+    {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool ause;
+    bool block;
+    bool crypto;
+    bool desc;
+    bool dry_run;
+    bool early;
+    bool fail;
+    bool invert;
+    bool overwrite;
+    bool quick;
+    bool verbose_given;
+    bool version_given;
+    bool wait;
+    bool znr;
+    int count;
+    int ipl;    /* initialization pattern length */
+    int test;
+    int timeout;        /* in seconds */
+    int verbose;
+    int zero;
+    const char * pattern_fn;
+};
+
+
+static void
+usage()
+{
+  pr2serr("Usage: sg_sanitize [--ause] [--block] [--count=OC] [--crypto] "
+          "[--dry-run]\n"
+          "                   [--early] [--fail] [--help] [--invert] "
+          "[--ipl=LEN]\n"
+          "                   [--overwrite] [--pattern=PF] [--quick] "
+          "[--test=TE]\n"
+          "                   [--timeout=SECS] [--verbose] [--version] "
+          "[--wait]\n"
+          "                   [--zero] [--znr] DEVICE\n"
+          "  where:\n"
+          "    --ause|-A            set AUSE bit in cdb\n"
+          "    --block|-B           do BLOCK ERASE sanitize\n"
+          "    --count=OC|-c OC     OC is overwrite count field (from 1 "
+          "(def) to 31)\n"
+          "    --crypto|-C          do CRYPTOGRAPHIC ERASE sanitize\n"
+          "    --desc|-d            polling request sense sets 'desc' "
+          "field\n"
+          "                         (def: clear 'desc' field)\n"
+          "    --dry-run|-D         to preparation but bypass SANITIZE "
+          "command\n"
+          "    --early|-e           exit once sanitize started (IMMED set "
+          "in cdb)\n"
+          "                         user can monitor progress with REQUEST "
+          "SENSE\n"
+          "    --fail|-F            do EXIT FAILURE MODE sanitize\n"
+          "    --help|-h            print out usage message\n"
+          "    --invert|-I          set INVERT bit in OVERWRITE parameter "
+          "list\n"
+          "    --ipl=LEN|-i LEN     initialization pattern length (in "
+          "bytes)\n"
+          "    --overwrite|-O       do OVERWRITE sanitize\n"
+          "    --pattern=PF|-p PF    PF is file containing initialization "
+          "pattern\n"
+          "                          for OVERWRITE\n"
+          "    --quick|-Q           start sanitize without pause for user\n"
+          "                         intervention (i.e. no time to "
+          "reconsider)\n"
+          "    --test=TE|-T TE      TE is placed in TEST field of "
+          "OVERWRITE\n"
+          "                         parameter list (def: 0)\n"
+          "    --timeout=SECS|-t SECS    SANITIZE command timeout in "
+          "seconds\n"
+          "    --verbose|-v         increase verbosity\n"
+          "    --version|-V         print version string then exit\n"
+          "    --wait|-w            wait for command to finish (could "
+          "take hours)\n"
+          "    --zero|-z            use pattern of zeros for "
+          "OVERWRITE\n"
+          "    --znr|-Z             set ZNR (zone no reset) bit in cdb\n\n"
+          "Performs a SCSI SANITIZE command.\n    <<<WARNING>>>: all data "
+          "on DEVICE will be lost.\nDefault action is to give user time to "
+          "reconsider; then execute SANITIZE\ncommand with IMMED bit set; "
+          "then use REQUEST SENSE command every 60\nseconds to poll for a "
+          "progress indication; then exit when there is no\nmore progress "
+          "indication.\n"
+          );
+}
+
+/* Invoke SCSI SANITIZE command. Returns 0 if successful, otherwise error */
+static int
+do_sanitize(int sg_fd, const struct opts_t * op, const void * param_lstp,
+            int param_lst_len)
+{
+    bool immed;
+    int ret, res, sense_cat, timeout;
+    uint8_t san_cdb[SANITIZE_OP_LEN];
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (op->early || op->wait)
+        immed = op->early;
+    else
+        immed = true;
+    timeout = (immed ? SHORT_TIMEOUT : LONG_TIMEOUT);
+    /* only use command line timeout if it exceeds previous defaults */
+    if (op->timeout > timeout)
+        timeout = op->timeout;
+    memset(san_cdb, 0, sizeof(san_cdb));
+    san_cdb[0] = SANITIZE_OP;
+    if (op->overwrite)
+        san_cdb[1] = SANITIZE_SA_OVERWRITE;
+    else if (op->block)
+        san_cdb[1] = SANITIZE_SA_BLOCK_ERASE;
+    else if (op->crypto)
+        san_cdb[1] = SANITIZE_SA_CRYPTO_ERASE;
+    else if (op->fail)
+        san_cdb[1] = SANITIZE_SA_EXIT_FAIL_MODE;
+    else
+        return SG_LIB_SYNTAX_ERROR;
+    if (immed)
+        san_cdb[1] |= 0x80;
+    if (op->znr)        /* added sbc4r07 */
+        san_cdb[1] |= 0x40;
+    if (op->ause)
+        san_cdb[1] |= 0x20;
+    sg_put_unaligned_be16((uint16_t)param_lst_len, san_cdb + 7);
+
+    if (op->verbose > 1) {
+        char b[128];
+
+        pr2serr("    Sanitize cdb: %s\n",
+                sg_get_command_str(san_cdb, SANITIZE_OP_LEN, false,
+                                   sizeof(b), b));
+        if (op->verbose > 2) {
+            if (param_lst_len > 0) {
+                pr2serr("    Parameter list contents:\n");
+                hex2stderr((const uint8_t *)param_lstp, param_lst_len, -1);
+            }
+            pr2serr("    Sanitize command timeout: %d seconds\n", timeout);
+        }
+    }
+    if (op->dry_run) {
+        pr2serr("Due to --dry-run option, bypassing SANITIZE command\n");
+        return 0;
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("Sanitize: out of memory\n");
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, san_cdb, sizeof(san_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)param_lstp, param_lst_len);
+    res = do_scsi_pt(ptvp, sg_fd, timeout, op->verbose);
+    ret = sg_cmds_process_resp(ptvp, "Sanitize", res, true /*noisy */,
+                               op->verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD:
+            {
+                bool valid;
+                int slen;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                if (valid)
+                    pr2serr("Medium or hardware error starting at "
+                            "lba=%" PRIu64 " [0x%" PRIx64 "]\n", ull, ull);
+            }
+            ret = sense_cat;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else {
+        ret = 0;
+        if (op->verbose)
+            pr2serr("Sanitize command %s without error\n",
+                    (immed ? "launched" : "completed"));
+    }
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+#define VPD_DEVICE_ID 0x83
+#define VPD_ASSOC_LU 0
+#define VPD_ASSOC_TPORT 1
+#define TPROTO_ISCSI 5
+
+static char *
+get_lu_name(const uint8_t * bp, int u_len, char * b, int b_len)
+{
+    int len, off, sns_dlen, dlen, k;
+    uint8_t u_sns[512];
+    char * cp;
+
+    len = u_len - 4;
+    bp += 4;
+    off = -1;
+    if (0 == sg_vpd_dev_id_iter(bp, len, &off, VPD_ASSOC_LU,
+                                8 /* SCSI name string (sns) */,
+                                3 /* UTF-8 */)) {
+        sns_dlen = bp[off + 3];
+        memcpy(u_sns, bp + off + 4, sns_dlen);
+        /* now want to check if this is iSCSI */
+        off = -1;
+        if (0 == sg_vpd_dev_id_iter(bp, len, &off, VPD_ASSOC_TPORT,
+                                    8 /* SCSI name string (sns) */,
+                                    3 /* UTF-8 */)) {
+            if ((0x80 & bp[1]) && (TPROTO_ISCSI == (bp[0] >> 4))) {
+                snprintf(b, b_len, "%.*s", sns_dlen, u_sns);
+                return b;
+            }
+        }
+    } else
+        sns_dlen = 0;
+    if (0 == sg_vpd_dev_id_iter(bp, len, &off, VPD_ASSOC_LU,
+                                3 /* NAA */, 1 /* binary */)) {
+        dlen = bp[off + 3];
+        if (! ((8 == dlen) || (16 ==dlen)))
+            return b;
+        cp = b;
+        for (k = 0; ((k < dlen) && (b_len > 1)); ++k) {
+            snprintf(cp, b_len, "%02x", bp[off + 4 + k]);
+            cp += 2;
+            b_len -= 2;
+        }
+    } else if (0 == sg_vpd_dev_id_iter(bp, len, &off, VPD_ASSOC_LU,
+                                       2 /* EUI */, 1 /* binary */)) {
+        dlen = bp[off + 3];
+        if (! ((8 == dlen) || (12 == dlen) || (16 ==dlen)))
+            return b;
+        cp = b;
+        for (k = 0; ((k < dlen) && (b_len > 1)); ++k) {
+            snprintf(cp, b_len, "%02x", bp[off + 4 + k]);
+            cp += 2;
+            b_len -= 2;
+        }
+    } else if (sns_dlen > 0)
+        snprintf(b, b_len, "%.*s", sns_dlen, u_sns);
+    return b;
+}
+
+#define SAFE_STD_INQ_RESP_LEN 36
+#define VPD_SUPPORTED_VPDS 0x0
+#define VPD_UNIT_SERIAL_NUM 0x80
+#define VPD_DEVICE_ID 0x83
+
+static int
+print_dev_id(int fd, uint8_t * sinq_resp, int max_rlen, int verbose)
+{
+    int res, k, n, verb, pdt, has_sn, has_di;
+    uint8_t b[256];
+    char a[256];
+    char pdt_name[64];
+
+    verb = (verbose > 1) ? verbose - 1 : 0;
+    memset(sinq_resp, 0, max_rlen);
+    res = sg_ll_inquiry(fd, false, false /* evpd */, 0 /* pg_op */, b,
+                        SAFE_STD_INQ_RESP_LEN, 1, verb);
+    if (res)
+        return res;
+    n = b[4] + 5;
+    if (n > SAFE_STD_INQ_RESP_LEN)
+        n = SAFE_STD_INQ_RESP_LEN;
+    memcpy(sinq_resp, b, (n < max_rlen) ? n : max_rlen);
+    if (n == SAFE_STD_INQ_RESP_LEN) {
+        pdt = b[0] & PDT_MASK;
+        printf("    %.8s  %.16s  %.4s   peripheral_type: %s [0x%x]\n",
+               (const char *)(b + 8), (const char *)(b + 16),
+               (const char *)(b + 32),
+               sg_get_pdt_str(pdt, sizeof(pdt_name), pdt_name), pdt);
+        if (verbose)
+            printf("      PROTECT=%d\n", !!(b[5] & 1));
+        if (b[5] & 1)
+            printf("      << supports protection information>>\n");
+    } else {
+        pr2serr("Short INQUIRY response: %d bytes, expect at least 36\n", n);
+        return SG_LIB_CAT_OTHER;
+    }
+    res = sg_ll_inquiry(fd, false, true /* evpd */, VPD_SUPPORTED_VPDS, b,
+                        SAFE_STD_INQ_RESP_LEN, 1, verb);
+    if (res) {
+        if (verbose)
+            pr2serr("VPD_SUPPORTED_VPDS gave res=%d\n", res);
+        return 0;
+    }
+    if (VPD_SUPPORTED_VPDS != b[1]) {
+        if (verbose)
+            pr2serr("VPD_SUPPORTED_VPDS corrupted\n");
+        return 0;
+    }
+    n = sg_get_unaligned_be16(b + 2);
+    if (n > (SAFE_STD_INQ_RESP_LEN - 4))
+        n = (SAFE_STD_INQ_RESP_LEN - 4);
+    for (k = 0, has_sn = 0, has_di = 0; k < n; ++k) {
+        if (VPD_UNIT_SERIAL_NUM == b[4 + k])
+            ++has_sn;
+        else if (VPD_DEVICE_ID == b[4 + k]) {
+            ++has_di;
+            break;
+        }
+    }
+    if (has_sn) {
+        res = sg_ll_inquiry(fd, false, true /* evpd */, VPD_UNIT_SERIAL_NUM,
+                            b, sizeof(b), 1, verb);
+        if (res) {
+            if (verbose)
+                pr2serr("VPD_UNIT_SERIAL_NUM gave res=%d\n", res);
+            return 0;
+        }
+        if (VPD_UNIT_SERIAL_NUM != b[1]) {
+            if (verbose)
+                pr2serr("VPD_UNIT_SERIAL_NUM corrupted\n");
+            return 0;
+        }
+        n = sg_get_unaligned_be16(b + 2);
+        if (n > (int)(sizeof(b) - 4))
+            n = (sizeof(b) - 4);
+        printf("      Unit serial number: %.*s\n", n, (const char *)(b + 4));
+    }
+    if (has_di) {
+        res = sg_ll_inquiry(fd, false, true /* evpd */, VPD_DEVICE_ID, b,
+                            sizeof(b), 1, verb);
+        if (res) {
+            if (verbose)
+                pr2serr("VPD_DEVICE_ID gave res=%d\n", res);
+            return 0;
+        }
+        if (VPD_DEVICE_ID != b[1]) {
+            if (verbose)
+                pr2serr("VPD_DEVICE_ID corrupted\n");
+            return 0;
+        }
+        n = sg_get_unaligned_be16(b + 2);
+        if (n > (int)(sizeof(b) - 4))
+            n = (sizeof(b) - 4);
+        n = strlen(get_lu_name(b, n + 4, a, sizeof(a)));
+        if (n > 0)
+            printf("      LU name: %.*s\n", n, a);
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool got_stdin = false;
+    int k, res, c, infd, progress, vb, n, resp_len, err;
+    int sg_fd = -1;
+    int param_lst_len = 0;
+    int ret = -1;
+    const char * device_name = NULL;
+    char ebuff[EBUFF_SZ];
+    char b[80];
+    uint8_t rsBuff[DEF_REQS_RESP_LEN];
+    uint8_t * wBuff = NULL;
+    uint8_t * free_wBuff = NULL;
+    struct opts_t opts;
+    struct opts_t * op;
+    struct stat a_stat;
+    uint8_t inq_resp[SAFE_STD_INQ_RESP_LEN];
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->count = 1;
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "ABc:CdDeFhi:IOp:Qt:T:vVwzZ",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'A':
+            op->ause = true;
+            break;
+        case 'B':
+            op->block = true;
+            break;
+        case 'c':
+            op->count = sg_get_num(optarg);
+            if ((op->count < 1) || (op->count > 31))  {
+                pr2serr("bad argument to '--count', expect 1 to 31\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'C':
+            op->crypto = true;
+            break;
+        case 'd':
+            op->desc = true;
+            break;
+        case 'D':
+            op->dry_run = true;
+            break;
+        case 'e':
+            op->early = true;
+            break;
+        case 'F':
+            op->fail = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+            op->ipl = sg_get_num(optarg);
+            if ((op->ipl < 1) || (op->ipl > 65535))  {
+                pr2serr("bad argument to '--ipl', expect 1 to 65535\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'I':
+            op->invert = true;
+            break;
+        case 'O':
+            op->overwrite = true;
+            break;
+        case 'p':
+            op->pattern_fn = optarg;
+            break;
+        case 'Q':
+            op->quick = true;
+            break;
+        case 't':
+            op->timeout = sg_get_num(optarg);
+            if (op->timeout < 0) {
+                pr2serr("bad argument to '--timeout=SECS', want 0 or more\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'T':
+            op->test = sg_get_num(optarg);
+            if ((op->test < 0) || (op->test > 3))  {
+                pr2serr("bad argument to '--test', expect 0 to 3\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'w':
+            op->wait = true;
+            break;
+        case 'z':
+            ++op->zero;
+            break;
+        case 'Z':
+            op->znr = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    vb = op->verbose;
+    n = (int)op->block + (int)op->crypto + (int)op->fail + (int)op->overwrite;
+    if (1 != n) {
+        pr2serr("one and only one of '--block', '--crypto', '--fail' or "
+                "'--overwrite' please\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (op->overwrite) {
+        if (op->zero) {
+            if (op->pattern_fn) {
+                pr2serr("confused: both '--pattern=PF' and '--zero' "
+                        "options\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->ipl = 4;
+        } else {
+            if (NULL == op->pattern_fn) {
+                pr2serr("'--overwrite' requires '--pattern=PF' or '--zero' "
+                        "option\n");
+                return SG_LIB_CONTRADICT;
+            }
+            got_stdin = (0 == strcmp(op->pattern_fn, "-"));
+            if (! got_stdin) {
+                memset(&a_stat, 0, sizeof(a_stat));
+                if (stat(op->pattern_fn, &a_stat) < 0) {
+                    err = errno;
+                    pr2serr("pattern file: unable to stat(%s): %s\n",
+                            op->pattern_fn, safe_strerror(err));
+                    ret = sg_convert_errno(err);
+                    goto err_out;
+                }
+                if (op->ipl <= 0) {
+                    op->ipl = (int)a_stat.st_size;
+                    if (op->ipl > MAX_XFER_LEN) {
+                        pr2serr("pattern file length exceeds 65535 bytes, "
+                                "need '--ipl=LEN' option\n");
+                         return SG_LIB_FILE_ERROR;
+                    }
+                }
+            }
+            if (op->ipl < 1) {
+                pr2serr("'--overwrite' requires '--ipl=LEN' option if can't "
+                        "get PF length\n");
+                return SG_LIB_CONTRADICT;
+            }
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, vb);
+    if (sg_fd < 0) {
+        if (op->verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    ret = print_dev_id(sg_fd, inq_resp, sizeof(inq_resp), op->verbose);
+    if (ret)
+        goto err_out;
+
+    if (op->overwrite) {
+        param_lst_len = op->ipl + 4;
+        wBuff = (uint8_t*)sg_memalign(op->ipl + 4, 0, &free_wBuff, false);
+        if (NULL == wBuff) {
+            pr2serr("unable to allocate %d bytes of memory with calloc()\n",
+                    op->ipl + 4);
+            ret = sg_convert_errno(ENOMEM);
+            goto err_out;
+        }
+        if (op->zero) {
+            if (2 == op->zero)  /* treat -zz as fill with 0xff bytes */
+                memset(wBuff + 4, 0xff, op->ipl);
+            else
+                memset(wBuff + 4, 0, op->ipl);
+        } else {
+            if (got_stdin) {
+                infd = STDIN_FILENO;
+                if (sg_set_binary_mode(STDIN_FILENO) < 0)
+                    perror("sg_set_binary_mode");
+            } else {
+                if ((infd = open(op->pattern_fn, O_RDONLY)) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, ME "could not open %s for "
+                             "reading", op->pattern_fn);
+                    perror(ebuff);
+                    ret = sg_convert_errno(err);
+                    goto err_out;
+                } else if (sg_set_binary_mode(infd) < 0)
+                    perror("sg_set_binary_mode");
+            }
+            res = read(infd, wBuff + 4, op->ipl);
+            if (res < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ, ME "couldn't read from %s",
+                         op->pattern_fn);
+                perror(ebuff);
+                if (! got_stdin)
+                    close(infd);
+                ret = sg_convert_errno(err);
+                goto err_out;
+            }
+            if (res < op->ipl) {
+                pr2serr("tried to read %d bytes from %s, got %d bytes\n",
+                         op->ipl, op->pattern_fn, res);
+                pr2serr("  so pad with 0x0 bytes and continue\n");
+            }
+            if (! got_stdin)
+                close(infd);
+        }
+        wBuff[0] = op->count & 0x1f;
+        if (op->test)
+            wBuff[0] |= ((op->test & 0x3) << 5);
+        if (op->invert)
+            wBuff[0] |= 0x80;
+        sg_put_unaligned_be16((uint16_t)op->ipl, wBuff + 2);
+    }
+
+    if ((! op->quick) && (! op->fail))
+        sg_warn_and_wait("SANITIZE", device_name, true);
+
+    ret = do_sanitize(sg_fd, op, wBuff, param_lst_len);
+    if (ret) {
+        sg_get_category_sense_str(ret, sizeof(b), b, vb);
+        pr2serr("Sanitize failed: %s\n", b);
+    }
+
+    if ((0 == ret) && (! op->early) && (! op->wait)) {
+        for (k = 0; ;++k) {     /* unbounded, exits via break */
+            if (op->dry_run && (k > 0)) {
+                pr2serr("Due to --dry-run option, leave poll loop\n");
+                break;
+            }
+            sg_sleep_secs(POLL_DURATION_SECS);
+            memset(rsBuff, 0x0, sizeof(rsBuff));
+            res = sg_ll_request_sense(sg_fd, op->desc, rsBuff, sizeof(rsBuff),
+                                      1, vb);
+            if (res) {
+                ret = res;
+                if (SG_LIB_CAT_INVALID_OP == res)
+                    pr2serr("Request Sense command not supported\n");
+                else if (SG_LIB_CAT_ILLEGAL_REQ == res) {
+                    pr2serr("bad field in Request Sense cdb\n");
+                    if (op->desc) {
+                        pr2serr("Descriptor type sense may not be supported, "
+                                "try again with fixed type\n");
+                        op->desc = false;
+                        continue;
+                    }
+                } else {
+                    sg_get_category_sense_str(res, sizeof(b), b, vb);
+                    pr2serr("Request Sense: %s\n", b);
+                    if (0 == vb)
+                        pr2serr("    try the '-v' option for more "
+                                "information\n");
+                }
+                break;
+            }
+            /* "Additional sense length" same in descriptor and fixed */
+            resp_len = rsBuff[7] + 8;
+            if (vb > 2) {
+                pr2serr("Parameter data in hex\n");
+                hex2stderr(rsBuff, resp_len, -1);
+            }
+            progress = -1;
+            sg_get_sense_progress_fld(rsBuff, resp_len, &progress);
+            if (progress < 0) {
+                ret = res;
+                if (vb > 1)
+                     pr2serr("No progress indication found, iteration %d\n",
+                             k + 1);
+                if ((0 == k) && vb)
+                     pr2serr("Sanitize seems to be successful and finished "
+                             "quickly\n");
+                /* N.B. exits first time there isn't a progress indication */
+                break;
+            } else
+                printf("Progress indication: %d%% done\n",
+                       (progress * 100) / 65536);
+        }
+    }
+
+err_out:
+    if (free_wBuff)
+        free(free_wBuff);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == op->verbose) {
+        if (! sg_if_can2stderr("sg_sanitize failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_sat_identify.c b/src/sg_sat_identify.c
new file mode 100644
index 0000000..7a8d825
--- /dev/null
+++ b/src/sg_sat_identify.c
@@ -0,0 +1,540 @@
+/*
+ * Copyright (c) 2006-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_pr2serr.h"
+#include "sg_unaligned.h"
+
+/* This program uses a ATA PASS-THROUGH SCSI command to package an
+ * ATA IDENTIFY (PACKAGE) DEVICE command. It is based on the SCSI to
+ * ATA Translation (SAT) drafts and standards. See https://www.t10.org
+ * for drafts. SAT is a standard: SAT ANSI INCITS 431-2007 (draft prior
+ * to that is sat-r09.pdf). SAT-2 is also a standard: SAT-2 ANSI INCITS
+ * 465-2010 and the draft prior to that is sat2r09.pdf . The SAT-3 is
+ * now a standard: SAT-3 ANSI INCITS 517-2015. The most current draft of
+ * SAT-4 is revision 5c (sat4r05c.pdf).
+ */
+
+#define SAT_ATA_PASS_THROUGH32_LEN 32
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_PASS_THROUGH12 0xa1     /* clashes with MMC BLANK command */
+#define SAT_ATA_PASS_THROUGH12_LEN 12
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return (sense) Descriptor */
+#define ASCQ_ATA_PT_INFO_AVAILABLE 0x1d
+
+#define ATA_IDENTIFY_DEVICE 0xec
+#define ATA_IDENTIFY_PACKET_DEVICE 0xa1
+#define ID_RESPONSE_LEN 512
+
+#define DEF_TIMEOUT 20
+
+#define EBUFF_SZ 256
+
+static const char * version_str = "1.19 20220425";
+
+static struct option long_options[] = {
+        {"ck-cond", no_argument, 0, 'c'},
+        {"ck_cond", no_argument, 0, 'c'},
+        {"extend", no_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"len", required_argument, 0, 'l'},
+        {"ident", no_argument, 0, 'i'},
+        {"packet", no_argument, 0, 'p'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_sat_identify [--ck_cond] [--extend] [--help] [--hex] "
+            "[--ident]\n"
+            "                       [--len=CLEN] [--packet] [--raw] "
+            "[--readonly]\n"
+            "                       [--verbose] [--version] DEVICE\n"
+            "  where:\n"
+            "    --ck_cond|-c     sets ck_cond bit in cdb (def: 0)\n"
+            "    --extend|-e      sets extend bit in cdb (def: 0)\n"
+            "    --help|-h        print out usage message then exit\n"
+            "    --hex|-H         output response in hex\n"
+            "    --ident|-i       output WWN prefixed by 0x, if not "
+            "available output\n"
+            "                     0x0000000000000000\n"
+            "    --len=CLEN| -l CLEN    CLEN is cdb length: 12, 16 or 32 "
+            "bytes\n"
+            "                           (default: 16)\n"
+            "    --packet|-p      do IDENTIFY PACKET DEVICE (def: IDENTIFY "
+            "DEVICE)\n"
+            "                     command\n"
+            "    --raw|-r         output response in binary to stdout\n"
+            "    --readonly|-R    open DEVICE read-only (def: read-write)\n"
+            "    --verbose|-v     increase verbosity\n"
+            "    --version|-V     print version string and exit\n\n"
+            "Performs a ATA IDENTIFY (PACKET) DEVICE command via a SAT "
+            "layer using\na SCSI ATA PASS-THROUGH(12), (16) or (32) command. "
+            "Only SAT layers\ncompliant with SAT-4 revision 5 or later will "
+            "support the SCSI ATA\nPASS-THROUGH(32) command.\n");
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+static int
+do_identify_dev(int sg_fd, bool do_packet, int cdb_len, bool ck_cond,
+                bool extend, bool do_ident, int do_hex, bool do_raw,
+                int verbose)
+{
+    const bool t_type = false;/* false -> 512 byte blocks,
+                                 true -> device's LB size */
+    bool t_dir = true;  /* false -> to device, true -> from device */
+    bool byte_block = true; /* false -> bytes, true -> 512 byte blocks (if
+                               t_type=false) */
+    bool got_ard = false;         /* got ATA result descriptor */
+    bool got_fixsense = false;    /* got ATA result in fixed format sense */
+    bool ok;
+    int j, res, ret, sb_sz;
+    /* Following for ATA READ/WRITE MULTIPLE (EXT) cmds, normally 0 */
+    int multiple_count = 0;
+    int protocol = 4;   /* PIO data-in */
+    int t_length = 2;   /* 0 -> no data transferred, 2 -> sector count */
+    int resid = 0;
+    uint64_t ull;
+    struct sg_scsi_sense_hdr ssh;
+    uint8_t inBuff[ID_RESPONSE_LEN];
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    uint8_t ata_return_desc[16] SG_C_CPP_ZERO_INIT;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t apt12_cdb[SAT_ATA_PASS_THROUGH12_LEN] =
+                {SAT_ATA_PASS_THROUGH12, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0};
+    uint8_t apt32_cdb[SAT_ATA_PASS_THROUGH32_LEN] SG_C_CPP_ZERO_INIT;
+    const unsigned short * usp;
+
+    sb_sz = sizeof(sense_buffer);
+    ok = false;
+    switch (cdb_len) {
+    case SAT_ATA_PASS_THROUGH32_LEN:    /* SAT-4 revision 5 or later */
+        /* Prepare SCSI ATA PASS-THROUGH COMMAND(32) command */
+        sg_put_unaligned_be16(1, apt32_cdb + 22);     /* count=1 */
+        apt32_cdb[25] = (do_packet ? ATA_IDENTIFY_PACKET_DEVICE :
+                                       ATA_IDENTIFY_DEVICE);
+        apt32_cdb[10] = (multiple_count << 5) | (protocol << 1);
+        if (extend)
+            apt32_cdb[10] |= 0x1;
+        apt32_cdb[11] = t_length;
+        if (ck_cond)
+            apt32_cdb[11] |= 0x20;
+        if (t_type)
+            apt32_cdb[11] |= 0x10;
+        if (t_dir)
+            apt32_cdb[11] |= 0x8;
+        if (byte_block)
+            apt32_cdb[11] |= 0x4;
+        /* following call takes care of all bytes below offset 10 in cdb */
+        res = sg_ll_ata_pt(sg_fd, apt32_cdb, cdb_len, DEF_TIMEOUT, inBuff,
+                           NULL /* doutp */, ID_RESPONSE_LEN, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, verbose);
+        break;
+    case SAT_ATA_PASS_THROUGH16_LEN:
+        /* Prepare SCSI ATA PASS-THROUGH COMMAND(16) command */
+        apt_cdb[6] = 1;   /* sector count */
+        apt_cdb[14] = (do_packet ? ATA_IDENTIFY_PACKET_DEVICE :
+                                     ATA_IDENTIFY_DEVICE);
+        apt_cdb[1] = (multiple_count << 5) | (protocol << 1);
+        if (extend)
+            apt_cdb[1] |= 0x1;
+        apt_cdb[2] = t_length;
+        if (ck_cond)
+            apt_cdb[2] |= 0x20;
+        if (t_type)
+            apt_cdb[2] |= 0x10;
+        if (t_dir)
+            apt_cdb[2] |= 0x8;
+        if (byte_block)
+            apt_cdb[2] |= 0x4;
+        res = sg_ll_ata_pt(sg_fd, apt_cdb, cdb_len, DEF_TIMEOUT, inBuff,
+                           NULL /* doutp */, ID_RESPONSE_LEN, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, verbose);
+        break;
+    case SAT_ATA_PASS_THROUGH12_LEN:
+        /* Prepare SCSI ATA PASS-THROUGH COMMAND(12) command */
+        apt12_cdb[4] = 1;   /* sector count */
+        apt12_cdb[9] = (do_packet ? ATA_IDENTIFY_PACKET_DEVICE :
+                                      ATA_IDENTIFY_DEVICE);
+        apt12_cdb[1] = (multiple_count << 5) | (protocol << 1);
+        apt12_cdb[2] = t_length;
+        if (ck_cond)
+            apt12_cdb[2] |= 0x20;
+        if (t_type)
+            apt12_cdb[2] |= 0x10;
+        if (t_dir)
+            apt12_cdb[2] |= 0x8;
+        if (byte_block)
+            apt12_cdb[2] |= 0x4;
+        res = sg_ll_ata_pt(sg_fd, apt12_cdb, cdb_len, DEF_TIMEOUT, inBuff,
+                           NULL /* doutp */, ID_RESPONSE_LEN, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, verbose);
+        break;
+    default:
+        pr2serr("%s: bad cdb_len=%d\n", __func__, cdb_len);
+        return -1;
+    }
+    if (0 == res) {
+        ok = true;
+        if (verbose > 2)
+            pr2serr("command completed with SCSI GOOD status\n");
+    } else if ((res > 0) && (res & SAM_STAT_CHECK_CONDITION)) {
+        if (verbose > 1) {
+            pr2serr("ATA pass-through:\n");
+            sg_print_sense(NULL, sense_buffer, sb_sz,
+                           ((verbose > 2) ? 1 : 0));
+        }
+        if (sg_scsi_normalize_sense(sense_buffer, sb_sz, &ssh)) {
+            switch (ssh.sense_key) {
+            case SPC_SK_ILLEGAL_REQUEST:
+                if ((0x20 == ssh.asc) && (0x0 == ssh.ascq)) {
+                    ret = SG_LIB_CAT_INVALID_OP;
+                    if (verbose < 2)
+                        pr2serr("ATA PASS-THROUGH (%d) not supported\n",
+                                cdb_len);
+                } else {
+                    ret = SG_LIB_CAT_ILLEGAL_REQ;
+                    if (verbose < 2)
+                        pr2serr("ATA PASS-THROUGH (%d), bad field in cdb\n",
+                                cdb_len);
+                }
+                return ret;
+            case SPC_SK_NO_SENSE:
+            case SPC_SK_RECOVERED_ERROR:
+                if ((0x0 == ssh.asc) &&
+                    (ASCQ_ATA_PT_INFO_AVAILABLE == ssh.ascq)) {
+                    if (0x72 == ssh.response_code) {
+                        if (SAT_ATA_RETURN_DESC != ata_return_desc[0]) {
+                            if (verbose)
+                                pr2serr("did not find ATA Return (sense) "
+                                        "Descriptor\n");
+                            return SG_LIB_CAT_RECOVERED;
+                        }
+                        got_ard = true;
+                        break;
+                    } else if (0x70 == ssh.response_code) {
+                        got_fixsense = true;
+                        break;
+                    } else {
+                        if (verbose < 2)
+                            pr2serr("ATA PASS-THROUGH (%d), unexpected  "
+                                    "response_code=0x%x\n", ssh.response_code,
+                                    cdb_len);
+                        return SG_LIB_CAT_RECOVERED;
+                    }
+                } else if (SPC_SK_RECOVERED_ERROR == ssh.sense_key)
+                    return SG_LIB_CAT_RECOVERED;
+                else {
+                    if ((0x0 == ssh.asc) && (0x0 == ssh.ascq))
+                        break;
+                    return SG_LIB_CAT_SENSE;
+                }
+            case SPC_SK_UNIT_ATTENTION:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), Unit Attention detected\n",
+                            cdb_len);
+                return SG_LIB_CAT_UNIT_ATTENTION;
+            case SPC_SK_NOT_READY:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), device not ready\n",
+                            cdb_len);
+                return SG_LIB_CAT_NOT_READY;
+            case SPC_SK_MEDIUM_ERROR:
+            case SPC_SK_HARDWARE_ERROR:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), medium or hardware "
+                            "error\n", cdb_len);
+                return SG_LIB_CAT_MEDIUM_HARD;
+            case SPC_SK_ABORTED_COMMAND:
+                if (0x10 == ssh.asc) {
+                    pr2serr("Aborted command: protection information\n");
+                    return SG_LIB_CAT_PROTECTION;
+                } else {
+                    pr2serr("Aborted command: try again with%s '-p' option\n",
+                            (do_packet ? "out" : ""));
+                    return SG_LIB_CAT_ABORTED_COMMAND;
+                }
+            case SPC_SK_DATA_PROTECT:
+                pr2serr("ATA PASS-THROUGH (%d): data protect, read only "
+                        "media?\n", cdb_len);
+                return SG_LIB_CAT_DATA_PROTECT;
+            default:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), some sense data, use "
+                            "'-v' for more information\n", cdb_len);
+                return SG_LIB_CAT_SENSE;
+            }
+        } else {
+            pr2serr("CHECK CONDITION without response code ??\n");
+            return SG_LIB_CAT_SENSE;
+        }
+        if (0x72 != (sense_buffer[0] & 0x7f)) {
+            pr2serr("expected descriptor sense format, response code=0x%x\n",
+                    sense_buffer[0]);
+            return SG_LIB_CAT_MALFORMED;
+        }
+    } else if (res > 0) {
+        if (SAM_STAT_RESERVATION_CONFLICT == res) {
+            pr2serr("SCSI status: RESERVATION CONFLICT\n");
+            return SG_LIB_CAT_RES_CONFLICT;
+        } else {
+            pr2serr("Unexpected SCSI status=0x%x\n", res);
+            return SG_LIB_CAT_MALFORMED;
+        }
+    } else {
+        pr2serr("ATA pass-through (%d) failed\n", cdb_len);
+        if (verbose < 2)
+            pr2serr("    try adding '-v' for more information\n");
+        return -1;
+    }
+
+    if ((SAT_ATA_RETURN_DESC == ata_return_desc[0]) && (! got_ard))
+        pr2serr("Seem to have got ATA Result Descriptor but it was not "
+                "indicated\n");
+    if (got_ard) {
+        if (ata_return_desc[3] & 0x4) {
+                pr2serr("error indication in returned FIS: aborted command\n");
+                pr2serr("    try again with%s '-p' option\n",
+                        (do_packet ? "out" : ""));
+                return SG_LIB_CAT_ABORTED_COMMAND;
+        }
+        ok = true;
+    }
+    if (got_fixsense) {
+        if (0x4 & sense_buffer[3]) { /* Error is MSB of Info field */
+                pr2serr("error indication in returned FIS: aborted command\n");
+                pr2serr("    try again with%s '-p' option\n",
+                        (do_packet ? "out" : ""));
+                return SG_LIB_CAT_ABORTED_COMMAND;
+        }
+        ok = true;
+    }
+
+    if (ok) { /* output result if it is available */
+        if (do_raw)
+            dStrRaw(inBuff, 512);
+        else if (0 == do_hex) {
+            if (do_ident) {
+                usp = (const unsigned short *)inBuff;
+                ull = 0;
+                for (j = 0; j < 4; ++j) {
+                    if (j > 0)
+                        ull <<= 16;
+                    ull |= usp[108 + j];
+                }
+                printf("0x%016" PRIx64 "\n", ull);
+            } else {
+                printf("Response for IDENTIFY %sDEVICE ATA command:\n",
+                       (do_packet ? "PACKET " : ""));
+                dWordHex((const unsigned short *)inBuff, 256, 0,
+                         sg_is_big_endian());
+            }
+        } else if (1 == do_hex)
+            hex2stdout(inBuff, 512, 0);
+        else if (2 == do_hex)
+            dWordHex((const unsigned short *)inBuff, 256, 0,
+                     sg_is_big_endian());
+        else if (3 == do_hex) /* '-HHH' suitable for "hdparm --Istdin" */
+            dWordHex((const unsigned short *)inBuff, 256, -2,
+                     sg_is_big_endian());
+        else     /* '-HHHH' hex bytes only */
+            hex2stdout(inBuff, 512, -1);
+    }
+    return 0;
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool do_packet = false;
+    bool do_ident = false;
+    bool do_raw = false;
+    bool o_readonly = false;
+    bool ck_cond = false;    /* set to true to read register(s) back */
+    bool extend = false;    /* set to true to send 48 bit LBA with command */
+    bool verbose_given = false;
+    bool version_given = false;
+    int c, res;
+    int sg_fd = -1;
+    int cdb_len = SAT_ATA_PASS_THROUGH16_LEN;
+    int do_hex = 0;
+    int verbose = 0;
+    int ret = 0;
+    const char * device_name = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "cehHil:prRvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            ck_cond = true;
+            break;
+        case 'e':
+            extend = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'i':
+            do_ident = true;
+            break;
+        case 'l':
+            cdb_len = sg_get_num(optarg);
+            switch (cdb_len) {
+            case 12:
+            case 16:
+            case 32:
+                break;
+            default:
+                pr2serr("argument to '--len' should be 12, 16 or 32\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'p':
+            do_packet = true;
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            o_readonly = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return 1;
+    }
+    if (do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    if ((sg_fd = sg_cmds_open_device(device_name, o_readonly, verbose)) < 0) {
+        if (verbose)
+            pr2serr("error opening file: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    ret = do_identify_dev(sg_fd, do_packet, cdb_len, ck_cond, extend,
+                          do_ident, do_hex, do_raw, verbose);
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_sat_identify failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_sat_phy_event.c b/src/sg_sat_phy_event.c
new file mode 100644
index 0000000..b7e9d53
--- /dev/null
+++ b/src/sg_sat_phy_event.c
@@ -0,0 +1,534 @@
+/*
+ * Copyright (c) 2006-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <string.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.15 20220425";
+
+/* This program uses a ATA PASS-THROUGH SCSI command. This usage is
+ * defined in the SCSI to ATA Translation (SAT) drafts and standards.
+ * See https://www.t10.org for drafts. SAT is a standard: SAT ANSI INCITS
+ * 431-2007 (draft prior to that is sat-r09.pdf). SAT-2 is also a
+ * standard: SAT-2 ANSI INCITS 465-2010 and the draft prior to that is
+ * sat2r09.pdf . The SAT-3 project has started and the most recent draft
+ * is sat3r01.pdf .
+ */
+
+/* This program uses a ATA PASS-THROUGH (16 or 12) SCSI command defined
+ * by SAT to package an ATA READ LOG EXT (2Fh) command to fetch
+ * log page 11h. That page contains SATA phy event counters.
+ * For ATA READ LOG EXT command see ATA-8/ACS at www.t13.org .
+ * For SATA phy counter definitions see SATA 2.5 .
+ *
+ * Invocation: see the usage() function below
+ */
+
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_PASS_THROUGH12 0xa1     /* clashes with MMC BLANK command */
+#define SAT_ATA_PASS_THROUGH12_LEN 12
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return (sense) Descriptor */
+#define ASCQ_ATA_PT_INFO_AVAILABLE 0x1d
+
+#define ATA_READ_LOG_EXT 0x2f
+#define SATA_PHY_EVENT_LPAGE 0x11
+#define READ_LOG_EXT_RESPONSE_LEN 512
+
+#define DEF_TIMEOUT 20
+
+#define EBUFF_SZ 256
+
+static struct option long_options[] = {
+        {"ck_cond", no_argument, 0, 'c'},
+        {"ck-cond", no_argument, 0, 'c'},
+        {"extend", no_argument, 0, 'e'},
+        {"hex", no_argument, 0, 'H'},
+        {"ignore", no_argument, 0, 'i'},
+        {"len", no_argument, 0, 'l'},
+        {"raw", no_argument, 0, 'r'},
+        {"reset", no_argument, 0, 'R'},
+        {"help", no_argument, 0, 'h'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+struct phy_event_t {
+    int id;
+    const char * desc;
+};
+
+static struct phy_event_t phy_event_arr[] = {   /* SATA 2.5 section 13.7.2 */
+    {0x1, "Command failed and ICRC error bit set in Error register"}, /* M */
+    {0x2, "R_ERR(p) response for data FIS"},
+    {0x3, "R_ERR(p) response for device-to-host data FIS"},
+    {0x4, "R_ERR(p) response for host-to-device data FIS"},
+    {0x5, "R_ERR(p) response for non-data FIS"},
+    {0x6, "R_ERR(p) response for device-to-host non-data FIS"},
+    {0x7, "R_ERR(p) response for host-to-device non-data FIS"},
+    {0x8, "Device-to-host non-data FIS retries"},
+    {0x9, "Transition from drive PHYRDY to drive PHYRDYn"},
+    {0xa, "Signature device-to-host register FISes due to COMRESET"}, /* M */
+    {0xb, "CRC errors within host-to-device FIS"},
+    {0xd, "non CRC errors within host-to-device FIS"},
+    {0xf, "R_ERR(p) response for host-to-device data FIS, CRC"},
+    {0x10, "R_ERR(p) response for host-to-device data FIS, non-CRC"},
+    {0x12, "R_ERR(p) response for host-to-device non-data FIS, CRC"},
+    {0x13, "R_ERR(p) response for host-to-device non-data FIS, non-CRC"},
+    {0xc00, "PM: host-to-device non-data FIS, R_ERR(p) due to collision"},
+    {0xc01, "PM: signature register - device-to-host FISes"},
+    {0xc02, "PM: corrupts CRC propagation of device-to-host FISes"},
+    {0x0, NULL},        /* end marker */        /* M(andatory) */
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_sat_phy_event [--ck_cond] [--extend] [--help] [--hex] "
+            "[--ignore]\n"
+            "                        [--len=16|12] [--raw] [--reset] "
+            "[--verbose]\n"
+            "                        [--version] DEVICE\n"
+            "  where:\n"
+            "    --ck_cond|-c    sets ck_cond bit in cdb (def: 0)\n"
+            "    --extend|-e     sets extend bit in cdb (def: 0)\n"
+            "    --help|-h       print this usage message then exit\n"
+            "    --hex|-H        output response in hex bytes, use twice for\n"
+            "                    hex words\n"
+            "    --ignore|-i     ignore identifier names, output id value "
+            "instead\n"
+            "    --len=16|12 | -l 16|12    cdb length: 16 or 12 bytes "
+            "(default: 16)\n"
+            "    --raw|-r        output response in binary to stdout\n"
+            "    --reset|-R      reset counters (after read)\n"
+            "    --verbose|-v    increase verbosity\n"
+            "    --version|-V    print version string then exit\n\n"
+            "Sends an ATA READ LOG EXT command via a SAT pass through to "
+            "fetch\nlog page 11h which contains SATA phy event counters\n");
+}
+
+static const char *
+find_phy_desc(int id)
+{
+    const struct phy_event_t * pep;
+
+    for (pep = phy_event_arr; pep->desc; ++pep) {
+        if ((id & 0xfff) == pep->id)
+            return pep->desc;
+    }
+    return NULL;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k =0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* ATA READ LOG EXT command [2Fh, PIO data-in] */
+/* N.B. "log_addr" is the log page number, "page_in_log" is usually 0 */
+static int
+do_read_log_ext(int sg_fd, int log_addr, int page_in_log, int feature,
+                int blk_count, void * resp, int mx_resp_len, int cdb_len,
+                bool ck_cond, bool extend, int do_hex, bool do_raw,
+                int verbose)
+{
+    /* Following for ATA READ/WRITE MULTIPLE (EXT) cmds, normally 0 */
+#if 0
+    bool t_type = false;/* false -> 512 byte LBs, true -> device's LB size */
+#endif
+    bool t_dir = true;  /* false -> to device, 1 -> from device */
+    bool byte_block = true; /* false -> bytes, true -> 512 byte blocks (if
+                               t_type=false) */
+    bool got_ard = false;    /* got ATA result descriptor */
+    bool ok;
+    int res, ret;
+    int multiple_count = 0;
+    int protocol = 4;   /* PIO data-in */
+    int t_length = 2;   /* 0 -> no data transferred, 2 -> sector count */
+    int resid = 0;
+    int sb_sz;
+    struct sg_scsi_sense_hdr ssh;
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    uint8_t ata_return_desc[16] SG_C_CPP_ZERO_INIT;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t apt12_cdb[SAT_ATA_PASS_THROUGH12_LEN] =
+                {SAT_ATA_PASS_THROUGH12, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0};
+
+    sb_sz = sizeof(sense_buffer);
+    ok = false;
+    if (SAT_ATA_PASS_THROUGH16_LEN == cdb_len) {
+        /* Prepare ATA PASS-THROUGH COMMAND (16) command */
+        apt_cdb[3] = (feature >> 8) & 0xff;   /* feature(15:8) */
+        apt_cdb[4] = feature & 0xff;          /* feature(7:0) */
+        apt_cdb[5] = (blk_count >> 8) & 0xff; /* sector_count(15:8) */
+        apt_cdb[6] = blk_count & 0xff;        /* sector_count(7:0) */
+        apt_cdb[8] = log_addr & 0xff;  /* lba_low(7:0) == LBA(7:0) */
+        apt_cdb[9] = (page_in_log >> 8) & 0xff;
+                /* lba_mid(15:8) == LBA(39:32) */
+        apt_cdb[10] = page_in_log & 0xff; /* lba_mid(7:0) == LBA(15:8) */
+        apt_cdb[14] = ATA_READ_LOG_EXT;
+        apt_cdb[1] = (multiple_count << 5) | (protocol << 1);
+        if (extend)
+            apt_cdb[1] |= 0x1;
+        apt_cdb[2] = t_length;
+        if (ck_cond)
+            apt_cdb[2] |= 0x20;
+#if 0
+        if (t_type)
+            apt_cdb[2] |= 0x10;
+#endif
+        if (t_dir)
+            apt_cdb[2] |= 0x8;
+        if (byte_block)
+            apt_cdb[2] |= 0x4;
+        res = sg_ll_ata_pt(sg_fd, apt_cdb, cdb_len, DEF_TIMEOUT, resp,
+                           NULL /* doutp */, mx_resp_len, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, verbose);
+    } else {
+        /* Prepare ATA PASS-THROUGH COMMAND (12) command */
+        apt12_cdb[3] = feature & 0xff;        /* feature(7:0) */
+        apt12_cdb[4] = blk_count & 0xff;        /* sector_count(7:0) */
+        apt12_cdb[5] = log_addr & 0xff;  /* lba_low(7:0) == LBA(7:0) */
+        apt12_cdb[6] = page_in_log & 0xff; /* lba_mid(7:0) == LBA(15:8) */
+        apt12_cdb[9] = ATA_READ_LOG_EXT;
+        apt12_cdb[1] = (multiple_count << 5) | (protocol << 1);
+        apt12_cdb[2] = t_length;
+        if (ck_cond)
+            apt12_cdb[2] |= 0x20;
+#if 0
+        if (t_type)
+            apt12_cdb[2] |= 0x10;
+#endif
+        if (t_dir)
+            apt12_cdb[2] |= 0x8;
+        if (byte_block)
+            apt12_cdb[2] |= 0x4;
+        res = sg_ll_ata_pt(sg_fd, apt12_cdb, cdb_len, DEF_TIMEOUT, resp,
+                           NULL /* doutp */, mx_resp_len, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, verbose);
+    }
+    if (0 == res) {
+        ok = true;
+        if (verbose > 2)
+            pr2serr("command completed with SCSI GOOD status\n");
+    } else if ((res > 0) && (res & SAM_STAT_CHECK_CONDITION)) {
+        if (verbose > 1) {
+            pr2serr("ATA pass through:\n");
+            sg_print_sense(NULL, sense_buffer, sb_sz,
+                           ((verbose > 2) ? 1 : 0));
+        }
+        if (sg_scsi_normalize_sense(sense_buffer, sb_sz, &ssh)) {
+            switch (ssh.sense_key) {
+            case SPC_SK_ILLEGAL_REQUEST:
+                if ((0x20 == ssh.asc) && (0x0 == ssh.ascq)) {
+                    ret = SG_LIB_CAT_INVALID_OP;
+                    if (verbose < 2)
+                        pr2serr("ATA PASS-THROUGH (%d) not supported\n",
+                                cdb_len);
+                } else {
+                    ret = SG_LIB_CAT_ILLEGAL_REQ;
+                    if (verbose < 2)
+                        pr2serr("ATA PASS-THROUGH (%d), bad field in cdb\n",
+                                cdb_len);
+                }
+                return ret;
+            case SPC_SK_NO_SENSE:
+            case SPC_SK_RECOVERED_ERROR:
+                if ((0x0 == ssh.asc) &&
+                    (ASCQ_ATA_PT_INFO_AVAILABLE == ssh.ascq)) {
+                    if (SAT_ATA_RETURN_DESC != ata_return_desc[0]) {
+                        if (verbose)
+                            pr2serr("did not find ATA Return (sense) "
+                                    "Descriptor\n");
+                        return SG_LIB_CAT_RECOVERED;
+                    }
+                    got_ard = true;
+                    break;
+                } else if (SPC_SK_RECOVERED_ERROR == ssh.sense_key)
+                    return SG_LIB_CAT_RECOVERED;
+                else {
+                    if ((0x0 == ssh.asc) && (0x0 == ssh.ascq))
+                        break;
+                    return SG_LIB_CAT_SENSE;
+                }
+            case SPC_SK_UNIT_ATTENTION:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), Unit Attention detected\n",
+                            cdb_len);
+                return SG_LIB_CAT_UNIT_ATTENTION;
+            case SPC_SK_NOT_READY:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), device not ready\n",
+                            cdb_len);
+                return SG_LIB_CAT_NOT_READY;
+            case SPC_SK_MEDIUM_ERROR:
+            case SPC_SK_HARDWARE_ERROR:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), medium or hardware "
+                            "error\n", cdb_len);
+                return SG_LIB_CAT_MEDIUM_HARD;
+            case SPC_SK_ABORTED_COMMAND:
+                if (0x10 == ssh.asc) {
+                    pr2serr("Aborted command: protection information\n");
+                    return SG_LIB_CAT_PROTECTION;
+                } else {
+                    pr2serr("Aborted command\n");
+                    return SG_LIB_CAT_ABORTED_COMMAND;
+                }
+            case SPC_SK_DATA_PROTECT:
+                pr2serr("ATA PASS-THROUGH (%d): data protect, read only "
+                        "media?\n", cdb_len);
+                return SG_LIB_CAT_DATA_PROTECT;
+            default:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), some sense data, use "
+                            "'-v' for more information\n", cdb_len);
+                return SG_LIB_CAT_SENSE;
+            }
+        } else {
+            pr2serr("CHECK CONDITION without response code ??\n");
+            return SG_LIB_CAT_SENSE;
+        }
+        if (0x72 != (sense_buffer[0] & 0x7f)) {
+            pr2serr("expected descriptor sense format, response code=0x%x\n",
+                    sense_buffer[0]);
+            return SG_LIB_CAT_MALFORMED;
+        }
+    } else if (res > 0) {
+        if (SAM_STAT_RESERVATION_CONFLICT == res) {
+            pr2serr("SCSI status: RESERVATION CONFLICT\n");
+            return SG_LIB_CAT_RES_CONFLICT;
+        } else {
+            pr2serr("Unexpected SCSI status=0x%x\n", res);
+            return SG_LIB_CAT_MALFORMED;
+        }
+    } else {
+        pr2serr("ATA pass through (%d) failed\n", cdb_len);
+        if (verbose < 2)
+            pr2serr("    try adding '-v' for more information\n");
+        return -1;
+    }
+
+    if ((SAT_ATA_RETURN_DESC == ata_return_desc[0]) && (! got_ard))
+        pr2serr("Seem to have got ATA Result Descriptor but it was not "
+                "indicated\n");
+    if (got_ard) {
+        if (ata_return_desc[3] & 0x4) {
+                pr2serr("error indication in returned FIS: aborted command\n");
+                return SG_LIB_CAT_ABORTED_COMMAND;
+        }
+        ok = true;
+    }
+
+    if (ok) { /* output result if ok and --hex or --raw given */
+        if (do_raw)
+            dStrRaw((const uint8_t *)resp, mx_resp_len);
+        else if (1 == do_hex)
+            hex2stdout((const uint8_t *)resp, mx_resp_len, 0);
+        else if (do_hex > 1)
+            dWordHex((const unsigned short *)resp, mx_resp_len / 2, 0,
+                     sg_is_big_endian());
+    }
+    return 0;
+}
+
+
+int main(int argc, char * argv[])
+{
+    bool ck_cond = false;   /* set to true to read register(s) back */
+    bool extend = false;
+    bool ignore = false;
+    bool raw = false;
+    bool reset = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int sg_fd, c, k, j, res, id, len, vendor, err;
+    char * device_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t inBuff[READ_LOG_EXT_RESPONSE_LEN];
+    int cdb_len = 16;
+    int hex = 0;
+    int verbose = 0;
+    int ret = 0;
+    uint64_t ull;
+    const char * cp;
+
+    memset(inBuff, 0, sizeof(inBuff));
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "cehHil:rRvV",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            ck_cond = true;
+            break;
+        case 'e':
+            extend = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            exit(0);
+        case 'H':
+            ++hex;
+            break;
+        case 'i':
+            ignore = true;
+            break;
+        case 'l':
+            cdb_len = sg_get_num(optarg);
+            if (! ((cdb_len == 12) || (cdb_len == 16))) {
+                pr2serr("argument to '--len' should be 12 or 16\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'r':
+            raw = true;
+            break;
+        case 'R':
+            reset = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+    if (0 == device_name) {
+        pr2serr("no DEVICE name detected\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    if ((sg_fd = open(device_name, O_RDWR)) < 0) {
+        err = errno;
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_sat_phy_event: error opening file: %s", device_name);
+        perror(ebuff);
+        return sg_convert_errno(err);
+    }
+    ret = do_read_log_ext(sg_fd, SATA_PHY_EVENT_LPAGE,
+                          0 /* page_in_log */,
+                          (reset ? 1 : 0) /* feature */,
+                          1 /* blk_count */, inBuff,
+                          READ_LOG_EXT_RESPONSE_LEN, cdb_len, ck_cond,
+                          extend, hex, raw, verbose);
+
+    if ((0 == ret) && (0 == hex) && (! raw)) {
+        printf("SATA phy event counters:\n");
+        for (k = 4; k < 512; k += (len + 2)) {
+            id = (inBuff[k + 1] << 8) + inBuff[k];
+            if (0 == id)
+                break;
+            len = ((id >> 12) & 0x7) * 2;
+            vendor = !!(id & 0x8000);
+            id = id & 0xfff;
+            ull = 0;
+            for (j = len - 1; j >= 0; --j) {
+                if (j < (len - 1))
+                    ull <<= 8;
+                ull |= inBuff[k + 2 + j];
+            }
+            cp = NULL;
+            if ((0 == vendor) && (! ignore))
+                cp = find_phy_desc(id);
+            if (cp)
+                printf("  %s: %" PRIu64 "\n", cp, ull);
+            else
+                printf("  id=0x%x, vendor=%d, data_len=%d, "
+                       "val=%" PRIu64 "\n", id, vendor, len, ull);
+        }
+    }
+
+    res = close(sg_fd);
+    if (res < 0) {
+        err = errno;
+        pr2serr("close error: %s\n", safe_strerror(err));
+        if (0 == ret)
+            ret = sg_convert_errno(err);
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_sat_read_gplog.c b/src/sg_sat_read_gplog.c
new file mode 100644
index 0000000..55c164e
--- /dev/null
+++ b/src/sg_sat_read_gplog.c
@@ -0,0 +1,495 @@
+/*
+ * Copyright (c) 2014-2022 Hannes Reinecke, SUSE Linux GmbH.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* This program uses a ATA PASS-THROUGH SCSI command. This usage is
+ * defined in the SCSI to ATA Translation (SAT) drafts and standards.
+ * See https://www.t10.org for drafts. SAT is a standard: SAT ANSI INCITS
+ * 431-2007 (draft prior to that is sat-r09.pdf). SAT-2 is also a
+ * standard: SAT-2 ANSI INCITS 465-2010 and the draft prior to that is
+ * sat2r09.pdf . The SAT-3 project has started and the most recent draft
+ * is sat3r01.pdf .
+ */
+
+/* This program performs a ATA PASS-THROUGH (16) SCSI command in order
+ * to perform an ATA READ LOG EXT or ATA READ LOG DMA EXT command.
+ *
+ * See man page (sg_sat_read_gplog.8) for details.
+ */
+
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_PASS_THROUGH12 0xa1     /* clashes with MMC BLANK command */
+#define SAT_ATA_PASS_THROUGH12_LEN 12
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return (sense) Descriptor */
+#define ASCQ_ATA_PT_INFO_AVAILABLE 0x1d
+
+#define ATA_READ_LOG_EXT 0x2f
+#define ATA_READ_LOG_DMA_EXT 0x47
+
+#define DEF_TIMEOUT 20
+
+static const char * version_str = "1.23 20220917";
+
+struct opts_t {
+    bool ck_cond;
+    bool rdonly;
+    int cdb_len;
+    int count;
+    int hex;
+    int la;             /* log address */
+    int pn;             /* page number within log address */
+    int verbose;
+    const char * device_name;
+};
+
+static struct option long_options[] = {
+    {"count", required_argument, 0, 'c'},
+    {"ck_cond", no_argument, 0, 'C'},
+    {"ck-cond", no_argument, 0, 'C'},
+    {"dma", no_argument, 0, 'd'},
+    {"help", no_argument, 0, 'h'},
+    {"hex", no_argument, 0, 'H'},
+    {"len", required_argument, 0, 'l'},
+    {"log", required_argument, 0, 'L'},
+    {"page", required_argument, 0, 'p'},
+    {"readonly", no_argument, 0, 'r'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+          "sg_sat_read_gplog [--ck_cond] [--count=CO] [--dma] [--help]\n"
+          "                         [--hex] [--len=16|12] [--log=LA] "
+          "[--page=PN]\n"
+          "                         [--readonly] [--verbose] [--version] "
+          "DEVICE\n"
+          "  where:\n"
+          "    --ck_cond | -C          set ck_cond field in pass-through "
+          "(def: 0)\n"
+          "    --count=CO | -c CO      block count (def: 1)\n"
+          "    --dma | -d              Use READ LOG DMA EXT (def: READ LOG "
+          "EXT)\n"
+          "    --help | -h             output this usage message\n"
+          "    --hex | -H              output response in hex bytes, -HH "
+          "yields hex\n"
+          "                            words + ASCII (def), -HHH hex words "
+          "only\n"
+          "    --len=16|12 | -l 16|12    cdb length: 16 or 12 bytes "
+          "(def: 16)\n"
+          "    --log=LA | -L LA        Log address to be read (def: 0)\n"
+          "    --page=PN|-p PN         Log page number within address (def: "
+          "0)\n"
+          "    --readonly | -r         open DEVICE read-only (def: "
+          "read-write)\n"
+          "    --verbose | -v          increase verbosity\n"
+          "                            recommended if DEVICE is ATA disk\n"
+          "    --version | -V          print version string and exit\n\n"
+          "Sends an ATA READ LOG EXT (or READ LOG DMA EXT) command via a "
+          "SAT pass\nthrough to fetch a General Purpose (GP) log page. Each "
+          "page is accessed\nvia a log address and then a page number "
+          "within that address: LA,PN .\n"
+          "By default the output is the response in hex (16 bit) words.\n"
+           );
+}
+
+static int
+do_read_gplog(int sg_fd, int ata_cmd, uint8_t * inbuff,
+              const struct opts_t * op)
+{
+    const bool extend = true;
+    const bool t_dir = true; /* false -> to device, true -> from device */
+    const bool byte_block = true;/* false -> bytes, true -> 512 byte blocks */
+    const bool t_type = false; /* false -> 512 byte blocks, true -> logical
+                                  sectors */
+    bool got_ard = false;      /* got ATA result descriptor */
+    int res, ret;
+    int protocol;
+    int t_length = 2;   /* 0 -> no data transferred, 2 -> sector count */
+    int resid = 0;
+    int sb_sz;
+    struct sg_scsi_sense_hdr ssh;
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    uint8_t ata_return_desc[16] SG_C_CPP_ZERO_INIT;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t apt12_cdb[SAT_ATA_PASS_THROUGH12_LEN] =
+                {SAT_ATA_PASS_THROUGH12, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0};
+    char cmd_name[32];
+
+    snprintf(cmd_name, sizeof(cmd_name), "ATA PASS-THROUGH (%d)",
+             op->cdb_len);
+    if (ata_cmd == ATA_READ_LOG_DMA_EXT) {
+        protocol = 6; /* DMA */
+    } else {
+        protocol = 4; /* PIO Data-In */
+    }
+    sb_sz = sizeof(sense_buffer);
+    memset(inbuff, 0, op->count * 512);
+    if (op->verbose > 1)
+        pr2serr("Building ATA READ LOG%s EXT command; la=0x%x, pn=0x%x\n",
+                ((ata_cmd == ATA_READ_LOG_DMA_EXT) ? " DMA" : ""), op->la,
+                op->pn);
+    if (op->cdb_len == 16) {
+        /* Prepare ATA PASS-THROUGH COMMAND (16) command */
+        apt_cdb[14] = ata_cmd;
+        sg_put_unaligned_be16((uint16_t)op->count, apt_cdb + 5);
+        apt_cdb[8] = op->la;
+        sg_put_unaligned_be16((uint16_t)op->pn, apt_cdb + 9);
+        apt_cdb[1] = (protocol << 1) | extend;
+        if (extend)
+            apt_cdb[1] |= 0x1;
+        apt_cdb[2] = t_length;
+        if (op->ck_cond)
+            apt_cdb[2] |= 0x20;
+        if (t_type)
+            apt_cdb[2] |= 0x10;
+        if (t_dir)
+            apt_cdb[2] |= 0x8;
+        if (byte_block)
+            apt_cdb[2] |= 0x4;
+        res = sg_ll_ata_pt(sg_fd, apt_cdb, op->cdb_len, DEF_TIMEOUT, inbuff,
+                           NULL, op->count * 512, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, op->verbose);
+    } else {
+        /* Prepare ATA PASS-THROUGH COMMAND (12) command */
+        /* Cannot map upper 8 bits of the pn since no LBA (39:32) field */
+        apt12_cdb[9] = ata_cmd;
+        apt12_cdb[4] = op->count;
+        apt12_cdb[5] = op->la;
+        apt12_cdb[6] = op->pn & 0xff;
+        /* apt12_cdb[7] = (op->pn >> 8) & 0xff; */
+        apt12_cdb[1] = (protocol << 1);
+        apt12_cdb[2] = t_length;
+        if (op->ck_cond)
+            apt12_cdb[2] |= 0x20;
+        if (t_type)
+            apt12_cdb[2] |= 0x10;
+        if (t_dir)
+            apt12_cdb[2] |= 0x8;
+        if (byte_block)
+            apt12_cdb[2] |= 0x4;
+        res = sg_ll_ata_pt(sg_fd, apt12_cdb, op->cdb_len, DEF_TIMEOUT,
+                           inbuff, NULL, op->count * 512, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, op->verbose);
+    }
+    if (0 == res) {
+        if (op->verbose > 2)
+            pr2serr("command completed with SCSI GOOD status\n");
+        if ((0 == op->hex) || (2 == op->hex))
+            dWordHex((const unsigned short *)inbuff, op->count * 256, 0,
+                     sg_is_big_endian());
+        else if (1 == op->hex)
+            hex2stdout(inbuff, 512, 0);
+        else if (3 == op->hex)  /* '-HHH' suitable for "hdparm --Istdin" */
+            dWordHex((const unsigned short *)inbuff, 256, -2,
+                     sg_is_big_endian());
+        else    /* '-HHHH' hex bytes only */
+            hex2stdout(inbuff, 512, -1);
+    } else if ((res > 0) && (res & SAM_STAT_CHECK_CONDITION)) {
+        if (op->verbose > 1) {
+            pr2serr("ATA pass through:\n");
+            sg_print_sense(NULL, sense_buffer, sb_sz,
+                           ((op->verbose > 2) ? 1 : 0));
+        }
+        if (sg_scsi_normalize_sense(sense_buffer, sb_sz, &ssh)) {
+            switch (ssh.sense_key) {
+            case SPC_SK_ILLEGAL_REQUEST:
+                if ((0x20 == ssh.asc) && (0x0 == ssh.ascq)) {
+                    ret = SG_LIB_CAT_INVALID_OP;
+                    if (op->verbose < 2)
+                        pr2serr("%s not supported\n", cmd_name);
+                } else {
+                    ret = SG_LIB_CAT_ILLEGAL_REQ;
+                    if (op->verbose < 2)
+                        pr2serr("%s, bad field in cdb\n", cmd_name);
+                }
+                return ret;
+            case SPC_SK_NO_SENSE:
+            case SPC_SK_RECOVERED_ERROR:
+                if ((0x0 == ssh.asc) &&
+                    (ASCQ_ATA_PT_INFO_AVAILABLE == ssh.ascq)) {
+                    if (SAT_ATA_RETURN_DESC != ata_return_desc[0]) {
+                        if (op->verbose)
+                            pr2serr("did not find ATA Return (sense) "
+                                    "Descriptor\n");
+                        return SG_LIB_CAT_RECOVERED;
+                    }
+                    got_ard = true;
+                    break;
+                } else if (SPC_SK_RECOVERED_ERROR == ssh.sense_key)
+                    return SG_LIB_CAT_RECOVERED;
+                else {
+                    if ((0x0 == ssh.asc) && (0x0 == ssh.ascq))
+                        break;
+                    return SG_LIB_CAT_SENSE;
+                }
+            case SPC_SK_UNIT_ATTENTION:
+                if (op->verbose < 2)
+                    pr2serr("%s, Unit Attention detected\n", cmd_name);
+                return SG_LIB_CAT_UNIT_ATTENTION;
+            case SPC_SK_NOT_READY:
+                if (op->verbose < 2)
+                    pr2serr("%s, device not ready\n", cmd_name);
+                return SG_LIB_CAT_NOT_READY;
+            case SPC_SK_MEDIUM_ERROR:
+            case SPC_SK_HARDWARE_ERROR:
+                if (op->verbose < 2)
+                    pr2serr("%s, medium or hardware error\n", cmd_name);
+                return SG_LIB_CAT_MEDIUM_HARD;
+            case SPC_SK_ABORTED_COMMAND:
+                if (0x10 == ssh.asc) {
+                    pr2serr("Aborted command: protection information\n");
+                    return SG_LIB_CAT_PROTECTION;
+                } else {
+                    pr2serr("Aborted command\n");
+                    return SG_LIB_CAT_ABORTED_COMMAND;
+                }
+            case SPC_SK_DATA_PROTECT:
+                pr2serr("%s: data protect, read only media?\n", cmd_name);
+                return SG_LIB_CAT_DATA_PROTECT;
+            default:
+                if (op->verbose < 2)
+                    pr2serr("%s, some sense data, use '-v' for more "
+                            "information\n", cmd_name);
+                return SG_LIB_CAT_SENSE;
+            }
+        } else {
+            pr2serr("CHECK CONDITION without response code ??\n");
+            return SG_LIB_CAT_SENSE;
+        }
+        if (0x72 != (sense_buffer[0] & 0x7f)) {
+            pr2serr("expected descriptor sense format, response "
+                    "code=0x%x\n", sense_buffer[0]);
+            return SG_LIB_CAT_MALFORMED;
+        }
+    } else if (res > 0) {
+        if (SAM_STAT_RESERVATION_CONFLICT == res) {
+            pr2serr("SCSI status: RESERVATION CONFLICT\n");
+            return SG_LIB_CAT_RES_CONFLICT;
+        } else {
+            pr2serr("Unexpected SCSI status=0x%x\n", res);
+            return SG_LIB_CAT_MALFORMED;
+        }
+    } else {
+        pr2serr("%s failed\n", cmd_name);
+        if (op->verbose < 2)
+            pr2serr("    try adding '-v' for more information\n");
+        return -1;
+    }
+
+    if ((SAT_ATA_RETURN_DESC == ata_return_desc[0]) && (! got_ard))
+        pr2serr("Seem to have got ATA Result Descriptor but it was not "
+                "indicated\n");
+    if (got_ard) {
+        if (ata_return_desc[3] & 0x4) {
+                pr2serr("error indication in returned FIS: aborted "
+                        "command\n");
+                return SG_LIB_CAT_ABORTED_COMMAND;
+        }
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool verbose_given = false;
+    bool version_given = false;
+    int c, ret, res, n;
+    int sg_fd = -1;
+    int ata_cmd = ATA_READ_LOG_EXT;
+    uint8_t *inbuff = NULL;
+    uint8_t *free_inbuff = NULL;
+    struct opts_t opts;
+    struct opts_t * op;
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->cdb_len = SAT_ATA_PASS_THROUGH16_LEN;
+    op->count = 1;
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "c:CdhHl:L:p:rvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            op->count = sg_get_num(optarg);
+            if ((op->count < 1) || (op->count > 0xffff)) {
+                pr2serr("bad argument for '--count'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'C':
+            op->ck_cond = true;
+            break;
+        case 'd':
+            ata_cmd = ATA_READ_LOG_DMA_EXT;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++op->hex;
+            break;
+        case 'l':
+           op->cdb_len = sg_get_num(optarg);
+           if (! ((op->cdb_len == 12) || (op->cdb_len == 16))) {
+                pr2serr("argument to '--len' should be 12 or 16\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'L':
+            op->la = sg_get_num(optarg);
+            if (op->la < 0 || op->la > 0xff) {
+                pr2serr("bad argument for '--log'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'p':
+            op->pn = sg_get_num(optarg);
+            if ((op->pn < 0) || (op->pn > 0xffff)) {
+                pr2serr("bad argument for '--page'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'r':
+            op->rdonly = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        op->verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == op->device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return 1;
+    }
+
+    if ((op->count > 0xff) && (12 == op->cdb_len)) {
+        op->cdb_len = 16;
+        if (op->verbose)
+            pr2serr("Since count > 0xff, forcing cdb length to "
+                    "16\n");
+    }
+
+    n = op->count * 512;
+    inbuff = (uint8_t *)sg_memalign(n, 0, &free_inbuff, op->verbose > 3);
+    if (!inbuff) {
+        pr2serr("Cannot allocate output buffer of size %d\n", n);
+        return SG_LIB_CAT_OTHER;
+    }
+
+    if ((sg_fd = sg_cmds_open_device(op->device_name, op->rdonly,
+                                     op->verbose)) < 0) {
+        if (op->verbose)
+            pr2serr("error opening file: %s: %s\n", op->device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    ret = do_read_gplog(sg_fd, ata_cmd, inbuff, op);
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == op->verbose) {
+        if (! sg_if_can2stderr("sg_sat_read_gplog failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    if (free_inbuff)
+        free(free_inbuff);
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_sat_set_features.c b/src/sg_sat_set_features.c
new file mode 100644
index 0000000..3a6712a
--- /dev/null
+++ b/src/sg_sat_set_features.c
@@ -0,0 +1,463 @@
+/*
+ * Copyright (c) 2006-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_pr2serr.h"
+
+/* This program uses a ATA PASS-THROUGH SCSI command. This usage is
+ * defined in the SCSI to ATA Translation (SAT) drafts and standards.
+ * See https://www.t10.org for drafts. SAT is a standard: SAT ANSI INCITS
+ * 431-2007 (draft prior to that is sat-r09.pdf). SAT-2 is also a
+ * standard: SAT-2 ANSI INCITS 465-2010 and the draft prior to that is
+ * sat2r09.pdf . The SAT-3 project has started and the most recent draft
+ * is sat3r01.pdf .
+ */
+
+/* This program performs a ATA PASS-THROUGH (16) SCSI command in order
+ * to perform an ATA SET FEATURES command.
+ *
+ * See man page (sg_sat_set_features.8) for details.
+ */
+
+#define SAT_ATA_PASS_THROUGH16 0x85
+#define SAT_ATA_PASS_THROUGH16_LEN 16
+#define SAT_ATA_PASS_THROUGH12 0xa1     /* clashes with MMC BLANK command */
+#define SAT_ATA_PASS_THROUGH12_LEN 12
+#define SAT_ATA_RETURN_DESC 9  /* ATA Return (sense) Descriptor */
+#define ASCQ_ATA_PT_INFO_AVAILABLE 0x1d
+
+#define ATA_SET_FEATURES 0xef
+
+#define DEF_TIMEOUT 20
+
+static const char * version_str = "1.19 20220425";
+
+static struct option long_options[] = {
+    {"count", required_argument, 0, 'c'},
+    {"ck_cond", no_argument, 0, 'C'},
+    {"ck-cond", no_argument, 0, 'C'},
+    {"extended", no_argument, 0, 'e'},
+    {"feature", required_argument, 0, 'f'},
+    {"help", no_argument, 0, 'h'},
+    {"len", required_argument, 0, 'l'},
+    {"lba", required_argument, 0, 'L'},
+    {"readonly", no_argument, 0, 'r'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_sat_set_features [--count=CO] [--ck_cond] [--extended] "
+            "[--feature=FEA]\n"
+            "                           [--help] [--lba=LBA] [--len=16|12] "
+            "[--readonly]\n"
+            "                           [--verbose] [--version] DEVICE\n"
+            "  where:\n"
+            "    --count=CO | -c CO      count field contents (def: 0)\n"
+            "    --ck_cond | -C          set ck_cond field in pass-through "
+            "(def: 0)\n"
+            "    --extended | -e         enable extended lba values\n"
+            "    --feature=FEA|-f FEA    feature field contents\n"
+            "                            (def: 0 (which is reserved))\n"
+            "    --help | -h             output this usage message\n"
+            "    --lba=LBA | -L LBA      LBA field contents (def: 0)\n"
+            "                            meaning depends on sub-command "
+            "(feature)\n"
+            "    --len=16|12 | -l 16|12    cdb length: 16 or 12 bytes "
+            "(def: 16)\n"
+            "    --verbose | -v          increase verbosity\n"
+            "    --readonly | -r         open DEVICE read-only (def: "
+            "read-write)\n"
+            "                            recommended if DEVICE is ATA disk\n"
+            "    --version | -V          print version string and exit\n\n"
+            "Sends an ATA SET FEATURES command via a SAT pass through.\n"
+            "Primary feature code is placed in '--feature=FEA' with "
+            "'--count=CO' and\n"
+            "'--lba=LBA' being auxiliaries for some features.  The arguments "
+            "CO, FEA\n"
+            "and LBA are decimal unless prefixed by '0x' or have a trailing "
+            "'h'.\n"
+            "Example enabling write cache: 'sg_sat_set_feature --feature=2 "
+            "/dev/sdc'\n");
+}
+
+static int
+do_set_features(int sg_fd, int feature, int count, uint64_t lba,
+                int cdb_len, bool ck_cond, bool extend, int verbose)
+{
+    const bool t_type = false;  /* false -> 512 byte blocks, true -> device's
+                                   LB size */
+    const bool t_dir = true;    /* false -> to device, true -> from device */
+    const bool byte_block = true; /* false -> bytes, true -> 512 byte blocks
+                                     (if t_type=false) */
+    bool got_ard = false;       /* got ATA result descriptor */
+    int res, ret;
+    /* Following for ATA READ/WRITE MULTIPLE (EXT) cmds, normally 0 */
+    int multiple_count = 0;
+    int protocol = 3;   /* non-data */
+    int t_length = 0;   /* 0 -> no data transferred, 2 -> sector count */
+    int resid = 0;
+    int sb_sz;
+    struct sg_scsi_sense_hdr ssh;
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    uint8_t ata_return_desc[16] SG_C_CPP_ZERO_INIT;
+    uint8_t apt_cdb[SAT_ATA_PASS_THROUGH16_LEN] =
+                {SAT_ATA_PASS_THROUGH16, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t apt12_cdb[SAT_ATA_PASS_THROUGH12_LEN] =
+                {SAT_ATA_PASS_THROUGH12, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0};
+
+    sb_sz = sizeof(sense_buffer);
+    if (16 == cdb_len) {
+        /* Prepare ATA PASS-THROUGH COMMAND (16) command */
+        apt_cdb[14] = ATA_SET_FEATURES;
+        apt_cdb[4] = feature;
+        apt_cdb[6] = count;
+        apt_cdb[8] = lba & 0xff;
+        apt_cdb[10] = (lba >> 8) & 0xff;
+        apt_cdb[12] = (lba >> 16) & 0xff;
+        apt_cdb[7] = (lba >> 24) & 0xff;
+        apt_cdb[9] = (lba >> 32) & 0xff;
+        apt_cdb[11] = (lba >> 40) & 0xff;
+        apt_cdb[1] = (multiple_count << 5) | (protocol << 1);
+        if (extend)
+           apt_cdb[1] |= 0x1;
+        apt_cdb[2] = t_length;
+        if (ck_cond)
+            apt_cdb[2] |= 0x20;
+        if (t_type)
+            apt_cdb[2] |= 0x10;
+        if (t_dir)
+            apt_cdb[2] |= 0x8;
+        if (byte_block)
+            apt_cdb[2] |= 0x4;
+        res = sg_ll_ata_pt(sg_fd, apt_cdb, cdb_len, DEF_TIMEOUT, NULL,
+                           NULL /* doutp */, 0, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, verbose);
+    } else {
+        /* Prepare ATA PASS-THROUGH COMMAND (12) command */
+        apt12_cdb[9] = ATA_SET_FEATURES;
+        apt12_cdb[3] = feature;
+        apt12_cdb[4] = count;
+        apt12_cdb[5] = lba & 0xff;
+        apt12_cdb[6] = (lba >> 8) & 0xff;
+        apt12_cdb[7] = (lba >> 16) & 0xff;
+        apt12_cdb[1] = (multiple_count << 5) | (protocol << 1);
+        apt12_cdb[2] = t_length;
+        if (ck_cond)
+            apt12_cdb[2] |= 0x20;
+        if (t_type)
+            apt12_cdb[2] |= 0x10;
+        if (t_dir)
+            apt12_cdb[2] |= 0x8;
+        if (byte_block)
+            apt12_cdb[2] |= 0x4;
+        res = sg_ll_ata_pt(sg_fd, apt12_cdb, cdb_len, DEF_TIMEOUT, NULL,
+                           NULL /* doutp */, 0, sense_buffer,
+                           sb_sz, ata_return_desc,
+                           sizeof(ata_return_desc), &resid, verbose);
+    }
+    if (0 == res) {
+        if (verbose > 2)
+            pr2serr("command completed with SCSI GOOD status\n");
+    } else if ((res > 0) && (res & SAM_STAT_CHECK_CONDITION)) {
+        if (verbose > 1) {
+            pr2serr("ATA pass through:\n");
+            sg_print_sense(NULL, sense_buffer, sb_sz,
+                           ((verbose > 2) ? 1 : 0));
+        }
+        if (sg_scsi_normalize_sense(sense_buffer, sb_sz, &ssh)) {
+            switch (ssh.sense_key) {
+            case SPC_SK_ILLEGAL_REQUEST:
+                if ((0x20 == ssh.asc) && (0x0 == ssh.ascq)) {
+                    ret = SG_LIB_CAT_INVALID_OP;
+                    if (verbose < 2)
+                        pr2serr("ATA PASS-THROUGH (%d) not supported\n",
+                                cdb_len);
+                } else {
+                    ret = SG_LIB_CAT_ILLEGAL_REQ;
+                    if (verbose < 2)
+                        pr2serr("ATA PASS-THROUGH (%d), bad field in cdb\n",
+                                cdb_len);
+                }
+                return ret;
+            case SPC_SK_NO_SENSE:
+            case SPC_SK_RECOVERED_ERROR:
+                if ((0x0 == ssh.asc) &&
+                    (ASCQ_ATA_PT_INFO_AVAILABLE == ssh.ascq)) {
+                    if (SAT_ATA_RETURN_DESC != ata_return_desc[0]) {
+                        if (verbose)
+                            pr2serr("did not find ATA Return (sense) "
+                                    "Descriptor\n");
+                        return SG_LIB_CAT_RECOVERED;
+                    }
+                    got_ard = true;
+                    break;
+                } else if (SPC_SK_RECOVERED_ERROR == ssh.sense_key)
+                    return SG_LIB_CAT_RECOVERED;
+                else {
+                    if ((0x0 == ssh.asc) && (0x0 == ssh.ascq))
+                        break;
+                    return SG_LIB_CAT_SENSE;
+                }
+            case SPC_SK_UNIT_ATTENTION:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), Unit Attention detected\n",
+                            cdb_len);
+                return SG_LIB_CAT_UNIT_ATTENTION;
+            case SPC_SK_NOT_READY:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), device not ready\n",
+                            cdb_len);
+                return SG_LIB_CAT_NOT_READY;
+            case SPC_SK_MEDIUM_ERROR:
+            case SPC_SK_HARDWARE_ERROR:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), medium or hardware "
+                            "error\n", cdb_len);
+                return SG_LIB_CAT_MEDIUM_HARD;
+            case SPC_SK_ABORTED_COMMAND:
+                if (0x10 == ssh.asc) {
+                    pr2serr("Aborted command: protection information\n");
+                    return SG_LIB_CAT_PROTECTION;
+                } else {
+                    pr2serr("Aborted command\n");
+                    return SG_LIB_CAT_ABORTED_COMMAND;
+                }
+            case SPC_SK_DATA_PROTECT:
+                pr2serr("ATA PASS-THROUGH (%d): data protect, read only "
+                        "media?\n", cdb_len);
+                return SG_LIB_CAT_DATA_PROTECT;
+            default:
+                if (verbose < 2)
+                    pr2serr("ATA PASS-THROUGH (%d), some sense data, use "
+                            "'-v' for more information\n", cdb_len);
+                return SG_LIB_CAT_SENSE;
+            }
+        } else {
+            pr2serr("CHECK CONDITION without response code ??\n");
+            return SG_LIB_CAT_SENSE;
+        }
+        if (0x72 != (sense_buffer[0] & 0x7f)) {
+            pr2serr("expected descriptor sense format, response code=0x%x\n",
+                    sense_buffer[0]);
+            return SG_LIB_CAT_MALFORMED;
+        }
+    } else if (res > 0) {
+        if (SAM_STAT_RESERVATION_CONFLICT == res) {
+            pr2serr("SCSI status: RESERVATION CONFLICT\n");
+            return SG_LIB_CAT_RES_CONFLICT;
+        } else {
+            pr2serr("Unexpected SCSI status=0x%x\n", res);
+            return SG_LIB_CAT_MALFORMED;
+        }
+    } else {
+        pr2serr("ATA pass through (%d) failed\n", cdb_len);
+        if (verbose < 2)
+            pr2serr("    try adding '-v' for more information\n");
+        return -1;
+    }
+
+    if ((SAT_ATA_RETURN_DESC == ata_return_desc[0]) && (! got_ard))
+        pr2serr("Seem to have got ATA Result Descriptor but it was not "
+                "indicated\n");
+    if (got_ard) {
+        if (ata_return_desc[3] & 0x4) {
+                pr2serr("error indication in returned FIS: aborted command\n");
+                return SG_LIB_CAT_ABORTED_COMMAND;
+        }
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool ck_cond = false;
+    bool extend = false;
+    bool rdonly = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int c, ret, res;
+    int sg_fd = -1;
+    int count = 0;
+    int feature = 0;
+    int verbose = 0;
+    int cdb_len = SAT_ATA_PASS_THROUGH16_LEN;
+    uint64_t lba = 0;
+    const char * device_name = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "c:Cef:hl:L:rvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            count = sg_get_num(optarg);
+            if ((count < 0) || (count > 255)) {
+                pr2serr("bad argument for '--count'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'C':
+            ck_cond = true;
+            break;
+        case 'e':
+            extend = true;
+            break;
+        case 'f':
+            feature = sg_get_num(optarg);
+            if ((feature < 0) || (feature > 255)) {
+                pr2serr("bad argument for '--feature'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'l':
+            cdb_len = sg_get_num(optarg);
+            if (! ((cdb_len == 12) || (cdb_len == 16))) {
+                pr2serr("argument to '--len' should be 12 or 16\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'L':       /* up to 32 bits, allow for 48 bits (less -1) */
+            lba = sg_get_llnum(optarg);
+            if ((uint64_t)-1 == lba) {
+                pr2serr("bad argument for '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'r':
+            rdonly = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return 1;
+    }
+
+    if (lba > 0xffffff) {
+        if (12 == cdb_len) {
+            cdb_len = 16;
+            if (verbose)
+                pr2serr("Since lba > 0xffffff, forcing cdb length to 16\n");
+        }
+        if (16 == cdb_len) {
+            if (! extend) {
+                extend = true;
+                if (verbose)
+                    pr2serr("Since lba > 0xffffff, set extend bit\n");
+            }
+        }
+    }
+
+    if ((sg_fd = sg_cmds_open_device(device_name, rdonly, verbose)) < 0) {
+        if (verbose)
+            pr2serr("error opening file: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    ret = do_set_features(sg_fd, feature, count, lba, cdb_len, ck_cond,
+                          extend, verbose);
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_sat_set_feature failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_scan_linux.c b/src/sg_scan_linux.c
new file mode 100644
index 0000000..0dddaa8
--- /dev/null
+++ b/src/sg_scan_linux.c
@@ -0,0 +1,629 @@
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *  Copyright (C) 1999 - 2022 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program scans the "sg" device space (ie actual + simulated SCSI
+ * generic devices). Optionally sg_scan can be given other device names
+ * to scan (in place of the sg devices).
+ * Options: -a   alpha scan: scan /dev/sga,b,c, ....
+ *          -i   do SCSI inquiry on device (implies -w)
+ *          -n   numeric scan: scan /dev/sg0,1,2, ....
+ *          -V   output version string and exit
+ *          -w   open writable (new driver opens readable unless -i)
+ *          -x   extra information output
+ *
+ * By default this program will look for /dev/sg0 first (i.e. numeric scan)
+ *
+ * Note: This program is written to work under both the original and
+ * the new sg driver.
+ *
+ * F. Jansen - modification to extend beyond 26 sg devices.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <dirent.h>
+#include <libgen.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <scsi/scsi_ioctl.h>
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "4.18 20220118";
+
+#define ME "sg_scan: "
+
+#define NUMERIC_SCAN_DEF true /* change to false to make alpha scan default */
+
+#define INQ_REPLY_LEN 36
+#define INQ_CMD_LEN 6
+#define MAX_ERRORS 4
+
+#define EBUFF_SZ 256
+#define FNAME_SZ 64
+#define PRESENT_ARRAY_SIZE 8192
+
+static const char * sysfs_sg_dir = "/sys/class/scsi_generic";
+static int * gen_index_arr;
+
+typedef struct my_scsi_idlun {
+/* why can't userland see this structure ??? */
+    int dev_id;
+    int host_unique_id;
+} My_scsi_idlun;
+
+typedef struct my_sg_scsi_id {
+    int host_no;        /* as in "scsi<n>" where 'n' is one of 0, 1, 2 etc */
+    int channel;
+    int scsi_id;        /* scsi id of target device */
+    int lun;
+    int scsi_type;      /* TYPE_... defined in scsi/scsi.h */
+    short h_cmd_per_lun;/* host (adapter) maximum commands per lun */
+    short d_queue_depth;/* device (or adapter) maximum queue length */
+    int unused1;        /* probably find a good use, set 0 for now */
+    int unused2;        /* ditto */
+} My_sg_scsi_id;
+
+int sg3_inq(int sg_fd, uint8_t * inqBuff, bool do_extra);
+int scsi_inq(int sg_fd, uint8_t * inqBuff);
+int try_ata_identity(const char * file_namep, int ata_fd, bool do_inq);
+
+static uint8_t inq_cdb[INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+
+
+void usage()
+{
+    printf("Usage: sg_scan [-a] [-i] [-n] [-v] [-V] [-w] [-x] "
+           "[DEVICE]*\n");
+    printf("  where:\n");
+    printf("    -a    do alpha scan (ie sga, sgb, sgc)\n");
+    printf("    -i    do SCSI INQUIRY, output results\n");
+    printf("    -n    do numeric scan (ie sg0, sg1...) [default]\n");
+    printf("    -v    increase verbosity\n");
+    printf("    -V    output version string then exit\n");
+    printf("    -w    force open with read/write flag\n");
+    printf("    -x    extra information output about queuing\n");
+    printf("   DEVICE    name of device\n");
+}
+
+static int scandir_select(const struct dirent * s)
+{
+    int k;
+
+    if (1 == sscanf(s->d_name, "sg%d", &k)) {
+        if ((k >= 0) && (k < PRESENT_ARRAY_SIZE)) {
+            gen_index_arr[k] = 1;
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int sysfs_sg_scan(const char * dir_name)
+{
+    struct dirent ** namelist;
+    int num, k;
+
+    num = scandir(dir_name, &namelist, scandir_select, NULL);
+    if (num < 0)
+        return -errno;
+    for (k = 0; k < num; ++k)
+        free(namelist[k]);
+    free(namelist);
+    return num;
+}
+
+void make_dev_name(char * fname, int k, bool do_numeric)
+{
+    char buff[FNAME_SZ];
+    int  big,little;
+
+    strcpy(fname, "/dev/sg");
+    if (do_numeric) {
+        snprintf(buff, sizeof(buff), "%d", k);
+        strcat(fname, buff);
+    }
+    else {
+        if (k < 26) {
+            buff[0] = 'a' + (char)k;
+            buff[1] = '\0';
+            strcat(fname, buff);
+        }
+        else if (k <= 255) { /* assumes sequence goes x,y,z,aa,ab,ac etc */
+            big    = k/26;
+            little = k - (26 * big);
+            big    = big - 1;
+
+            buff[0] = 'a' + (char)big;
+            buff[1] = 'a' + (char)little;
+            buff[2] = '\0';
+            strcat(fname, buff);
+        }
+        else
+            strcat(fname, "xxxx");
+    }
+}
+
+
+int main(int argc, char * argv[])
+{
+    bool do_extra = false;
+    bool do_inquiry = false;
+    bool do_numeric = NUMERIC_SCAN_DEF;
+    bool eacces_err = false;
+    bool has_file_args = false;
+    bool has_sysfs_sg = false;
+    bool jmp_out;
+    bool sg_ver3 = false;
+    bool sg_ver3_set = false;
+    bool writeable = false;
+    int sg_fd, res, k, j, f, plen;
+    int emul = -1;
+    int flags;
+    int host_no;
+    const int max_file_args = PRESENT_ARRAY_SIZE;
+    int num_errors = 0;
+    int num_silent = 0;
+    int verbose = 0;
+    char * file_namep;
+    const char * cp;
+    char fname[FNAME_SZ];
+    char ebuff[EBUFF_SZ];
+    uint8_t inqBuff[INQ_REPLY_LEN];
+    My_scsi_idlun my_idlun;
+    struct stat a_stat;
+
+    if (NULL == (gen_index_arr =
+                 (int *)calloc(max_file_args + 1, sizeof(int)))) {
+        printf(ME "Out of memory\n");
+        return SG_LIB_CAT_OTHER;
+    }
+    strcpy(fname, "<null>");
+
+    for (k = 1, j = 0; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case 'a':
+                    do_numeric = false;
+                    break;
+                case 'h':
+                case '?':
+                    printf("Scan sg device names and optionally do an "
+                           "INQUIRY\n\n");
+                    usage();
+                    return 0;
+                case 'i':
+                    do_inquiry = true;
+                    break;
+                case 'n':
+                    do_numeric = true;
+                    break;
+                case 'v':
+                    ++verbose;
+                    break;
+                case 'V':
+                    pr2serr("Version string: %s\n", version_str);
+                    exit(0);
+                case 'w':
+                    writeable = true;
+                    break;
+                case 'x':
+                    do_extra = true;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else {
+            if (j < max_file_args) {
+                has_file_args = true;
+                gen_index_arr[j++] = k;
+            } else {
+                printf("Too many command line arguments\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+    }
+
+    if ((! has_file_args) && (stat(sysfs_sg_dir, &a_stat) >= 0) &&
+        (S_ISDIR(a_stat.st_mode)))
+        has_sysfs_sg = !! sysfs_sg_scan(sysfs_sg_dir);
+
+    flags = O_NONBLOCK | (writeable ? O_RDWR : O_RDONLY);
+
+    for (k = 0, res = 0, j = 0, sg_fd = -1;
+         (k < max_file_args)  && (has_file_args || (num_errors < MAX_ERRORS));
+         ++k, res = ((sg_fd >= 0) ? close(sg_fd) : 0)) {
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ, ME "Error closing %s ", fname);
+            perror(ebuff);
+            return SG_LIB_FILE_ERROR;
+        }
+        if (has_file_args) {
+            if (gen_index_arr[j])
+                file_namep = argv[gen_index_arr[j++]];
+            else
+                break;
+        } else if (has_sysfs_sg) {
+            if (0 == gen_index_arr[k]) {
+                sg_fd = -1;
+                continue;
+            }
+            make_dev_name(fname, k, 1);
+            file_namep = fname;
+        } else {
+            make_dev_name(fname, k, do_numeric);
+            file_namep = fname;
+        }
+
+        sg_fd = open(file_namep, flags);
+        if (sg_fd < 0) {
+            if (EBUSY == errno) {
+                printf("%s: device busy (O_EXCL lock), skipping\n",
+                       file_namep);
+                continue;
+            }
+            else if ((ENODEV == errno) || (ENOENT == errno) ||
+                     (ENXIO == errno)) {
+                if (verbose)
+                    pr2serr("Unable to open: %s, errno=%d\n", file_namep,
+                            errno);
+                ++num_errors;
+                ++num_silent;
+                continue;
+            }
+            else {
+                if (EACCES == errno)
+                    eacces_err = true;
+                snprintf(ebuff, EBUFF_SZ, ME "Error opening %s ", file_namep);
+                perror(ebuff);
+                ++num_errors;
+                continue;
+            }
+        }
+        res = ioctl(sg_fd, SCSI_IOCTL_GET_IDLUN, &my_idlun);
+        if (res < 0) {
+            res = try_ata_identity(file_namep, sg_fd, do_inquiry);
+            if (res == 0)
+                continue;
+            snprintf(ebuff, EBUFF_SZ, ME "device %s failed on scsi+ata "
+                     "ioctl, skip", file_namep);
+            perror(ebuff);
+            ++num_errors;
+            continue;
+        }
+        res = ioctl(sg_fd, SCSI_IOCTL_GET_BUS_NUMBER, &host_no);
+        if (res < 0) {
+            snprintf(ebuff, EBUFF_SZ, ME "device %s failed on scsi "
+                     "ioctl(2), skip", file_namep);
+            perror(ebuff);
+            ++num_errors;
+            continue;
+        }
+        res = ioctl(sg_fd, SG_EMULATED_HOST, &emul);
+        if (res < 0)
+            emul = -1;
+        printf("%s: scsi%d channel=%d id=%d lun=%d", file_namep, host_no,
+               (my_idlun.dev_id >> 16) & 0xff, my_idlun.dev_id & 0xff,
+               (my_idlun.dev_id >> 8) & 0xff);
+        if (1 == emul)
+            printf(" [em]");
+#if 0
+        printf(", huid=%d", my_idlun.host_unique_id);
+#endif
+        if (! has_file_args) {
+            My_sg_scsi_id m_id; /* compatible with sg_scsi_id_t in sg.h */
+
+            res = ioctl(sg_fd, SG_GET_SCSI_ID, &m_id);
+            if (res < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "device %s failed "
+                         "SG_GET_SCSI_ID ioctl(4), skip", file_namep);
+                perror(ebuff);
+                ++num_errors;
+                continue;
+            }
+            /* printf("  type=%d", m_id.scsi_type); */
+            if (do_extra)
+                printf("  cmd_per_lun=%hd queue_depth=%hd\n",
+                       m_id.h_cmd_per_lun, m_id.d_queue_depth);
+            else
+                printf("\n");
+        }
+        else
+            printf("\n");
+        if (do_inquiry) {
+            if (! sg_ver3_set) {
+                sg_ver3 = false;
+                sg_ver3_set = true;
+                if ((ioctl(sg_fd, SG_GET_VERSION_NUM, &f) >= 0) &&
+                    (f >= 30000))
+                    sg_ver3 = true;
+            }
+            if (sg_ver3) {
+                res = sg3_inq(sg_fd, inqBuff, do_extra);
+                if (res)
+                    ++num_errors;
+            }
+        }
+    }
+    if ((num_errors >= MAX_ERRORS) && (num_silent < num_errors) &&
+        (! has_file_args)) {
+        printf("Stopping because there are too many error\n");
+        if (eacces_err)
+            printf("    root access may be required\n");
+    }
+    return 0;
+}
+
+int sg3_inq(int sg_fd, uint8_t * inqBuff, bool do_extra)
+{
+    bool ok;
+    int err, sg_io;
+    uint8_t sense_buffer[32] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr SG_C_CPP_ZERO_INIT;
+
+    memset(inqBuff, 0, INQ_REPLY_LEN);
+    inqBuff[0] = 0x7f;
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(inq_cdb);
+    io_hdr.mx_sb_len = sizeof(sense_buffer);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = INQ_REPLY_LEN;
+    io_hdr.dxferp = inqBuff;
+    io_hdr.cmdp = inq_cdb;
+    io_hdr.sbp = sense_buffer;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+
+    ok = true;
+    sg_io = 0;
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        if ((err = scsi_inq(sg_fd, inqBuff)) < 0) {
+            perror(ME "Inquiry SG_IO + SCSI_IOCTL_SEND_COMMAND ioctl error");
+            return 1;
+        } else if (err) {
+            printf(ME "SCSI_IOCTL_SEND_COMMAND ioctl error=0x%x\n", err);
+            return 1;
+        }
+    } else {
+        sg_io = 1;
+        /* now for the error processing */
+        switch (sg_err_category3(&io_hdr)) {
+        case SG_LIB_CAT_RECOVERED:
+            sg_chk_n_print3("Inquiry, continuing", &io_hdr, true);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        case SG_LIB_CAT_CLEAN:
+            break;
+        default: /* won't bother decoding other categories */
+            ok = false;
+            sg_chk_n_print3("INQUIRY command error", &io_hdr, true);
+            break;
+        }
+    }
+
+    if (ok) { /* output result if it is available */
+        char * p = (char *)inqBuff;
+
+        printf("    %.8s  %.16s  %.4s ", p + 8, p + 16, p + 32);
+        printf("[rmb=%d cmdq=%d pqual=%d pdev=0x%x] ",
+               !!(p[1] & 0x80), !!(p[7] & 2), (p[0] & 0xe0) >> 5,
+               (p[0] & PDT_MASK));
+        if (do_extra && sg_io)
+            printf("dur=%ums\n", io_hdr.duration);
+        else
+            printf("\n");
+    }
+    return 0;
+}
+
+struct lscsi_ioctl_command {
+        unsigned int inlen;  /* _excluding_ scsi command length */
+        unsigned int outlen;
+        uint8_t data[1];  /* was 0 but that's not ISO C!! */
+                /* on input, scsi command starts here then opt. data */
+};
+
+/* fallback INQUIRY using scsi mid-level's SCSI_IOCTL_SEND_COMMAND ioctl */
+int scsi_inq(int sg_fd, uint8_t * inqBuff)
+{
+    int res;
+    uint8_t buff[1024];
+    struct lscsi_ioctl_command * sicp = (struct lscsi_ioctl_command *)buff;
+
+    memset(buff, 0, sizeof(buff));
+    sicp->inlen = 0;
+    sicp->outlen = INQ_REPLY_LEN;
+    memcpy(sicp->data, inq_cdb, INQ_CMD_LEN);
+    res = ioctl(sg_fd, SCSI_IOCTL_SEND_COMMAND, sicp);
+    if (0 == res)
+        memcpy(inqBuff, sicp->data, INQ_REPLY_LEN);
+    return res;
+}
+
+/* Following code permits ATA IDENTIFY commands to be performed on
+   ATA non "Packet Interface" devices (e.g. ATA disks).
+   GPL-ed code borrowed from smartmontools (smartmontools.sf.net).
+   Copyright (C) 2002-4 Bruce Allen
+                <smartmontools-support@lists.sourceforge.net>
+ */
+#ifndef ATA_IDENTIFY_DEVICE
+#define ATA_IDENTIFY_DEVICE 0xec
+#endif
+#ifndef HDIO_DRIVE_CMD
+#define HDIO_DRIVE_CMD    0x031f
+#endif
+
+/* Needed parts of the ATA DRIVE IDENTIFY Structure. Those labeled
+ * word* are NOT used.
+ */
+struct ata_identify_device {
+  unsigned short words000_009[10];
+  uint8_t  serial_no[20];
+  unsigned short words020_022[3];
+  uint8_t  fw_rev[8];
+  uint8_t  model[40];
+  unsigned short words047_079[33];
+  unsigned short major_rev_num;
+  unsigned short minor_rev_num;
+  unsigned short command_set_1;
+  unsigned short command_set_2;
+  unsigned short command_set_extension;
+  unsigned short cfs_enable_1;
+  unsigned short word086;
+  unsigned short csf_default;
+  unsigned short words088_255[168];
+};
+
+/* Copies n bytes (or n-1 if n is odd) from in to out, but swaps adjacents
+ * bytes.
+ */
+void swapbytes(char *out, const char *in, size_t n)
+{
+    size_t k;
+
+    if (n > 1) {
+        for (k = 0; k < (n - 1); k += 2) {
+            out[k] = in[k + 1];
+            out[k + 1] = in[k];
+        }
+    }
+}
+
+/* Copies in to out, but removes leading and trailing whitespace. */
+void trim(char *out, const char *in)
+{
+    int k, first, last, num;
+
+    /* Find the first non-space character (maybe none). */
+    first = -1;
+    for (k = 0; in[k]; k++) {
+        if (! isspace((int)in[k])) {
+            first = k;
+            break;
+        }
+    }
+
+    if (first == -1) {
+        /* There are no non-space characters. */
+        out[0] = '\0';
+        return;
+    }
+
+    /* Find the last non-space character. */
+    for (k = strlen(in) - 1; k >= first && isspace((int)in[k]); k--)
+        ;
+    last = k;
+    num = last - first + 1;
+    for (k = 0; k < num; ++k)
+        out[k] = in[first + k];
+    out[num] = '\0';
+}
+
+/* Convenience function for formatting strings from ata_identify_device */
+void formatdriveidstring(char *out, const char *in, int n)
+{
+    char tmp[65];
+
+    n = n > 64 ? 64 : n;
+    swapbytes(tmp, in, n);
+    tmp[n] = '\0';
+    trim(out, tmp);
+}
+
+/* Function for printing ASCII byte-swapped strings, skipping white
+ * space. Please note that this is needed on both big- and
+ * little-endian hardware.
+ */
+void printswap(char *output, char *in, unsigned int n)
+{
+    formatdriveidstring(output, in, n);
+    if (*output)
+        printf("%.*s   ", (int)n, output);
+    else
+        printf("%.*s   ", (int)n, "[No Information Found]\n");
+}
+
+#define ATA_IDENTIFY_BUFF_SZ  sizeof(struct ata_identify_device)
+#define HDIO_DRIVE_CMD_OFFSET 4
+
+int ata_command_interface(int device, char *data)
+{
+    uint8_t buff[ATA_IDENTIFY_BUFF_SZ + HDIO_DRIVE_CMD_OFFSET];
+    int retval;
+
+    buff[0] = ATA_IDENTIFY_DEVICE;
+    buff[3] = 1;
+    /* We are now doing the HDIO_DRIVE_CMD type ioctl. */
+    if ((retval = ioctl(device, HDIO_DRIVE_CMD, buff)))
+        return retval;
+
+    /* if the command returns data, copy it back */
+    memcpy(data, buff + HDIO_DRIVE_CMD_OFFSET, ATA_IDENTIFY_BUFF_SZ);
+    return 0;
+}
+
+int try_ata_identity(const char * file_namep, int ata_fd, bool do_inq)
+{
+    struct ata_identify_device ata_ident;
+    char model[64];
+    char serial[64];
+    char firm[64];
+    int res;
+
+    res = ata_command_interface(ata_fd, (char *)&ata_ident);
+    if (res)
+        return res;
+    printf("%s: ATA device\n", file_namep);
+    if (do_inq) {
+        printf("    ");
+        printswap(model, (char *)ata_ident.model, 40);
+        printswap(serial, (char *)ata_ident.serial_no, 20);
+        printswap(firm, (char *)ata_ident.fw_rev, 8);
+        printf("\n");
+    }
+    return res;
+}
diff --git a/src/sg_scan_win32.c b/src/sg_scan_win32.c
new file mode 100644
index 0000000..d39212c
--- /dev/null
+++ b/src/sg_scan_win32.c
@@ -0,0 +1,733 @@
+/*
+ * Copyright (c) 2006-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * This utility shows the relationship between various device names and
+ * volumes in Windows OSes (Windows 2000, 2003, XP and Vista). There is
+ * an optional scsi adapter scan.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#include <errno.h>
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_pr2serr.h"
+
+#ifdef _WIN32_WINNT
+ #if _WIN32_WINNT < 0x0602
+ #undef _WIN32_WINNT
+ #define _WIN32_WINNT 0x0602
+ #endif
+#else
+#define _WIN32_WINNT 0x0602
+/* claim its W8 */
+#endif
+
+#include "sg_pt_win32.h"
+
+static const char * version_str = "1.23 (win32) 20220127";
+
+#define MAX_SCSI_ELEMS 4096
+#define MAX_ADAPTER_NUM 256
+#define MAX_PHYSICALDRIVE_NUM 2048
+#define MAX_CDROM_NUM 512
+#define MAX_TAPE_NUM 512
+#define MAX_HOLE_COUNT 16
+#define MAX_GET_INQUIRY_DATA_SZ (32 * 1024)
+
+
+union STORAGE_DEVICE_DESCRIPTOR_DATA {
+    STORAGE_DEVICE_DESCRIPTOR desc;
+    char raw[256];
+};
+
+union STORAGE_DEVICE_UID_DATA {
+    STORAGE_DEVICE_UNIQUE_IDENTIFIER desc;
+    char raw[1060];
+};
+
+struct storage_elem {
+    char    name[32];
+    char    volume_letters[32];
+    bool qp_descriptor_valid;
+    bool qp_uid_valid;
+    union STORAGE_DEVICE_DESCRIPTOR_DATA qp_descriptor;
+    union STORAGE_DEVICE_UID_DATA qp_uid;
+};
+
+
+static struct storage_elem * storage_arr;
+static uint8_t * free_storage_arr;
+static int next_unused_elem = 0;
+static int verbose = 0;
+
+static struct option long_options[] = {
+        {"bus", no_argument, 0, 'b'},
+        {"help", no_argument, 0, 'h'},
+        {"letter", required_argument, 0, 'l'},
+        {"verbose", no_argument, 0, 'v'},
+        {"scsi", no_argument, 0, 's'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_scan  [--bus] [--help] [--letter=VL] [--scsi] "
+            "[--verbose] [--version]\n");
+    pr2serr("       --bus|-b        output bus type\n"
+            "       --help|-h       output this usage message then exit\n"
+            "       --letter=VL|-l VL    volume letter (e.g. 'F' for F:) "
+            "to match\n"
+            "       --scsi|-s       used once: show SCSI adapters (tuple) "
+            "scan after\n"
+            "                       device scan; default: show no "
+            "adapters;\n"
+            "                       used twice: show only adapters\n"
+            "       --verbose|-v    increase verbosity\n"
+            "       --version|-V    print version string and exit\n\n"
+            "Scan for storage and related device names\n");
+}
+
+static char *
+get_err_str(DWORD err, int max_b_len, char * b)
+{
+    char * cp;
+    struct sg_pt_base * tmp_p = construct_scsi_pt_obj();
+
+    if ((NULL == b) || (max_b_len < 2)) {
+        if (b && (max_b_len > 0))
+            b[0] = '\0';
+        return b;
+    }
+    if (NULL == tmp_p) {
+        snprintf(b, max_b_len, "%s: construct_scsi_pt_obj() failed\n",
+                 __func__);
+
+        return b;
+    }
+    set_scsi_pt_transport_err(tmp_p, (int)err);
+    cp = get_scsi_pt_transport_err_str(tmp_p, max_b_len, b);
+    destruct_scsi_pt_obj(tmp_p);
+    return cp;
+}
+
+static const char *
+get_bus_type(int bt)
+{
+    switch (bt)
+    {
+    case BusTypeUnknown:
+        return "Unkno";
+    case BusTypeScsi:
+        return "Scsi ";
+    case BusTypeAtapi:
+        return "Atapi";
+    case BusTypeAta:
+        return "Ata  ";
+    case BusType1394:
+        return "1394 ";
+    case BusTypeSsa:
+        return "Ssa  ";
+    case BusTypeFibre:
+        return "Fibre";
+    case BusTypeUsb:
+        return "Usb  ";
+    case BusTypeRAID:
+        return "RAID ";
+    case BusTypeiScsi:
+        return "iScsi";
+    case BusTypeSas:
+        return "Sas  ";
+    case BusTypeSata:
+        return "Sata ";
+    case BusTypeSd:
+        return "Sd   ";
+    case BusTypeMmc:
+        return "Mmc  ";
+    case BusTypeVirtual:
+        return "Virt ";
+    case BusTypeFileBackedVirtual:
+        return "FBVir";
+#ifdef BusTypeSpaces
+    case BusTypeSpaces:
+#else
+    case 0x10:
+#endif
+        return "Spaces";
+#ifdef BusTypeNvme
+    case BusTypeNvme:
+#else
+    case 0x11:
+#endif
+        return "NVMe ";
+#ifdef BusTypeSCM
+    case BusTypeSCM:
+#else
+    case 0x12:
+#endif
+        return "SCM  ";
+#ifdef BusTypeUfs
+    case BusTypeUfs:
+#else
+    case 0x13:
+#endif
+        return "Ufs ";
+    case 0x14:
+        return "Max ";
+    case 0x7f:
+        return "Max Reserved";
+    default:
+        return "_unkn";
+    }
+}
+
+static int
+query_dev_property(HANDLE hdevice,
+                   union STORAGE_DEVICE_DESCRIPTOR_DATA * data)
+{
+    DWORD num_out, err;
+    char b[256];
+    STORAGE_PROPERTY_QUERY query = {StorageDeviceProperty,
+                                    PropertyStandardQuery, {0} };
+
+    memset(data, 0, sizeof(*data));
+    if (! DeviceIoControl(hdevice, IOCTL_STORAGE_QUERY_PROPERTY,
+                          &query, sizeof(query), data, sizeof(*data),
+                          &num_out, NULL)) {
+        if (verbose > 2) {
+            err = GetLastError();
+            pr2serr("  IOCTL_STORAGE_QUERY_PROPERTY(Devprop) failed, "
+                    "Error=%u %s\n", (unsigned int)err,
+                    get_err_str(err, sizeof(b), b));
+        }
+        return -ENOSYS;
+    }
+
+    if (verbose > 3)
+        pr2serr("  IOCTL_STORAGE_QUERY_PROPERTY(DevProp) num_out=%u\n",
+                (unsigned int)num_out);
+    return 0;
+}
+
+static int
+query_dev_uid(HANDLE hdevice, union STORAGE_DEVICE_UID_DATA * data)
+{
+    DWORD num_out, err;
+    char b[256];
+    STORAGE_PROPERTY_QUERY query = {StorageDeviceUniqueIdProperty,
+                                    PropertyStandardQuery, {0} };
+
+    memset(data, 0, sizeof(*data));
+    num_out = 0;
+    query.QueryType = PropertyExistsQuery;
+    if (! DeviceIoControl(hdevice, IOCTL_STORAGE_QUERY_PROPERTY,
+                          &query, sizeof(query), NULL, 0, &num_out, NULL)) {
+        if (verbose > 2) {
+            err = GetLastError();
+            pr2serr("  IOCTL_STORAGE_QUERY_PROPERTY(DevUid(exists)) failed, "
+                    "Error=%u %s\n", (unsigned int)err,
+                    get_err_str(err, sizeof(b), b));
+        }
+        if (verbose > 3)
+            pr2serr("      num_out=%u\n", (unsigned int)num_out);
+        /* interpret any error to mean this property doesn't exist */
+        return 0;
+    }
+
+    query.QueryType = PropertyStandardQuery;
+    if (! DeviceIoControl(hdevice, IOCTL_STORAGE_QUERY_PROPERTY,
+                          &query, sizeof(query), data, sizeof(*data),
+                          &num_out, NULL)) {
+        if (verbose > 2) {
+            err = GetLastError();
+            pr2serr("  IOCTL_STORAGE_QUERY_PROPERTY(DevUid) failed, Error=%u "
+                    "%s\n", (unsigned int)err,
+                    get_err_str(err, sizeof(b), b));
+        }
+        return -ENOSYS;
+    }
+    if (verbose > 3)
+        pr2serr("  IOCTL_STORAGE_QUERY_PROPERTY(DevUid) num_out=%u\n",
+                (unsigned int)num_out);
+    return 0;
+}
+
+/* Updates storage_arr based on sep. Returns 1 if update occurred, 0 if
+ * no update occurred. */
+static int
+check_devices(const struct storage_elem * sep)
+{
+    int k, j;
+    struct storage_elem * sarr = storage_arr;
+
+    for (k = 0; k < next_unused_elem; ++k, ++sarr) {
+        if ('\0' == sarr->name[0])
+            continue;
+        if (sep->qp_uid_valid && sarr->qp_uid_valid) {
+            if (0 == memcmp(&sep->qp_uid, &sarr->qp_uid,
+                            sizeof(sep->qp_uid))) {
+                for (j = 0; j < (int)sizeof(sep->volume_letters); ++j) {
+                    if ('\0' == sarr->volume_letters[j]) {
+                        sarr->volume_letters[j] = sep->name[0];
+                        break;
+                    }
+                }
+                return 1;
+            }
+        } else if (sep->qp_descriptor_valid && sarr->qp_descriptor_valid) {
+            if (0 == memcmp(&sep->qp_descriptor, &sarr->qp_descriptor,
+                            sizeof(sep->qp_descriptor))) {
+                for (j = 0; j < (int)sizeof(sep->volume_letters); ++j) {
+                    if ('\0' == sarr->volume_letters[j]) {
+                        sarr->volume_letters[j] = sep->name[0];
+                        break;
+                    }
+                }
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
+
+static int
+enum_scsi_adapters(void)
+{
+    int k, j;
+    int hole_count = 0;
+    HANDLE fh;
+    ULONG dummy;
+    DWORD err = 0;
+    BYTE bus;
+    BOOL success;
+    char adapter_name[64];
+    char * inq_dbp;
+    uint8_t * free_inq_dbp = NULL;
+    PSCSI_ADAPTER_BUS_INFO  ai;
+    char b[256];
+
+    inq_dbp = (char *)sg_memalign(MAX_GET_INQUIRY_DATA_SZ, 0, &free_inq_dbp,
+                                  false);
+    if (NULL == inq_dbp) {
+        pr2serr("%s: unable to allocate %d bytes on heap\n", __func__,
+                MAX_GET_INQUIRY_DATA_SZ);
+        return sg_convert_errno(ENOMEM);
+    }
+
+    for (k = 0; k < MAX_ADAPTER_NUM; ++k) {
+        snprintf(adapter_name, sizeof (adapter_name), "\\\\.\\SCSI%d:", k);
+        fh = CreateFile(adapter_name, GENERIC_READ | GENERIC_WRITE,
+                        FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
+                        OPEN_EXISTING, 0, NULL);
+        if (fh != INVALID_HANDLE_VALUE) {
+            hole_count = 0;
+            success = DeviceIoControl(fh, IOCTL_SCSI_GET_INQUIRY_DATA, NULL,
+                                      0, inq_dbp, MAX_GET_INQUIRY_DATA_SZ,
+                                      &dummy, NULL);
+            if (success) {
+                PSCSI_BUS_DATA pbd;
+                PSCSI_INQUIRY_DATA pid;
+                int num_lus, off;
+
+                ai = (PSCSI_ADAPTER_BUS_INFO)inq_dbp;
+                for (bus = 0; bus < ai->NumberOfBusses; bus++) {
+                    pbd = ai->BusData + bus;
+                    num_lus = pbd->NumberOfLogicalUnits;
+                    off = pbd->InquiryDataOffset;
+                    for (j = 0; j < num_lus; ++j) {
+                        if ((off < (int)sizeof(SCSI_ADAPTER_BUS_INFO)) ||
+                            (off > (MAX_GET_INQUIRY_DATA_SZ -
+                                    (int)sizeof(SCSI_INQUIRY_DATA))))
+                            break;
+                        pid = (PSCSI_INQUIRY_DATA)(inq_dbp + off);
+                        snprintf(b, sizeof(b) - 1, "SCSI%d:%d,%d,%d ", k,
+                                 pid->PathId, pid->TargetId, pid->Lun);
+                        printf("%-15s", b);
+                        snprintf(b, sizeof(b) - 1, "claimed=%d pdt=%xh %s ",
+                                 pid->DeviceClaimed,
+                                 pid->InquiryData[0] % PDT_MASK,
+                                 ((0 == pid->InquiryData[4]) ? "dubious" :
+                                                               ""));
+                        printf("%-26s", b);
+                        printf("%.8s  %.16s  %.4s\n", pid->InquiryData + 8,
+                               pid->InquiryData + 16, pid->InquiryData + 32);
+                        off = pid->NextInquiryDataOffset;
+                    }
+                }
+            } else {
+                err = GetLastError();
+                pr2serr("%s: IOCTL_SCSI_GET_INQUIRY_DATA failed err=%u\n\t%s",
+                        adapter_name, (unsigned int)err,
+                        get_err_str(err, sizeof(b), b));
+                err = SG_LIB_WINDOWS_ERR;
+            }
+            CloseHandle(fh);
+        } else {
+            err = GetLastError();
+            if (ERROR_SHARING_VIOLATION == err)
+                pr2serr("%s: in use by other process (sharing violation "
+                        "[34])\n", adapter_name);
+            else if (verbose > 3)
+                pr2serr("%s: CreateFile failed err=%u\n\t%s", adapter_name,
+                        (unsigned int)err, get_err_str(err, sizeof(b), b));
+            if (++hole_count >= MAX_HOLE_COUNT)
+                break;
+            /* hope problem is local to this adapter so continue to next */
+        }
+    }
+    if (free_inq_dbp)
+        free(free_inq_dbp);
+    return 0;
+}
+
+static int
+enum_volumes(char letter)
+{
+    int k;
+    HANDLE fh;
+    char adapter_name[64];
+    struct storage_elem tmp_se;
+
+    if (verbose > 2)
+        pr2serr("%s: enter\n", __FUNCTION__ );
+    for (k = 0; k < 24; ++k) {
+        memset(&tmp_se, 0, sizeof(tmp_se));
+        snprintf(adapter_name, sizeof (adapter_name), "\\\\.\\%c:", 'C' + k);
+        tmp_se.name[0] = 'C' + k;
+        fh = CreateFile(adapter_name, GENERIC_READ | GENERIC_WRITE,
+                        FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
+                        OPEN_EXISTING, 0, NULL);
+        if (fh != INVALID_HANDLE_VALUE) {
+            if (query_dev_property(fh, &tmp_se.qp_descriptor) < 0)
+                pr2serr("%s: query_dev_property failed\n", __FUNCTION__ );
+            else
+                tmp_se.qp_descriptor_valid = true;
+            if (query_dev_uid(fh, &tmp_se.qp_uid) < 0) {
+                if (verbose > 2)
+                    pr2serr("%s: query_dev_uid failed\n", __FUNCTION__ );
+            } else
+                tmp_se.qp_uid_valid = true;
+            if (('\0' == letter) || (letter == tmp_se.name[0]))
+                check_devices(&tmp_se);
+            CloseHandle(fh);
+        }
+    }
+    return 0;
+}
+
+static int
+enum_pds(void)
+{
+    int k;
+    int hole_count = 0;
+    HANDLE fh;
+    DWORD err;
+    char adapter_name[64];
+    char b[256];
+    struct storage_elem tmp_se;
+
+    if (verbose > 2)
+        pr2serr("%s: enter\n", __FUNCTION__ );
+    for (k = 0; k < MAX_PHYSICALDRIVE_NUM; ++k) {
+        memset(&tmp_se, 0, sizeof(tmp_se));
+        snprintf(adapter_name, sizeof (adapter_name),
+                 "\\\\.\\PhysicalDrive%d", k);
+        snprintf(tmp_se.name, sizeof(tmp_se.name), "PD%d", k);
+        fh = CreateFile(adapter_name, GENERIC_READ | GENERIC_WRITE,
+                        FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
+                        OPEN_EXISTING, 0, NULL);
+        if (fh != INVALID_HANDLE_VALUE) {
+            if (query_dev_property(fh, &tmp_se.qp_descriptor) < 0)
+                pr2serr("%s: query_dev_property failed\n", __FUNCTION__ );
+            else
+                tmp_se.qp_descriptor_valid = true;
+            if (query_dev_uid(fh, &tmp_se.qp_uid) < 0) {
+                if (verbose > 2)
+                    pr2serr("%s: query_dev_uid failed\n", __FUNCTION__ );
+            } else
+                tmp_se.qp_uid_valid = true;
+            hole_count = 0;
+            memcpy(&storage_arr[next_unused_elem++], &tmp_se, sizeof(tmp_se));
+            CloseHandle(fh);
+        } else {
+            err = GetLastError();
+            if ((0 == k) && (ERROR_ACCESS_DENIED == err))
+                pr2serr("Access denied on %s, may need Administrator\n",
+                        adapter_name);
+            if (ERROR_SHARING_VIOLATION == err)
+                pr2serr("%s: in use by other process (sharing violation "
+                        "[34])\n", adapter_name);
+            else if (verbose > 3)
+                pr2serr("%s: CreateFile failed err=%u\n\t%s", adapter_name,
+                        (unsigned int)err, get_err_str(err, sizeof(b), b));
+            if (++hole_count >= MAX_HOLE_COUNT)
+                break;
+        }
+    }
+    return 0;
+}
+
+static int
+enum_cdroms(void)
+{
+    int k;
+    int hole_count = 0;
+    HANDLE fh;
+    DWORD err;
+    char adapter_name[64];
+    char b[256];
+    struct storage_elem tmp_se;
+
+    if (verbose > 2)
+        pr2serr("%s: enter\n", __FUNCTION__ );
+    for (k = 0; k < MAX_CDROM_NUM; ++k) {
+        memset(&tmp_se, 0, sizeof(tmp_se));
+        snprintf(adapter_name, sizeof (adapter_name), "\\\\.\\CDROM%d", k);
+        snprintf(tmp_se.name, sizeof(tmp_se.name), "CDROM%d", k);
+        fh = CreateFile(adapter_name, GENERIC_READ | GENERIC_WRITE,
+                        FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
+                        OPEN_EXISTING, 0, NULL);
+        if (fh != INVALID_HANDLE_VALUE) {
+            if (query_dev_property(fh, &tmp_se.qp_descriptor) < 0)
+                pr2serr("%s: query_dev_property failed\n", __FUNCTION__ );
+            else
+                tmp_se.qp_descriptor_valid = true;
+            if (query_dev_uid(fh, &tmp_se.qp_uid) < 0) {
+                if (verbose > 2)
+                    pr2serr("%s: query_dev_uid failed\n", __FUNCTION__ );
+            } else
+                tmp_se.qp_uid_valid = true;
+            hole_count = 0;
+            memcpy(&storage_arr[next_unused_elem++], &tmp_se, sizeof(tmp_se));
+            CloseHandle(fh);
+        } else {
+            err = GetLastError();
+            if (ERROR_SHARING_VIOLATION == err)
+                pr2serr("%s: in use by other process (sharing violation "
+                        "[34])\n", adapter_name);
+            else if (verbose > 3)
+                pr2serr("%s: CreateFile failed err=%u\n\t%s", adapter_name,
+                        (unsigned int)err, get_err_str(err, sizeof(b), b));
+            if (++hole_count >= MAX_HOLE_COUNT)
+                break;
+        }
+    }
+    return 0;
+}
+
+static int
+enum_tapes(void)
+{
+    int k;
+    int hole_count = 0;
+    HANDLE fh;
+    DWORD err;
+    char adapter_name[64];
+    char b[256];
+    struct storage_elem tmp_se;
+
+    if (verbose > 2)
+        pr2serr("%s: enter\n", __FUNCTION__ );
+    for (k = 0; k < MAX_TAPE_NUM; ++k) {
+        memset(&tmp_se, 0, sizeof(tmp_se));
+        snprintf(adapter_name, sizeof (adapter_name), "\\\\.\\TAPE%d", k);
+        snprintf(tmp_se.name, sizeof(tmp_se.name), "TAPE%d", k);
+        fh = CreateFile(adapter_name, GENERIC_READ | GENERIC_WRITE,
+                        FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
+                        OPEN_EXISTING, 0, NULL);
+        if (fh != INVALID_HANDLE_VALUE) {
+            if (query_dev_property(fh, &tmp_se.qp_descriptor) < 0)
+                pr2serr("%s: query_dev_property failed\n", __FUNCTION__ );
+            else
+                tmp_se.qp_descriptor_valid = true;
+            if (query_dev_uid(fh, &tmp_se.qp_uid) < 0) {
+                if (verbose > 2)
+                    pr2serr("%s: query_dev_uid failed\n", __FUNCTION__ );
+            } else
+                tmp_se.qp_uid_valid = true;
+            hole_count = 0;
+            memcpy(&storage_arr[next_unused_elem++], &tmp_se, sizeof(tmp_se));
+            CloseHandle(fh);
+        } else {
+            err = GetLastError();
+            if (ERROR_SHARING_VIOLATION == err)
+                pr2serr("%s: in use by other process (sharing violation "
+                        "[34])\n", adapter_name);
+            else if (verbose > 3)
+                pr2serr("%s: CreateFile failed err=%u\n\t%s", adapter_name,
+                        (unsigned int)err, get_err_str(err, sizeof(b), b));
+            if (++hole_count >= MAX_HOLE_COUNT)
+                break;
+        }
+    }
+    return 0;
+}
+
+static int
+sg_do_wscan(char letter, bool show_bt, int scsi_scan)
+{
+    int k, j, n;
+    struct storage_elem * sp;
+
+    if (scsi_scan < 2) {
+        k = enum_pds();
+        if (k)
+            return k;
+        k = enum_cdroms();
+        if (k)
+            return k;
+        k = enum_tapes();
+        if (k)
+            return k;
+        k = enum_volumes(letter);
+        if (k)
+            return k;
+
+        for (k = 0; k < next_unused_elem; ++k) {
+            sp = storage_arr + k;
+            if ('\0' == sp->name[0])
+                continue;
+            printf("%-7s ", sp->name);
+            n = strlen(sp->volume_letters);
+            if (0 == n)
+                printf("        ");
+            else if (1 == n)
+                printf("[%s]     ", sp->volume_letters);
+            else if (2 == n)
+                printf("[%s]    ", sp->volume_letters);
+            else if (3 == n)
+                printf("[%s]   ", sp->volume_letters);
+            else if (4 == n)
+                printf("[%s]  ", sp->volume_letters);
+            else
+                printf("[%4s+] ", sp->volume_letters);
+            if (sp->qp_descriptor_valid) {
+                if (show_bt)
+                    printf("<%s>  ",
+                           get_bus_type(sp->qp_descriptor.desc.BusType));
+                j = sp->qp_descriptor.desc.VendorIdOffset;
+                if (j > 0)
+                    printf("%s  ", sp->qp_descriptor.raw + j);
+                j = sp->qp_descriptor.desc.ProductIdOffset;
+                if (j > 0)
+                    printf("%s  ", sp->qp_descriptor.raw + j);
+                j = sp->qp_descriptor.desc.ProductRevisionOffset;
+                if (j > 0)
+                    printf("%s  ", sp->qp_descriptor.raw + j);
+                j = sp->qp_descriptor.desc.SerialNumberOffset;
+                if (j > 0)
+                    printf("%s", sp->qp_descriptor.raw + j);
+                printf("\n");
+                if (verbose > 2)
+                    hex2stderr((const uint8_t *)sp->qp_descriptor.raw, 144, 0);
+            } else
+                printf("\n");
+            if ((verbose > 3) && sp->qp_uid_valid) {
+                printf("  UID valid, in hex:\n");
+                hex2stderr((const uint8_t *)sp->qp_uid.raw,
+                           sizeof(sp->qp_uid.raw), 0);
+            }
+        }
+    }
+
+    if (scsi_scan) {
+        if (scsi_scan < 2)
+            printf("\n");
+        enum_scsi_adapters();
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool show_bt = false;
+    int c, ret;
+    int vol_letter = 0;
+    int scsi_scan = 0;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "bhHl:svV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            show_bt = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'l':
+            vol_letter = toupper(optarg[0]);
+            if ((vol_letter < 'C') || (vol_letter > 'Z')) {
+                pr2serr("'--letter=' expects a letter in the 'C' to 'Z' "
+                        "range\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 's':
+            ++scsi_scan;
+            break;
+        case 'v':
+            ++verbose;
+            break;
+        case 'V':
+            pr2serr("version: %s\n", version_str);
+            return 0;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    storage_arr = (struct storage_elem *)
+                  sg_memalign(sizeof(struct storage_elem) * MAX_SCSI_ELEMS, 0,
+                              &free_storage_arr, false);
+    if (storage_arr) {
+        ret = sg_do_wscan(vol_letter, show_bt, scsi_scan);
+        if (free_storage_arr)
+            free(free_storage_arr);
+    } else {
+        pr2serr("Failed to allocate storage_arr (%d bytes) on heap\n",
+                (int)(sizeof(struct storage_elem) * MAX_SCSI_ELEMS));
+        ret = sg_convert_errno(ENOMEM);
+    }
+    return ret;
+}
diff --git a/src/sg_seek.c b/src/sg_seek.c
new file mode 100644
index 0000000..fb64763
--- /dev/null
+++ b/src/sg_seek.c
@@ -0,0 +1,429 @@
+/*
+ * Copyright (c) 2018-2020 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+#include <time.h>
+#elif defined(HAVE_GETTIMEOFDAY)
+#include <time.h>
+#include <sys/time.h>
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/*
+ * This program issues one or more SCSI SEEK(10), PRE-FETCH(10) or
+ * PRE-FETCH(16) commands. Both PRE-FETCH commands are current and appear
+ * in the most recent SBC-4 draft (sbc4r15.pdf at time of writing) while
+ * SEEK(10) has been obsolete since SBC-2 (2004). Currently more hard disks
+ * and SSDs support SEEK(10) than PRE-FETCH. It is even unclear what
+ * SEEK(10) means (defined in SBC-1 as moving the hard disk heads to the
+ * track containing the given LBA) for a SSD. But if the manufacturers'
+ * support it, then it must have a use, presumably to speed the next access
+ * to that LBA ...
+ */
+
+static const char * version_str = "1.08 20200115";
+
+#define BACKGROUND_CONTROL_SA 0x15
+
+#define CMD_ABORT_TIMEOUT  60      /* 60 seconds */
+
+
+static struct option long_options[] = {
+        {"10", no_argument, 0, 'T'},
+        {"count", required_argument, 0, 'c'},
+        {"grpnum", required_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"immed", no_argument, 0, 'i'},
+        {"lba", required_argument, 0, 'l'},
+        {"num-blocks", required_argument, 0, 'n'},
+        {"num_blocks", required_argument, 0, 'n'},
+        {"pre-fetch", no_argument, 0, 'p'},
+        {"pre_fetch", no_argument, 0, 'p'},
+        {"readonly", no_argument, 0, 'r'},
+        {"skip", required_argument, 0, 's'},
+        {"time", required_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"wrap-offset", required_argument, 0, 'w'},
+        {"wrap_offset", required_argument, 0, 'w'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_seek  [--10] [--count=NC] [--grpnum=GN] [--help] [--immed]\n"
+            "                [--lba=LBA] [--num-blocks=NUM] [--pre-fetch] "
+            "[--readonly]\n"
+            "                [--skip=SB] [--time] [--verbose] [--version]\n"
+            "                [--wrap-offset=WO] DEVICE\n");
+    pr2serr("  where:\n"
+            "    --10|-T             do PRE-FETCH(10) command (def: "
+            "SEEK(10), or\n"
+            "                        PRE-FETCH(16) if --pre-fetch also "
+            "given)\n"
+            "    --count=NC|-c NC    NC is number of commands to execute "
+            "(def: 1)\n"
+            "    --grpnum=GN|-g GN    GN is group number to place in "
+            "PRE-FETCH\n"
+            "                         cdb; 0 to 63 (def: 0)\n"
+            "    --help|-h           print out usage message\n"
+            "    --immed|-i          set IMMED bit in PRE-FETCH command\n"
+            "    --lba=LBA|-l LBA    starting Logical Block Address (LBA) "
+            "(def: 0)\n"
+            "    --num-blocks=NUM|-n NUM    number of blocks to cache (for "
+            "PRE-FETCH)\n"
+            "                               (def: 1). Ignored by "
+            "SEEK(10)\n");
+    pr2serr("    --pre-fetch|-p     do PRE-FETCH command, 16 byte variant if "
+            "--10 not\n"
+            "                       given (def: do SEEK(10))\n"
+            "    --readonly|-r      open DEVICE read-only (if supported)\n"
+            "    --skip=SB|-s SB    when NC>1 skip SB blocks to next LBA "
+            "(def: 1)\n"
+            "    --time|-t          time the command(s) and if NC>1 show "
+            "usecs/command\n"
+            "                       (def: don't time)\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n"
+            "    --wrap-offset=WO|-w WO    if SB>0 and WO>0 then if "
+            "LBAn>LBA+WO\n"
+            "                       then reset LBAn back to LBA (def: 0)\n\n"
+            "Performs SCSI SEEK(10), PRE-FETCH(10) or PRE-FETCH(16) "
+            "command(s).If no\noptions are given does one SEEK(10) command "
+            "with an LBA of 0 . If NC>1\nthen a tally is kept of successes, "
+            "'condition-met's and errors that is\nprinted on completion. "
+            "'condition-met' is from PRE-FETCH when NUM blocks\nfit in "
+            "the DEVICE's cache.\n"
+           );
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool cdb10 = false;
+    bool count_given = false;
+    bool do_time = false;
+    bool immed = false;
+    bool prefetch = false;
+    bool readonly = false;
+    bool start_tm_valid = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c;
+    int sg_fd = -1;
+    int first_err = 0;
+    int last_err = 0;
+    int ret = 0;
+    int verbose = 0;
+    uint32_t count = 1;
+    int32_t l;
+    uint32_t grpnum = 0;
+    uint32_t k;
+    uint32_t num_cond_met = 0;
+    uint32_t num_err = 0;
+    uint32_t num_good = 0;
+    uint32_t numblocks = 1;
+    uint32_t skip = 1;
+    uint32_t wrap_offs = 0;
+    int64_t ll;
+    int64_t elapsed_usecs = 0;
+    uint64_t lba = 0;
+    uint64_t lba_n;
+    const char * device_name = NULL;
+    const char * cdb_name = NULL;
+    char b[64];
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+    struct timespec start_tm, end_tm;
+#elif defined(HAVE_GETTIMEOFDAY)
+    struct timeval start_tm, end_tm;
+#endif
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "c:g:hil:n:prs:tTvVw:", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            l = sg_get_num(optarg);
+            if (l < 0) {
+                pr2serr("--count= unable to decode argument, want 0 or "
+                        "higher\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            count = (uint32_t)l;
+            count_given = true;
+            break;
+        case 'g':
+            l = sg_get_num(optarg);
+            if ((l > 63) || (l < 0)) {
+                pr2serr("--grpnum= expect argument in range 0 to 63\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            grpnum = (uint32_t)l;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+            immed = true;
+            break;
+        case 'l':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("--lba= unable to decode argument\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            lba = (uint64_t)ll;
+            break;
+        case 'n':
+            l = sg_get_num(optarg);
+            if (-1 == l) {
+                pr2serr("--num= unable to decode argument\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            numblocks = (uint32_t)l;
+            break;
+        case 'p':
+            prefetch = true;
+            break;
+        case 'r':
+            readonly = true;
+            break;
+        case 's':
+            l = sg_get_num(optarg);
+            if (-1 == l) {
+                pr2serr("--skip= unable to decode argument\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            skip = (uint32_t)l;
+            break;
+        case 't':
+            do_time = true;
+            break;
+        case 'T':
+            cdb10 = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        case 'w':
+            l = sg_get_num(optarg);
+            if (-1 == l) {
+                pr2serr("--wrap-offset= unable to decode argument\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            wrap_offs = (uint32_t)l;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (prefetch) {
+        if (cdb10)
+            cdb_name = "Pre-fetch(10)";
+        else
+            cdb_name = "Pre-fetch(16)";
+    } else
+        cdb_name = "Seek(10)";
+
+    sg_fd = sg_cmds_open_device(device_name, readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s %s\n", device_name, cdb_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_nsec = 0;
+        if (0 == clock_gettime(CLOCK_MONOTONIC, &start_tm))
+            start_tm_valid = true;
+        else
+            perror("clock_gettime(CLOCK_MONOTONIC)\n");
+    }
+#elif defined(HAVE_GETTIMEOFDAY)
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+        start_tm_valid = true;
+    }
+#else
+    start_tm_valid = false;
+#endif
+
+    for (k = 0, lba_n = lba; k < count; ++k, lba_n += skip) {
+        if (wrap_offs && (lba_n > lba) && ((lba_n - lba) > wrap_offs))
+            lba_n = lba;
+        res = sg_ll_pre_fetch_x(sg_fd, ! prefetch, ! cdb10, immed, lba_n,
+                                numblocks, grpnum, 0, (verbose > 0), verbose);
+        ret = res;      /* last command executed sets exit status */
+        if (SG_LIB_CAT_CONDITION_MET == res)
+            ++num_cond_met;
+        else if (res) {
+            ++num_err;
+            if (0 == first_err)
+                first_err = res;
+            last_err = res;
+        } else
+            ++num_good;
+    }
+
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+    if ((count > 0) && start_tm_valid &&
+        (start_tm.tv_sec || start_tm.tv_nsec)) {
+        int err;
+
+        res = clock_gettime(CLOCK_MONOTONIC, &end_tm);
+        if (res < 0) {
+            err = errno;
+            perror("clock_gettime");
+            if (EINVAL == err)
+                pr2serr("clock_gettime(CLOCK_MONOTONIC) not supported\n");
+        }
+        elapsed_usecs = (end_tm.tv_sec - start_tm.tv_sec) * 1000000;
+        /* Note that (end_tm.tv_nsec - start_tm.tv_nsec) may be negative */
+        elapsed_usecs += (end_tm.tv_nsec - start_tm.tv_nsec) / 1000;
+    }
+#elif defined(HAVE_GETTIMEOFDAY)
+    if ((count > 0) && start_tm_valid &&
+        (start_tm.tv_sec || start_tm.tv_usec)) {
+        gettimeofday(&end_tm, NULL);
+        elapsed_usecs = (end_tm.tv_sec - start_tm.tv_sec) * 1000000;
+        elapsed_usecs += (end_tm.tv_usec - start_tm.tv_usec);
+    }
+#endif
+
+    if (elapsed_usecs > 0) {
+        if (elapsed_usecs > 1000000)
+            snprintf(b, sizeof(b), " (over %d seconds)",
+                    (int)elapsed_usecs / 1000000);
+        else
+            b[0] = '\0';
+        printf("Elapsed time: %" PRId64 " microseconds%s, per command time: "
+               "%" PRId64 "\n", elapsed_usecs, b, elapsed_usecs / count);
+    }
+
+    if (count_given && verbose_given)
+        printf("Command count=%u, number of condition_mets=%u, number of "
+               "goods=%u\n", count, num_cond_met, num_good);
+    if (first_err) {
+        bool printed;
+
+        printf(" number of errors=%d\n", num_err);
+        printf("    first error");
+        printed = sg_if_can2stdout(": ", first_err);
+        if (! printed)
+            printf(" code: %d\n", first_err);
+        if (num_err > 1) {
+            printf("    last error");
+            printed = sg_if_can2stdout(": ", last_err);
+            if (! printed)
+                printf(" code: %d\n", last_err);
+        }
+    }
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        const char * e_str = (SG_LIB_CAT_CONDITION_MET == ret) ?
+                             "sg_seek: " : "sg_seek: failed";
+        
+        if (! sg_if_can2stderr(e_str, ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_senddiag.c b/src/sg_senddiag.c
new file mode 100644
index 0000000..7e82dd4
--- /dev/null
+++ b/src/sg_senddiag.c
@@ -0,0 +1,971 @@
+/*
+ * A utility program originally written for the Linux OS SCSI subsystem
+ *    Copyright (C) 2003-2022 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+
+   This program issues the SCSI SEND DIAGNOSTIC command and in one case
+   the SCSI RECEIVE DIAGNOSTIC command to list supported diagnostic pages.
+*/
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#if SG_LIB_WIN32
+#include "sg_pt.h"      /* needed for scsi_pt_win32_direct() */
+#endif
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "0.65 20220128";
+
+#define ME "sg_senddiag: "
+
+#define DEF_ALLOC_LEN (1024 * 4)
+
+static struct option long_options[] = {
+        {"doff", no_argument, 0, 'd'},
+        {"extdur", no_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"list", no_argument, 0, 'l'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"new", no_argument, 0, 'N'},
+        {"old", no_argument, 0, 'O'},
+        {"page", required_argument, 0, 'P'},
+        {"pf", no_argument, 0, 'p'},
+        {"raw", required_argument, 0, 'r'},
+        {"selftest", required_argument, 0, 's'},
+        {"test", no_argument, 0, 't'},
+        {"timeout", required_argument, 0, 'T'},
+        {"uoff", no_argument, 0, 'u'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool do_deftest;
+    bool do_doff;
+    bool do_extdur;
+    bool do_list;
+    bool do_pf;
+    bool do_raw;
+    bool do_uoff;
+    bool opt_new;
+    bool verbose_given;
+    bool version_given;
+    int do_help;
+    int do_hex;
+    int maxlen;
+    int page_code;
+    int do_selftest;
+    int timeout;
+    int verbose;
+    const char * device_name;
+    const char * raw_arg;
+};
+
+
+static void
+usage()
+{
+    printf("Usage: sg_senddiag [--doff] [--extdur] [--help] [--hex] "
+           "[--list]\n"
+           "                   [--maxlen=LEN] [--page=PG] [--pf] "
+           "[--raw=H,H...]\n"
+           "                   [--selftest=ST] [--test] [--timeout=SECS] "
+           "[--uoff]\n"
+           "                   [--verbose] [--version] [DEVICE]\n"
+           "  where:\n"
+           "    --doff|-d       device online (def: 0, only with '--test')\n"
+           "    --extdur|-e     duration of an extended self-test (from mode "
+           "page 0xa)\n"
+           "    --help|-h       print usage message then exit\n"
+           "    --hex|-H        output RDR in hex; twice: plus ASCII; thrice: "
+           "suitable\n"
+           "                    for '--raw=-' with later invocation\n"
+           "    --list|-l       list supported page codes (with or without "
+           "DEVICE)\n"
+           "    --maxlen=LEN|-m LEN    parameter list length or maximum "
+           "allocation\n"
+           "                           length (default: 4096 bytes)\n"
+           "    --page=PG|-P PG    do RECEIVE DIAGNOSTIC RESULTS only, set "
+           "PCV\n"
+           "    --pf|-p         set PF bit (def: 0)\n"
+           "    --raw=H,H...|-r H,H...    sequence of hex bytes to form "
+           "diag page to send\n"
+           "    --raw=-|-r -    read stdin for sequence of bytes to send\n"
+           "    --selftest=ST|-s ST    self-test code, default: 0 "
+           "(inactive)\n"
+           "                           1->background short, 2->background "
+           "extended\n"
+           "                           4->abort test\n"
+           "                           5->foreground short, 6->foreground "
+           "extended\n"
+           "    --test|-t       default self-test\n"
+           "    --timeout=SECS|-T SECS    timeout for foreground self tests\n"
+           "                            unit: second (def: 7200 seconds)\n"
+           "    --uoff|-u       unit offline (def: 0, only with '--test')\n"
+           "    --verbose|-v    increase verbosity\n"
+           "    --old|-O        use old interface (use as first option)\n"
+           "    --version|-V    output version string then exit\n\n"
+           "Performs a SCSI SEND DIAGNOSTIC (and/or a RECEIVE DIAGNOSTIC "
+           "RESULTS) command\n"
+        );
+}
+
+static void
+usage_old()
+{
+    printf("Usage: sg_senddiag [-doff] [-e] [-h] [-H] [-l] [-pf]"
+           " [-raw=H,H...]\n"
+           "                   [-s=SF] [-t] [-T=SECS] [-uoff] [-v] [-V] "
+           "[DEVICE]\n"
+           "  where:\n"
+           "    -doff   device online (def: 0, only with '-t')\n"
+           "    -e      duration of an extended self-test (from mode page "
+           "0xa)\n"
+           "    -h      output in hex\n"
+           "    -H      output in hex (same as '-h')\n"
+           "    -l      list supported page codes\n"
+           "    -pf     set PF bit (def: 0)\n"
+           "    -raw=H,H...    sequence of bytes to form diag page to "
+           "send\n"
+           "    -raw=-  read stdin for sequence of bytes to send\n"
+           "    -s=SF   self-test code (def: 0)\n"
+           "            1->background short, 2->background extended,"
+           " 4->abort test\n"
+           "            5->foreground short, 6->foreground extended\n"
+           "    -t      default self-test\n"
+           "    -T SECS    timeout for foreground self tests\n"
+           "    -uoff   unit offline (def: 0, only with '-t')\n"
+           "    -v      increase verbosity (print issued SCSI cmds)\n"
+           "    -V      output version string\n"
+           "    -N|--new   use new interface\n"
+           "    -?      output this usage message\n\n"
+           "Performs a SCSI SEND DIAGNOSTIC (and/or a RECEIVE DIAGNOSTIC "
+           "RESULTS) command\n"
+        );
+}
+
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int c, n;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "dehHlm:NOpP:r:s:tT:uvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'd':
+            op->do_doff = true;
+            break;
+        case 'e':
+            op->do_extdur = true;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'l':
+            op->do_list = true;
+            break;
+        case 'm':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 0xffff)) {
+                pr2serr("bad argument to '--maxlen=' or greater than 65535 "
+                        "[0xffff]\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->maxlen = n;
+            break;
+        case 'N':
+            break;      /* ignore */
+        case 'O':
+            op->opt_new = false;
+            return 0;
+        case 'p':
+            op->do_pf = true;
+            break;
+        case 'P':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 0xff)) {
+                pr2serr("bad argument to '--page=' or greater than 255 "
+                        "[0xff]\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->page_code = n;
+            break;
+        case 'r':
+            op->raw_arg = optarg;
+            op->do_raw = true;
+            break;
+        case 's':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 7)) {
+                pr2serr("bad argument to '--selftest='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_selftest = n;
+            break;
+        case 't':
+            op->do_deftest = true;
+            break;
+        case 'T':
+            n = sg_get_num(optarg);
+            if (n < 0) {
+                pr2serr("bad argument to '--timeout=SECS'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->timeout = n;
+            break;
+        case 'u':
+            op->do_uoff = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool jmp_out;
+    int k, plen, num, n;
+    unsigned int u;
+    const char * cp;
+
+    for (k = 1; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case 'd':
+                    if (0 == strncmp("doff", cp, 4)) {
+                        op->do_doff = true;
+                        cp += 3;
+                        plen -= 3;
+                    } else
+                        jmp_out = true;
+                    break;
+                case 'e':
+                    op->do_extdur = true;
+                    break;
+                case 'h':
+                case 'H':
+                    ++op->do_hex;
+                    break;
+                case 'l':
+                    op->do_list = true;
+                    break;
+                case 'N':
+                    op->opt_new = true;
+                    return 0;
+                case 'O':
+                    break;
+                case 'p':
+                    if (0 == strncmp("pf", cp, 2)) {
+                        op->do_pf = true;
+                        ++cp;
+                        --plen;
+                    } else
+                        jmp_out = true;
+                    break;
+                case 't':
+                    op->do_deftest = true;
+                    break;
+                case 'u':
+                    if (0 == strncmp("uoff", cp, 4)) {
+                        op->do_uoff = true;
+                        cp += 3;
+                        plen -= 3;
+                    } else
+                        jmp_out = true;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                case '?':
+                    ++op->do_help;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            if (0 == strncmp("raw=", cp, 4)) {
+                op->raw_arg = cp + 4;
+                op->do_raw = true;
+            } else if (0 == strncmp("s=", cp, 2)) {
+                num = sscanf(cp + 2, "%x", &u);
+                if ((1 != num) || (u > 7)) {
+                    printf("Bad page code after '-s=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->do_selftest = u;
+            } else if (0 == strncmp("T=", cp, 2)) {
+                num = sscanf(cp + 2, "%d", &n);
+                if ((1 != num) || (n < 0)) {
+                    printf("Bad page code after '-T=SECS' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->timeout = n;
+            } else if (0 == strncmp("-old", cp, 5))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage_old();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == op->device_name)
+            op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not expecting: %s\n",
+                    op->device_name, cp);
+            usage_old();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opt_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opt_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opt_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (! op->opt_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+/* Return of 0 -> success, otherwise see sg_ll_send_diag() */
+static int
+do_senddiag(int sg_fd, int sf_code, bool pf_bit, bool sf_bit,
+            bool devofl_bit, bool unitofl_bit, void * outgoing_pg,
+            int outgoing_len, int tmout, bool noisy, int verbose)
+{
+    int long_duration = 0;
+
+    if ((0 == sf_bit) && ((5 == sf_code) || (6 == sf_code))) {
+        /* foreground self-tests */
+        if (tmout <= 0)
+            long_duration = 1;
+        else
+            long_duration = tmout;
+    }
+    return sg_ll_send_diag(sg_fd, sf_code, pf_bit, sf_bit, devofl_bit,
+                           unitofl_bit, long_duration, outgoing_pg,
+                           outgoing_len, noisy, verbose);
+}
+
+/* Get expected extended self-test time from mode page 0xa (for '-e') */
+static int
+do_modes_0a(int sg_fd, void * resp, int mx_resp_len, bool mode6, bool noisy,
+            int verbose)
+{
+    int res;
+    int resid = 0;
+
+    if (mode6)
+        res = sg_ll_mode_sense6(sg_fd, true /* dbd */, false /* pc */,
+                                0xa /* page */, false, resp, mx_resp_len,
+                                noisy, verbose);
+    else
+        res = sg_ll_mode_sense10_v2(sg_fd, false /* llbaa */, true /* dbd */,
+                                    false, 0xa, false, resp, mx_resp_len,
+                                    0, &resid, noisy, verbose);
+    if (res) {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Mode sense (%s): %s\n", (mode6 ? "6" : "10"), b);
+    } else {
+        mx_resp_len -= resid;
+        if (mx_resp_len < 4) {
+            pr2serr("%s: response length (%d) too small (resid=%d)\n",
+                    __func__, mx_resp_len, resid);
+            res = SG_LIB_WILD_RESID;
+        }
+    }
+    return res;
+}
+
+/* Read hex numbers from command line (comma separated list) or from */
+/* stdin (one per line, comma separated list or space separated list). */
+/* Returns 0 if ok, or 1 if error. */
+static int
+build_diag_page(const char * inp, uint8_t * mp_arr, int * mp_arr_len,
+                int max_arr_len)
+{
+    int in_len, k, j, m;
+    unsigned int h;
+    const char * lcp;
+    char * cp;
+    char * c2p;
+
+    if ((NULL == inp) || (NULL == mp_arr) ||
+        (NULL == mp_arr_len))
+        return 1;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *mp_arr_len = 0;
+    if ('-' == inp[0]) {        /* read from stdin */
+        bool split_line;
+        int off = 0;
+        char line[512];
+        char carry_over[4];
+
+        carry_over[0] = 0;
+        for (j = 0; j < 512; ++j) {
+            if (NULL == fgets(line, sizeof(line), stdin))
+                break;
+            in_len = strlen(line);
+            if (in_len > 0) {
+                if ('\n' == line[in_len - 1]) {
+                    --in_len;
+                    line[in_len] = '\0';
+                    split_line = false;
+                } else
+                    split_line = true;
+            }
+            if (in_len < 1) {
+                carry_over[0] = 0;
+                continue;
+            }
+            if (carry_over[0]) {
+                if (isxdigit((uint8_t)line[0])) {
+                    carry_over[1] = line[0];
+                    carry_over[2] = '\0';
+                    if (1 == sscanf(carry_over, "%x", &h))
+                        mp_arr[off - 1] = h;       /* back up and overwrite */
+                    else {
+                        pr2serr("build_diag_page: carry_over error ['%s'] "
+                                "around line %d\n", carry_over, j + 1);
+                        return 1;
+                    }
+                    lcp = line + 1;
+                    --in_len;
+                } else
+                    lcp = line;
+                carry_over[0] = 0;
+            } else
+                lcp = line;
+            m = strspn(lcp, " \t");
+            if (m == in_len)
+                continue;
+            lcp += m;
+            in_len -= m;
+            if ('#' == *lcp)
+                continue;
+            k = strspn(lcp, "0123456789aAbBcCdDeEfF ,\t");
+            if ((k < in_len) && ('#' != lcp[k])) {
+                pr2serr("build_diag_page: syntax error at line %d, pos %d\n",
+                        j + 1, m + k + 1);
+                return 1;
+            }
+            for (k = 0; k < 1024; ++k) {
+                if (1 == sscanf(lcp, "%x", &h)) {
+                    if (h > 0xff) {
+                        pr2serr("build_diag_page: hex number larger than "
+                                "0xff in line %d, pos %d\n", j + 1,
+                                (int)(lcp - line + 1));
+                        return 1;
+                    }
+                    if (split_line && (1 == strlen(lcp))) {
+                        /* single trailing hex digit might be a split pair */
+                        carry_over[0] = *lcp;
+                    }
+                    if ((off + k) >= max_arr_len) {
+                        pr2serr("build_diag_page: array length exceeded\n");
+                        return 1;
+                    }
+                    mp_arr[off + k] = h;
+                    lcp = strpbrk(lcp, " ,\t");
+                    if (NULL == lcp)
+                        break;
+                    lcp += strspn(lcp, " ,\t");
+                    if ('\0' == *lcp)
+                        break;
+                } else {
+                    if ('#' == *lcp) {
+                        --k;
+                        break;
+                    }
+                    pr2serr("build_diag_page: error in line %d, at pos %d\n",
+                            j + 1, (int)(lcp - line + 1));
+                    return 1;
+                }
+            }
+            off += (k + 1);
+        }
+        *mp_arr_len = off;
+    } else {        /* hex string on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfF, ");
+        if (in_len != k) {
+            pr2serr("build_diag_page: error at pos %d\n", k + 1);
+            return 1;
+        }
+        for (k = 0; k < max_arr_len; ++k) {
+            if (1 == sscanf(lcp, "%x", &h)) {
+                if (h > 0xff) {
+                    pr2serr("build_diag_page: hex number larger than 0xff at "
+                            "pos %d\n", (int)(lcp - inp + 1));
+                    return 1;
+                }
+                mp_arr[k] = h;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("build_diag_page: error at pos %d\n",
+                        (int)(lcp - inp + 1));
+                return 1;
+            }
+        }
+        *mp_arr_len = k + 1;
+        if (k == max_arr_len) {
+            pr2serr("build_diag_page: array length exceeded\n");
+            return 1;
+        }
+    }
+    return 0;
+}
+
+
+struct page_code_desc {
+        int page_code;
+        const char * desc;
+};
+static struct page_code_desc pc_desc_arr[] = {
+        {0x0, "Supported diagnostic pages"},
+        {0x1, "Configuration (SES)"},
+        {0x2, "Enclosure status/control (SES)"},
+        {0x3, "Help text (SES)"},
+        {0x4, "String In/Out (SES)"},
+        {0x5, "Threshold In/Out (SES)"},
+        {0x6, "Array Status/Control (SES, obsolete)"},
+        {0x7, "Element descriptor (SES)"},
+        {0x8, "Short enclosure status (SES)"},
+        {0x9, "Enclosure busy (SES-2)"},
+        {0xa, "Additional (device) element status (SES-2)"},
+        {0xb, "Subenclosure help text (SES-2)"},
+        {0xc, "Subenclosure string In/Out (SES-2)"},
+        {0xd, "Supported SES diagnostic pages (SES-2)"},
+        {0xe, "Download microcode diagnostic pages (SES-2)"},
+        {0xf, "Subenclosure nickname diagnostic pages (SES-2)"},
+        {0x3f, "Protocol specific (SAS transport)"},
+        {0x40, "Translate address (direct access)"},
+        {0x41, "Device status (direct access)"},
+        {0x42, "Rebuild assist (direct access)"}, /* sbc3r31 */
+};
+
+static const char *
+find_page_code_desc(int page_num)
+{
+    int k;
+    int num = SG_ARRAY_SIZE(pc_desc_arr);
+    const struct page_code_desc * pcdp = &pc_desc_arr[0];
+
+    for (k = 0; k < num; ++k, ++pcdp) {
+        if (page_num == pcdp->page_code)
+            return pcdp->desc;
+        else if (page_num < pcdp->page_code)
+            return NULL;
+    }
+    return NULL;
+}
+
+static void
+list_page_codes()
+{
+    int k;
+    int num = SG_ARRAY_SIZE(pc_desc_arr);
+    const struct page_code_desc * pcdp = &pc_desc_arr[0];
+
+    printf("Page_Code  Description\n");
+    for (k = 0; k < num; ++k, ++pcdp)
+        printf(" 0x%02x      %s\n", pcdp->page_code,
+               (pcdp->desc ? pcdp->desc : "<unknown>"));
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    int k, num, rsp_len, res, rsp_buff_size, pg, bd_len, resid, vb;
+    int sg_fd = -1;
+    int read_in_len = 0;
+    int ret = 0;
+    struct opts_t opts;
+    struct opts_t * op;
+    uint8_t * rsp_buff = NULL;
+    uint8_t * free_rsp_buff = NULL;
+    const char * cp;
+    uint8_t * read_in = NULL;
+    uint8_t * free_read_in = NULL;
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->maxlen = DEF_ALLOC_LEN;
+    op->page_code = -1;
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return SG_LIB_SYNTAX_ERROR;
+    if (op->do_help) {
+        if (op->opt_new)
+            usage();
+        else
+            usage_old();
+        return 0;
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+
+    rsp_buff_size = op->maxlen;
+
+    if (NULL == op->device_name) {
+        if (op->do_list) {
+            list_page_codes();
+            return 0;
+        }
+        pr2serr("No DEVICE argument given\n\n");
+        if (op->opt_new)
+            usage();
+        else
+            usage_old();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    vb = op->verbose;
+    if (op->do_raw) {
+        read_in = sg_memalign(op->maxlen, 0, &free_read_in, vb > 3);
+        if (NULL == read_in) {
+            pr2serr("unable to allocate %d bytes\n", op->maxlen);
+            return SG_LIB_CAT_OTHER;
+        }
+        if (build_diag_page(op->raw_arg, read_in, &read_in_len, op->maxlen)) {
+            if (op->opt_new) {
+                printf("Bad sequence after '--raw=' option\n");
+                usage();
+            } else {
+                printf("Bad sequence after '-raw=' option\n");
+                usage_old();
+            }
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto fini;
+        }
+    }
+
+    if ((op->do_doff || op->do_uoff) && (! op->do_deftest)) {
+        if (op->opt_new) {
+            printf("setting --doff or --uoff only useful when -t is set\n");
+            usage();
+        } else {
+            printf("setting -doff or -uoff only useful when -t is set\n");
+            usage_old();
+        }
+        ret = SG_LIB_CONTRADICT;
+        goto fini;
+    }
+    if ((op->do_selftest > 0) && op->do_deftest) {
+        if (op->opt_new) {
+            printf("either set --selftest=SF or --test (not both)\n");
+            usage();
+        } else {
+            printf("either set -s=SF or -t (not both)\n");
+            usage_old();
+        }
+        ret = SG_LIB_CONTRADICT;
+        goto fini;
+    }
+    if (op->do_raw) {
+        if ((op->do_selftest > 0) || op->do_deftest || op->do_extdur ||
+            op->do_list) {
+            if (op->opt_new) {
+                printf("'--raw=' cannot be used with self-tests, '-e' or "
+                       "'-l'\n");
+                usage();
+            } else {
+                printf("'-raw=' cannot be used with self-tests, '-e' or "
+                       "'-l'\n");
+                usage_old();
+            }
+            ret = SG_LIB_CONTRADICT;
+            goto fini;
+        }
+        if (! op->do_pf) {
+            if (op->opt_new)
+                printf(">>> warning, '--pf' probably should be used with "
+                       "'--raw='\n");
+            else
+                printf(">>> warning, '-pf' probably should be used with "
+                       "'-raw='\n");
+        }
+    }
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+    if (vb > 4)
+        pr2serr("Initial win32 SPT interface state: %s\n",
+                scsi_pt_win32_spt_state() ? "direct" : "indirect");
+    if (op->maxlen >= 16384)
+        scsi_pt_win32_direct(SG_LIB_WIN32_DIRECT /* SPT pt interface */);
+#endif
+#endif
+
+    if ((sg_fd = sg_cmds_open_device(op->device_name, false /* rw */, vb)) <
+         0) {
+        if (vb)
+            pr2serr(ME "error opening file: %s: %s\n", op->device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+    rsp_buff = sg_memalign(op->maxlen, 0, &free_rsp_buff, vb > 3);
+    if (NULL == rsp_buff) {
+        pr2serr("unable to allocate %d bytes (2)\n", op->maxlen);
+        ret = SG_LIB_CAT_OTHER;
+        goto close_fini;
+    }
+    if (op->do_extdur) {  /* fetch Extended self-test time from Control
+                           * mode page with Mode Sense(10) command*/
+        res = do_modes_0a(sg_fd, rsp_buff, 32, false /* mode6 */,
+                          true /* noisy */, vb);
+        if (0 == res) {
+            /* Mode sense(10) response, step over any block descriptors */
+            num = sg_msense_calc_length(rsp_buff, 32, false, &bd_len);
+            num -= (8 /* MS(10) header length */ + bd_len);
+            if (num >= 0xc) {
+                int secs = sg_get_unaligned_be16(rsp_buff + 8 + bd_len + 10);
+
+		if (0xffff == secs) {
+		    if (op->verbose > 1)
+			printf("Expected extended self-test duration's value "
+			       "[65535] indicates the\nsimilarly named field "
+			       "in the Extended Inquiry VPD page should be "
+			       "used\n");
+		} else {
+#ifdef SG_LIB_MINGW
+                    printf("Expected extended self-test duration=%d seconds "
+                           "(%g minutes)\n", secs, secs / 60.0);
+#else
+                    printf("Expected extended self-test duration=%d seconds "
+                           "(%.2f minutes)\n", secs, secs / 60.0);
+#endif
+		}
+            } else
+                printf("Extended self-test duration not available\n");
+        } else {
+            ret = res;
+            printf("Extended self-test duration (mode page 0xa) failed\n");
+            goto err_out9;
+        }
+    } else if (op->do_list || (op->page_code >= 0x0)) {
+        pg = op->page_code;
+        if (pg < 0)
+            res = do_senddiag(sg_fd, 0, true /* pf */, false, false, false,
+                              rsp_buff, 4, op->timeout, 1, vb);
+        else
+            res = 0;
+        if (0 == res) {
+            resid = 0;
+            if (0 == sg_ll_receive_diag_v2(sg_fd, (pg >= 0x0),
+                                           ((pg >= 0x0) ? pg : 0), rsp_buff,
+                                           rsp_buff_size, 0, &resid,
+                                           true, vb)) {
+                rsp_buff_size -= resid;
+                if (rsp_buff_size < 4) {
+                    pr2serr("RD resid (%d) indicates response too small "
+                            "(lem=%d)\n", resid, rsp_buff_size);
+                    goto err_out;
+                }
+                rsp_len = sg_get_unaligned_be16(rsp_buff + 2) + 4;
+                rsp_len= (rsp_len < rsp_buff_size) ? rsp_len : rsp_buff_size;
+                if (op->do_hex > 1)
+                    hex2stdout(rsp_buff, rsp_len,
+                            (2 == op->do_hex) ? 0 : -1);
+                else if (pg < 0x1) {
+                    printf("Supported diagnostic pages response:\n");
+                    if (op->do_hex)
+                        hex2stdout(rsp_buff, rsp_len, 1);
+                    else {
+                        for (k = 0; k < (rsp_len - 4); ++k) {
+                            pg = rsp_buff[k + 4];
+                            cp = find_page_code_desc(pg);
+                            if (NULL == cp)
+                                cp = (pg < 0x80) ? "<unknown>" :
+                                                   "<vendor specific>";
+                            printf("  0x%02x  %s\n", pg, cp);
+                        }
+                    }
+                } else {
+                    cp = find_page_code_desc(pg);
+                    if (cp)
+                        printf("%s diagnostic page [0x%x] response in "
+                               "hex:\n", cp, pg);
+                    else
+                        printf("diagnostic page 0x%x response in hex:\n", pg);
+                    hex2stdout(rsp_buff, rsp_len, 1);
+                }
+            } else {
+                ret = res;
+                pr2serr("RECEIVE DIAGNOSTIC RESULTS command failed\n");
+                goto err_out9;
+            }
+        } else {
+            ret = res;
+            goto err_out;
+        }
+    } else if (op->do_raw) {
+        res = do_senddiag(sg_fd, 0, op->do_pf, false, false, false, read_in,
+                          read_in_len, op->timeout, 1, vb);
+        if (res) {
+            ret = res;
+            goto err_out;
+        }
+    } else {
+        res = do_senddiag(sg_fd, op->do_selftest, op->do_pf, op->do_deftest,
+                          op->do_doff, op->do_uoff, NULL, 0, op->timeout, 1,
+                          vb);
+        if (0 == res) {
+            if ((5 == op->do_selftest) || (6 == op->do_selftest))
+                printf("Foreground self-test returned GOOD status\n");
+            else if (op->do_deftest && (! op->do_doff) && (! op->do_uoff))
+                printf("Default self-test returned GOOD status\n");
+        } else {
+            ret = res;
+            goto err_out;
+        }
+    }
+    goto close_fini;
+
+err_out:
+    if (SG_LIB_CAT_UNIT_ATTENTION == res)
+        pr2serr("SEND DIAGNOSTIC, unit attention\n");
+    else if (SG_LIB_CAT_ABORTED_COMMAND == res)
+        pr2serr("SEND DIAGNOSTIC, aborted command\n");
+    else if (SG_LIB_CAT_NOT_READY == res)
+        pr2serr("SEND DIAGNOSTIC, device not ready\n");
+    else
+        pr2serr("SEND DIAGNOSTIC command, failed\n");
+err_out9:
+    if (vb < 2)
+        pr2serr("  try again with '-vv' for more information\n");
+close_fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+fini:
+    if (free_read_in)
+        free(free_read_in);
+    if (free_rsp_buff)
+        free(free_rsp_buff);
+    if (0 == vb) {
+        if (! sg_if_can2stderr("sg_senddiag failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_ses.c b/src/sg_ses.c
new file mode 100644
index 0000000..6ac26e8
--- /dev/null
+++ b/src/sg_ses.c
@@ -0,0 +1,5986 @@
+/*
+ * Copyright (c) 2004-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pt.h"
+#include "sg_pr2serr.h"
+
+/*
+ * This program issues SCSI SEND DIAGNOSTIC and RECEIVE DIAGNOSTIC RESULTS
+ * commands tailored for SES (enclosure) devices.
+ */
+
+static const char * version_str = "2.58 20220813";    /* ses4r04 */
+
+#define MX_ALLOC_LEN ((64 * 1024) - 4)  /* max allowable for big enclosures */
+#define MX_ELEM_HDR 1024
+#define REQUEST_SENSE_RESP_SZ 252
+#define DATA_IN_OFF 4
+#define MIN_MAXLEN 16
+#define MIN_DATA_IN_SZ 8192     /* use max(MIN_DATA_IN_SZ, op->maxlen) for
+                                 * the size of data_arr */
+#define MX_DATA_IN_LINES (16 * 1024)
+#define MX_JOIN_ROWS 520        /* element index fields in dpages are only 8
+                                 * bit, and index 0xff (255) is sometimes used
+                                 * for 'not applicable'. However this limit
+                                 * can bypassed with sub-enclosure numbers.
+                                 * So try higher figure. */
+#define MX_DATA_IN_DESCS 32
+#define NUM_ACTIVE_ET_AESP_ARR 32
+
+#define TEMPERAT_OFF 20         /* 8 bits represents -19 C to +235 C */
+                                /* value of 0 (would imply -20 C) reserved */
+
+/* Send Diagnostic and Receive Diagnostic Results page codes */
+/* Sometimes referred to as "dpage"s in code comments */
+#define SUPPORTED_DPC 0x0
+#define CONFIGURATION_DPC 0x1
+#define ENC_CONTROL_DPC 0x2
+#define ENC_STATUS_DPC 0x2
+#define HELP_TEXT_DPC 0x3
+#define STRING_DPC 0x4
+#define THRESHOLD_DPC 0x5
+#define ARRAY_CONTROL_DPC 0x6   /* obsolete, last seen ses-r08b.pdf */
+#define ARRAY_STATUS_DPC 0x6    /* obsolete */
+#define ELEM_DESC_DPC 0x7
+#define SHORT_ENC_STATUS_DPC 0x8
+#define ENC_BUSY_DPC 0x9
+#define ADD_ELEM_STATUS_DPC 0xa /* Additional Element Status dpage code */
+#define SUBENC_HELP_TEXT_DPC 0xb
+#define SUBENC_STRING_DPC 0xc
+#define SUPPORTED_SES_DPC 0xd   /* should be 0x1 <= dpc <= 0x2f */
+#define DOWNLOAD_MICROCODE_DPC 0xe
+#define SUBENC_NICKNAME_DPC 0xf
+#define ALL_DPC 0xff
+
+/* Element Type codes */
+#define UNSPECIFIED_ETC 0x0
+#define DEVICE_ETC 0x1
+#define POWER_SUPPLY_ETC 0x2
+#define COOLING_ETC 0x3
+#define TEMPERATURE_ETC 0x4
+#define DOOR_ETC 0x5    /* prior to ses3r05 was DOOR_LOCK_ETC */
+#define AUD_ALARM_ETC 0x6
+#define ENC_SCELECTR_ETC 0x7 /* Enclosure services controller electronics */
+#define SCC_CELECTR_ETC 0x8  /* SCC: SCSI Controller Commands (e.g. RAID
+                              * controller). SCC Controller Elecronics */
+#define NV_CACHE_ETC 0x9
+#define INV_OP_REASON_ETC 0xa
+#define UI_POWER_SUPPLY_ETC 0xb
+#define DISPLAY_ETC 0xc
+#define KEY_PAD_ETC 0xd
+#define ENCLOSURE_ETC 0xe
+#define SCSI_PORT_TRAN_ETC 0xf
+#define LANGUAGE_ETC 0x10
+#define COMM_PORT_ETC 0x11
+#define VOLT_SENSOR_ETC 0x12
+#define CURR_SENSOR_ETC 0x13
+#define SCSI_TPORT_ETC 0x14
+#define SCSI_IPORT_ETC 0x15
+#define SIMPLE_SUBENC_ETC 0x16
+#define ARRAY_DEV_ETC 0x17
+#define SAS_EXPANDER_ETC 0x18
+#define SAS_CONNECTOR_ETC 0x19
+#define LAST_ETC SAS_CONNECTOR_ETC      /* adjust as necessary */
+
+#define TPROTO_PCIE_PS_NVME 1   /* NVMe regarded as subset of PCIe */
+#define NUM_ETC (LAST_ETC + 1)
+
+#define DEF_CLEAR_VAL 0
+#define DEF_SET_VAL 1
+
+
+struct element_type_t {
+    int elem_type_code;
+    const char * abbrev;
+    const char * desc;
+};
+
+#define CGS_CL_ARR_MAX_SZ 8
+#define CGS_STR_MAX_SZ 80
+
+enum cgs_select_t {CLEAR_OPT, GET_OPT, SET_OPT};
+
+struct cgs_cl_t {
+    enum cgs_select_t cgs_sel;
+    bool last_cs;       /* true only for last --clear= or --set= */
+    char cgs_str[CGS_STR_MAX_SZ];
+};
+
+struct opts_t {
+    bool byte1_given;   /* true if -b B1 or --byte1=B1 given */
+    bool do_control;    /* want to write to DEVICE */
+    bool do_data;       /* flag if --data= option has been used */
+    bool do_list;
+    bool do_status;     /* want to read from DEVICE (or user data) */
+    bool eiioe_auto;    /* Element Index Includes Overall (status) Element */
+    bool eiioe_force;
+    bool ind_given;     /* '--index=...' or '-I ...' */
+    bool inner_hex;
+    bool many_dpages;   /* user supplied data has more than one dpage */
+    bool mask_ign;      /* element read-mask-modify-write actions */
+    bool o_readonly;
+    bool page_code_given;       /* or suitable abbreviation */
+    bool quiet;         /* exit status unaltered by --quiet */
+    bool seid_given;
+    bool verbose_given;
+    bool version_given;
+    bool warn;
+    int byte1;          /* (origin 0 so second byte) in Control dpage */
+    int dev_slot_num;
+    int do_filter;
+    int do_help;
+    int do_hex;
+    int do_join;        /* relational join of Enclosure status, Element
+                           descriptor and Additional element status dpages.
+                           Use twice to add Threshold in dpage to join. */
+    int do_raw;
+    int enumerate;
+    int ind_th;    /* type header index, set by build_type_desc_hdr_arr() */
+    int ind_indiv;      /* individual element index; -1 for overall */
+    int ind_indiv_last; /* if > ind_indiv then [ind_indiv..ind_indiv_last] */
+    int ind_et_inst;    /* ETs can have multiple type header instances */
+    int maxlen;
+    int seid;
+    int page_code;      /* recognised abbreviations converted to dpage num */
+    int verbose;
+    int num_cgs;        /* number of --clear-, --get= and --set= options */
+    int mx_arr_len;     /* allocated size of data_arr */
+    int arr_len;        /* valid bytes in data_arr */
+    uint8_t * data_arr;
+    uint8_t * free_data_arr;
+    const char * desc_name;
+    const char * dev_name;
+    const struct element_type_t * ind_etp;
+    const char * index_str;
+    const char * nickname_str;
+    struct cgs_cl_t cgs_cl_arr[CGS_CL_ARR_MAX_SZ];
+    uint8_t sas_addr[8];  /* Big endian byte sequence */
+};
+
+struct diag_page_code {
+    int page_code;
+    const char * desc;
+};
+
+struct diag_page_abbrev {
+    const char * abbrev;
+    int page_code;
+};
+
+/* The Configuration diagnostic page contains one or more of these. The
+ * elements of the Enclosure Control/Status and Threshold In/ Out page follow
+ * this format. The additional element status page is closely related to
+ * this format (with some element types and all overall elements excluded). */
+struct type_desc_hdr_t {
+    uint8_t etype;              /* element type code (0: unspecified) */
+    uint8_t num_elements;       /* number of possible elements, excluding
+                                 * overall element */
+    uint8_t se_id;              /* subenclosure id (0 for primary enclosure) */
+    uint8_t txt_len;            /* type descriptor text length; (unused) */
+};
+
+/* A SQL-like join of the Enclosure Status, Threshold In and Additional
+ * Element Status pages based of the format indicated in the Configuration
+ * page. Note that the array of these struct instances is built such that
+ * the array index is equal to the 'ei_ioe' (element index that includes
+ * overall elements). */
+struct join_row_t {  /* this struct is 72 bytes long on Intel "64" bit arch */
+    int th_i;           /* type header index (origin 0) */
+    int indiv_i;        /* individual (element) index, -1 for overall
+                         * instance, otherwise origin 0 */
+    uint8_t etype;      /* element type */
+    uint8_t se_id;      /* subenclosure id (0 for primary enclosure) */
+    int ei_eoe;         /* element index referring to Enclosure status dpage
+                         * descriptors, origin 0 and excludes overall
+                         * elements, -1 for not applicable. As defined by
+                         * SES-2 standard for the AES descriptor, EIP=1 */
+    int ei_aess;        /* subset of ei_eoe that only includes elements of
+                         * these types:  excludes DEVICE_ETC, ARRAY_DEV_ETC,
+                         * SAS_EXPANDER_ETC, SCSI_IPORT_ETC, SCSI_TPORT_ETC
+                         * and ENC_SCELECTR_ETC. -1 for not applicable */
+    /* following point into Element Descriptor, Enclosure Status, Threshold
+     * In and Additional element status diagnostic pages. enc_statp only
+     * NULL beyond last, other pointers can be NULL . */
+    const uint8_t * elem_descp;
+    uint8_t * enc_statp;  /* NULL indicates past last */
+    uint8_t * thresh_inp;
+    const uint8_t * ae_statp;
+    int dev_slot_num;           /* if not available, set to -1 */
+    uint8_t sas_addr[8];  /* big endian, if not available, set to 0 */
+};
+
+enum fj_select_t {FJ_IOE, FJ_EOE, FJ_AESS, FJ_SAS_CON};
+
+/* Instance ('tes' in main() ) holds a type_desc_hdr_t array potentially with
+   the matching join array if present. */
+struct th_es_t {
+    const struct type_desc_hdr_t * th_base;
+    int num_ths;        /* items in array pointed to by th_base */
+    struct join_row_t * j_base;
+    int num_j_rows;
+    int num_j_eoe;
+};
+
+/* Representation of <acronym>[=<value>] or
+ * <start_byte>:<start_bit>[:<num_bits>][=<value>]. Associated with
+ * --clear=, --get= or --set= option. */
+struct tuple_acronym_val {
+    const char * acron;
+    const char * val_str;
+    enum cgs_select_t cgs_sel;  /* indicates --clear=, --get= or --set= */
+    int start_byte;     /* -1 indicates no start_byte */
+    int start_bit;
+    int num_bits;
+    int64_t val;
+};
+
+/* Mapping from <acronym> to <start_byte>:<start_bit>:<num_bits> for a
+ * given element type. Table of known acronyms made from these elements. */
+struct acronym2tuple {
+    const char * acron; /* element name or acronym, NULL for past end */
+    int etype;          /* -1 for all element types */
+    int start_byte;     /* origin 0, normally 0 to 3 */
+    int start_bit;      /* 7 (MSbit or leftmost in SES drafts) to 0 (LSbit) */
+    int num_bits;       /* usually 1, maximum is 64 */
+    const char * info;  /* optional, set to NULL if not used */
+};
+
+/* Structure for holding (sub-)enclosure information found in the
+ * Configuration diagnostic page. */
+struct enclosure_info {
+    int have_info;
+    int rel_esp_id;     /* relative enclosure services process id (origin 1) */
+    int num_esp;        /* number of enclosure services processes */
+    uint8_t enc_log_id[8];        /* 8 byte NAA */
+    uint8_t enc_vendor_id[8];     /* may differ from INQUIRY response */
+    uint8_t product_id[16];       /* may differ from INQUIRY response */
+    uint8_t product_rev_level[4]; /* may differ from INQUIRY response */
+};
+
+/* When --status is given with --data= the file contents may contain more
+ * than one dpage to be decoded. */
+struct data_in_desc_t {
+    bool in_use;
+    int page_code;
+    int offset;         /* byte offset from op->data_arr + DATA_IN_OFF */
+    int dp_len;         /* byte length of this diagnostic page */
+};
+
+
+/* Join array has four "element index"ing strategies:
+ *   [1] based on all descriptors in the Enclosure Status (ES) dpage
+ *   [2] based on the non-overall descriptors in the ES dpage
+ *   [3] based on the non-overall descriptors of these element types
+ *       in the ES dpage: DEVICE_ETC, ARRAY_DEV_ETC, SAS_EXPANDER_ETC,
+ *       SCSI_IPORT_ETC, SCSI_TPORT_ETC and ENC_SCELECTR_ETC.
+ *   [4] based on the non-overall descriptors of the SAS_CONNECTOR_ETC
+ *       element type
+ *
+ * The indexes are all origin 0 with the maximum index being one less then
+ * the number of status descriptors in the ES dpage. Table of supported
+ * permutations follows:
+ *
+ *  ==========|===============================================================
+ *  Algorithm |              Indexes                  |    Notes
+ *            |Element|Connector element|Other element|
+ *  ==========|=======|=================|=============|=======================
+ *   [A]      |  [2]  |       [4]       |    [3]      | SES-2, OR
+ *   [A]      |  [2]  |       [4]       |    [3]      | SES-3,EIIOE=0
+ *  ----------|-------|-----------------|-------------|-----------------------
+ *   [B]      |  [1]  |       [1]       |    [1]      | SES-3, EIIOE=1
+ *  ----------|-------|-----------------|-------------|-----------------------
+ *   [C]      |  [2]  |       [2]       |    [2]      | SES-3, EIIOE=2
+ *  ----------|-------|-----------------|-------------|-----------------------
+ *   [D]      |  [2]  |       [1]       |    [1]      | SES-3, EIIOE=3
+ *  ----------|-------|-----------------|-------------|-----------------------
+ *   [E]      |  [1]  |       [4]       |    [3]      | EIIOE=0 and
+ *            |       |                 |             | --eiioe=force, OR
+ *   [E]      |  [1]  |       [4]       |    [3]      | {HP JBOD} EIIOE=0 and
+ *            |       |                 |             | --eiioe=auto and
+ *            |       |                 |             | AES[desc_0].ei==1 .
+ *  ----------|-------|-----------------|-------------|-----------------------
+ *   [F]      | [2->3]|       [4]       |    [3]      | "broken_ei" when any
+ *            |       |                 |             | of AES[*].ei invalid
+ *            |       |                 |             | using strategy [2]
+ *  ----------|-------|-----------------|-------------|-----------------------
+ *   [Z]      |  -    |       [4]       |    [3]      | EIP=0, implicit
+ *            |       |                 |             | element index of [3]
+ *  ==========================================================================
+ *
+ *
+ */
+static struct join_row_t join_arr[MX_JOIN_ROWS];
+static struct join_row_t * join_arr_lastp = join_arr + MX_JOIN_ROWS - 1;
+static bool join_done = false;
+
+static struct type_desc_hdr_t type_desc_hdr_arr[MX_ELEM_HDR];
+static int type_desc_hdr_count = 0;
+static uint8_t * config_dp_resp = NULL;
+static uint8_t * free_config_dp_resp = NULL;
+static int config_dp_resp_len;
+
+static struct data_in_desc_t data_in_desc_arr[MX_DATA_IN_DESCS];
+
+/* Large buffers on heap, aligned to page size and zeroed */
+static uint8_t * enc_stat_rsp;
+static uint8_t * elem_desc_rsp;
+static uint8_t * add_elem_rsp;
+static uint8_t * threshold_rsp;
+
+static unsigned enc_stat_rsp_sz;
+static unsigned elem_desc_rsp_sz;
+static unsigned add_elem_rsp_sz;
+static unsigned threshold_rsp_sz;
+
+static int enc_stat_rsp_len;
+static int elem_desc_rsp_len;
+static int add_elem_rsp_len;
+static int threshold_rsp_len;
+
+
+/* Diagnostic page names, control and/or status (in and/or out) */
+static struct diag_page_code dpc_arr[] = {
+    {SUPPORTED_DPC, "Supported Diagnostic Pages"},  /* 0 */
+    {CONFIGURATION_DPC, "Configuration (SES)"},
+    {ENC_STATUS_DPC, "Enclosure Status/Control (SES)"},
+    {HELP_TEXT_DPC, "Help Text (SES)"},
+    {STRING_DPC, "String In/Out (SES)"},
+    {THRESHOLD_DPC, "Threshold In/Out (SES)"},
+    {ARRAY_STATUS_DPC, "Array Status/Control (SES, obsolete)"},
+    {ELEM_DESC_DPC, "Element Descriptor (SES)"},
+    {SHORT_ENC_STATUS_DPC, "Short Enclosure Status (SES)"},  /* 8 */
+    {ENC_BUSY_DPC, "Enclosure Busy (SES-2)"},
+    {ADD_ELEM_STATUS_DPC, "Additional Element Status (SES-2)"},
+    {SUBENC_HELP_TEXT_DPC, "Subenclosure Help Text (SES-2)"},
+    {SUBENC_STRING_DPC, "Subenclosure String In/Out (SES-2)"},
+    {SUPPORTED_SES_DPC, "Supported SES Diagnostic Pages (SES-2)"},
+    {DOWNLOAD_MICROCODE_DPC, "Download Microcode (SES-2)"},
+    {SUBENC_NICKNAME_DPC, "Subenclosure Nickname (SES-2)"},
+    {0x3f, "Protocol Specific (SAS transport)"},
+    {0x40, "Translate Address (SBC)"},
+    {0x41, "Device Status (SBC)"},
+    {0x42, "Rebuild Assist (SBC)"},     /* sbc3r31 */
+    {ALL_DPC, "All SES diagnostic pages output (sg_ses)"},
+    {-1, NULL},
+};
+
+/* Diagnostic page names, for status (or in) pages */
+static struct diag_page_code in_dpc_arr[] = {
+    {SUPPORTED_DPC, "Supported Diagnostic Pages"},  /* 0 */
+    {CONFIGURATION_DPC, "Configuration (SES)"},
+    {ENC_STATUS_DPC, "Enclosure Status (SES)"},
+    {HELP_TEXT_DPC, "Help Text (SES)"},
+    {STRING_DPC, "String In (SES)"},
+    {THRESHOLD_DPC, "Threshold In (SES)"},
+    {ARRAY_STATUS_DPC, "Array Status (SES, obsolete)"},
+    {ELEM_DESC_DPC, "Element Descriptor (SES)"},
+    {SHORT_ENC_STATUS_DPC, "Short Enclosure Status (SES)"},  /* 8 */
+    {ENC_BUSY_DPC, "Enclosure Busy (SES-2)"},
+    {ADD_ELEM_STATUS_DPC, "Additional Element Status (SES-2)"},
+    {SUBENC_HELP_TEXT_DPC, "Subenclosure Help Text (SES-2)"},
+    {SUBENC_STRING_DPC, "Subenclosure String In (SES-2)"},
+    {SUPPORTED_SES_DPC, "Supported SES Diagnostic Pages (SES-2)"},
+    {DOWNLOAD_MICROCODE_DPC, "Download Microcode (SES-2)"},
+    {SUBENC_NICKNAME_DPC, "Subenclosure Nickname (SES-2)"},
+    {0x3f, "Protocol Specific (SAS transport)"},
+    {0x40, "Translate Address (SBC)"},
+    {0x41, "Device Status (SBC)"},
+    {0x42, "Rebuild Assist Input (SBC)"},
+    {-1, NULL},
+};
+
+/* Diagnostic page names, for control (or out) pages */
+static struct diag_page_code out_dpc_arr[] = {
+    {SUPPORTED_DPC, "?? [Supported Diagnostic Pages]"},  /* 0 */
+    {CONFIGURATION_DPC, "?? [Configuration (SES)]"},
+    {ENC_CONTROL_DPC, "Enclosure Control (SES)"},
+    {HELP_TEXT_DPC, "Help Text (SES)"},
+    {STRING_DPC, "String Out (SES)"},
+    {THRESHOLD_DPC, "Threshold Out (SES)"},
+    {ARRAY_CONTROL_DPC, "Array Control (SES, obsolete)"},
+    {ELEM_DESC_DPC, "?? [Element Descriptor (SES)]"},
+    {SHORT_ENC_STATUS_DPC, "?? [Short Enclosure Status (SES)]"},  /* 8 */
+    {ENC_BUSY_DPC, "?? [Enclosure Busy (SES-2)]"},
+    {ADD_ELEM_STATUS_DPC, "?? [Additional Element Status (SES-2)]"},
+    {SUBENC_HELP_TEXT_DPC, "?? [Subenclosure Help Text (SES-2)]"},
+    {SUBENC_STRING_DPC, "Subenclosure String Out (SES-2)"},
+    {SUPPORTED_SES_DPC, "?? [Supported SES Diagnostic Pages (SES-2)]"},
+    {DOWNLOAD_MICROCODE_DPC, "Download Microcode (SES-2)"},
+    {SUBENC_NICKNAME_DPC, "Subenclosure Nickname (SES-2)"},
+    {0x3f, "Protocol Specific (SAS transport)"},
+    {0x40, "Translate Address (SBC)"},
+    {0x41, "Device Status (SBC)"},
+    {0x42, "Rebuild Assist Output (SBC)"},
+    {-1, NULL},
+};
+
+static struct diag_page_abbrev dp_abbrev[] = {
+    {"ac", ARRAY_CONTROL_DPC},
+    {"aes", ADD_ELEM_STATUS_DPC},
+    {"all", ALL_DPC},
+    {"as", ARRAY_STATUS_DPC},
+    {"cf", CONFIGURATION_DPC},
+    {"dm", DOWNLOAD_MICROCODE_DPC},
+    {"eb", ENC_BUSY_DPC},
+    {"ec", ENC_CONTROL_DPC},
+    {"ed", ELEM_DESC_DPC},
+    {"es", ENC_STATUS_DPC},
+    {"ht", HELP_TEXT_DPC},
+    {"sdp", SUPPORTED_DPC},
+    {"ses", SHORT_ENC_STATUS_DPC},
+    {"sht", SUBENC_HELP_TEXT_DPC},
+    {"snic", SUBENC_NICKNAME_DPC},
+    {"ssp", SUPPORTED_SES_DPC},
+    {"sstr", SUBENC_STRING_DPC},
+    {"str", STRING_DPC},
+    {"th", THRESHOLD_DPC},
+    {NULL, -999},
+};
+
+/* Names of element types used by the Enclosure Control/Status diagnostic
+ * page. */
+static struct element_type_t element_type_arr[] = {
+    {UNSPECIFIED_ETC, "un", "Unspecified"},
+    {DEVICE_ETC, "dev", "Device slot"},
+    {POWER_SUPPLY_ETC, "ps", "Power supply"},
+    {COOLING_ETC, "coo", "Cooling"},
+    {TEMPERATURE_ETC, "ts", "Temperature sensor"},
+    {DOOR_ETC, "do", "Door"},   /* prior to ses3r05 was 'dl' (for Door Lock)
+                                   but the "Lock" has been dropped */
+    {AUD_ALARM_ETC, "aa", "Audible alarm"},
+    {ENC_SCELECTR_ETC, "esc", "Enclosure services controller electronics"},
+    {SCC_CELECTR_ETC, "sce", "SCC controller electronics"},
+    {NV_CACHE_ETC, "nc", "Nonvolatile cache"},
+    {INV_OP_REASON_ETC, "ior", "Invalid operation reason"},
+    {UI_POWER_SUPPLY_ETC, "ups", "Uninterruptible power supply"},
+    {DISPLAY_ETC, "dis", "Display"},
+    {KEY_PAD_ETC, "kpe", "Key pad entry"},
+    {ENCLOSURE_ETC, "enc", "Enclosure"},
+    {SCSI_PORT_TRAN_ETC, "sp", "SCSI port/transceiver"},
+    {LANGUAGE_ETC, "lan", "Language"},
+    {COMM_PORT_ETC, "cp", "Communication port"},
+    {VOLT_SENSOR_ETC, "vs", "Voltage sensor"},
+    {CURR_SENSOR_ETC, "cs", "Current sensor"},
+    {SCSI_TPORT_ETC, "stp", "SCSI target port"},
+    {SCSI_IPORT_ETC, "sip", "SCSI initiator port"},
+    {SIMPLE_SUBENC_ETC, "ss", "Simple subenclosure"},
+    {ARRAY_DEV_ETC, "arr", "Array device slot"},
+    {SAS_EXPANDER_ETC, "sse", "SAS expander"},
+    {SAS_CONNECTOR_ETC, "ssc", "SAS connector"},
+    {-1, NULL, NULL},
+};
+
+static struct element_type_t element_type_by_code =
+    {0, NULL, "element type code form"};
+
+/* Many control element names below have "RQST" in front in drafts.
+   These are for the Enclosure Control/Status diagnostic page */
+static struct acronym2tuple ecs_a2t_arr[] = {
+    /* acron   element_type  start_byte  start_bit  num_bits */
+    {"ac_fail", UI_POWER_SUPPLY_ETC, 2, 4, 1, NULL},
+    {"ac_hi", UI_POWER_SUPPLY_ETC, 2, 6, 1, NULL},
+    {"ac_lo", UI_POWER_SUPPLY_ETC, 2, 7, 1, NULL},
+    {"ac_qual", UI_POWER_SUPPLY_ETC, 2, 5, 1, NULL},
+    {"active", DEVICE_ETC, 2, 7, 1, NULL},     /* for control only */
+    {"active", ARRAY_DEV_ETC, 2, 7, 1, NULL},  /* for control only */
+    {"batt_fail", UI_POWER_SUPPLY_ETC, 3, 1, 1, NULL},
+    {"bpf", UI_POWER_SUPPLY_ETC, 3, 0, 1, NULL},
+    {"bypa", DEVICE_ETC, 3, 3, 1, "bypass port A"},
+    {"bypa", ARRAY_DEV_ETC, 3, 3, 1, "bypass port A"},
+    {"bypb", DEVICE_ETC, 3, 2, 1, "bypass port B"},
+    {"bypb", ARRAY_DEV_ETC, 3, 2, 1, "bypass port B"},
+    {"conscheck", ARRAY_DEV_ETC, 1, 4, 1, "consistency check"},
+    {"ctr_link", SAS_CONNECTOR_ETC, 2, 7, 8, "connector physical link"},
+    {"ctr_type", SAS_CONNECTOR_ETC, 1, 6, 7, "connector type"},
+    {"current", CURR_SENSOR_ETC, 2, 7, 16, "current in centiamps"},
+    {"dc_fail", UI_POWER_SUPPLY_ETC, 2, 3, 1, NULL},
+    {"disable", -1, 0, 5, 1, NULL},        /* -1 is for all element types */
+    {"disable_elm", SCSI_PORT_TRAN_ETC, 3, 4, 1, "disable port/transceiver"},
+    {"disable_elm", COMM_PORT_ETC, 3, 0, 1, "disable communication port"},
+    {"devoff", DEVICE_ETC, 3, 4, 1, NULL},     /* device off */
+    {"devoff", ARRAY_DEV_ETC, 3, 4, 1, NULL},
+    {"disp_mode", DISPLAY_ETC, 1, 1, 2, NULL},
+    {"disp_char", DISPLAY_ETC, 2, 7, 16, NULL},
+    {"dnr", ARRAY_DEV_ETC, 2, 6, 1, "do not remove"},
+    {"dnr", COOLING_ETC, 1, 6, 1, "do not remove"},
+    {"dnr", DEVICE_ETC, 2, 6, 1, "do not remove"},
+    {"dnr", ENC_SCELECTR_ETC, 1, 5, 1, "do not remove"},
+    {"dnr", POWER_SUPPLY_ETC, 1, 6, 1, "do not remove"},
+    {"dnr", UI_POWER_SUPPLY_ETC, 3, 3, 1, "do not remove"},
+    {"enable", SCSI_IPORT_ETC, 3, 0, 1, NULL},
+    {"enable", SCSI_TPORT_ETC, 3, 0, 1, NULL},
+    {"fail", AUD_ALARM_ETC, 1, 6, 1, NULL},
+    {"fail", COMM_PORT_ETC, 1, 7, 1, NULL},
+    {"fail", COOLING_ETC, 3, 6, 1, NULL},
+    {"fail", CURR_SENSOR_ETC, 3, 6, 1, NULL},
+    {"fail", DISPLAY_ETC, 1, 6, 1, NULL},
+    {"fail", DOOR_ETC, 1, 6, 1, NULL},
+    {"fail", ENC_SCELECTR_ETC, 1, 6, 1, NULL},
+    {"fail", KEY_PAD_ETC, 1, 6, 1, NULL},
+    {"fail", NV_CACHE_ETC, 3, 6, 1, NULL},
+    {"fail", POWER_SUPPLY_ETC, 3, 6, 1, NULL},
+    {"fail", SAS_CONNECTOR_ETC, 3, 6, 1, NULL},
+    {"fail", SAS_EXPANDER_ETC, 1, 6, 1, NULL},
+    {"fail", SCC_CELECTR_ETC, 3, 6, 1, NULL},
+    {"fail", SCSI_IPORT_ETC, 1, 6, 1, NULL},
+    {"fail", SCSI_PORT_TRAN_ETC, 1, 6, 1, NULL},
+    {"fail", SCSI_TPORT_ETC, 1, 6, 1, NULL},
+    {"fail", SIMPLE_SUBENC_ETC, 1, 6, 1, NULL},
+    {"fail", TEMPERATURE_ETC, 3, 6, 1, NULL},
+    {"fail", UI_POWER_SUPPLY_ETC, 3, 6, 1, NULL},
+    {"fail", VOLT_SENSOR_ETC, 1, 6, 1, NULL},
+    {"failure_ind", ENCLOSURE_ETC, 2, 1, 1, NULL},
+    {"failure", ENCLOSURE_ETC, 3, 1, 1, NULL},
+    {"fault", DEVICE_ETC, 3, 5, 1, NULL},
+    {"fault", ARRAY_DEV_ETC, 3, 5, 1, NULL},
+    {"hotspare", ARRAY_DEV_ETC, 1, 5, 1, NULL},
+    {"hotswap", COOLING_ETC, 3, 7, 1, NULL},
+    {"hotswap", ENC_SCELECTR_ETC, 3, 7, 1, NULL},       /* status only */
+    {"hw_reset", ENC_SCELECTR_ETC, 1, 2, 1, "hardware reset"}, /* 18-047r1 */
+    {"ident", DEVICE_ETC, 2, 1, 1, "flash LED"},
+    {"ident", ARRAY_DEV_ETC, 2, 1, 1, "flash LED"},
+    {"ident", POWER_SUPPLY_ETC, 1, 7, 1, "flash LED"},
+    {"ident", COMM_PORT_ETC, 1, 7, 1, "flash LED"},
+    {"ident", COOLING_ETC, 1, 7, 1, "flash LED"},
+    {"ident", CURR_SENSOR_ETC, 1, 7, 1, "flash LED"},
+    {"ident", DISPLAY_ETC, 1, 7, 1, "flash LED"},
+    {"ident", DOOR_ETC, 1, 7, 1, "flash LED"},
+    {"ident", ENC_SCELECTR_ETC, 1, 7, 1, "flash LED"},
+    {"ident", ENCLOSURE_ETC, 1, 7, 1, "flash LED"},
+    {"ident", KEY_PAD_ETC, 1, 7, 1, "flash LED"},
+    {"ident", LANGUAGE_ETC, 1, 7, 1, "flash LED"},
+    {"ident", AUD_ALARM_ETC, 1, 7, 1, NULL},
+    {"ident", NV_CACHE_ETC, 1, 7, 1, "flash LED"},
+    {"ident", SAS_CONNECTOR_ETC, 1, 7, 1, "flash LED"},
+    {"ident", SAS_EXPANDER_ETC, 1, 7, 1, "flash LED"},
+    {"ident", SCC_CELECTR_ETC, 1, 7, 1, "flash LED"},
+    {"ident", SCSI_IPORT_ETC, 1, 7, 1, "flash LED"},
+    {"ident", SCSI_PORT_TRAN_ETC, 1, 7, 1, "flash LED"},
+    {"ident", SCSI_TPORT_ETC, 1, 7, 1, "flash LED"},
+    {"ident", SIMPLE_SUBENC_ETC, 1, 7, 1, "flash LED"},
+    {"ident", TEMPERATURE_ETC, 1, 7, 1, "flash LED"},
+    {"ident", UI_POWER_SUPPLY_ETC, 3, 7, 1, "flash LED"},
+    {"ident", VOLT_SENSOR_ETC, 1, 7, 1, "flash LED"},
+    {"incritarray", ARRAY_DEV_ETC, 1, 3, 1, NULL},
+    {"infailedarray", ARRAY_DEV_ETC, 1, 2, 1, NULL},
+    {"info", AUD_ALARM_ETC, 3, 3, 1, "emits warning tone when set"},
+    {"insert", DEVICE_ETC, 2, 3, 1, NULL},
+    {"insert", ARRAY_DEV_ETC, 2, 3, 1, NULL},
+    {"intf_fail", UI_POWER_SUPPLY_ETC, 2, 0, 1, NULL},
+    {"language", LANGUAGE_ETC, 2, 7, 16, "language code"},
+    {"locate", DEVICE_ETC, 2, 1, 1, "flash LED"},
+    {"locate", ARRAY_DEV_ETC, 2, 1, 1, "flash LED"},
+    {"locate", POWER_SUPPLY_ETC, 1, 7, 1, "flash LED"},
+    {"locate", COMM_PORT_ETC, 1, 7, 1, "flash LED"},
+    {"locate", COOLING_ETC, 1, 7, 1, "flash LED"},
+    {"locate", CURR_SENSOR_ETC, 1, 7, 1, "flash LED"},
+    {"locate", DISPLAY_ETC, 1, 7, 1, "flash LED"},
+    {"locate", DOOR_ETC, 1, 7, 1, "flash LED"},
+    {"locate", ENC_SCELECTR_ETC, 1, 7, 1, "flash LED"},
+    {"locate", ENCLOSURE_ETC, 1, 7, 1, "flash LED"},
+    {"locate", KEY_PAD_ETC, 1, 7, 1, "flash LED"},
+    {"locate", LANGUAGE_ETC, 1, 7, 1, "flash LED"},
+    {"locate", AUD_ALARM_ETC, 1, 7, 1, NULL},
+    {"locate", NV_CACHE_ETC, 1, 7, 1, "flash LED"},
+    {"locate", SAS_CONNECTOR_ETC, 1, 7, 1, "flash LED"},
+    {"locate", SAS_EXPANDER_ETC, 1, 7, 1, "flash LED"},
+    {"locate", SCC_CELECTR_ETC, 1, 7, 1, "flash LED"},
+    {"locate", SCSI_IPORT_ETC, 1, 7, 1, "flash LED"},
+    {"locate", SCSI_PORT_TRAN_ETC, 1, 7, 1, "flash LED"},
+    {"locate", SCSI_TPORT_ETC, 1, 7, 1, "flash LED"},
+    {"locate", SIMPLE_SUBENC_ETC, 1, 7, 1, "flash LED"},
+    {"locate", TEMPERATURE_ETC, 1, 7, 1, "flash LED"},
+    {"locate", UI_POWER_SUPPLY_ETC, 3, 7, 1, "flash LED"},
+    {"locate", VOLT_SENSOR_ETC, 1, 7, 1, "flash LED"},
+    {"lol", SCSI_PORT_TRAN_ETC, 3, 1, 1, "Loss of Link"},
+    {"mated", SAS_CONNECTOR_ETC, 3, 7, 1, NULL},
+    {"missing", DEVICE_ETC, 2, 4, 1, NULL},
+    {"missing", ARRAY_DEV_ETC, 2, 4, 1, NULL},
+    {"mute", AUD_ALARM_ETC, 3, 6, 1, "control only: mute the alarm"},
+    {"muted", AUD_ALARM_ETC, 3, 6, 1, "status only: alarm is muted"},
+    {"off", POWER_SUPPLY_ETC, 3, 4, 1, "Not providing power"},
+    {"off", COOLING_ETC, 3, 4, 1, "Not providing cooling"},
+    {"offset_temp", TEMPERATURE_ETC, 1, 5, 6, "Offset for reference "
+     "temperature"},
+    {"ok", ARRAY_DEV_ETC, 1, 7, 1, NULL},
+    {"on", COOLING_ETC, 3, 5, 1, NULL},
+    {"on", POWER_SUPPLY_ETC, 3, 5, 1, "0: turn (remain) off; 1: turn on"},
+    {"open", DOOR_ETC, 3, 1, 1, NULL},
+    {"overcurrent", CURR_SENSOR_ETC, 1, 1, 1, "overcurrent"},
+    {"overcurrent", POWER_SUPPLY_ETC, 2, 1, 1, "DC overcurrent"},
+    {"overcurrent", SAS_CONNECTOR_ETC, 3, 5, 1, NULL},  /* added ses3r07 */
+    {"overcurrent_warn", CURR_SENSOR_ETC, 1, 3, 1, "overcurrent warning"},
+    {"overtemp_fail", TEMPERATURE_ETC, 3, 3, 1, "Overtemperature failure"},
+    {"overtemp_warn", TEMPERATURE_ETC, 3, 2, 1, "Overtemperature warning"},
+    {"overvoltage", POWER_SUPPLY_ETC, 2, 3, 1, "DC overvoltage"},
+    {"overvoltage", VOLT_SENSOR_ETC, 1, 1, 1, "overvoltage"},
+    {"overvoltage_warn", POWER_SUPPLY_ETC, 1, 3, 1, "DC overvoltage warning"},
+    {"pow_cycle", ENCLOSURE_ETC, 2, 7, 2,
+     "0: no; 1: start in pow_c_delay minutes; 2: cancel"},
+    {"pow_c_delay", ENCLOSURE_ETC, 2, 5, 6,
+     "delay in minutes before starting power cycle (max: 60)"},
+    {"pow_c_duration", ENCLOSURE_ETC, 3, 7, 6,
+     "0: power off, restore within 1 minute; <=60: restore within that many "
+     "minutes; 63: power off, wait for manual power on"},
+     /* slightly different in Enclosure status element */
+    {"pow_c_time", ENCLOSURE_ETC, 2, 7, 6,
+     "time in minutes remaining until starting power cycle; 0: not "
+     "scheduled; <=60: scheduled in that many minutes; 63: in zero minutes"},
+    {"prdfail", -1, 0, 6, 1, "predict failure"},
+    {"rebuildremap", ARRAY_DEV_ETC, 1, 1, 1, NULL},
+    {"remove", DEVICE_ETC, 2, 2, 1, NULL},
+    {"remove", ARRAY_DEV_ETC, 2, 2, 1, NULL},
+    {"remind", AUD_ALARM_ETC, 3, 4, 1, NULL},
+    {"report", ENC_SCELECTR_ETC, 2, 0, 1, NULL},        /* status only */
+    {"report", SCC_CELECTR_ETC, 2, 0, 1, NULL},
+    {"report", SCSI_IPORT_ETC, 2, 0, 1, NULL},
+    {"report", SCSI_TPORT_ETC, 2, 0, 1, NULL},
+    {"rqst_mute", AUD_ALARM_ETC, 3, 7, 1,
+     "status only: alarm was manually muted"},
+    {"rqst_override", TEMPERATURE_ETC, 3, 7, 1, "Request(ed) override"},
+    {"rrabort", ARRAY_DEV_ETC, 1, 0, 1, "rebuild/remap abort"},
+    {"rsvddevice", ARRAY_DEV_ETC, 1, 6, 1, "reserved device"},
+    {"select_element", ENC_SCELECTR_ETC, 2, 0, 1, NULL},        /* control */
+    {"short_stat", SIMPLE_SUBENC_ETC, 3, 7, 8, "short enclosure status"},
+    {"size", NV_CACHE_ETC, 2, 7, 16, NULL},
+    {"speed_act", COOLING_ETC, 1, 2, 11, "actual speed (rpm / 10)"},
+    {"speed_code", COOLING_ETC, 3, 2, 3,
+     "0: leave; 1: lowest... 7: highest"},
+    {"size_mult", NV_CACHE_ETC, 1, 1, 2, NULL},
+    {"swap", -1, 0, 4, 1, NULL},               /* Reset swap */
+    {"sw_reset", ENC_SCELECTR_ETC, 1, 3, 1, "software reset"},/* 18-047r1 */
+    {"temp", TEMPERATURE_ETC, 2, 7, 8, "(Requested) temperature"},
+    {"unlock", DOOR_ETC, 3, 0, 1, NULL},
+    {"undertemp_fail", TEMPERATURE_ETC, 3, 1, 1, "Undertemperature failure"},
+    {"undertemp_warn", TEMPERATURE_ETC, 3, 0, 1, "Undertemperature warning"},
+    {"undervoltage", POWER_SUPPLY_ETC, 2, 2, 1, "DC undervoltage"},
+    {"undervoltage", VOLT_SENSOR_ETC, 1, 0, 1, "undervoltage"},
+    {"undervoltage_warn", POWER_SUPPLY_ETC, 1, 2, 1,
+     "DC undervoltage warning"},
+    {"ups_fail", UI_POWER_SUPPLY_ETC, 2, 2, 1, NULL},
+    {"urgency", AUD_ALARM_ETC, 3, 3, 4, NULL},  /* Tone urgency control bits */
+    {"voltage", VOLT_SENSOR_ETC, 2, 7, 16, "voltage in centivolts"},
+    {"warning", UI_POWER_SUPPLY_ETC, 2, 1, 1, NULL},
+    {"warning", ENCLOSURE_ETC, 3, 0, 1, NULL},
+    {"warning_ind", ENCLOSURE_ETC, 2, 0, 1, NULL},
+    {"xmit_fail", SCSI_PORT_TRAN_ETC, 3, 0, 1, "Transmitter failure"},
+    {NULL, 0, 0, 0, 0, NULL},
+};
+
+/* These are for the Threshold in/out diagnostic page */
+static struct acronym2tuple th_a2t_arr[] = {
+    {"high_crit", -1, 0, 7, 8, NULL},
+    {"high_warn", -1, 1, 7, 8, NULL},
+    {"low_crit", -1, 2, 7, 8, NULL},
+    {"low_warn", -1, 3, 7, 8, NULL},
+    {NULL, 0, 0, 0, 0, NULL},
+};
+
+/* These are for the Additional element status diagnostic page for SAS with
+ * the EIP bit set. First phy only. Index from start of AES descriptor */
+static struct acronym2tuple ae_sas_a2t_arr[] = {
+    {"at_sas_addr", -1, 12, 7, 64, NULL},  /* best viewed with --hex --get= */
+        /* typically this is the expander's SAS address */
+    {"dev_type", -1, 8, 6, 3, "1: SAS/SATA dev, 2: expander"},
+    {"dsn", -1, 7, 7, 8, "device slot number (255: none)"},
+    {"num_phys", -1, 4, 7, 8, "number of phys"},
+    {"phy_id", -1, 28, 7, 8, NULL},
+    {"sas_addr", -1, 20, 7, 64, NULL},  /* should be disk or tape ... */
+    {"exp_sas_addr", -1, 8, 7, 64, NULL},  /* expander address */
+    {"sata_dev", -1, 11, 0, 1, NULL},
+    {"sata_port_sel", -1, 11, 7, 1, NULL},
+    {"smp_init", -1, 10, 1, 1, NULL},
+    {"smp_targ", -1, 11, 1, 1, NULL},
+    {"ssp_init", -1, 10, 3, 1, NULL},
+    {"ssp_targ", -1, 11, 3, 1, NULL},
+    {"stp_init", -1, 10, 2, 1, NULL},
+    {"stp_targ", -1, 11, 2, 1, NULL},
+    {NULL, 0, 0, 0, 0, NULL},
+};
+
+/* Boolean array of element types of interest to the Additional Element
+ * Status page. Indexed by element type (0 <= et < 32). */
+static bool active_et_aesp_arr[NUM_ACTIVE_ET_AESP_ARR] = {
+    false, true /* dev */, false, false,
+    false, false, false, true /* esce */,
+    false, false, false, false,
+    false, false, false, false,
+    false, false, false, false,
+    true /* starg */, true /* sinit */, false, true /* arr */,
+    true /* sas exp */, false, false, false,
+    false, false, false, false,
+};
+
+/* Command line long option names with corresponding short letter. */
+static struct option long_options[] = {
+    {"all", no_argument, 0, 'a'},
+    {"ALL", no_argument, 0, 'z'},
+    {"byte1", required_argument, 0, 'b'},
+    {"clear", required_argument, 0, 'C'},
+    {"control", no_argument, 0, 'c'},
+    {"data", required_argument, 0, 'd'},
+    {"descriptor", required_argument, 0, 'D'},
+    {"dev-slot-num", required_argument, 0, 'x'},
+    {"dev_slot_num", required_argument, 0, 'x'},
+    {"dsn", required_argument, 0, 'x'},
+    {"eiioe", required_argument, 0, 'E'},
+    {"enumerate", no_argument, 0, 'e'},
+    {"filter", no_argument, 0, 'f'},
+    {"get", required_argument, 0, 'G'},
+    {"help", no_argument, 0, 'h'},
+    {"hex", no_argument, 0, 'H'},
+    {"index", required_argument, 0, 'I'},
+    {"inhex", required_argument, 0, 'X'},
+    {"inner-hex", no_argument, 0, 'i'},
+    {"inner_hex", no_argument, 0, 'i'},
+    {"join", no_argument, 0, 'j'},
+    {"list", no_argument, 0, 'l'},
+    {"nickid", required_argument, 0, 'N'},
+    {"nickname", required_argument, 0, 'n'},
+    {"mask", required_argument, 0, 'M'},
+    {"maxlen", required_argument, 0, 'm'},
+    {"page", required_argument, 0, 'p'},
+    {"quiet", no_argument, 0, 'q'},
+    {"raw", no_argument, 0, 'r'},
+    {"readonly", no_argument, 0, 'R'},
+    {"sas-addr", required_argument, 0, 'A'},
+    {"sas_addr", required_argument, 0, 'A'},
+    {"set", required_argument, 0, 'S'},
+    {"status", no_argument, 0, 's'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {"warn", no_argument, 0, 'w'},
+    {0, 0, 0, 0},
+};
+
+/* For overzealous SES device servers that don't like some status elements
+ * sent back as control elements. This table is as per ses3r06. */
+static uint8_t ses3_element_cmask_arr[NUM_ETC][4] = {
+                                /* Element type code (ETC) names; comment */
+    {0x40, 0xff, 0xff, 0xff},   /* [0] unspecified */
+    {0x40, 0, 0x4e, 0x3c},      /* DEVICE */
+    {0x40, 0x80, 0, 0x60},      /* POWER_SUPPLY */
+    {0x40, 0x80, 0, 0x60},      /* COOLING; requested speed as is unless */
+    {0x40, 0xc0, 0, 0},         /* TEMPERATURE */
+    {0x40, 0xc0, 0, 0x1},       /* DOOR */
+    {0x40, 0xc0, 0, 0x5f},      /* AUD_ALARM */
+    {0x40, 0xc0, 0x1, 0},       /* ENC_SCELECTR_ETC */
+    {0x40, 0xc0, 0, 0},         /* SCC_CELECTR */
+    {0x40, 0xc0, 0, 0},         /* NV_CACHE */
+    {0x40, 0, 0, 0},            /* [10] INV_OP_REASON */
+    {0x40, 0, 0, 0xc0},         /* UI_POWER_SUPPLY */
+    {0x40, 0xc0, 0xff, 0xff},   /* DISPLAY */
+    {0x40, 0xc3, 0, 0},         /* KEY_PAD */
+    {0x40, 0x80, 0, 0xff},      /* ENCLOSURE */
+    {0x40, 0xc0, 0, 0x10},      /* SCSI_PORT_TRAN */
+    {0x40, 0x80, 0xff, 0xff},   /* LANGUAGE */
+    {0x40, 0xc0, 0, 0x1},       /* COMM_PORT */
+    {0x40, 0xc0, 0, 0},         /* VOLT_SENSOR */
+    {0x40, 0xc0, 0, 0},         /* CURR_SENSOR */
+    {0x40, 0xc0, 0, 0x1},       /* [20] SCSI_TPORT */
+    {0x40, 0xc0, 0, 0x1},       /* SCSI_IPORT */
+    {0x40, 0xc0, 0, 0},         /* SIMPLE_SUBENC */
+    {0x40, 0xff, 0x4e, 0x3c},   /* ARRAY */
+    {0x40, 0xc0, 0, 0},         /* SAS_EXPANDER */
+    {0x40, 0x80, 0, 0x40},      /* SAS_CONNECTOR */
+};
+
+
+static int read_hex(const char * inp, uint8_t * arr, int mx_arr_len,
+                    int * arr_len, bool in_hex, bool may_gave_at, int verb);
+static int strcase_eq(const char * s1p, const char * s2p);
+static void enumerate_diag_pages(void);
+static bool saddr_non_zero(const uint8_t * bp);
+static const char * find_in_diag_page_desc(int page_num);
+
+
+static void
+usage(int help_num)
+{
+    if (2 != help_num) {
+        pr2serr(
+            "Usage: sg_ses [--all] [--ALL] [--descriptor=DES] "
+            "[--dev-slot-num=SN]\n"
+            "              [--eiioe=A_F] [--filter] [--get=STR] "
+            "[--hex]\n"
+            "              [--index=IIA | =TIA,II] [--inner-hex] [--join] "
+            "[--maxlen=LEN]\n"
+            "              [--page=PG] [--quiet] [--raw] [--readonly] "
+            "[--sas-addr=SA]\n"
+            "              [--status] [--verbose] [--warn] DEVICE\n\n"
+            "       sg_ses --control [--byte1=B1] [--clear=STR] "
+            "[--data=H,H...]\n"
+            "              [--descriptor=DES] [--dev-slot-num=SN] "
+            "[--index=IIA | =TIA,II]\n"
+            "              [--inhex=FN] [--mask] [--maxlen=LEN] "
+            "[--nickid=SEID]\n"
+            "              [--nickname=SEN] [--page=PG] [--sas-addr=SA] "
+            "[--set=STR]\n"
+            "              [--verbose] DEVICE\n\n"
+            "       sg_ses --data=@FN --status [-rr] [<most options from "
+            "first form>]\n"
+            "       sg_ses --inhex=FN --status [-rr] [<most options from "
+            "first form>]\n\n"
+            "       sg_ses [--enumerate] [--help] [--index=IIA] [--list] "
+            "[--version]\n\n"
+               );
+        if ((help_num < 1) || (help_num > 2)) {
+            pr2serr("Or the corresponding short option usage: \n"
+                    "  sg_ses [-a] [-D DES] [-x SN] [-E A_F] [-f] [-G STR] "
+                    "[-H] [-I IIA|TIA,II]\n"
+                    "         [-i] [-j] [-m LEN] [-p PG] [-q] [-r] [-R] "
+                    "[-A SA] [-s] [-v] [-w]\n"
+                    "         DEVICE\n\n"
+                    "  sg_ses [-b B1] [-C STR] [-c] [-d H,H...] [-D DES] "
+                    "[-x SN] [-I IIA|TIA,II]\n"
+                    "         [-M] [-m LEN] [-N SEID] [-n SEN] [-p PG] "
+                    "[-A SA] [-S STR]\n"
+                    "         [-v] DEVICE\n\n"
+                    "  sg_ses -d @FN -s [-rr] [<most options from first "
+                    "form>]\n"
+                    "  sg_ses -X FN -s [-rr] [<most options from first "
+                    "form>]\n\n"
+                    "  sg_ses [-e] [-h] [-I IIA] [-l] [-V]\n"
+                   );
+            pr2serr("\nFor help use '-h' one or more times.\n");
+            return;
+        }
+        pr2serr(
+            "  where the main options are:\n"
+            "    --all|-a            show (almost) all status pages (same "
+            "as --join)\n"
+            "    --clear=STR|-C STR    clear field by acronym or position\n"
+            "    --control|-c        send control information (def: fetch "
+            "status)\n"
+            "    --descriptor=DES|-D DES    descriptor name (for indexing)\n"
+            "    --dev-slot-num=SN|--dsn=SN|-x SN    device slot number "
+            "(for indexing)\n"
+            "    --filter|-f         filter out enclosure status flags that "
+            "are clear\n"
+            "                        use twice for status=okay entries "
+            "only\n"
+            "    --get=STR|-G STR    get value of field by acronym or "
+            "position\n"
+            "    --help|-h           print out usage message, use twice for "
+            "additional\n"
+            "    --index=IIA|-I IIA    individual index ('-1' for overall) "
+            "or element\n"
+            "                          type abbreviation (e.g. 'arr'). A "
+            "range may be\n"
+            "                          given for the individual index "
+            "(e.g. '2-5')\n"
+            "    --index=TIA,II|-I TIA,II    comma separated pair: TIA is "
+            "type header\n"
+            "                                index or element type "
+            "abbreviation;\n"
+            "                                II is individual index ('-1' "
+            "for overall)\n"
+            );
+        pr2serr(
+            "    --join|-j           group Enclosure Status, Element "
+            "Descriptor\n"
+            "                        and Additional Element Status pages. "
+            "Use twice\n"
+            "                        to add Threshold In page\n"
+            "    --page=PG|-p PG     diagnostic page code (abbreviation "
+            "or number)\n"
+            "                        (def: 'ssp' [0x0] (supported diagnostic "
+            "pages))\n"
+            "    --sas-addr=SA|-A SA    SAS address in hex (for indexing)\n"
+            "    --set=STR|-S STR    set value of field by acronym or "
+            "position\n"
+            "    --status|-s         fetch status information (default "
+            "action)\n\n"
+            "First usage above is for fetching pages or fields from a SCSI "
+            "enclosure.\nThe second usage is for changing a page or field in "
+            "an enclosure. The\n'--clear=', '--get=' and '--set=' options "
+            "can appear multiple times.\nUse '-hh' for more help, including "
+            "the options not explained above.\n");
+    } else {    /* for '-hh' or '--help --help' */
+        pr2serr(
+            "  where the remaining sg_ses options are:\n"
+            "    --ALL|-z            same as --all twice (adds thresholds)\n"
+            "    --byte1=B1|-b B1    byte 1 (2nd byte) of control page set "
+            "to B1\n"
+            "    --data=H,H...|-d H,H...    string of ASCII hex bytes to "
+            "send as a\n"
+            "                               control page or decode as a "
+            "status page\n"
+            "    --data=- | -d -     fetch string of ASCII hex bytes from "
+            "stdin\n"
+            "    --data=@FN | -d @FN    fetch string of ASCII hex bytes from "
+            "file: FN\n"
+            "    --eiioe=A_F|-E A_F    A_F is either 'auto' or 'force'. "
+            "'force' acts\n"
+            "                          as if EIIOE field is 1, 'auto' tries "
+            "to guess\n"
+            "    --enumerate|-e      enumerate page names + element types "
+            "(ignore\n"
+            "                        DEVICE). Use twice for clear,get,set "
+            "acronyms\n"
+            "    --hex|-H            print page response (or field) in hex\n"
+            "    --inhex=FN|-X FN    alternate form of --data=@FN\n"
+            "    --inner-hex|-i      print innermost level of a"
+            " status page in hex\n"
+            "    --list|-l           same as '--enumerate' option\n"
+            "    --mask|-M           ignore status element mask in modify "
+            "actions\n"
+            "                        (e.g.--set= and --clear=) (def: apply "
+            "mask)\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "    --nickid=SEID|-N SEID   SEID is subenclosure identifier "
+            "(def: 0)\n"
+            "                            used to specify which nickname to "
+            "change\n"
+            "    --nickname=SEN|-n SEN   SEN is new subenclosure nickname\n"
+            "    --quiet|-q          suppress some output messages\n"
+            "    --raw|-r            print status page in ASCII hex suitable "
+            "for '-d';\n"
+            "                        when used twice outputs page in binary "
+            "to stdout\n"
+            "    --readonly|-R       open DEVICE read-only (def: "
+            "read-write)\n"
+            "    --verbose|-v        increase verbosity\n"
+            "    --version|-V        print version string and exit\n"
+            "    --warn|-w           warn about join (and other) issues\n\n"
+            "If no options are given then DEVICE's supported diagnostic "
+            "pages are\nlisted. STR can be '<start_byte>:<start_bit>"
+            "[:<num_bits>][=<val>]'\nor '<acronym>[=val]'. Element type "
+            "abbreviations may be followed by a\nnumber (e.g. 'ps1' is "
+            "the second power supply element type). Use\n'sg_ses -e' and "
+            "'sg_ses -ee' for more information.\n\n"
+            );
+        pr2serr(
+            "Low level indexing can be done with one of the two '--index=' "
+            "options.\nAlternatively, medium level indexing can be done "
+            "with either the\n'--descriptor=', 'dev-slot-num=' or "
+            "'--sas-addr=' options. Support for\nthe medium level options "
+            "in the SES device is itself optional.\n"
+            );
+    }
+}
+
+/* Return 0 for okay, else an error */
+static int
+parse_index(struct opts_t *op)
+{
+    int n, n2;
+    const char * cp;
+    char * mallcp;
+    char * c2p;
+    const struct element_type_t * etp;
+    char b[64];
+    const int blen = sizeof(b);
+
+    op->ind_given = true;
+    n2 = 0;
+    if ((cp = strchr(op->index_str, ','))) {
+        /* decode number following comma */
+        if (0 == strcmp("-1", cp + 1))
+            n = -1;
+        else {
+            const char * cc3p;
+
+            n = sg_get_num_nomult(cp + 1);
+            if ((n < 0) || (n > 255)) {
+                pr2serr("bad argument to '--index=', after comma expect "
+                        "number from -1 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((cc3p = strchr(cp + 1, '-'))) {
+                n2 = sg_get_num_nomult(cc3p + 1);
+                if ((n2 < n) || (n2 > 255)) {
+                    pr2serr("bad argument to '--index', after '-' expect "
+                            "number from -%d to 255\n", n);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+        }
+        op->ind_indiv = n;
+        if (n2 > 0)
+            op->ind_indiv_last = n2;
+        n = cp - op->index_str;
+        if (n >= (blen - 1)) {
+            pr2serr("bad argument to '--index', string prior to comma too "
+                    "long\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    } else {    /* no comma found in index_str */
+        n = strlen(op->index_str);
+        if (n >= (blen - 1)) {
+            pr2serr("bad argument to '--index', string too long\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    snprintf(b, blen, "%.*s", n, op->index_str);
+    if (0 == strcmp("-1", b)) {
+        if (cp) {
+            pr2serr("bad argument to '--index', unexpected '-1' type header "
+                    "index\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        op->ind_th = 0;
+        op->ind_indiv = -1;
+    } else if (isdigit((uint8_t)b[0])) {
+        n = sg_get_num_nomult(b);
+        if ((n < 0) || (n > 255)) {
+            pr2serr("bad numeric argument to '--index', expect number from 0 "
+                    "to 255\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if (cp)         /* argument to left of comma */
+            op->ind_th = n;
+        else {          /* no comma found, so 'n' is ind_indiv */
+            op->ind_th = 0;
+            op->ind_indiv = n;
+            if ((c2p = strchr(b, '-'))) {
+                n2 = sg_get_num_nomult(c2p + 1);
+                if ((n2 < n) || (n2 > 255)) {
+                    pr2serr("bad argument to '--index', after '-' expect "
+                            "number from -%d to 255\n", n);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            op->ind_indiv_last = n2;
+        }
+    } else if ('_' == b[0]) {   /* leading "_" prefixes element type code */
+        if ((c2p = strchr(b + 1, '_')))
+            *c2p = '\0';        /* subsequent "_" prefixes e.t. index */
+        n = sg_get_num_nomult(b + 1);
+        if ((n < 0) || (n > 255)) {
+            pr2serr("bad element type code for '--index', expect value from "
+                    "0 to 255\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        element_type_by_code.elem_type_code = n;
+        mallcp = (char *)malloc(8);  /* willfully forget about freeing this */
+        if (NULL == mallcp)
+             return sg_convert_errno(ENOMEM);
+        mallcp[0] = '_';
+        snprintf(mallcp + 1, 6, "%d", n);
+        element_type_by_code.abbrev = mallcp;
+        if (c2p) {
+            n = sg_get_num_nomult(c2p + 1);
+            if ((n < 0) || (n > 255)) {
+                pr2serr("bad element type code <num> for '--index', expect "
+                        "<num> from 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->ind_et_inst = n;
+        }
+        op->ind_etp = &element_type_by_code;
+        if (NULL == cp)
+            op->ind_indiv = -1;
+    } else { /* element type abbreviation perhaps followed by <num> */
+        int b_len = strlen(b);
+
+        for (etp = element_type_arr; etp->desc; ++etp) {
+            n = strlen(etp->abbrev);
+            if ((n == b_len) && (0 == strncmp(b, etp->abbrev, n)))
+                break;
+        }
+        if (NULL == etp->desc) {
+            pr2serr("bad element type abbreviation [%s] for '--index'\n"
+                    "use '--enumerate' to see possibles\n", b);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if (b_len > n) {
+            n = sg_get_num_nomult(b + n);
+            if ((n < 0) || (n > 255)) {
+                pr2serr("bad element type abbreviation <num> for '--index', "
+                        "expect <num> from 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->ind_et_inst = n;
+        }
+        op->ind_etp = etp;
+        if (NULL == cp)
+            op->ind_indiv = -1;
+    }
+    if (op->verbose > 1) {
+        if (op->ind_etp)
+            pr2serr("   element type abbreviation: %s, etp_num=%d, "
+                    "individual index=%d\n", op->ind_etp->abbrev,
+                    op->ind_et_inst, op->ind_indiv);
+        else
+            pr2serr("   type header index=%d, individual index=%d\n",
+                    op->ind_th, op->ind_indiv);
+    }
+    return 0;
+}
+
+
+/* command line process, options and arguments. Returns 0 if ok. */
+static int
+parse_cmd_line(struct opts_t *op, int argc, char *argv[])
+{
+    int c, j, n, d_len, ret;
+    const char * data_arg = NULL;
+    const char * inhex_arg = NULL;
+    uint64_t saddr;
+    const char * cp;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "aA:b:cC:d:D:eE:fG:hHiI:jln:N:m:Mp:qrRs"
+                        "S:vVwx:z", long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':       /* --all is synonym for --join */
+            ++op->do_join;
+            break;
+        case 'A':       /* SAS address, assumed to be hex */
+            cp = optarg;
+            if ((strlen(optarg) > 2) && ('X' == toupper((uint8_t)optarg[1])))
+                cp = optarg + 2;
+            if (1 != sscanf(cp, "%" SCNx64 "", &saddr)) {
+                pr2serr("bad argument to '--sas-addr=SA'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            sg_put_unaligned_be64(saddr, op->sas_addr + 0);
+            if (sg_all_ffs(op->sas_addr, 8)) {
+                pr2serr("error decoding '--sas-addr=SA' argument\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'b':
+            op->byte1 = sg_get_num_nomult(optarg);
+            if ((op->byte1 < 0) || (op->byte1 > 255)) {
+                pr2serr("bad argument to '--byte1=B1' (0 to 255 "
+                        "inclusive)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->byte1_given = true;
+            break;
+        case 'c':
+            op->do_control = true;
+            break;
+        case 'C':
+            if (strlen(optarg) >= CGS_STR_MAX_SZ) {
+                pr2serr("--clear= option too long (max %d characters)\n",
+                        CGS_STR_MAX_SZ);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (op->num_cgs < CGS_CL_ARR_MAX_SZ) {
+                op->cgs_cl_arr[op->num_cgs].cgs_sel = CLEAR_OPT;
+                strcpy(op->cgs_cl_arr[op->num_cgs].cgs_str, optarg);
+                ++op->num_cgs;
+            } else {
+                pr2serr("Too many --clear=, --get= and --set= options "
+                        "(max: %d)\n", CGS_CL_ARR_MAX_SZ);
+                return SG_LIB_CONTRADICT;
+            }
+            break;
+        case 'd':
+            data_arg = optarg;
+            op->do_data = true;
+            break;
+        case 'D':
+            op->desc_name = optarg;
+            break;
+        case 'e':
+            ++op->enumerate;
+            break;
+        case 'E':
+            if (0 == strcmp("auto", optarg))
+                op->eiioe_auto = true;
+            else if (0 == strcmp("force", optarg))
+                op->eiioe_force = true;
+            else {
+                pr2serr("--eiioe option expects 'auto' or 'force' as an "
+                        "argument\n");
+                return SG_LIB_CONTRADICT;
+            }
+            break;
+        case 'f':
+            ++op->do_filter;
+            break;
+        case 'G':
+            if (strlen(optarg) >= CGS_STR_MAX_SZ) {
+                pr2serr("--get= option too long (max %d characters)\n",
+                        CGS_STR_MAX_SZ);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (op->num_cgs < CGS_CL_ARR_MAX_SZ) {
+                op->cgs_cl_arr[op->num_cgs].cgs_sel = GET_OPT;
+                strcpy(op->cgs_cl_arr[op->num_cgs].cgs_str, optarg);
+                ++op->num_cgs;
+            } else {
+                pr2serr("Too many --clear=, --get= and --set= options "
+                        "(max: %d)\n", CGS_CL_ARR_MAX_SZ);
+                return SG_LIB_CONTRADICT;
+            }
+            break;
+        case 'h':
+            ++op->do_help;
+            break;
+        case '?':
+            pr2serr("\n");
+            usage(0);
+            return SG_LIB_SYNTAX_ERROR;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'i':
+            op->inner_hex = true;
+            break;
+        case 'I':
+            op->index_str = optarg;
+            break;
+        case 'j':
+            ++op->do_join;
+            break;
+        case 'l':
+            op->do_list = true;
+            break;
+        case 'n':
+            op->nickname_str = optarg;
+            break;
+        case 'N':
+            op->seid = sg_get_num_nomult(optarg);
+            if ((op->seid < 0) || (op->seid > 255)) {
+                pr2serr("bad argument to '--nickid=SEID' (0 to 255 "
+                        "inclusive)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->seid_given = true;
+            break;
+        case 'm':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 65535)) {
+                pr2serr("bad argument to '--maxlen=LEN' (0 to 65535 "
+                        "inclusive expected)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (0 == n)
+                op->maxlen = MX_ALLOC_LEN;
+            else if (n < MIN_MAXLEN) {
+                pr2serr("Warning: --maxlen=LEN less than %d ignored\n",
+                        MIN_MAXLEN);
+                op->maxlen = MX_ALLOC_LEN;
+            } else
+                op->maxlen = n;
+            break;
+        case 'M':
+            op->mask_ign = true;
+            break;
+        case 'p':
+            if (isdigit((uint8_t)optarg[0])) {
+                op->page_code = sg_get_num_nomult(optarg);
+                if ((op->page_code < 0) || (op->page_code > 255)) {
+                    pr2serr("bad argument to '--page=PG' (0 to 255 "
+                            "inclusive)\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else {
+                const struct diag_page_abbrev * ap;
+
+                for (ap = dp_abbrev; ap->abbrev; ++ap) {
+                    if (strcase_eq(ap->abbrev, optarg)) {
+                        op->page_code = ap->page_code;
+                        break;
+                    }
+                }
+                if (NULL == ap->abbrev) {
+                    pr2serr("'--page=PG' argument abbreviation \"%s\" not "
+                            "found\nHere are the choices:\n", optarg);
+                    enumerate_diag_pages();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            op->page_code_given = true;
+            break;
+        case 'q':
+            op->quiet = true;
+            break;
+        case 'r':
+            ++op->do_raw;
+            break;
+        case 'R':
+            op->o_readonly = true;
+            break;
+        case 's':
+            op->do_status = true;
+            break;
+        case 'S':
+            if (strlen(optarg) >= CGS_STR_MAX_SZ) {
+                pr2serr("--set= option too long (max %d characters)\n",
+                        CGS_STR_MAX_SZ);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (op->num_cgs < CGS_CL_ARR_MAX_SZ) {
+                op->cgs_cl_arr[op->num_cgs].cgs_sel = SET_OPT;
+                strcpy(op->cgs_cl_arr[op->num_cgs].cgs_str, optarg);
+                ++op->num_cgs;
+            } else {
+                pr2serr("Too many --clear=, --get= and --set= options "
+                        "(max: %d)\n", CGS_CL_ARR_MAX_SZ);
+                return SG_LIB_CONTRADICT;
+            }
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            return 0;
+        case 'w':
+            op->warn = true;
+            break;
+        case 'x':
+            op->dev_slot_num = sg_get_num_nomult(optarg);
+            if ((op->dev_slot_num < 0) || (op->dev_slot_num > 255)) {
+                pr2serr("bad argument to '--dev-slot-num' (0 to 255 "
+                        "inclusive)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'X':       /* --inhex=FN for compatibility with other utils */
+            inhex_arg = optarg;
+            op->do_data = true;
+            break;
+        case 'z':       /* --ALL and -z are synonyms for '--join --join' */
+            /* -A already used for --sas-addr=SA shortened form */
+            op->do_join += 2;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            goto err_help;
+        }
+    }
+    if (op->do_help)
+        return 0;
+    if (optind < argc) {
+        if (NULL == op->dev_name) {
+            op->dev_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            goto err_help;
+        }
+    }
+    op->mx_arr_len = (op->maxlen > MIN_DATA_IN_SZ) ? op->maxlen :
+                                                     MIN_DATA_IN_SZ;
+    op->data_arr = sg_memalign(op->mx_arr_len, 0 /* page aligned */,
+                               &op->free_data_arr, false);
+    if (NULL == op->data_arr) {
+        pr2serr("unable to allocate %u bytes on heap\n", op->mx_arr_len);
+        return sg_convert_errno(ENOMEM);
+    }
+    if (data_arg || inhex_arg) {
+        if (inhex_arg) {
+            data_arg = inhex_arg;
+            if (read_hex(data_arg, op->data_arr + DATA_IN_OFF,
+                         op->mx_arr_len - DATA_IN_OFF, &op->arr_len,
+                         (op->do_raw < 2), false, op->verbose)) {
+                pr2serr("bad argument, expect '--inhex=FN' or '--inhex=-'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else {
+            if (read_hex(data_arg, op->data_arr + DATA_IN_OFF,
+                         op->mx_arr_len - DATA_IN_OFF, &op->arr_len,
+                         (op->do_raw < 2), true, op->verbose)) {
+                pr2serr("bad argument, expect '--data=H,H...', '--data=-' or "
+                        "'--data=@FN'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+        op->do_raw = 0;
+        /* struct data_in_desc_t stuff does not apply when --control */
+        if (op->do_status && (op->arr_len > 3)) {
+            int off;
+            int pc = 0;
+            const uint8_t * bp = op->data_arr + DATA_IN_OFF;
+            struct data_in_desc_t * didp = data_in_desc_arr;
+
+            d_len = sg_get_unaligned_be16(bp + 2) + 4;
+            for (n = 0, off = 0; n < MX_DATA_IN_DESCS; ++n, ++didp) {
+                didp->in_use = true;
+                pc = bp[0];
+                didp->page_code = pc;
+                didp->offset = off;
+                didp->dp_len = d_len;
+                off += d_len;
+                if ((off + 3) < op->arr_len) {
+                    bp += d_len;
+                    d_len = sg_get_unaligned_be16(bp + 2) + 4;
+                } else {
+                    ++n;
+                    break;
+                }
+            }
+            if (1 == n) {
+                op->page_code_given = true;
+                op->page_code = pc;
+            } else      /* n must be > 1 */
+                op->many_dpages = true;
+
+            if (op->verbose > 3) {
+                int k;
+                char b[128];
+
+                for (didp = data_in_desc_arr, k = 0; k < n; ++k, ++didp) {
+                    if ((cp = find_in_diag_page_desc(didp->page_code)))
+                        snprintf(b, sizeof(b), "%s dpage", cp);
+                    else
+                        snprintf(b, sizeof(b), "dpage 0x%x", didp->page_code);
+                    pr2serr("%s found, offset %d, dp_len=%d\n", b,
+                            didp->offset, didp->dp_len);
+                }
+            }
+        }
+    }
+    if (op->do_join && op->do_control) {
+        pr2serr("cannot have '--join' and '--control'\n");
+        goto err_help;
+    }
+    if (op->index_str) {
+        ret = parse_index(op);
+        if (ret) {
+            pr2serr("  For more information use '--help'\n");
+            return ret;
+        }
+    }
+    if (op->desc_name || (op->dev_slot_num >= 0) ||
+        saddr_non_zero(op->sas_addr)) {
+        if (op->ind_given) {
+            pr2serr("cannot have --index with either --descriptor, "
+                    "--dev-slot-num or --sas-addr\n");
+            goto err_help;
+        }
+        if (((!! op->desc_name) + (op->dev_slot_num >= 0) +
+             saddr_non_zero(op->sas_addr)) > 1) {
+            pr2serr("can only have one of --descriptor, "
+                    "--dev-slot-num and --sas-addr\n");
+            goto err_help;
+        }
+        if ((0 == op->do_join) && (! op->do_control) &&
+            (0 == op->num_cgs) && (! op->page_code_given)) {
+            ++op->do_join;      /* implicit --join */
+            if (op->verbose)
+                pr2serr("process as if --join option is set\n");
+        }
+    }
+    if (op->ind_given) {
+        if ((0 == op->do_join) && (! op->do_control) &&
+            (0 == op->num_cgs) && (! op->page_code_given)) {
+            op->page_code_given = true;
+            op->page_code = ENC_STATUS_DPC;  /* implicit status page */
+            if (op->verbose)
+                pr2serr("assume --page=2 (es) option is set\n");
+        }
+    }
+    if (op->do_list || op->enumerate)
+        return 0;
+
+    if (op->do_control && op->do_status) {
+        pr2serr("cannot have both '--control' and '--status'\n");
+        goto err_help;
+    } else if (op->do_control) {
+        if (op->nickname_str || op->seid_given)
+            ;
+        else if (! op->do_data) {
+            pr2serr("need to give '--data' in control mode\n");
+            goto err_help;
+        }
+    } else if (! op->do_status) {
+        if (op->do_data) {
+            pr2serr("when user data given, require '--control' or "
+                    "'--status' option\n");
+            goto err_help;
+        }
+        op->do_status = true;  /* default to receiving status pages */
+    } else if (op->do_status && op->do_data && op->dev_name) {
+        pr2serr(">>> Warning: device name (%s) will be ignored\n",
+                op->dev_name);
+        op->dev_name = NULL;    /* quash device name */
+    }
+
+    if (op->nickname_str) {
+        if (! op->do_control) {
+            pr2serr("since '--nickname=' implies control mode, require "
+                    "'--control' as well\n");
+            goto err_help;
+        }
+        if (op->page_code_given) {
+            if (SUBENC_NICKNAME_DPC != op->page_code) {
+                pr2serr("since '--nickname=' assume or expect "
+                        "'--page=snic'\n");
+                goto err_help;
+            }
+        } else
+            op->page_code = SUBENC_NICKNAME_DPC;
+    } else if (op->seid_given) {
+        pr2serr("'--nickid=' must be used together with '--nickname='\n");
+        goto err_help;
+
+    }
+    if ((op->verbose > 4) && saddr_non_zero(op->sas_addr)) {
+        pr2serr("    SAS address (in hex): ");
+        for (j = 0; j < 8; ++j)
+            pr2serr("%02x", op->sas_addr[j]);
+        pr2serr("\n");
+    }
+
+    if ((! (op->do_data && op->do_status)) && (NULL == op->dev_name)) {
+        pr2serr("missing DEVICE name!\n\n");
+        goto err_help;
+    }
+    return 0;
+
+err_help:
+    if (op->verbose) {
+        pr2serr("\n");
+        usage(0);
+    }
+    return SG_LIB_SYNTAX_ERROR;
+}
+
+/* Parse clear/get/set string, writes output to '*tavp'. Uses 'buff' for
+ * scratch area. Returns 0 on success, else -1. */
+static int
+parse_cgs_str(char * buff, struct tuple_acronym_val * tavp)
+{
+    char * esp;
+    char * colp;
+    unsigned int ui;
+
+    tavp->acron = NULL;
+    tavp->val_str = NULL;
+    tavp->start_byte = -1;
+    tavp->num_bits = 1;
+    if ((esp = strchr(buff, '='))) {
+        tavp->val_str = esp + 1;
+        *esp = '\0';
+        if (0 == strcmp("-1", esp + 1))
+            tavp->val = -1;
+        else {
+            tavp->val = sg_get_llnum_nomult(esp + 1);
+            if (-1 == tavp->val) {
+                pr2serr("unable to decode: %s value\n", esp + 1);
+                pr2serr("    expected: <acronym>[=<val>]\n");
+                return -1;
+            }
+        }
+    }
+    if (isalpha((uint8_t)buff[0]))
+        tavp->acron = buff;
+    else {
+        char * cp;
+
+        colp = strchr(buff, ':');
+        if ((NULL == colp) || (buff == colp))
+            return -1;
+        *colp = '\0';
+        if (('0' == buff[0]) && ('X' == toupper((uint8_t)buff[1]))) {
+            if (1 != sscanf(buff + 2, "%x", &ui))
+                return -1;
+            tavp->start_byte = ui;
+        } else if ('H' == toupper((uint8_t)*(colp - 1))) {
+            if (1 != sscanf(buff, "%x", &ui))
+                return -1;
+            tavp->start_byte = ui;
+        } else {
+            if (1 != sscanf(buff, "%d", &tavp->start_byte))
+                return -1;
+        }
+        if ((tavp->start_byte < 0) || (tavp->start_byte > 127)) {
+            pr2serr("<start_byte> needs to be between 0 and 127\n");
+            return -1;
+        }
+        cp = colp + 1;
+        colp = strchr(cp, ':');
+        if (cp == colp)
+            return -1;
+        if (colp)
+            *colp = '\0';
+        if (1 != sscanf(cp, "%d", &tavp->start_bit))
+            return -1;
+        if ((tavp->start_bit < 0) || (tavp->start_bit > 7)) {
+            pr2serr("<start_bit> needs to be between 0 and 7\n");
+            return -1;
+        }
+        if (colp) {
+            if (1 != sscanf(colp + 1, "%d", &tavp->num_bits))
+                return -1;
+        }
+        if ((tavp->num_bits < 1) || (tavp->num_bits > 64)) {
+            pr2serr("<num_bits> needs to be between 1 and 64\n");
+            return -1;
+        }
+    }
+    return 0;
+}
+
+/* Fetch diagnostic page name (control or out). Returns NULL if not found. */
+static const char *
+find_out_diag_page_desc(int page_num)
+{
+    const struct diag_page_code * pcdp;
+
+    for (pcdp = out_dpc_arr; pcdp->desc; ++pcdp) {
+        if (page_num == pcdp->page_code)
+            return pcdp->desc;
+        else if (page_num < pcdp->page_code)
+            return NULL;
+    }
+    return NULL;
+}
+
+static bool
+match_ind_indiv(int index, const struct opts_t * op)
+{
+    if (index == op->ind_indiv)
+        return true;
+    if (op->ind_indiv_last > op->ind_indiv) {
+        if ((index > op->ind_indiv) && (index <= op->ind_indiv_last))
+            return true;
+    }
+    return false;
+}
+
+#if 0
+static bool
+match_last_ind_indiv(int index, const struct opts_t * op)
+{
+    if (op->ind_indiv_last >= op->ind_indiv)
+        return (index == op->ind_indiv_last);
+    return (index == op->ind_indiv);
+}
+#endif
+
+/* Return of 0 -> success, SG_LIB_CAT_* positive values or -1 -> other
+ * failures */
+static int
+do_senddiag(struct sg_pt_base * ptvp, void * outgoing_pg, int outgoing_len,
+            bool noisy, int verbose)
+{
+    int ret;
+
+    if (outgoing_pg && (verbose > 2)) {
+        int page_num = ((const char *)outgoing_pg)[0];
+        const char * cp = find_out_diag_page_desc(page_num);
+
+        if (cp)
+            pr2serr("    Send diagnostic command page name: %s\n", cp);
+        else
+            pr2serr("    Send diagnostic command page number: 0x%x\n",
+                    page_num);
+    }
+    ret = sg_ll_send_diag_pt(ptvp, 0 /* sf_code */, true /* pf_bit */,
+                             false /* sf_bit */, false /* devofl_bit */,
+                             false /* unitofl_bit */, 0 /* long_duration */,
+                             outgoing_pg, outgoing_len, noisy, verbose);
+    clear_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Fetch diagnostic page name (status and/or control). Returns NULL if not
+ * found. */
+static const char *
+find_diag_page_desc(int page_num)
+{
+    const struct diag_page_code * pcdp;
+
+    for (pcdp = dpc_arr; pcdp->desc; ++pcdp) {
+        if (page_num == pcdp->page_code)
+            return pcdp->desc;
+        else if (page_num < pcdp->page_code)
+            return NULL;
+    }
+    return NULL;
+}
+
+/* Fetch diagnostic page name (status or in). Returns NULL if not found. */
+static const char *
+find_in_diag_page_desc(int page_num)
+{
+    const struct diag_page_code * pcdp;
+
+    for (pcdp = in_dpc_arr; pcdp->desc; ++pcdp) {
+        if (page_num == pcdp->page_code)
+            return pcdp->desc;
+        else if (page_num < pcdp->page_code)
+            return NULL;
+    }
+    return NULL;
+}
+
+/* Fetch element type name. Returns NULL if not found. */
+static char *
+etype_str(int elem_type_code, char * b, int mlen_b)
+{
+    const struct element_type_t * etp;
+    int len;
+
+    if ((NULL == b) || (mlen_b < 1))
+        return b;
+    for (etp = element_type_arr; etp->desc; ++etp) {
+        if (elem_type_code == etp->elem_type_code) {
+            len = strlen(etp->desc);
+            if (len < mlen_b)
+                strcpy(b, etp->desc);
+            else {
+                strncpy(b, etp->desc, mlen_b - 1);
+                b[mlen_b - 1] = '\0';
+            }
+            return b;
+        } else if (elem_type_code < etp->elem_type_code)
+            break;
+    }
+    if (elem_type_code < 0x80)
+        snprintf(b, mlen_b - 1, "[0x%x]", elem_type_code);
+    else
+        snprintf(b, mlen_b - 1, "vendor specific [0x%x]", elem_type_code);
+    b[mlen_b - 1] = '\0';
+    return b;
+}
+
+/* Returns true if el_type (element type) is of interest to the Additional
+ * Element Status page. Otherwise return false. */
+static bool
+is_et_used_by_aes(int el_type)
+{
+    if ((el_type >= 0) && (el_type < NUM_ACTIVE_ET_AESP_ARR))
+        return active_et_aesp_arr[el_type];
+    else
+        return false;
+}
+
+#if 0
+static struct join_row_t *
+find_join_row(struct th_es_t * tesp, int index, enum fj_select_t sel)
+{
+    int k;
+    struct join_row_t * jrp = tesp->j_base;
+
+    if (index < 0)
+        return NULL;
+    switch (sel) {
+    case FJ_IOE:     /* index includes overall element */
+        if (index >= tesp->num_j_rows)
+            return NULL;
+        return jrp + index;
+    case FJ_EOE:     /* index excludes overall element */
+        if (index >= tesp->num_j_eoe)
+            return NULL;
+        for (k = 0; k < tesp->num_j_rows; ++k, ++jrp) {
+            if (index == jrp->ei_eoe)
+                return jrp;
+        }
+        return NULL;
+    case FJ_AESS:    /* index includes only AES listed element types */
+        if (index >= tesp->num_j_eoe)
+            return NULL;
+        for (k = 0; k < tesp->num_j_rows; ++k, ++jrp) {
+            if (index == jrp->ei_aess)
+                return jrp;
+        }
+        return NULL;
+    case FJ_SAS_CON: /* index on non-overall SAS connector etype */
+        if (index >= tesp->num_j_rows)
+            return NULL;
+        for (k = 0; k < tesp->num_j_rows; ++k, ++jrp) {
+            if (SAS_CONNECTOR_ETC == jrp->etype) {
+                if (index == jrp->indiv_i)
+                    return jrp;
+            }
+        }
+        return NULL;
+    default:
+        pr2serr("%s: bad selector: %d\n", __func__, (int)sel);
+        return NULL;
+    }
+}
+#endif
+
+static const struct join_row_t *
+find_join_row_cnst(const struct th_es_t * tesp, int index,
+                   enum fj_select_t sel)
+{
+    int k;
+    const struct join_row_t * jrp = tesp->j_base;
+
+    if (index < 0)
+        return NULL;
+    switch (sel) {
+    case FJ_IOE:     /* index includes overall element */
+        if (index >= tesp->num_j_rows)
+            return NULL;
+        return jrp + index;
+    case FJ_EOE:     /* index excludes overall element */
+        if (index >= tesp->num_j_eoe)
+            return NULL;
+        for (k = 0; k < tesp->num_j_rows; ++k, ++jrp) {
+            if (index == jrp->ei_eoe)
+                return jrp;
+        }
+        return NULL;
+    case FJ_AESS:   /* index includes only AES listed element types */
+        if (index >= tesp->num_j_eoe)
+            return NULL;
+        for (k = 0; k < tesp->num_j_rows; ++k, ++jrp) {
+            if (index == jrp->ei_aess)
+                return jrp;
+        }
+        return NULL;
+    case FJ_SAS_CON: /* index on non-overall SAS connector etype */
+        if (index >= tesp->num_j_rows)
+            return NULL;
+        for (k = 0; k < tesp->num_j_rows; ++k, ++jrp) {
+            if (SAS_CONNECTOR_ETC == jrp->etype) {
+                if (index == jrp->indiv_i)
+                    return jrp;
+            }
+        }
+        return NULL;
+    default:
+        pr2serr("%s: bad selector: %d\n", __func__, (int)sel);
+        return NULL;
+    }
+}
+
+/* Return of 0 -> success, SG_LIB_CAT_* positive values or -2 if response
+ * had bad format, -1 -> other failures */
+static int
+do_rec_diag(struct sg_pt_base * ptvp, int page_code, uint8_t * rsp_buff,
+            int rsp_buff_size, struct opts_t * op, int * rsp_lenp)
+{
+    int k, d_len, rsp_len, res;
+    int resid = 0;
+    int vb = op->verbose;
+    const char * cp;
+    char b[80];
+    char bb[120];
+    static const char * rdr = "Receive diagnostic results";
+
+    memset(rsp_buff, 0, rsp_buff_size);
+    if (rsp_lenp)
+        *rsp_lenp = 0;
+    if ((cp = find_in_diag_page_desc(page_code)))
+        snprintf(bb, sizeof(bb), "%s dpage", cp);
+    else
+        snprintf(bb, sizeof(bb), "dpage 0x%x", page_code);
+    cp = bb;
+
+    if (op->data_arr && op->do_data) {  /* user provided data */
+        /* N.B. First 4 bytes in data_arr are not used, user data was read in
+         * starting at byte offset 4 */
+        bool found = false;
+        int off = 0;
+        const uint8_t * bp = op->data_arr + DATA_IN_OFF;
+        const struct data_in_desc_t * didp = data_in_desc_arr;
+
+        for (k = 0, d_len = 0; k < MX_DATA_IN_DESCS; ++k, ++didp) {
+            if (! didp->in_use)
+                break;
+            if (page_code == didp->page_code) {
+                off = didp->offset;
+                d_len = didp->dp_len;
+                found = true;
+                break;
+            }
+        }
+        if (found)
+            memcpy(rsp_buff, bp + off, d_len);
+        else {
+            if (vb)
+                pr2serr("%s: %s not found in user data\n", __func__, cp);
+            return SG_LIB_CAT_OTHER;
+        }
+
+        cp = find_in_diag_page_desc(page_code);
+        if (vb > 2) {
+            pr2serr("    %s: response data from user", rdr);
+            if (3 == vb) {
+                pr2serr("%s:\n", (d_len > 256 ? ", first 256 bytes" : ""));
+                hex2stderr(rsp_buff, (d_len > 256 ? 256 : d_len), -1);
+            } else {
+                pr2serr(":\n");
+                hex2stderr(rsp_buff, d_len, 0);
+            }
+        }
+        res = 0;
+        resid = rsp_buff_size - d_len;
+        goto decode;    /* step over the device access */
+    }
+    if (vb > 1)
+        pr2serr("    %s command for %s\n", rdr, cp);
+    res = sg_ll_receive_diag_pt(ptvp, true /* pcv */, page_code, rsp_buff,
+                                rsp_buff_size, 0 /* default timeout */,
+                                &resid, ! op->quiet, vb);
+    clear_scsi_pt_obj(ptvp);
+decode:
+    if (0 == res) {
+        rsp_len = sg_get_unaligned_be16(rsp_buff + 2) + 4;
+        if (rsp_len > rsp_buff_size) {
+            if (rsp_buff_size > 8) /* tried to get more than header */
+                pr2serr("<<< warning response buffer too small [was %d but "
+                        "need %d]>>>\n", rsp_buff_size, rsp_len);
+            if (resid > 0)
+                rsp_buff_size -= resid;
+        } else if (resid > 0)
+            rsp_buff_size -= resid;
+        rsp_len = (rsp_len < rsp_buff_size) ? rsp_len : rsp_buff_size;
+        if (rsp_len < 0) {
+            pr2serr("<<< warning: resid=%d too large, implies negative "
+                    "reply length: %d\n", resid, rsp_len);
+            rsp_len = 0;
+        }
+        if (rsp_lenp)
+            *rsp_lenp = rsp_len;
+        if ((rsp_len > 1) && (page_code != rsp_buff[0])) {
+            if ((0x9 == rsp_buff[0]) && (1 & rsp_buff[1])) {
+                pr2serr("Enclosure busy, try again later\n");
+                if (op->do_hex)
+                    hex2stderr(rsp_buff, rsp_len, 0);
+            } else if (0x8 == rsp_buff[0]) {
+                pr2serr("Enclosure only supports Short Enclosure Status: "
+                        "0x%x\n", rsp_buff[1]);
+            } else {
+                pr2serr("Invalid response, wanted page code: 0x%x but got "
+                        "0x%x\n", page_code, rsp_buff[0]);
+                hex2stderr(rsp_buff, rsp_len, 0);
+            }
+            return -2;
+        }
+        return 0;
+    } else if (vb) {
+        pr2serr("Attempt to fetch %s failed\n", cp);
+        sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+        pr2serr("    %s\n", b);
+    }
+    return res;
+}
+
+#if 1
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+#else
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int res, err;
+
+    if (len > 0) {
+        res = write(fileno(stdout), str, len);
+        if (res < 0) {
+            err = errno;
+            pr2serr("%s: write to stdout failed: %s [%d]\n", __func__,
+                    strerror(err), err);
+        }
+    }
+}
+
+#endif
+
+/* CONFIGURATION_DPC [0x1]
+ * Display Configuration diagnostic page. */
+static void
+configuration_sdg(const uint8_t * resp, int resp_len)
+{
+    int j, k, el, num_subs, sum_elem_types;
+    uint32_t gen_code;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+    const uint8_t * text_bp;
+    char b[64];
+
+    printf("Configuration diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    num_subs = resp[1] + 1;  /* number of subenclosures (add 1 for primary) */
+    sum_elem_types = 0;
+    last_bp = resp + resp_len - 1;
+    printf("  number of secondary subenclosures: %d\n",
+            num_subs - 1);
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    bp = resp + 8;
+    printf("  enclosure descriptor list\n");
+    for (k = 0; k < num_subs; ++k, bp += el) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        el = bp[3] + 4;
+        sum_elem_types += bp[2];
+        printf("    Subenclosure identifier: %d%s\n", bp[1],
+               (bp[1] ? "" : " [primary]"));
+        printf("      relative ES process id: %d, number of ES processes"
+               ": %d\n", ((bp[0] & 0x70) >> 4), (bp[0] & 0x7));
+        printf("      number of type descriptor headers: %d\n", bp[2]);
+        if (el < 40) {
+            pr2serr("      enc descriptor len=%d ??\n", el);
+            continue;
+        }
+        printf("      enclosure logical identifier (hex): ");
+        for (j = 0; j < 8; ++j)
+            printf("%02x", bp[4 + j]);
+        printf("\n      enclosure vendor: %.8s  product: %.16s  rev: %.4s\n",
+               bp + 12, bp + 20, bp + 36);
+        if (el > 40) {
+            char bb[1024];
+
+            printf("      vendor-specific data:\n");
+            hex2str(bp + 40, el - 40, "        ", 0, sizeof(bb), bb);
+            printf("%s\n", bb);
+        }
+    }
+    /* printf("\n"); */
+    printf("  type descriptor header and text list\n");
+    text_bp = bp + (sum_elem_types * 4);
+    for (k = 0; k < sum_elem_types; ++k, bp += 4) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        printf("    Element type: %s, subenclosure id: %d\n",
+               etype_str(bp[0], b, sizeof(b)), bp[2]);
+        printf("      number of possible elements: %d\n", bp[1]);
+        if (bp[3] > 0) {
+            if (text_bp > last_bp)
+                goto truncated;
+            printf("      text: %.*s\n", bp[3], text_bp);
+            text_bp += bp[3];
+        }
+    }
+    return;
+truncated:
+    pr2serr("    <<<ses_configuration_sdg: response too short>>>\n");
+    return;
+}
+
+/* CONFIGURATION_DPC [0x1] read and used to build array pointed to by
+ * 'tdhp' with no more than 'max_elems' elements. If 'generationp' is non
+ * NULL then writes generation code where it points. if 'primary_ip" is
+ * non NULL the writes rimary enclosure info where it points.
+ * Returns total number of type descriptor headers written to 'tdhp' or -1
+ * if there is a problem */
+static int
+build_type_desc_hdr_arr(struct sg_pt_base * ptvp,
+                         struct type_desc_hdr_t * tdhp, int max_elems,
+                        uint32_t * generationp,
+                        struct enclosure_info * primary_ip,
+                        struct opts_t * op)
+{
+    int resp_len, k, el, num_subs, sum_type_dheaders, res, n;
+    int ret = 0;
+    uint32_t gen_code;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+
+    if (NULL == config_dp_resp) {
+        config_dp_resp = sg_memalign(op->maxlen, 0, &free_config_dp_resp,
+                                     false);
+        if (NULL == config_dp_resp) {
+            pr2serr("%s: unable to allocate %d bytes on heap\n", __func__,
+                    op->maxlen);
+            ret = -1;
+            goto the_end;
+        }
+        res = do_rec_diag(ptvp, CONFIGURATION_DPC, config_dp_resp, op->maxlen,
+                          op, &resp_len);
+        if (res) {
+            pr2serr("%s: couldn't read config page, res=%d\n", __func__, res);
+            ret = -1;
+            free(free_config_dp_resp);
+            free_config_dp_resp = NULL;
+            goto the_end;
+        }
+        if (resp_len < 4) {
+            ret = -1;
+            free(free_config_dp_resp);
+            free_config_dp_resp = NULL;
+            goto the_end;
+        }
+        config_dp_resp_len = resp_len;
+    } else
+        resp_len = config_dp_resp_len;
+
+    num_subs = config_dp_resp[1] + 1;
+    sum_type_dheaders = 0;
+    last_bp = config_dp_resp + resp_len - 1;
+    gen_code = sg_get_unaligned_be32(config_dp_resp + 4);
+    if (generationp)
+        *generationp = gen_code;
+    bp = config_dp_resp + 8;
+    for (k = 0; k < num_subs; ++k, bp += el) {
+        if ((bp + 3) > last_bp)
+            goto p_truncated;
+        el = bp[3] + 4;
+        sum_type_dheaders += bp[2];
+        if (el < 40) {
+            pr2serr("%s: short enc descriptor len=%d ??\n", __func__, el);
+            continue;
+        }
+        if ((0 == k) && primary_ip) {
+            ++primary_ip->have_info;
+            primary_ip->rel_esp_id = (bp[0] & 0x70) >> 4;
+            primary_ip->num_esp = (bp[0] & 0x7);
+            memcpy(primary_ip->enc_log_id, bp + 4, 8);
+            memcpy(primary_ip->enc_vendor_id, bp + 12, 8);
+            memcpy(primary_ip->product_id, bp + 20, 16);
+            memcpy(primary_ip->product_rev_level, bp + 36, 4);
+        }
+    }
+    for (k = 0; k < sum_type_dheaders; ++k, bp += 4) {
+        if ((bp + 3) > last_bp)
+            goto p_truncated;
+        if (k >= max_elems) {
+            pr2serr("%s: too many elements\n", __func__);
+            ret = -1;
+            goto the_end;
+        }
+        tdhp[k].etype = bp[0];
+        tdhp[k].num_elements = bp[1];
+        tdhp[k].se_id = bp[2];
+        tdhp[k].txt_len = bp[3];
+    }
+    if (op->ind_given && op->ind_etp) {
+        n = op->ind_et_inst;
+        for (k = 0; k < sum_type_dheaders; ++k) {
+            if (op->ind_etp->elem_type_code == tdhp[k].etype) {
+                if (0 == n)
+                    break;
+                else
+                    --n;
+            }
+        }
+        if (k < sum_type_dheaders)
+            op->ind_th = k;
+        else {
+            if (op->ind_et_inst)
+                pr2serr("%s: unable to find element type '%s%d'\n", __func__,
+                        op->ind_etp->abbrev, op->ind_et_inst);
+            else
+                pr2serr("%s: unable to find element type '%s'\n", __func__,
+                        op->ind_etp->abbrev);
+            ret = -1;
+            goto the_end;
+        }
+    }
+    ret = sum_type_dheaders;
+    goto the_end;
+
+p_truncated:
+    pr2serr("%s: config too short\n", __func__);
+    ret = -1;
+
+the_end:
+    if (0 == ret)
+        ++type_desc_hdr_count;
+    return ret;
+}
+
+static char *
+find_sas_connector_type(int conn_type, bool abridged, char * buff,
+                        int buff_len)
+{
+    switch (conn_type) {
+    case 0x0:
+        snprintf(buff, buff_len, "No information");
+        break;
+    case 0x1:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS 4x");
+        else
+            snprintf(buff, buff_len, "SAS 4x receptacle (SFF-8470) "
+                     "[max 4 phys]");
+        break;
+    case 0x2:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS 4x");
+        else
+            snprintf(buff, buff_len, "Mini SAS 4x receptacle (SFF-8088) "
+                     "[max 4 phys]");
+        break;
+    case 0x3:
+        if (abridged)
+            snprintf(buff, buff_len, "QSFP+");
+        else
+            snprintf(buff, buff_len, "QSFP+ receptacle (SFF-8436) "
+                     "[max 4 phys]");
+        break;
+    case 0x4:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS 4x active");
+        else
+            snprintf(buff, buff_len, "Mini SAS 4x active receptacle "
+                     "(SFF-8088) [max 4 phys]");
+        break;
+    case 0x5:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS HD 4x");
+        else
+            snprintf(buff, buff_len, "Mini SAS HD 4x receptacle (SFF-8644) "
+                     "[max 4 phys]");
+        break;
+    case 0x6:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS HD 8x");
+        else
+            snprintf(buff, buff_len, "Mini SAS HD 8x receptacle (SFF-8644) "
+                     "[max 8 phys]");
+        break;
+    case 0x7:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS HD 16x");
+        else
+            snprintf(buff, buff_len, "Mini SAS HD 16x receptacle (SFF-8644) "
+                     "[max 16 phys]");
+        break;
+    case 0xf:
+        snprintf(buff, buff_len, "Vendor specific");
+        break;
+    case 0x10:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS 4i");
+        else
+            snprintf(buff, buff_len, "SAS 4i plug (SFF-8484) [max 4 phys]");
+        break;
+    case 0x11:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS 4i");
+        else
+            snprintf(buff, buff_len, "Mini SAS 4i receptacle (SFF-8087) "
+                     "[max 4 phys]");
+        break;
+    case 0x12:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS HD 4i");
+        else
+            snprintf(buff, buff_len, "Mini SAS HD 4i receptacle (SFF-8643) "
+                     "[max 4 phys]");
+        break;
+    case 0x13:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS HD 8i");
+        else
+            snprintf(buff, buff_len, "Mini SAS HD 8i receptacle (SFF-8643) "
+                     "[max 8 phys]");
+        break;
+    case 0x14:
+        if (abridged)
+            snprintf(buff, buff_len, "Mini SAS HD 16i");
+        else
+            snprintf(buff, buff_len, "Mini SAS HD 16i receptacle (SFF-8643) "
+                     "[max 16 phys]");
+        break;
+    case 0x15:
+        if (abridged)
+            snprintf(buff, buff_len, "SlimSAS 4i");  /* was "SAS SlimLine" */
+        else
+            snprintf(buff, buff_len, "SlimSAS 4i (SFF-8654) [max 4 phys]");
+        break;
+    case 0x16:
+        if (abridged)
+            snprintf(buff, buff_len, "SlimSAS 8i");  /* was "SAS SlimLine" */
+        else
+            snprintf(buff, buff_len, "SlimSAS 8i (SFF-8654) [max 8 phys]");
+        break;
+    case 0x17:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS MiniLink 4i");
+        else
+            snprintf(buff, buff_len, "SAS MiniLink 4i (SFF-8612) "
+                     "[max 4 phys]");
+        break;
+    case 0x18:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS MiniLink 8i");
+        else
+            snprintf(buff, buff_len, "SAS MiniLink 8i (SFF-8612) "
+                     "[max 8 phys]");
+        break;
+    case 0x20:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS Drive backplane");
+        else
+            snprintf(buff, buff_len, "SAS Drive backplane receptacle "
+                     "(SFF-8482) [max 2 phys]");
+        break;
+    case 0x21:
+        if (abridged)
+            snprintf(buff, buff_len, "SATA host plug");
+        else
+            snprintf(buff, buff_len, "SATA host plug [max 1 phy]");
+        break;
+    case 0x22:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS Drive plug");
+        else
+            snprintf(buff, buff_len, "SAS Drive plug (SFF-8482) "
+                     "[max 2 phys]");
+        break;
+    case 0x23:
+        if (abridged)
+            snprintf(buff, buff_len, "SATA device plug");
+        else
+            snprintf(buff, buff_len, "SATA device plug [max 1 phy]");
+        break;
+    case 0x24:
+        if (abridged)
+            snprintf(buff, buff_len, "Micro SAS receptacle");
+        else
+            snprintf(buff, buff_len, "Micro SAS receptacle [max 2 phys]");
+        break;
+    case 0x25:
+        if (abridged)
+            snprintf(buff, buff_len, "Micro SATA device plug");
+        else
+            snprintf(buff, buff_len, "Micro SATA device plug [max 1 phy]");
+        break;
+    case 0x26:
+        if (abridged)
+            snprintf(buff, buff_len, "Micro SAS plug");
+        else
+            snprintf(buff, buff_len, "Micro SAS plug (SFF-8486) [max 2 "
+                     "phys]");
+        break;
+    case 0x27:
+        if (abridged)
+            snprintf(buff, buff_len, "Micro SAS/SATA plug");
+        else
+            snprintf(buff, buff_len, "Micro SAS/SATA plug (SFF-8486) "
+                     "[max 2 phys]");
+        break;
+    case 0x28:
+        if (abridged)
+            snprintf(buff, buff_len, "12 Gb/s SAS drive backplane");
+        else
+            snprintf(buff, buff_len, "12 Gb/s SAS drive backplane receptacle "
+                     "(SFF-8680) [max 2 phys]");
+        break;
+    case 0x29:
+        if (abridged)
+            snprintf(buff, buff_len, "12 Gb/s SAS drive plug");
+        else
+            snprintf(buff, buff_len, "12 Gb/s SAS drive plug (SFF-8680) "
+                     "[max 2 phys]");
+        break;
+    case 0x2a:
+        if (abridged)
+            snprintf(buff, buff_len, "Multifunction 12 Gb/s 6x receptacle");
+        else
+            snprintf(buff, buff_len, "Multifunction 12 Gb/s 6x unshielded "
+                     "receptacle (SFF-8639)");
+        break;
+    case 0x2b:
+        if (abridged)
+            snprintf(buff, buff_len, "Multifunction 12 Gb/s 6x plug");
+        else
+            snprintf(buff, buff_len, "Multifunction 12 Gb/s 6x unshielded "
+                     "plug (SFF-8639)");
+        break;
+    case 0x2c:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS MultiLink Drive backplane "
+                     "receptacle");
+        else
+            snprintf(buff, buff_len, "SAS MultiLink Drive backplane "
+                     "receptacle (SFF-8630)");
+        break;
+    case 0x2d:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS MultiLink Drive backplane plug");
+        else
+            snprintf(buff, buff_len, "SAS MultiLink Drive backplane plug "
+                     "(SFF-8630)");
+        break;
+    case 0x2e:
+        if (abridged)
+            snprintf(buff, buff_len, "Reserved");
+        else
+            snprintf(buff, buff_len, "Reserved for internal connectors to "
+                     "end device");
+        break;
+    case 0x2f:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS virtual connector");
+        else
+            snprintf(buff, buff_len, "SAS virtual connector [max 1 phy]");
+        break;
+    case 0x3f:
+        if (abridged)
+            snprintf(buff, buff_len, "VS internal connector");
+        else
+            snprintf(buff, buff_len, "Vendor specific internal connector");
+        break;
+    case 0x40:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS high density drive backplane "
+                     "receptacle");
+        else
+            snprintf(buff, buff_len, "SAS high density drive backplane "
+                     "receptacle (SFF-8631) [max 8 phys]");
+        break;
+    case 0x41:
+        if (abridged)
+            snprintf(buff, buff_len, "SAS high density drive backplane "
+                     "plug");
+        else
+            snprintf(buff, buff_len, "SAS high density drive backplane "
+                     "plug (SFF-8631) [max 8 phys]");
+        break;
+    default:
+        if (conn_type < 0x10)
+            snprintf(buff, buff_len, "unknown external connector type: 0x%x",
+                     conn_type);
+        else if (conn_type < 0x20)
+            snprintf(buff, buff_len, "unknown internal wide connector type: "
+                     "0x%x", conn_type);
+        else if (conn_type < 0x3f)
+            snprintf(buff, buff_len, "reserved for internal connector, "
+                     "type: 0x%x", conn_type);
+        else if (conn_type < 0x70)
+            snprintf(buff, buff_len, "reserved connector type: 0x%x",
+                     conn_type);
+        else if (conn_type < 0x80)
+            snprintf(buff, buff_len, "vendor specific connector type: 0x%x",
+                     conn_type);
+        else    /* conn_type is a 7 bit field, so this is impossible */
+            snprintf(buff, buff_len, "unexpected connector type: 0x%x",
+                     conn_type);
+        break;
+    }
+    return buff;
+}
+
+/* 'Fan speed factor' new in ses4r04 */
+static int
+calc_fan_speed(int fan_speed_factor, int actual_fan_speed)
+{
+    switch (fan_speed_factor) {
+    case 0:
+        return actual_fan_speed * 10;
+    case 1:
+        return (actual_fan_speed * 10) + 20480;
+    case 2:
+        return actual_fan_speed * 100;
+    default:
+        break;
+    }
+    return -1;        /* something is wrong */
+}
+
+static const char * elem_status_code_desc[] = {
+    "Unsupported", "OK", "Critical", "Noncritical",
+    "Unrecoverable", "Not installed", "Unknown", "Not available",
+    "No access allowed", "reserved [9]", "reserved [10]", "reserved [11]",
+    "reserved [12]", "reserved [13]", "reserved [14]", "reserved [15]",
+};
+
+static const char * actual_speed_desc[] = {
+    "stopped", "at lowest speed", "at second lowest speed",
+    "at third lowest speed", "at intermediate speed",
+    "at third highest speed", "at second highest speed", "at highest speed"
+};
+
+static const char * nv_cache_unit[] = {
+    "Bytes", "KiB", "MiB", "GiB"
+};
+
+static const char * invop_type_desc[] = {
+    "SEND DIAGNOSTIC page code error", "SEND DIAGNOSTIC page format error",
+    "Reserved", "Vendor specific error"
+};
+
+static void
+enc_status_helper(const char * pad, const uint8_t * statp, int etype,
+                  bool abridged, const struct opts_t * op)
+{
+    int res, a, b, ct, bblen;
+    bool nofilter = ! op->do_filter;
+    char bb[128];
+
+
+    if (op->inner_hex) {
+        printf("%s%02x %02x %02x %02x\n", pad, statp[0], statp[1], statp[2],
+               statp[3]);
+        return;
+    }
+    if (! abridged)
+        printf("%sPredicted failure=%d, Disabled=%d, Swap=%d, status: %s\n",
+               pad, !!(statp[0] & 0x40), !!(statp[0] & 0x20),
+               !!(statp[0] & 0x10), elem_status_code_desc[statp[0] & 0xf]);
+    switch (etype) { /* element types */
+    case UNSPECIFIED_ETC:
+        if (op->verbose)
+            printf("%sstatus in hex: %02x %02x %02x %02x\n",
+                   pad, statp[0], statp[1], statp[2], statp[3]);
+        break;
+    case DEVICE_ETC:
+        if (ARRAY_STATUS_DPC == op->page_code) {  /* obsolete after SES-1 */
+            if (nofilter || (0xf0 & statp[1]))
+                printf("%sOK=%d, Reserved device=%d, Hot spare=%d, Cons "
+                       "check=%d\n", pad, !!(statp[1] & 0x80),
+                       !!(statp[1] & 0x40), !!(statp[1] & 0x20),
+                       !!(statp[1] & 0x10));
+            if (nofilter || (0xf & statp[1]))
+                printf("%sIn crit array=%d, In failed array=%d, Rebuild/"
+                       "remap=%d, R/R abort=%d\n", pad, !!(statp[1] & 0x8),
+                       !!(statp[1] & 0x4), !!(statp[1] & 0x2),
+                       !!(statp[1] & 0x1));
+            if (nofilter || ((0x46 & statp[2]) || (0x8 & statp[3])))
+                printf("%sDo not remove=%d, RMV=%d, Ident=%d, Enable bypass "
+                       "A=%d\n", pad, !!(statp[2] & 0x40), !!(statp[2] & 0x4),
+                       !!(statp[2] & 0x2), !!(statp[3] & 0x8));
+            if (nofilter || (0x7 & statp[3]))
+                printf("%sEnable bypass B=%d, Bypass A enabled=%d, Bypass B "
+                        "enabled=%d\n", pad, !!(statp[3] & 0x4),
+                       !!(statp[3] & 0x2), !!(statp[3] & 0x1));
+            break;
+        }
+        printf("%sSlot address: %d\n", pad, statp[1]);
+        if (nofilter || (0xe0 & statp[2]))
+            printf("%sApp client bypassed A=%d, Do not remove=%d, Enc "
+                   "bypassed A=%d\n", pad, !!(statp[2] & 0x80),
+                   !!(statp[2] & 0x40), !!(statp[2] & 0x20));
+        if (nofilter || (0x1c & statp[2]))
+            printf("%sEnc bypassed B=%d, Ready to insert=%d, RMV=%d, Ident="
+                   "%d\n", pad, !!(statp[2] & 0x10), !!(statp[2] & 0x8),
+                   !!(statp[2] & 0x4), !!(statp[2] & 0x2));
+        if (nofilter || ((1 & statp[2]) || (0xe0 & statp[3])))
+            printf("%sReport=%d, App client bypassed B=%d, Fault sensed=%d, "
+                   "Fault requested=%d\n", pad, !!(statp[2] & 0x1),
+                   !!(statp[3] & 0x80), !!(statp[3] & 0x40),
+                   !!(statp[3] & 0x20));
+        if (nofilter || (0x1e & statp[3]))
+            printf("%sDevice off=%d, Bypassed A=%d, Bypassed B=%d, Device "
+                   "bypassed A=%d\n", pad, !!(statp[3] & 0x10),
+                   !!(statp[3] & 0x8), !!(statp[3] & 0x4), !!(statp[3] & 0x2));
+        if (nofilter || (0x1 & statp[3]))
+            printf("%sDevice bypassed B=%d\n", pad, !!(statp[3] & 0x1));
+        break;
+    case POWER_SUPPLY_ETC:
+        if (nofilter || ((0xc0 & statp[1]) || (0xc & statp[2]))) {
+            printf("%sIdent=%d, Do not remove=%d, DC overvoltage=%d, "
+                   "DC undervoltage=%d\n", pad, !!(statp[1] & 0x80),
+                   !!(statp[1] & 0x40), !!(statp[2] & 0x8),
+                   !!(statp[2] & 0x4));
+        }
+        if (nofilter || ((0x2 & statp[2]) || (0xf0 & statp[3])))
+            printf("%sDC overcurrent=%d, Hot swap=%d, Fail=%d, Requested "
+                   "on=%d, Off=%d\n", pad, !!(statp[2] & 0x2),
+                   !!(statp[3] & 0x80), !!(statp[3] & 0x40),
+                   !!(statp[3] & 0x20), !!(statp[3] & 0x10));
+        if (nofilter || (0xf & statp[3]))
+            printf("%sOvertmp fail=%d, Temperature warn=%d, AC fail=%d, "
+                   "DC fail=%d\n", pad, !!(statp[3] & 0x8),
+                   !!(statp[3] & 0x4), !!(statp[3] & 0x2),
+                   !!(statp[3] & 0x1));
+        break;
+    case COOLING_ETC:
+        if (nofilter || ((0xc0 & statp[1]) || (0xf0 & statp[3])))
+            printf("%sIdent=%d, Do not remove=%d, Hot swap=%d, Fail=%d, "
+                   "Requested on=%d\n", pad, !!(statp[1] & 0x80),
+                   !!(statp[1] & 0x40), !!(statp[3] & 0x80),
+                   !!(statp[3] & 0x40), !!(statp[3] & 0x20));
+        printf("%sOff=%d, Actual speed=%d rpm, Fan %s\n", pad,
+               !!(statp[3] & 0x10),
+               calc_fan_speed((statp[1] >> 3) & 0x3,
+                              ((0x7 & statp[1]) << 8) + statp[2]),
+               actual_speed_desc[7 & statp[3]]);
+        if (op->verbose > 1)    /* show real field values */
+            printf("%s  [Fan_speed_factor=%d, Actual_fan_speed=%d]\n",
+                   pad, (statp[1] >> 3) & 0x3,
+                   ((0x7 & statp[1]) << 8) + statp[2]);
+        break;
+    case TEMPERATURE_ETC:     /* temperature sensor */
+        if (nofilter || ((0xc0 & statp[1]) || (0xf & statp[3]))) {
+            printf("%sIdent=%d, Fail=%d, OT failure=%d, OT warning=%d, "
+                   "UT failure=%d\n", pad, !!(statp[1] & 0x80),
+                   !!(statp[1] & 0x40), !!(statp[3] & 0x8),
+                   !!(statp[3] & 0x4), !!(statp[3] & 0x2));
+            printf("%sUT warning=%d\n", pad, !!(statp[3] & 0x1));
+        }
+        if (statp[2])
+            printf("%sTemperature=%d C\n", pad,
+                   (int)statp[2] - TEMPERAT_OFF);
+        else
+            printf("%sTemperature: <reserved>\n", pad);
+        break;
+    case DOOR_ETC:      /* OPEN field added in ses3r05 */
+        if (nofilter || ((0xc0 & statp[1]) || (0x1 & statp[3])))
+            printf("%sIdent=%d, Fail=%d, Open=%d, Unlock=%d\n", pad,
+                   !!(statp[1] & 0x80), !!(statp[1] & 0x40),
+                   !!(statp[3] & 0x2), !!(statp[3] & 0x1));
+        break;
+    case AUD_ALARM_ETC:     /* audible alarm */
+        if (nofilter || ((0xc0 & statp[1]) || (0xd0 & statp[3])))
+            printf("%sIdent=%d, Fail=%d, Request mute=%d, Mute=%d, "
+                   "Remind=%d\n", pad, !!(statp[1] & 0x80),
+                   !!(statp[1] & 0x40), !!(statp[3] & 0x80),
+                   !!(statp[3] & 0x40), !!(statp[3] & 0x10));
+        if (nofilter || (0xf & statp[3]))
+            printf("%sTone indicator: Info=%d, Non-crit=%d, Crit=%d, "
+                   "Unrecov=%d\n", pad, !!(statp[3] & 0x8), !!(statp[3] & 0x4),
+                   !!(statp[3] & 0x2), !!(statp[3] & 0x1));
+        break;
+    case ENC_SCELECTR_ETC: /* enclosure services controller electronics */
+        if (nofilter || (0xe0 & statp[1]) || (0x1 & statp[2]) ||
+            (0x80 & statp[3]))
+            printf("%sIdent=%d, Fail=%d, Do not remove=%d, Report=%d, "
+                   "Hot swap=%d\n", pad, !!(statp[1] & 0x80),
+                   !!(statp[1] & 0x40), !!(statp[1] & 0x20),
+                   !!(statp[2] & 0x1), !!(statp[3] & 0x80));
+        break;
+    case SCC_CELECTR_ETC:     /* SCC controller electronics */
+        if (nofilter || ((0xc0 & statp[1]) || (0x1 & statp[2])))
+            printf("%sIdent=%d, Fail=%d, Report=%d\n", pad,
+                   !!(statp[1] & 0x80), !!(statp[1] & 0x40),
+                   !!(statp[2] & 0x1));
+        break;
+    case NV_CACHE_ETC:     /* Non volatile cache */
+        res = sg_get_unaligned_be16(statp + 2);
+        printf("%sIdent=%d, Fail=%d, Size multiplier=%d, Non volatile cache "
+               "size=0x%x\n", pad, !!(statp[1] & 0x80), !!(statp[1] & 0x40),
+               (statp[1] & 0x3), res);
+        printf("%sHence non volatile cache size: %d %s\n", pad, res,
+               nv_cache_unit[statp[1] & 0x3]);
+        break;
+    case INV_OP_REASON_ETC:   /* Invalid operation reason */
+        res = ((statp[1] >> 6) & 3);
+        printf("%sInvop type=%d   %s\n", pad, res, invop_type_desc[res]);
+        switch (res) {
+        case 0:
+            printf("%sPage not supported=%d\n", pad, (statp[1] & 1));
+            break;
+        case 1:
+            printf("%sByte offset=%d, bit number=%d\n", pad,
+                   sg_get_unaligned_be16(statp + 2), (statp[1] & 7));
+            break;
+        case 2:
+        case 3:
+            printf("%slast 3 bytes (hex): %02x %02x %02x\n", pad, statp[1],
+                   statp[2], statp[3]);
+            break;
+        }
+        break;
+    case UI_POWER_SUPPLY_ETC:   /* Uninterruptible power supply */
+        if (0 == statp[1])
+            printf("%sBattery status: discharged or unknown\n", pad);
+        else if (255 == statp[1])
+            printf("%sBattery status: 255 or more minutes remaining\n", pad);
+        else
+            printf("%sBattery status: %d minutes remaining\n", pad, statp[1]);
+        if (nofilter || (0xf8 & statp[2]))
+            printf("%sAC low=%d, AC high=%d, AC qual=%d, AC fail=%d, DC fail="
+                   "%d\n", pad, !!(statp[2] & 0x80), !!(statp[2] & 0x40),
+                   !!(statp[2] & 0x20), !!(statp[2] & 0x10),
+                   !!(statp[2] & 0x8));
+        if (nofilter || ((0x7 & statp[2]) || (0xe3 & statp[3]))) {
+            printf("%sUPS fail=%d, Warn=%d, Intf fail=%d, Ident=%d, Fail=%d, "
+                   "Do not remove=%d\n", pad, !!(statp[2] & 0x4),
+                   !!(statp[2] & 0x2), !!(statp[2] & 0x1),
+                   !!(statp[3] & 0x80), !!(statp[3] & 0x40),
+                   !!(statp[3] & 0x20));
+            printf("%sBatt fail=%d, BPF=%d\n", pad, !!(statp[3] & 0x2),
+                   !!(statp[3] & 0x1));
+        }
+        break;
+    case DISPLAY_ETC:   /* Display (ses2r15) */
+        if (nofilter || (0xc0 & statp[1])) {
+            int dms = statp[1] & 0x3;
+
+            printf("%sIdent=%d, Fail=%d, Display mode status=%d", pad,
+                   !!(statp[1] & 0x80), !!(statp[1] & 0x40), dms);
+            if ((1 == dms) || (2 == dms)) {
+                uint16_t dcs = sg_get_unaligned_be16(statp + 2);
+
+                printf(", Display character status=0x%x", dcs);
+                if (statp[2] && (0 == statp[3]))
+                    printf(" ['%c']", statp[2]);
+            }
+            printf("\n");
+        }
+        break;
+    case KEY_PAD_ETC:   /* Key pad entry */
+        if (nofilter || (0xc0 & statp[1]))
+            printf("%sIdent=%d, Fail=%d\n", pad, !!(statp[1] & 0x80),
+                   !!(statp[1] & 0x40));
+        break;
+    case ENCLOSURE_ETC:
+        a = ((statp[2] >> 2) & 0x3f);
+        if (nofilter || ((0x80 & statp[1]) || a || (0x2 & statp[2])))
+            printf("%sIdent=%d, Time until power cycle=%d, "
+                   "Failure indication=%d\n", pad, !!(statp[1] & 0x80),
+                   a, !!(statp[2] & 0x2));
+        b = ((statp[3] >> 2) & 0x3f);
+        if (nofilter || (0x1 & statp[2]) || a || b)
+            printf("%sWarning indication=%d, Requested power off "
+                   "duration=%d\n", pad, !!(statp[2] & 0x1), b);
+        if (nofilter || (0x3 & statp[3]))
+            printf("%sFailure requested=%d, Warning requested=%d\n",
+                   pad, !!(statp[3] & 0x2), !!(statp[3] & 0x1));
+        break;
+    case SCSI_PORT_TRAN_ETC:   /* SCSI port/transceiver */
+        if (nofilter || ((0xc0 & statp[1]) || (0x1 & statp[2]) ||
+                           (0x13 & statp[3])))
+            printf("%sIdent=%d, Fail=%d, Report=%d, Disabled=%d, Loss of "
+                   "link=%d, Xmit fail=%d\n", pad, !!(statp[1] & 0x80),
+                   !!(statp[1] & 0x40), !!(statp[2] & 0x1),
+                   !!(statp[3] & 0x10), !!(statp[3] & 0x2),
+                   !!(statp[3] & 0x1));
+        break;
+    case LANGUAGE_ETC:
+        printf("%sIdent=%d, Language code: %.2s\n", pad, !!(statp[1] & 0x80),
+               statp + 2);
+        break;
+    case COMM_PORT_ETC:   /* Communication port */
+        if (nofilter || ((0xc0 & statp[1]) || (0x1 & statp[3])))
+            printf("%sIdent=%d, Fail=%d, Disabled=%d\n", pad,
+                   !!(statp[1] & 0x80), !!(statp[1] & 0x40),
+                   !!(statp[3] & 0x1));
+        break;
+    case VOLT_SENSOR_ETC:   /* Voltage sensor */
+        if (nofilter || (0xcf & statp[1])) {
+            printf("%sIdent=%d, Fail=%d,  Warn Over=%d, Warn Under=%d, "
+                   "Crit Over=%d\n", pad, !!(statp[1] & 0x80),
+                   !!(statp[1] & 0x40), !!(statp[1] & 0x8),
+                   !!(statp[1] & 0x4), !!(statp[1] & 0x2));
+            printf("%sCrit Under=%d\n", pad, !!(statp[1] & 0x1));
+        }
+#ifdef SG_LIB_MINGW
+        printf("%sVoltage: %g volts\n", pad,
+               ((int)(short)sg_get_unaligned_be16(statp + 2) / 100.0));
+#else
+        printf("%sVoltage: %.2f volts\n", pad,
+               ((int)(short)sg_get_unaligned_be16(statp + 2) / 100.0));
+#endif
+        break;
+    case CURR_SENSOR_ETC:   /* Current sensor */
+        if (nofilter || (0xca & statp[1]))
+            printf("%sIdent=%d, Fail=%d, Warn Over=%d, Crit Over=%d\n",
+                    pad, !!(statp[1] & 0x80), !!(statp[1] & 0x40),
+                    !!(statp[1] & 0x8), !!(statp[1] & 0x2));
+#ifdef SG_LIB_MINGW
+        printf("%sCurrent: %g amps\n", pad,
+               ((int)(short)sg_get_unaligned_be16(statp + 2) / 100.0));
+#else
+        printf("%sCurrent: %.2f amps\n", pad,
+               ((int)(short)sg_get_unaligned_be16(statp + 2) / 100.0));
+#endif
+        break;
+    case SCSI_TPORT_ETC:   /* SCSI target port */
+        if (nofilter || ((0xc0 & statp[1]) || (0x1 & statp[2]) ||
+                           (0x1 & statp[3])))
+            printf("%sIdent=%d, Fail=%d, Report=%d, Enabled=%d\n", pad,
+                   !!(statp[1] & 0x80), !!(statp[1] & 0x40),
+                   !!(statp[2] & 0x1), !!(statp[3] & 0x1));
+        break;
+    case SCSI_IPORT_ETC:   /* SCSI initiator port */
+        if (nofilter || ((0xc0 & statp[1]) || (0x1 & statp[2]) ||
+                           (0x1 & statp[3])))
+            printf("%sIdent=%d, Fail=%d, Report=%d, Enabled=%d\n", pad,
+                   !!(statp[1] & 0x80), !!(statp[1] & 0x40),
+                   !!(statp[2] & 0x1), !!(statp[3] & 0x1));
+        break;
+    case SIMPLE_SUBENC_ETC:   /* Simple subenclosure */
+        printf("%sIdent=%d, Fail=%d, Short enclosure status: 0x%x\n", pad,
+               !!(statp[1] & 0x80), !!(statp[1] & 0x40), statp[3]);
+        break;
+    case ARRAY_DEV_ETC:   /* Array device */
+        if (nofilter || (0xf0 & statp[1]))
+            printf("%sOK=%d, Reserved device=%d, Hot spare=%d, Cons check="
+                   "%d\n", pad, !!(statp[1] & 0x80), !!(statp[1] & 0x40),
+                   !!(statp[1] & 0x20), !!(statp[1] & 0x10));
+        if (nofilter || (0xf & statp[1]))
+            printf("%sIn crit array=%d, In failed array=%d, Rebuild/remap=%d"
+                   ", R/R abort=%d\n", pad, !!(statp[1] & 0x8),
+                   !!(statp[1] & 0x4), !!(statp[1] & 0x2),
+                   !!(statp[1] & 0x1));
+        if (nofilter || (0xf0 & statp[2]))
+            printf("%sApp client bypass A=%d, Do not remove=%d, Enc bypass "
+                   "A=%d, Enc bypass B=%d\n", pad, !!(statp[2] & 0x80),
+                   !!(statp[2] & 0x40), !!(statp[2] & 0x20),
+                   !!(statp[2] & 0x10));
+        if (nofilter || (0xf & statp[2]))
+            printf("%sReady to insert=%d, RMV=%d, Ident=%d, Report=%d\n",
+                   pad, !!(statp[2] & 0x8), !!(statp[2] & 0x4),
+                   !!(statp[2] & 0x2), !!(statp[2] & 0x1));
+        if (nofilter || (0xf0 & statp[3]))
+            printf("%sApp client bypass B=%d, Fault sensed=%d, Fault reqstd="
+                   "%d, Device off=%d\n", pad, !!(statp[3] & 0x80),
+                   !!(statp[3] & 0x40), !!(statp[3] & 0x20),
+                   !!(statp[3] & 0x10));
+        if (nofilter || (0xf & statp[3]))
+            printf("%sBypassed A=%d, Bypassed B=%d, Dev bypassed A=%d, "
+                   "Dev bypassed B=%d\n",
+                   pad, !!(statp[3] & 0x8), !!(statp[3] & 0x4),
+                   !!(statp[3] & 0x2), !!(statp[3] & 0x1));
+        break;
+    case SAS_EXPANDER_ETC:
+        printf("%sIdent=%d, Fail=%d\n", pad, !!(statp[1] & 0x80),
+               !!(statp[1] & 0x40));
+        break;
+    case SAS_CONNECTOR_ETC:     /* OC (overcurrent) added in ses3r07 */
+        ct = (statp[1] & 0x7f);
+        bblen = sizeof(bb);
+        if (abridged)
+            printf("%s%s, pl=%d", pad,
+                   find_sas_connector_type(ct, true, bb, bblen), statp[2]);
+        else {
+            printf("%sIdent=%d, %s\n", pad, !!(statp[1] & 0x80),
+                   find_sas_connector_type(ct, false, bb, bblen));
+            /* Mated added in ses3r10 */
+            printf("%sConnector physical link=0x%x, Mated=%d, Fail=%d, "
+                   "OC=%d\n", pad, statp[2], !!(statp[3] & 0x80),
+                   !!(statp[3] & 0x40), !!(statp[3] & 0x20));
+        }
+        break;
+    default:
+        if (etype < 0x80)
+            printf("%sUnknown element type, status in hex: %02x %02x %02x "
+                   "%02x\n", pad, statp[0], statp[1], statp[2], statp[3]);
+        else
+            printf("%sVendor specific element type, status in hex: %02x "
+                   "%02x %02x %02x\n", pad, statp[0], statp[1], statp[2],
+                   statp[3]);
+        break;
+    }
+}
+
+/* ENC_STATUS_DPC [0x2]
+ * Display enclosure status diagnostic page. */
+static void
+enc_status_dp(const struct th_es_t * tesp, uint32_t ref_gen_code,
+              const uint8_t * resp, int resp_len,
+              const struct opts_t * op)
+{
+    int j, k;
+    uint32_t gen_code;
+    bool got1, match_ind_th;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+    const struct type_desc_hdr_t * tdhp = tesp->th_base;
+    char b[64];
+
+    printf("Enclosure Status diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    printf("  INVOP=%d, INFO=%d, NON-CRIT=%d, CRIT=%d, UNRECOV=%d\n",
+           !!(resp[1] & 0x10), !!(resp[1] & 0x8), !!(resp[1] & 0x4),
+           !!(resp[1] & 0x2), !!(resp[1] & 0x1));
+    last_bp = resp + resp_len - 1;
+    if (resp_len < 8)
+        goto truncated;
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%x\n", gen_code);
+    if (ref_gen_code != gen_code) {
+        pr2serr("  <<state of enclosure changed, please try again>>\n");
+        return;
+    }
+    printf("  status descriptor list\n");
+    bp = resp + 8;
+    for (k = 0, got1 = false; k < tesp->num_ths; ++k, ++tdhp) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        match_ind_th = (op->ind_given && (k == op->ind_th));
+        if ((! op->ind_given) || (match_ind_th && (-1 == op->ind_indiv))) {
+            printf("    Element type: %s, subenclosure id: %d [ti=%d]\n",
+                   etype_str(tdhp->etype, b, sizeof(b)), tdhp->se_id, k);
+            printf("      Overall descriptor:\n");
+            enc_status_helper("        ", bp, tdhp->etype, false, op);
+            got1 = true;
+        }
+        for (bp += 4, j = 0; j < tdhp->num_elements; ++j, bp += 4) {
+            if (op->ind_given) {
+                if ((! match_ind_th) || (-1 == op->ind_indiv) ||
+                    (! match_ind_indiv(j, op)))
+                    continue;
+            }
+            printf("      Element %d descriptor:\n", j);
+            enc_status_helper("        ", bp, tdhp->etype, false, op);
+            got1 = true;
+        }
+    }
+    if (op->ind_given && (! got1)) {
+        printf("      >>> no match on --index=%d,%d", op->ind_th,
+               op->ind_indiv);
+        if (op->ind_indiv_last > op->ind_indiv)
+            printf("-%d\n", op->ind_indiv_last);
+        else
+            printf("\n");
+    }
+    return;
+truncated:
+    pr2serr("    <<<enc: response too short>>>\n");
+    return;
+}
+
+/* ARRAY_STATUS_DPC [0x6]
+ * Display array status diagnostic page. */
+static void
+array_status_dp(const struct th_es_t * tesp, uint32_t ref_gen_code,
+                const uint8_t * resp, int resp_len,
+                const struct opts_t * op)
+{
+    int j, k;
+    uint32_t gen_code;
+    bool got1, match_ind_th;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+    const struct type_desc_hdr_t * tdhp = tesp->th_base;
+    char b[64];
+
+    printf("Array Status diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    printf("  INVOP=%d, INFO=%d, NON-CRIT=%d, CRIT=%d, UNRECOV=%d\n",
+           !!(resp[1] & 0x10), !!(resp[1] & 0x8), !!(resp[1] & 0x4),
+           !!(resp[1] & 0x2), !!(resp[1] & 0x1));
+    last_bp = resp + resp_len - 1;
+    if (resp_len < 8)
+        goto truncated;
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%x\n", gen_code);
+    if (ref_gen_code != gen_code) {
+        pr2serr("  <<state of enclosure changed, please try again>>\n");
+        return;
+    }
+    printf("  status descriptor list\n");
+    bp = resp + 8;
+    for (k = 0, got1 = false; k < tesp->num_ths; ++k, ++tdhp) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        match_ind_th = (op->ind_given && (k == op->ind_th));
+        if ((! op->ind_given) || (match_ind_th && (-1 == op->ind_indiv))) {
+            printf("    Element type: %s, subenclosure id: %d [ti=%d]\n",
+                   etype_str(tdhp->etype, b, sizeof(b)), tdhp->se_id, k);
+            printf("      Overall descriptor:\n");
+            enc_status_helper("        ", bp, tdhp->etype, false, op);
+            got1 = true;
+        }
+        for (bp += 4, j = 0; j < tdhp->num_elements; ++j, bp += 4) {
+            if (op->ind_given) {
+                if ((! match_ind_th) || (-1 == op->ind_indiv) ||
+                    (! match_ind_indiv(j, op)))
+                    continue;
+            }
+            printf("      Element %d descriptor:\n", j);
+            enc_status_helper("        ", bp, tdhp->etype, false, op);
+            got1 = true;
+        }
+    }
+    if (op->ind_given && (! got1)) {
+        printf("      >>> no match on --index=%d,%d", op->ind_th,
+               op->ind_indiv);
+        if (op->ind_indiv_last > op->ind_indiv)
+            printf("-%d\n", op->ind_indiv_last);
+        else
+            printf("\n");
+    }
+    return;
+truncated:
+    pr2serr("    <<<arr: response too short>>>\n");
+    return;
+}
+
+static char *
+reserved_or_num(char * buff, int buff_len, int num, int reserve_num)
+{
+    if (num == reserve_num)
+        strncpy(buff, "<res>", buff_len);
+    else
+        snprintf(buff, buff_len, "%d", num);
+    if (buff_len > 0)
+        buff[buff_len - 1] = '\0';
+    return buff;
+}
+
+static void
+threshold_helper(const char * header, const char * pad,
+                 const uint8_t *tp, int etype,
+                 const struct opts_t * op)
+{
+    char b[128];
+    char b2[128];
+
+    if (op->inner_hex) {
+        if (header)
+            printf("%s", header);
+        printf("%s%02x %02x %02x %02x\n", pad, tp[0], tp[1], tp[2], tp[3]);
+        return;
+    }
+    switch (etype) {
+    case 0x4:  /*temperature */
+        if (header)
+            printf("%s", header);
+        printf("%shigh critical=%s, high warning=%s", pad,
+               reserved_or_num(b, 128, tp[0] - TEMPERAT_OFF, -TEMPERAT_OFF),
+               reserved_or_num(b2, 128, tp[1] - TEMPERAT_OFF, -TEMPERAT_OFF));
+        if (op->do_filter && (0 == tp[2]) && (0 == tp[3])) {
+            printf(" (in Celsius)\n");
+            break;
+        }
+        printf("\n%slow warning=%s, low critical=%s (in Celsius)\n", pad,
+               reserved_or_num(b, 128, tp[2] - TEMPERAT_OFF, -TEMPERAT_OFF),
+               reserved_or_num(b2, 128, tp[3] - TEMPERAT_OFF, -TEMPERAT_OFF));
+        break;
+    case 0xb:  /* UPS */
+        if (header)
+            printf("%s", header);
+        if (0 == tp[2])
+            strcpy(b, "<vendor>");
+        else
+            snprintf(b, sizeof(b), "%d", tp[2]);
+        printf("%slow warning=%s, ", pad, b);
+        if (0 == tp[3])
+            strcpy(b, "<vendor>");
+        else
+            snprintf(b, sizeof(b), "%d", tp[3]);
+        printf("low critical=%s (in minutes)\n", b);
+        break;
+    case 0x12: /* voltage */
+        if (header)
+            printf("%s", header);
+#ifdef SG_LIB_MINGW
+        printf("%shigh critical=%g %%, high warning=%g %% (above nominal "
+               "voltage)\n", pad, 0.5 * tp[0], 0.5 * tp[1]);
+        printf("%slow warning=%g %%, low critical=%g %% (below nominal "
+               "voltage)\n", pad, 0.5 * tp[2], 0.5 * tp[3]);
+#else
+        printf("%shigh critical=%.1f %%, high warning=%.1f %% (above nominal "
+               "voltage)\n", pad, 0.5 * tp[0], 0.5 * tp[1]);
+        printf("%slow warning=%.1f %%, low critical=%.1f %% (below nominal "
+               "voltage)\n", pad, 0.5 * tp[2], 0.5 * tp[3]);
+#endif
+        break;
+    case 0x13: /* current */
+        if (header)
+            printf("%s", header);
+#ifdef SG_LIB_MINGW
+        printf("%shigh critical=%g %%, high warning=%g %%", pad,
+               0.5 * tp[0], 0.5 * tp[1]);
+#else
+        printf("%shigh critical=%.1f %%, high warning=%.1f %%", pad,
+               0.5 * tp[0], 0.5 * tp[1]);
+#endif
+        printf(" (above nominal current)\n");
+        break;
+    default:
+        if (op->verbose) {
+            if (header)
+                printf("%s", header);
+            printf("%s<< no thresholds for this element type >>\n", pad);
+        }
+        break;
+    }
+}
+
+/* THRESHOLD_DPC [0x5] */
+static void
+threshold_sdg(const struct th_es_t * tesp, uint32_t ref_gen_code,
+              const uint8_t * resp, int resp_len,
+              const struct opts_t * op)
+{
+    int j, k;
+    uint32_t gen_code;
+    bool got1, match_ind_th;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+    const struct type_desc_hdr_t * tdhp = tesp->th_base;
+    char b[64];
+
+    printf("Threshold In diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    printf("  INVOP=%d\n", !!(resp[1] & 0x10));
+    last_bp = resp + resp_len - 1;
+    if (resp_len < 8)
+        goto truncated;
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    if (ref_gen_code != gen_code) {
+        pr2serr("  <<state of enclosure changed, please try again>>\n");
+        return;
+    }
+    printf("  Threshold status descriptor list\n");
+    bp = resp + 8;
+    for (k = 0, got1 = false; k < tesp->num_ths; ++k, ++tdhp) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        match_ind_th = (op->ind_given && (k == op->ind_th));
+        if ((! op->ind_given) || (match_ind_th && (-1 == op->ind_indiv))) {
+            printf("    Element type: %s, subenclosure id: %d [ti=%d]\n",
+                   etype_str(tdhp->etype, b, sizeof(b)), tdhp->se_id, k);
+            threshold_helper("      Overall descriptor:\n", "        ", bp,
+                             tdhp->etype, op);
+            got1 = true;
+        }
+        for (bp += 4, j = 0; j < tdhp->num_elements; ++j, bp += 4) {
+            if (op->ind_given) {
+                if ((! match_ind_th) || (-1 == op->ind_indiv) ||
+                    (! match_ind_indiv(j, op)))
+                    continue;
+            }
+            snprintf(b, sizeof(b), "      Element %d descriptor:\n", j);
+            threshold_helper(b, "        ", bp, tdhp->etype, op);
+            got1 = true;
+        }
+    }
+    if (op->ind_given && (! got1)) {
+        printf("      >>> no match on --index=%d,%d", op->ind_th,
+               op->ind_indiv);
+        if (op->ind_indiv_last > op->ind_indiv)
+            printf("-%d\n", op->ind_indiv_last);
+        else
+            printf("\n");
+    }
+    return;
+truncated:
+    pr2serr("    <<<thresh: response too short>>>\n");
+    return;
+}
+
+/* ELEM_DESC_DPC [0x7]
+ * This page essentially contains names of overall and individual
+ * elements. */
+static void
+element_desc_sdg(const struct th_es_t * tesp, uint32_t ref_gen_code,
+                 const uint8_t * resp, int resp_len,
+                 const struct opts_t * op)
+{
+    int j, k, desc_len;
+    uint32_t gen_code;
+    bool got1, match_ind_th;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+    const struct type_desc_hdr_t * tp;
+    char b[64];
+
+    printf("Element Descriptor In diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    last_bp = resp + resp_len - 1;
+    if (resp_len < 8)
+        goto truncated;
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    if (ref_gen_code != gen_code) {
+        pr2serr("  <<state of enclosure changed, please try again>>\n");
+        return;
+    }
+    printf("  element descriptor list (grouped by type):\n");
+    bp = resp + 8;
+    got1 = false;
+    for (k = 0, tp = tesp->th_base; k < tesp->num_ths; ++k, ++tp) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        desc_len = sg_get_unaligned_be16(bp + 2) + 4;
+        match_ind_th = (op->ind_given && (k == op->ind_th));
+        if ((! op->ind_given) || (match_ind_th && (-1 == op->ind_indiv))) {
+            printf("    Element type: %s, subenclosure id: %d [ti=%d]\n",
+                   etype_str(tp->etype, b, sizeof(b)), tp->se_id, k);
+            if (desc_len > 4)
+                printf("      Overall descriptor: %.*s\n", desc_len - 4,
+                       bp + 4);
+            else
+                printf("      Overall descriptor: <empty>\n");
+            got1 = true;
+        }
+        for (bp += desc_len, j = 0; j < tp->num_elements;
+             ++j, bp += desc_len) {
+            desc_len = sg_get_unaligned_be16(bp + 2) + 4;
+            if (op->ind_given) {
+                if ((! match_ind_th) || (-1 == op->ind_indiv) ||
+                    (! match_ind_indiv(j, op)))
+                    continue;
+            }
+            if (desc_len > 4)
+                printf("      Element %d descriptor: %.*s\n", j,
+                       desc_len - 4, bp + 4);
+            else
+                printf("      Element %d descriptor: <empty>\n", j);
+            got1 = true;
+        }
+    }
+    if (op->ind_given && (! got1)) {
+        printf("      >>> no match on --index=%d,%d", op->ind_th,
+               op->ind_indiv);
+        if (op->ind_indiv_last > op->ind_indiv)
+            printf("-%d\n", op->ind_indiv_last);
+        else
+            printf("\n");
+    }
+    return;
+truncated:
+    pr2serr("    <<<element: response too short>>>\n");
+    return;
+}
+
+static bool
+saddr_non_zero(const uint8_t * bp)
+{
+    return ! sg_all_zeros(bp, 8);
+}
+
+static const char * sas_device_type[] = {
+    "no SAS device attached",   /* but might be SATA device */
+    "end device",
+    "expander device",  /* in SAS-1.1 this was a "edge expander device */
+    "expander device (fanout, SAS-1.1)",  /* marked obsolete in SAS-2 */
+    "reserved [4]", "reserved [5]", "reserved [6]", "reserved [7]"
+};
+
+static void
+additional_elem_sas(const char * pad, const uint8_t * ae_bp, int etype,
+                    const struct th_es_t * tesp, const struct opts_t * op)
+{
+    int phys, j, m, n, desc_type, eiioe, eip_offset;
+    bool nofilter = ! op->do_filter;
+    bool eip;
+    const struct join_row_t * jrp;
+    const uint8_t * aep;
+    const uint8_t * ed_bp;
+    const char * cp;
+    char b[64];
+
+    eip = !!(0x10 & ae_bp[0]);
+    eiioe = eip ? (0x3 & ae_bp[2]) : 0;
+    eip_offset = eip ? 2 : 0;
+    desc_type = (ae_bp[3 + eip_offset] >> 6) & 0x3;
+    if (op->verbose > 1)
+        printf("%sdescriptor_type: %d\n", pad, desc_type);
+    if (0 == desc_type) {
+        phys = ae_bp[2 + eip_offset];
+        printf("%snumber of phys: %d, not all phys: %d", pad, phys,
+               ae_bp[3 + eip_offset] & 1);
+        if (eip_offset)
+            printf(", device slot number: %d", ae_bp[5 + eip_offset]);
+        printf("\n");
+        aep = ae_bp + 4 + eip_offset + eip_offset;
+        for (j = 0; j < phys; ++j, aep += 28) {
+            bool print_sas_addr = false;
+            bool saddr_nz;
+
+            printf("%sphy index: %d\n", pad, j);
+            printf("%s  SAS device type: %s\n", pad,
+                   sas_device_type[(0x70 & aep[0]) >> 4]);
+            if (nofilter || (0xe & aep[2]))
+                printf("%s  initiator port for:%s%s%s\n", pad,
+                       ((aep[2] & 8) ? " SSP" : ""),
+                       ((aep[2] & 4) ? " STP" : ""),
+                       ((aep[2] & 2) ? " SMP" : ""));
+            if (nofilter || (0x8f & aep[3]))
+                printf("%s  target port for:%s%s%s%s%s\n", pad,
+                       ((aep[3] & 0x80) ? " SATA_port_selector" : ""),
+                       ((aep[3] & 8) ? " SSP" : ""),
+                       ((aep[3] & 4) ? " STP" : ""),
+                       ((aep[3] & 2) ? " SMP" : ""),
+                       ((aep[3] & 1) ? " SATA_device" : ""));
+            saddr_nz = saddr_non_zero(aep + 4);
+            if (nofilter || saddr_nz) {
+                print_sas_addr = true;
+                printf("%s  attached SAS address: 0x", pad);
+                if (saddr_nz) {
+                    for (m = 0; m < 8; ++m)
+                        printf("%02x", aep[4 + m]);
+                } else
+                    printf("0");
+            }
+            saddr_nz = saddr_non_zero(aep + 12);
+            if (nofilter || saddr_nz) {
+                print_sas_addr = true;
+                printf("\n%s  SAS address: 0x", pad);
+                if (saddr_nz) {
+                    for (m = 0; m < 8; ++m)
+                        printf("%02x", aep[12 + m]);
+                } else
+                    printf("0");
+            }
+            if (print_sas_addr)
+                printf("\n%s  phy identifier: 0x%x\n", pad, aep[20]);
+        }
+    } else if (1 == desc_type) {
+        phys = ae_bp[2 + eip_offset];
+        if (SAS_EXPANDER_ETC == etype) {
+            printf("%snumber of phys: %d\n", pad, phys);
+            printf("%sSAS address: 0x", pad);
+            for (m = 0; m < 8; ++m)
+                printf("%02x", ae_bp[6 + eip_offset + m]);
+            printf("\n%sAttached connector; other_element pairs:\n", pad);
+            aep = ae_bp + 14 + eip_offset;
+            for (j = 0; j < phys; ++j, aep += 2) {
+                printf("%s  [%d] ", pad, j);
+                m = aep[0];     /* connector element index */
+                if (0xff == m)
+                    printf("no connector");
+                else {
+                    if (tesp->j_base) {
+                        if (0 == eiioe)
+                            jrp = find_join_row_cnst(tesp, m, FJ_SAS_CON);
+                        else if ((1 == eiioe) || (3 == eiioe))
+                            jrp = find_join_row_cnst(tesp, m, FJ_IOE);
+                        else
+                            jrp = find_join_row_cnst(tesp, m, FJ_EOE);
+                        if ((NULL == jrp) || (NULL == jrp->enc_statp) ||
+                            (SAS_CONNECTOR_ETC != jrp->etype))
+                            printf("broken [conn_idx=%d]", m);
+                        else {
+                            enc_status_helper("", jrp->enc_statp, jrp->etype,
+                                              true, op);
+                            printf(" [%d]", jrp->indiv_i);
+                        }
+                    } else
+                        printf("connector ei: %d", m);
+                }
+                m = aep[1];     /* other element index */
+                if (0xff != m) {
+                    printf("; ");
+                    if (tesp->j_base) {
+
+                        if (0 == eiioe)
+                            jrp = find_join_row_cnst(tesp, m, FJ_AESS);
+                        else if ((1 == eiioe) || (3 == eiioe))
+                            jrp = find_join_row_cnst(tesp, m, FJ_IOE);
+                        else
+                            jrp = find_join_row_cnst(tesp, m, FJ_EOE);
+                        if (NULL == jrp)
+                            printf("broken [oth_elem_idx=%d]", m);
+                        else if (jrp->elem_descp) {
+                            cp = etype_str(jrp->etype, b, sizeof(b));
+                            ed_bp = jrp->elem_descp;
+                            n = sg_get_unaligned_be16(ed_bp + 2);
+                            if (n > 0)
+                                printf("%.*s [%d,%d] etype: %s", n,
+                                       (const char *)(ed_bp + 4),
+                                       jrp->th_i, jrp->indiv_i, cp);
+                            else
+                                printf("[%d,%d] etype: %s", jrp->th_i,
+                                       jrp->indiv_i, cp);
+                        } else {
+                            cp = etype_str(jrp->etype, b, sizeof(b));
+                            printf("[%d,%d] etype: %s", jrp->th_i,
+                                   jrp->indiv_i, cp);
+                        }
+                    } else
+                        printf("other ei: %d", m);
+                }
+                printf("\n");
+            }
+        } else if ((SCSI_TPORT_ETC == etype) ||
+                   (SCSI_IPORT_ETC == etype) ||
+                   (ENC_SCELECTR_ETC == etype)) {
+            printf("%snumber of phys: %d\n", pad, phys);
+            aep = ae_bp + 6 + eip_offset;
+            for (j = 0; j < phys; ++j, aep += 12) {
+                printf("%sphy index: %d\n", pad, j);
+                printf("%s  phy_id: 0x%x\n", pad, aep[0]);
+                printf("%s  ", pad);
+                m = aep[2];     /* connector element index */
+                if (0xff == m)
+                    printf("no connector");
+                else {
+                    if (tesp->j_base) {
+                        if (0 == eiioe)
+                            jrp = find_join_row_cnst(tesp, m, FJ_SAS_CON);
+                        else if ((1 == eiioe) || (3 == eiioe))
+                            jrp = find_join_row_cnst(tesp, m, FJ_IOE);
+                        else
+                            jrp = find_join_row_cnst(tesp, m, FJ_EOE);
+                        if ((NULL == jrp) || (NULL == jrp->enc_statp) ||
+                            (SAS_CONNECTOR_ETC != jrp->etype))
+                            printf("broken [conn_idx=%d]", m);
+                        else {
+                            enc_status_helper("", jrp->enc_statp, jrp->etype,
+                                              true, op);
+                            printf(" [%d]", jrp->indiv_i);
+                        }
+                    } else
+                        printf("connector ei: %d", m);
+                }
+                m = aep[3];     /* other element index */
+                if (0xff != m) {
+                    printf("; ");
+                    if (tesp->j_base) {
+                        if (0 == eiioe)
+                            jrp = find_join_row_cnst(tesp, m, FJ_AESS);
+                        else if ((1 == eiioe) || (3 == eiioe))
+                            jrp = find_join_row_cnst(tesp, m, FJ_IOE);
+                        else
+                            jrp = find_join_row_cnst(tesp, m, FJ_EOE);
+                        if (NULL == jrp)
+                            printf("broken [oth_elem_idx=%d]", m);
+                        else if (jrp->elem_descp) {
+                            cp = etype_str(jrp->etype, b, sizeof(b));
+                            ed_bp = jrp->elem_descp;
+                            n = sg_get_unaligned_be16(ed_bp + 2);
+                            if (n > 0)
+                                printf("%.*s [%d,%d] etype: %s", n,
+                                       (const char *)(ed_bp + 4),
+                                       jrp->th_i, jrp->indiv_i, cp);
+                            else
+                                printf("[%d,%d] etype: %s", jrp->th_i,
+                                       jrp->indiv_i, cp);
+                        } else {
+                            cp = etype_str(jrp->etype, b, sizeof(b));
+                            printf("[%d,%d] etype: %s", jrp->th_i,
+                                   jrp->indiv_i, cp);
+                        }
+                    } else
+                        printf("other ei: %d", m);
+                }
+                printf("\n");
+                printf("%s  SAS address: 0x", pad);
+                for (m = 0; m < 8; ++m)
+                    printf("%02x", aep[4 + m]);
+                printf("\n");
+            }   /* end_for: loop over phys in SCSI initiator, target */
+        } else
+            printf("%sunrecognised element type [%d] for desc_type "
+                   "1\n", pad, etype);
+    } else
+        printf("%sunrecognised descriptor type [%d]\n", pad, desc_type);
+}
+
+static void
+additional_elem_helper(const char * pad, const uint8_t * ae_bp,
+                       int len, int etype, const struct th_es_t * tesp,
+                       const struct opts_t * op)
+{
+    int ports, phys, j, m, eip_offset, pcie_pt;
+    bool eip;
+    uint16_t pcie_vid;
+    const uint8_t * aep;
+    char b[64];
+
+    if (op->inner_hex) {
+        for (j = 0; j < len; ++j) {
+            if (0 == (j % 16))
+                printf("%s%s", ((0 == j) ? "" : "\n"), pad);
+            printf("%02x ", ae_bp[j]);
+        }
+        printf("\n");
+        return;
+    }
+    eip = !!(0x10 & ae_bp[0]);
+    eip_offset = eip ? 2 : 0;
+    switch (0xf & ae_bp[0]) {     /* switch on protocol identifier */
+    case TPROTO_FCP:
+        printf("%sTransport protocol: FCP\n", pad);
+        if (len < (12 + eip_offset))
+            break;
+        ports = ae_bp[2 + eip_offset];
+        printf("%snumber of ports: %d\n", pad, ports);
+        printf("%snode_name: ", pad);
+        for (m = 0; m < 8; ++m)
+            printf("%02x", ae_bp[6 + eip_offset + m]);
+        if (eip_offset)
+            printf(", device slot number: %d", ae_bp[5 + eip_offset]);
+        printf("\n");
+        aep = ae_bp + 14 + eip_offset;
+        for (j = 0; j < ports; ++j, aep += 16) {
+            printf("%s  port index: %d, port loop position: %d, port "
+                   "bypass reason: 0x%x\n", pad, j, aep[0], aep[1]);
+            printf("%srequested hard address: %d, n_port identifier: "
+                   "%02x%02x%02x\n", pad, aep[4], aep[5],
+                   aep[6], aep[7]);
+            printf("%s  n_port name: ", pad);
+            for (m = 0; m < 8; ++m)
+                printf("%02x", aep[8 + m]);
+            printf("\n");
+        }
+        break;
+    case TPROTO_SAS:
+        printf("%sTransport protocol: SAS\n", pad);
+        if (len < (4 + eip_offset))
+            break;
+        additional_elem_sas(pad, ae_bp, etype, tesp, op);
+        break;
+    case TPROTO_PCIE: /* added in ses3r08; contains little endian fields */
+        printf("%sTransport protocol: PCIe\n", pad);
+        if (0 == eip_offset) {
+            printf("%sfor this protocol EIP must be set (it isn't)\n", pad);
+            break;
+        }
+        if (len < 6)
+            break;
+        pcie_pt = (ae_bp[5] >> 5) & 0x7;
+        if (TPROTO_PCIE_PS_NVME == pcie_pt)
+            printf("%sPCIe protocol type: NVMe\n", pad);
+        else {  /* no others currently defined */
+            printf("%sTransport protocol: PCIe subprotocol=0x%x not "
+                   "decoded\n", pad, pcie_pt);
+            if (op->verbose)
+                hex2stdout(ae_bp, len, 0);
+            break;
+        }
+        phys = ae_bp[4];
+        printf("%snumber of ports: %d, not all ports: %d", pad, phys,
+               ae_bp[5] & 1);
+        printf(", device slot number: %d\n", ae_bp[7]);
+
+        pcie_vid = sg_get_unaligned_le16(ae_bp + 10);   /* N.B. LE */
+        printf("%sPCIe vendor id: 0x%" PRIx16 "%s\n", pad, pcie_vid,
+               (0xffff == pcie_vid) ? " (not reported)" : "");
+        printf("%sserial number: %.20s\n", pad, ae_bp + 12);
+        printf("%smodel number: %.40s\n", pad, ae_bp + 32);
+        aep = ae_bp + 72;
+        for (j = 0; j < phys; ++j, aep += 8) {
+            bool psn_valid = !!(0x4 & aep[0]);
+            bool bdf_valid = !!(0x2 & aep[0]);
+            bool cid_valid = !!(0x1 & aep[0]);
+
+            printf("%sport index: %d\n", pad, j);
+            printf("%s  PSN_VALID=%d, BDF_VALID=%d, CID_VALID=%d\n", pad,
+                   (int)psn_valid, (int)bdf_valid, (int)cid_valid);
+            if (cid_valid)      /* N.B. little endian */
+                printf("%s  controller id: 0x%" PRIx16 "\n", pad,
+                       sg_get_unaligned_le16(aep + 1)); /* N.B. LEndian */
+            if (bdf_valid)
+                printf("%s  bus number: 0x%x, device number: 0x%x, "
+                       "function number: 0x%x\n", pad, aep[4],
+                       (aep[5] >> 3) & 0x1f, 0x7 & aep[5]);
+            if (psn_valid)      /* little endian, top 3 bits assumed zero */
+                printf("%s  physical slot number: 0x%" PRIx16 "\n", pad,
+                       0x1fff & sg_get_unaligned_le16(aep + 6)); /* N.B. LE */
+        }
+        break;
+    default:
+        printf("%sTransport protocol: %s not decoded\n", pad,
+               sg_get_trans_proto_str((0xf & ae_bp[0]), sizeof(b), b));
+        if (op->verbose)
+            hex2stdout(ae_bp, len, 0);
+        break;
+    }
+}
+
+/* ADD_ELEM_STATUS_DPC [0xa] Additional Element Status dpage
+ * Previously called "Device element status descriptor". Changed "device"
+ * to "additional" to allow for SAS expander and SATA devices */
+static void
+additional_elem_sdg(const struct th_es_t * tesp, uint32_t ref_gen_code,
+                    const uint8_t * resp, int resp_len,
+                    const struct opts_t * op)
+{
+    int j, k, desc_len, etype, el_num, ind, elem_count, ei, eiioe, num_elems;
+    int fake_ei;
+    uint32_t gen_code;
+    bool eip, invalid, match_ind_th, my_eiioe_force, skip;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+    const struct type_desc_hdr_t * tp = tesp->th_base;
+    char b[64];
+
+    printf("Additional element status diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    last_bp = resp + resp_len - 1;
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    if (ref_gen_code != gen_code) {
+        pr2serr("  <<state of enclosure changed, please try again>>\n");
+        return;
+    }
+    printf("  additional element status descriptor list\n");
+    bp = resp + 8;
+    my_eiioe_force = op->eiioe_force;
+    for (k = 0, elem_count = 0; k < tesp->num_ths; ++k, ++tp) {
+        fake_ei = -1;
+        etype = tp->etype;
+        num_elems = tp->num_elements;
+        if (! is_et_used_by_aes(etype)) {
+            elem_count += num_elems;
+            continue;   /* skip if not element type of interest */
+        }
+        if ((bp + 1) > last_bp)
+            goto truncated;
+
+        eip = !! (bp[0] & 0x10);
+        if (eip) { /* do bounds check on the element index */
+            ei = bp[3];
+            skip = false;
+            if ((0 == k) && op->eiioe_auto && (1 == ei)) {
+                /* heuristic: if first AES descriptor has EIP set and its
+                 * element index equal to 1, then act as if the EIIOE field
+                 * is one. */
+                my_eiioe_force = true;
+            }
+            eiioe = (0x3 & bp[2]);
+            if (my_eiioe_force && (0 == eiioe))
+                eiioe = 1;
+            if (1 == eiioe) {
+                if ((ei < (elem_count + k)) ||
+                    (ei > (elem_count + k + num_elems))) {
+                    elem_count += num_elems;
+                    skip = true;
+                }
+            } else {
+                if ((ei < elem_count) || (ei > elem_count + num_elems)) {
+                    if ((0 == ei) && (TPROTO_SAS == (0xf & bp[0])) &&
+                        (1 == (bp[5] >> 6))) {
+                        /* heuristic (hack) for Areca 8028 */
+                        fake_ei = elem_count;
+                        if (op->verbose > 2)
+                            pr2serr("%s: hack, bad ei=%d, fake_ei=%d\n",
+                                    __func__, ei, fake_ei);
+                        ei = fake_ei;
+                    } else {
+                        elem_count += num_elems;
+                        skip = true;
+                    }
+                }
+            }
+            if (skip) {
+                if (op->verbose > 2)
+                    pr2serr("skipping etype=0x%x, k=%d due to "
+                            "element_index=%d bounds\n  effective eiioe=%d, "
+                            "elem_count=%d, num_elems=%d\n", etype, k,
+                            ei, eiioe, elem_count, num_elems);
+                continue;
+            }
+        }
+        match_ind_th = (op->ind_given && (k == op->ind_th));
+        if ((! op->ind_given) || (match_ind_th && (-1 == op->ind_indiv))) {
+            printf("    Element type: %s, subenclosure id: %d [ti=%d]\n",
+                   etype_str(etype, b, sizeof(b)), tp->se_id, k);
+        }
+        el_num = 0;
+        for (j = 0; j < num_elems; ++j, bp += desc_len, ++el_num) {
+            invalid = !!(bp[0] & 0x80);
+            desc_len = bp[1] + 2;
+            eip = !!(bp[0] & 0x10);
+            eiioe = eip ? (0x3 & bp[2]) : 0;
+            if (fake_ei >= 0)
+                ind = fake_ei;
+            else
+                ind = eip ? bp[3] : el_num;
+            if (op->ind_given) {
+                if ((! match_ind_th) || (-1 == op->ind_indiv) ||
+                    (! match_ind_indiv(el_num, op)))
+                    continue;
+            }
+            if (eip)
+                printf("      Element index: %d  eiioe=%d%s\n", ind, eiioe,
+                       (((0 != eiioe) && my_eiioe_force) ?
+                        " but overridden" : ""));
+            else
+                printf("      Element %d descriptor\n", ind);
+            if (invalid && (! op->inner_hex))
+                printf("        flagged as invalid (no further "
+                       "information)\n");
+            else
+                additional_elem_helper("        ", bp, desc_len, etype,
+                                       tesp, op);
+        }
+        elem_count += tp->num_elements;
+    }           /* end_for: loop over type descriptor headers */
+    return;
+truncated:
+    pr2serr("    <<<additional: response too short>>>\n");
+    return;
+}
+
+/* SUBENC_HELP_TEXT_DPC [0xb] */
+static void
+subenc_help_sdg(const uint8_t * resp, int resp_len)
+{
+    int k, el, num_subs;
+    uint32_t gen_code;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+
+    printf("Subenclosure help text diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    num_subs = resp[1] + 1;  /* number of subenclosures (add 1 for primary) */
+    last_bp = resp + resp_len - 1;
+    printf("  number of secondary subenclosures: %d\n", num_subs - 1);
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    bp = resp + 8;
+    for (k = 0; k < num_subs; ++k, bp += el) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        el = sg_get_unaligned_be16(bp + 2) + 4;
+        printf("   subenclosure identifier: %d\n", bp[1]);
+        if (el > 4)
+            printf("    %.*s\n", el - 4, bp + 4);
+        else
+            printf("    <empty>\n");
+    }
+    return;
+truncated:
+    pr2serr("    <<<subenc: response too short>>>\n");
+    return;
+}
+
+/* SUBENC_STRING_DPC [0xc] */
+static void
+subenc_string_sdg(const uint8_t * resp, int resp_len)
+{
+    int k, el, num_subs;
+    uint32_t gen_code;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+
+    printf("Subenclosure string in diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    num_subs = resp[1] + 1;  /* number of subenclosures (add 1 for primary) */
+    last_bp = resp + resp_len - 1;
+    printf("  number of secondary subenclosures: %d\n", num_subs - 1);
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    bp = resp + 8;
+    for (k = 0; k < num_subs; ++k, bp += el) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        el = sg_get_unaligned_be16(bp + 2) + 4;
+        printf("   subenclosure identifier: %d\n", bp[1]);
+        if (el > 4) {
+            char bb[1024];
+
+            hex2str(bp + 40, el - 40, "    ", 0, sizeof(bb), bb);
+            printf("%s\n", bb);
+        } else
+            printf("    <empty>\n");
+    }
+    return;
+truncated:
+    pr2serr("    <<<subence str: response too short>>>\n");
+    return;
+}
+
+/* SUBENC_NICKNAME_DPC [0xf] */
+static void
+subenc_nickname_sdg(const uint8_t * resp, int resp_len)
+{
+    int k, el, num_subs;
+    uint32_t gen_code;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+
+    printf("Subenclosure nickname status diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    num_subs = resp[1] + 1;  /* number of subenclosures (add 1 for primary) */
+    last_bp = resp + resp_len - 1;
+    printf("  number of secondary subenclosures: %d\n", num_subs - 1);
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    bp = resp + 8;
+    el = 40;
+    for (k = 0; k < num_subs; ++k, bp += el) {
+        if ((bp + el - 1) > last_bp)
+            goto truncated;
+        printf("   subenclosure identifier: %d\n", bp[1]);
+        printf("   nickname status: 0x%x\n", bp[2]);
+        printf("   nickname additional status: 0x%x\n", bp[3]);
+        printf("   nickname language code: %.2s\n", bp + 6);
+        printf("   nickname: %.*s\n", 32, bp + 8);
+    }
+    return;
+truncated:
+    pr2serr("    <<<subence str: response too short>>>\n");
+    return;
+}
+
+/* SUPPORTED_SES_DPC [0xd] */
+static void
+supported_pages_sdg(const char * leadin, const uint8_t * resp,
+                    int resp_len)
+{
+    int k, code, prev;
+    bool got1;
+    const struct diag_page_abbrev * ap;
+
+    printf("%s:\n", leadin);
+    for (k = 0, prev = 0; k < (resp_len - 4); ++k, prev = code) {
+        const char * cp;
+
+        code = resp[k + 4];
+        if (code < prev)
+            break;      /* assume to be padding at end */
+        cp = find_diag_page_desc(code);
+        if (cp) {
+            printf("  %s [", cp);
+            for (ap = dp_abbrev, got1 = false; ap->abbrev; ++ap) {
+                if (ap->page_code == code) {
+                    printf("%s%s", (got1 ? "," : ""), ap->abbrev);
+                    got1 = true;
+                }
+            }
+            printf("] [0x%x]\n", code);
+        } else
+            printf("  <unknown> [0x%x]\n", code);
+    }
+}
+
+/* An array of Download microcode status field values and descriptions */
+static struct diag_page_code mc_status_arr[] = {
+    {0x0, "No download microcode operation in progress"},
+    {0x1, "Download in progress, awaiting more"},
+    {0x2, "Download complete, updating non-volatile storage"},
+    {0x3, "Updating non-volatile storage with deferred microcode"},
+    {0x10, "Complete, no error, starting now"},
+    {0x11, "Complete, no error, start after hard reset or power cycle"},
+    {0x12, "Complete, no error, start after power cycle"},
+    {0x13, "Complete, no error, start after activate_mc, hard reset or "
+           "power cycle"},
+    {0x80, "Error, discarded, see additional status"},
+    {0x81, "Error, discarded, image error"},
+    {0x82, "Timeout, discarded"},
+    {0x83, "Internal error, need new microcode before reset"},
+    {0x84, "Internal error, need new microcode, reset safe"},
+    {0x85, "Unexpected activate_mc received"},
+    {0x1000, NULL},
+};
+
+static const char *
+get_mc_status(uint8_t status_val)
+{
+    const struct diag_page_code * mcsp;
+
+    for (mcsp = mc_status_arr; mcsp->desc; ++mcsp) {
+        if (status_val == mcsp->page_code)
+            return mcsp->desc;
+    }
+    return "";
+}
+
+/* DOWNLOAD_MICROCODE_DPC [0xe] */
+static void
+download_code_sdg(const uint8_t * resp, int resp_len)
+{
+    int k, num_subs;
+    uint32_t gen_code;
+    const uint8_t * bp;
+    const uint8_t * last_bp;
+    const char * cp;
+
+    printf("Download microcode status diagnostic page:\n");
+    if (resp_len < 4)
+        goto truncated;
+    num_subs = resp[1] + 1;  /* number of subenclosures (add 1 for primary) */
+    last_bp = resp + resp_len - 1;
+    printf("  number of secondary subenclosures: %d\n", num_subs - 1);
+    gen_code = sg_get_unaligned_be32(resp + 4);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    bp = resp + 8;
+    for (k = 0; k < num_subs; ++k, bp += 16) {
+        if ((bp + 3) > last_bp)
+            goto truncated;
+        cp = (0 == bp[1]) ? " [primary]" : "";
+        printf("   subenclosure identifier: %d%s\n", bp[1], cp);
+        cp = get_mc_status(bp[2]);
+        if (strlen(cp) > 0) {
+            printf("     download microcode status: %s [0x%x]\n", cp, bp[2]);
+            printf("     download microcode additional status: 0x%x\n",
+                   bp[3]);
+        } else
+            printf("     download microcode status: 0x%x [additional "
+                   "status: 0x%x]\n", bp[2], bp[3]);
+        printf("     download microcode maximum size: %d bytes\n",
+               sg_get_unaligned_be32(bp + 4));
+        printf("     download microcode expected buffer id: 0x%x\n", bp[11]);
+        printf("     download microcode expected buffer id offset: %d\n",
+               sg_get_unaligned_be32(bp + 12));
+    }
+    return;
+truncated:
+    pr2serr("    <<<download: response too short>>>\n");
+    return;
+}
+
+/* Reads hex data from command line, stdin or a file when in_hex is true.
+ * Reads binary from stdin or file when in_hex is false. Returns 0 on
+ * success, 1 otherwise. If inp is a file and may_have_at, then the
+ * first character is skipped to get filename (since it should be '@'). */
+static int
+read_hex(const char * inp, uint8_t * arr, int mx_arr_len, int * arr_len,
+         bool in_hex, bool may_have_at, int vb)
+{
+    bool has_stdin, split_line;
+    int in_len, k, j, m, off, off_fn;
+    unsigned int h;
+    const char * lcp;
+    char * cp;
+    char * c2p;
+    char line[512];
+    char carry_over[4];
+    FILE * fp = NULL;
+
+    if ((NULL == inp) || (NULL == arr) || (NULL == arr_len))
+        return 1;
+    off_fn = may_have_at ? 1 : 0;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len) {
+        *arr_len = 0;
+        return 0;
+    }
+    has_stdin = ((1 == in_len) && ('-' == inp[0]));
+
+    if (! in_hex) {     /* binary, assume its not on the command line, */
+        int fd;         /* that leaves stdin or a file (pipe) */
+        struct stat a_stat;
+
+        if (has_stdin)
+            fd = STDIN_FILENO;
+        else {
+            fd = open(inp + off_fn, O_RDONLY);
+            if (fd < 0) {
+                pr2serr("unable to open binary file %s: %s\n", inp + off_fn,
+                         safe_strerror(errno));
+                return 1;
+            }
+        }
+        k = read(fd, arr, mx_arr_len);
+        if (k <= 0) {
+            if (0 == k)
+                pr2serr("read 0 bytes from binary file %s\n", inp + off_fn);
+            else
+                pr2serr("read from binary file %s: %s\n", inp + off_fn,
+                        safe_strerror(errno));
+            if (! has_stdin)
+                close(fd);
+            return 1;
+        }
+        if ((0 == fstat(fd, &a_stat)) && S_ISFIFO(a_stat.st_mode)) {
+            /* pipe; keep reading till error or 0 read */
+            while (k < mx_arr_len) {
+                m = read(fd, arr + k, mx_arr_len - k);
+                if (0 == m)
+                   break;
+                if (m < 0) {
+                    pr2serr("read from binary pipe %s: %s\n", inp + off_fn,
+                            safe_strerror(errno));
+                    if (! has_stdin)
+                        close(fd);
+                    return 1;
+                }
+                k += m;
+            }
+        }
+        *arr_len = k;
+        if (! has_stdin)
+            close(fd);
+        return 0;
+    }
+    if (has_stdin || (! may_have_at) || ('@' == inp[0])) {
+        /* read hex from stdin or file */
+        if (has_stdin)
+            fp = stdin;
+        else {
+            fp = fopen(inp + off_fn, "r");
+            if (NULL == fp) {
+                pr2serr("%s: unable to open file: %s\n", __func__,
+                        inp + off_fn);
+                return 1;
+            }
+        }
+        carry_over[0] = 0;
+        for (j = 0, off = 0; j < MX_DATA_IN_LINES; ++j) {
+            if (NULL == fgets(line, sizeof(line), fp))
+                break;
+            in_len = strlen(line);
+            if (in_len > 0) {
+                if ('\n' == line[in_len - 1]) {
+                    --in_len;
+                    line[in_len] = '\0';
+                    split_line = false;
+                } else
+                    split_line = true;
+            }
+            if (in_len < 1) {
+                carry_over[0] = 0;
+                continue;
+            }
+            if (carry_over[0]) {
+                if (isxdigit((uint8_t)line[0])) {
+                    carry_over[1] = line[0];
+                    carry_over[2] = '\0';
+                    if (1 == sscanf(carry_over, "%x", &h))
+                        arr[off - 1] = h;       /* back up and overwrite */
+                    else {
+                        pr2serr("%s: carry_over error ['%s'] around line "
+                                "%d\n", __func__, carry_over, j + 1);
+                        goto err_with_fp;
+                    }
+                    lcp = line + 1;
+                    --in_len;
+                } else
+                    lcp = line;
+                carry_over[0] = 0;
+            } else
+                lcp = line;
+            m = strspn(lcp, " \t");
+            if (m == in_len)
+                continue;
+            lcp += m;
+            in_len -= m;
+            if ('#' == *lcp)
+                continue;
+            k = strspn(lcp, "0123456789aAbBcCdDeEfF ,\t");
+            if (in_len != k) {
+                pr2serr("%s: syntax error at line %d, pos %d\n", __func__,
+                        j + 1, m + k + 1);
+                if (vb > 2)
+                    pr2serr("first 40 characters of line: %.40s\n", line);
+                goto err_with_fp;
+            }
+            for (k = 0; k < (mx_arr_len - off); ++k) {
+                if (1 == sscanf(lcp, "%x", &h)) {
+                    if (h > 0xff) {
+                        pr2serr("%s: hex number larger than 0xff in line %d, "
+                                "pos %d\n", __func__, j + 1,
+                                (int)(lcp - line + 1));
+                        if (vb > 2)
+                            pr2serr("first 40 characters of line: %.40s\n",
+                                    line);
+                        goto err_with_fp;
+                    }
+                    if (split_line && (1 == strlen(lcp))) {
+                        /* single trailing hex digit might be a split pair */
+                        carry_over[0] = *lcp;
+                    }
+                    arr[off + k] = h;
+                    lcp = strpbrk(lcp, " ,\t");
+                    if (NULL == lcp)
+                        break;
+                    lcp += strspn(lcp, " ,\t");
+                    if ('\0' == *lcp)
+                        break;
+                } else {
+                    pr2serr("%s: error in line %d, at pos %d\n", __func__,
+                            j + 1, (int)(lcp - line + 1));
+                    if (vb > 2)
+                        pr2serr("first 40 characters of line: %.40s\n", line);
+                    goto err_with_fp;
+                }
+            }
+            off += k + 1;
+            if (off >= mx_arr_len)
+                break;
+        }
+        *arr_len = off;
+    } else {        /* hex string on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfF, ");
+        if (in_len != k) {
+            pr2serr("%s: error at pos %d\n", __func__, k + 1);
+            goto err_with_fp;
+        }
+        for (k = 0; k < mx_arr_len; ++k) {
+            if (1 == sscanf(lcp, "%x", &h)) {
+                if (h > 0xff) {
+                    pr2serr("%s: hex number larger than 0xff at pos %d\n",
+                            __func__, (int)(lcp - inp + 1));
+                    goto err_with_fp;
+                }
+                arr[k] = h;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("%s: error at pos %d\n", __func__,
+                        (int)(lcp - inp + 1));
+                goto err_with_fp;
+            }
+        }
+        *arr_len = k + 1;
+    }
+    if (vb > 3) {
+        pr2serr("%s: user provided data:\n", __func__);
+        hex2stderr(arr, *arr_len, 0);
+    }
+    if (fp && (fp != stdin))
+        fclose(fp);
+    return 0;
+
+err_with_fp:
+    if (fp && (fp != stdin))
+        fclose(fp);
+    return 1;
+}
+
+static int
+process_status_dpage(struct sg_pt_base * ptvp, int page_code, uint8_t * resp,
+                     int resp_len, struct opts_t * op)
+{
+    int j, num_ths;
+    int ret = 0;
+    uint32_t ref_gen_code;
+    const char * cp;
+    struct enclosure_info primary_info;
+    struct th_es_t tes;
+    struct th_es_t * tesp;
+    char bb[120];
+
+    tesp = &tes;
+    memset(tesp, 0, sizeof(tes));
+    if ((cp = find_in_diag_page_desc(page_code)))
+        snprintf(bb, sizeof(bb), "%s dpage", cp);
+    else
+        snprintf(bb, sizeof(bb), "dpage 0x%x", page_code);
+    cp = bb;
+    if (op->do_raw) {
+        if (1 == op->do_raw)
+            hex2stdout(resp + 4, resp_len - 4, -1);
+        else {
+            if (sg_set_binary_mode(STDOUT_FILENO) < 0)
+                perror("sg_set_binary_mode");
+            dStrRaw(resp, resp_len);
+        }
+        goto fini;
+    } else if (op->do_hex) {
+        if (op->do_hex > 2) {
+            if (op->do_hex > 3) {
+                if (4 == op->do_hex)
+                    printf("\n# %s:\n", cp);
+                else
+                    printf("\n# %s [0x%x]:\n", cp, page_code);
+            }
+            hex2stdout(resp, resp_len, -1);
+         } else {
+            printf("# Response in hex for %s:\n", cp);
+            hex2stdout(resp, resp_len, (2 == op->do_hex));
+        }
+        goto fini;
+    }
+
+    memset(&primary_info, 0, sizeof(primary_info));
+    switch (page_code) {
+    case SUPPORTED_DPC:
+        supported_pages_sdg("Supported diagnostic pages", resp, resp_len);
+        break;
+    case CONFIGURATION_DPC:
+        configuration_sdg(resp, resp_len);
+        break;
+    case ENC_STATUS_DPC:
+        num_ths = build_type_desc_hdr_arr(ptvp, type_desc_hdr_arr,
+                                          MX_ELEM_HDR, &ref_gen_code,
+                                          &primary_info, op);
+        if (num_ths < 0) {
+            ret = num_ths;
+            goto fini;
+        }
+        if ((1 == type_desc_hdr_count) && primary_info.have_info) {
+            printf("  Primary enclosure logical identifier (hex): ");
+            for (j = 0; j < 8; ++j)
+                printf("%02x", primary_info.enc_log_id[j]);
+            printf("\n");
+        }
+        tesp->th_base = type_desc_hdr_arr;
+        tesp->num_ths = num_ths;
+        enc_status_dp(tesp, ref_gen_code, resp, resp_len, op);
+        break;
+    case ARRAY_STATUS_DPC:
+        num_ths = build_type_desc_hdr_arr(ptvp, type_desc_hdr_arr,
+                                          MX_ELEM_HDR, &ref_gen_code,
+                                          &primary_info, op);
+        if (num_ths < 0) {
+            ret = num_ths;
+            goto fini;
+        }
+        if ((1 == type_desc_hdr_count) && primary_info.have_info) {
+            printf("  Primary enclosure logical identifier (hex): ");
+            for (j = 0; j < 8; ++j)
+                printf("%02x", primary_info.enc_log_id[j]);
+            printf("\n");
+        }
+        tesp->th_base = type_desc_hdr_arr;
+        tesp->num_ths = num_ths;
+        array_status_dp(tesp, ref_gen_code, resp, resp_len, op);
+        break;
+    case HELP_TEXT_DPC:
+        printf("Help text diagnostic page (for primary "
+               "subenclosure):\n");
+        if (resp_len > 4)
+            printf("  %.*s\n", resp_len - 4, resp + 4);
+        else
+            printf("  <empty>\n");
+        break;
+    case STRING_DPC:
+        printf("String In diagnostic page (for primary "
+               "subenclosure):\n");
+        if (resp_len > 4)
+            hex2stdout(resp + 4, resp_len - 4, 0);
+        else
+            printf("  <empty>\n");
+        break;
+    case THRESHOLD_DPC:
+        num_ths = build_type_desc_hdr_arr(ptvp, type_desc_hdr_arr,
+                                          MX_ELEM_HDR, &ref_gen_code,
+                                          &primary_info, op);
+        if (num_ths < 0) {
+            ret = num_ths;
+            goto fini;
+        }
+        if ((1 == type_desc_hdr_count) && primary_info.have_info) {
+            printf("  Primary enclosure logical identifier (hex): ");
+            for (j = 0; j < 8; ++j)
+                printf("%02x", primary_info.enc_log_id[j]);
+            printf("\n");
+        }
+        tesp->th_base = type_desc_hdr_arr;
+        tesp->num_ths = num_ths;
+        threshold_sdg(tesp, ref_gen_code, resp, resp_len, op);
+        break;
+    case ELEM_DESC_DPC:
+        num_ths = build_type_desc_hdr_arr(ptvp, type_desc_hdr_arr,
+                                              MX_ELEM_HDR, &ref_gen_code,
+                                              &primary_info, op);
+            if (num_ths < 0) {
+            ret = num_ths;
+            goto fini;
+        }
+        if ((1 == type_desc_hdr_count) && primary_info.have_info) {
+            printf("  Primary enclosure logical identifier (hex): ");
+            for (j = 0; j < 8; ++j)
+                printf("%02x", primary_info.enc_log_id[j]);
+            printf("\n");
+        }
+        tesp->th_base = type_desc_hdr_arr;
+        tesp->num_ths = num_ths;
+        element_desc_sdg(tesp, ref_gen_code, resp, resp_len, op);
+        break;
+    case SHORT_ENC_STATUS_DPC:
+        printf("Short enclosure status diagnostic page, "
+               "status=0x%x\n", resp[1]);
+        break;
+    case ENC_BUSY_DPC:
+        printf("Enclosure Busy diagnostic page, "
+               "busy=%d [vendor specific=0x%x]\n",
+               resp[1] & 1, (resp[1] >> 1) & 0xff);
+        break;
+    case ADD_ELEM_STATUS_DPC:
+        num_ths = build_type_desc_hdr_arr(ptvp, type_desc_hdr_arr,
+                                          MX_ELEM_HDR, &ref_gen_code,
+                                          &primary_info, op);
+        if (num_ths < 0) {
+            ret = num_ths;
+            goto fini;
+        }
+        if (primary_info.have_info) {
+            printf("  Primary enclosure logical identifier (hex): ");
+            for (j = 0; j < 8; ++j)
+                printf("%02x", primary_info.enc_log_id[j]);
+            printf("\n");
+        }
+        tesp->th_base = type_desc_hdr_arr;
+        tesp->num_ths = num_ths;
+        additional_elem_sdg(tesp, ref_gen_code, resp, resp_len, op);
+        break;
+    case SUBENC_HELP_TEXT_DPC:
+        subenc_help_sdg(resp, resp_len);
+        break;
+    case SUBENC_STRING_DPC:
+        subenc_string_sdg(resp, resp_len);
+        break;
+    case SUPPORTED_SES_DPC:
+        supported_pages_sdg("Supported SES diagnostic pages", resp,
+                            resp_len);
+        break;
+    case DOWNLOAD_MICROCODE_DPC:
+        download_code_sdg(resp, resp_len);
+        break;
+    case SUBENC_NICKNAME_DPC:
+        subenc_nickname_sdg(resp, resp_len);
+        break;
+    default:
+        printf("Cannot decode response from diagnostic page: %s\n", cp);
+        hex2stdout(resp, resp_len, 0);
+    }
+
+fini:
+    return ret;
+}
+
+/* Display "status" page or pages (if op->page_code==0xff) . data-in from
+ * SES device or user provided (with --data= option). Return 0 for success */
+static int
+process_status_page_s(struct sg_pt_base * ptvp, struct opts_t * op)
+{
+    int page_code, ret, resp_len;
+    uint8_t * resp = NULL;
+    uint8_t * free_resp = NULL;
+
+    resp = sg_memalign(op->maxlen, 0, &free_resp, false);
+    if (NULL == resp) {
+        pr2serr("%s: unable to allocate %d bytes on heap\n", __func__,
+                op->maxlen);
+        ret = -1;
+        goto fini;
+    }
+    page_code = op->page_code;
+    if (ALL_DPC == page_code) {
+        int k, n;
+        uint8_t pc, prev;
+        uint8_t supp_dpg_arr[256];
+        const int s_arr_sz = sizeof(supp_dpg_arr);
+
+        memset(supp_dpg_arr, 0, s_arr_sz);
+        ret = do_rec_diag(ptvp, SUPPORTED_DPC, resp, op->maxlen, op,
+                          &resp_len);
+        if (ret)        /* SUPPORTED_DPC failed so try SUPPORTED_SES_DPC */
+            ret = do_rec_diag(ptvp, SUPPORTED_SES_DPC, resp, op->maxlen, op,
+                              &resp_len);
+        if (ret)
+            goto fini;
+        for (n = 0, pc = 0; (n < s_arr_sz) && (n < (resp_len - 4)); ++n) {
+            prev = pc;
+            pc = resp[4 + n];
+            if (prev > pc) {
+                if (pc) {       /* could be zero pad at end which is ok */
+                    pr2serr("%s: Supported (SES) dpage seems corrupt, "
+                            "should ascend\n", __func__);
+                    ret = SG_LIB_CAT_OTHER;
+                    goto fini;
+                }
+                break;
+            }
+            if (pc > 0x2f)
+                break;
+            supp_dpg_arr[n] = pc;
+        }
+        for (k = 0; k < n; ++k) {
+            page_code = supp_dpg_arr[k];
+            ret = do_rec_diag(ptvp, page_code, resp, op->maxlen, op,
+                              &resp_len);
+            if (ret)
+                goto fini;
+            ret = process_status_dpage(ptvp, page_code, resp, resp_len, op);
+        }
+    } else {    /* asking for a specific page code */
+        ret = do_rec_diag(ptvp, page_code, resp, op->maxlen, op, &resp_len);
+        if (ret)
+            goto fini;
+        ret = process_status_dpage(ptvp, page_code, resp, resp_len, op);
+    }
+
+fini:
+    if (free_resp)
+        free(free_resp);
+    return ret;
+}
+
+static void
+devslotnum_and_sasaddr(struct join_row_t * jrp, const uint8_t * ae_bp)
+{
+    if ((NULL == jrp) || (NULL == ae_bp) || (0 == (0x10 & ae_bp[0])))
+        return; /* sanity and expect EIP=1 */
+    switch (0xf & ae_bp[0]) {
+    case TPROTO_FCP:
+        jrp->dev_slot_num = ae_bp[7];
+        break;
+    case TPROTO_SAS:
+        if (0 == (0xc0 & ae_bp[5])) {
+            /* only for device slot and array device slot elements */
+            jrp->dev_slot_num = ae_bp[7];
+            if (ae_bp[4] > 0) {        /* number of phys */
+                int m;
+
+                /* Use the first phy's "SAS ADDRESS" field */
+                for (m = 0; m < 8; ++m)
+                    jrp->sas_addr[m] = ae_bp[(4 + 4 + 12) + m];
+            }
+        }
+        break;
+    case TPROTO_PCIE:
+        jrp->dev_slot_num = ae_bp[7];
+        break;
+    default:
+        ;
+    }
+}
+
+static const char *
+offset_str(long offset, bool in_hex, char * b, int blen)
+{
+    if (in_hex && (offset >= 0))
+        snprintf(b, blen, "0x%lx", offset);
+    else
+        snprintf(b, blen, "%ld", offset);
+    return b;
+}
+
+/* Returns broken_ei which is only true when EIP=1 and EIIOE=0 is overridden
+ * as outlined in join array description near the top of this file. */
+static bool
+join_aes_helper(const uint8_t * ae_bp, const uint8_t * ae_last_bp,
+                const struct th_es_t * tesp, const struct opts_t * op)
+{
+    int k, j, ei, eiioe, aes_i, hex, blen;
+    bool eip, broken_ei;
+    struct join_row_t * jrp;
+    struct join_row_t * jr2p;
+    const struct type_desc_hdr_t * tdhp = tesp->th_base;
+    char b[20];
+
+    jrp = tesp->j_base;
+    blen = sizeof(b);
+    hex = op->do_hex;
+    broken_ei = false;
+    /* loop over all type descriptor headers in the Configuration dpge */
+    for (k = 0, aes_i = 0; k < tesp->num_ths; ++k, ++tdhp) {
+        if (is_et_used_by_aes(tdhp->etype)) {
+            /* only consider element types that AES element are permiited
+             * to refer to, then loop over those number of elements */
+            for (j = 0; j < tdhp->num_elements;
+                 ++j, ++aes_i, ae_bp += ae_bp[1] + 2) {
+                if ((ae_bp + 1) > ae_last_bp) {
+                    if (op->verbose || op->warn)
+                        pr2serr("warning: %s: off end of ae page\n",
+                                __func__);
+                    return broken_ei;
+                }
+                eip = !!(ae_bp[0] & 0x10); /* EIP == Element Index Present */
+                if (eip) {
+                    eiioe = 0x3 & ae_bp[2];
+                    if ((0 == eiioe) && op->eiioe_force)
+                        eiioe = 1;
+                } else
+                    eiioe = 0;
+                if (eip && (1 == eiioe)) {         /* EIP and EIIOE=1 */
+                    ei = ae_bp[3];
+                    jr2p = tesp->j_base + ei;
+                    if ((ei >= tesp->num_j_eoe) ||
+                        (NULL == jr2p->enc_statp)) {
+                        pr2serr("%s: oi=%d, ei=%d [num_eoe=%d], eiioe=1 "
+                                "not in join_arr\n", __func__, k, ei,
+                                tesp->num_j_eoe);
+                        return broken_ei;
+                    }
+                    devslotnum_and_sasaddr(jr2p, ae_bp);
+                    if (jr2p->ae_statp) {
+                        if (op->warn || op->verbose) {
+                            pr2serr("warning: aes slot already in use, "
+                                    "keep existing AES+%s\n\t",
+                                    offset_str(jr2p->ae_statp - add_elem_rsp,
+                                               hex, b, blen));
+                            pr2serr("dropping AES+%s [length=%d, oi=%d, "
+                                    "ei=%d, aes_i=%d]\n",
+                                    offset_str(ae_bp - add_elem_rsp, hex, b,
+                                               blen),
+                                    ae_bp[1] + 2, k, ei, aes_i);
+                        }
+                    } else
+                        jr2p->ae_statp = ae_bp;
+                } else if (eip && (0 == eiioe)) {     /* SES-2 so be careful */
+                    ei = ae_bp[3];
+try_again:
+                    /* Check AES dpage descriptor ei is valid */
+                    for (jr2p = tesp->j_base; jr2p->enc_statp; ++jr2p) {
+                        if (broken_ei) {
+                            if (ei == jr2p->ei_aess)
+                                break;
+                        } else {
+                            if (ei == jr2p->ei_eoe)
+                                break;
+                        }
+                    }
+                    if (NULL == jr2p->enc_statp) {
+                        pr2serr("warning: %s: oi=%d, ei=%d (broken_ei=%d) "
+                                "not in join_arr\n", __func__, k, ei,
+                                (int)broken_ei);
+                        return broken_ei;
+                    }
+                    if (! is_et_used_by_aes(jr2p->etype)) {
+                        /* unexpected element type so  ... */
+                        broken_ei = true;
+                        goto try_again;
+                    }
+                    devslotnum_and_sasaddr(jr2p, ae_bp);
+                    if (jr2p->ae_statp) {
+                        /* 1 to 1 AES to ES mapping assumption violated */
+                        if ((0 == ei) && (TPROTO_SAS == (0xf & ae_bp[0])) &&
+                            (1 == (ae_bp[5] >> 6))) {
+                            /* heuristic for (hack) Areca 8028 */
+                            for (jr2p = tesp->j_base; jr2p->enc_statp;
+                                 ++jr2p) {
+                                if ((-1 == jr2p->indiv_i) ||
+                                    (! is_et_used_by_aes(jr2p->etype)) ||
+                                    jr2p->ae_statp)
+                                    continue;
+                                jr2p->ae_statp = ae_bp;
+                                break;
+                            }
+                            if ((NULL == jr2p->enc_statp) &&
+                                (op->warn || op->verbose))
+                                pr2serr("warning2: dropping AES+%s [length="
+                                        "%d, oi=%d, ei=%d, aes_i=%d]\n",
+                                        offset_str(ae_bp - add_elem_rsp, hex,
+                                                   b, blen),
+                                        ae_bp[1] + 2, k, ei, aes_i);
+                        } else if (op->warn || op->verbose) {
+                            pr2serr("warning3: aes slot already in use, "
+                                    "keep existing AES+%s\n\t",
+                                    offset_str(jr2p->ae_statp - add_elem_rsp,
+                                               hex, b, blen));
+                            pr2serr("dropping AES+%s [length=%d, oi=%d, ei="
+                                    "%d, aes_i=%d]\n",
+                                    offset_str(ae_bp - add_elem_rsp, hex, b,
+                                               blen),
+                                    ae_bp[1] + 2, k, ei, aes_i);
+                        }
+                    } else
+                        jr2p->ae_statp = ae_bp;
+                } else if (eip) {              /* EIP and EIIOE=2,3 */
+                    ei = ae_bp[3];
+                    for (jr2p = tesp->j_base; jr2p->enc_statp; ++jr2p) {
+                        if (ei == jr2p->ei_eoe)
+                            break;  /* good, found match on ei_eoe */
+                    }
+                    if (NULL == jr2p->enc_statp) {
+                        pr2serr("warning: %s: oi=%d, ei=%d, not in "
+                                "join_arr\n", __func__, k, ei);
+                        return broken_ei;
+                    }
+                    if (! is_et_used_by_aes(jr2p->etype)) {
+                        pr2serr("warning: %s: oi=%d, ei=%d, unexpected "
+                                "element_type=0x%x\n", __func__, k, ei,
+                                jr2p->etype);
+                        return broken_ei;
+                    }
+                    devslotnum_and_sasaddr(jr2p, ae_bp);
+                    if (jr2p->ae_statp) {
+                        if (op->warn || op->verbose) {
+                            pr2serr("warning3: aes slot already in use, "
+                                    "keep existing AES+%s\n\t",
+                                    offset_str(jr2p->ae_statp - add_elem_rsp,
+                                               hex, b, blen));
+                            pr2serr("dropping AES+%s [length=%d, oi=%d, ei="
+                                    "%d, aes_i=%d]\n",
+                                    offset_str(ae_bp - add_elem_rsp, hex, b,
+                                               blen),
+                                    ae_bp[1] + 2, k, ei, aes_i);
+                        }
+                    } else
+                        jr2p->ae_statp = ae_bp;
+                } else {    /* EIP=0 */
+                    /* step jrp over overall elements or those with
+                     * jrp->ae_statp already used */
+                    while (jrp->enc_statp && ((-1 == jrp->indiv_i) ||
+                                              jrp->ae_statp))
+                        ++jrp;
+                    if (NULL == jrp->enc_statp) {
+                        pr2serr("warning: %s: join_arr has no space for "
+                                "ae\n", __func__);
+                        return broken_ei;
+                    }
+                    jrp->ae_statp = ae_bp;
+                    ++jrp;
+                }
+            }       /* end_for: loop over non-overall elements of the
+                     * current type descriptor header */
+        } else {    /* element type _not_ relevant to ae status */
+            /* step jrp over overall and individual elements */
+            for (j = 0; j <= tdhp->num_elements; ++j, ++jrp) {
+                if (NULL == jrp->enc_statp) {
+                    pr2serr("warning: %s: join_arr has no space\n",
+                            __func__);
+                    return broken_ei;
+                }
+            }
+        }
+    }       /* end_for: loop over type descriptor headers */
+    return broken_ei;
+}
+
+
+/* User output of join array */
+static void
+join_array_display(struct th_es_t * tesp, struct opts_t * op)
+{
+    bool got1, need_aes;
+    int k, j, blen, desc_len, dn_len;
+    const uint8_t * ae_bp;
+    const char * cp;
+    const uint8_t * ed_bp;
+    struct join_row_t * jrp;
+    uint8_t * t_bp;
+    char b[64];
+
+    blen = sizeof(b);
+    need_aes = (op->page_code_given &&
+                (ADD_ELEM_STATUS_DPC == op->page_code));
+    dn_len = op->desc_name ? (int)strlen(op->desc_name) : 0;
+    for (k = 0, jrp = tesp->j_base, got1 = false;
+         ((k < MX_JOIN_ROWS) && jrp->enc_statp); ++k, ++jrp) {
+        if (op->ind_given) {
+            if (op->ind_th != jrp->th_i)
+                continue;
+            if (! match_ind_indiv(jrp->indiv_i, op))
+                continue;
+        }
+        if (need_aes && (NULL == jrp->ae_statp))
+            continue;
+        ed_bp = jrp->elem_descp;
+        if (op->desc_name) {
+            if (NULL == ed_bp)
+                continue;
+            desc_len = sg_get_unaligned_be16(ed_bp + 2);
+            /* some element descriptor strings have trailing NULLs and
+             * count them in their length; adjust */
+            while (desc_len && ('\0' == ed_bp[4 + desc_len - 1]))
+                --desc_len;
+            if (desc_len != dn_len)
+                continue;
+            if (0 != strncmp(op->desc_name, (const char *)(ed_bp + 4),
+                             desc_len))
+                continue;
+        } else if (op->dev_slot_num >= 0) {
+            if (op->dev_slot_num != jrp->dev_slot_num)
+                continue;
+        } else if (saddr_non_zero(op->sas_addr)) {
+            for (j = 0; j < 8; ++j) {
+                if (op->sas_addr[j] != jrp->sas_addr[j])
+                    break;
+            }
+            if (j < 8)
+                continue;
+        }
+        got1 = true;
+        if ((op->do_filter > 1) && (1 != (0xf & jrp->enc_statp[0])))
+            continue;   /* when '-ff' and status!=OK, skip */
+        cp = etype_str(jrp->etype, b, blen);
+        if (ed_bp) {
+            desc_len = sg_get_unaligned_be16(ed_bp + 2) + 4;
+            if (desc_len > 4)
+                printf("%.*s [%d,%d]  Element type: %s\n", desc_len - 4,
+                       (const char *)(ed_bp + 4), jrp->th_i,
+                       jrp->indiv_i, cp);
+            else
+                printf("[%d,%d]  Element type: %s\n", jrp->th_i,
+                       jrp->indiv_i, cp);
+        } else
+            printf("[%d,%d]  Element type: %s\n", jrp->th_i,
+                   jrp->indiv_i, cp);
+        printf("  Enclosure Status:\n");
+        enc_status_helper("    ", jrp->enc_statp, jrp->etype, false, op);
+        if (jrp->ae_statp) {
+            printf("  Additional Element Status:\n");
+            ae_bp = jrp->ae_statp;
+            desc_len = ae_bp[1] + 2;
+            additional_elem_helper("    ",  ae_bp, desc_len, jrp->etype,
+                                   tesp, op);
+        }
+        if (jrp->thresh_inp) {
+            t_bp = jrp->thresh_inp;
+            threshold_helper("  Threshold In:\n", "    ", t_bp, jrp->etype,
+                             op);
+        }
+    }
+    if (! got1) {
+        if (op->ind_given) {
+            printf("      >>> no match on --index=%d,%d", op->ind_th,
+                   op->ind_indiv);
+            if (op->ind_indiv_last > op->ind_indiv)
+                printf("-%d\n", op->ind_indiv_last);
+            else
+                printf("\n");
+        } else if (op->desc_name)
+            printf("      >>> no match on --descriptor=%s\n", op->desc_name);
+        else if (op->dev_slot_num >= 0)
+            printf("      >>> no match on --dev-slot-name=%d\n",
+                   op->dev_slot_num);
+        else if (saddr_non_zero(op->sas_addr)) {
+            printf("      >>> no match on --sas-addr=0x");
+            for (j = 0; j < 8; ++j)
+                printf("%02x", op->sas_addr[j]);
+            printf("\n");
+        }
+    }
+}
+
+/* This is for debugging, output to stderr */
+static void
+join_array_dump(struct th_es_t * tesp, int broken_ei, struct opts_t * op)
+{
+    int k, j, blen, hex;
+    int eiioe_count = 0;
+    int eip_count = 0;
+    struct join_row_t * jrp;
+    char b[64];
+
+    blen = sizeof(b);
+    hex = op->do_hex;
+    pr2serr("Dump of join array, each line is a row. Lines start with\n");
+    pr2serr("[<element_type>: <type_hdr_index>,<elem_ind_within>]\n");
+    pr2serr("'-1' indicates overall element or not applicable.\n");
+    jrp = tesp->j_base;
+    for (k = 0; ((k < MX_JOIN_ROWS) && jrp->enc_statp); ++k, ++jrp) {
+        pr2serr("[0x%x: %d,%d] ", jrp->etype, jrp->th_i, jrp->indiv_i);
+        if (jrp->se_id > 0)
+            pr2serr("se_id=%d ", jrp->se_id);
+        pr2serr("ei_ioe,_eoe,_aess=%s", offset_str(k, hex, b, blen));
+        pr2serr(",%s", offset_str(jrp->ei_eoe, hex, b, blen));
+        pr2serr(",%s", offset_str(jrp->ei_aess, hex, b, blen));
+        pr2serr(" dsn=%s", offset_str(jrp->dev_slot_num, hex, b, blen));
+        if (op->do_join > 2) {
+            pr2serr(" sa=0x");
+            if (saddr_non_zero(jrp->sas_addr)) {
+                for (j = 0; j < 8; ++j)
+                    pr2serr("%02x", jrp->sas_addr[j]);
+            } else
+                pr2serr("0");
+        }
+        if (jrp->enc_statp)
+            pr2serr(" ES+%s", offset_str(jrp->enc_statp - enc_stat_rsp,
+                                         hex, b, blen));
+        if (jrp->elem_descp)
+            pr2serr(" ED+%s", offset_str(jrp->elem_descp - elem_desc_rsp,
+                                         hex, b, blen));
+        if (jrp->ae_statp) {
+            pr2serr(" AES+%s", offset_str(jrp->ae_statp - add_elem_rsp,
+                                          hex, b, blen));
+            if (jrp->ae_statp[0] & 0x10) {
+                ++eip_count;
+                if (jrp->ae_statp[2] & 0x3)
+                    ++eiioe_count;
+            }
+        }
+        if (jrp->thresh_inp)
+            pr2serr(" TI+%s", offset_str(jrp->thresh_inp - threshold_rsp,
+                                         hex, b, blen));
+        pr2serr("\n");
+    }
+    pr2serr(">> ES len=%s, ", offset_str(enc_stat_rsp_len, hex, b, blen));
+    pr2serr("ED len=%s, ", offset_str(elem_desc_rsp_len, hex, b, blen));
+    pr2serr("AES len=%s, ", offset_str(add_elem_rsp_len, hex, b, blen));
+    pr2serr("TI len=%s\n", offset_str(threshold_rsp_len, hex, b, blen));
+    pr2serr(">> join_arr elements=%s, ", offset_str(k, hex, b, blen));
+    pr2serr("eip_count=%s, ", offset_str(eip_count, hex, b, blen));
+    pr2serr("eiioe_count=%s ", offset_str(eiioe_count, hex, b, blen));
+    pr2serr("broken_ei=%d\n", (int)broken_ei);
+}
+
+/* EIIOE juggling (standards + heuristics) for join with AES page */
+static void
+join_juggle_aes(struct th_es_t * tesp, uint8_t * es_bp, const uint8_t * ed_bp,
+                uint8_t * t_bp)
+{
+    int k, j, eoe, ei4aess;
+    struct join_row_t * jrp;
+    const struct type_desc_hdr_t * tdhp;
+
+    jrp = tesp->j_base;
+    tdhp = tesp->th_base;
+    for (k = 0, eoe = 0, ei4aess = 0; k < tesp->num_ths; ++k, ++tdhp) {
+        bool et_used_by_aes;
+
+        jrp->th_i = k;
+        jrp->indiv_i = -1;
+        jrp->etype = tdhp->etype;
+        jrp->ei_eoe = -1;
+        et_used_by_aes = is_et_used_by_aes(tdhp->etype);
+        jrp->ei_aess = -1;
+        jrp->se_id = tdhp->se_id;
+        /* check es_bp < es_last_bp still in range */
+        jrp->enc_statp = es_bp;
+        es_bp += 4;
+        jrp->elem_descp = ed_bp;
+        if (ed_bp)
+            ed_bp += sg_get_unaligned_be16(ed_bp + 2) + 4;
+        jrp->ae_statp = NULL;
+        jrp->thresh_inp = t_bp;
+        jrp->dev_slot_num = -1;
+        /* assume sas_addr[8] zeroed since it's static file scope */
+        if (t_bp)
+            t_bp += 4;
+        ++jrp;
+        for (j = 0; j < tdhp->num_elements; ++j, ++jrp) {
+            if (jrp >= join_arr_lastp)
+                break;
+            jrp->th_i = k;
+            jrp->indiv_i = j;
+            jrp->ei_eoe = eoe++;
+            if (et_used_by_aes)
+                jrp->ei_aess = ei4aess++;
+            else
+                jrp->ei_aess = -1;
+            jrp->etype = tdhp->etype;
+            jrp->se_id = tdhp->se_id;
+            jrp->enc_statp = es_bp;
+            es_bp += 4;
+            jrp->elem_descp = ed_bp;
+            if (ed_bp)
+                ed_bp += sg_get_unaligned_be16(ed_bp + 2) + 4;
+            jrp->thresh_inp = t_bp;
+            jrp->dev_slot_num = -1;
+            /* assume sas_addr[8] zeroed since it's static file scope */
+            if (t_bp)
+                t_bp += 4;
+            jrp->ae_statp = NULL;
+            ++tesp->num_j_eoe;
+        }
+        if (jrp >= join_arr_lastp) {
+            /* ++k; */
+            break;      /* leave last row all zeros */
+        }
+    }
+    tesp->num_j_rows = jrp - tesp->j_base;
+}
+
+/* Fetch Configuration, Enclosure Status, Element Descriptor, Additional
+ * Element Status and optionally Threshold In pages, place in static arrays.
+ * Collate (join) overall and individual elements into the static join_arr[].
+ * When 'display' is true then the join_arr[]  is output to stdout in a form
+ * suitable for end users. For debug purposes the join_arr[] is output to
+ * stderr when op->verbose > 3. Returns 0 for success, any other return value
+ * is an error. */
+static int
+join_work(struct sg_pt_base * ptvp, struct opts_t * op, bool display)
+{
+    bool broken_ei;
+    int res, num_ths, mlen;
+    uint32_t ref_gen_code, gen_code;
+    const uint8_t * ae_bp;
+    const uint8_t * ae_last_bp;
+    const char * enc_state_changed = "  <<state of enclosure changed, "
+                                     "please try again>>\n";
+    uint8_t * es_bp;
+    const uint8_t * ed_bp;
+    uint8_t * t_bp;
+    struct th_es_t * tesp;
+    struct enclosure_info primary_info;
+    struct th_es_t tes;
+
+    memset(&primary_info, 0, sizeof(primary_info));
+    num_ths = build_type_desc_hdr_arr(ptvp, type_desc_hdr_arr, MX_ELEM_HDR,
+                                      &ref_gen_code, &primary_info, op);
+    if (num_ths < 0)
+        return num_ths;
+    tesp = &tes;
+    memset(tesp, 0, sizeof(tes));
+    tesp->th_base = type_desc_hdr_arr;
+    tesp->num_ths = num_ths;
+    if (display && primary_info.have_info) {
+        int j;
+
+        printf("  Primary enclosure logical identifier (hex): ");
+        for (j = 0; j < 8; ++j)
+            printf("%02x", primary_info.enc_log_id[j]);
+        printf("\n");
+    }
+    mlen = enc_stat_rsp_sz;
+    if (mlen > op->maxlen)
+        mlen = op->maxlen;
+    res = do_rec_diag(ptvp, ENC_STATUS_DPC, enc_stat_rsp, mlen, op,
+                      &enc_stat_rsp_len);
+    if (res)
+        return res;
+    if (enc_stat_rsp_len < 8) {
+        pr2serr("Enclosure Status response too short\n");
+        return -1;
+    }
+    gen_code = sg_get_unaligned_be32(enc_stat_rsp + 4);
+    if (ref_gen_code != gen_code) {
+        pr2serr("%s", enc_state_changed);
+        return -1;
+    }
+    es_bp = enc_stat_rsp + 8;
+    /* es_last_bp = enc_stat_rsp + enc_stat_rsp_len - 1; */
+
+    mlen = elem_desc_rsp_sz;
+    if (mlen > op->maxlen)
+        mlen = op->maxlen;
+    res = do_rec_diag(ptvp, ELEM_DESC_DPC, elem_desc_rsp, mlen, op,
+                      &elem_desc_rsp_len);
+    if (0 == res) {
+        if (elem_desc_rsp_len < 8) {
+            pr2serr("Element Descriptor response too short\n");
+            return -1;
+        }
+        gen_code = sg_get_unaligned_be32(elem_desc_rsp + 4);
+        if (ref_gen_code != gen_code) {
+            pr2serr("%s", enc_state_changed);
+            return -1;
+        }
+        ed_bp = elem_desc_rsp + 8;
+        /* ed_last_bp = elem_desc_rsp + elem_desc_rsp_len - 1; */
+    } else {
+        elem_desc_rsp_len = 0;
+        ed_bp = NULL;
+        res = 0;
+        if (op->verbose)
+            pr2serr("  Element Descriptor page not available\n");
+    }
+
+    /* check if we want to add the AES page to the join */
+    if (display || (ADD_ELEM_STATUS_DPC == op->page_code) ||
+        (op->dev_slot_num >= 0) || saddr_non_zero(op->sas_addr)) {
+        mlen = add_elem_rsp_sz;
+        if (mlen > op->maxlen)
+            mlen = op->maxlen;
+        res = do_rec_diag(ptvp, ADD_ELEM_STATUS_DPC, add_elem_rsp, mlen, op,
+                          &add_elem_rsp_len);
+        if (0 == res) {
+            if (add_elem_rsp_len < 8) {
+                pr2serr("Additional Element Status response too short\n");
+                return -1;
+            }
+            gen_code = sg_get_unaligned_be32(add_elem_rsp + 4);
+            if (ref_gen_code != gen_code) {
+                pr2serr("%s", enc_state_changed);
+                return -1;
+            }
+            ae_bp = add_elem_rsp + 8;
+            ae_last_bp = add_elem_rsp + add_elem_rsp_len - 1;
+            if (op->eiioe_auto && (add_elem_rsp_len > 11)) {
+                /* heuristic: if first AES descriptor has EIP set and its
+                 * EI equal to 1, then act as if the EIIOE field is 1. */
+                if ((ae_bp[0] & 0x10) && (1 == ae_bp[3]))
+                    op->eiioe_force = true;
+            }
+        } else {        /* unable to read AES dpage */
+            add_elem_rsp_len = 0;
+            ae_bp = NULL;
+            ae_last_bp = NULL;
+            res = 0;
+            if (op->verbose)
+                pr2serr("  Additional Element Status page not available\n");
+        }
+    } else {
+        ae_bp = NULL;
+        ae_last_bp = NULL;
+    }
+
+    if ((op->do_join > 1) ||
+        ((! display) && (THRESHOLD_DPC == op->page_code))) {
+        mlen = threshold_rsp_sz;
+        if (mlen > op->maxlen)
+            mlen = op->maxlen;
+        res = do_rec_diag(ptvp, THRESHOLD_DPC, threshold_rsp, mlen, op,
+                          &threshold_rsp_len);
+        if (0 == res) {
+            if (threshold_rsp_len < 8) {
+                pr2serr("Threshold In response too short\n");
+                return -1;
+            }
+            gen_code = sg_get_unaligned_be32(threshold_rsp + 4);
+            if (ref_gen_code != gen_code) {
+                pr2serr("%s", enc_state_changed);
+                return -1;
+            }
+            t_bp = threshold_rsp + 8;
+            /* t_last_bp = threshold_rsp + threshold_rsp_len - 1; */
+        } else {
+            threshold_rsp_len = 0;
+            t_bp = NULL;
+            res = 0;
+            if (op->verbose)
+                pr2serr("  Threshold In page not available\n");
+        }
+    } else {
+        threshold_rsp_len = 0;
+        t_bp = NULL;
+    }
+
+
+    tesp->j_base = join_arr;
+    join_juggle_aes(tesp, es_bp, ed_bp, t_bp);
+
+    broken_ei = false;
+    if (ae_bp)
+        broken_ei = join_aes_helper(ae_bp, ae_last_bp, tesp, op);
+
+    if (op->verbose > 3)
+        join_array_dump(tesp, broken_ei, op);
+
+    join_done = true;
+    if (display)      /* probably wanted join_arr[] built only */
+        join_array_display(tesp, op);
+
+    return res;
+
+}
+
+/* Returns 1 if strings equal (same length, characters same or only differ
+ * by case), else returns 0. Assumes 7 bit ASCII (English alphabet). */
+static int
+strcase_eq(const char * s1p, const char * s2p)
+{
+    int c1;
+
+    do {
+        int c2;
+
+        c1 = *s1p++;
+        c2 = *s2p++;
+        if (c1 != c2) {
+            if (c2 >= 'a')
+                c2 = toupper(c2);
+            else if (c1 >= 'a')
+                c1 = toupper(c1);
+            else
+                return 0;
+            if (c1 != c2)
+                return 0;
+        }
+    } while (c1);
+    return 1;
+}
+
+static bool
+is_acronym_in_status_ctl(const struct tuple_acronym_val * tavp)
+{
+    const struct acronym2tuple * ap;
+
+    for (ap = ecs_a2t_arr; ap->acron; ++ ap) {
+        if (strcase_eq(tavp->acron, ap->acron))
+            break;
+    }
+    return ap->acron;
+}
+
+static bool
+is_acronym_in_threshold(const struct tuple_acronym_val * tavp)
+{
+    const struct acronym2tuple * ap;
+
+    for (ap = th_a2t_arr; ap->acron; ++ ap) {
+        if (strcase_eq(tavp->acron, ap->acron))
+            break;
+    }
+    return ap->acron;
+}
+
+static bool
+is_acronym_in_additional(const struct tuple_acronym_val * tavp)
+{
+    const struct acronym2tuple * ap;
+
+    for (ap = ae_sas_a2t_arr; ap->acron; ++ ap) {
+        if (strcase_eq(tavp->acron, ap->acron))
+            break;
+    }
+    return ap->acron;
+}
+
+/* ENC_STATUS_DPC  ENC_CONTROL_DPC
+ * Do clear/get/set (cgs) on Enclosure Control/Status page. Return 0 for ok
+ * -2 for acronym not found, else -1 . */
+static int
+cgs_enc_ctl_stat(struct sg_pt_base * ptvp, struct join_row_t * jrp,
+                 const struct tuple_acronym_val * tavp,
+                 const struct opts_t * op, bool last)
+{
+    int s_byte, s_bit, n_bits;
+    const struct acronym2tuple * ap;
+
+    if (NULL == tavp->acron) {
+        s_byte = tavp->start_byte;
+        s_bit = tavp->start_bit;
+        n_bits = tavp->num_bits;
+    }
+    if (tavp->acron) {
+        for (ap = ecs_a2t_arr; ap->acron; ++ ap) {
+            if (((jrp->etype == ap->etype) || (-1 == ap->etype)) &&
+                strcase_eq(tavp->acron, ap->acron))
+                break;
+        }
+        if (ap->acron) {
+            s_byte = ap->start_byte;
+            s_bit = ap->start_bit;
+            n_bits = ap->num_bits;
+        } else {
+            if (-1 != ap->etype) {
+                for (ap = ecs_a2t_arr; ap->acron; ++ap) {
+                    if (0 == strcase_eq(tavp->acron, ap->acron)) {
+                        pr2serr(">>> Found %s acronym but not for element "
+                                "type %d\n", tavp->acron, jrp->etype);
+                        break;
+                    }
+                }
+            }
+            return -2;
+        }
+    }
+    if (op->verbose > 1)
+        pr2serr("  s_byte=%d, s_bit=%d, n_bits=%d\n", s_byte, s_bit, n_bits);
+    if (GET_OPT == tavp->cgs_sel) {
+        uint64_t ui = sg_get_big_endian(jrp->enc_statp + s_byte, s_bit,
+                                        n_bits);
+
+        if (op->do_hex)
+            printf("0x%" PRIx64 "\n", ui);
+        else
+            printf("%" PRId64 "\n", (int64_t)ui);
+    } else {    /* --set or --clear */
+        int len;
+
+        if ((! op->mask_ign) && (jrp->etype < NUM_ETC)) {
+            int k;
+
+            if (op->verbose > 2)
+                pr2serr("Applying mask to element status [etc=%d] prior to "
+                        "modify then write\n", jrp->etype);
+            for (k = 0; k < 4; ++k)
+                jrp->enc_statp[k] &= ses3_element_cmask_arr[jrp->etype][k];
+        } else
+            jrp->enc_statp[0] &= 0x40;  /* keep PRDFAIL is set in byte 0 */
+        /* next we modify requested bit(s) */
+        sg_set_big_endian((uint64_t)tavp->val,
+                          jrp->enc_statp + s_byte, s_bit, n_bits);
+        jrp->enc_statp[0] |= 0x80;  /* set SELECT bit */
+        if (op->byte1_given)
+            enc_stat_rsp[1] = op->byte1;
+        len = sg_get_unaligned_be16(enc_stat_rsp + 2) + 4;
+        if (last) {
+            int ret = do_senddiag(ptvp, enc_stat_rsp, len, ! op->quiet,
+                                  op->verbose);
+
+            if (ret) {
+                pr2serr("couldn't send Enclosure Control page\n");
+                return -1;
+            }
+        }
+    }
+    return 0;
+}
+
+/* THRESHOLD_DPC
+ * Do clear/get/set (cgs) on Threshold In/Out page. Return 0 for ok,
+ * -2 for acronym not found, else -1 . */
+static int
+cgs_threshold(struct sg_pt_base * ptvp, const struct join_row_t * jrp,
+              const struct tuple_acronym_val * tavp,
+              const struct opts_t * op, bool last)
+{
+    int s_byte, s_bit, n_bits;
+    const struct acronym2tuple * ap;
+
+    if (NULL == jrp->thresh_inp) {
+        pr2serr("No Threshold In/Out element available\n");
+        return -1;
+    }
+    if (NULL == tavp->acron) {
+        s_byte = tavp->start_byte;
+        s_bit = tavp->start_bit;
+        n_bits = tavp->num_bits;
+    }
+    if (tavp->acron) {
+        for (ap = th_a2t_arr; ap->acron; ++ap) {
+            if (((jrp->etype == ap->etype) || (-1 == ap->etype)) &&
+                strcase_eq(tavp->acron, ap->acron))
+                break;
+        }
+        if (ap->acron) {
+            s_byte = ap->start_byte;
+            s_bit = ap->start_bit;
+            n_bits = ap->num_bits;
+        } else
+            return -2;
+    }
+    if (GET_OPT == tavp->cgs_sel) {
+        uint64_t ui = sg_get_big_endian(jrp->thresh_inp + s_byte, s_bit,
+                                         n_bits);
+
+        if (op->do_hex)
+            printf("0x%" PRIx64 "\n", ui);
+        else
+            printf("%" PRId64 "\n", (int64_t)ui);
+    } else {
+        int len;
+
+        sg_set_big_endian((uint64_t)tavp->val,
+                          jrp->thresh_inp + s_byte, s_bit, n_bits);
+        if (op->byte1_given)
+            threshold_rsp[1] = op->byte1;
+        len = sg_get_unaligned_be16(threshold_rsp + 2) + 4;
+        if (last) {
+            int ret = do_senddiag(ptvp, threshold_rsp, len, ! op->quiet,
+                                  op->verbose);
+
+            if (ret) {
+                pr2serr("couldn't send Threshold Out page\n");
+                return -1;
+            }
+        }
+    }
+    return 0;
+}
+
+/* ADD_ELEM_STATUS_DPC
+ * Do get (cgs) on Additional element status page. Return 0 for ok,
+ * -2 for acronym not found, else -1 . */
+static int
+cgs_additional_el(const struct join_row_t * jrp,
+                  const struct tuple_acronym_val * tavp,
+                  const struct opts_t * op)
+{
+    int s_byte, s_bit, n_bits;
+    const struct acronym2tuple * ap;
+
+    if (NULL == jrp->ae_statp) {
+        pr2serr("No additional element status element available\n");
+        return -1;
+    }
+    if (NULL == tavp->acron) {
+        s_byte = tavp->start_byte;
+        s_bit = tavp->start_bit;
+        n_bits = tavp->num_bits;
+    }
+    if (tavp->acron) {
+        for (ap = ae_sas_a2t_arr; ap->acron; ++ap) {
+            if (((jrp->etype == ap->etype) || (-1 == ap->etype)) &&
+                strcase_eq(tavp->acron, ap->acron))
+                break;
+        }
+        if (ap->acron) {
+            s_byte = ap->start_byte;
+            s_bit = ap->start_bit;
+            n_bits = ap->num_bits;
+        } else
+            return -2;
+    }
+    if (GET_OPT == tavp->cgs_sel) {
+        uint64_t ui = sg_get_big_endian(jrp->ae_statp + s_byte, s_bit,
+                                         n_bits);
+
+        if (op->do_hex)
+            printf("0x%" PRIx64 "\n", ui);
+        else
+            printf("%" PRId64 "\n", (int64_t)ui);
+    } else {
+        pr2serr("--clear and --set not available for Additional Element "
+                "Status page\n");
+        return -1;
+    }
+    return 0;
+}
+
+/* Do --clear, --get or --set .
+ * Returns 0 for success, any other return value is an error. */
+static int
+ses_cgs(struct sg_pt_base * ptvp, const struct tuple_acronym_val * tavp,
+        struct opts_t * op, bool last)
+{
+    int ret, k, j, desc_len, dn_len;
+    bool found;
+    struct join_row_t * jrp;
+    const uint8_t * ed_bp;
+    char b[64];
+
+    if ((NULL == ptvp) && (GET_OPT != tavp->cgs_sel)) {
+        pr2serr("%s: --clear= and --set= only supported when DEVICE is "
+                "given\n", __func__);
+        return SG_LIB_CONTRADICT;
+    }
+    found = false;
+    if (NULL == tavp->acron) {
+        if (! op->page_code_given)
+            op->page_code = ENC_CONTROL_DPC;
+        found = true;
+    } else if (is_acronym_in_status_ctl(tavp)) {
+        if (op->page_code > 0) {
+            if (ENC_CONTROL_DPC != op->page_code)
+                goto inconsistent;
+        } else
+            op->page_code = ENC_CONTROL_DPC;
+        found = true;
+    } else if (is_acronym_in_threshold(tavp)) {
+        if (op->page_code > 0) {
+            if (THRESHOLD_DPC != op->page_code)
+                goto inconsistent;
+        } else
+            op->page_code = THRESHOLD_DPC;
+        found = true;
+    } else if (is_acronym_in_additional(tavp)) {
+        if (op->page_code > 0) {
+            if (ADD_ELEM_STATUS_DPC != op->page_code)
+                goto inconsistent;
+        } else
+            op->page_code = ADD_ELEM_STATUS_DPC;
+        found = true;
+    }
+    if (! found) {
+        pr2serr("acroynm %s not found (try '-ee' option)\n", tavp->acron);
+        return -1;
+    }
+    if (false == join_done) {
+        ret = join_work(ptvp, op, false);
+        if (ret)
+            return ret;
+    }
+    dn_len = op->desc_name ? (int)strlen(op->desc_name) : 0;
+    for (k = 0, jrp = join_arr; ((k < MX_JOIN_ROWS) && jrp->enc_statp);
+         ++k, ++jrp) {
+        if (op->ind_given) {
+            if (op->ind_th != jrp->th_i)
+                continue;
+            if (! match_ind_indiv(jrp->indiv_i, op))
+                continue;
+        } else if (op->desc_name) {
+            ed_bp = jrp->elem_descp;
+            if (NULL == ed_bp)
+                continue;
+            desc_len = sg_get_unaligned_be16(ed_bp + 2);
+            /* some element descriptor strings have trailing NULLs and
+             * count them; adjust */
+            while (desc_len && ('\0' == ed_bp[4 + desc_len - 1]))
+                --desc_len;
+            if (desc_len != dn_len)
+                continue;
+            if (0 != strncmp(op->desc_name, (const char *)(ed_bp + 4),
+                             desc_len))
+                continue;
+        } else if (op->dev_slot_num >= 0) {
+            if (op->dev_slot_num != jrp->dev_slot_num)
+                continue;
+        } else if (saddr_non_zero(op->sas_addr)) {
+            for (j = 0; j < 8; ++j) {
+                if (op->sas_addr[j] != jrp->sas_addr[j])
+                    break;
+            }
+            if (j < 8)
+                continue;
+        }
+        if (ENC_CONTROL_DPC == op->page_code)
+            ret = cgs_enc_ctl_stat(ptvp, jrp, tavp, op, last);
+        else if (THRESHOLD_DPC == op->page_code)
+            ret = cgs_threshold(ptvp, jrp, tavp, op, last);
+        else if (ADD_ELEM_STATUS_DPC == op->page_code)
+            ret = cgs_additional_el(jrp, tavp, op);
+        else {
+            pr2serr("page %s not supported for cgs\n",
+                    etype_str(op->page_code, b, sizeof(b)));
+            ret = -1;
+        }
+        if (ret)
+            return ret;
+        if (op->ind_indiv_last <= op->ind_indiv)
+            break;
+    }   /* end of loop over join array */
+    if ((k >= MX_JOIN_ROWS || (NULL == jrp->enc_statp))) {
+        if (op->desc_name)
+            pr2serr("descriptor name: %s not found (check the 'ed' page "
+                    "[0x7])\n", op->desc_name);
+        else if (op->dev_slot_num >= 0)
+            pr2serr("device slot number: %d not found\n", op->dev_slot_num);
+        else if (saddr_non_zero(op->sas_addr))
+            pr2serr("SAS address not found\n");
+        else {
+            pr2serr("index: %d,%d", op->ind_th, op->ind_indiv);
+            if (op->ind_indiv_last > op->ind_indiv)
+                printf("-%d not found\n", op->ind_indiv_last);
+            else
+                printf(" not found\n");
+        }
+        return -1;
+    }
+    return 0;
+
+inconsistent:
+    pr2serr("acroynm %s inconsistent with page_code=0x%x\n", tavp->acron,
+            op->page_code);
+    return -1;
+}
+
+/* Called when '--nickname=SEN' given. First calls status page to fetch
+ * the generation code. Returns 0 for success, any other return value is
+ * an error. */
+static int
+ses_set_nickname(struct sg_pt_base * ptvp, struct opts_t * op)
+{
+    int res, len;
+    int resp_len = 0;
+    uint8_t b[64];
+    const int control_plen = 0x24;
+
+    if (NULL == ptvp) {
+        pr2serr("%s: ignored when no device name\n", __func__);
+        return 0;
+    }
+    memset(b, 0, sizeof(b));
+    /* Only after the generation code, offset 4 for 4 bytes */
+    res = do_rec_diag(ptvp, SUBENC_NICKNAME_DPC, b, 8, op, &resp_len);
+    if (res) {
+        pr2serr("%s: Subenclosure nickname status page, res=%d\n", __func__,
+                res);
+        return -1;
+    }
+    if (resp_len < 8) {
+        pr2serr("%s: Subenclosure nickname status page, response length too "
+                "short: %d\n", __func__, resp_len);
+        return -1;
+    }
+    if (op->verbose) {
+        uint32_t gc;
+
+        gc = sg_get_unaligned_be32(b + 4);
+        pr2serr("%s: generation code from status page: %" PRIu32 "\n",
+                __func__, gc);
+    }
+    b[0] = (uint8_t)SUBENC_NICKNAME_DPC;  /* just in case */
+    b[1] = (uint8_t)op->seid;
+    sg_put_unaligned_be16((uint16_t)control_plen, b + 2);
+    len = strlen(op->nickname_str);
+    if (len > 32)
+        len = 32;
+    memcpy(b + 8, op->nickname_str, len);
+    return do_senddiag(ptvp, b, control_plen + 4, ! op->quiet,
+                       op->verbose);
+}
+
+static void
+enumerate_diag_pages(void)
+{
+    bool got1;
+    const struct diag_page_code * pcdp;
+    const struct diag_page_abbrev * ap;
+
+    printf("Diagnostic pages, followed by abbreviation(s) then page code:\n");
+    for (pcdp = dpc_arr; pcdp->desc; ++pcdp) {
+        printf("    %s  [", pcdp->desc);
+        for (ap = dp_abbrev, got1 = false; ap->abbrev; ++ap) {
+            if (ap->page_code == pcdp->page_code) {
+                printf("%s%s", (got1 ? "," : ""), ap->abbrev);
+                got1 = true;
+            }
+        }
+        printf("] [0x%x]\n", pcdp->page_code);
+    }
+}
+
+/* Output from --enumerate or --list option. Note that the output is
+ * different when the option is given twice. */
+static void
+enumerate_work(const struct opts_t * op)
+{
+    int num;
+
+    if (op->dev_name)
+        printf(">>> DEVICE %s ignored when --%s option given.\n",
+               op->dev_name, (op->do_list ? "list" : "enumerate"));
+    num = op->enumerate + (int)op->do_list;
+    if (num < 2) {
+        const struct element_type_t * etp;
+
+        enumerate_diag_pages();
+        printf("\nSES element type names, followed by abbreviation and "
+               "element type code:\n");
+        for (etp = element_type_arr; etp->desc; ++etp)
+            printf("    %s  [%s] [0x%x]\n", etp->desc, etp->abbrev,
+                   etp->elem_type_code);
+    } else {
+        bool given_et = false;
+        const struct acronym2tuple * ap;
+        const char * cp;
+        char a[160];
+        char b[64];
+        char bb[64];
+
+        /* command line has multiple --enumerate and/or --list options */
+        printf("--clear, --get, --set acronyms for Enclosure Status/Control "
+               "['es' or 'ec'] page");
+        if (op->ind_given && op->ind_etp &&
+            (cp = etype_str(op->ind_etp->elem_type_code, bb, sizeof(bb)))) {
+            printf("\n(element type: %s)", cp);
+            given_et = true;
+        }
+        printf(":\n");
+        for (ap = ecs_a2t_arr; ap->acron; ++ap) {
+            if (given_et && (op->ind_etp->elem_type_code != ap->etype))
+                continue;
+            cp = (ap->etype < 0) ?  "*" : etype_str(ap->etype, b, sizeof(b));
+            snprintf(a, sizeof(a), "  %s  [%s] [%d:%d:%d]", ap->acron,
+                     (cp ? cp : "??"), ap->start_byte, ap->start_bit,
+                     ap->num_bits);
+            if (ap->info)
+                printf("%-44s  %s\n", a, ap->info);
+            else
+                printf("%s\n", a);
+        }
+        if (given_et)
+            return;
+        printf("\n--clear, --get, --set acronyms for Threshold In/Out "
+               "['th'] page:\n");
+        for (ap = th_a2t_arr; ap->acron; ++ap) {
+            cp = (ap->etype < 0) ? "*" : etype_str(ap->etype, b, sizeof(b));
+            snprintf(a, sizeof(a), "  %s  [%s] [%d:%d:%d]", ap->acron,
+                     (cp ? cp : "??"), ap->start_byte, ap->start_bit,
+                     ap->num_bits);
+            if (ap->info)
+                printf("%-34s  %s\n", a, ap->info);
+            else
+                printf("%s\n", a);
+        }
+        printf("\n--get acronyms for Additional Element Status ['aes'] page "
+               "(SAS EIP=1):\n");
+        for (ap = ae_sas_a2t_arr; ap->acron; ++ap) {
+            cp = (ap->etype < 0) ? "*" : etype_str(ap->etype, b, sizeof(b));
+            snprintf(a, sizeof(a), "  %s  [%s] [%d:%d:%d]", ap->acron,
+                     (cp ? cp : "??"), ap->start_byte, ap->start_bit,
+                     ap->num_bits);
+            if (ap->info)
+                printf("%-34s  %s\n", a, ap->info);
+            else
+                printf("%s\n", a);
+        }
+    }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool have_cgs = false;
+    int k, n, d_len, res, resid, vb;
+    int sg_fd = -1;
+    int pd_type = 0;
+    int ret = 0;
+    const char * cp;
+    struct opts_t opts;
+    struct opts_t * op;
+    struct tuple_acronym_val * tavp;
+    struct cgs_cl_t * cgs_clp;
+    uint8_t * free_enc_stat_rsp = NULL;
+    uint8_t * free_elem_desc_rsp = NULL;
+    uint8_t * free_add_elem_rsp = NULL;
+    uint8_t * free_threshold_rsp = NULL;
+    struct sg_pt_base * ptvp = NULL;
+    struct tuple_acronym_val tav_arr[CGS_CL_ARR_MAX_SZ];
+    char buff[128];
+    char b[128];
+
+    op = &opts;
+    memset(op, 0, sizeof(*op));
+    op->dev_slot_num = -1;
+    op->ind_indiv_last = -1;
+    op->maxlen = MX_ALLOC_LEN;
+    res = parse_cmd_line(op, argc, argv);
+    vb = op->verbose;
+    if (res) {
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto early_out;
+    }
+    if (op->do_help) {
+        usage(op->do_help);
+        goto early_out;
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("version: %s\n", version_str);
+        goto early_out;
+    }
+
+    vb = op->verbose;   /* may have changed */
+    if (op->enumerate || op->do_list) {
+        enumerate_work(op);
+        goto early_out;
+    }
+    enc_stat_rsp = sg_memalign(op->maxlen, 0, &free_enc_stat_rsp, false);
+    if (NULL == enc_stat_rsp) {
+        pr2serr("Unable to get heap for enc_stat_rsp\n");
+        goto err_out;
+    }
+    enc_stat_rsp_sz = op->maxlen;
+    elem_desc_rsp = sg_memalign(op->maxlen, 0, &free_elem_desc_rsp, false);
+    if (NULL == elem_desc_rsp) {
+        pr2serr("Unable to get heap for elem_desc_rsp\n");
+        goto err_out;
+    }
+    elem_desc_rsp_sz = op->maxlen;
+    add_elem_rsp = sg_memalign(op->maxlen, 0, &free_add_elem_rsp, false);
+    if (NULL == add_elem_rsp) {
+        pr2serr("Unable to get heap for add_elem_rsp\n");
+        goto err_out;
+    }
+    add_elem_rsp_sz = op->maxlen;
+    threshold_rsp = sg_memalign(op->maxlen, 0, &free_threshold_rsp, false);
+    if (NULL == threshold_rsp) {
+        pr2serr("Unable to get heap for threshold_rsp\n");
+        goto err_out;
+    }
+    threshold_rsp_sz = op->maxlen;
+
+    if (op->num_cgs) {
+        have_cgs = true;
+        if (op->page_code_given &&
+            ! ((ENC_STATUS_DPC == op->page_code) ||
+               (THRESHOLD_DPC == op->page_code) ||
+               (ADD_ELEM_STATUS_DPC == op->page_code))) {
+            pr2serr("--clear, --get or --set options only supported for the "
+                    "Enclosure\nControl/Status, Threshold In/Out and "
+                    "Additional Element Status pages\n");
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto err_out;
+        }
+        if (! (op->ind_given || op->desc_name || (op->dev_slot_num >= 0) ||
+               saddr_non_zero(op->sas_addr))) {
+            pr2serr("with --clear, --get or --set option need either\n   "
+                    "--index, --descriptor, --dev-slot-num or --sas-addr\n");
+            ret = SG_LIB_CONTRADICT;
+            goto err_out;
+        }
+        for (k = 0, cgs_clp = op->cgs_cl_arr, tavp = tav_arr; k < op->num_cgs;
+             ++k, ++cgs_clp, ++tavp) {
+            if (parse_cgs_str(cgs_clp->cgs_str, tavp)) {
+                pr2serr("unable to decode STR argument to: %s\n",
+                        cgs_clp->cgs_str);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto err_out;
+            }
+            if ((GET_OPT == cgs_clp->cgs_sel) && tavp->val_str)
+                pr2serr("--get option ignoring =<val> at the end of STR "
+                        "argument\n");
+            if (NULL == tavp->val_str) {
+                if (CLEAR_OPT == cgs_clp->cgs_sel)
+                    tavp->val = DEF_CLEAR_VAL;
+                if (SET_OPT == cgs_clp->cgs_sel)
+                    tavp->val = DEF_SET_VAL;
+            }
+            if (!strcmp(cgs_clp->cgs_str, "sas_addr") &&
+                op->dev_slot_num < 0) {
+                pr2serr("--get=sas_addr requires --dev-slot-num.  For "
+                        "expander SAS address, use exp_sas_addr instead.\n");
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto err_out;
+            }
+            tavp->cgs_sel = cgs_clp->cgs_sel;
+        }
+        /* keep this descending for loop directly after ascending for loop */
+        for (--k, --cgs_clp; k >= 0; --k, --cgs_clp) {
+            if ((CLEAR_OPT == cgs_clp->cgs_sel) ||
+                (SET_OPT == cgs_clp->cgs_sel)) {
+                cgs_clp->last_cs = true;
+                break;
+            }
+        }
+    }
+
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+    if (vb > 4)
+        pr2serr("Initial win32 SPT interface state: %s\n",
+                scsi_pt_win32_spt_state() ? "direct" : "indirect");
+    if (op->maxlen >= 16384)
+        scsi_pt_win32_direct(SG_LIB_WIN32_DIRECT /* SPT pt interface */);
+#endif
+#endif
+
+#if 0
+    pr2serr("Debug dump of input parameters:\n");
+    pr2serr("  index option given: %d, ind_th=%d, ind_indiv=%d, "
+            "ind_indiv_last=%d\n", op->ind_given, op->ind_th,
+            op->ind_indiv, op->ind_indiv_last);
+    pr2serr("  num_cgs=%d, contents:\n", op->num_cgs);
+    for (k = 0, tavp = tav_arr, cgs_clp = op->cgs_cl_arr;
+         k < op->num_cgs; ++k, ++tavp, ++cgs_clp) {
+        pr2serr("  k=%d, cgs_sel=%d, last_cs=%d, tavp=%p str: %s\n",
+                k, (int)cgs_clp->cgs_sel, (int)cgs_clp->last_cs, tavp,
+                cgs_clp->cgs_str);
+    }
+#endif
+
+    if (op->dev_name) {
+        sg_fd = sg_cmds_open_device(op->dev_name, op->o_readonly, vb);
+        if (sg_fd < 0) {
+            if (vb)
+                pr2serr("open error: %s: %s\n", op->dev_name,
+                        safe_strerror(-sg_fd));
+            ret = sg_convert_errno(-sg_fd);
+            goto early_out;
+        }
+        ptvp = construct_scsi_pt_obj_with_fd(sg_fd, vb);
+        if (NULL == ptvp) {
+            pr2serr("construct pt_base failed, probably out of memory\n");
+            ret = sg_convert_errno(ENOMEM);
+            goto err_out;
+        }
+        if (! (op->do_raw || have_cgs || (op->do_hex > 2))) {
+            uint8_t inq_rsp[36];
+
+            memset(inq_rsp, 0, sizeof(inq_rsp));
+            if ((ret = sg_ll_inquiry_pt(ptvp, false, 0, inq_rsp, 36,
+                                        0, &resid, ! op->quiet, vb))) {
+                pr2serr("%s doesn't respond to a SCSI INQUIRY\n",
+                        op->dev_name);
+                goto err_out;
+            } else {
+                if (resid > 0)
+                    pr2serr("Short INQUIRY response, not looking good\n");
+                printf("  %.8s  %.16s  %.4s\n", inq_rsp + 8, inq_rsp + 16,
+                       inq_rsp + 32);
+                pd_type = PDT_MASK & inq_rsp[0];
+                cp = sg_get_pdt_str(pd_type, sizeof(buff), buff);
+                if (0xd == pd_type) {
+                    if (vb)
+                        printf("    enclosure services device\n");
+                } else if (0x40 & inq_rsp[6])
+                    printf("    %s device has EncServ bit set\n", cp);
+                else {
+                    if (0 != memcmp("NVMe", inq_rsp + 8, 4))
+                        printf("    %s device (not an enclosure)\n", cp);
+                }
+            }
+            clear_scsi_pt_obj(ptvp);
+        }
+    } else if (op->do_control) {
+        pr2serr("Cannot do SCSI Send diagnostic command without a DEVICE\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+#if (HAVE_NVME && (! IGNORE_NVME))
+    if (ptvp && pt_device_is_nvme(ptvp) && (enc_stat_rsp_sz > 4095)) {
+        /* Fetch VPD 0xde (vendor specific: sg3_utils) for Identify ctl */
+        ret = sg_ll_inquiry_pt(ptvp, true, 0xde, enc_stat_rsp, 4096, 0,
+                               &resid, ! op->quiet, vb);
+        if (ret) {
+            if (vb)
+                pr2serr("Fetch VPD page 0xde (NVMe Identify ctl) failed, "
+                        "continue\n");
+        } else if (resid > 0) {
+            if (vb)
+                pr2serr("VPD page 0xde (NVMe Identify ctl) less than 4096 "
+                        "bytes, continue\n");
+        } else {
+            uint8_t nvmsr;
+            uint16_t oacs;
+
+            nvmsr = enc_stat_rsp[253];
+            oacs = sg_get_unaligned_le16(enc_stat_rsp + 256);   /* N.B. LE */
+            if (vb > 3)
+                pr2serr("NVMe Identify ctl response: nvmsr=%u, oacs=0x%x\n",
+                        nvmsr, oacs);
+            if (! ((0x2 & nvmsr) && (0x40 & oacs))) {
+                pr2serr(">>> Warning: A NVMe enclosure needs both the "
+                        "enclosure bit and support for\n");
+                pr2serr(">>> MI Send+Receive commands bit set; current "
+                        "state: %s, %s\n", (0x2 & nvmsr) ? "set" : "clear",
+                        (0x40 & oacs) ? "set" : "clear");
+            }
+        }
+        clear_scsi_pt_obj(ptvp);
+        memset(enc_stat_rsp, 0, enc_stat_rsp_sz);
+    }
+#endif
+
+    if (ptvp) {
+        n = (enc_stat_rsp_sz < REQUEST_SENSE_RESP_SZ) ? enc_stat_rsp_sz :
+                                                        REQUEST_SENSE_RESP_SZ;
+        ret = sg_ll_request_sense_pt(ptvp, false, enc_stat_rsp, n,
+                                     ! op->quiet, vb);
+        if (0 == ret) {
+            int sense_len = n - get_scsi_pt_resid(ptvp);
+            struct sg_scsi_sense_hdr ssh;
+
+            if ((sense_len > 7) && sg_scsi_normalize_sense(enc_stat_rsp,
+                                        sense_len, &ssh)) {
+                const char * aa_str = sg_get_asc_ascq_str(ssh.asc, ssh.ascq,
+                                                          sizeof(b), b);
+
+                /* Ignore the possibility that multiple UAs queued up */
+                if (SPC_SK_UNIT_ATTENTION == ssh.sense_key)
+                    pr2serr("Unit attention detected: %s\n  ... continue\n",
+                            aa_str);
+                else {
+                    if (vb) {
+                        pr2serr("Request Sense near startup detected "
+                                "something:\n");
+                        pr2serr("  Sense key: %s, additional: %s\n  ... "
+                                "continue\n",
+                                sg_get_sense_key_str(ssh.sense_key,
+                                         sizeof(buff), buff), aa_str);
+                    }
+                }
+            }
+        } else {
+            if (vb)
+                pr2serr("Request sense failed (res=%d), most likely "
+                        " problems ahead\n", ret);
+        }
+        clear_scsi_pt_obj(ptvp);
+        memset(enc_stat_rsp, 0, enc_stat_rsp_sz);
+    }
+
+    if (op->nickname_str)
+        ret = ses_set_nickname(ptvp, op);
+    else if (have_cgs) {
+        for (k = 0, tavp = tav_arr, cgs_clp = op->cgs_cl_arr;
+             k < op->num_cgs; ++k, ++tavp, ++cgs_clp) {
+            ret = ses_cgs(ptvp, tavp, op,  cgs_clp->last_cs);
+            if (ret)
+                break;
+        }
+    } else if (op->do_join)
+        ret = join_work(ptvp, op, true);
+    else if (op->do_status)
+        ret = process_status_page_s(ptvp, op);
+    else { /* control page requested */
+        op->data_arr[0] = op->page_code;
+        op->data_arr[1] = op->byte1;
+        d_len = op->arr_len + DATA_IN_OFF;
+        sg_put_unaligned_be16((uint16_t)op->arr_len, op->data_arr + 2);
+        switch (op->page_code) {
+        case ENC_CONTROL_DPC:  /* Enclosure Control diagnostic page [0x2] */
+            printf("Sending Enclosure Control [0x%x] page, with page "
+                   "length=%d bytes\n", op->page_code, op->arr_len);
+            ret = do_senddiag(ptvp, op->data_arr, d_len, ! op->quiet, vb);
+            if (ret) {
+                pr2serr("couldn't send Enclosure Control page\n");
+                goto err_out;
+            }
+            break;
+        case STRING_DPC:       /* String Out diagnostic page [0x4] */
+            printf("Sending String Out [0x%x] page, with page length=%d "
+                   "bytes\n", op->page_code, op->arr_len);
+            ret = do_senddiag(ptvp, op->data_arr, d_len, ! op->quiet, vb);
+            if (ret) {
+                pr2serr("couldn't send String Out page\n");
+                goto err_out;
+            }
+            break;
+        case THRESHOLD_DPC:       /* Threshold Out diagnostic page [0x5] */
+            printf("Sending Threshold Out [0x%x] page, with page length=%d "
+                   "bytes\n", op->page_code, op->arr_len);
+            ret = do_senddiag(ptvp, op->data_arr, d_len, ! op->quiet, vb);
+            if (ret) {
+                pr2serr("couldn't send Threshold Out page\n");
+                goto err_out;
+            }
+            break;
+        case ARRAY_CONTROL_DPC:   /* Array control diagnostic page [0x6] */
+            printf("Sending Array Control [0x%x] page, with page "
+                   "length=%d bytes\n", op->page_code, op->arr_len);
+            ret = do_senddiag(ptvp, op->data_arr, d_len, ! op->quiet, vb);
+            if (ret) {
+                pr2serr("couldn't send Array Control page\n");
+                goto err_out;
+            }
+            break;
+        case SUBENC_STRING_DPC: /* Subenclosure String Out page [0xc] */
+            printf("Sending Subenclosure String Out [0x%x] page, with page "
+                   "length=%d bytes\n", op->page_code, op->arr_len);
+            ret = do_senddiag(ptvp, op->data_arr, d_len, ! op->quiet, vb);
+            if (ret) {
+                pr2serr("couldn't send Subenclosure String Out page\n");
+                goto err_out;
+            }
+            break;
+        case DOWNLOAD_MICROCODE_DPC: /* Download Microcode Control [0xe] */
+            printf("Sending Download Microcode Control [0x%x] page, with "
+                   "page length=%d bytes\n", op->page_code, d_len);
+            printf("  Perhaps it would be better to use the sg_ses_microcode "
+                   "utility\n");
+            ret = do_senddiag(ptvp, op->data_arr, d_len, ! op->quiet, vb);
+            if (ret) {
+                pr2serr("couldn't send Download Microcode Control page\n");
+                goto err_out;
+            }
+            break;
+        case SUBENC_NICKNAME_DPC: /* Subenclosure Nickname Control [0xf] */
+            printf("Sending Subenclosure Nickname Control [0x%x] page, with "
+                   "page length=%d bytes\n", op->page_code, d_len);
+            ret = do_senddiag(ptvp, op->data_arr, d_len, ! op->quiet, vb);
+            if (ret) {
+                pr2serr("couldn't send Subenclosure Nickname Control page\n");
+                goto err_out;
+            }
+            break;
+        default:
+            pr2serr("Setting SES control page 0x%x not supported by this "
+                    "utility\n", op->page_code);
+            pr2serr("That can be done with the sg_senddiag utility with its "
+                    "'--raw=' option\n");
+            ret = SG_LIB_SYNTAX_ERROR;
+            break;
+        }
+    }
+
+err_out:
+    if (! op->do_status) {
+        sg_get_category_sense_str(ret, sizeof(b), b, vb);
+        pr2serr("    %s\n", b);
+    }
+    if (free_enc_stat_rsp)
+        free(free_enc_stat_rsp);
+    if (free_elem_desc_rsp)
+        free(free_elem_desc_rsp);
+    if (free_add_elem_rsp)
+        free(free_add_elem_rsp);
+    if (free_threshold_rsp)
+        free(free_threshold_rsp);
+
+early_out:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (ptvp)
+        destruct_scsi_pt_obj(ptvp);
+    if ((0 == vb) && (! op->quiet)) {
+        if (! sg_if_can2stderr("sg_ses failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+        else if ((SG_LIB_SYNTAX_ERROR == ret) && (0 == vb))
+            pr2serr("Add '-h' to command line for usage information\n");
+    }
+    if (op->free_data_arr)
+        free(op->free_data_arr);
+    if (free_config_dp_resp)
+        free(free_config_dp_resp);
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_ses_microcode.c b/src/sg_ses_microcode.c
new file mode 100644
index 0000000..a00b6d5
--- /dev/null
+++ b/src/sg_ses_microcode.c
@@ -0,0 +1,941 @@
+/*
+ * Copyright (c) 2014-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <ctype.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+#include "sg_pt.h"      /* needed for scsi_pt_win32_direct() */
+#endif
+#endif
+
+/*
+ * This utility issues the SCSI SEND DIAGNOSTIC and RECEIVE DIAGNOSTIC
+ * RESULTS commands in order to send microcode to the given SES device.
+ */
+
+static const char * version_str = "1.19 20210610";    /* ses4r02 */
+
+#define ME "sg_ses_microcode: "
+#define MAX_XFER_LEN (128 * 1024 * 1024)
+#define DEF_XFER_LEN (8 * 1024 * 1024)
+#define DEF_DIN_LEN (8 * 1024)
+#define EBUFF_SZ 256
+
+#define DPC_DOWNLOAD_MICROCODE 0xe
+
+struct opts_t {
+    bool dry_run;
+    bool ealsd;
+    bool mc_non;
+    bool bpw_then_activate;
+    bool mc_len_given;
+    int bpw;            /* bytes per write, chunk size */
+    int mc_id;
+    int mc_len;         /* --length=LEN */
+    int mc_mode;
+    int mc_offset;      /* Buffer offset in SCSI commands */
+    int mc_skip;        /* on FILE */
+    int mc_subenc;
+    int mc_tlen;        /* --tlength=TLEN */
+    int verbose;
+};
+
+static struct option long_options[] = {
+    {"bpw", required_argument, 0, 'b'},
+    {"dry-run", no_argument, 0, 'd'},
+    {"dry_run", no_argument, 0, 'd'},
+    {"ealsd", no_argument, 0, 'e'},
+    {"help", no_argument, 0, 'h'},
+    {"id", required_argument, 0, 'i'},
+    {"in", required_argument, 0, 'I'},
+    {"length", required_argument, 0, 'l'},
+    {"mode", required_argument, 0, 'm'},
+    {"non", no_argument, 0, 'N'},
+    {"offset", required_argument, 0, 'o'},
+    {"skip", required_argument, 0, 's'},
+    {"subenc", required_argument, 0, 'S'},
+    {"tlength", required_argument, 0, 't'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {0, 0, 0, 0},
+};
+
+#define MODE_DNLD_STATUS        0
+#define MODE_DNLD_MC_OFFS       6
+#define MODE_DNLD_MC_OFFS_SAVE  7
+#define MODE_DNLD_MC_OFFS_DEFER 0x0E
+#define MODE_ACTIVATE_MC        0x0F
+#define MODE_ABORT_MC           0xFF    /* actually reserved; any reserved
+                                         * value aborts a microcode download
+                                         * in progress */
+
+struct mode_s {
+        const char *mode_string;
+        int   mode;
+        const char *comment;
+};
+
+static struct mode_s mode_arr[] = {
+    {"dmc_status", MODE_DNLD_STATUS, "report status of microcode "
+     "download"},
+    {"dmc_offs", MODE_DNLD_MC_OFFS, "download microcode with offsets "
+     "and activate"},
+    {"dmc_offs_save", MODE_DNLD_MC_OFFS_SAVE, "download microcode with "
+     "offsets, save and\n\t\t\t\tactivate"},
+    {"dmc_offs_defer", MODE_DNLD_MC_OFFS_DEFER, "download microcode "
+     "with offsets, save and\n\t\t\t\tdefer activation"},
+    {"activate_mc", MODE_ACTIVATE_MC, "activate deferred microcode"},
+    {"dmc_abort", MODE_ABORT_MC, "abort download microcode in progress"},
+    {NULL, 0, NULL},
+};
+
+/* An array of Download microcode status field values and descriptions.
+ * This table is a subset of one in sg_read_buffer for the read microcode
+ * status page. */
+static struct sg_lib_simple_value_name_t mc_status_arr[] = {
+    {0x0, "No download microcode operation in progress"},
+    {0x1, "Download in progress, awaiting more"},
+    {0x2, "Download complete, updating storage"},
+    {0x3, "Updating storage with deferred microcode"},
+    {0x10, "Complete, no error, starting now"},
+    {0x11, "Complete, no error, start after hard reset or power cycle"},
+    {0x12, "Complete, no error, start after power cycle"},
+    {0x13, "Complete, no error, start after activate_mc, hard reset or "
+           "power cycle"},
+    {0x80, "Error, discarded, see additional status"},
+    {0x81, "Error, discarded, image error"},
+    {0x82, "Timeout, discarded"},
+    {0x83, "Internal error, need new microcode before reset"},
+    {0x84, "Internal error, need new microcode, reset safe"},
+    {0x85, "Unexpected activate_mc received"},
+    {0x1000, NULL},
+};
+
+struct dout_buff_t {
+    uint8_t * doutp;
+    uint8_t * free_doutp;
+    int dout_len;
+};
+
+/* This dummy response is used when --dry-run skips the RECEIVE DIAGNOSTICS
+ * RESULTS command. Say maximum download MC size is 4 MB. Set generation
+ * code to 0 . */
+uint8_t dummy_rd_resp[] = {
+    0xe,  3,  0, 68,  0, 0, 0, 0,
+    0,  0,  0,  0,  0x0, 0x40, 0x0, 0x0,  0, 0, 0,  0,  0x0, 0x0, 0x0, 0x0,
+    0,  1,  0,  0,  0x0, 0x40, 0x0, 0x0,  0, 0, 0,  0,  0x0, 0x0, 0x0, 0x0,
+    0,  2,  0,  0,  0x0, 0x40, 0x0, 0x0,  0, 0, 0,  0,  0x0, 0x0, 0x0, 0x0,
+    0,  3,  0,  0,  0x0, 0x40, 0x0, 0x0,  0, 0, 0,  0,  0x0, 0x0, 0x0, 0x0,
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_ses_microcode [--bpw=CS] [--dry-run] [--ealsd] [--help] "
+            "[--id=ID]\n"
+            "                        [--in=FILE] [--length=LEN] [--mode=MO] "
+            "[--non]\n"
+            "                        [--offset=OFF] [--skip=SKIP] "
+            "[--subenc=SEID]\n"
+            "                        [--tlength=TLEN] [--verbose] "
+            "[--version]\n"
+            "                        DEVICE\n"
+            "  where:\n"
+            "    --bpw=CS|-b CS         CS is chunk size: bytes per send "
+            "diagnostic\n"
+            "                           command (def: 0 -> as many as "
+            "possible)\n"
+            "                           can append ',act' to do activate "
+            "after last\n"
+            "    --dry-run|-d           skip SCSI commands, do everything "
+            "else\n"
+            "    --ealsd|-e             exit after last Send Diagnostic "
+            "command\n"
+            "    --help|-h              print out usage message then exit\n"
+            "    --id=ID|-i ID          buffer identifier (0 (default) to "
+            "255)\n"
+            "    --in=FILE|-I FILE      read from FILE ('-I -' read "
+            "from stdin)\n"
+            "    --length=LEN|-l LEN    length in bytes to send (def: "
+            "deduced from\n"
+            "                           FILE taking SKIP into account)\n"
+            "    --mode=MO|-m MO        download microcode mode, MO is "
+            "number or\n"
+            "                           acronym (def: 0 -> 'dmc_status')\n"
+            "    --non|-N               non-standard: bypass all receive "
+            "diagnostic\n"
+            "                           results commands except after check "
+            "condition\n"
+            "    --offset=OFF|-o OFF    buffer offset (unit: bytes, def: "
+            "0);\n"
+            "                           ignored if --bpw=CS given\n"
+            "    --skip=SKIP|-s SKIP    bytes in file FILE to skip before "
+            "reading\n"
+            "    --subenc=SEID|-S SEID     subenclosure identifier (def: 0 "
+            "(primary))\n"
+            "    --tlength=TLEN|-t TLEN    total length of firmware in "
+            "bytes\n"
+            "                              (def: 0). Only needed if "
+            "TLEN>LEN\n"
+            "    --verbose|-v           increase verbosity\n"
+            "    --version|-V           print version string and exit\n\n"
+            "Does one or more SCSI SEND DIAGNOSTIC followed by RECEIVE "
+            "DIAGNOSTIC\nRESULTS command sequences in order to download "
+            "microcode. Use '-m xxx'\nto list available modes. With only "
+            "DEVICE given, the Download Microcode\nStatus dpage is output.\n"
+          );
+}
+
+static void
+print_modes(void)
+{
+    const struct mode_s * mp;
+
+    pr2serr("The modes parameter argument can be numeric (hex or decimal)\n"
+            "or symbolic:\n");
+    for (mp = mode_arr; mp->mode_string; ++mp) {
+        pr2serr(" %3d [0x%02x]  %-18s%s\n", mp->mode, mp->mode,
+                mp->mode_string, mp->comment);
+    }
+    pr2serr("\nAdditionally '--bpw=<val>,act' does a activate deferred "
+            "microcode after a\nsuccessful multipart dmc_offs_defer mode "
+            "download.\n");
+}
+
+static const char *
+get_mc_status_str(uint8_t status_val)
+{
+    const struct sg_lib_simple_value_name_t * mcsp;
+
+    for (mcsp = mc_status_arr; mcsp->name; ++mcsp) {
+        if (status_val == mcsp->value)
+            return mcsp->name;
+    }
+    return "";
+}
+
+/* display DPC_DOWNLOAD_MICROCODE status dpage [0xe] */
+static void
+show_download_mc_sdg(const uint8_t * resp, int resp_len,
+                     uint32_t gen_code)
+{
+    int k, num_subs, num;
+    const uint8_t * bp;
+    const char * cp;
+
+    printf("Download microcode status diagnostic page:\n");
+    if (resp_len < 8)
+        goto truncated;
+    num_subs = resp[1];  /* primary is additional one) */
+    num = (resp_len - 8) / 16;
+    if ((resp_len - 8) % 16)
+        pr2serr("Found %d Download microcode status descriptors, but there "
+                "is residual\n", num);
+    printf("  number of secondary subenclosures: %d\n", num_subs);
+    printf("  generation code: 0x%" PRIx32 "\n", gen_code);
+    bp = resp + 8;
+    for (k = 0; k < num; ++k, bp += 16) {
+        cp = (0 == bp[1]) ? " [primary]" : "";
+        printf("   subenclosure identifier: %d%s\n", bp[1], cp);
+        cp = get_mc_status_str(bp[2]);
+        if (strlen(cp) > 0) {
+            printf("     download microcode status: %s [0x%x]\n", cp, bp[2]);
+            printf("     download microcode additional status: 0x%x\n",
+                   bp[3]);
+        } else
+            printf("     download microcode status: 0x%x [additional "
+                   "status: 0x%x]\n", bp[2], bp[3]);
+        printf("     download microcode maximum size: %" PRIu32 " bytes\n",
+               sg_get_unaligned_be32(bp + 4));
+        printf("     download microcode expected buffer id: 0x%x\n", bp[11]);
+        printf("     download microcode expected buffer id offset: %" PRIu32
+               "\n", sg_get_unaligned_be32(bp + 12));
+    }
+    return;
+truncated:
+    pr2serr("    <<<download status: response too short>>>\n");
+    return;
+}
+
+static int
+send_then_receive(int sg_fd, uint32_t gen_code, int off_off,
+                  const uint8_t * dmp, int dmp_len,
+                  struct dout_buff_t * wp, uint8_t * dip,
+                  int din_len, bool last, const struct opts_t * op)
+{
+    bool send_data = false;
+    int do_len, rem, res, rsp_len, k, n, num, mc_status, resid, act_len, verb;
+    int ret = 0;
+    uint32_t rec_gen_code;
+    const uint8_t * bp;
+    const char * cp;
+
+    verb = (op->verbose > 1) ? op->verbose - 1 : 0;
+    switch (op->mc_mode) {
+    case MODE_DNLD_MC_OFFS:
+    case MODE_DNLD_MC_OFFS_SAVE:
+    case MODE_DNLD_MC_OFFS_DEFER:
+        send_data = true;
+        do_len = 24 + dmp_len;
+        rem = do_len % 4;
+        if (rem)
+            do_len += (4 - rem);
+        break;
+    case MODE_ACTIVATE_MC:
+    case MODE_ABORT_MC:
+        do_len = 24;
+        break;
+    default:
+        pr2serr("%s: unexpected mc_mode=0x%x\n", __func__, op->mc_mode);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (do_len > wp->dout_len) {
+        if (wp->doutp)
+            free(wp->doutp);
+        wp->doutp = sg_memalign(do_len, 0, &wp->free_doutp, op->verbose > 3);
+        if (! wp->doutp) {
+            pr2serr("%s: unable to alloc %d bytes\n", __func__, do_len);
+            return SG_LIB_CAT_OTHER;
+        }
+        wp->dout_len = do_len;
+    } else
+        memset(wp->doutp, 0, do_len);
+    wp->doutp[0] = DPC_DOWNLOAD_MICROCODE;
+    wp->doutp[1] = op->mc_subenc;
+    sg_put_unaligned_be16(do_len - 4, wp->doutp + 2);
+    sg_put_unaligned_be32(gen_code, wp->doutp + 4);
+    wp->doutp[8] = op->mc_mode;
+    wp->doutp[11] = op->mc_id;
+    if (send_data)
+        sg_put_unaligned_be32(op->mc_offset + off_off, wp->doutp + 12);
+    sg_put_unaligned_be32(op->mc_tlen, wp->doutp + 16);
+    sg_put_unaligned_be32(dmp_len, wp->doutp + 20);
+    if (send_data && (dmp_len > 0))
+        memcpy(wp->doutp + 24, dmp, dmp_len);
+    if ((op->verbose > 2) || (op->dry_run && op->verbose)) {
+        pr2serr("send diag: sub-enc id=%u exp_gen=%u download_mc_code=%u "
+                "buff_id=%u\n", op->mc_subenc, gen_code, op->mc_mode,
+                op->mc_id);
+        pr2serr("    buff_off=%u image_len=%u this_mc_data_len=%u "
+                "dout_len=%u\n", op->mc_offset + off_off, op->mc_tlen,
+                dmp_len, do_len);
+    }
+    /* select long duration timeout (7200 seconds) */
+    if (op->dry_run) {
+        if (op->mc_subenc < 4) {
+            int s = op->mc_offset + off_off + dmp_len;
+
+            n = 8 + (op->mc_subenc * 16);
+            dummy_rd_resp[n + 11] = op->mc_id;
+            sg_put_unaligned_be32(((send_data && (! last)) ? s : 0),
+                                  dummy_rd_resp + n + 12);
+            if (MODE_ABORT_MC == op->mc_mode)
+                dummy_rd_resp[n + 2] = 0x80;
+            else if (MODE_ACTIVATE_MC == op->mc_mode)
+                dummy_rd_resp[n + 2] = 0x0;     /* done */
+            else
+                dummy_rd_resp[n + 2] = (s >= op->mc_tlen) ? 0x13 : 0x1;
+        }
+        res = 0;
+    } else
+        res = sg_ll_send_diag(sg_fd, 0 /* st_code */, true /* pf */,
+                              false /* st */, false /* devofl */,
+                              false /* unitofl */, 1 /* long_duration */,
+                              wp->doutp, do_len, true /* noisy */, verb);
+    if (op->mc_non) {
+        /* If non-standard, only call RDR after failed SD */
+        if (0 == res)
+            return 0;
+        /* If RDR error after SD error, prefer reporting SD error */
+        ret = res;
+    } else {
+        switch (op->mc_mode) {
+        case MODE_DNLD_MC_OFFS:
+        case MODE_DNLD_MC_OFFS_SAVE:
+            if (res)
+                return res;
+            else if (last) {
+                if (op->ealsd)
+                    return 0;   /* RDR after last may hit a device reset */
+            }
+            break;
+        case MODE_DNLD_MC_OFFS_DEFER:
+            if (res)
+                return res;
+            break;
+        case MODE_ACTIVATE_MC:
+        case MODE_ABORT_MC:
+            if (0 == res) {
+                if (op->ealsd)
+                    return 0;   /* RDR after this may hit a device reset */
+            }
+            /* SD has failed, so do a RDR but return SD's error */
+            ret = res;
+            break;
+        default:
+            pr2serr("%s: mc_mode=0x%x\n", __func__, op->mc_mode);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    if (op->dry_run) {
+        n = sizeof(dummy_rd_resp);
+        n = (n < din_len) ? n : din_len;
+        memcpy(dip, dummy_rd_resp, n);
+        resid = din_len - n;
+        res = 0;
+    } else
+        res = sg_ll_receive_diag_v2(sg_fd, true /* pcv */,
+                                    DPC_DOWNLOAD_MICROCODE, dip, din_len,
+                                    0 /* default timeout */, &resid, true,
+                                    verb);
+    if (res)
+        return ret ? ret : res;
+    rsp_len = sg_get_unaligned_be16(dip + 2) + 4;
+    act_len = din_len - resid;
+    if (rsp_len > din_len) {
+        pr2serr("<<< warning response buffer too small [%d but need "
+                "%d]>>>\n", din_len, rsp_len);
+        rsp_len = din_len;
+    }
+    if (rsp_len > act_len) {
+        pr2serr("<<< warning response too short [actually got %d but need "
+                "%d]>>>\n", act_len, rsp_len);
+        rsp_len = act_len;
+    }
+    if (rsp_len < 8) {
+        pr2serr("Download microcode status dpage too short [%d]\n", rsp_len);
+        return ret ? ret : SG_LIB_CAT_OTHER;
+    }
+    rec_gen_code = sg_get_unaligned_be32(dip + 4);
+    if ((op->verbose > 2) || (op->dry_run && op->verbose)) {
+        n = 8 + (op->mc_subenc * 16);
+        pr2serr("rec diag: rsp_len=%d, num_sub-enc=%u rec_gen_code=%u "
+                "exp_buff_off=%u\n", rsp_len, dip[1],
+                sg_get_unaligned_be32(dip + 4),
+                sg_get_unaligned_be32(dip + n + 12));
+    }
+    if (rec_gen_code != gen_code)
+        pr2serr("gen_code changed from %" PRIu32 " to %" PRIu32
+                ", continuing but may fail\n", gen_code, rec_gen_code);
+    num = (rsp_len - 8) / 16;
+    if ((rsp_len - 8) % 16)
+        pr2serr("Found %d Download microcode status descriptors, but there "
+                "is residual\n", num);
+    bp = dip + 8;
+    for (k = 0; k < num; ++k, bp += 16) {
+        if ((unsigned int)op->mc_subenc == (unsigned int)bp[1]) {
+            mc_status = bp[2];
+            cp = get_mc_status_str(mc_status);
+            if ((mc_status >= 0x80) || op->verbose)
+                pr2serr("mc offset=%u: status: %s [0x%x, additional=0x%x]\n",
+                        sg_get_unaligned_be32(bp + 12), cp, mc_status, bp[3]);
+            if (op->verbose > 1)
+                pr2serr("  subenc_id=%d, expected_buffer_id=%d, "
+                        "expected_offset=0x%" PRIx32 "\n", bp[1], bp[11],
+                        sg_get_unaligned_be32(bp + 12));
+            if (mc_status >= 0x80)
+                ret = ret ? ret : SG_LIB_CAT_OTHER;
+        }
+    }
+    return ret;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool last, got_stdin, is_reg;
+    bool want_file = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, len, k, n, rsp_len, resid, act_len, din_len, verb;
+    int sg_fd = -1;
+    int infd = -1;
+    int do_help = 0;
+    int ret = 0;
+    uint32_t gen_code = 0;
+    const char * device_name = NULL;
+    const char * file_name = NULL;
+    uint8_t * dmp = NULL;
+    uint8_t * dip = NULL;
+    uint8_t * free_dip = NULL;
+    char * cp;
+    char ebuff[EBUFF_SZ];
+    struct stat a_stat;
+    struct dout_buff_t dout;
+    struct opts_t opts;
+    struct opts_t * op;
+    const struct mode_s * mp;
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    memset(&dout, 0, sizeof(dout));
+    din_len = DEF_DIN_LEN;
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "b:dehi:I:l:m:No:s:S:t:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            op->bpw = sg_get_num(optarg);
+            if (op->bpw < 0) {
+                pr2serr("argument to '--bpw' should be in a positive "
+                        "number\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((cp = strchr(optarg, ','))) {
+                if (0 == strncmp("act", cp + 1, 3))
+                    op->bpw_then_activate = true;
+            }
+            break;
+        case 'd':
+            op->dry_run = true;
+            break;
+        case 'e':
+            op->ealsd = true;
+            break;
+        case 'h':
+        case '?':
+            ++do_help;
+            break;
+        case 'i':
+            op->mc_id = sg_get_num_nomult(optarg);
+            if ((op->mc_id < 0) || (op->mc_id > 255)) {
+                pr2serr("argument to '--id' should be in the range 0 to "
+                        "255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'I':
+            file_name = optarg;
+            break;
+        case 'l':
+            op->mc_len = sg_get_num(optarg);
+            if (op->mc_len < 0) {
+                pr2serr("bad argument to '--length'\n");
+                return SG_LIB_SYNTAX_ERROR;
+             }
+             op->mc_len_given = true;
+             break;
+        case 'm':
+            if (isdigit((uint8_t)*optarg)) {
+                op->mc_mode = sg_get_num_nomult(optarg);
+                if ((op->mc_mode < 0) || (op->mc_mode > 255)) {
+                    pr2serr("argument to '--mode' should be in the range 0 "
+                            "to 255\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else {
+                len = strlen(optarg);
+                for (mp = mode_arr; mp->mode_string; ++mp) {
+                    if (0 == strncmp(mp->mode_string, optarg, len)) {
+                        op->mc_mode = mp->mode;
+                        break;
+                    }
+                }
+                if (! mp->mode_string) {
+                    print_modes();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            break;
+        case 'N':
+            op->mc_non = true;
+            break;
+        case 'o':
+           op->mc_offset = sg_get_num(optarg);
+           if (op->mc_offset < 0) {
+                pr2serr("bad argument to '--offset'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (0 != (op->mc_offset % 4)) {
+                pr2serr("'--offset' value needs to be a multiple of 4\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 's':
+           op->mc_skip = sg_get_num(optarg);
+           if (op->mc_skip < 0) {
+                pr2serr("bad argument to '--skip'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'S':
+           op->mc_subenc = sg_get_num_nomult(optarg);
+           if ((op->mc_subenc < 0) || (op->mc_subenc > 255)) {
+                pr2serr("expected argument to '--subenc' to be 0 to 255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 't':
+           op->mc_tlen = sg_get_num(optarg);
+           if (op->mc_tlen < 0) {
+                pr2serr("bad argument to '--tlength'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (do_help) {
+        if (do_help > 1) {
+            usage();
+            pr2serr("\n");
+            print_modes();
+        } else
+            usage();
+        return 0;
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        op->verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    switch (op->mc_mode) {
+    case MODE_DNLD_MC_OFFS:
+    case MODE_DNLD_MC_OFFS_SAVE:
+    case MODE_DNLD_MC_OFFS_DEFER:
+        want_file = true;
+        break;
+    case MODE_DNLD_STATUS:
+    case MODE_ACTIVATE_MC:
+    case MODE_ABORT_MC:
+        want_file = false;
+        break;
+    default:
+        pr2serr("%s: mc_mode=0x%x, continue for now\n", __func__,
+                op->mc_mode);
+        break;
+    }
+
+    if ((op->mc_len > 0) && (op->bpw > op->mc_len)) {
+        pr2serr("trim chunk size (CS) to be the same as LEN\n");
+        op->bpw = op->mc_len;
+    }
+    if ((op->mc_offset > 0) && (op->bpw > 0)) {
+        op->mc_offset = 0;
+        pr2serr("WARNING: --offset= ignored (set back to 0) when --bpw= "
+                "argument given (and > 0)\n");
+    }
+
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+    if (op->verbose > 4)
+        pr2serr("Initial win32 SPT interface state: %s\n",
+                scsi_pt_win32_spt_state() ? "direct" : "indirect");
+    scsi_pt_win32_direct(SG_LIB_WIN32_DIRECT /* SPT pt interface */);
+#endif
+#endif
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, op->verbose);
+    if (sg_fd < 0) {
+        if (op->verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    if (file_name && (! want_file))
+        pr2serr("ignoring --in=FILE option\n");
+    else if (file_name) {
+        got_stdin = (0 == strcmp(file_name, "-"));
+        if (got_stdin)
+            infd = STDIN_FILENO;
+        else {
+            if ((infd = open(file_name, O_RDONLY)) < 0) {
+                ret = sg_convert_errno(errno);
+                snprintf(ebuff, EBUFF_SZ,
+                         ME "could not open %s for reading", file_name);
+                perror(ebuff);
+                goto fini;
+            } else if (sg_set_binary_mode(infd) < 0)
+                perror("sg_set_binary_mode");
+        }
+        if ((0 == fstat(infd, &a_stat)) && S_ISREG(a_stat.st_mode)) {
+            is_reg = true;
+            if (0 == op->mc_len) {
+                if (op->mc_skip >= a_stat.st_size) {
+                    pr2serr("skip exceeds file size of %d bytes\n",
+                            (int)a_stat.st_size);
+                    ret = SG_LIB_FILE_ERROR;
+                    goto fini;
+                }
+                op->mc_len = (int)(a_stat.st_size) - op->mc_skip;
+            }
+        } else {
+            is_reg = false;
+            if (0 == op->mc_len)
+                op->mc_len = DEF_XFER_LEN;
+        }
+        if (op->mc_len > MAX_XFER_LEN) {
+            pr2serr("file size or requested length (%d) exceeds "
+                    "MAX_XFER_LEN of %d bytes\n", op->mc_len,
+                    MAX_XFER_LEN);
+            ret = SG_LIB_FILE_ERROR;
+            goto fini;
+        }
+        if (NULL == (dmp = (uint8_t *)malloc(op->mc_len))) {
+            pr2serr(ME "out of memory to hold microcode read from FILE\n");
+            ret = SG_LIB_CAT_OTHER;
+            goto fini;
+        }
+        /* Don't remember why this is preset to 0xff, from write_buffer */
+        memset(dmp, 0xff, op->mc_len);
+        if (op->mc_skip > 0) {
+            if (! is_reg) {
+                if (got_stdin)
+                    pr2serr("Can't skip on stdin\n");
+                else
+                    pr2serr(ME "not a 'regular' file so can't apply skip\n");
+                ret = SG_LIB_FILE_ERROR;
+                goto fini;
+            }
+            if (lseek(infd, op->mc_skip, SEEK_SET) < 0) {
+                ret = sg_convert_errno(errno);
+                snprintf(ebuff,  EBUFF_SZ, ME "couldn't skip to "
+                         "required position on %s", file_name);
+                perror(ebuff);
+                goto fini;
+            }
+        }
+        res = read(infd, dmp, op->mc_len);
+        if (res < 0) {
+            ret = sg_convert_errno(errno);
+            snprintf(ebuff, EBUFF_SZ, ME "couldn't read from %s",
+                     file_name);
+            perror(ebuff);
+            goto fini;
+        }
+        if (res < op->mc_len) {
+            if (op->mc_len_given) {
+                pr2serr("tried to read %d bytes from %s, got %d bytes\n",
+                        op->mc_len, file_name, res);
+                pr2serr("pad with 0xff bytes and continue\n");
+            } else {
+                if (op->verbose) {
+                    pr2serr("tried to read %d bytes from %s, got %d "
+                            "bytes\n", op->mc_len, file_name, res);
+                    pr2serr("will send %d bytes", res);
+                    if ((op->bpw > 0) && (op->bpw < op->mc_len))
+                        pr2serr(", %d bytes per WRITE BUFFER command\n",
+                                op->bpw);
+                    else
+                        pr2serr("\n");
+                }
+                op->mc_len = res;
+            }
+        }
+        if (! got_stdin)
+            close(infd);
+        infd = -1;
+    } else if (want_file) {
+        pr2serr("need --in=FILE option with given mode\n");
+        ret = SG_LIB_CONTRADICT;
+        goto fini;
+    }
+    if (op->mc_tlen < op->mc_len)
+        op->mc_tlen = op->mc_len;
+    if (op->mc_non && (MODE_DNLD_STATUS == op->mc_mode)) {
+        pr2serr("Do nothing because '--non' given so fetching the Download "
+                "microcode status\ndpage might be dangerous\n");
+        goto fini;
+    }
+
+    dip = sg_memalign(din_len, 0, &free_dip, op->verbose > 3);
+    if (NULL == dip) {
+        pr2serr(ME "out of memory (data-in buffer)\n");
+        ret = SG_LIB_CAT_OTHER;
+        goto fini;
+    }
+    verb = (op->verbose > 1) ? op->verbose - 1 : 0;
+    /* Fetch Download microcode status dpage for generation code ++ */
+    if (op->dry_run) {
+        n = sizeof(dummy_rd_resp);
+        n = (n < din_len) ? n : din_len;
+        memcpy(dip, dummy_rd_resp, n);
+        resid = din_len - n;
+        res = 0;
+    } else
+        res = sg_ll_receive_diag_v2(sg_fd, true /* pcv */,
+                                    DPC_DOWNLOAD_MICROCODE, dip, din_len,
+                                    0 /*default timeout */, &resid, true,
+                                    verb);
+    if (0 == res) {
+        rsp_len = sg_get_unaligned_be16(dip + 2) + 4;
+        act_len = din_len - resid;
+        if (rsp_len > din_len) {
+            pr2serr("<<< warning response buffer too small [%d but need "
+                    "%d]>>>\n", din_len, rsp_len);
+            rsp_len = din_len;
+        }
+        if (rsp_len > act_len) {
+            pr2serr("<<< warning response too short [actually got %d but "
+                    "need %d]>>>\n", act_len, rsp_len);
+            rsp_len = act_len;
+        }
+        if (rsp_len < 8) {
+            pr2serr("Download microcode status dpage too short\n");
+            ret = SG_LIB_CAT_OTHER;
+            goto fini;
+        }
+        if ((op->verbose > 2) || (op->dry_run && op->verbose))
+            pr2serr("rec diag(ini): rsp_len=%d, num_sub-enc=%u "
+                    "rec_gen_code=%u\n", rsp_len, dip[1],
+                    sg_get_unaligned_be32(dip + 4));
+    } else {
+        ret = res;
+        goto fini;
+    }
+    gen_code = sg_get_unaligned_be32(dip + 4);
+
+    if (MODE_DNLD_STATUS == op->mc_mode) {
+        show_download_mc_sdg(dip, rsp_len, gen_code);
+        goto fini;
+    } else if (! want_file) {   /* ACTIVATE and ABORT */
+        res = send_then_receive(sg_fd, gen_code, 0, NULL, 0, &dout, dip,
+                                din_len, true, op);
+        ret = res;
+        goto fini;
+    }
+
+    res = 0;
+    if (op->bpw > 0) {
+        for (k = 0, last = false; k < op->mc_len; k += n) {
+            n = op->mc_len - k;
+            if (n > op->bpw)
+                n = op->bpw;
+            else
+                last = true;
+            if (op->verbose)
+                pr2serr("bpw loop: mode=0x%x, id=%d, off_off=%d, len=%d, "
+                        "last=%d\n", op->mc_mode, op->mc_id, k, n, last);
+            res = send_then_receive(sg_fd, gen_code, k, dmp + k, n, &dout,
+                                    dip, din_len, last, op);
+            if (res)
+                break;
+        }
+        if (op->bpw_then_activate && (0 == res)) {
+            op->mc_mode = MODE_ACTIVATE_MC;
+            if (op->verbose)
+                pr2serr("sending Activate deferred microcode [0xf]\n");
+            res = send_then_receive(sg_fd, gen_code, 0, NULL, 0, &dout,
+                                    dip, din_len, true, op);
+        }
+    } else {
+        if (op->verbose)
+            pr2serr("single: mode=0x%x, id=%d, offset=%d, len=%d\n",
+                    op->mc_mode, op->mc_id, op->mc_offset, op->mc_len);
+        res = send_then_receive(sg_fd, gen_code, 0, dmp, op->mc_len, &dout,
+                                dip, din_len, true, op);
+    }
+    if (res)
+        ret = res;
+
+fini:
+    if ((infd >= 0) && (! got_stdin))
+        close(infd);
+    if (dmp)
+        free(dmp);
+    if (dout.free_doutp)
+        free(dout.free_doutp);
+    if (free_dip)
+        free(free_dip);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == op->verbose) {
+        if (! sg_if_can2stderr("sg_ses_microcode failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_start.c b/src/sg_start.c
new file mode 100644
index 0000000..890b696
--- /dev/null
+++ b/src/sg_start.c
@@ -0,0 +1,618 @@
+/*
+ *  Copyright (C) 1999-2020 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+
+    Start/Stop parameter by Kurt Garloff, 6/2000
+    Sync cache parameter by Kurt Garloff, 1/2001
+    Guard block device answering sg's ioctls.
+                     <dgilbert at interlog dot com> 12/2002
+    Convert to SG_IO ioctl so can use sg or block devices in 2.6.* 3/2003
+
+    This utility was written for the Linux 2.4 kernel series. It now
+    builds for the Linux 2.6 and 3 kernel series and various other
+    Operating Systems.
+
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "0.67 20200930";  /* sbc3r14; mmc6r01a */
+
+static struct option long_options[] = {
+        {"eject", no_argument, 0, 'e'},
+        {"fl", required_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"immed", no_argument, 0, 'i'},
+        {"load", no_argument, 0, 'l'},
+        {"loej", no_argument, 0, 'L'},
+        {"mod", required_argument, 0, 'm'},
+        {"noflush", no_argument, 0, 'n'},
+        {"new", no_argument, 0, 'N'},
+        {"old", no_argument, 0, 'O'},
+        {"pc", required_argument, 0, 'p'},
+        {"readonly", no_argument, 0, 'r'},
+        {"start", no_argument, 0, 's'},
+        {"stop", no_argument, 0, 'S'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool do_eject;
+    bool do_immed;
+    bool do_load;
+    bool do_loej;
+    bool do_noflush;
+    bool do_readonly;
+    bool do_start;
+    bool do_stop;
+    bool opt_new;
+    bool verbose_given;
+    bool version_given;
+    int do_fl;
+    int do_help;
+    int do_mod;
+    int do_pc;
+    int verbose;
+    const char * device_name;
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_start [--eject] [--fl=FL] [--help] "
+            "[--immed] [--load] [--loej]\n"
+            "                [--mod=PC_MOD] [--noflush] [--pc=PC] "
+            "[--readonly]\n"
+            "                [--start] [--stop] [--verbose] "
+            "[--version] DEVICE\n"
+            "  where:\n"
+            "    --eject|-e      stop unit then eject the medium\n"
+            "    --fl=FL|-f FL    format layer number (mmc5)\n"
+            "    --help|-h       print usage message then exit\n"
+            "    --immed|-i      device should return control after "
+            "receiving cdb,\n"
+            "                    default action is to wait until action "
+            "is complete\n"
+            "    --load|-l       load medium then start the unit\n"
+            "    --loej|-L       load or eject, corresponds to LOEJ bit "
+            "in cdb;\n"
+            "                    load when START bit also set, else "
+            "eject\n"
+            "    --mod=PC_MOD|-m PC_MOD    power condition modifier "
+            "(def: 0) (sbc)\n"
+            "    --noflush|-n    no flush prior to operation that limits "
+            "access (sbc)\n"
+            "    --pc=PC|-p PC    power condition: 0 (default) -> no "
+            "power condition,\n"
+            "                    1 -> active, 2 -> idle, 3 -> standby, "
+            "5 -> sleep (mmc)\n"
+            "    --readonly|-r    open DEVICE read-only (def: read-write)\n"
+            "                     recommended if DEVICE is ATA disk\n"
+            "    --start|-s      start unit, corresponds to START bit "
+            "in cdb,\n"
+            "                    default (START=1) if no other options "
+            "given\n"
+            "    --stop|-S       stop unit (e.g. spin down disk)\n"
+            "    --verbose|-v    increase verbosity\n"
+            "    --old|-O        use old interface (use as first option)\n"
+            "    --version|-V    print version string then exit\n\n"
+            "    Example: 'sg_start --stop /dev/sdb'    stops unit\n"
+            "             'sg_start --eject /dev/scd0'  stops unit and "
+            "ejects medium\n\n"
+            "Performs a SCSI START STOP UNIT command\n"
+            );
+}
+
+static void
+usage_old()
+{
+    pr2serr("Usage:  sg_start [0] [1] [--eject] [--fl=FL] "
+            "[-i] [--imm=0|1]\n"
+            "                 [--load] [--loej] [--mod=PC_MOD] "
+            "[--noflush] [--pc=PC]\n"
+            "                 [--readonly] [--start] [--stop] [-v] [-V]\n"
+            "                 DEVICE\n"
+            "  where:\n"
+            "    0          stop unit (e.g. spin down a disk or a "
+            "cd/dvd)\n"
+            "    1          start unit (e.g. spin up a disk or a "
+            "cd/dvd)\n"
+            "    --eject    stop then eject the medium\n"
+            "    --fl=FL    format layer number (mmc5)\n"
+            "    -i         return immediately (same as '--imm=1')\n"
+            "    --imm=0|1  0->await completion(def), 1->return "
+            "immediately\n"
+            "    --load     load then start the medium\n"
+            "    --loej     load the medium if '-start' option is "
+            "also given\n"
+            "               or stop unit and eject\n"
+            "    --mod=PC_MOD    power condition modifier "
+            "(def: 0) (sbc)\n"
+            "    --noflush    no flush prior to operation that limits "
+            "access (sbc)\n"
+            "    --pc=PC    power condition (in hex, default 0 -> no "
+            "power condition)\n"
+            "               1 -> active, 2 -> idle, 3 -> standby, "
+            "5 -> sleep (mmc)\n"
+            "    --readonly|-r    open DEVICE read-only (def: read-write)\n"
+            "                     recommended if DEVICE is ATA disk\n"
+            "    --start    start unit (same as '1'), default "
+            "action\n"
+            "    --stop     stop unit (same as '0')\n"
+            "    -v         verbose (print out SCSI commands)\n"
+            "    --new|-N   use new interface\n"
+            "    -V         print version string then exit\n\n"
+            "    Example: 'sg_start --stop /dev/sdb'    stops unit\n"
+            "             'sg_start --eject /dev/scd0'  stops unit and "
+            "ejects medium\n\n"
+            "Performs a SCSI START STOP UNIT command\n"
+            );
+}
+
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int c, n, err;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "ef:hilLm:nNOp:rsSvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'e':
+            op->do_eject = true;
+            op->do_loej = true;
+            break;
+        case 'f':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 3)) {
+                pr2serr("bad argument to '--fl='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_loej = true;
+            op->do_start = true;
+            op->do_fl = n;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'i':
+            op->do_immed = true;
+            break;
+        case 'l':
+            op->do_load = true;
+            op->do_loej = true;
+            break;
+        case 'L':
+            op->do_loej = true;
+            break;
+        case 'm':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 15)) {
+                pr2serr("bad argument to '--mod='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_mod = n;
+            break;
+        case 'n':
+            op->do_noflush = true;
+            break;
+        case 'N':
+            break;      /* ignore */
+        case 'O':
+            op->opt_new = false;
+            return 0;
+        case 'p':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 15)) {
+                pr2serr("bad argument to '--pc='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_pc = n;
+            break;
+        case 'r':
+            op->do_readonly = true;
+            break;
+        case 's':
+            op->do_start = true;
+            break;
+        case 'S':
+            op->do_stop = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    err = 0;
+    for (; optind < argc; ++optind) {
+        if (1 == strlen(argv[optind])) {
+            if (0 == strcmp("0", argv[optind])) {
+                op->do_stop = true;
+                continue;
+            } else if (0 == strcmp("1", argv[optind])) {
+                op->do_start = true;
+                continue;
+            }
+        }
+        if (NULL == op->device_name)
+            op->device_name = argv[optind];
+        else {
+            pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            ++err;
+        }
+    }
+    if (err) {
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    } else
+        return 0;
+}
+
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool ambigu = false;
+    bool jmp_out;
+    bool startstop = false;
+    bool startstop_set = false;
+    int k, plen, num;
+    unsigned int u;
+    const char * cp;
+
+    for (k = 1; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0;
+                 --plen, ++cp) {
+                switch (*cp) {
+                case 'i':
+                    if ('\0' == *(cp + 1))
+                        op->do_immed = true;
+                    else
+                        jmp_out = true;
+                    break;
+                case 'r':
+                    op->do_readonly = true;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                case 'h':
+                case '?':
+                    ++op->do_help;
+                    break;
+                case 'N':
+                    op->opt_new = true;
+                    return 0;
+                case 'O':
+                    break;
+                case '-':
+                    ++cp;
+                    --plen;
+                    jmp_out = true;
+                    break;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+
+            if (0 == strncmp(cp, "eject", 5)) {
+                op->do_loej = true;
+                if (startstop_set && startstop)
+                    ambigu = true;
+                else {
+                    startstop = false;
+                    startstop_set = true;
+                }
+            } else if (0 == strncmp("fl=", cp, 3)) {
+                num = sscanf(cp + 3, "%x", &u);
+                if (1 != num) {
+                    pr2serr("Bad value after 'fl=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                startstop = true;
+                startstop_set = true;
+                op->do_loej = true;
+                op->do_fl = u;
+            } else if (0 == strncmp("imm=", cp, 4)) {
+                num = sscanf(cp + 4, "%x", &u);
+                if ((1 != num) || (u > 1)) {
+                    pr2serr("Bad value after 'imm=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->do_immed = !! u;
+            } else if (0 == strncmp(cp, "load", 4)) {
+                op->do_loej = true;
+                if (startstop_set && (! startstop))
+                    ambigu = true;
+                else {
+                    startstop = true;
+                    startstop_set = true;
+                }
+            } else if (0 == strncmp(cp, "loej", 4))
+                op->do_loej = true;
+            else if (0 == strncmp("pc=", cp, 3)) {
+                num = sscanf(cp + 3, "%x", &u);
+                if ((1 != num) || (u > 15)) {
+                    pr2serr("Bad value after after 'pc=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->do_pc = u;
+            } else if (0 == strncmp("mod=", cp, 4)) {
+                num = sscanf(cp + 3, "%x", &u);
+                if (1 != num) {
+                    pr2serr("Bad value after 'mod=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->do_mod = u;
+            } else if (0 == strncmp(cp, "noflush", 7)) {
+                op->do_noflush = true;
+            } else if (0 == strncmp(cp, "start", 5)) {
+                if (startstop_set && (! startstop))
+                    ambigu = true;
+                else {
+                    startstop = true;
+                    startstop_set = true;
+                }
+            } else if (0 == strncmp(cp, "stop", 4)) {
+                if (startstop_set && startstop)
+                    ambigu = true;
+                else {
+                    startstop = false;
+                    startstop_set = true;
+                }
+            } else if (0 == strncmp(cp, "old", 3))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage_old();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp("0", cp)) {
+            if (startstop_set && startstop)
+                ambigu = true;
+            else {
+                startstop = false;
+                startstop_set = true;
+            }
+        } else if (0 == strcmp("1", cp)) {
+            if (startstop_set && (! startstop))
+                ambigu = true;
+            else {
+                startstop = true;
+                startstop_set = true;
+            }
+        } else if (0 == op->device_name)
+                op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not "
+                    "expecting: %s\n", op->device_name, cp);
+            usage_old();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if (ambigu) {
+            pr2serr("please, only one of 0, 1, --eject, "
+                    "--load, --start or --stop\n");
+            usage_old();
+            return SG_LIB_CONTRADICT;
+        } else if (startstop_set) {
+            if (startstop)
+                op->do_start = true;
+            else
+                op->do_stop = true;
+        }
+    }
+    return 0;
+}
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opt_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opt_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opt_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (! op->opt_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    int res;
+    int sg_fd = -1;
+    int ret = 0;
+    struct opts_t opts;
+    struct opts_t * op;
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->do_fl = -1;    /* only when >= 0 set FL bit */
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return res;
+    if (op->do_help) {
+        if (op->opt_new)
+            usage();
+        else
+            usage_old();
+        return 0;
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+
+    if (op->do_start && op->do_stop) {
+        pr2serr("Ambiguous to give both '--start' and '--stop'\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (op->do_load && op->do_eject) {
+        pr2serr("Ambiguous to give both '--load' and '--eject'\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (op->do_load)
+       op->do_start = true;
+    else if ((op->do_eject) || op->do_stop)
+       op->do_start = false;
+    else if (op->opt_new && op->do_loej && (! op->do_start))
+        op->do_start = true;      /* --loej alone in new interface is load */
+    else if ((! op->do_loej) && (-1 == op->do_fl) && (0 == op->do_pc))
+       op->do_start = true;
+    /* default action is to start when no other active options */
+
+    if (0 == op->device_name) {
+        pr2serr("No DEVICE argument given\n");
+        if (op->opt_new)
+            usage();
+        else
+            usage_old();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (op->do_fl >= 0) {
+        if (! op->do_start) {
+            pr2serr("Giving '--fl=FL' with '--stop' (or '--eject') is "
+                    "invalid\n");
+            return SG_LIB_CONTRADICT;
+        }
+        if (op->do_pc > 0) {
+            pr2serr("Giving '--fl=FL' with '--pc=PC' when PC is non-zero "
+                    "is invalid\n");
+            return SG_LIB_CONTRADICT;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(op->device_name, op->do_readonly,
+                                op->verbose);
+    if (sg_fd < 0) {
+        if (op->verbose)
+            pr2serr("Error trying to open %s: %s\n", op->device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    if (op->do_fl >= 0)
+        res = sg_ll_start_stop_unit(sg_fd, op->do_immed, op->do_fl, 0 /* pc */,
+                                    true /* fl */, true /* loej */,
+                                    true /*start */, true /* noisy */,
+                                    op->verbose);
+    else if (op->do_pc > 0)
+        res = sg_ll_start_stop_unit(sg_fd, op->do_immed, op->do_mod,
+                                    op->do_pc, op->do_noflush, false, false,
+                                    true, op->verbose);
+    else
+        res = sg_ll_start_stop_unit(sg_fd, op->do_immed, 0, false,
+                                    op->do_noflush, op->do_loej,
+                                    op->do_start, true, op->verbose);
+    ret = res;
+    if (res) {
+        if (op->verbose < 2) {
+            char b[80];
+
+            sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+            pr2serr("%s\n", b);
+        }
+        pr2serr("START STOP UNIT command failed\n");
+    }
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == op->verbose) {
+        if (! sg_if_can2stderr("sg_start failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_stpg.c b/src/sg_stpg.c
new file mode 100644
index 0000000..ce21c9c
--- /dev/null
+++ b/src/sg_stpg.c
@@ -0,0 +1,726 @@
+/*
+ * Copyright (c) 2004-2022 Hannes Reinecke, Christophe Varoqui, Douglas Gilbert
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI command SET TARGET PORT GROUPS
+ * to the given SCSI device.
+ */
+
+static const char * version_str = "1.21 20220118";
+
+#define TGT_GRP_BUFF_LEN 1024
+#define MX_ALLOC_LEN (0xc000 + 0x80)
+
+#define TPGS_STATE_OPTIMIZED 0x0
+#define TPGS_STATE_NONOPTIMIZED 0x1
+#define TPGS_STATE_STANDBY 0x2
+#define TPGS_STATE_UNAVAILABLE 0x3
+#define TPGS_STATE_OFFLINE 0xe          /* SPC-4 rev 9 */
+#define TPGS_STATE_TRANSITIONING 0xf
+
+/* See also table 306 - Target port group descriptor format in SPC-4 rev 36e */
+#ifdef __cplusplus
+
+// C++ does not support designated initializers
+static const uint8_t state_sup_mask[] = {
+    0x1, 0x2, 0x4, 0x8, 0x0, 0x0, 0x0, 0x0,
+    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x80,
+};
+
+#else
+
+static const uint8_t state_sup_mask[] = {
+        [TPGS_STATE_OPTIMIZED]     = 0x01,
+        [TPGS_STATE_NONOPTIMIZED]  = 0x02,
+        [TPGS_STATE_STANDBY]       = 0x04,
+        [TPGS_STATE_UNAVAILABLE]   = 0x08,
+        [TPGS_STATE_OFFLINE]       = 0x40,
+        [TPGS_STATE_TRANSITIONING] = 0x80,
+};
+
+#endif  /* C or C++ ? */
+
+#define VPD_DEVICE_ID  0x83
+#define DEF_VPD_DEVICE_ID_LEN  252
+
+#define MAX_PORT_LIST_ARR_LEN 16
+
+struct tgtgrp {
+        int id;
+        int current;
+        int valid;
+};
+
+static struct option long_options[] = {
+        {"active", no_argument, 0, 'a'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"offline", no_argument, 0, 'l'},
+        {"optimized", no_argument, 0, 'o'},
+        {"raw", no_argument, 0, 'r'},
+        {"standby", no_argument, 0, 's'},
+        {"state", required_argument, 0, 'S'},
+        {"tp", required_argument, 0, 't'},
+        {"unavailable", no_argument, 0, 'u'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_stpg   [--active] [--help] [--hex] [--offline] "
+            "[--optimized] [--raw]\n"
+            "                 [--standby] [--state=S,S...] [--tp=P,P...] "
+            "[--unavailable]\n"
+            "                 [--verbose] [--version] DEVICE\n"
+            "  where:\n"
+            "    --active|-a        set asymm. access state to "
+            "active/non-optimized\n"
+            "    --help|-h          print out usage message\n"
+            "    --hex|-H           print out report response in hex, then "
+            "exit\n"
+            "    --offline|-l|-O    set asymm. access state to offline, takes "
+            "relative\n"
+            "                       target port id, rather than target port "
+            "group id\n"
+            "    --optimized|-o     set asymm. access state to "
+            "active/optimized\n"
+            "    --raw|-r           output report response in binary to "
+            "stdout, then exit\n"
+            "    --standby|-s       set asymm. access state to standby\n"
+            "    --state=S,S.. |-S S,S...     list of states (values or "
+            "acronyms)\n"
+            "    --tp=P,P.. |-t P,P...        list of target port group "
+            "identifiers,\n"
+            "                                 or relative target port "
+            "identifiers\n"
+            "    --unavailable|-u   set asymm. access state to unavailable\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Performs a SCSI SET TARGET PORT GROUPS command\n");
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+static int
+decode_target_port(uint8_t * buff, int len, int *d_id, int *d_tpg)
+{
+    int c_set, assoc, desig_type, i_len, off;
+    const uint8_t * bp;
+    const uint8_t * ip;
+
+    *d_id = -1;
+    *d_tpg = -1;
+    off = -1;
+    while (sg_vpd_dev_id_iter(buff, len, &off, -1, -1, -1) == 0) {
+        bp = buff + off;
+        i_len = bp[3];
+        if ((off + i_len + 4) > len) {
+            pr2serr("    VPD page error: designator length longer than\n     "
+                    "remaining response length=%d\n", (len - off));
+            return SG_LIB_CAT_MALFORMED;
+        }
+        ip = bp + 4;
+        c_set = (bp[0] & 0xf);
+        /* piv = ((bp[1] & 0x80) ? 1 : 0); */
+        assoc = ((bp[1] >> 4) & 0x3);
+        desig_type = (bp[1] & 0xf);
+        switch (desig_type) {
+        case 4: /* Relative target port */
+            if ((1 != c_set) || (1 != assoc) || (4 != i_len)) {
+                pr2serr("      << expected binary code_set, target port "
+                        "association, length 4>>\n");
+                hex2stderr(ip, i_len, 0);
+                break;
+            }
+            *d_id = sg_get_unaligned_be16(ip + 2);
+            break;
+        case 5: /* (primary) Target port group */
+            if ((1 != c_set) || (1 != assoc) || (4 != i_len)) {
+                pr2serr("      << expected binary code_set, target port "
+                        "association, length 4>>\n");
+                hex2stderr(ip, i_len, 0);
+                break;
+            }
+            *d_tpg = sg_get_unaligned_be16(ip + 2);
+            break;
+        default:
+            break;
+        }
+    }
+    if (-1 == *d_id || -1 == *d_tpg) {
+        pr2serr("VPD page error: no target port group information\n");
+        return SG_LIB_CAT_MALFORMED;
+    }
+    return 0;
+}
+
+static void
+decode_tpgs_state(const int st)
+{
+    switch (st) {
+    case TPGS_STATE_OPTIMIZED:
+        printf(" (active/optimized)");
+        break;
+    case TPGS_STATE_NONOPTIMIZED:
+        printf(" (active/non optimized)");
+        break;
+    case TPGS_STATE_STANDBY:
+        printf(" (standby)");
+        break;
+    case TPGS_STATE_UNAVAILABLE:
+        printf(" (unavailable)");
+        break;
+    case TPGS_STATE_OFFLINE:
+        printf(" (offline)");
+        break;
+    case TPGS_STATE_TRANSITIONING:
+        printf(" (transitioning between states)");
+        break;
+    default:
+        printf(" (unknown: 0x%x)", st);
+        break;
+    }
+}
+
+static int
+transition_tpgs_states(struct tgtgrp *tgtState, int numgrp, int portgroup,
+                       int newstate)
+{
+     int i,oldstate;
+
+     for ( i = 0; i < numgrp; i++) {
+          if (tgtState[i].id == portgroup)
+               break;
+     }
+     if (i == numgrp) {
+          printf("Portgroup 0x%02x does not exist\n", portgroup);
+          return 1;
+     }
+
+     if (!( state_sup_mask[newstate] & tgtState[i].valid )) {
+          printf("Portgroup 0x%02x: Invalid state 0x%x\n",
+                 portgroup, newstate);
+          return 1;
+     }
+     oldstate = tgtState[i].current;
+     tgtState[i].current = newstate;
+     if (newstate == TPGS_STATE_OPTIMIZED) {
+          /* Switch with current optimized path */
+          for ( i = 0; i < numgrp; i++) {
+               if (tgtState[i].id == portgroup)
+                    continue;
+               if (tgtState[i].current == TPGS_STATE_OPTIMIZED)
+                    tgtState[i].current = oldstate;
+          }
+     } else if (oldstate == TPGS_STATE_OPTIMIZED) {
+          /* Enable next path group */
+          for ( i = 0; i < numgrp; i++) {
+               if (tgtState[i].id == portgroup)
+                    continue;
+               if (tgtState[i].current == TPGS_STATE_NONOPTIMIZED) {
+                    tgtState[i].current = TPGS_STATE_OPTIMIZED;
+                    break;
+               }
+          }
+     }
+     printf("New target port groups:\n");
+     for (i = 0; i < numgrp; i++) {
+            printf("  target port group id : 0x%x\n",
+                   tgtState[i].id);
+            printf("    target port group asymmetric access state : ");
+            printf("0x%02x\n", tgtState[i].current);
+     }
+     return 0;
+}
+
+static void
+encode_tpgs_states(uint8_t *buff, struct tgtgrp *tgtState, int numgrp)
+{
+     int i;
+     uint8_t *desc;
+
+     for (i = 0, desc = buff + 4; i < numgrp; desc += 4, i++) {
+          desc[0] = tgtState[i].current & 0x0f;
+          sg_put_unaligned_be16((uint16_t)tgtState[i].id, desc + 2);
+     }
+}
+
+/* Read numbers (up to 32 bits in size) from command line (comma separated
+ * list). Assumed decimal unless prefixed by '0x', '0X' or contains trailing
+ * 'h' or 'H' (which indicate hex). Returns 0 if ok, else error code. */
+static int
+build_port_arr(const char * inp, int * port_arr, int * port_arr_len,
+               int max_arr_len)
+{
+    int in_len, k;
+    const char * lcp;
+    int v;
+    char * cp;
+
+    if ((NULL == inp) || (NULL == port_arr) ||
+        (NULL == port_arr_len))
+        return SG_LIB_LOGIC_ERROR;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *port_arr_len = 0;
+    k = strspn(inp, "0123456789aAbBcCdDeEfFhHxX,");
+    if (in_len != k) {
+        pr2serr("%s: error at pos %d\n", __func__, k + 1);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    for (k = 0; k < max_arr_len; ++k) {
+        v = sg_get_num_nomult(lcp);
+        if (-1 != v) {
+            port_arr[k] = v;
+            cp = (char *)strchr(lcp, ',');
+            if (NULL == cp)
+                break;
+            lcp = cp + 1;
+        } else {
+            pr2serr("%s: error at pos %d\n", __func__, (int)(lcp - inp + 1));
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    *port_arr_len = k + 1;
+    if (k == max_arr_len) {
+        pr2serr("%s: array length exceeded\n", __func__);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    return 0;
+}
+
+/* Read numbers (up to 32 bits in size) from command line (comma separated
+ * list). Assumed decimal unless prefixed by '0x', '0X' or contains trailing
+ * 'h' or 'H' (which indicate hex). Also accepts 'ao' for active optimized
+ * [0], 'an' for active/non-optimized [1], 's' for standby [2], 'u' for
+ * unavailable [3], 'o' for offline [14]. Returns 0 if ok, else error code. */
+static int
+build_state_arr(const char * inp, int * state_arr, int * state_arr_len,
+                int max_arr_len)
+{
+    bool try_num;
+    int in_len, k, v;
+    const char * lcp;
+    char * cp;
+
+    if ((NULL == inp) || (NULL == state_arr) ||
+        (NULL == state_arr_len))
+        return SG_LIB_LOGIC_ERROR;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *state_arr_len = 0;
+    k = strspn(inp, "0123456789aAbBcCdDeEfFhHnNoOsSuUxX,");
+    if (in_len != k) {
+        pr2serr("%s: error at pos %d\n", __func__, k + 1);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    for (k = 0; k < max_arr_len; ++k) {
+        try_num = true;
+        if (isalpha((uint8_t)*lcp)) {
+            try_num = false;
+            switch (toupper((uint8_t)*lcp)) {
+            case 'A':
+                if ('N' == toupper((uint8_t)*(lcp + 1)))
+                    state_arr[k] = 1;
+                else if ('O' == toupper((uint8_t)*(lcp + 1)))
+                    state_arr[k] = 0;
+                else
+                    try_num = true;
+                break;
+            case 'O':
+                state_arr[k] = 14;
+                break;
+            case 'S':
+                state_arr[k] = 2;
+                break;
+            case 'U':
+                state_arr[k] = 3;
+                break;
+            default:
+                pr2serr("%s: expected 'ao', 'an', 'o', 's' or 'u' at pos "
+                        "%d\n", __func__, (int)(lcp - inp + 1));
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+        if (try_num) {
+            v = sg_get_num_nomult(lcp);
+            if (((v >= 0) && (v <= 3)) || (14 ==v))
+                state_arr[k] = v;
+            else if (-1 == v) {
+                pr2serr("%s: error at pos %d\n", __func__,
+                        (int)(lcp - inp + 1));
+                return SG_LIB_SYNTAX_ERROR;
+            } else {
+                pr2serr("%s: expect 0,1,2,3 or 14\n", __func__);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+        cp = (char *)strchr(lcp, ',');
+        if (NULL == cp)
+            break;
+        lcp = cp + 1;
+    }
+    *state_arr_len = k + 1;
+    if (k == max_arr_len) {
+        pr2serr("%s: array length exceeded\n", __func__);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool hex = false;
+    bool raw = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int k, off, res, c, report_len, tgt_port_count;
+    int sg_fd = -1;
+    int port_arr_len = 0;
+    int verbose = 0;
+    uint8_t reportTgtGrpBuff[TGT_GRP_BUFF_LEN];
+    uint8_t setTgtGrpBuff[TGT_GRP_BUFF_LEN];
+    uint8_t rsp_buff[MX_ALLOC_LEN + 2];
+    uint8_t * bp;
+    struct tgtgrp tgtGrpState[256], *tgtStatePtr;
+    int state = -1;
+    const char * state_arg = NULL;
+    const char * tp_arg = NULL;
+    int port_arr[MAX_PORT_LIST_ARR_LEN];
+    int state_arr[MAX_PORT_LIST_ARR_LEN];
+    char b[80];
+    int state_arr_len = 0;
+    int portgroup = -1;
+    int relport = -1;
+    int numgrp = 0;
+    const char * device_name = NULL;
+    int ret = 0;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "ahHloOrsS:t:uvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            state = TPGS_STATE_NONOPTIMIZED;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            hex = true;
+            break;
+        case 'l':
+        case 'O':
+            state = TPGS_STATE_OFFLINE;
+            break;
+        case 'o':
+            state = TPGS_STATE_OPTIMIZED;
+            break;
+        case 'r':
+            raw = true;
+            break;
+        case 's':
+            state = TPGS_STATE_STANDBY;
+            break;
+        case 'S':
+            state_arg = optarg;
+            break;
+        case 't':
+            tp_arg = optarg;
+            break;
+        case 'u':
+            state = TPGS_STATE_UNAVAILABLE;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("Version: %s\n", version_str);
+        return 0;
+    }
+
+    if (state_arg) {
+        if ((ret = build_state_arr(state_arg, state_arr, &state_arr_len,
+                                   MAX_PORT_LIST_ARR_LEN))) {
+            usage();
+            return ret;
+        }
+    }
+    if (tp_arg) {
+        if ((ret = build_port_arr(tp_arg, port_arr, &port_arr_len,
+                                  MAX_PORT_LIST_ARR_LEN))) {
+            usage();
+            return ret;
+        }
+    }
+    if ((state >= 0) && (state_arr_len > 0)) {
+        pr2serr("either use individual state option or '--state=' but not "
+                "both\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((0 == state_arr_len) && (0 == port_arr_len) && (-1 == state))
+        state = 0;      /* default to active/optimized */
+    if ((1 == state_arr_len) && (0 == port_arr_len) && (-1 == state)) {
+        state = state_arr[0];
+        state_arr_len = 0;
+    }
+    if (state_arr_len > port_arr_len) {
+        pr2serr("'state=' list longer than expected\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((port_arr_len > 0) && (0 == state_arr_len)) {
+        if (-1 == state) {
+            pr2serr("target port list given but no state indicated\n");
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        state_arr[0] = state;
+        state_arr_len = 1;
+        state = -1;
+    }
+    if ((port_arr_len > 1) && (1 == state_arr_len)) {
+        for (k = 1; k < port_arr_len; ++k)
+            state_arr[k] = state_arr[0];
+        state_arr_len = port_arr_len;
+    }
+    if (port_arr_len != state_arr_len) {
+        pr2serr("'state=' and '--tp=' lists mismatched\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    if (0 == port_arr_len) {
+        res = sg_ll_inquiry(sg_fd, false, true /* EVPD */, VPD_DEVICE_ID,
+                            rsp_buff, DEF_VPD_DEVICE_ID_LEN, true, verbose);
+        if (0 == res) {
+            report_len = sg_get_unaligned_be16(rsp_buff + 2) + 4;
+            if (VPD_DEVICE_ID != rsp_buff[1]) {
+                pr2serr("invalid VPD response; probably a STANDARD INQUIRY "
+                        "response\n");
+                if (verbose) {
+                    pr2serr("First 32 bytes of bad response\n");
+                    hex2stderr(rsp_buff, 32, 0);
+                }
+                return SG_LIB_CAT_MALFORMED;
+            }
+            if (report_len > MX_ALLOC_LEN) {
+                pr2serr("response length too long: %d > %d\n", report_len,
+                        MX_ALLOC_LEN);
+                return SG_LIB_CAT_MALFORMED;
+            } else if (report_len > DEF_VPD_DEVICE_ID_LEN) {
+                if (sg_ll_inquiry(sg_fd, false, true, VPD_DEVICE_ID, rsp_buff,
+                                  report_len, true, verbose))
+                    return SG_LIB_CAT_OTHER;
+            }
+            decode_target_port(rsp_buff + 4, report_len - 4, &relport,
+                               &portgroup);
+            printf("Device is at port Group 0x%02x, relative port 0x%02x\n",
+                   portgroup, relport);
+        }
+
+        memset(reportTgtGrpBuff, 0x0, sizeof(reportTgtGrpBuff));
+        /* trunc = 0; */
+
+        res = sg_ll_report_tgt_prt_grp2(sg_fd, reportTgtGrpBuff,
+                                        sizeof(reportTgtGrpBuff),
+                                        false /* extended */, true, verbose);
+        ret = res;
+        if (0 == res) {
+            report_len = sg_get_unaligned_be32(reportTgtGrpBuff + 0) + 4;
+            if (report_len > (int)sizeof(reportTgtGrpBuff)) {
+                /* trunc = 1; */
+                pr2serr("  <<report too long for internal buffer, output "
+                        "truncated\n");
+                report_len = (int)sizeof(reportTgtGrpBuff);
+            }
+            if (raw) {
+                dStrRaw(reportTgtGrpBuff, report_len);
+                goto err_out;
+            }
+            if (verbose)
+                printf("Report list length = %d\n", report_len);
+            if (hex) {
+                if (verbose)
+                    printf("\nOutput response in hex:\n");
+                hex2stdout(reportTgtGrpBuff, report_len, 1);
+                goto err_out;
+            }
+            memset(tgtGrpState, 0, sizeof(struct tgtgrp) * 256);
+            tgtStatePtr = tgtGrpState;
+            printf("Current target port groups:\n");
+            for (k = 4, bp = reportTgtGrpBuff + 4, numgrp = 0; k < report_len;
+                 k += off, bp += off, numgrp ++) {
+
+                printf("  target port group id : 0x%x , Pref=%d\n",
+                       sg_get_unaligned_be16(bp + 2), !!(bp[0] & 0x80));
+                printf("    target port group asymmetric access state : ");
+                printf("0x%02x", bp[0] & 0x0f);
+                printf("\n");
+                tgtStatePtr->id = sg_get_unaligned_be16(bp + 2);
+                tgtStatePtr->current = bp[0] & 0x0f;
+                tgtStatePtr->valid = bp[1];
+
+                tgt_port_count = bp[7];
+
+                tgtStatePtr++;
+                off = 8 + tgt_port_count * 4;
+            }
+        } else {
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("Report Target Port Groups: %s\n", b);
+            if (0 == verbose)
+                pr2serr("    try '-v' for more information\n");
+        }
+        if (0 != res)
+             goto err_out;
+
+        printf("Port group 0x%02x: Set asymmetric access state to", portgroup);
+        decode_tpgs_state(state);
+        printf("\n");
+
+        transition_tpgs_states(tgtGrpState, numgrp, portgroup, state);
+
+        memset(setTgtGrpBuff, 0x0, sizeof(setTgtGrpBuff));
+        /* trunc = 0; */
+
+        encode_tpgs_states(setTgtGrpBuff, tgtGrpState, numgrp);
+        report_len = numgrp * 4 + 4;
+    } else { /* port_arr_len > 0 */
+        memset(setTgtGrpBuff, 0x0, sizeof(setTgtGrpBuff));
+        for (k = 0, bp = setTgtGrpBuff + 4; k < port_arr_len; ++k, bp +=4) {
+            bp[0] = state_arr[k] & 0xf;
+            sg_put_unaligned_be16((uint16_t)port_arr[k], bp + 2);
+        }
+        report_len = port_arr_len * 4 + 4;
+    }
+
+    res = sg_ll_set_tgt_prt_grp(sg_fd, setTgtGrpBuff, report_len, true,
+                                verbose);
+
+    if (0 == res)
+        goto err_out;
+    else {
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Set Target Port Groups: %s\n", b);
+        if (0 == verbose)
+            pr2serr("    try '-v' for more information\n");
+    }
+
+err_out:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_stpg failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_stream_ctl.c b/src/sg_stream_ctl.c
new file mode 100644
index 0000000..77f78eb
--- /dev/null
+++ b/src/sg_stream_ctl.c
@@ -0,0 +1,517 @@
+/*
+ * Copyright (c) 2018-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#include <errno.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/*
+ * This program issues the SCSI STREAM CONTROL or GET STREAM STATUS command
+ * to the given SCSI device. Based on sbc4r15.pdf .
+ */
+
+static const char * version_str = "1.13 20221028";
+
+#define STREAM_CONTROL_SA 0x14
+#define GET_STREAM_STATUS_SA 0x16
+
+#define STREAM_CONTROL_OPEN 0x1
+#define STREAM_CONTROL_CLOSE 0x2
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+
+static struct option long_options[] = {
+        {"brief", no_argument, 0, 'b'},
+        {"close", no_argument, 0, 'c'},
+        {"ctl", required_argument, 0, 'C'},
+        {"get", no_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"id", required_argument, 0, 'i'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"open", no_argument, 0, 'o'},
+        {"readonly", no_argument, 0, 'r'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_stream_ctl  [-brief] [--close] [--ctl=CTL] [-get] [--help]\n"
+            "                      [--id=SID] [--maxlen=LEN] [--open] "
+            "[--readonly]\n"
+            "                      [--verbose] [--version] DEVICE\n");
+    pr2serr("  where:\n"
+            "    --brief|-b          for open, output assigned stream id to "
+            "stdout, or\n"
+            "                        -1 if error; for close, output 0, or "
+            "-1; for get\n"
+            "                        output list of stream id, 1 per line\n"
+            "    --close|-c          close stream given by --id=SID\n"
+            "    --ctl=CTL|-C CTL    CTL is stream control value, "
+            "(STR_CTL field)\n"
+            "                        1 -> open; 2 -> close\n"
+            "    --get|-g            do GET STREAM STATUS command (default "
+            "if no other)\n"
+            "    --help|-h           print out usage message\n"
+            "    --id=SID|-i SID     for close, SID is stream_id to close; "
+            "for get,\n"
+            "                        list from and include this stream id\n"
+            "    --maxlen=LEN|-m LEN    length in bytes of buffer to "
+            "receive data-in\n"
+            "                           (def: 8 (for open and close); 252 "
+            "(for get,\n"
+            "                           but increase if needed)\n"
+            "    --open|-o           open a new stream, return assigned "
+            "stream id\n"
+            "    --readonly|-r       open DEVICE read-only (if supported)\n"
+            "    --verbose|-v        increase verbosity\n"
+            "    --version|-V        print version string and exit\n\n"
+            "Performs a SCSI STREAM CONTROL or GET STREAM STATUS command. "
+            "If --open,\n--close or --ctl=CTL given (only one) then "
+            "performs STREAM CONTROL\ncommand. If --get or no other "
+            "selecting option given then performs a\nGET STREAM STATUS "
+            "command. A successful --open will output the assigned\nstream "
+            "id to stdout (and ignore --id=SID , if given).\n"
+           );
+}
+
+/* Invokes a SCSI GET STREAM STATUS command (SBC-4).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_get_stream_status(int sg_fd, uint16_t s_str_id, uint8_t * resp,
+                        uint32_t alloc_len, int * residp, bool noisy,
+                        int verbose)
+{
+    int k, ret, res, sense_cat;
+    uint8_t gssCdb[16] = {SG_SERVICE_ACTION_IN_16,
+           GET_STREAM_STATUS_SA, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+    static const char * const cmd_name = "Get stream status";
+
+    if (s_str_id)         /* starting stream id, fetch from and including */
+        sg_put_unaligned_be16(s_str_id, gssCdb + 4);
+    sg_put_unaligned_be32(alloc_len, gssCdb + 10);
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", cmd_name,
+                sg_get_command_str(gssCdb, (int)sizeof(gssCdb), false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj_with_fd(sg_fd, verbose);
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", cmd_name);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, gssCdb, sizeof(gssCdb));
+    set_scsi_pt_data_in(ptvp, resp, alloc_len);
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, -1, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cmd_name, res, noisy, verbose,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    k = ret ? (int)alloc_len : get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = k;
+    if ((verbose > 2) && ((alloc_len - k) > 0)) {
+        pr2serr("%s: parameter data returned:\n", cmd_name);
+        hex2stderr((const uint8_t *)resp, alloc_len - k,
+                   ((verbose > 3) ? -1 : 1));
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI STREAM CONTROL command (SBC-4).  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors.
+ * N.B. The is a device modifying command that is SERVICE ACTION IN(16)
+ * command since it has data-in buffer that for open returns the
+ * ASSIGNED_STR_ID field . */
+static int
+sg_ll_stream_control(int sg_fd, uint32_t str_ctl, uint16_t str_id,
+                     uint8_t * resp, uint32_t alloc_len, int * residp,
+                     bool noisy, int verbose)
+{
+    int k, ret, res, sense_cat;
+    uint8_t scCdb[16] = {SG_SERVICE_ACTION_IN_16,
+           STREAM_CONTROL_SA, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+    static const char * const cmd_name = "Stream control";
+
+    if (str_ctl)
+        scCdb[1] |= (str_ctl & 0x3) << 5;
+    if (str_id)         /* Only used for close, stream id to close */
+        sg_put_unaligned_be16(str_id, scCdb + 4);
+    sg_put_unaligned_be32(alloc_len, scCdb + 10);
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", cmd_name,
+                sg_get_command_str(scCdb, (int)sizeof(scCdb), false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj_with_fd(sg_fd, verbose);
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", cmd_name);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, scCdb, sizeof(scCdb));
+    set_scsi_pt_data_in(ptvp, resp, alloc_len);
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, -1, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, cmd_name, res, noisy, verbose,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    k = ret ? (int)alloc_len : get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = k;
+    if ((verbose > 2) && ((alloc_len - k) > 0)) {
+        pr2serr("%s: parameter data returned:\n", cmd_name);
+        hex2stderr((const uint8_t *)resp, alloc_len - k,
+                   ((verbose > 3) ? -1 : 1));
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool do_brief = false;
+    bool do_close = false;
+    bool do_get = false;
+    bool do_open = false;
+    bool ctl_given = false;
+    bool maxlen_given = false;
+    bool read_only = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int c, k, res, resid;
+    int sg_fd = -1;
+    int maxlen = 0;
+    int ret = 0;
+    int verbose = 0;
+    uint16_t stream_id = 0;
+    uint16_t num_streams = 0;
+    uint32_t ctl = 0;
+    uint32_t pg_sz = sg_get_page_size();
+    uint32_t param_dl;
+    const char * device_name = NULL;
+    const char * cmd_name = NULL;
+    uint8_t * arr = NULL;
+    uint8_t * free_arr = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "bcC:ghi:m:orvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            do_brief = true;
+            break;
+        case 'c':
+            do_close = true;
+            break;
+        case 'C':
+            if ((1 != sscanf(optarg, "%4u", &ctl)) || (ctl > 3)) {
+                pr2serr("--ctl= expects a number from 0 to 3\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            ctl_given = true;
+            break;
+        case 'g':
+            do_get = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+            k = sg_get_num(optarg);
+            if ((k < 0) || (k > (int)UINT16_MAX)) {
+                pr2serr("--id= expects a number from 0 to 65535\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            stream_id = (uint16_t)k;
+            break;
+        case 'm':
+            k = sg_get_num(optarg);
+            if (k < 0) {
+                pr2serr("--maxlen= unable to decode argument\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            maxlen_given = true;
+            if (k > 0)
+                maxlen = k;
+            break;
+        case 'o':
+            do_open = true;
+            break;
+        case 'r':
+            read_only = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    k = (int)do_close + (int)do_get + (int)do_open + (int)ctl_given;
+    if (k > 1) {
+        pr2serr("Can only have one of: --close, --ctl==, --get, or --open\n");
+        return SG_LIB_CONTRADICT;
+    } else if (0 == k)
+        do_get = true;
+    if (do_close)
+        ctl = STREAM_CONTROL_CLOSE;
+    else if (do_open)
+        ctl = STREAM_CONTROL_OPEN;
+
+    if (maxlen_given) {
+        if (0 == maxlen)
+            maxlen = do_get ? 248 : 8;
+    } else
+        maxlen = do_get ? 248 : 8;
+
+    if (verbose) {
+        if (read_only && (! do_get))
+            pr2serr("Probably need to open %s read-write\n", device_name);
+        if (do_open && (stream_id > 0))
+            pr2serr("With --open the --id-SID option is ignored\n");
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, read_only, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    if (maxlen > (int)pg_sz)
+        arr = sg_memalign(maxlen, pg_sz, &free_arr, verbose > 3);
+    else
+        arr = sg_memalign(pg_sz, pg_sz, &free_arr, verbose > 3);
+    if (NULL == arr) {
+        pr2serr("Unable to allocate space for response\n");
+        ret = sg_convert_errno(ENOMEM);
+        goto fini;
+    }
+
+    resid = 0;
+    if (do_get) {       /* Get stream status */
+        cmd_name = "Get stream status";
+        ret = sg_ll_get_stream_status(sg_fd, stream_id, arr, maxlen,
+                                      &resid, false, verbose);
+        if (ret) {
+            if (SG_LIB_CAT_INVALID_OP == ret)
+                pr2serr("%s command not supported\n", cmd_name);
+            else {
+                char b[80];
+
+                sg_get_category_sense_str(ret, sizeof(b), b, verbose);
+                pr2serr("%s command: %s\n", cmd_name, b);
+            }
+            goto fini;
+        }
+        if ((maxlen - resid) < 8) {
+            pr2serr("Response too short (%d bytes) assigned stream id\n",
+                        k);
+            printf("-1\n");
+            ret = SG_LIB_CAT_MALFORMED;
+            goto fini;
+        } else
+            maxlen -= resid;
+        param_dl = sg_get_unaligned_be32(arr + 0) + 8;
+        if (param_dl > (uint32_t)maxlen) {
+            pr2serr("Response truncated, need to set --maxlen=%u\n",
+                    param_dl);
+            if (maxlen < (8 /* header */ + 4 /* enough of first */)) {
+                pr2serr("Response too short to continue\n");
+                goto fini;
+            }
+        }
+        num_streams = sg_get_unaligned_be16(arr + 6);
+        if (! do_brief) {
+            if (stream_id > 0)
+            printf("Starting at stream id: %u\n", stream_id);
+            printf("Number of open streams: %u\n", num_streams);
+        }
+        maxlen = ((uint32_t)maxlen < param_dl) ? maxlen : (int)param_dl;
+        for (k = 8; k < maxlen; k += 8) {
+            stream_id = sg_get_unaligned_be16(arr + k + 2);
+            if (do_brief)
+                printf("%u\n", stream_id);
+            else
+                printf("Open stream id: %u\n", stream_id);
+        }
+    } else {            /* Stream control */
+        cmd_name = "Stream control";
+        ret = sg_ll_stream_control(sg_fd, ctl, stream_id, arr, maxlen,
+                                   &resid, false, verbose);
+        if (ret) {
+            if (SG_LIB_CAT_INVALID_OP == ret)
+                pr2serr("%s command not supported\n", cmd_name);
+            else {
+                char b[80];
+
+                sg_get_category_sense_str(ret, sizeof(b), b, verbose);
+                pr2serr("%s command: %s\n", cmd_name, b);
+            }
+            goto fini;
+        }
+        if (do_open) {
+            k = arr[0] + 1;
+            k = (k < (maxlen - resid)) ? k : (maxlen - resid);
+            if (k < 5) {
+                pr2serr("Response too short (%d bytes) assigned stream id\n",
+                        k);
+                printf("-1\n");
+                ret = SG_LIB_CAT_MALFORMED;
+            } else {
+                stream_id = sg_get_unaligned_be16(arr + 4);
+                if (do_brief)
+                    printf("%u\n", stream_id);
+                else
+                    printf("Assigned stream id: %u\n", stream_id);
+            }
+        }
+    }
+
+fini:
+    if (free_arr)
+        free(free_arr);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_stream_ctl failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_sync.c b/src/sg_sync.c
new file mode 100644
index 0000000..86d817d
--- /dev/null
+++ b/src/sg_sync.c
@@ -0,0 +1,314 @@
+/*
+ * Copyright (c) 2004-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <limits.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues the SCSI command SYNCHRONIZE CACHE(10 or 16) to the
+ * given device. This command is defined for SCSI "direct access" devices
+ * (e.g. disks).
+ */
+
+static const char * version_str = "1.27 20211114";
+
+#define SYNCHRONIZE_CACHE16_CMD     0x91
+#define SYNCHRONIZE_CACHE16_CMDLEN  16
+#define SENSE_BUFF_LEN  64
+#define DEF_PT_TIMEOUT  60       /* 60 seconds */
+
+
+static struct option long_options[] = {
+        {"16", no_argument, 0, 'S'},
+        {"count", required_argument, 0, 'c'},
+        {"group", required_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"immed", no_argument, 0, 'i'},
+        {"lba", required_argument, 0, 'l'},
+        {"sync-nv", no_argument, 0, 's'},
+        {"timeout", required_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void usage()
+{
+    pr2serr("Usage: sg_sync    [--16] [--count=COUNT] [--group=GN] [--help] "
+            "[--immed]\n"
+            "                  [--lba=LBA] [--sync-nv] [--timeout=SECS] "
+            "[--verbose]\n"
+            "                  [--version] DEVICE\n"
+            "  where:\n"
+            "    --16|-S             calls SYNCHRONIZE CACHE(16) (def: is "
+            "10 byte\n"
+            "                        variant)\n"
+            "    --count=COUNT|-c COUNT    number of blocks to sync (def: 0 "
+            "which\n"
+            "                              implies rest of device)\n"
+            "    --group=GN|-g GN    set group number field to GN (def: 0)\n"
+            "    --help|-h           print out usage message\n"
+            "    --immed|-i          command returns immediately when set "
+            "else wait\n"
+            "                        for 'sync' to complete\n"
+            "    --lba=LBA|-l LBA    logical block address to start sync "
+            "operation\n"
+            "                        from (def: 0)\n"
+            "    --sync-nv|-s        synchronize to non-volatile storage "
+            "(if distinct\n"
+            "                        from medium). Obsolete in sbc3r35d.\n"
+            "    --timeout=SECS|-t SECS    command timeout in seconds, only "
+            "active\n"
+            "                              if '--16' given (def: 60 seconds)\n"
+            "    --verbose|-v        increase verbosity\n"
+            "    --version|-V        print version string and exit\n\n"
+            "Performs a SCSI SYNCHRONIZE CACHE(10 or 16) command\n");
+}
+
+static int
+sg_ll_sync_cache_16(int sg_fd, bool sync_nv, bool immed, int group,
+                    uint64_t lba, unsigned int num_lb, int to_secs,
+                    bool noisy, int verbose)
+{
+    int res, ret, sense_cat;
+    uint8_t sc_cdb[SYNCHRONIZE_CACHE16_CMDLEN] =
+                {SYNCHRONIZE_CACHE16_CMD, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    if (sync_nv)
+        sc_cdb[1] |= 4;       /* obsolete in sbc3r35d */
+    if (immed)
+        sc_cdb[1] |= 2;
+    sg_put_unaligned_be64(lba, sc_cdb + 2);
+    sc_cdb[14] = group & GRPNUM_MASK;
+    sg_put_unaligned_be32((uint32_t)num_lb, sc_cdb + 10);
+
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    Synchronize cache(16) cdb: %s\n",
+                sg_get_command_str(sc_cdb, SYNCHRONIZE_CACHE16_CMDLEN, false,
+                                   sizeof(b), b));
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("synchronize cache(16): out of memory\n");
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, sc_cdb, sizeof(sc_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, to_secs, verbose);
+    ret = sg_cmds_process_resp(ptvp, "synchronize cache(16)", res,
+                               noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+int main(int argc, char * argv[])
+{
+    bool do_16 = false;
+    bool immed = false;
+    bool sync_nv = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c;
+    int sg_fd = -1;
+    int group = 0;
+    int ret = 0;
+    int to_secs = DEF_PT_TIMEOUT;
+    int verbose = 0;
+    unsigned int num_lb = 0;
+    int64_t count = 0;
+    int64_t lba = 0;
+    const char * device_name = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "c:g:hil:sSt:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            count = sg_get_llnum(optarg);
+            if ((count < 0) || (count > UINT_MAX)) {
+                pr2serr("bad argument to '--count'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            num_lb = (unsigned int)count;
+            break;
+        case 'g':
+            group = sg_get_num(optarg);
+            if ((group < 0) || (group > 63)) {
+                pr2serr("bad argument to '--group'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+            immed = true;
+            break;
+        case 'l':
+            lba = sg_get_llnum(optarg);
+            if (lba < 0) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 's':
+            sync_nv = true;
+            break;
+        case 'S':
+            do_16 = true;
+            break;
+        case 't':
+            to_secs = sg_get_num(optarg);
+            if (to_secs < 0) {
+                pr2serr("bad argument to '--timeout'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    if (do_16)
+        res = sg_ll_sync_cache_16(sg_fd, sync_nv, immed, group, lba, num_lb,
+                                  to_secs, true, verbose);
+    else
+        res = sg_ll_sync_cache_10(sg_fd, sync_nv, immed, group,
+                                  (unsigned int)lba, num_lb, true, verbose);
+    ret = res;
+    if (res) {
+        char b[80];
+
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Synchronize cache failed: %s\n", b);
+    }
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_sync failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_test_rwbuf.c b/src/sg_test_rwbuf.c
new file mode 100644
index 0000000..5b87c3b
--- /dev/null
+++ b/src/sg_test_rwbuf.c
@@ -0,0 +1,584 @@
+/*
+ * (c) 2000 Kurt Garloff
+ * heavily based on Douglas Gilbert's sg_rbuf program.
+ * (c) 1999-2022 Douglas Gilbert
+ *
+ * Program to test the SCSI host adapter by issuing
+ * write and read operations on a device's buffer
+ * and calculating checksums.
+ * NOTE: If you can not reserve the buffer of the device
+ * for this purpose (SG_GET_RESERVED_SIZE), you risk
+ * serious data corruption, if the device is accessed by
+ * somebody else in the meantime.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * $Id: sg_test_rwbuf.c,v 1.1 2000/03/02 13:50:03 garloff Exp $
+ *
+ *   2003/11/11  switch sg3_utils version to use SG_IO ioctl [dpg]
+ *   2004/06/08  remove SG_GET_VERSION_NUM check [dpg]
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <time.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "1.21 20220118";
+
+#define BPI (signed)(sizeof(int))
+
+#define RB_MODE_DESC 3
+#define RWB_MODE_DATA 2
+#define RB_DESC_LEN 4
+
+/*  The microcode in a SCSI device is _not_ modified by doing a WRITE BUFFER
+ *  with mode set to "data" (0x2) as done by this utility. Therefore this
+ *  utility is safe in that respect. [Mode values 0x4, 0x5, 0x6 and 0x7 are
+ *  the dangerous ones :-)]
+ */
+
+#define ME "sg_test_rwbuf: "
+
+static int base = 0x12345678;
+static int buf_capacity = 0;
+static int buf_granul = 255;
+static uint8_t *cmpbuf = NULL;
+static uint8_t *free_cmpbuf = NULL;
+
+
+/* Options */
+static int size = -1;
+static bool do_quick = false;
+static int addwrite  = 0;
+static int addread   = 0;
+static int verbose   = 0;
+
+static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"quick", no_argument, 0, 'q'},
+        {"addrd", required_argument, 0, 'r'},
+        {"size", required_argument, 0, 's'},
+        {"times", required_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"addwr", required_argument, 0, 'w'},
+        {0, 0, 0, 0},
+};
+
+static int
+find_out_about_buffer(int sg_fd)
+{
+        uint8_t rb_cdb[] = {READ_BUFFER, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        uint8_t rbBuff[RB_DESC_LEN];
+        uint8_t sense_buffer[32];
+        struct sg_io_hdr io_hdr;
+        int res;
+
+        rb_cdb[1] = RB_MODE_DESC;
+        rb_cdb[8] = RB_DESC_LEN;
+        memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+        io_hdr.interface_id = 'S';
+        io_hdr.cmd_len = sizeof(rb_cdb);
+        io_hdr.mx_sb_len = sizeof(sense_buffer);
+        io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+        io_hdr.dxfer_len = RB_DESC_LEN;
+        io_hdr.dxferp = rbBuff;
+        io_hdr.cmdp = rb_cdb;
+        io_hdr.sbp = sense_buffer;
+        io_hdr.timeout = 60000;     /* 60000 millisecs == 60 seconds */
+
+        if (verbose) {
+                char b[128];
+
+                pr2serr("    read buffer [mode desc] cdb: %s\n",
+                        sg_get_command_str(rb_cdb, (int)sizeof(rb_cdb), false,
+                                           sizeof(b), b));
+        }
+        if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+                perror(ME "SG_IO READ BUFFER descriptor error");
+                return -1;
+        }
+        /* now for the error processing */
+        res = sg_err_category3(&io_hdr);
+        switch (res) {
+        case SG_LIB_CAT_RECOVERED:
+                sg_chk_n_print3("READ BUFFER descriptor, continuing",
+                                &io_hdr, true);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+                __attribute__((fallthrough));
+                /* FALL THROUGH */
+#endif
+#endif
+        case SG_LIB_CAT_CLEAN:
+                break;
+        default: /* won't bother decoding other categories */
+                sg_chk_n_print3("READ BUFFER descriptor error", &io_hdr,
+                                true);
+                return res;
+        }
+
+        buf_capacity = sg_get_unaligned_be24(rbBuff + 1);
+        buf_granul = (uint8_t)rbBuff[0];
+#if 0
+        printf("READ BUFFER reports: %02x %02x %02x %02x\n",
+               rbBuff[0], rbBuff[1], rbBuff[2], rbBuff[3]);
+#endif
+        if (verbose)
+                printf("READ BUFFER reports: buffer capacity=%d, offset "
+                       "boundary=%d\n", buf_capacity, buf_granul);
+        return 0;
+}
+
+static int
+mymemcmp (uint8_t *bf1, uint8_t *bf2, int len)
+{
+        int df;
+        for (df = 0; df < len; df++)
+                if (bf1[df] != bf2[df]) return df;
+        return 0;
+}
+
+/* return 0 if good, else 2222 */
+static int
+do_checksum(int *buf, int len, bool quiet)
+{
+        int sum = base;
+        int i; int rln = len;
+        for (i = 0; i < len/BPI; i++)
+                sum += buf[i];
+        while (rln%BPI) sum += ((char*)buf)[--rln];
+        if (sum != 0x12345678) {
+                if (!quiet) printf ("sg_test_rwbuf: Checksum error (sz=%i):"
+                                    " %08x\n", len, sum);
+                if (cmpbuf && !quiet) {
+                        int diff = mymemcmp (cmpbuf, (uint8_t*)buf,
+                                             len);
+                        printf ("Differ at pos %i/%i:\n", diff, len);
+                        for (i = 0; i < 24 && i+diff < len; i++)
+                                printf (" %02x", cmpbuf[i+diff]);
+                        printf ("\n");
+                        for (i = 0; i < 24 && i+diff < len; i++)
+                                printf (" %02x",
+                                        ((uint8_t*)buf)[i+diff]);
+                        printf ("\n");
+                }
+                return 2222;
+        }
+        else {
+                if (verbose > 1)
+                        printf("Checksum value: 0x%x\n", sum);
+                return 0;
+        }
+}
+
+void do_fill_buffer (int *buf, int len)
+{
+        int sum;
+        int i; int rln = len;
+
+        srand(time(0));
+    retry:
+        if (len >= BPI)
+                base = 0x12345678 + rand();     /* don't need strong crypto */
+        else
+                base = 0x12345678 + (char)rand();
+        sum = base;
+        for (i = 0; i < len/BPI - 1; i++)
+        {
+                /* we rely on rand() giving full range of int */
+                buf[i] = rand();
+                sum += buf[i];
+        }
+        while (rln%BPI)
+        {
+                ((char*)buf)[--rln] = rand();
+                sum += ((char*)buf)[rln];
+        }
+        if (len >= BPI) buf[len/BPI - 1] = 0x12345678 - sum;
+        else ((char*)buf)[0] = 0x12345678 + ((char*)buf)[0] - sum;
+        if (do_checksum(buf, len, true)) {
+                if (len < BPI) goto retry;
+                printf ("sg_test_rwbuf: Memory corruption?\n");
+                exit (1);
+        }
+        if (cmpbuf) memcpy (cmpbuf, (char*)buf, len);
+}
+
+
+int read_buffer (int sg_fd, unsigned ssize)
+{
+        int res;
+        uint8_t rb_cdb[] = {READ_BUFFER, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        int bufSize = ssize + addread;
+        uint8_t * free_rbBuff = NULL;
+        uint8_t * rbBuff = (uint8_t *)sg_memalign(bufSize, 0, &free_rbBuff,
+                                                  false);
+        uint8_t sense_buffer[32];
+        struct sg_io_hdr io_hdr;
+
+        if (NULL == rbBuff)
+                return -1;
+        rb_cdb[1] = RWB_MODE_DATA;
+        sg_put_unaligned_be24((uint32_t)bufSize, rb_cdb + 6);
+        memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+        io_hdr.interface_id = 'S';
+        io_hdr.cmd_len = sizeof(rb_cdb);
+        io_hdr.mx_sb_len = sizeof(sense_buffer);
+        io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+        io_hdr.dxfer_len = bufSize;
+        io_hdr.dxferp = rbBuff;
+        io_hdr.cmdp = rb_cdb;
+        io_hdr.sbp = sense_buffer;
+        io_hdr.pack_id = 2;
+        io_hdr.timeout = 60000;     /* 60000 millisecs == 60 seconds */
+        if (verbose) {
+                char b[128];
+
+                pr2serr("    read buffer [mode data] cdb: %s\n",
+                        sg_get_command_str(rb_cdb, (int)sizeof(rb_cdb), false,
+                                           sizeof(b), b));
+        }
+
+        if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+                perror(ME "SG_IO READ BUFFER data error");
+                free(rbBuff);
+                return -1;
+        }
+        /* now for the error processing */
+        res = sg_err_category3(&io_hdr);
+        switch (res) {
+        case SG_LIB_CAT_RECOVERED:
+            sg_chk_n_print3("READ BUFFER data, continuing", &io_hdr, true);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        case SG_LIB_CAT_CLEAN:
+                break;
+        default: /* won't bother decoding other categories */
+                sg_chk_n_print3("READ BUFFER data error", &io_hdr, true);
+                free(rbBuff);
+                return res;
+        }
+
+        res = do_checksum((int*)rbBuff, ssize, false);
+        if (free_rbBuff)
+                free(free_rbBuff);
+        return res;
+}
+
+int write_buffer (int sg_fd, unsigned ssize)
+{
+        uint8_t wb_cdb[] = {WRITE_BUFFER, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        int bufSize = ssize + addwrite;
+        uint8_t * free_wbBuff = NULL;
+        uint8_t * wbBuff = (uint8_t *)sg_memalign(bufSize, 0, &free_wbBuff,
+                                                  false);
+        uint8_t sense_buffer[32];
+        struct sg_io_hdr io_hdr;
+        int res;
+
+        if (NULL == wbBuff)
+                return -1;
+        memset(wbBuff, 0, bufSize);
+        do_fill_buffer ((int*)wbBuff, ssize);
+        wb_cdb[1] = RWB_MODE_DATA;
+        sg_put_unaligned_be24((uint32_t)bufSize, wb_cdb + 6);
+        memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+        io_hdr.interface_id = 'S';
+        io_hdr.cmd_len = sizeof(wb_cdb);
+        io_hdr.mx_sb_len = sizeof(sense_buffer);
+        io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
+        io_hdr.dxfer_len = bufSize;
+        io_hdr.dxferp = wbBuff;
+        io_hdr.cmdp = wb_cdb;
+        io_hdr.sbp = sense_buffer;
+        io_hdr.pack_id = 1;
+        io_hdr.timeout = 60000;     /* 60000 millisecs == 60 seconds */
+        if (verbose) {
+                char b[128];
+
+                pr2serr("    write buffer [mode data] cdb: %s\n",
+                        sg_get_command_str(wb_cdb, (int)sizeof(wb_cdb), false,
+                                           sizeof(b), b));
+        }
+
+        if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+                perror(ME "SG_IO WRITE BUFFER data error");
+                free(wbBuff);
+                return -1;
+        }
+        /* now for the error processing */
+        res = sg_err_category3(&io_hdr);
+        switch (res) {
+        case SG_LIB_CAT_RECOVERED:
+            sg_chk_n_print3("WRITE BUFFER data, continuing", &io_hdr, true);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        case SG_LIB_CAT_CLEAN:
+                break;
+        default: /* won't bother decoding other categories */
+                sg_chk_n_print3("WRITE BUFFER data error", &io_hdr, true);
+                free(wbBuff);
+                return res;
+        }
+        if (free_wbBuff)
+                free(free_wbBuff);
+        return res;
+}
+
+void usage ()
+{
+        printf ("Usage: sg_test_rwbuf [--addrd=AR] [--addwr=AW] [--help] "
+                "[--quick]\n");
+        printf ("                     --size=SZ [--times=NUM] [--verbose] "
+                "[--version]\n"
+                "                     DEVICE\n"
+                " or\n"
+                "       sg_test_rwbuf DEVICE SZ [AW] [AR]\n");
+        printf ("  where:\n"
+                "    --addrd=AR|-r    extra bytes to fetch during READ "
+                "BUFFER\n"
+                "    --addwr=AW|-w    extra bytes to send to WRITE BUFFER\n"
+                "    --help|-l        output this usage message then exit\n"
+                "    --quick|-q       output read buffer size then exit\n"
+                "    --size=SZ|-s     size of buffer (in bytes) to write "
+                "then read back\n"
+                "    --times=NUM|-t   number of times to run test "
+                "(default 1)\n"
+                "    --verbose|-v     increase verbosity of output\n"
+                "    --version|-V     output version then exit\n");
+        printf ("\nWARNING: If you access the device at the same time, e.g. "
+                "because it's a\n");
+        printf (" mounted hard disk, the device's buffer may be used by the "
+                "device itself\n");
+        printf (" for other data at the same time, and overwriting it may or "
+                "may not\n");
+        printf (" cause data corruption!\n");
+        printf ("(c) Douglas Gilbert, Kurt Garloff, 2000-2007, GNU GPL\n");
+}
+
+
+int main (int argc, char * argv[])
+{
+        bool verbose_given = false;
+        bool version_given = false;
+        int sg_fd, res;
+        const char * device_name = NULL;
+        int times = 1;
+        int ret = 0;
+        int k = 0;
+        int err;
+
+        while (1) {
+                int option_index = 0;
+                int c;
+
+                c = getopt_long(argc, argv, "hqr:s:t:w:vV",
+                                long_options, &option_index);
+                if (c == -1)
+                        break;
+
+                switch (c) {
+                case 'h':
+                        usage();
+                        return 0;
+                case 'q':
+                        do_quick = true;
+                        break;
+                case 'r':
+                        addread = sg_get_num(optarg);
+                        if (-1 == addread) {
+                                pr2serr("bad argument to '--addrd'\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 's':
+                        size = sg_get_num(optarg);
+                        if (-1 == size) {
+                                pr2serr("bad argument to '--size'\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 't':
+                        times = sg_get_num(optarg);
+                        if (-1 == times) {
+                                pr2serr("bad argument to '--times'\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                case 'v':
+                        verbose_given = true;
+                        verbose++;
+                        break;
+                case 'V':
+                        version_given = true;
+                        break;
+                case 'w':
+                        addwrite = sg_get_num(optarg);
+                        if (-1 == addwrite) {
+                                pr2serr("bad argument to '--addwr'\n");
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        break;
+                default:
+                        usage();
+                        return SG_LIB_SYNTAX_ERROR;
+                }
+        }
+        if (optind < argc) {
+                if (NULL == device_name) {
+                        device_name = argv[optind];
+                        ++optind;
+                }
+        }
+        if (optind < argc) {
+                if (-1 == size) {
+                        size = sg_get_num(argv[optind]);
+                        if (-1 == size) {
+                                pr2serr("bad <sz>\n");
+                                usage();
+                                return SG_LIB_SYNTAX_ERROR;
+                        }
+                        if (++optind < argc) {
+                                addwrite = sg_get_num(argv[optind]);
+                                if (-1 == addwrite) {
+                                        pr2serr("bad [addwr]\n");
+                                        usage();
+                                        return SG_LIB_SYNTAX_ERROR;
+                                }
+                                if (++optind < argc) {
+                                        addread = sg_get_num(argv[optind]);
+                                        if (-1 == addread) {
+                                                pr2serr("bad [addrd]\n");
+                                                usage();
+                                                return SG_LIB_SYNTAX_ERROR;
+                                        }
+                                }
+                        }
+
+                }
+                if (optind < argc) {
+                        for (; optind < argc; ++optind)
+                                pr2serr("Unexpected extra argument" ": %s\n",
+                                        argv[optind]);
+                        usage();
+                        return SG_LIB_SYNTAX_ERROR;
+                }
+        }
+
+#ifdef DEBUG
+        pr2serr("In DEBUG mode, ");
+        if (verbose_given && version_given) {
+                pr2serr("but override: '-vV' given, zero verbose and "
+                        "continue\n");
+                verbose_given = false;
+                version_given = false;
+                verbose = 0;
+        } else if (! verbose_given) {
+                pr2serr("set '-vv'\n");
+                verbose = 2;
+        } else
+                pr2serr("keep verbose=%d\n", verbose);
+#else
+        if (verbose_given && version_given)
+                pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+        if (version_given) {
+                pr2serr(ME "version: %s\n", version_str);
+                return 0;
+        }
+
+        if (NULL == device_name) {
+                pr2serr("no device name given\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+        if ((size <= 0) && (! do_quick)) {
+                pr2serr("must give '--size' or '--quick' options or <sz> "
+                        "argument\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+        }
+
+        sg_fd = open(device_name, O_RDWR | O_NONBLOCK);
+        if (sg_fd < 0) {
+                err = errno;
+                perror("sg_test_rwbuf: open error");
+                return sg_convert_errno(err);
+        }
+        ret = find_out_about_buffer(sg_fd);
+        if (ret)
+                goto err_out;
+        if (do_quick) {
+                printf ("READ BUFFER read descriptor reports a buffer "
+                        "of %d bytes [%d KiB]\n", buf_capacity,
+                        buf_capacity / 1024);
+                goto err_out;
+        }
+        if (size > buf_capacity) {
+                pr2serr (ME "sz=%i > buf_capacity=%i\n", size, buf_capacity);
+                ret = SG_LIB_CAT_OTHER;
+                goto err_out;
+        }
+
+        cmpbuf = (uint8_t *)sg_memalign(size, 0, &free_cmpbuf, false);
+        for (k = 0; k < times; ++k) {
+                ret = write_buffer (sg_fd, size);
+                if (ret) {
+                        goto err_out;
+                }
+                ret = read_buffer (sg_fd, size);
+                if (ret) {
+                        if (2222 == ret)
+                                ret = SG_LIB_CAT_MALFORMED;
+                        goto err_out;
+                }
+        }
+
+err_out:
+        if (free_cmpbuf)
+                free(free_cmpbuf);
+        res = close(sg_fd);
+        if (res < 0) {
+                perror(ME "close error");
+                if (0 == ret)
+                        ret = sg_convert_errno(errno);
+        }
+        if ((0 == ret) && (! do_quick))
+                printf ("Success\n");
+        else if (times > 1)
+                printf ("Failed after %d successful cycles\n", k);
+        return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_timestamp.c b/src/sg_timestamp.c
new file mode 100644
index 0000000..fdb68fd
--- /dev/null
+++ b/src/sg_timestamp.c
@@ -0,0 +1,550 @@
+/*
+ * Copyright (c) 2015-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues a SCSI REPORT TIMESTAMP and SET TIMESTAMP commands
+ * to the given SCSI device. Based on spc5r07.pdf .
+ */
+
+static const char * version_str = "1.14 20210830";
+
+#define REP_TIMESTAMP_CMDLEN 12
+#define SET_TIMESTAMP_CMDLEN 12
+#define REP_TIMESTAMP_SA 0xf
+#define SET_TIMESTAMP_SA 0xf
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+uint8_t d_buff[256];
+
+/* example Report timestamp parameter data */
+/* uint8_t test[12] = {0, 0xa, 2, 0, 0x1, 0x51, 0x5b, 0xe2, 0xc1, 0x30,
+ *                     0, 0}; */
+
+
+static struct option long_options[] = {
+        {"elapsed", no_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"milliseconds", required_argument, 0, 'm'},
+        {"no_timestamp", no_argument, 0, 'N'},
+        {"no-timestamp", no_argument, 0, 'N'},
+        {"origin", no_argument, 0, 'o'},
+        {"raw", no_argument, 0, 'r'},
+        {"readonly", no_argument, 0, 'R'},
+        {"seconds", required_argument, 0, 's'},
+        {"srep", no_argument, 0, 'S'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+/* Indexed by 'timestamp origin' field value */
+static const char * ts_origin_arr[] = {
+    "initialized to zero at power on or by hard reset",
+    "reserved [0x1]",
+    "initialized by SET TIMESTAMP command",
+    "initialized by other method",
+    "reserved [0x4]",
+    "reserved [0x5]",
+    "reserved [0x6]",
+    "reserved [0x7]",
+};
+
+
+static void
+usage(int num)
+{
+    if (num > 1)
+        goto page2;
+
+    pr2serr("Usage: "
+            "sg_timestamp  [--elapsed] [--help] [--hex] [--milliseconds=MS]\n"
+            "                     [--no-timestamp] [--origin] [--raw] "
+            "[--readonly]\n"
+            "                     [--seconds=SECS] [--srep] [--verbose] "
+            "[--version]\n"
+            "                     DEVICE\n"
+           );
+    pr2serr("  where:\n"
+            "    --elapsed|-e       show time as '<n> days hh:mm:ss.xxx' "
+            "where\n"
+            "                       '.xxx' is the remainder milliseconds. "
+            "Don't show\n"
+            "                       '<n> days' if <n> is 0 (unless '-e' "
+            "given twice)\n"
+            "    --help|-h          print out usage message, use twice for "
+            "examples\n"
+            "    --hex|-H           output response in ASCII hexadecimal\n"
+            "    --milliseconds=MS|-m MS    set timestamp to MS "
+            "milliseconds since\n"
+            "                               1970-01-01 00:00:00 UTC\n"
+            "    --no-timestamp|-N    suppress output of timestamp\n"
+            "    --origin|-o        show Report timestamp origin "
+            "(def: don't)\n"
+            "                       used twice outputs value of field\n"
+            "                       0: power up or hard reset; 2: SET "
+            "TIMESTAMP\n"
+            "    --raw|-r           output Report timestamp response to "
+            "stdout in\n"
+            "                       binary\n"
+            "    --readonly|-R      open DEVICE read only (def: "
+            "read/write)\n"
+            "    --seconds=SECS|-s SECS    set timestamp to SECS "
+            "seconds since\n"
+            "                            1970-01-01 00:00:00 UTC\n"
+            "    --srep|-S          output Report timestamp in seconds "
+            "(def:\n"
+            "                       milliseconds)\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+          );
+    pr2serr("Performs a SCSI REPORT TIMESTAMP or SET TIMESTAMP command. "
+            "The timestamp\nis SET if either the --milliseconds=MS or "
+            "--seconds=SECS option is given,\notherwise the existing "
+            "timestamp is reported in milliseconds. The\nDEVICE stores "
+            "the timestamp as the number of milliseconds since power up\n"
+            "(or reset) or since 1970-01-01 00:00:00 UTC which also "
+            "happens to\nbe the time 'epoch'of Unix machines.\n\n"
+            "Use '-hh' (the '-h' option twice) for examples.\n"
+#if 0
+ "The 'date +%%s' command in "
+            "Unix returns the number of\nseconds since the epoch. To "
+            "convert a reported timestamp (in seconds since\nthe epoch) "
+            "to a more readable form use "
+            "'date --date=@<secs_since_epoch>' .\n"
+#endif
+           );
+    return;
+page2:
+    pr2serr("sg_timestamp examples:\n"
+            "It is possible that the target device containing a SCSI "
+            "Logical Unit (LU)\nhas a battery (or supercapacitor) to "
+            "keep its RTC (real time clock)\nticking during a power "
+            "outage. More likely it doesn't and its RTC is\ncleared to "
+            "zero after a power cycle or hard reset.\n\n"
+            "Either way REPORT TIMESTAMP returns a 48 bit counter value "
+            "whose unit is\na millisecond. A heuristic to determine if a "
+            "date or elapsed time is\nbeing returned is to choose a date "
+            "like 1 January 2000 which is 30 years\nafter the Unix epoch "
+            "(946,684,800,000 milliseconds) and values less than\nthat are "
+            "elapsed times and greater are timestamps. Observing the "
+            "TIMESTAMP\nORIGIN field of REPORT TIMESTAMP is a better "
+            "method:\n\n"
+           );
+    pr2serr(" $ sg_timestamp -o -N /dev/sg1\n"
+            "Device clock initialized to zero at power on or by hard "
+            "reset\n"
+            " $ sg_timestamp -oo -N /dev/sg1\n"
+            "0\n\n"
+            " $ sg_timestamp /dev/sg1\n"
+            "3984499\n"
+            " $ sg_timestamp --elapsed /dev/sg1\n"
+            "01:06:28.802\n\n"
+            "The last output indicates an elapsed time of 1 hour, 6 minutes "
+            "and 28.802\nseconds. Next set the clock to the current time:\n\n"
+            " $ sg_timestamp --seconds=`date +%%s` /dev/sg1\n\n"
+            " $ sg_timestamp -o -N /dev/sg1\n"
+            "Device clock initialized by SET TIMESTAMP command\n\n"
+            "Now show that as an elapsed time:\n\n"
+            " $ sg_timestamp -e /dev/sg1\n"
+            "17652 days 20:53:22.545\n\n"
+            "That is over 48 years worth of days. Lets try again as a "
+            "data-time\nstamp in UTC:\n\n"
+            " $ date -u -R --date=@`sg_timestamp -S /dev/sg1`\n"
+            "Tue, 01 May 2018 20:56:38 +0000\n"
+           );
+}
+
+/* Invokes a SCSI REPORT TIMESTAMP command.  Return of 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_rep_timestamp(int sg_fd, void * resp, int mx_resp_len, int * residp,
+                    bool noisy, int verbose)
+{
+    int k, ret, res, sense_cat;
+    uint8_t rt_cdb[REP_TIMESTAMP_CMDLEN] =
+          {SG_MAINTENANCE_IN, REP_TIMESTAMP_SA, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be32((uint32_t)mx_resp_len, rt_cdb + 6);
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    Report timestamp cdb: %s\n",
+                sg_get_command_str(rt_cdb, REP_TIMESTAMP_CMDLEN, false,
+                                   sizeof(b), b));
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", __func__);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, rt_cdb, sizeof(rt_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, "report timestamp", res, noisy, verbose,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    k = get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = k;
+    if ((verbose > 2) && ((mx_resp_len - k) > 0)) {
+        pr2serr("Parameter data returned:\n");
+        hex2stderr((const uint8_t *)resp, mx_resp_len - k,
+                   ((verbose > 3) ? -1 : 1));
+    }
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+/* Invokes the SET TIMESTAMP command.  Return of 0 -> success, various
+ * SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_set_timestamp(int sg_fd, void * paramp, int param_len, bool noisy,
+                    int verbose)
+{
+    int ret, res, sense_cat;
+    uint8_t st_cdb[SET_TIMESTAMP_CMDLEN] =
+          {SG_MAINTENANCE_OUT, SET_TIMESTAMP_SA, 0, 0,  0, 0, 0, 0,
+           0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    sg_put_unaligned_be32(param_len, st_cdb + 6);
+    if (verbose) {
+        char b[128];
+
+        pr2serr("    Set timestamp cdb: %s\n",
+                sg_get_command_str(st_cdb, SET_TIMESTAMP_CMDLEN, false,
+                                   sizeof(b), b));
+        if ((verbose > 1) && paramp && param_len) {
+            pr2serr("    set timestamp parameter list:\n");
+            hex2stderr((const uint8_t *)paramp, param_len, -1);
+        }
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", __func__);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, st_cdb, sizeof(st_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)paramp, param_len);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, "set timestamp", res, noisy, verbose,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool do_srep = false;
+    bool do_raw = false;
+    bool no_timestamp = false;
+    bool readonly = false;
+    bool secs_given = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c;
+    int sg_fd = 1;
+    int elapsed = 0;
+    int do_origin = 0;
+    int do_help = 0;
+    int do_hex = 0;
+    int do_set = 0;
+    int ret = 0;
+    int verbose = 0;
+    uint64_t secs = 0;
+    uint64_t msecs = 0;
+    int64_t ll;
+    const char * device_name = NULL;
+    const char * cmd_name;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "ehHm:NorRs:SvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'e':
+            ++elapsed;
+            break;
+        case 'h':
+        case '?':
+            ++do_help;
+            break;
+        case 'H':
+            ++do_hex;
+            break;
+        case 'm':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--milliseconds=MS'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            msecs = (uint64_t)ll;
+            ++do_set;
+            break;
+        case 'N':
+            no_timestamp = true;
+            break;
+        case 'o':
+            ++do_origin;
+            break;
+        case 'r':
+            do_raw = true;
+            break;
+        case 'R':
+            readonly = true;
+            break;
+        case 's':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--seconds=SECS'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            secs = (uint64_t)ll;
+            ++do_set;
+            secs_given = true;
+            break;
+        case 'S':
+            do_srep = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage(1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (do_help) {
+        usage(do_help);
+        return 0;
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (do_set > 1) {
+        pr2serr("either --milliseconds=MS or --seconds=SECS may be given, "
+                "not both\n");
+        usage(1);
+        return SG_LIB_CONTRADICT;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage(1);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+
+    memset(d_buff, 0, 12);
+    if (do_set) {
+        cmd_name = "Set timestamp";
+        sg_put_unaligned_be48(secs_given ? (secs * 1000) : msecs, d_buff + 4);
+        res = sg_ll_set_timestamp(sg_fd, d_buff, 12, true, verbose);
+    } else {
+        cmd_name = "Report timestamp";
+        res = sg_ll_rep_timestamp(sg_fd, d_buff, 12, NULL, true, verbose);
+        if (0 == res) {
+            if (do_raw)
+                dStrRaw(d_buff, 12);
+            else if (do_hex)
+                hex2stderr(d_buff, 12, 1);
+            else {
+                int len = sg_get_unaligned_be16(d_buff + 0);
+
+                if (len < 8)
+                    pr2serr("timestamp parameter data length too short, "
+                            "expect >= 10, got %d\n", len + 2);
+                else {
+                    if (do_origin) {
+                        if (1 == do_origin)
+                            printf("Device clock %s\n",
+                                   ts_origin_arr[0x7 & d_buff[2]]);
+                        else if (2 == do_origin)
+                            printf("%d\n", 0x7 & d_buff[2]);
+                        else
+                            printf("TIMESTAMP_ORIGIN=%d\n", 0x7 & d_buff[2]);
+                    }
+                    if (! no_timestamp) {
+                        msecs = sg_get_unaligned_be48(d_buff + 4);
+                        if (elapsed) {
+                            int days = (int)(msecs / 1000 / 60 / 60 / 24);
+                            int hours = (int)(msecs / 1000 / 60 / 60 % 24);
+                            int mins = (int)(msecs / 1000 / 60 % 60);
+                            int secs_in_min =(int)( msecs / 1000 % 60);
+                            int rem_msecs = (int)(msecs % 1000);
+
+                            if ((elapsed > 1) || (days > 0))
+                                printf("%d day%s ", days,
+                                       ((1 == days) ? "" : "s"));
+                            printf("%02d:%02d:%02d.%03d\n", hours, mins,
+                                   secs_in_min, rem_msecs);
+                        } else
+                            printf("%" PRIu64 "\n", do_srep ?
+                                                    (msecs / 1000) : msecs);
+                    }
+                }
+            }
+        }
+    }
+    ret = res;
+    if (res) {
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("%s command not supported\n", cmd_name);
+        else {
+            char b[80];
+
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("%s command: %s\n", cmd_name, b);
+        }
+    }
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_timestamp failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_turs.c b/src/sg_turs.c
new file mode 100644
index 0000000..2d473c4
--- /dev/null
+++ b/src/sg_turs.c
@@ -0,0 +1,657 @@
+/*
+ * Copyright (C) 2000-2022 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+/*
+ * This program sends a user specified number of TEST UNIT READY ("tur")
+ * commands to the given sg device. Since TUR is a simple command involing
+ * no data transfer (and no REQUEST SENSE command iff the unit is ready)
+ * then this can be used for timing per SCSI command overheads.
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+#include <time.h>
+#elif defined(HAVE_GETTIMEOFDAY)
+#include <time.h>
+#include <sys/time.h>
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_pt.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "3.51 20220425";
+
+#define DEF_PT_TIMEOUT  60       /* 60 seconds */
+
+
+static struct option long_options[] = {
+        {"delay", required_argument, 0, 'd'},
+        {"help", no_argument, 0, 'h'},
+        {"low", no_argument, 0, 'l'},
+        {"new", no_argument, 0, 'N'},
+        {"number", required_argument, 0, 'n'},
+        {"num", required_argument, 0, 'n'}, /* added in v3.32 (sg3_utils
+                                * v1.43) for sg_requests compatibility */
+        {"old", no_argument, 0, 'O'},
+        {"progress", no_argument, 0, 'p'},
+        {"time", no_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool delay_given;
+    bool do_low;
+    bool do_progress;
+    bool do_time;
+    bool opts_new;
+    bool verbose_given;
+    bool version_given;
+    int delay;
+    int do_help;
+    int do_number;
+    int verbose;
+    const char * device_name;
+};
+
+struct loop_res_t {
+    bool reported;
+    int num_errs;
+    int ret;
+};
+
+
+static void
+usage()
+{
+    printf("Usage: sg_turs [--delay=MS] [--help] [--low] [--number=NUM] "
+           "[--num=NUM]\n"
+           "               [--progress] [--time] [--verbose] [--version] "
+           "DEVICE\n"
+           "  where:\n"
+           "    --delay=MS|-d MS    delay MS miiliseconds before sending "
+           "each tur\n"
+           "    --help|-h        print usage message then exit\n"
+           "    --low|-l         use low level (sg_pt) interface for "
+           "speed\n"
+           "    --number=NUM|-n NUM    number of test_unit_ready commands "
+           "(def: 1)\n"
+           "    --num=NUM|-n NUM       same action as '--number=NUM'\n"
+           "    --old|-O         use old interface (use as first option)\n"
+           "    --progress|-p    outputs progress indication (percentage) "
+           "if available\n"
+           "                     waits 30 seconds before TUR unless "
+           "--delay=MS given\n"
+           "    --time|-t        outputs total duration and commands per "
+           "second\n"
+           "    --verbose|-v     increase verbosity\n"
+           "    --version|-V     print version string then exit\n\n"
+           "Performs a SCSI TEST UNIT READY command (or many of them).\n"
+           "This SCSI command is often known by its abbreviation: TUR .\n");
+}
+
+static void
+usage_old()
+{
+    printf("Usage: sg_turs [-d=MS] [-l] [-n=NUM] [-p] [-t] [-v] [-V] "
+           "DEVICE\n"
+           "  where:\n"
+           "    -d=MS     same as --delay=MS in new interface\n"
+           "    -l        use low level interface (sg_pt) for speed\n"
+           "    -n=NUM    number of test_unit_ready commands "
+           "(def: 1)\n"
+           "    -p        outputs progress indication (percentage) "
+           "if available\n"
+           "    -t        outputs total duration and commands per "
+           "second\n"
+           "    -v        increase verbosity\n"
+           "    -N|--new  use new interface\n"
+           "    -V        print version string then exit\n\n"
+           "Performs a SCSI TEST UNIT READY command (or many of them).\n");
+}
+
+static void
+usage_for(const struct opts_t * op)
+{
+    if (op->opts_new)
+        usage();
+    else
+        usage_old();
+}
+
+static int
+new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int c, n;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "d:hln:NOptvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'd':
+            n = sg_get_num(optarg);
+            if (n < 0) {
+                pr2serr("bad argument to '--delay='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->delay = n;
+            op->delay_given = true;
+            break;
+        case 'h':
+        case '?':
+            ++op->do_help;
+            break;
+        case 'l':
+            op->do_low = true;
+            break;
+        case 'n':
+            n = sg_get_num(optarg);
+            if (n < 0) {
+                pr2serr("bad argument to '--number='\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_number = n;
+            break;
+        case 'N':
+            break;      /* ignore */
+        case 'O':
+            op->opts_new = false;
+            return 0;
+        case 'p':
+            op->do_progress = true;
+            break;
+        case 't':
+            op->do_time = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
+            if (op->do_help)
+                break;
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    bool jmp_out;
+    int k, plen;
+    const char * cp;
+
+    for (k = 1; k < argc; ++k) {
+        cp = argv[k];
+        plen = strlen(cp);
+        if (plen <= 0)
+            continue;
+        if ('-' == *cp) {
+            for (--plen, ++cp, jmp_out = false; plen > 0; --plen, ++cp) {
+                switch (*cp) {
+                case 'l':
+                    op->do_low = true;
+                    return 0;
+                case 'N':
+                    op->opts_new = true;
+                    return 0;
+                case 'O':
+                    break;
+                case 'p':
+                    op->do_progress = true;
+                    break;
+                case 't':
+                    op->do_time = true;
+                    break;
+                case 'v':
+                    op->verbose_given = true;
+                    ++op->verbose;
+                    break;
+                case 'V':
+                    op->version_given = true;
+                    break;
+                case '?':
+                    ++op->do_help;
+                    return 0;
+                default:
+                    jmp_out = true;
+                    break;
+                }
+                if (jmp_out)
+                    break;
+            }
+            if (plen <= 0)
+                continue;
+            if (0 == strncmp("d=", cp, 2)) {
+                op->delay = sg_get_num(cp + 2);
+                if (op->delay < 0) {
+                    printf("Couldn't decode number after 'd=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->delay_given = true;
+            } else if (0 == strncmp("n=", cp, 2)) {
+                op->do_number = sg_get_num(cp + 2);
+                if (op->do_number <= 0) {
+                    printf("Couldn't decode number after 'n=' option\n");
+                    usage_old();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else if (0 == strncmp("-old", cp, 4))
+                ;
+            else if (jmp_out) {
+                pr2serr("Unrecognized option: %s\n", cp);
+                usage_old();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == op->device_name)
+            op->device_name = cp;
+        else {
+            pr2serr("too many arguments, got: %s, not expecting: %s\n",
+                    op->device_name, cp);
+            usage_old();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+static int
+parse_cmd_line(struct opts_t * op, int argc, char * argv[])
+{
+    int res;
+    char * cp;
+
+    cp = getenv("SG3_UTILS_OLD_OPTS");
+    if (cp) {
+        op->opts_new = false;
+        res = old_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && op->opts_new)
+            res = new_parse_cmd_line(op, argc, argv);
+    } else {
+        op->opts_new = true;
+        res = new_parse_cmd_line(op, argc, argv);
+        if ((0 == res) && (0 == op->opts_new))
+            res = old_parse_cmd_line(op, argc, argv);
+    }
+    return res;
+}
+
+#ifdef SG_LIB_MINGW
+
+#include <windows.h>
+
+static void
+wait_millisecs(int millisecs)
+{
+    /* MinGW requires pthreads library for nanosleep, use Sleep() instead */
+    Sleep(millisecs);
+}
+
+#else
+
+static void
+wait_millisecs(int millisecs)
+{
+    struct timespec wait_period, rem;
+
+    wait_period.tv_sec = millisecs / 1000;
+    wait_period.tv_nsec = (millisecs % 1000) * 1000000;
+    while ((nanosleep(&wait_period, &rem) < 0) && (EINTR == errno))
+                wait_period = rem;
+}
+
+#endif
+
+/* Returns true if prints estimate of duration to ready */
+bool
+check_for_lu_becoming(struct sg_pt_base * ptvp)
+{
+    int s_len = get_scsi_pt_sense_len(ptvp);
+    uint64_t info;
+    uint8_t * sense_b = get_scsi_pt_sense_buf(ptvp);
+    struct sg_scsi_sense_hdr ssh;
+
+    /* Check for "LU is in process of becoming ready" with a non-zero INFO
+     * field that isn't too big. As per 20-061r2 it means the following: */
+    if (sg_scsi_normalize_sense(sense_b, s_len, &ssh) && (ssh.asc == 0x4) &&
+        (ssh.ascq == 0x1) && sg_get_sense_info_fld(sense_b, s_len, &info) &&
+        (info > 0x0) && (info < 0x1000000)) {
+        printf("device not ready, estimated to be ready in %" PRIu64
+               " milliseconds\n", info);
+        return true;
+    }
+    return false;
+}
+
+/* Returns number of TURs performed */
+static int
+loop_turs(struct sg_pt_base * ptvp, struct loop_res_t * resp,
+          struct opts_t * op)
+{
+    int k, res;
+    int packet_id = 0;
+    int vb = op->verbose;
+    char b[80];
+    uint8_t sense_b[32] SG_C_CPP_ZERO_INIT;
+
+    if (op->do_low) {
+        int rs, n, sense_cat;
+        uint8_t cdb[6];
+
+        for (k = 0; k < op->do_number; ++k) {
+            if (op->delay > 0)
+                wait_millisecs(op->delay);
+            /* Might get Unit Attention on first invocation */
+            memset(cdb, 0, sizeof(cdb));    /* TUR's cdb is 6 zeros */
+            set_scsi_pt_cdb(ptvp, cdb, sizeof(cdb));
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+            set_scsi_pt_packet_id(ptvp, ++packet_id);
+            rs = do_scsi_pt(ptvp, -1, DEF_PT_TIMEOUT, vb);
+            n = sg_cmds_process_resp(ptvp, "Test unit ready", rs, (0 == k),
+                                     vb, &sense_cat);
+            if (-1 == n) {
+                if (get_scsi_pt_transport_err(ptvp))
+                    resp->ret = SG_LIB_TRANSPORT_ERROR;
+                else
+                    resp->ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+                return k;
+            } else if (-2 == n) {
+                switch (sense_cat) {
+                case SG_LIB_CAT_RECOVERED:
+                case SG_LIB_CAT_NO_SENSE:
+                    break;
+                case SG_LIB_CAT_NOT_READY:
+                    ++resp->num_errs;
+                    if ((1 == op->do_number) || (op->delay > 0)) {
+                        if (! check_for_lu_becoming(ptvp))
+                            printf("device not ready\n");
+                        resp->ret = sense_cat;
+                        resp->reported = true;
+                    }
+                    break;
+                case SG_LIB_CAT_UNIT_ATTENTION:
+                    ++resp->num_errs;
+                    if (vb) {
+                        pr2serr("Ignoring Unit attention (sense key)\n");
+                        resp->reported = true;
+                    }
+                    break;
+                case SG_LIB_CAT_STANDBY:
+                    ++resp->num_errs;
+                    if (vb) {
+                        pr2serr("Ignoring standby device (sense key)\n");
+                        resp->reported = true;
+                    }
+                    break;
+                case SG_LIB_CAT_UNAVAILABLE:
+                    ++resp->num_errs;
+                    if (vb) {
+                        pr2serr("Ignoring unavailable device (sense key)\n");
+                        resp->reported = true;
+                    }
+                    break;
+                default:
+                    ++resp->num_errs;
+                    if (1 == op->do_number) {
+                        resp->ret = sense_cat;
+                        sg_get_category_sense_str(sense_cat, sizeof(b), b, vb);
+                        printf("%s\n", b);
+                        resp->reported = true;
+                        return k;
+                    }
+                    break;
+                }
+            }
+            partial_clear_scsi_pt_obj(ptvp);
+        }
+        return k;
+    } else {
+        for (k = 0; k < op->do_number; ++k) {
+            if (op->delay > 0)
+                wait_millisecs(op->delay);
+            set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+            /* Might get Unit Attention on first invocation */
+            res = sg_ll_test_unit_ready_pt(ptvp, k, (0 == k), vb);
+            if (res) {
+                ++resp->num_errs;
+                resp->ret = res;
+                if ((1 == op->do_number) || (op->delay > 0)) {
+                    if (SG_LIB_CAT_NOT_READY == res) {
+                        if (! check_for_lu_becoming(ptvp))
+                            printf("device not ready\n");
+                        continue;
+                    } else {
+                        sg_get_category_sense_str(res, sizeof(b), b, vb);
+                        printf("%s\n", b);
+                    }
+                    resp->reported = true;
+                    break;
+                }
+            }
+        }
+        return k;
+    }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool start_tm_valid = false;
+    int k, res, progress, pr, rem, num_done;
+    int err = 0;
+    int ret = 0;
+    int sg_fd = -1;
+    int64_t elapsed_usecs = 0;
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+    struct timespec start_tm, end_tm;
+#elif defined(HAVE_GETTIMEOFDAY)
+    struct timeval start_tm, end_tm;
+#endif
+    struct loop_res_t loop_res;
+    struct loop_res_t * resp = &loop_res;
+    struct sg_pt_base * ptvp = NULL;
+    struct opts_t opts;
+    struct opts_t * op = &opts;
+
+
+    memset(op, 0, sizeof(opts));
+    memset(resp, 0, sizeof(loop_res));
+    op->do_number = 1;
+    res = parse_cmd_line(op, argc, argv);
+    if (res)
+        return res;
+    if (op->do_help) {
+        usage_for(op);
+        return 0;
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("Version string: %s\n", version_str);
+        return 0;
+    }
+    if (op->do_progress && (! op->delay_given))
+        op->delay = 30 * 1000;  /* progress has 30 second default delay */
+
+    if (NULL == op->device_name) {
+        pr2serr("No DEVICE argument given\n");
+        usage_for(op);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if ((sg_fd = sg_cmds_open_device(op->device_name, true /* ro */,
+                                     op->verbose)) < 0) {
+        pr2serr("%s: error opening file: %s: %s\n", __func__,
+                op->device_name, safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+    ptvp = construct_scsi_pt_obj_with_fd(sg_fd, op->verbose);
+    if ((NULL == ptvp) || ((err = get_scsi_pt_os_err(ptvp)))) {
+        pr2serr("%s: unable to construct pt object\n", __func__);
+        ret = sg_convert_errno(err ? err : ENOMEM);
+        goto fini;
+    }
+    if (op->do_progress) {
+        for (k = 0; k < op->do_number; ++k) {
+            if (op->delay > 0) {
+                if (op->delay_given)
+                    wait_millisecs(op->delay);
+                else if (k > 0)
+                    wait_millisecs(op->delay);
+            }
+            progress = -1;
+            res = sg_ll_test_unit_ready_progress_pt(ptvp, k, &progress,
+                             (1 == op->do_number), op->verbose);
+            if (progress < 0) {
+                ret = res;
+                break;
+            } else {
+                pr = (progress * 100) / 65536;
+                rem = ((progress * 100) % 65536) / 656;
+                printf("Progress indication: %d.%02d%% done\n", pr, rem);
+            }
+        }
+        if (op->do_number > 1)
+            printf("Completed %d Test Unit Ready commands\n",
+                   ((k < op->do_number) ? k + 1 : k));
+    } else {            /* --progress not given */
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+        if (op->do_time) {
+            start_tm.tv_sec = 0;
+            start_tm.tv_nsec = 0;
+            if (0 == clock_gettime(CLOCK_MONOTONIC, &start_tm))
+                start_tm_valid = true;
+            else
+                perror("clock_gettime(CLOCK_MONOTONIC)\n");
+        }
+#elif defined(HAVE_GETTIMEOFDAY)
+        if (op->do_time) {
+            start_tm.tv_sec = 0;
+            start_tm.tv_usec = 0;
+            gettimeofday(&start_tm, NULL);
+            start_tm_valid = true;
+        }
+#else
+        start_tm_valid = false;
+#endif
+
+        num_done = loop_turs(ptvp, resp, op);
+
+        if (op->do_time && start_tm_valid) {
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
+            if (start_tm.tv_sec || start_tm.tv_nsec) {
+
+                res = clock_gettime(CLOCK_MONOTONIC, &end_tm);
+                if (res < 0) {
+                    err = errno;
+                    perror("clock_gettime");
+                    if (EINVAL == err)
+                        pr2serr("clock_gettime(CLOCK_MONOTONIC) not "
+                                "supported\n");
+                }
+                elapsed_usecs = (end_tm.tv_sec - start_tm.tv_sec) * 1000000;
+                /* Note: (end_tm.tv_nsec - start_tm.tv_nsec) may be negative */
+                elapsed_usecs += (end_tm.tv_nsec - start_tm.tv_nsec) / 1000;
+            }
+#elif defined(HAVE_GETTIMEOFDAY)
+            if (start_tm.tv_sec || start_tm.tv_usec) {
+                gettimeofday(&end_tm, NULL);
+                elapsed_usecs = (end_tm.tv_sec - start_tm.tv_sec) * 1000000;
+                elapsed_usecs += (end_tm.tv_usec - start_tm.tv_usec);
+            }
+#endif
+            if (elapsed_usecs > 0) {
+                int64_t nom = num_done;
+
+                printf("time to perform commands was %u.%06u secs",
+                       (unsigned)(elapsed_usecs / 1000000),
+                       (unsigned)(elapsed_usecs % 1000000));
+                nom *= 1000000; /* scale for integer division */
+                printf("; %d operations/sec\n", (int)(nom / elapsed_usecs));
+            } else
+                printf("Recorded 0 or less elapsed microseconds ??\n");
+        }
+        if (((op->do_number > 1) || (resp->num_errs > 0)) &&
+            (! resp->reported))
+            printf("Completed %d Test Unit Ready commands with %d errors\n",
+                   op->do_number, resp->num_errs);
+        if (1 == op->do_number)
+            ret = resp->ret;
+    }
+fini:
+    if (ptvp)
+        destruct_scsi_pt_obj(ptvp);
+    if (sg_fd >= 0)
+        sg_cmds_close_device(sg_fd);
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_unmap.c b/src/sg_unmap.c
new file mode 100644
index 0000000..2bfb12d
--- /dev/null
+++ b/src/sg_unmap.c
@@ -0,0 +1,794 @@
+/*
+ * Copyright (c) 2009-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#include <limits.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ * This utility invokes the UNMAP SCSI command to unmap (trim) one or more
+ * logical blocks. Note that DATA MAY BE LOST.
+ */
+
+static const char * version_str = "1.19 20220813";
+
+
+#define DEF_TIMEOUT_SECS 60
+#define MAX_NUM_ADDR 128
+#define RCAP10_RESP_LEN 8
+#define RCAP16_RESP_LEN 32
+
+#ifndef UINT32_MAX
+#define UINT32_MAX ((uint32_t)-1)
+#endif
+
+
+static struct option long_options[] = {
+        {"all", required_argument, 0, 'A'},
+        {"anchor", no_argument, 0, 'a'},
+        {"dry-run", no_argument, 0, 'd'},
+        {"dry_run", no_argument, 0, 'd'},
+        {"force", no_argument, 0, 'f'},
+        {"grpnum", required_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"in", required_argument, 0, 'I'},
+        {"lba", required_argument, 0, 'l'},
+        {"num", required_argument, 0, 'n'},
+        {"timeout", required_argument, 0, 't'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+          "sg_unmap [--all=ST,RN[,LA]] [--anchor] [--dry-run] [--force]\n"
+          "                [--grpnum=GN] [--help] [--in=FILE] "
+          "[--lba=LBA,LBA...]\n"
+          "                [--num=NUM,NUM...] [--timeout=TO] [--verbose] "
+          "[--version]\n"
+          "                DEVICE\n"
+          "  where:\n"
+          "    --all=ST,RN[,LA]|-A ST,RN[,LA]    start unmaps at LBA ST, "
+          "RN blocks\n"
+          "                         per unmap until the end of disk, or "
+          "until\n"
+          "                         and including LBA LA (last)\n"
+          "    --anchor|-a          set anchor field in cdb\n"
+          "    --dry-run|-d         prepare but skip UNMAP call(s)\n"
+          "    --force|-f           don't ask for confirmation before "
+          "zapping media\n"
+          "    --grpnum=GN|-g GN    GN is group number field (def: 0)\n"
+          "    --help|-h            print out usage message\n"
+          "    --in=FILE|-I FILE    read LBA, NUM pairs from FILE (if "
+          "FILE is '-'\n"
+          "                         then stdin is read)\n"
+          "    --lba=LBA,LBA...|-l LBA,LBA...    LBA is the logical block "
+          "address\n"
+          "                                      to start NUM unmaps\n"
+          "    --num=NUM,NUM...|-n NUM,NUM...    NUM is number of logical "
+          "blocks to\n"
+          "                                      unmap starting at "
+          "corresponding LBA\n"
+          "    --timeout=TO|-t TO    command timeout (unit: seconds) "
+          "(def: 60)\n"
+          "    --verbose|-v         increase verbosity\n"
+          "    --version|-V         print version string and exit\n\n"
+          "Perform a SCSI UNMAP command. LBA, NUM and the values in FILE "
+          "are assumed\nto be decimal. Use '0x' prefix or 'h' suffix for "
+          "hex values.\n"
+          "Example to unmap LBA 0x12345:\n"
+          "    sg_unmap --lba=0x12345 --num=1 /dev/sdb\n"
+          "Example to unmap starting at LBA 0x12345, 256 blocks per command:"
+          "\n    sg_unmap --all=0x12345,256 /dev/sg2\n"
+          "until the end if /dev/sg2 (assumed to be a storage device)\n\n"
+          );
+    pr2serr("WARNING: This utility will destroy data on DEVICE in the given "
+            "range(s)\nthat will be unmapped. Unmap is also known as 'trim' "
+            "and is irreversible.\n");
+}
+
+/* Read numbers (up to 64 bits in size) from command line (comma (or
+ * (single) space) separated list). Assumed decimal unless prefixed
+ * by '0x', '0X' or contains trailing 'h' or 'H' (which indicate hex).
+ * Returns 0 if ok, or 1 if error. */
+static int
+build_lba_arr(const char * inp, uint64_t * lba_arr, int * lba_arr_len,
+              int max_arr_len)
+{
+    int in_len, k;
+    int64_t ll;
+    const char * lcp;
+    char * cp;
+    char * c2p;
+
+    if ((NULL == inp) || (NULL == lba_arr) ||
+        (NULL == lba_arr_len))
+        return 1;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *lba_arr_len = 0;
+    if ('-' == inp[0]) {        /* read from stdin */
+        pr2serr("'--lba' cannot be read from stdin\n");
+        return 1;
+    } else {        /* list of numbers (default decimal) on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfFhHxXiIkKmMgGtTpP, ");
+        if (in_len != k) {
+            pr2serr("build_lba_arr: error at pos %d\n", k + 1);
+            return 1;
+        }
+        for (k = 0; k < max_arr_len; ++k) {
+            ll = sg_get_llnum(lcp);
+            if (-1 != ll) {
+                lba_arr[k] = (uint64_t)ll;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("build_lba_arr: error at pos %d\n",
+                        (int)(lcp - inp + 1));
+                return 1;
+            }
+        }
+        *lba_arr_len = k + 1;
+        if (k == max_arr_len) {
+            pr2serr("build_lba_arr: array length exceeded\n");
+            return 1;
+        }
+    }
+    return 0;
+}
+
+/* Read numbers (up to 32 bits in size) from command line (comma (or
+ * (single) space) separated list). Assumed decimal unless prefixed
+ * by '0x', '0X' or contains trailing 'h' or 'H' (which indicate hex).
+ * Returns 0 if ok, or 1 if error. */
+static int
+build_num_arr(const char * inp, uint32_t * num_arr, int * num_arr_len,
+              int max_arr_len)
+{
+    int in_len, k;
+    const char * lcp;
+    int64_t ll;
+    char * cp;
+    char * c2p;
+
+    if ((NULL == inp) || (NULL == num_arr) ||
+        (NULL == num_arr_len))
+        return 1;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *num_arr_len = 0;
+    if ('-' == inp[0]) {        /* read from stdin */
+        pr2serr("'--len' cannot be read from stdin\n");
+        return 1;
+    } else {        /* list of numbers (default decimal) on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfFhHxXiIkKmMgGtTpP, ");
+        if (in_len != k) {
+            pr2serr("build_num_arr: error at pos %d\n", k + 1);
+            return 1;
+        }
+        for (k = 0; k < max_arr_len; ++k) {
+            ll = sg_get_llnum(lcp);
+            if (-1 != ll) {
+                if (ll > UINT32_MAX) {
+                    pr2serr("build_num_arr: number exceeds 32 bits at pos "
+                            "%d\n", (int)(lcp - inp + 1));
+                    return 1;
+                }
+                num_arr[k] = (uint32_t)ll;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("build_num_arr: error at pos %d\n",
+                        (int)(lcp - inp + 1));
+                return 1;
+            }
+        }
+        *num_arr_len = k + 1;
+        if (k == max_arr_len) {
+            pr2serr("build_num_arr: array length exceeded\n");
+            return 1;
+        }
+    }
+    return 0;
+}
+
+
+/* Read numbers from filename (or stdin) line by line (comma (or
+ * (single) space) separated list). Assumed decimal unless prefixed
+ * by '0x', '0X' or contains trailing 'h' or 'H' (which indicate hex).
+ * Returns 0 if ok, or 1 if error. */
+static int
+build_joint_arr(const char * file_name, uint64_t * lba_arr, uint32_t * num_arr,
+                int * arr_len, int max_arr_len)
+{
+    bool have_stdin;
+    int off = 0;
+    int in_len, k, j, m, ind, bit0;
+    int64_t ll;
+    char line[1024];
+    char * lcp;
+    FILE * fp = NULL;
+
+    have_stdin = ((1 == strlen(file_name)) && ('-' == file_name[0]));
+    if (have_stdin)
+        fp = stdin;
+    else {
+        fp = fopen(file_name, "r");
+        if (NULL == fp) {
+            pr2serr("%s: unable to open %s\n", __func__, file_name);
+            return 1;
+        }
+    }
+
+    for (j = 0; j < 512; ++j) {
+        if (NULL == fgets(line, sizeof(line), fp))
+            break;
+        // could improve with carry_over logic if sizeof(line) too small
+        in_len = strlen(line);
+        if (in_len > 0) {
+            if ('\n' == line[in_len - 1]) {
+                --in_len;
+                line[in_len] = '\0';
+            }
+        }
+        if (in_len < 1)
+            continue;
+        lcp = line;
+        m = strspn(lcp, " \t");
+        if (m == in_len)
+            continue;
+        lcp += m;
+        in_len -= m;
+        if ('#' == *lcp)
+            continue;
+        k = strspn(lcp, "0123456789aAbBcCdDeEfFhHxXiIkKmMgGtTpP ,\t");
+        if ((k < in_len) && ('#' != lcp[k])) {
+            pr2serr("%s: syntax error at line %d, pos %d\n", __func__, j + 1,
+                    m + k + 1);
+            goto bad_exit;
+        }
+        for (k = 0; k < 1024; ++k) {
+            ll = sg_get_llnum(lcp);
+            if (-1 != ll) {
+                ind = ((off + k) >> 1);
+                bit0 = 0x1 & (off + k);
+                if (ind >= max_arr_len) {
+                    pr2serr("%s: array length exceeded\n", __func__);
+                    goto bad_exit;
+                }
+                if (bit0) {
+                    if (ll > UINT32_MAX) {
+                        pr2serr("%s: number exceeds 32 bits in line %d, at "
+                                "pos %d\n", __func__, j + 1,
+                                (int)(lcp - line + 1));
+                        goto bad_exit;
+                    }
+                    num_arr[ind] = (uint32_t)ll;
+                } else
+                   lba_arr[ind] = (uint64_t)ll;
+                lcp = strpbrk(lcp, " ,\t");
+                if (NULL == lcp)
+                    break;
+                lcp += strspn(lcp, " ,\t");
+                if ('\0' == *lcp)
+                    break;
+            } else {
+                if ('#' == *lcp) {
+                    --k;
+                    break;
+                }
+                pr2serr("%s: error on line %d, at pos %d\n", __func__, j + 1,
+                        (int)(lcp - line + 1));
+                goto bad_exit;
+            }
+        }
+        off += (k + 1);
+    }
+    if (0x1 & off) {
+        pr2serr("%s: expect LBA,NUM pairs but decoded odd number\n  from "
+                "%s\n", __func__, have_stdin ? "stdin" : file_name);
+        goto bad_exit;
+    }
+    *arr_len = off >> 1;
+    if (fp && (! have_stdin))
+        fclose(fp);
+    return 0;
+
+bad_exit:
+    if (fp && (! have_stdin))
+        fclose(fp);
+    return 1;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool anchor = false;
+    bool do_force = false;
+    bool dry_run = false;
+    bool err_printed = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, num, k, j;
+    int sg_fd = -1;
+    int grpnum = 0;
+    int addr_arr_len = 0;
+    int num_arr_len = 0;
+    int param_len = 4;
+    int ret = 0;
+    int timeout = DEF_TIMEOUT_SECS;
+    int vb = 0;
+    uint32_t all_rn = 0;        /* Repetition Number, 0 for inactive */
+    uint64_t all_start = 0;
+    uint64_t all_last = 0;
+    int64_t ll;
+    const char * lba_op = NULL;
+    const char * num_op = NULL;
+    const char * in_op = NULL;
+    const char * device_name = NULL;
+    char * first_comma = NULL;
+    char * second_comma = NULL;
+    struct sg_simple_inquiry_resp inq_resp;
+    uint64_t addr_arr[MAX_NUM_ADDR];
+    uint32_t num_arr[MAX_NUM_ADDR];
+    uint8_t param_arr[8 + (MAX_NUM_ADDR * 16)];
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "aA:dfg:hI:Hl:n:t:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            anchor = true;
+            break;
+        case 'A':
+            first_comma = strchr(optarg, ',');
+            if (NULL == first_comma) {
+                pr2serr("--all=ST,RN[,LA] expects at least one comma in "
+                        "argument, found none\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            ll = sg_get_llnum(optarg);
+            if (ll < 0) {
+                pr2serr("unable to decode --all=ST,.... (starting LBA)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            all_start = (uint64_t)ll;
+            ll = sg_get_llnum(first_comma + 1);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("unable to decode --all=ST,RN.... (repeat number)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            all_rn = (uint32_t)ll;
+            if (0 == ll)
+                pr2serr("warning: --all=ST,RN... being ignored because RN "
+                        "is 0\n");
+            second_comma = strchr(first_comma + 1, ',');
+            if (second_comma) {
+                ll = sg_get_llnum(second_comma + 1);
+                if (ll < 0) {
+                    pr2serr("unable to decode --all=ST,NR,LA (last LBA)\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                all_last = (uint64_t)ll;
+            }
+            break;
+        case 'd':
+            dry_run = true;
+            break;
+        case 'f':
+            do_force = true;
+            break;
+        case 'g':
+            num = sscanf(optarg, "%d", &res);
+            if ((1 == num) && (res >= 0) && (res <= 63))
+                grpnum = res;
+            else {
+                pr2serr("value for '--grpnum=' must be 0 to 63\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'I':
+            in_op = optarg;
+            break;
+        case 'l':
+            lba_op = optarg;
+            break;
+        case 'n':
+            num_op = optarg;
+            break;
+        case 't':
+            timeout = sg_get_num(optarg);
+            if (timeout < 0)  {
+                pr2serr("bad argument to '--timeout'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            } else if (0 == timeout)
+                timeout = DEF_TIMEOUT_SECS;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++vb;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        vb = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        vb = 2;
+    } else
+        pr2serr("keep verbose=%d\n", vb);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (all_rn > 0) {
+        if (lba_op || num_op || in_op) {
+            pr2serr("Can't have --all= together with --lba=, --num= or "
+                    "--in=\n\n");
+            usage();
+            return SG_LIB_CONTRADICT;
+        }
+        /* here if --all= looks okay so far */
+    } else if (in_op && (lba_op || num_op)) {
+        pr2serr("expect '--in=' by itself, or both '--lba=' and "
+                "'--num='\n\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    } else if (in_op || (lba_op && num_op))
+        ;
+    else {
+        if (lba_op)
+            pr2serr("since '--lba=' is given, also need '--num='\n\n");
+        else
+            pr2serr("expect either both '--lba=' and '--num=', or "
+                    "'--in=', or '--all='\n\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+
+    if (all_rn > 0) {
+        if ((all_last > 0) && (all_start > all_last)) {
+            pr2serr("in --all=ST,RN,LA start address (ST) exceeds last "
+                    "address (LA)\n");
+            return SG_LIB_CONTRADICT;
+        }
+    } else {
+        memset(addr_arr, 0, sizeof(addr_arr));
+        memset(num_arr, 0, sizeof(num_arr));
+        addr_arr_len = 0;
+        if (lba_op && num_op) {
+            if (0 != build_lba_arr(lba_op, addr_arr, &addr_arr_len,
+                                   MAX_NUM_ADDR)) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (0 != build_num_arr(num_op, num_arr, &num_arr_len,
+                                   MAX_NUM_ADDR)) {
+                pr2serr("bad argument to '--num'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((addr_arr_len != num_arr_len) || (num_arr_len <= 0)) {
+                pr2serr("need same number of arguments to '--lba=' "
+                        "and '--num=' options\n");
+                return SG_LIB_CONTRADICT;
+            }
+        }
+        if (in_op) {
+            if (0 != build_joint_arr(in_op, addr_arr, num_arr, &addr_arr_len,
+                                     MAX_NUM_ADDR)) {
+                pr2serr("bad argument to '--in'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (addr_arr_len <= 0) {
+                pr2serr("no addresses found in '--in=' argument, file: %s\n",
+                        in_op);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+        param_len = 8 + (16 * addr_arr_len);
+        memset(param_arr, 0, param_len);
+        k = 8;
+        for (j = 0; j < addr_arr_len; ++j) {
+            sg_put_unaligned_be64(addr_arr[j], param_arr + k);
+            k += 8;
+            sg_put_unaligned_be32(num_arr[j], param_arr + k);
+            k += 4 + 4;
+        }
+        k = 0;
+        num = param_len - 2;
+        sg_put_unaligned_be16((uint16_t)num, param_arr + k);
+        k += 2;
+        num = param_len - 8;
+        sg_put_unaligned_be16((uint16_t)num, param_arr + k);
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, vb);
+    if (sg_fd < 0) {
+        ret = sg_convert_errno(-sg_fd);
+        pr2serr("open error: %s: %s\n", device_name, safe_strerror(-sg_fd));
+        goto err_out;
+    }
+    ret = sg_simple_inquiry(sg_fd, &inq_resp, true, vb);
+
+    if (all_rn > 0) {
+        bool last_retry;
+        bool to_end_of_device = false;
+        uint64_t ull;
+        uint32_t bump;
+
+        if (0 == all_last) {    /* READ CAPACITY(10 or 16) to find last */
+            uint8_t resp_buff[RCAP16_RESP_LEN];
+
+            res = sg_ll_readcap_16(sg_fd, false /* pmi */, 0 /* llba */,
+                                   resp_buff, RCAP16_RESP_LEN, true, vb);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Read capacity(16) unit attention, try again\n");
+                res = sg_ll_readcap_16(sg_fd, false, 0, resp_buff,
+                                       RCAP16_RESP_LEN, true, vb);
+            }
+            if (0 == res) {
+                if (vb > 3) {
+                    pr2serr("Read capacity(16) response:\n");
+                    hex2stderr(resp_buff, RCAP16_RESP_LEN, 1);
+                }
+                all_last = sg_get_unaligned_be64(resp_buff + 0);
+            } else if ((SG_LIB_CAT_INVALID_OP == res) ||
+                       (SG_LIB_CAT_ILLEGAL_REQ == res)) {
+                if (vb)
+                    pr2serr("Read capacity(16) not supported, try Read "
+                            "capacity(10)\n");
+                res = sg_ll_readcap_10(sg_fd, false /* pmi */, 0 /* lba */,
+                                       resp_buff, RCAP10_RESP_LEN, true,
+                                       vb);
+                if (0 == res) {
+                    if (vb > 3) {
+                        pr2serr("Read capacity(10) response:\n");
+                        hex2stderr(resp_buff, RCAP10_RESP_LEN, 1);
+                    }
+                    all_last = (uint64_t)sg_get_unaligned_be32(resp_buff + 0);
+                } else {
+                    if (res < 0)
+                        res = sg_convert_errno(-res);
+                    pr2serr("Read capacity(10) failed\n");
+                    ret = res;
+                    goto err_out;
+                }
+            } else {
+                if (res < 0)
+                    res = sg_convert_errno(-res);
+                pr2serr("Read capacity(16) failed\n");
+                ret = res;
+                goto err_out;
+            }
+            if (all_start > all_last) {
+                pr2serr("after READ CAPACITY the last block (0x%" PRIx64
+                        ") less than start address (0x%" PRIx64 ")\n",
+                        all_start, all_last);
+                ret = SG_LIB_CONTRADICT;
+                goto err_out;
+            }
+            to_end_of_device = true;
+        }
+        if (! do_force) {
+            char b[120];
+
+            printf("%s is:  %.8s  %.16s  %.4s\n", device_name,
+                   inq_resp.vendor, inq_resp.product, inq_resp.revision);
+            sg_sleep_secs(3);
+            if (to_end_of_device)
+                snprintf(b, sizeof(b), "%s from LBA 0x%" PRIx64 " to end "
+                         "(0x%" PRIx64 ")", device_name, all_start, all_last);
+            else
+                snprintf(b, sizeof(b), "%s from LBA 0x%" PRIx64 " to 0x%"
+                         PRIx64, device_name, all_start, all_last);
+            sg_warn_and_wait("UNMAP (a.k.a. trim)", b, false);
+        }
+        if (dry_run) {
+            pr2serr("Doing dry-run, would have unmapped from LBA 0x%" PRIx64
+                    " to 0x%" PRIx64 "\n    %u blocks per UNMAP command\n",
+                    all_start, all_last, all_rn);
+           goto err_out;
+        }
+        last_retry = false;
+        param_len = 8 + (16 * 1);
+        for (ull = all_start, j = 0; ull <= all_last; ull += bump, ++j) {
+            if ((all_last - ull) < all_rn)
+                bump = (uint32_t)(all_last + 1 - ull);
+            else
+                bump = all_rn;
+retry:
+            memset(param_arr, 0, param_len);
+            k = 8;
+            sg_put_unaligned_be64(ull, param_arr + k);
+            k += 8;
+            sg_put_unaligned_be32(bump, param_arr + k);
+            k = 0;
+            num = param_len - 2;
+            sg_put_unaligned_be16((uint16_t)num, param_arr + k);
+            k += 2;
+            num = param_len - 8;
+            sg_put_unaligned_be16((uint16_t)num, param_arr + k);
+            ret = sg_ll_unmap_v2(sg_fd, anchor, grpnum, timeout, param_arr,
+                                 param_len, true, (vb > 2 ? vb - 2 : 0));
+            if (last_retry)
+                break;
+            if (ret) {
+                if ((SG_LIB_LBA_OUT_OF_RANGE == ret) &&
+                    ((ull + bump) > all_last)) {
+                    pr2serr("Typical end of disk out-of-range, decrement "
+                            "count and retry\n");
+                    if (bump > 1) {
+                        --bump;
+                        last_retry = true;
+                        goto retry;
+                    }  /* if bump==1 can't do last, so we are finished */
+                }
+                break;
+            }
+        }       /* end of for loop doing unmaps */
+        if (vb)
+            pr2serr("Completed %d UNMAP commands\n", j);
+    } else {            /* --all= not given */
+        if (dry_run) {
+            pr2serr("Doing dry-run so here is 'LBA, number_of_blocks' list "
+                    "of candidates\n");
+            k = 8;
+            for (j = 0; j < addr_arr_len; ++j) {
+                printf("    0x%" PRIx64 ", 0x%u\n",
+                      sg_get_unaligned_be64(param_arr + k),
+                      sg_get_unaligned_be32(param_arr + k + 8));
+                k += (8 + 4 + 4);
+            }
+            goto err_out;
+        }
+        if (! do_force) {
+            printf("%s is:  %.8s  %.16s  %.4s\n", device_name,
+                   inq_resp.vendor, inq_resp.product, inq_resp.revision);
+            sg_sleep_secs(3);
+            printf("\nAn UNMAP (a.k.a. trim) will commence in 15 seconds\n");
+            printf("    Some data will be LOST\n");
+            printf("        Press control-C to abort\n");
+            sg_sleep_secs(5);
+            printf("\nAn UNMAP will commence in 10 seconds\n");
+            printf("    Some data will be LOST\n");
+            printf("        Press control-C to abort\n");
+            sg_sleep_secs(5);
+            printf("\nAn UNMAP (a.k.a. trim) will commence in 5 seconds\n");
+            printf("    Some data will be LOST\n");
+            printf("        Press control-C to abort\n");
+            sg_sleep_secs(7);
+        }
+        res = sg_ll_unmap_v2(sg_fd, anchor, grpnum, timeout, param_arr,
+                             param_len, true, vb);
+        ret = res;
+        err_printed = true;
+        switch (ret) {
+        case SG_LIB_CAT_NOT_READY:
+            pr2serr("UNMAP failed, device not ready\n");
+            break;
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            pr2serr("UNMAP, unit attention\n");
+            break;
+        case SG_LIB_CAT_ABORTED_COMMAND:
+            pr2serr("UNMAP, aborted command\n");
+            break;
+        case SG_LIB_CAT_INVALID_OP:
+            pr2serr("UNMAP not supported\n");
+            break;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            pr2serr("bad field in UNMAP cdb\n");
+            break;
+        default:
+            err_printed = false;
+            break;
+        }
+    }
+
+err_out:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if ((0 == vb) && (! err_printed)) {
+        if (! sg_if_can2stderr("sg_unmap failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_verify.c b/src/sg_verify.c
new file mode 100644
index 0000000..ddd71c8
--- /dev/null
+++ b/src/sg_verify.c
@@ -0,0 +1,475 @@
+/*
+ * Copyright (c) 2004-2020 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <string.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_pr2serr.h"
+
+/* A utility program for the Linux OS SCSI subsystem.
+ *
+ * This program issues the SCSI VERIFY(10) or VERIFY(16) command to the given
+ * SCSI block device.
+ *
+ * N.B. This utility should, but doesn't, check the logical block size with
+ * the SCSI READ CAPACITY command. It is up to the user to make sure that
+ * the count of blocks requested and the number of bytes transferred (when
+ * BYTCHK>0) are "in sync". That caclculation is somewhat complicated by
+ * the possibility of protection data (DIF).
+ */
+
+static const char * version_str = "1.27 20201029";    /* sbc4r17 */
+
+#define ME "sg_verify: "
+
+#define EBUFF_SZ 256
+
+
+static struct option long_options[] = {
+        {"0", no_argument, 0, '0'},
+        {"16", no_argument, 0, 'S'},
+        {"bpc", required_argument, 0, 'b'},
+        {"bytchk", required_argument, 0, 'B'},  /* 4 backward compatibility */
+        {"count", required_argument, 0, 'c'},
+        {"dpo", no_argument, 0, 'd'},
+        {"ebytchk", required_argument, 0, 'E'}, /* extended bytchk (2 bits) */
+        {"group", required_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"in", required_argument, 0, 'i'},
+        {"lba", required_argument, 0, 'l'},
+        {"nbo", required_argument, 0, 'n'},     /* misspelling, legacy */
+        {"ndo", required_argument, 0, 'n'},
+        {"quiet", no_argument, 0, 'q'},
+        {"readonly", no_argument, 0, 'r'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"vrprotect", required_argument, 0, 'P'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_verify [--0] [--16] [--bpc=BPC] [--count=COUNT] "
+            "[--dpo]\n"
+            "                 [--ebytchk=BCH] [--ff] [--group=GN] [--help] "
+            "[--in=IF]\n"
+            "                 [--lba=LBA] [--ndo=NDO] [--quiet] "
+            "[--readonly]\n"
+            "                 [--verbose] [--version] [--vrprotect=VRP] "
+            "DEVICE\n"
+            "  where:\n"
+            "    --0|-0              fill buffer with zeros (don't read "
+            "stdin)\n"
+            "    --16|-S             use VERIFY(16) (def: use "
+            "VERIFY(10) )\n"
+            "    --bpc=BPC|-b BPC    max blocks per verify command "
+            "(def: 128)\n"
+            "    --count=COUNT|-c COUNT    count of blocks to verify "
+            "(def: 1).\n"
+            "    --dpo|-d            disable page out (cache retention "
+            "priority)\n"
+            "    --ebytchk=BCH|-E BCH    sets BYTCHK value, either 1, 2 "
+            "or 3 (def: 0).\n"
+            "                            BCH overrides BYTCHK=1 set by "
+            "'--ndo='. If\n"
+            "                            BCH is 3 then NDO must be the LBA "
+            "size\n"
+            "                            (plus protection size if DIF "
+            "active)\n"
+            "    --ff|-f             fill buffer with 0xff bytes (don't read "
+            "stdin)\n"
+            "    --group=GN|-g GN    set group number field to GN (def: 0)\n"
+            "    --help|-h           print out usage message\n"
+            "    --in=IF|-i IF       input from file called IF (def: "
+            "stdin)\n"
+            "                        only active if --ebytchk=BCH given\n"
+            "    --lba=LBA|-l LBA    logical block address to start "
+            "verify (def: 0)\n"
+            "    --ndo=NDO|-n NDO    NDO is number of bytes placed in "
+            "data-out buffer.\n"
+            "                        These are fetched from IF (or "
+            "stdin) and used\n"
+            "                        to verify the device data against. "
+            "Forces\n"
+            "                        --bpc=COUNT. Sets BYTCHK (byte check) "
+            "to 1\n"
+            "    --quiet|-q          suppress miscompare report to stderr, "
+            "still\n"
+            "                        causes an exit status of 14\n"
+            "    --readonly|-r       open DEVICE read-only (def: open it "
+            "read-write)\n"
+            "    --verbose|-v        increase verbosity\n"
+            "    --version|-V        print version string and exit\n"
+            "    --vrprotect=VRP|-P VRP    set vrprotect field to VRP "
+            "(def: 0)\n\n"
+            "Performs one or more SCSI VERIFY(10) or SCSI VERIFY(16) "
+            "commands. sbc3r34\nmade the BYTCHK field two bits wide "
+            "(it was a single bit).\n");
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool bpc_given = false;
+    bool dpo = false;
+    bool ff_given = false;
+    bool got_stdin = false;
+    bool quiet = false;
+    bool readonly = false;
+    bool verbose_given = false;
+    bool verify16 = false;
+    bool version_given = false;
+    bool zero_given = false;
+    int res, c, num, nread, infd;
+    int sg_fd = -1;
+    int bpc = 128;
+    int group = 0;
+    int bytchk = 0;
+    int ndo = 0;        /* number of bytes in data-out buffer */
+    int verbose = 0;
+    int ret = 0;
+    int vrprotect = 0;
+    unsigned int info = 0;
+    int64_t count = 1;
+    int64_t ll;
+    int64_t orig_count;
+    uint64_t info64 = 0;
+    uint64_t lba = 0;
+    uint64_t orig_lba;
+    uint8_t * ref_data = NULL;
+    uint8_t * free_ref_data = NULL;
+    const char * device_name = NULL;
+    const char * file_name = NULL;
+    const char * vc;
+    char ebuff[EBUFF_SZ];
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "0b:B:c:dE:fg:hi:l:n:P:qrSvV",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case '0':
+            zero_given = true;
+            break;
+        case 'b':
+            bpc = sg_get_num(optarg);
+            if (bpc < 1) {
+                pr2serr("bad argument to '--bpc'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            bpc_given = true;
+            break;
+        case 'c':
+            count = sg_get_llnum(optarg);
+            if (count < 0) {
+                pr2serr("bad argument to '--count'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'd':
+            dpo = true;
+            break;
+        case 'E':
+            bytchk = sg_get_num(optarg);
+            if ((bytchk < 0) || (bytchk > 3)) {
+                pr2serr("bad argument to '--ebytchk'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'f':
+            ff_given = true;
+            break;
+        case 'g':
+            group = sg_get_num(optarg);
+            if ((group < 0) || (group > 63)) {
+                pr2serr("bad argument to '--group'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+            file_name = optarg;
+            break;
+        case 'l':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            lba = (uint64_t)ll;
+            break;
+        case 'n':       /* number of bytes in data-out buffer */
+        case 'B':       /* undocumented, old --bytchk=NDO option */
+            ndo = sg_get_num(optarg);
+            if (ndo < 1) {
+                pr2serr("bad argument to '--ndo'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'P':
+            vrprotect = sg_get_num(optarg);
+            if (-1 == vrprotect) {
+                pr2serr("bad argument to '--vrprotect'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((vrprotect < 0) || (vrprotect > 7)) {
+                pr2serr("'--vrprotect' requires a value from 0 to 7 "
+                        "(inclusive)\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'q':
+            quiet = true;
+            break;
+        case 'r':
+            readonly = true;
+            break;
+        case 'S':
+            verify16 = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (ndo > 0) {
+        if (0 == bytchk)
+            bytchk = 1;
+        if (bpc_given && (bpc != count))
+            pr2serr("'bpc' argument ignored, using --bpc=%" PRIu64 "\n",
+                    count);
+        if (count > 0x7fffffffLL) {
+            pr2serr("count exceed 31 bits, way too large\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+#if 0
+        if ((3 == bytchk) && (1 != count)) {
+            pr2serr("count must be 1 when bytchk=3\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        // bpc = (int)count;
+#endif
+    } else if (bytchk > 0) {
+        pr2serr("when the 'ebytchk=BCH' option is given, then '--ndo=NDO' "
+                "must also be given\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if ((zero_given || ff_given) && file_name) {
+        pr2serr("giving --0 or --ff is not compatible with --if=%s\n",
+                file_name);
+        return SG_LIB_CONTRADICT;
+    }
+
+    if ((bpc > 0xffff) && (! verify16)) {
+        pr2serr("'%s' exceeds 65535, so use VERIFY(16)\n",
+                (ndo > 0) ? "count" : "bpc");
+        verify16 = true;
+    }
+    if (((lba + count - 1) > 0xffffffffLLU) && (! verify16)) {
+        pr2serr("'lba' exceed 32 bits, so use VERIFY(16)\n");
+        verify16 = true;
+    }
+    if ((group > 0) && (! verify16))
+        pr2serr("group number ignored with VERIFY(10) command, use the --16 "
+                "option\n");
+
+    orig_count = count;
+    orig_lba = lba;
+
+    if (ndo > 0) {
+        ref_data = (uint8_t *)sg_memalign(ndo, 0, &free_ref_data, verbose > 4);
+        if (NULL == ref_data) {
+            pr2serr("failed to allocate %d byte buffer\n", ndo);
+            ret = sg_convert_errno(ENOMEM);
+            goto err_out;
+        }
+        if (ff_given)
+            memset(ref_data, 0xff, ndo);
+        if (zero_given || ff_given)
+            goto skip;
+        if ((NULL == file_name) || (0 == strcmp(file_name, "-"))) {
+            got_stdin = true;
+            infd = STDIN_FILENO;
+            if (sg_set_binary_mode(STDIN_FILENO) < 0)
+                perror("sg_set_binary_mode");
+        } else {
+            if ((infd = open(file_name, O_RDONLY)) < 0) {
+                ret = sg_convert_errno(errno);
+                snprintf(ebuff, EBUFF_SZ,
+                         ME "could not open %s for reading", file_name);
+                perror(ebuff);
+                goto err_out;
+            } else if (sg_set_binary_mode(infd) < 0)
+                perror("sg_set_binary_mode");
+        }
+        if (verbose && got_stdin)
+                pr2serr("about to wait on STDIN\n");
+        for (nread = 0; nread < ndo; nread += res) {
+            res = read(infd, ref_data + nread, ndo - nread);
+            if (res <= 0) {
+                ret = sg_convert_errno(errno);
+                pr2serr("reading from %s failed at file offset=%d\n",
+                        (got_stdin ? "stdin" : file_name), nread);
+                goto err_out;
+            }
+        }
+        if (! got_stdin)
+            close(infd);
+    }
+skip:
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto err_out;
+    }
+    sg_fd = sg_cmds_open_device(device_name, readonly, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    vc = verify16 ? "VERIFY(16)" : "VERIFY(10)";
+    for (; count > 0; count -= bpc, lba += bpc) {
+        num = (count > bpc) ? bpc : count;
+        if (verify16)
+            res = sg_ll_verify16(sg_fd, vrprotect, dpo, bytchk,
+                                 lba, num, group, ref_data,
+                                 ndo, &info64, !quiet , verbose);
+        else
+            res = sg_ll_verify10(sg_fd, vrprotect, dpo, bytchk,
+                                 (unsigned int)lba, num, ref_data,
+                                 ndo, &info, !quiet, verbose);
+        if (0 != res) {
+            char b[80];
+
+            ret = res;
+            switch (res) {
+            case SG_LIB_CAT_ILLEGAL_REQ:
+                pr2serr("bad field in %s cdb, near lba=0x%" PRIx64 "\n", vc,
+                        lba);
+                break;
+            case SG_LIB_CAT_MEDIUM_HARD:
+                pr2serr("%s medium or hardware error near lba=0x%" PRIx64 "\n",
+                        vc, lba);
+                break;
+            case SG_LIB_CAT_MEDIUM_HARD_WITH_INFO:
+                if (verify16)
+                    pr2serr("%s medium or hardware error, reported lba=0x%"
+                            PRIx64 "\n", vc, info64);
+                else
+                    pr2serr("%s medium or hardware error, reported lba=0x%x\n",
+                            vc, info);
+                break;
+            case SG_LIB_CAT_MISCOMPARE:
+                if ((0 == quiet) || verbose)
+                    pr2serr("%s MISCOMPARE: started at LBA 0x%" PRIx64 "\n",
+                            vc, lba);
+                break;
+            default:
+                sg_get_category_sense_str(res, sizeof(b), b, verbose);
+                pr2serr("%s: %s\n", vc, b);
+                pr2serr("    failed near lba=%" PRIu64 " [0x%" PRIx64 "]\n",
+                        lba, lba);
+                break;
+            }
+            break;
+        }
+    }
+
+    if (verbose && (0 == ret) && (orig_count > 1))
+        pr2serr("Verified %" PRId64 " [0x%" PRIx64 "] blocks from lba %" PRIu64
+                " [0x%" PRIx64 "]\n    without error\n", orig_count,
+                (uint64_t)orig_count, orig_lba, orig_lba);
+
+ err_out:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (free_ref_data)
+        free(free_ref_data);
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_verify failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_vpd.c b/src/sg_vpd.c
new file mode 100644
index 0000000..c8900fb
--- /dev/null
+++ b/src/sg_vpd.c
@@ -0,0 +1,2770 @@
+/*
+ * Copyright (c) 2006-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+#include "sg_vpd_common.h"      /* shared with sg_inq */
+
+/* This utility program was originally written for the Linux OS SCSI subsystem.
+
+   This program fetches Vital Product Data (VPD) pages from the given
+   device and outputs it as directed. VPD pages are obtained via a
+   SCSI INQUIRY command. Most of the data in this program is obtained
+   from the SCSI SPC-4 document at https://www.t10.org .
+
+*/
+
+static const char * version_str = "1.83 20220915";  /* spc6r06 + sbc5r03 */
+
+#define MY_NAME "sg_vpd"
+
+/* Device identification VPD page associations */
+#define VPD_ASSOC_LU 0
+#define VPD_ASSOC_TPORT 1
+#define VPD_ASSOC_TDEVICE 2
+
+/* values for selection one or more associations (2**vpd_assoc),
+   except _AS_IS */
+#define VPD_DI_SEL_LU 1
+#define VPD_DI_SEL_TPORT 2
+#define VPD_DI_SEL_TARGET 4
+#define VPD_DI_SEL_AS_IS 32
+
+#define DEF_ALLOC_LEN 252
+#define MIN_MAXLEN 16
+#define MX_ALLOC_LEN (0xc000 + 0x80)
+#define VPD_ATA_INFO_LEN  572
+
+#define SENSE_BUFF_LEN  64       /* Arbitrary, could be larger */
+#define INQUIRY_CMD     0x12
+#define INQUIRY_CMDLEN  6
+#define DEF_PT_TIMEOUT  60       /* 60 seconds */
+
+
+uint8_t * rsp_buff;
+
+static int svpd_decode_t10(int sg_fd, struct opts_t * op, sgj_opaque_p jop,
+                           int subvalue, int off, const char * prefix);
+static int svpd_unable_to_decode(int sg_fd, struct opts_t * op, int subvalue,
+                                 int off);
+
+static int filter_dev_ids(const char * print_if_found, int num_leading,
+                          uint8_t * buff, int len, int m_assoc,
+                          struct opts_t * op, sgj_opaque_p jop);
+
+static const int rsp_buff_sz = MX_ALLOC_LEN + 2;
+
+static uint8_t * free_rsp_buff;
+
+static struct option long_options[] = {
+        {"all", no_argument, 0, 'a'},
+        {"enumerate", no_argument, 0, 'e'},
+        {"examine", no_argument, 0, 'E'},
+        {"force", no_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"ident", no_argument, 0, 'i'},
+        {"inhex", required_argument, 0, 'I'},
+        {"json", optional_argument, 0, 'j'},
+        {"long", no_argument, 0, 'l'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"page", required_argument, 0, 'p'},
+        {"quiet", no_argument, 0, 'q'},
+        {"raw", no_argument, 0, 'r'},
+        {"sinq_inraw", required_argument, 0, 'Q'},
+        {"sinq-inraw", required_argument, 0, 'Q'},
+        {"vendor", required_argument, 0, 'M'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+/* arranged in alphabetical order by acronym */
+static struct svpd_values_name_t standard_vpd_pg[] = {
+    {VPD_AUTOMATION_DEV_SN, 0, 1, "adsn", "Automation device serial "
+     "number (SSC)"},
+    {VPD_ATA_INFO, 0, -1, "ai", "ATA information (SAT)"},
+    {VPD_ASCII_OP_DEF, 0, -1, "aod",
+     "ASCII implemented operating definition (obsolete)"},
+    {VPD_BLOCK_DEV_CHARS, 0, 0, "bdc", "Block device characteristics "
+     "(SBC)"},
+    {VPD_BLOCK_DEV_C_EXTENS, 0, 0, "bdce", "Block device characteristics "
+     "extension (SBC)"},
+    {VPD_BLOCK_LIMITS, 0, 0, "bl", "Block limits (SBC)"},
+    {VPD_BLOCK_LIMITS_EXT, 0, 0, "ble", "Block limits extension (SBC)"},
+    {VPD_CFA_PROFILE_INFO, 0, 0, "cfa", "CFA profile information"},
+    {VPD_CON_POS_RANGE, 0, 0, "cpr", "Concurrent positioning ranges"},
+    {VPD_DEVICE_CONSTITUENTS, 0, -1, "dc", "Device constituents"},
+    {VPD_DEVICE_ID, 0, -1, "di", "Device identification"},
+    {VPD_DEVICE_ID, VPD_DI_SEL_AS_IS, -1, "di_asis", "Like 'di' "
+     "but designators ordered as found"},
+    {VPD_DEVICE_ID, VPD_DI_SEL_LU, -1, "di_lu", "Device identification, "
+     "lu only"},
+    {VPD_DEVICE_ID, VPD_DI_SEL_TPORT, -1, "di_port", "Device "
+     "identification, target port only"},
+    {VPD_DEVICE_ID, VPD_DI_SEL_TARGET, -1, "di_target", "Device "
+     "identification, target device only"},
+    {VPD_DTDE_ADDRESS, 0, 1, "dtde",
+     "Data transfer device element address (SSC)"},
+    {VPD_EXT_INQ, 0, -1, "ei", "Extended inquiry data"},
+    {VPD_FORMAT_PRESETS, 0, 0, "fp", "Format presets"},
+    {VPD_IMP_OP_DEF, 0, -1, "iod",
+     "Implemented operating definition (obsolete)"},
+    {VPD_LB_PROTECTION, 0, 0, "lbpro", "Logical block protection (SSC)"},
+    {VPD_LB_PROVISIONING, 0, 0, "lbpv", "Logical block provisioning (SBC)"},
+    {VPD_MAN_ASS_SN, 0, 1, "mas", "Manufacturer assigned serial number (SSC)"},
+    {VPD_MAN_ASS_SN, 0, 0x12, "masa",
+     "Manufacturer assigned serial number (ADC)"},
+    {VPD_MAN_NET_ADDR, 0, -1, "mna", "Management network addresses"},
+    {VPD_MODE_PG_POLICY, 0, -1, "mpp", "Mode page policy"},
+    {VPD_OSD_INFO, 0, 0x11, "oi", "OSD information"},
+    {VPD_POWER_CONDITION, 0, -1, "pc", "Power condition"},/* "po" in sg_inq */
+    {VPD_POWER_CONSUMPTION, 0, -1, "psm", "Power consumption"},
+    {VPD_PROTO_LU, 0, -1, "pslu", "Protocol-specific logical unit "
+     "information"},
+    {VPD_PROTO_PORT, 0, -1, "pspo", "Protocol-specific port information"},
+    {VPD_REFERRALS, 0, 0, "ref", "Referrals (SBC)"},
+    {VPD_SA_DEV_CAP, 0, 1, "sad",
+     "Sequential access device capabilities (SSC)"},
+    {VPD_SUP_BLOCK_LENS, 0, 0, "sbl", "Supported block lengths and "
+     "protection types (SBC)"},
+    {VPD_SCSI_FEATURE_SETS, 0, -1, "sfs", "SCSI feature sets"},
+    {VPD_SOFTW_INF_ID, 0, -1, "sii", "Software interface identification"},
+    {VPD_NOPE_WANT_STD_INQ, 0, -1, "sinq", "Standard inquiry data format"},
+    {VPD_UNIT_SERIAL_NUM, 0, -1, "sn", "Unit serial number"},
+    {VPD_SCSI_PORTS, 0, -1, "sp", "SCSI ports"},
+    {VPD_SECURITY_TOKEN, 0, 0x11, "st", "Security token (OSD)"},
+    {VPD_SUPPORTED_VPDS, 0, -1, "sv", "Supported VPD pages"},
+    {VPD_TA_SUPPORTED, 0, 1, "tas", "TapeAlert supported flags (SSC)"},
+    {VPD_3PARTY_COPY, 0, -1, "tpc", "Third party copy"},
+    {VPD_ZBC_DEV_CHARS, 0, -1, "zbdch", "Zoned block device characteristics"},
+        /* Use pdt of -1 since this page both for pdt=0 and pdt=0x14 */
+    {0, 0, 0, NULL, NULL},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_vpd  [--all] [--enumerate] [--examine] [--force] "
+            "[--help] [--hex]\n"
+            "               [--ident] [--inhex=FN] [--long] [--maxlen=LEN] "
+            "[--page=PG]\n"
+            "               [--quiet] [--raw] [--sinq_inraw=RFN] "
+            "[--vendor=VP] [--verbose]\n"
+            "               [--version] DEVICE\n");
+    pr2serr("  where:\n"
+            "    --all|-a        output all pages listed in the supported "
+            "pages VPD\n"
+            "                    page\n"
+            "    --enumerate|-e    enumerate known VPD pages names (ignore "
+            "DEVICE),\n"
+            "                      can be used with --page=num to search\n"
+            "    --examine|-E    starting at 0x80 scan pages code to 0xff\n"
+            "    --force|-f      skip VPD page 0 (supported VPD pages) "
+            "checking\n"
+            "    --help|-h       output this usage message then exit\n"
+            "    --hex|-H        output page in ASCII hexadecimal\n"
+            "    --ident|-i      output device identification VPD page, "
+            "twice for\n"
+            "                    short logical unit designator (equiv: "
+            "'-qp di_lu')\n"
+            "    --inhex=FN|-I FN    read ASCII hex from file FN instead of "
+            "DEVICE;\n"
+            "                        if used with --raw then read binary "
+            "from FN\n"
+            "    --json[=JO]|-j[JO]    output in JSON instead of human "
+            "readable text.\n"
+            "                          Use --json=? for JSON help\n"
+            "    --long|-l       perform extra decoding\n"
+            "    --maxlen=LEN|-m LEN    max response length (allocation "
+            "length in cdb)\n"
+            "                           (def: 0 -> 252 bytes)\n"
+            "    --page=PG|-p PG    fetch VPD page where PG is an "
+            "acronym, or a decimal\n"
+            "                       number unless hex indicator "
+            "is given (e.g. '0x83');\n"
+            "                       can also take PG,VP as an "
+            "operand\n"
+            "    --quiet|-q      suppress some output when decoding\n"
+            "    --raw|-r        output page in binary; if --inhex=FN is "
+            "also\n"
+            "                    given, FN is in binary (else FN is in "
+            "hex)\n"
+            "    --sinq_inraw=RFN|-Q RFN    read raw (binary) standard "
+            "INQUIRY\n"
+            "                               response from the RFN filename\n"
+            "    --vendor=VP|-M VP    vendor/product abbreviation [or "
+            "number]\n"
+            "    --verbose|-v    increase verbosity\n"
+            "    --version|-V    print version string and exit\n\n"
+            "Fetch Vital Product Data (VPD) page using SCSI INQUIRY or "
+            "decodes VPD\npage response held in file FN. To list available "
+            "pages use '-e'. Also\n'-p -1' or '-p sinq' yields the standard "
+            "INQUIRY response.\n");
+}
+
+static const struct svpd_values_name_t *
+sdp_get_vpd_detail(int page_num, int subvalue, int pdt)
+{
+    const struct svpd_values_name_t * vnp;
+    int sv, ty;
+
+    sv = (subvalue < 0) ? 1 : 0;
+    ty = (pdt < 0) ? 1 : 0;
+    for (vnp = standard_vpd_pg; vnp->acron; ++vnp) {
+        if ((page_num == vnp->value) &&
+            (sv || (subvalue == vnp->subvalue)) &&
+            (ty || (pdt == vnp->pdt)))
+            return vnp;
+    }
+    if (! ty)
+        return sdp_get_vpd_detail(page_num, subvalue, -1);
+    if (! sv)
+        return sdp_get_vpd_detail(page_num, -1, -1);
+    return NULL;
+}
+
+static const struct svpd_values_name_t *
+sdp_find_vpd_by_acron(const char * ap)
+{
+    const struct svpd_values_name_t * vnp;
+
+    for (vnp = standard_vpd_pg; vnp->acron; ++vnp) {
+        if (0 == strcmp(vnp->acron, ap))
+            return vnp;
+    }
+    return NULL;
+}
+
+static void
+enumerate_vpds(int standard, int vendor)
+{
+    const struct svpd_values_name_t * vnp;
+
+    if (standard) {
+        for (vnp = standard_vpd_pg; vnp->acron; ++vnp) {
+            if (vnp->name) {
+                if (vnp->value < 0)
+                    printf("  %-10s -1        %s\n", vnp->acron, vnp->name);
+                else
+                    printf("  %-10s 0x%02x      %s\n", vnp->acron, vnp->value,
+                       vnp->name);
+            }
+        }
+    }
+    if (vendor)
+        svpd_enumerate_vendor(-2);
+}
+
+static int
+count_standard_vpds(int vpd_pn)
+{
+    const struct svpd_values_name_t * vnp;
+    int matches = 0;
+
+    for (vnp = standard_vpd_pg; vnp->acron; ++vnp) {
+        if ((vpd_pn == vnp->value) && vnp->name) {
+            if (0 == matches)
+                printf("Matching standard VPD pages:\n");
+            ++matches;
+            if (vnp->value < 0)
+                printf("  %-10s -1        %s\n", vnp->acron, vnp->name);
+            else
+                printf("  %-10s 0x%02x      %s\n", vnp->acron, vnp->value,
+                   vnp->name);
+        }
+    }
+    return matches;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+/* Assume index is less than 16 */
+static const char * sg_ansi_version_arr[16] =
+{
+    "no conformance claimed",
+    "SCSI-1",           /* obsolete, ANSI X3.131-1986 */
+    "SCSI-2",           /* obsolete, ANSI X3.131-1994 */
+    "SPC",              /* withdrawn, ANSI INCITS 301-1997 */
+    "SPC-2",            /* ANSI INCITS 351-2001, ISO/IEC 14776-452 */
+    "SPC-3",            /* ANSI INCITS 408-2005, ISO/IEC 14776-453 */
+    "SPC-4",            /* ANSI INCITS 513-2015 */
+    "SPC-5",            /* ANSI INCITS 502-2020 */
+    "ecma=1, [8h]",
+    "ecma=1, [9h]",
+    "ecma=1, [Ah]",
+    "ecma=1, [Bh]",
+    "reserved [Ch]",
+    "reserved [Dh]",
+    "reserved [Eh]",
+    "reserved [Fh]",
+};
+
+static void
+std_inq_decode(uint8_t * b, int len, struct opts_t * op, sgj_opaque_p jop)
+{
+    uint8_t ver;
+    int pqual, pdt, hp, j, n;
+    sgj_state * jsp = &op->json_st;
+    const char * cp;
+    char c[256];
+    static const int clen = sizeof(c);
+    static const char * np = "Standard INQUIRY data format:";
+
+    if (len < 4) {
+        pr2serr("%s: len [%d] too short\n", __func__, len);
+        return;
+    }
+    pqual = (b[0] & 0xe0) >> 5;
+    pdt = b[0] & PDT_MASK;
+    hp = (b[1] >> 4) & 0x3;
+    ver = b[2];
+    sgj_pr_hr(jsp, "%s", np);
+    if (0 == pqual)
+        sgj_pr_hr(jsp, "\n");
+    else {
+        cp = pqual_str(pqual);
+
+        if (pqual < 3)
+            sgj_pr_hr(jsp, " [PQ indicates %s]\n", cp);
+        else
+            sgj_pr_hr(jsp, " [PQ indicates %s [0x%x] ]\n", cp, pqual);
+    }
+    sgj_pr_hr(jsp, "  PQual=%d  PDT=%d  RMB=%d  LU_CONG=%d  hot_pluggable="
+              "%d  version=0x%02x  [%s]\n", pqual, pdt, !!(b[1] & 0x80),
+              !!(b[1] & 0x40), hp, ver, sg_ansi_version_arr[ver & 0xf]);
+    sgj_pr_hr(jsp, "  [AERC=%d]  [TrmTsk=%d]  NormACA=%d  HiSUP=%d "
+           " Resp_data_format=%d\n",
+           !!(b[3] & 0x80), !!(b[3] & 0x40), !!(b[3] & 0x20),
+           !!(b[3] & 0x10), b[3] & 0x0f);
+    if (len < 5)
+        goto skip1;
+    j = b[4] + 5;
+    if (op->verbose > 2)
+        pr2serr(">> requested %d bytes, %d bytes available\n", len, j);
+    sgj_pr_hr(jsp, "  SCCS=%d  ACC=%d  TPGS=%d  3PC=%d  Protect=%d  "
+              "[BQue=%d]\n", !!(b[5] & 0x80), !!(b[5] & 0x40),
+              ((b[5] & 0x30) >> 4), !!(b[5] & 0x08), !!(b[5] & 0x01),
+              !!(b[6] & 0x80));
+    n = 0;
+    n += sg_scnpr(c + n, clen - n, "EncServ=%d  ", !!(b[6] & 0x40));
+    if (b[6] & 0x10)
+        n += sg_scnpr(c + n, clen - n, "MultiP=1 (VS=%d)  ", !!(b[6] & 0x20));
+    else
+        n += sg_scnpr(c + n, clen - n, "MultiP=0  ");
+    n += sg_scnpr(c + n, clen - n, "[MChngr=%d]  [ACKREQQ=%d]  Addr16=%d",
+                  !!(b[6] & 0x08), !!(b[6] & 0x04), !!(b[6] & 0x01));
+    sgj_pr_hr(jsp, "  %s\n", c);
+    sgj_pr_hr(jsp, "  [RelAdr=%d]  WBus16=%d  Sync=%d  [Linked=%d]  "
+              "[TranDis=%d]  CmdQue=%d\n", !!(b[7] & 0x80), !!(b[7] & 0x20),
+              !!(b[7] & 0x10), !!(b[7] & 0x08), !!(b[7] & 0x04),
+              !!(b[7] & 0x02));
+    if (len < 36)
+        goto skip1;
+    sgj_pr_hr(jsp, "  %s: %.8s\n", t10_vendor_id_hr, b + 8);
+    sgj_pr_hr(jsp, "  %s: %.16s\n", product_id_hr, b + 16);
+    sgj_pr_hr(jsp, "  %s: %.4s\n", product_rev_lev_hr, b + 32);
+skip1:
+    if (! jsp->pr_as_json || (len < 8))
+        return;
+    std_inq_decode_js(b, len, op, jop);
+}
+
+/* VPD_DEVICE_ID 0x83 ["di, di_asis, di_lu, di_port, di_target"] */
+static void
+device_id_vpd_variants(uint8_t * buff, int len, int subvalue,
+                       struct opts_t * op, sgj_opaque_p jap)
+{
+    int m_a, blen;
+    uint8_t * b;
+
+    if (len < 4) {
+        pr2serr("Device identification VPD page length too short=%d\n", len);
+        return;
+    }
+    blen = len - 4;
+    b = buff + 4;
+    m_a = -1;
+    if (0 == subvalue) {
+        filter_dev_ids(sg_get_desig_assoc_str(VPD_ASSOC_LU), 0, b, blen,
+                       VPD_ASSOC_LU, op, jap);
+        filter_dev_ids(sg_get_desig_assoc_str(VPD_ASSOC_TPORT), 0, b, blen,
+                       VPD_ASSOC_TPORT, op, jap);
+        filter_dev_ids(sg_get_desig_assoc_str(VPD_ASSOC_TDEVICE), 0, b, blen,
+                       VPD_ASSOC_TDEVICE, op, jap);
+    } else if (VPD_DI_SEL_AS_IS == subvalue)
+        filter_dev_ids(NULL, 0, b, blen, m_a, op, jap);
+    else {
+        if (VPD_DI_SEL_LU & subvalue)
+            filter_dev_ids(sg_get_desig_assoc_str(VPD_ASSOC_LU), 0, b, blen,
+                           VPD_ASSOC_LU, op, jap);
+        if (VPD_DI_SEL_TPORT & subvalue)
+            filter_dev_ids(sg_get_desig_assoc_str(VPD_ASSOC_TPORT), 0, b,
+                           blen, VPD_ASSOC_TPORT, op, jap);
+        if (VPD_DI_SEL_TARGET & subvalue)
+            filter_dev_ids(sg_get_desig_assoc_str(VPD_ASSOC_TDEVICE), 0,
+                           b, blen, VPD_ASSOC_TDEVICE, op, jap);
+    }
+}
+
+static void             /* VPD_SUPPORTED_VPDS  ["sv"] */
+decode_supported_vpd_4vpd(uint8_t * buff, int len, struct opts_t * op,
+                          sgj_opaque_p jap)
+{
+    uint8_t pn;
+    int k, rlen, pdt;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    const struct svpd_values_name_t * vnp;
+    uint8_t * bp;
+    char b[144];
+    static const int blen = sizeof(b);
+    static const char * svps = "Supported VPD pages";
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    pdt = PDT_MASK & buff[0];
+    rlen = buff[3] + 4;
+    if (rlen > len)
+        pr2serr("%s VPD page truncated, indicates %d, got %d\n", svps, rlen,
+                len);
+    else
+        len = rlen;
+    if (len < 4) {
+        pr2serr("%s VPD page length too short=%d\n", svps, len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+
+    for (k = 0; k < len; ++k) {
+        pn = bp[k];
+        snprintf(b, blen, "0x%02x", pn);
+        vnp = sdp_get_vpd_detail(pn, -1, pdt);
+        if (vnp) {
+            if (op->do_long)
+                sgj_pr_hr(jsp, "  %s  %s [%s]\n", b, vnp->name, vnp->acron);
+            else
+                sgj_pr_hr(jsp, "  %s [%s]\n", vnp->name, vnp->acron);
+        } else if (op->vend_prod_num >= 0) {
+            vnp = svpd_find_vendor_by_num(pn, op->vend_prod_num);
+            if (vnp) {
+                if (op->do_long)
+                    sgj_pr_hr(jsp, "  %s  %s [%s]\n", b, vnp->name,
+                              vnp->acron);
+                else
+                    sgj_pr_hr(jsp, "  %s [%s]\n", vnp->name, vnp->acron);
+            } else
+                sgj_pr_hr(jsp, "  %s\n", b);
+        } else
+            sgj_pr_hr(jsp, "  %s\n", b);
+        if (jsp->pr_as_json) {
+            jo2p = sgj_new_unattached_object_r(jsp);
+            sgj_js_nv_i(jsp, jo2p, "i", pn);
+            sgj_js_nv_s(jsp, jo2p, "hex", b + 2);
+            if (vnp) {
+                sgj_js_nv_s(jsp, jo2p, "name", vnp->name);
+                sgj_js_nv_s(jsp, jo2p, "acronym", vnp->acron);
+            } else {
+                sgj_js_nv_s(jsp, jo2p, "name", "unknown");
+                sgj_js_nv_s(jsp, jo2p, "acronym", "unknown");
+            }
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+        }
+    }
+}
+
+/* VPD_SCSI_PORTS     0x88  ["sp"] */
+static void
+decode_scsi_ports_vpd_4vpd(uint8_t * buff, int len, struct opts_t * op,
+                           sgj_opaque_p jap)
+{
+    int k, bump, rel_port, ip_tid_len, tpd_len;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    sgj_opaque_p ja2p = NULL;
+    uint8_t * bp;
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    if (len < 4) {
+        pr2serr("SCSI Ports VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        rel_port = sg_get_unaligned_be16(bp + 2);
+        sgj_pr_hr(jsp, "  Relative port=%d\n", rel_port);
+        jo2p = sgj_new_unattached_object_r(jsp);
+        sgj_js_nv_i(jsp, jo2p, "relative_port", rel_port);
+        ip_tid_len = sg_get_unaligned_be16(bp + 6);
+        bump = 8 + ip_tid_len;
+        if ((k + bump) > len) {
+            pr2serr("SCSI Ports VPD page, short descriptor "
+                    "length=%d, left=%d\n", bump, (len - k));
+            return;
+        }
+        if (ip_tid_len > 0) {
+            if (op->do_hex > 1) {
+                sgj_pr_hr(jsp, "    Initiator port transport id:\n");
+                hex2stdout((bp + 8), ip_tid_len, 1);
+            } else {
+                char b[1024];
+
+                sg_decode_transportid_str("    ", bp + 8, ip_tid_len,
+                                          true, sizeof(b), b);
+                if (jsp->pr_as_json)
+                    sgj_js_nv_s(jsp, jo2p, "initiator_port_transport_id", b);
+                sgj_pr_hr(jsp, "%s",
+                          sg_decode_transportid_str("    ", bp + 8,
+                                            ip_tid_len, true, sizeof(b), b));
+            }
+        }
+        tpd_len = sg_get_unaligned_be16(bp + bump + 2);
+        if ((k + bump + tpd_len + 4) > len) {
+            pr2serr("SCSI Ports VPD page, short descriptor(tgt) "
+                    "length=%d, left=%d\n", bump, (len - k));
+            return;
+        }
+        if (tpd_len > 0) {
+            if (op->do_hex > 1) {
+                sgj_pr_hr(jsp, "    Target port descriptor(s):\n");
+                hex2stdout(bp + bump + 4, tpd_len, 1);
+            } else {
+                if ((0 == op->do_quiet) || (ip_tid_len > 0))
+                    sgj_pr_hr(jsp, "    Target port descriptor(s):\n");
+                if (jsp->pr_as_json) {
+                    sgj_opaque_p jo3p = sgj_named_subobject_r(jsp, jo2p,
+                                                              "target_port");
+
+                    ja2p = sgj_named_subarray_r(jsp, jo3p,
+                                        "designation_descriptor_list");
+                }
+                filter_dev_ids("", 2 /* leading spaces */, bp + bump + 4,
+                               tpd_len, VPD_ASSOC_TPORT, op, ja2p);
+            }
+        }
+        bump += tpd_len + 4;
+        sgj_js_nv_o(jsp, jap, NULL, jo2p);
+    }
+}
+
+/* Prints outs an abridged set of device identification designators
+   selected by association, designator type and/or code set. Not used
+   for JSON output. */
+static int
+filter_dev_ids_quiet(uint8_t * buff, int len, int m_assoc)
+{
+    int k, m, p_id, c_set, piv, desig_type, i_len, naa, off, u;
+    int assoc, is_sas, rtp;
+    const uint8_t * bp;
+    const uint8_t * ip;
+    uint8_t sas_tport_addr[8];
+
+    rtp = 0;
+    memset(sas_tport_addr, 0, sizeof(sas_tport_addr));
+    for (k = 0, off = -1; true; ++k) {
+        if ((0 == k) && (0 != buff[2])) {
+            /* first already in buff */
+            if (m_assoc != VPD_ASSOC_LU)
+                return 0;
+            ip = buff;
+            c_set = 1;
+            assoc = VPD_ASSOC_LU;
+            is_sas = 0;
+            desig_type = 3;
+            i_len = 16;
+        } else {
+            u = sg_vpd_dev_id_iter(buff, len, &off, m_assoc, -1, -1);
+            if (0 != u)
+                break;
+            bp = buff + off;
+            i_len = bp[3];
+            if ((off + i_len + 4) > len) {
+                pr2serr("    VPD page error: designator length longer than\n"
+                        "     remaining response length=%d\n", (len - off));
+                return SG_LIB_CAT_MALFORMED;
+            }
+            ip = bp + 4;
+            p_id = ((bp[0] >> 4) & 0xf);
+            c_set = (bp[0] & 0xf);
+            piv = ((bp[1] & 0x80) ? 1 : 0);
+            is_sas = (piv && (6 == p_id)) ? 1 : 0;
+            assoc = ((bp[1] >> 4) & 0x3);
+            desig_type = (bp[1] & 0xf);
+        }
+        switch (desig_type) {
+        case 0: /* vendor specific */
+            break;
+        case 1: /* T10 vendor identification */
+            break;
+        case 2: /* EUI-64 based */
+            if ((8 != i_len) && (12 != i_len) && (16 != i_len))
+                pr2serr("      << expect 8, 12 and 16 byte "
+                        "EUI, got %d>>\n", i_len);
+            printf("  0x");
+            for (m = 0; m < i_len; ++m)
+                printf("%02x", (unsigned int)ip[m]);
+            printf("\n");
+            break;
+        case 3: /* NAA */
+            naa = (ip[0] >> 4) & 0xff;
+            if (1 != c_set) {
+                pr2serr("      << expected binary code_set (1), got %d for "
+                        "NAA=%d>>\n", c_set, naa);
+                hex2stderr(ip, i_len, 0);
+                break;
+            }
+            switch (naa) {
+            case 2:             /* NAA IEEE extended */
+                if (8 != i_len) {
+                    pr2serr("      << unexpected NAA 2 identifier "
+                            "length: 0x%x>>\n", i_len);
+                    hex2stderr(ip, i_len, 0);
+                    break;
+                }
+                printf("  0x");
+                for (m = 0; m < 8; ++m)
+                    printf("%02x", (unsigned int)ip[m]);
+                printf("\n");
+                break;
+            case 3:             /* Locally assigned */
+            case 5:             /* IEEE Registered */
+                if (8 != i_len) {
+                    pr2serr("      << unexpected NAA 3 or 5 "
+                            "identifier length: 0x%x>>\n", i_len);
+                    hex2stderr(ip, i_len, 0);
+                    break;
+                }
+                if ((0 == is_sas) || (1 != assoc)) {
+                    printf("  0x");
+                    for (m = 0; m < 8; ++m)
+                        printf("%02x", (unsigned int)ip[m]);
+                    printf("\n");
+                } else if (rtp) {
+                    printf("  0x");
+                    for (m = 0; m < 8; ++m)
+                        printf("%02x", (unsigned int)ip[m]);
+                    printf(",0x%x\n", rtp);
+                    rtp = 0;
+                } else {
+                    if (sas_tport_addr[0]) {
+                        printf("  0x");
+                        for (m = 0; m < 8; ++m)
+                            printf("%02x", (unsigned int)sas_tport_addr[m]);
+                        printf("\n");
+                    }
+                    memcpy(sas_tport_addr, ip, sizeof(sas_tport_addr));
+                }
+                break;
+            case 6:             /* NAA IEEE registered extended */
+                if (16 != i_len) {
+                    pr2serr("      << unexpected NAA 6 identifier length: "
+                            "0x%x>>\n", i_len);
+                    hex2stderr(ip, i_len, 0);
+                    break;
+                }
+                printf("  0x");
+                for (m = 0; m < 16; ++m)
+                    printf("%02x", (unsigned int)ip[m]);
+                printf("\n");
+                break;
+            default:
+                pr2serr("      << bad NAA nibble, expected 2, 3, 5 or 6, got "
+                        "%d>>\n", naa);
+                hex2stderr(ip, i_len, 0);
+                break;
+            }
+            break;
+        case 4: /* Relative target port */
+            if ((0 == is_sas) || (1 != c_set) || (1 != assoc) || (4 != i_len))
+                break;
+            rtp = sg_get_unaligned_be16(ip + 2);
+            if (sas_tport_addr[0]) {
+                printf("  0x");
+                for (m = 0; m < 8; ++m)
+                    printf("%02x", (unsigned int)sas_tport_addr[m]);
+                printf(",0x%x\n", rtp);
+                memset(sas_tport_addr, 0, sizeof(sas_tport_addr));
+                rtp = 0;
+            }
+            break;
+        case 5: /* (primary) Target port group */
+            break;
+        case 6: /* Logical unit group */
+            break;
+        case 7: /* MD5 logical unit identifier */
+            break;
+        case 8: /* SCSI name string */
+            if (c_set < 2) {    /* quietly accept ASCII for UTF-8 */
+                pr2serr("      << expected UTF-8 code_set>>\n");
+                hex2stderr(ip, i_len, 0);
+                break;
+            }
+            if (! (strncmp((const char *)ip, "eui.", 4) ||
+                   strncmp((const char *)ip, "EUI.", 4) ||
+                   strncmp((const char *)ip, "naa.", 4) ||
+                   strncmp((const char *)ip, "NAA.", 4) ||
+                   strncmp((const char *)ip, "iqn.", 4))) {
+                pr2serr("      << expected name string prefix>>\n");
+                hex2stderr(ip, i_len, -1);
+                break;
+            }
+            /* does %s print out UTF-8 ok??
+             * Seems to depend on the locale. Looks ok here with my
+             * locale setting: en_AU.UTF-8
+             */
+            printf("  %.*s\n", i_len, (const char *)ip);
+            break;
+        case 9: /* Protocol specific port identifier */
+            break;
+        case 0xa: /* UUID identifier [spc5r08] RFC 4122 */
+            if ((1 != c_set) || (18 != i_len) || (1 != ((ip[0] >> 4) & 0xf)))
+                break;
+            for (m = 0; m < 16; ++m) {
+                if ((4 == m) || (6 == m) || (8 == m) || (10 == m))
+                    printf("-");
+                printf("%02x", (unsigned int)ip[2 + m]);
+            }
+            printf("\n");
+            break;
+        default: /* reserved */
+            break;
+        }
+    }
+    if (sas_tport_addr[0]) {
+        printf("  0x");
+        for (m = 0; m < 8; ++m)
+            printf("%02x", (unsigned int)sas_tport_addr[m]);
+        printf("\n");
+    }
+    if (-2 == u) {
+        pr2serr("VPD page error: short designator around offset %d\n", off);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    return 0;
+}
+
+/* Prints outs designation descriptors (dd_s) selected by association,
+   designator type and/or code set. VPD_DEVICE_ID and VPD_SCSI_PORTS */
+static int
+filter_dev_ids(const char * print_if_found, int num_leading, uint8_t * buff,
+               int len, int m_assoc, struct opts_t * op, sgj_opaque_p jap)
+{
+    bool printed, sgj_out_hr;
+    int assoc, off, u, i_len;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    char b[1024];
+    char sp[82];
+    static const int blen = sizeof(b);
+
+    if (op->do_quiet && (! jsp->pr_as_json))
+        return filter_dev_ids_quiet(buff, len, m_assoc);
+    sgj_out_hr = false;
+    if (jsp->pr_as_json) {
+        int ret = filter_json_dev_ids(buff, len, m_assoc, op, jap);
+
+        if (ret || (! jsp->pr_out_hr))
+            return ret;
+        sgj_out_hr = true;
+    }
+    if (num_leading > (int)(sizeof(sp) - 2))
+        num_leading = sizeof(sp) - 2;
+    if (num_leading > 0)
+        snprintf(sp, sizeof(sp), "%*c", num_leading, ' ');
+    else
+        sp[0] = '\0';
+    if (buff[2] != 0) { /* all valid dd_s should have 0 in this byte */
+        if (op->verbose)
+            pr2serr("%s: designation descriptors byte 2 should be 0\n"
+                    "perhaps this is a standard inquiry response, ignore\n",
+                    __func__);
+        return 0;
+    }
+    off = -1;
+    printed = false;
+    while ((u = sg_vpd_dev_id_iter(buff, len, &off, m_assoc, -1, -1)) == 0) {
+        bp = buff + off;
+        i_len = bp[3];
+        if ((off + i_len + 4) > len) {
+            pr2serr("    VPD page error: designator length longer than\n"
+                    "     remaining response length=%d\n", (len - off));
+            return SG_LIB_CAT_MALFORMED;
+        }
+        assoc = ((bp[1] >> 4) & 0x3);
+        if (print_if_found && (! printed)) {
+            printed = true;
+            if (strlen(print_if_found) > 0) {
+                snprintf(b, blen, "  %s:", print_if_found);
+                if (sgj_out_hr)
+                    sgj_js_str_out(jsp, b, strlen(b));
+                else
+                    printf("%s\n", b);
+            }
+        }
+        if (NULL == print_if_found) {
+            snprintf(b, blen, "  %s%s:", sp, sg_get_desig_assoc_str(assoc));
+            if (sgj_out_hr)
+                sgj_js_str_out(jsp, b, strlen(b));
+            else
+                printf("%s\n", b);
+        }
+        sg_get_designation_descriptor_str(sp, bp, i_len + 4, false,
+                                          op->do_long, blen, b);
+        if (sgj_out_hr)
+            sgj_js_str_out(jsp, b, strlen(b));
+        else
+            printf("%s", b);
+    }
+    if (-2 == u) {
+        pr2serr("VPD page error: short designator around offset %d\n", off);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    return 0;
+}
+
+/* VPD_BLOCK_LIMITS sbc */
+/* VPD_SA_DEV_CAP ssc */
+/* VPD_OSD_INFO osd */
+static void
+decode_b0_vpd(uint8_t * buff, int len, struct opts_t * op, sgj_opaque_p jop)
+{
+    int pdt = PDT_MASK & buff[0];
+    sgj_state * jsp = &op->json_st;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+         /* now done by decode_block_limits_vpd() in sg_vpd_common.c */
+        break;
+    case PDT_TAPE: case PDT_MCHANGER:
+        sgj_haj_vi_nex(jsp, jop, 2, "TSMC", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(buff[4] & 0x2), false, "Tape Stream Mirror "
+                       "Capable");
+        sgj_haj_vi_nex(jsp, jop, 2, "WORM", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(buff[4] & 0x1), false, "Write Once Read Multiple "
+                       "supported");
+        break;
+    case PDT_OSD:
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(buff, len, 0);
+        break;
+    }
+}
+
+/* VPD_BLOCK_DEV_CHARS sbc  0xb1 ["bdc"] */
+/* VPD_MAN_ASS_SN ssc */
+/* VPD_SECURITY_TOKEN osd */
+/* VPD_ES_DEV_CHARS ses-4 */
+static void
+decode_b1_vpd(uint8_t * buff, int len, struct opts_t * op, sgj_opaque_p jop)
+{
+    int pdt;
+    sgj_state * jsp = &op->json_st;
+
+    pdt = buff[0] & PDT_MASK;
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+        /* now done by decode_block_dev_ch_vpd() in sg_vpd_common.c */
+    case PDT_TAPE: case PDT_MCHANGER: case PDT_ADC:
+        sgj_pr_hr(jsp, "  Manufacturer-assigned serial number: %.*s\n",
+                  len - 4, buff + 4);
+        sgj_js_nv_s_len(jsp, jop, "manufacturer_assigned_serial_number",
+                        (const char *)buff + 4, len - 4);
+        break;
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(buff, len, 0);
+        break;
+    }
+}
+
+/* VPD_LB_PROVISIONING sbc */
+/* VPD_TA_SUPPORTED ssc */
+static void
+decode_b2_vpd(uint8_t * buff, int len, int pdt, struct opts_t * op)
+{
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+        /* decode_block_lb_prov_vpd() is now in sg_vpd_common.c */
+        break;
+    case PDT_TAPE: case PDT_MCHANGER:
+        /* decode_tapealert_supported_vpd() is now in sg_vpd_common.c */
+        break;
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(buff, len, 0);
+        break;
+    }
+}
+
+/* VPD_REFERRALS sbc          0xb3 ["ref"] */
+/* VPD_AUTOMATION_DEV_SN ssc  0xb3 ["adsn"] */
+static void
+decode_b3_vpd(uint8_t * buff, int len, struct opts_t * op, sgj_opaque_p jop)
+{
+    int pdt;
+    sgj_state * jsp = &op->json_st;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    pdt = buff[0] & PDT_MASK;
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+        /* now done in decode_referrals_vpd() in sg_vpd_common.c */
+        break;
+    case PDT_TAPE: case PDT_MCHANGER:
+        sgj_pr_hr(jsp, "  Automation device serial number: %.*s\n",
+                  len - 4, buff + 4);
+        sgj_js_nv_s_len(jsp, jop, "automation_device_serial_number",
+                        (const char *)buff + 4, len - 4);
+        break;
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(buff, len, 0);
+        break;
+    }
+}
+
+/* VPD_SUP_BLOCK_LENS   sbc ["sbl"] */
+/* VPD_DTDE_ADDRESS ssc */
+static void
+decode_b4_vpd(uint8_t * buff, int len, struct opts_t * op, sgj_opaque_p jop)
+{
+    int pdt = buff[0] & PDT_MASK;
+    sgj_state * jsp = &op->json_st;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+        /* now done by decode_sup_block_lens_vpd() in sg_vpd_common.c */
+        break;
+    case PDT_TAPE: case PDT_MCHANGER:
+        sgj_pr_hr(jsp, "  Device transfer data element:\n");
+        if (! jsp->pr_as_json)
+            hex2stdout(buff + 4, len - 4, 1);
+        sgj_js_nv_hex_bytes(jsp, jop, "device_transfer_data_element",
+                            buff + 4, len - 4);
+        break;
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(buff, len, 0);
+        break;
+    }
+}
+
+/* VPD_BLOCK_DEV_C_EXTENS sbc */
+/* VPD_LB_PROTECTION  0xb5 ["lbpro"] ssc */
+static void
+decode_b5_vpd(uint8_t * b, int len, int do_hex, int pdt)
+{
+    if (do_hex) {
+        hex2stdout(b, len, (1 == do_hex) ? 0 : -1);
+        return;
+    }
+    switch (pdt) {
+    case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+        /* now done by decode_block_dev_char_ext_vpd() in sg_vpd_common.c */
+        break;
+    case PDT_TAPE: case PDT_MCHANGER:
+        /* now done by decode_lb_protection_vpd() in sg_vpd_common.c */
+        break;
+    default:
+        pr2serr("  Unable to decode pdt=0x%x, in hex:\n", pdt);
+        hex2stderr(b, len, 0);
+        break;
+    }
+}
+
+/* Returns 0 if successful */
+static int
+svpd_unable_to_decode(int sg_fd, struct opts_t * op, int subvalue, int off)
+{
+    bool as_json, json_o_hr, hex0;
+    int res, len, n;
+    sgj_state * jsp = &op->json_st;
+    uint8_t * rp;
+
+    as_json = jsp->pr_as_json;
+    json_o_hr = as_json && jsp->pr_out_hr;
+    hex0 = (0 == op->do_hex);
+    rp = rsp_buff + off;
+    if (hex0 && (! op->do_raw) && (! op->examine_given))
+        sgj_pr_hr(jsp, "Only hex output supported\n");
+    if ((!op->do_raw) && (op->do_hex < 2) && (! op->examine_given)) {
+        if (subvalue) {
+            if (hex0)
+                sgj_pr_hr(jsp, "VPD page code=0x%.2x, subvalue=0x%.2x:\n",
+                          op->vpd_pn, subvalue);
+            else
+                printf("VPD page code=0x%.2x, subvalue=0x%.2x:\n", op->vpd_pn,
+                       subvalue);
+        } else if (op->vpd_pn >= 0) {
+            if (hex0)
+                sgj_pr_hr(jsp, "VPD page code=0x%.2x:\n", op->vpd_pn);
+            else
+                printf("VPD page code=0x%.2x:\n", op->vpd_pn);
+        } else {
+            if (hex0)
+                sgj_pr_hr(jsp, "VPD page code=%d:\n", op->vpd_pn);
+            else
+                printf("VPD page code=%d:\n", op->vpd_pn);
+        }
+    }
+
+    res = vpd_fetch_page(sg_fd, rp, op->vpd_pn, op->maxlen, op->do_quiet,
+                         op->verbose, &len);
+    if (0 == res) {
+        if (op->do_raw)
+            dStrRaw(rp, len);
+        else {
+           if (json_o_hr && hex0 && (len > 0) && (len < UINT16_MAX)) {
+                char * p;
+
+                n = len * 4;
+                p = malloc(n);
+                if (p) {
+                    n = hex2str(rp, len, NULL, 1, n - 1, p);
+                    sgj_js_str_out(jsp, p, n);
+                }
+            } else
+                hex2stdout(rp, len, no_ascii_4hex(op));
+        }
+    } else if ((! op->do_quiet) && (! op->examine_given)) {
+        if (op->vpd_pn >= 0)
+            pr2serr("fetching VPD page code=0x%.2x: failed\n", op->vpd_pn);
+        else
+            pr2serr("fetching VPD page code=%d: failed\n", op->vpd_pn);
+    }
+    return res;
+}
+
+static int
+recurse_vpd_decode(struct opts_t * op, sgj_opaque_p jop, int off)
+{
+    int res = svpd_decode_t10(-1, op, jop, 0, off, NULL);
+
+    if (SG_LIB_CAT_OTHER == res) {
+        res = svpd_decode_vendor(-1, op, jop, off);
+        if (SG_LIB_CAT_OTHER == res)
+            svpd_unable_to_decode(-1, op, 0, off);
+    }
+    return res;
+}
+
+/* Returns 0 if successful. If don't know how to decode, returns
+ * SG_LIB_CAT_OTHER else see sg_ll_inquiry(). */
+static int
+svpd_decode_t10(int sg_fd, struct opts_t * op, sgj_opaque_p jop,
+                int subvalue, int off, const char * prefix)
+{
+    bool allow_name, allow_if_found, long_notquiet, qt;
+    bool vpd_supported = false;
+    bool inhex_active = (-1 == sg_fd);
+    bool exam_not_given = ! op->examine_given;
+    int len, pdt, pqual, num, k, resid, alloc_len, pn, vb;
+    int res = 0;
+    sgj_state * jsp = &op->json_st;
+    uint8_t * rp;
+    sgj_opaque_p jap = NULL;
+    sgj_opaque_p jo2p = NULL;
+    const char * np;
+    const char * ep;
+    const char * pre = (prefix ? prefix : "");
+    const char * pdt_str;
+    bool as_json = jsp->pr_as_json;
+    bool not_json = ! as_json;
+    char obuff[DEF_ALLOC_LEN];
+    char d[48];
+
+    vb = op->verbose;
+    qt = op->do_quiet;
+    long_notquiet = op->do_long && (! op->do_quiet);
+    if (op->do_raw || (op->do_quiet && (! op->do_long) && (! op->do_all)) ||
+        (op->do_hex >= 3) || op->examine_given)
+        allow_name = false;
+    else
+        allow_name = true;
+    allow_if_found = op->examine_given && (! op->do_quiet);
+    rp = rsp_buff + off;
+    pn = op->vpd_pn;
+    if ((off > 0) && (VPD_NOPE_WANT_STD_INQ != op->vpd_pn))
+        pn = rp[1];
+    else
+        pn = op->vpd_pn;
+    if (!inhex_active && !op->do_force && exam_not_given &&
+        pn != VPD_NOPE_WANT_STD_INQ &&
+        pn != VPD_SUPPORTED_VPDS) {
+        res = vpd_fetch_page(sg_fd, rp, VPD_SUPPORTED_VPDS, op->maxlen, qt,
+                             vb, &len);
+        if (res)
+            return res;
+
+        num = rp[3];
+        if (num > (len - 4))
+            num = (len - 4);
+        if (vb > 1) {
+            pr2serr("Supported VPD pages, hex list: ");
+            hex2stderr(rp + 4, num, -1);
+        }
+        for (k = 0; k < num; ++k) {
+            if (pn == rp[4 + k]) {
+                vpd_supported = true;
+                break;
+            }
+        }
+        if (! vpd_supported) { /* get creative, was SG_LIB_CAT_ILLEGAL_REQ */
+            if (vb)
+                pr2serr("Given VPD page not in supported list, use --force "
+                        "to override this check\n");
+            return sg_convert_errno(EDOM);
+        }
+    }
+    pdt = rp[0] & PDT_MASK;
+    pdt_str = sg_get_pdt_str(pdt, sizeof(d), d);
+    pqual = (rp[0] & 0xe0) >> 5;
+
+    switch(pn) {
+    case VPD_NOPE_WANT_STD_INQ:    /* -2 (want standard inquiry response) */
+        if (!inhex_active) {
+            if (op->maxlen > 0)
+                alloc_len = op->maxlen;
+            else if (op->do_long)
+                alloc_len = DEF_ALLOC_LEN;
+            else
+                alloc_len = 36;
+            res = sg_ll_inquiry_v2(sg_fd, false, 0, rp, alloc_len,
+                                   DEF_PT_TIMEOUT, &resid, ! op->do_quiet, vb);
+        } else {
+            alloc_len = op->maxlen;
+            resid = 0;
+            res = 0;
+        }
+        if (0 == res) {
+            alloc_len -= resid;
+            if (op->do_raw)
+                dStrRaw(rp, alloc_len);
+            else if (op->do_hex) {
+                if (! op->do_quiet && (op->do_hex < 3))
+                    sgj_pr_hr(jsp, "Standard Inquiry data format:\n");
+                hex2stdout(rp, alloc_len, (1 == op->do_hex) ? 0 : -1);
+            } else
+                std_inq_decode(rp, alloc_len, op, jop);
+            return 0;
+        }
+        break;
+    case VPD_SUPPORTED_VPDS:    /* 0x0 ["sv"] */
+        np = "Supported VPD pages VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else if (op->do_hex)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                num = rp[3];
+                if (num > (len - 4))
+                    num = (len - 4);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                               "supported_vpd_page_list");
+                }
+                decode_supported_vpd_4vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_UNIT_SERIAL_NUM:   /* 0x80 ["sn"] */
+        np = "Unit serial number VPD page";
+        if (allow_name && not_json)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else if (op->do_hex)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                memset(obuff, 0, sizeof(obuff));
+                len -= 4;
+                if (len >= (int)sizeof(obuff))
+                    len = sizeof(obuff) - 1;
+                memcpy(obuff, rp + 4, len);
+                jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                sgj_haj_vs(jsp, jo2p, 2, np, SGJ_SEP_COLON_1_SPACE, obuff);
+            }
+            return 0;
+        }
+        break;
+    case VPD_DEVICE_ID: /* 0x83 ["di, di_asis, di_lu, di_port, di_target"] */
+        np = "Device Identification VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else if (op->do_hex)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                               "designation_descriptor_list");
+                }
+                device_id_vpd_variants(rp, len, subvalue, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_SOFTW_INF_ID:      /* 0x84 ["sii"] */
+        np = "Software interface identification VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                      "software_interface_identifier_list");
+                }
+                decode_softw_inf_id(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_MAN_NET_ADDR:      /* 0x85 ["mna"] */
+        np= "Management network addresses VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                      "network_services_descriptor_list");
+                }
+                decode_net_man_vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_EXT_INQ:           /* 0x86 ["ei"] */
+        np = "extended INQUIRY data VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                bool protect = false;
+
+                op->protect_not_sure = false;
+                if (op->std_inq_a_valid)
+                     protect = !! (0x1 & op->std_inq_a[5]);
+                else if ((sg_fd >= 0) && (! op->do_force)) {
+                    struct sg_simple_inquiry_resp sir;
+
+                    res = sg_simple_inquiry(sg_fd, &sir, false, vb);
+                    if (res) {
+                        if (op->verbose)
+                            pr2serr("%s: sg_simple_inquiry() failed, "
+                                    "res=%d\n", __func__, res);
+                        op->protect_not_sure = true;
+                    } else
+                        protect = !!(sir.byte_5 & 0x1); /* SPC-3 and later */
+                } else
+                    op->protect_not_sure = true;
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp,"   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                decode_x_inq_vpd(rp, len, protect, op, jo2p);
+            }
+            return 0;
+        }
+        break;
+    case VPD_MODE_PG_POLICY:    /* 0x87 */
+        np = "Mode page policy VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", (prefix ? prefix : ""), np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                      "mode_page_policy_descriptor_list");
+                }
+                decode_mode_policy_vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_SCSI_PORTS:        /* 0x88  ["sp"] */
+        np = "SCSI Ports VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                               "scsi_ports_descriptor_list");
+                }
+                decode_scsi_ports_vpd_4vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_ATA_INFO:          /* 0x89 ['ai"] */
+        np = "ATA information VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        alloc_len = op->maxlen ? op->maxlen : VPD_ATA_INFO_LEN;
+        res = vpd_fetch_page(sg_fd, rp, pn, alloc_len, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", (prefix ? prefix : ""), np);
+            if ((2 == op->do_raw) || (3 == op->do_hex)) {  /* for hdparm */
+                if (len < (60 + 512))
+                    pr2serr("ATA_INFO VPD page len (%d) less than expected "
+                            "572\n", len);
+                else
+                    dWordHex((const unsigned short *)(rp + 60), 256, -2,
+                             sg_is_big_endian());
+            }
+            else if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                decode_ata_info_vpd(rp, len, op, jo2p);
+            }
+            return 0;
+        }
+        break;
+    case VPD_POWER_CONDITION:          /* 0x8a ["pc"] */
+        np = "Power condition VPD page:";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp,  "%s%s\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                decode_power_condition(rp, len, op, jo2p);
+            }
+            return 0;
+        }
+        break;
+    case VPD_DEVICE_CONSTITUENTS:      /* 0x8b  ["dc"] */
+        np = "Device constituents VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                               "constituent_descriptor_list");
+                }
+                decode_dev_constit_vpd(rp, len, op, jap, recurse_vpd_decode);
+            }
+            return 0;
+        }
+        break;
+    case VPD_CFA_PROFILE_INFO:    /* 0x8c ["cfa"] */
+        np = "CFA profile information VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                      "cfa_profile_descriptor_list");
+                }
+                decode_cga_profile_vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_POWER_CONSUMPTION:    /* 0x8d ["psm"] */
+        np = "Power consumption VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                      "power_consumption_descriptor_list");
+                }
+                decode_power_consumption(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_3PARTY_COPY:   /* 0x8f */
+        np = "Third party copy VPD page";       /* ["tpc"] */
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                      "third_party_copy_descriptors");
+                }
+                decode_3party_copy_vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_PROTO_LU:          /* 0x90 ["pslu"] */
+        np = "Protocol-specific logical unit information VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                              "logical_unit_information_descriptor_list");
+                }
+                decode_proto_lu_vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_PROTO_PORT:        /* 0x91  ["pspo"] */
+        np = "Protocol-specific port VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                              "port_information_descriptor_list");
+                }
+                decode_proto_port_vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case VPD_SCSI_FEATURE_SETS:         /* 0x92  ["sfs"] */
+        np = "SCSI Feature sets VPD page";
+        if (allow_name)
+            sgj_pr_hr(jsp, "%s%s:\n", pre, np);
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            if (! allow_name && allow_if_found)
+                sgj_pr_hr(jsp, "%s%s\n", pre, np);
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p,
+                                      "feature_set_code_list");
+                }
+                decode_feature_sets_vpd(rp, len, op, jap);
+            }
+            return 0;
+        }
+        break;
+    case 0xb0:  /* depends on pdt */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool bl = false;
+            bool sad = false;
+            bool oi = false;
+
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Block limits VPD page";
+                ep = "(SBC)";
+                bl = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "Sequential-access device capabilities VPD page";
+                ep = "(SSC)";
+                sad = true;
+                break;
+            case PDT_OSD:
+                np = "OSD information VPD page";
+                ep = "(OSD)";
+                oi = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                if (bl)
+                    decode_block_limits_vpd(rp, len, op, jo2p);
+                else if (sad) {
+                    decode_b0_vpd(rp, len, op, jop);
+                } else if (oi) {
+                    decode_b0_vpd(rp, len, op, jop);
+                } else {
+
+                }
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb0\n", pre);
+        break;
+    case 0xb1:  /* depends on pdt */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool bdc = false;
+            static const char * masn =
+                        "Manufactured-assigned serial number VPD page";
+
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Block device characteristics VPD page";
+                ep = "(SBC)";
+                bdc = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = masn;
+                ep = "(SSC)";
+                break;
+            case PDT_OSD:
+                np = "Security token VPD page";
+                ep = "(OSD)";
+                break;
+            case PDT_ADC:
+                np = masn;
+                ep = "(ADC)";
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                if (bdc)
+                    decode_block_dev_ch_vpd(rp, len, op, jo2p);
+                else
+                    decode_b1_vpd(rp, len, op, jo2p);
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb1\n", pre);
+        break;
+    case 0xb2:          /* VPD page depends on pdt */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool lbpv = false;
+            bool tas = false;
+
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Logical block provisioning VPD page";
+                ep = "(SBC)";
+                lbpv = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "TapeAlert supported flags VPD page";
+                ep = "(SSC)";
+                tas = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                if (lbpv)
+                    decode_block_lb_prov_vpd(rp, len, op, jo2p);
+                else if (tas)
+                    decode_tapealert_supported_vpd(rp, len, op, jo2p);
+                else
+                    decode_b2_vpd(rp, len, pdt, op);
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb2\n", pre);
+        break;
+    case 0xb3:          /* VPD page depends on pdt */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool ref = false;
+
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Referrals VPD page";
+                ep = "(SBC)";
+                ref = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "Automation device serial number VPD page";
+                ep = "(SSC)";
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                if (ref)
+                    decode_referrals_vpd(rp, len, op, jo2p);
+                else
+                    decode_b3_vpd(rp, len, op, jo2p);
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb3\n", pre);
+        break;
+    case 0xb4:          /* VPD page depends on pdt */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool sbl = false;
+
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Supported block lengths and protection types VPD page";
+                ep = "(SBC)";
+                sbl = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "Data transfer device element address";
+                ep = "(SSC)";
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                if (sbl) {
+                    if (as_json)
+                        jap = sgj_named_subarray_r(jsp, jo2p, "logical_block_"
+                                "length_and_protection_types_descriptor_list");
+                    decode_sup_block_lens_vpd(rp, len, op, jap);
+                } else
+                    decode_b4_vpd(rp, len, op, jo2p);
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb4\n", pre);
+        break;
+    case 0xb5:          /* VPD page depends on pdt */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool bdce = false;
+            bool lbp = false;
+
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Block device characteristics extension VPD page";
+                ep = "(SBC)";
+                bdce = true;
+                break;
+            case PDT_TAPE: case PDT_MCHANGER:
+                np = "Logical block protection VPD page";
+                ep = "(SSC)";
+                lbp = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                if (bdce)
+                    decode_block_dev_char_ext_vpd(rp, len, op, jo2p);
+                else if (lbp) {
+                    if (as_json)
+                        jap = sgj_named_subarray_r(jsp, jo2p,
+                         "logical_block_protection_method_descriptor_list");
+                     decode_lb_protection_vpd(rp, len, op, jap);
+                } else
+                    decode_b5_vpd(rp, len, op->do_hex, pdt);
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb5\n", pre);
+        break;
+    case VPD_ZBC_DEV_CHARS:       /* 0xb6 for both pdt=0 and pdt=0x14 */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool zbdch = false;
+
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Zoned block device characteristics VPD page";
+                ep = "(SBC, ZBC)";
+                zbdch = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                if (zbdch)
+                    decode_zbdch_vpd(rp, len, op, jo2p);
+                else
+                    return SG_LIB_CAT_OTHER;
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb6\n", pre);
+        break;
+    case 0xb7:
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool ble = false;
+
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Block limits extension VPD page";
+                ep = "(SBC)";
+                ble = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s:\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                if (ble)
+                    decode_block_limits_ext_vpd(rp, len, op, jo2p);
+                else
+                    return SG_LIB_CAT_OTHER;
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb7\n", pre);
+        break;
+    case 0xb8:          /* VPD_FORMAT_PRESETS */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool fp = false;
+
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Format presets VPD page";
+                ep = "(SBC)";
+                fp = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s:\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p, "format_preset_"
+                                               "descriptor_list");
+                }
+                if (fp)
+                    decode_format_presets_vpd(rp, len, op, jap);
+                else
+                    return SG_LIB_CAT_OTHER;
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb8\n", pre);
+        break;
+    case 0xb9:          /* VPD_CON_POS_RANGE */
+        res = vpd_fetch_page(sg_fd, rp, pn, op->maxlen, qt, vb, &len);
+        if (0 == res) {
+            bool cpr = false;           /* ["cpr"] */
+
+            pdt = rp[0] & PDT_MASK;
+            switch (pdt) {
+            case PDT_DISK: case PDT_WO: case PDT_OPTICAL: case PDT_ZBC:
+                np = "Concurrent positioning ranges VPD page";
+                ep = "(SBC)";
+                cpr = true;
+                break;
+            default:
+                np = NULL;
+                break;
+            }
+            if (NULL == np)
+                sgj_pr_hr(jsp, "VPD page=0x%x, pdt=0x%x:\n", pn, pdt);
+            else if (allow_name || allow_if_found)
+                sgj_pr_hr(jsp, "%s%s %s:\n", pre, np, ep ? ep : "");
+            if (op->do_raw)
+                dStrRaw(rp, len);
+            else {
+                if (vb || long_notquiet)
+                    sgj_pr_hr(jsp, "   [PQual=%d  Peripheral device type: "
+                              "%s]\n", pqual, pdt_str);
+                if (as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    jap = sgj_named_subarray_r(jsp, jo2p, "lba_range_"
+                                               "descriptor_list");
+                }
+                if (cpr)
+                    decode_con_pos_range_vpd(rp, len, op, jap);
+                else
+                    return SG_LIB_CAT_OTHER;
+            }
+            return 0;
+        } else if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3) &&
+                   exam_not_given)
+            sgj_pr_hr(jsp, "%sVPD page=0xb8\n", pre);
+        break;
+    default:
+        return SG_LIB_CAT_OTHER;
+    }
+    return res;
+}
+
+static int
+svpd_decode_all(int sg_fd, struct opts_t * op, sgj_opaque_p jop)
+{
+    int k, res, rlen, n, pn;
+    int max_pn = 255;
+    int any_err = 0;
+    sgj_state * jsp = &op->json_st;
+    uint8_t vpd0_buff[512];
+    uint8_t * rp = vpd0_buff;
+
+    if (op->vpd_pn > 0)
+        max_pn = op->vpd_pn;
+    if (sg_fd >= 0) {   /* have valid open file descriptor (handle) */
+        res = vpd_fetch_page(sg_fd, rp, VPD_SUPPORTED_VPDS, op->maxlen,
+                             op->do_quiet, op->verbose, &rlen);
+        if (res) {
+            if (! op->do_quiet) {
+                if (SG_LIB_CAT_ABORTED_COMMAND == res)
+                    pr2serr("%s: VPD page 0, aborted command\n", __func__);
+                else if (res) {
+                    char b[80];
+
+                    sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+                    pr2serr("%s: fetching VPD page 0 failed: %s\n", __func__,
+                            b);
+                }
+            }
+            return res;
+        }
+        n = sg_get_unaligned_be16(rp + 2);
+        if (n > (rlen - 4)) {
+            if (op->verbose)
+                pr2serr("%s: rlen=%d > page0 size=%d\n", __func__, rlen,
+                        n + 4);
+            n = (rlen - 4);
+        }
+        for (k = 0; k < n; ++k) {
+            pn = rp[4 + k];
+            if (pn > max_pn)
+                continue;
+            op->vpd_pn = pn;
+            if (k > 0)
+                sgj_pr_hr(jsp, "\n");
+            if (op->do_long) {
+                if (jsp->pr_as_json)
+                    sgj_pr_hr(jsp, "[0x%x]:\n", pn);
+                else
+                    printf("[0x%x] ", pn);
+            }
+
+            res = svpd_decode_t10(sg_fd, op, jop, 0, 0, NULL);
+            if (SG_LIB_CAT_OTHER == res) {
+                res = svpd_decode_vendor(sg_fd, op, jop, 0);
+                if (SG_LIB_CAT_OTHER == res)
+                    res = svpd_unable_to_decode(sg_fd, op, 0, 0);
+            }
+            if (! op->do_quiet) {
+                if (SG_LIB_CAT_ABORTED_COMMAND == res)
+                    pr2serr("fetching VPD page failed, aborted command\n");
+                else if (res) {
+                    char b[80];
+
+                    sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+                    pr2serr("fetching VPD page failed: %s\n", b);
+                }
+            }
+            if (res)
+                any_err = res;
+        }
+        res = any_err;
+    } else {    /* input is coming from --inhex=FN */
+        int bump, off;
+        int in_len = op->maxlen;
+        int prev_pn = -1;
+
+        res = 0;
+        if (op->page_given && (VPD_NOPE_WANT_STD_INQ == op->vpd_pn))
+            return svpd_decode_t10(-1, op, jop, 0, 0, NULL);
+
+        for (k = 0, off = 0; off < in_len; ++k, off += bump) {
+            rp = rsp_buff + off;
+            pn = rp[1];
+            bump = sg_get_unaligned_be16(rp + 2) + 4;
+            if ((off + bump) > in_len) {
+                pr2serr("%s: page 0x%x size (%d) exceeds buffer\n", __func__,
+                        pn, bump);
+                bump = in_len - off;
+            }
+            if (op->page_given && (pn != op->vpd_pn))
+                continue;
+            if (pn <= prev_pn) {
+                pr2serr("%s: prev_pn=0x%x, this pn=0x%x, not ascending so "
+                        "exit\n", __func__, prev_pn, pn);
+                break;
+            }
+            prev_pn = pn;
+            op->vpd_pn = pn;
+            if (pn > max_pn) {
+                if (op->verbose > 2)
+                    pr2serr("%s: skipping as this pn=0x%x exceeds "
+                            "max_pn=0x%x\n", __func__, pn, max_pn);
+                continue;
+            }
+            if (op->do_long) {
+                if (jsp->pr_as_json)
+                    sgj_pr_hr(jsp, "[0x%x]:\n", pn);
+                else
+                    printf("[0x%x] ", pn);
+            }
+
+            res = svpd_decode_t10(-1, op, jop, 0, off, NULL);
+            if (SG_LIB_CAT_OTHER == res) {
+                res = svpd_decode_vendor(-1, op, jop, off);
+                if (SG_LIB_CAT_OTHER == res)
+                    res = svpd_unable_to_decode(-1, op, 0, off);
+            }
+        }
+    }
+    return res;
+}
+
+static int
+svpd_examine_all(int sg_fd, struct opts_t * op, sgj_opaque_p jop)
+{
+    bool first = true;
+    bool got_one = false;
+    int k, res, start;
+    int max_pn;
+    int any_err = 0;
+    sgj_state * jsp = &op->json_st;
+    char b[80];
+
+    max_pn = (op->page_given ? op->vpd_pn : 0xff);
+    switch (op->examine) {
+    case 1:
+        start = 0x80;
+        break;
+    case 2:
+        start = 0x0;
+        break;
+    default:
+        start = 0xc0;
+        break;
+    }
+    if (start > max_pn) {       /* swap them around */
+        k = start;
+        start = max_pn;
+        max_pn = k;
+    }
+    for (k = start; k <= max_pn; ++k) {
+        op->vpd_pn = k;
+        if (first)
+            first = false;
+        else if (got_one) {
+            sgj_pr_hr(jsp, "\n");
+            got_one = false;
+        }
+        if (op->do_long)
+            snprintf(b, sizeof(b), "[0x%x] ", k);
+        else
+            b[0] = '\0';
+        res = svpd_decode_t10(sg_fd, op, jop, 0, 0, b);
+        if (SG_LIB_CAT_OTHER == res) {
+            res = svpd_decode_vendor(sg_fd, op, jop, 0);
+            if (SG_LIB_CAT_OTHER == res)
+                res = svpd_unable_to_decode(sg_fd, op, 0, 0);
+        }
+        if (! op->do_quiet) {
+            if (SG_LIB_CAT_ABORTED_COMMAND == res)
+                pr2serr("fetching VPD page failed, aborted command\n");
+            else if (res && (SG_LIB_CAT_ILLEGAL_REQ != res)) {
+                /* SG_LIB_CAT_ILLEGAL_REQ expected as well examine all */
+                sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+                pr2serr("fetching VPD page failed: %s\n", b);
+            }
+        }
+        if (res && (SG_LIB_CAT_ILLEGAL_REQ != res))
+            any_err = res;
+        if (0 == res)
+            got_one = true;
+    }
+    return any_err;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool as_json;
+    int c, res, matches;
+    int sg_fd = -1;
+    int inhex_len = 0;
+    int inraw_len = 0;
+    int ret = 0;
+    int subvalue = 0;
+    const char * cp;
+    sgj_state * jsp;
+    sgj_opaque_p jop = NULL;
+    const struct svpd_values_name_t * vnp;
+    struct opts_t opts SG_C_CPP_ZERO_INIT;
+    struct opts_t * op = &opts;
+
+    op->invoker = SG_VPD_INV_SG_VPD;
+    dup_sanity_chk((int)sizeof(opts), (int)sizeof(*vnp));
+    op->vend_prod_num = -1;
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "aeEfhHiI:j::lm:M:p:qQ:rvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            op->do_all = true;
+            break;
+        case 'e':
+            op->do_enum = true;
+            break;
+        case 'E':
+            ++op->examine;
+            op->examine_given = true;
+            break;
+        case 'f':
+            op->do_force = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++op->do_hex;
+            break;
+        case 'i':
+            ++op->do_ident;
+            break;
+        case 'I':
+            if (op->inhex_fn) {
+                pr2serr("only one '--inhex=' option permitted\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            } else
+                op->inhex_fn = optarg;
+            break;
+        case 'j':
+            if (! sgj_init_state(&op->json_st, optarg)) {
+                int bad_char = op->json_st.first_bad_char;
+                char e[1500];
+
+                if (bad_char) {
+                    pr2serr("bad argument to --json= option, unrecognized "
+                            "character '%c'\n\n", bad_char);
+                }
+                sg_json_usage(0, e, sizeof(e));
+                pr2serr("%s", e);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'l':
+            op->do_long = true;
+            break;
+        case 'm':
+            op->maxlen = sg_get_num(optarg);
+            if ((op->maxlen < 0) || (op->maxlen > MX_ALLOC_LEN)) {
+                pr2serr("argument to '--maxlen' should be %d or less\n",
+                        MX_ALLOC_LEN);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((op->maxlen > 0) && (op->maxlen < MIN_MAXLEN)) {
+                pr2serr("Warning: overriding '--maxlen' < %d, using "
+                        "default\n", MIN_MAXLEN);
+                op->maxlen = 0;
+            }
+            break;
+        case 'M':
+            if (op->vend_prod) {
+                pr2serr("only one '--vendor=' option permitted\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            } else
+                op->vend_prod = optarg;
+            break;
+        case 'p':
+            if (op->page_str) {
+                pr2serr("only one '--page=' option permitted\n");
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            } else
+                op->page_str = optarg;
+            op->page_given = true;
+            break;
+        case 'q':
+            op->do_quiet = true;
+            break;
+        case 'Q':
+            op->sinq_inraw_fn = optarg;
+            break;
+        case 'r':
+            ++op->do_raw;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    jsp = &op->json_st;
+    if (op->do_enum) {
+        if (op->device_name)
+            pr2serr("Device name %s ignored when --enumerate given\n",
+                    op->device_name);
+        if (op->vend_prod) {
+            if (isdigit((uint8_t)op->vend_prod[0])) {
+                op->vend_prod_num = sg_get_num_nomult(op->vend_prod);
+                if ((op->vend_prod_num < 0) || (op->vend_prod_num > 10)) {
+                    pr2serr("Bad vendor/product number after '--vendor=' "
+                            "option\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else {
+                op->vend_prod_num = svpd_find_vp_num_by_acron(op->vend_prod);
+                if (op->vend_prod_num < 0) {
+                    pr2serr("Bad vendor/product acronym after '--vendor=' "
+                            "option\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            svpd_enumerate_vendor(op->vend_prod_num);
+            return 0;
+        }
+        if (op->page_str) {
+            if ((0 == strcmp("-1", op->page_str)) ||
+                (0 == strcmp("-2", op->page_str)))
+                op->vpd_pn = VPD_NOPE_WANT_STD_INQ;
+            else if (isdigit((uint8_t)op->page_str[0])) {
+                op->vpd_pn = sg_get_num_nomult(op->page_str);
+                if ((op->vpd_pn < 0) || (op->vpd_pn > 255)) {
+                    pr2serr("Bad page code value after '-p' option\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else {
+                pr2serr("with --enumerate only search using VPD page "
+                        "numbers\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            matches = count_standard_vpds(op->vpd_pn);
+            if (0 == matches)
+                matches = svpd_count_vendor_vpds(op->vpd_pn,
+                                                 op->vend_prod_num);
+            if (0 == matches)
+                sgj_pr_hr(jsp, "No matches found for VPD page number 0x%x\n",
+                          op->vpd_pn);
+        } else {        /* enumerate standard then vendor VPD pages */
+            sgj_pr_hr(jsp, "Standard VPD pages:\n");
+            enumerate_vpds(1, 1);
+        }
+        return 0;
+    }
+
+    as_json = jsp->pr_as_json;
+    if (as_json)
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+
+    if (op->page_str) {
+        if ('-' == op->page_str[0])
+            op->vpd_pn = VPD_NOPE_WANT_STD_INQ;
+        else if (isalpha((uint8_t)op->page_str[0])) {
+            vnp = sdp_find_vpd_by_acron(op->page_str);
+            if (NULL == vnp) {
+                vnp = svpd_find_vendor_by_acron(op->page_str);
+                if (NULL == vnp) {
+                    if (0 == strcmp("stdinq", op->page_str)) {
+                        vnp = sdp_find_vpd_by_acron("sinq");
+                    } else {
+                        pr2serr("abbreviation doesn't match a VPD page\n");
+                        sgj_pr_hr(jsp, "Available standard VPD pages:\n");
+                        enumerate_vpds(1, 1);
+                        ret = SG_LIB_SYNTAX_ERROR;
+                        goto fini;
+                    }
+                }
+            }
+            op->vpd_pn = vnp->value;
+            subvalue = vnp->subvalue;
+            op->vend_prod_num = subvalue;
+        } else {
+            cp = strchr(op->page_str, ',');
+            if (cp && op->vend_prod) {
+                pr2serr("the --page=pg,vp and the --vendor=vp forms overlap, "
+                        "choose one or the other\n");
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto fini;
+            }
+            op->vpd_pn = sg_get_num_nomult(op->page_str);
+            if ((op->vpd_pn < 0) || (op->vpd_pn > 255)) {
+                pr2serr("Bad page code value after '-p' option\n");
+                sgj_pr_hr(jsp, "Available standard VPD pages:\n");
+                enumerate_vpds(1, 1);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto fini;
+            }
+            if (cp) {
+                if (isdigit((uint8_t)*(cp + 1)))
+                    op->vend_prod_num = sg_get_num_nomult(cp + 1);
+                else
+                    op->vend_prod_num = svpd_find_vp_num_by_acron(cp + 1);
+                if ((op->vend_prod_num < 0) || (op->vend_prod_num > 255)) {
+                    pr2serr("Bad vendor/product acronym after comma in '-p' "
+                            "option\n");
+                    if (op->vend_prod_num < 0)
+                        svpd_enumerate_vendor(-1);
+                    ret = SG_LIB_SYNTAX_ERROR;
+                    goto fini;
+                }
+                subvalue = op->vend_prod_num;
+            } else if (op->vend_prod) {
+                if (isdigit((uint8_t)op->vend_prod[0]))
+                    op->vend_prod_num = sg_get_num_nomult(op->vend_prod);
+                else
+                    op->vend_prod_num =
+                        svpd_find_vp_num_by_acron(op->vend_prod);
+                if ((op->vend_prod_num < 0) || (op->vend_prod_num > 255)) {
+                    pr2serr("Bad vendor/product acronym after '--vendor=' "
+                            "option\n");
+                    svpd_enumerate_vendor(-1);
+                    ret = SG_LIB_SYNTAX_ERROR;
+                    goto fini;
+                }
+                subvalue = op->vend_prod_num;
+            }
+        }
+        if (op->verbose > 3)
+               pr2serr("'--page=' matched pn=%d [0x%x], subvalue=%d\n",
+                       op->vpd_pn, op->vpd_pn, subvalue);
+    } else if (op->vend_prod) {
+        if (isdigit((uint8_t)op->vend_prod[0]))
+            op->vend_prod_num = sg_get_num_nomult(op->vend_prod);
+        else
+            op->vend_prod_num = svpd_find_vp_num_by_acron(op->vend_prod);
+        if ((op->vend_prod_num < 0) || (op->vend_prod_num > 255)) {
+            pr2serr("Bad vendor/product acronym after '--vendor=' "
+                    "option\n");
+            svpd_enumerate_vendor(-1);
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto fini;
+        }
+        subvalue = op->vend_prod_num;
+    }
+
+    rsp_buff = sg_memalign(rsp_buff_sz, 0 /* page align */, &free_rsp_buff,
+                           false);
+    if (NULL == rsp_buff) {
+        pr2serr("Unable to allocate %d bytes on heap\n", rsp_buff_sz);
+        ret = sg_convert_errno(ENOMEM);
+        goto fini;
+    }
+    if (op->sinq_inraw_fn) {
+        if ((ret = sg_f2hex_arr(op->sinq_inraw_fn, true, false, rsp_buff,
+                                &inraw_len, rsp_buff_sz))) {
+            goto err_out;
+        }
+        if (inraw_len < 36) {
+            pr2serr("Unable to read 36 or more bytes from %s\n",
+                    op->sinq_inraw_fn);
+            ret = SG_LIB_FILE_ERROR;
+            goto err_out;
+        }
+        memcpy(op->std_inq_a,  rsp_buff, 36);
+        op->std_inq_a_valid = true;
+    }
+    if (op->inhex_fn) {
+        if (op->device_name) {
+            pr2serr("Cannot have both a DEVICE and --inhex= option\n");
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto err_out;
+        }
+        if ((ret = sg_f2hex_arr(op->inhex_fn, !!op->do_raw, false, rsp_buff,
+                                &inhex_len, rsp_buff_sz))) {
+            goto err_out;
+        }
+        if (op->verbose > 2)
+            pr2serr("Read %d [0x%x] bytes of user supplied data\n", inhex_len,
+                    inhex_len);
+        if (op->verbose > 3)
+            hex2stderr(rsp_buff, inhex_len, 0);
+        op->do_raw = 0;         /* don't want raw on output with --inhex= */
+        if ((NULL == op->page_str) && (! op->do_all)) {
+            /* may be able to deduce VPD page */
+            if ((0x2 == (0xf & rsp_buff[3])) && (rsp_buff[2] > 2)) {
+                if (op->verbose)
+                    pr2serr("Guessing from --inhex= this is a standard "
+                            "INQUIRY\n");
+            } else if (rsp_buff[2] <= 2) {
+                if (op->verbose)
+                    pr2serr("Guessing from --inhex this is VPD page 0x%x\n",
+                            rsp_buff[1]);
+                op->vpd_pn = rsp_buff[1];
+            } else {
+                if (op->vpd_pn > 0x80) {
+                    op->vpd_pn = rsp_buff[1];
+                    if (op->verbose)
+                        pr2serr("Guessing from --inhex this is VPD page "
+                                "0x%x\n", rsp_buff[1]);
+                } else {
+                    op->vpd_pn = VPD_NOPE_WANT_STD_INQ;
+                    if (op->verbose)
+                        pr2serr("page number unclear from --inhex, hope "
+                                "it's a standard INQUIRY response\n");
+                }
+            }
+        }
+    } else if ((NULL == op->device_name) && (! op->std_inq_a_valid)) {
+        pr2serr("No DEVICE argument given\n\n");
+        usage();
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto err_out;
+    }
+
+    if (op->do_raw && op->do_hex) {
+        pr2serr("Can't do hex and raw at the same time\n");
+        usage();
+        ret = SG_LIB_SYNTAX_ERROR;
+        goto err_out;
+    }
+    if (op->do_ident) {
+        op->vpd_pn = VPD_DEVICE_ID;
+        if (op->do_ident > 1) {
+            if (! op->do_long)
+                op->do_quiet = true;
+            subvalue = VPD_DI_SEL_LU;
+        }
+    }
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            ret = SG_LIB_FILE_ERROR;
+            goto err_out;
+        }
+    }
+
+    if (op->inhex_fn) {
+        if ((0 == op->maxlen) || (inhex_len < op->maxlen))
+            op->maxlen = inhex_len;
+        if (op->do_all || op->page_given)
+            res = svpd_decode_all(-1, op, jop);
+        else {
+            res = svpd_decode_t10(-1, op, jop, subvalue, 0, NULL);
+            if (SG_LIB_CAT_OTHER == res) {
+                res = svpd_decode_vendor(-1, op, jop, 0);
+                if (SG_LIB_CAT_OTHER == res)
+                    res = svpd_unable_to_decode(-1, op, subvalue, 0);
+            }
+        }
+        ret = res;
+        goto err_out;
+    } else if (op->std_inq_a_valid && (NULL == op->device_name)) {
+        /* nothing else to do ... */
+        /* --sinq_inraw=RFN contents still in rsp_buff */
+        if (op->do_raw)
+            dStrRaw(rsp_buff, inraw_len);
+        else if (op->do_hex) {
+            if (! op->do_quiet && (op->do_hex < 3))
+                sgj_pr_hr(jsp, "Standard Inquiry data format:\n");
+            hex2stdout(rsp_buff, inraw_len, (1 == op->do_hex) ? 0 : -1);
+        } else
+            std_inq_decode(rsp_buff, inraw_len, op, jop);
+        ret = 0;
+        goto fini;
+    }
+
+    if ((sg_fd = sg_cmds_open_device(op->device_name, true /* ro */,
+                                     op->verbose)) < 0) {
+        if (op->verbose > 0)
+            pr2serr("error opening file: %s: %s\n", op->device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        if (ret < 0)
+            ret = SG_LIB_FILE_ERROR;
+        goto err_out;
+    }
+
+    if (op->examine_given) {
+        ret = svpd_examine_all(sg_fd, op, jop);
+    } else if (op->do_all)
+        ret = svpd_decode_all(sg_fd, op, jop);
+    else {
+        memset(rsp_buff, 0, rsp_buff_sz);
+
+        res = svpd_decode_t10(sg_fd, op, jop, subvalue, 0, NULL);
+        if (SG_LIB_CAT_OTHER == res) {
+            res = svpd_decode_vendor(sg_fd, op, jop, 0);
+            if (SG_LIB_CAT_OTHER == res)
+                res = svpd_unable_to_decode(sg_fd, op, subvalue, 0);
+        }
+        if (! op->do_quiet) {
+            if (SG_LIB_CAT_ABORTED_COMMAND == res)
+                pr2serr("fetching VPD page failed, aborted command\n");
+            else if (res) {
+                char b[80];
+
+                sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
+                pr2serr("fetching VPD page failed: %s\n", b);
+            }
+        }
+        ret = res;
+    }
+err_out:
+    if (free_rsp_buff)
+        free(free_rsp_buff);
+    if ((0 == op->verbose) && (! op->do_quiet)) {
+        if (! sg_if_can2stderr("sg_vpd failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+fini:
+    res = (sg_fd >= 0) ? sg_cmds_close_device(sg_fd) : 0;
+
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+    ret = (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+    if (as_json) {
+        if (0 == op->do_hex)
+            sgj_js2file(jsp, NULL, ret, stdout);
+        sgj_finish(jsp);
+    }
+    return ret;
+}
diff --git a/src/sg_vpd_common.c b/src/sg_vpd_common.c
new file mode 100644
index 0000000..4ec5802
--- /dev/null
+++ b/src/sg_vpd_common.c
@@ -0,0 +1,3501 @@
+/*
+ * Copyright (c) 2006-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+#include "sg_vpd_common.h"
+
+/* This file holds common code for sg_inq and sg_vpd as both those utilities
+ * decode SCSI VPD pages. */
+
+const char * t10_vendor_id_hr = "T10_vendor_identification";
+const char * t10_vendor_id_js = "t10_vendor_identification";
+const char * product_id_hr = "Product_identification";
+const char * product_id_js = "product_identification";
+const char * product_rev_lev_hr = "Product_revision_level";
+const char * product_rev_lev_js = "product_revision_level";
+static const char * const y_s = "yes";
+static const char * const n_s = "no";
+static const char * const nl_s = "no limit";
+static const char * const nlr_s = "no limit reported";
+/* Earlier gcc compilers (e.g. 6.4) don't accept this first form when it is
+ * used in another array of strings initialization (e.g. bdc_zoned_strs) */
+// static const char * const nr_s = "not reported";
+static char nr_s[] = "not reported";
+static const char * const ns_s = "not supported";
+// static const char * const rsv_s = "Reserved";
+static char rsv_s[] = "Reserved";
+static const char * const vs_s = "Vendor specific";
+static const char * const null_s = "";
+static const char * const mn_s = "meaning";
+
+/* Supported vendor specific VPD pages */
+/* Arrange in alphabetical order by acronym */
+struct svpd_vp_name_t vp_arr[] = {
+    {VPD_VP_DDS, "dds", "DDS tape family from IBM"},
+    {VPD_VP_EMC, "emc", "EMC (company)"},
+    {VPD_VP_WDC_HITACHI, "hit", "WDC/Hitachi disk"},
+    {VPD_VP_HP3PAR, "hp3par", "3PAR array (HP was Left Hand)"},
+    {VPD_VP_HP_LTO, "hp_lto", "HP LTO tape/systems"},
+    {VPD_VP_IBM_LTO, "ibm_lto", "IBM LTO tape/systems"},
+    {VPD_VP_NVME, "nvme", "NVMe related"},
+    {VPD_VP_RDAC, "rdac", "RDAC array (NetApp E-Series)"},
+    {VPD_VP_SEAGATE, "sea", "Seagate disk"},
+    {VPD_VP_SG, "sg", "sg3_utils extensions"},
+    {VPD_VP_WDC_HITACHI, "wdc", "WDC/Hitachi disk"},
+    {0, NULL, NULL},
+};
+
+/* Supported vendor specific VPD pages */
+/* 'subvalue' holds vendor/product number to disambiguate */
+/* Arrange in alphabetical order by acronym */
+struct svpd_values_name_t vendor_vpd_pg[] = {
+    {VPD_V_ACI_LTO, VPD_VP_HP_LTO, 1, "aci", "ACI revision level (HP LTO)"},
+    {VPD_V_DATC_SEA, VPD_VP_SEAGATE, 0, "datc", "Date code (Seagate)"},
+    {VPD_V_DCRL_LTO, VPD_VP_IBM_LTO, 1, "dcrl", "Drive component revision "
+     "levels (IBM LTO)"},
+    {VPD_V_FVER_DDS, VPD_VP_DDS, 1, "ddsver", "Firmware revision (DDS)"},
+    {VPD_V_DEV_BEH_SEA, VPD_VP_SEAGATE, 0, "devb", "Device behavior "
+     "(Seagate)"},
+    {VPD_V_DSN_LTO, VPD_VP_IBM_LTO, 1, "dsn", "Drive serial numbers (IBM "
+     "LTO)"},
+    {VPD_V_DUCD_LTO, VPD_VP_IBM_LTO, 1, "ducd", "Device unique "
+     "configuration data (IBM LTO)"},
+    {VPD_V_EDID_RDAC, VPD_VP_RDAC, 0, "edid", "Extended device "
+     "identification (RDAC)"},
+    {VPD_V_FIRM_SEA, VPD_VP_SEAGATE, 0, "firm", "Firmware numbers "
+     "(Seagate)"},
+    {VPD_V_FVER_LTO, VPD_VP_HP_LTO, 0, "frl", "Firmware revision level "
+     "(HP LTO)"},
+    {VPD_V_FVER_RDAC, VPD_VP_RDAC, 0, "fwr4", "Firmware version (RDAC)"},
+    {VPD_V_HEAD_LTO, VPD_VP_HP_LTO, 1, "head", "Head Assy revision level "
+     "(HP LTO)"},
+    {VPD_V_HP3PAR, VPD_VP_HP3PAR, 0, "hp3par", "Volume information "
+     "(HP/3PAR)"},
+    {VPD_V_HVER_LTO, VPD_VP_HP_LTO, 1, "hrl", "Hardware revision level "
+     "(HP LTO)"},
+    {VPD_V_HVER_RDAC, VPD_VP_RDAC, 0, "hwr4", "Hardware version (RDAC)"},
+    {VPD_V_JUMP_SEA, VPD_VP_SEAGATE, 0, "jump", "Jump setting (Seagate)"},
+    {VPD_V_MECH_LTO, VPD_VP_HP_LTO, 1, "mech", "Mechanism revision level "
+     "(HP LTO)"},
+    {VPD_V_MPDS_LTO, VPD_VP_IBM_LTO, 1, "mpds", "Mode parameter default "
+     "settings (IBM LTO)"},
+    {SG_NVME_VPD_NICR, VPD_VP_SG, 0, "nicr",
+     "NVMe Identify Controller Response (sg3_utils)"},
+    {VPD_V_PCA_LTO, VPD_VP_HP_LTO, 1, "pca", "PCA revision level (HP LTO)"},
+    {VPD_V_FEAT_RDAC, VPD_VP_RDAC, 0, "prm4", "Feature Parameters (RDAC)"},
+    {VPD_V_RVSI_RDAC, VPD_VP_RDAC, 0, "rvsi", "Replicated volume source "
+     "identifier (RDAC)"},
+    {VPD_V_SAID_RDAC, VPD_VP_RDAC, 0, "said", "Storage array world wide "
+     "name (RDAC)"},
+    {VPD_V_SUBS_RDAC, VPD_VP_RDAC, 0, "subs", "Subsystem identifier (RDAC)"},
+    {VPD_V_SVER_RDAC, VPD_VP_RDAC, 0, "swr4", "Software version (RDAC)"},
+    {VPD_V_UPR_EMC, VPD_VP_EMC, 0, "upr", "Unit path report (EMC)"},
+    {VPD_V_VAC_RDAC, VPD_VP_RDAC, 0, "vac1", "Volume access control (RDAC)"},
+    {VPD_V_HIT_PG3, VPD_VP_WDC_HITACHI, 0, "wp3", "Page 0x3 (WDC/Hitachi)"},
+    {VPD_V_HIT_PG_D1, VPD_VP_WDC_HITACHI, 0, "wpd1",
+     "Page 0xd1 (WDC/Hitachi)"},
+    {VPD_V_HIT_PG_D2, VPD_VP_WDC_HITACHI, 0, "wpd2",
+     "Page 0xd2 (WDC/Hitachi)"},
+    {0, 0, 0, NULL, NULL},
+};
+
+
+int
+no_ascii_4hex(const struct opts_t * op)
+{
+    if (op->do_hex < 2)
+        return 1;
+    else if (2 == op->do_hex)
+        return 0;
+    else
+        return -1;
+}
+
+int
+svpd_find_vp_num_by_acron(const char * vp_ap)
+{
+    size_t len;
+    const struct svpd_vp_name_t * vpp;
+
+    for (vpp = vp_arr; vpp->acron; ++vpp) {
+        len = strlen(vpp->acron);
+        if (0 == strncmp(vpp->acron, vp_ap, len))
+            return vpp->vend_prod_num;
+    }
+    return -1;
+}
+
+/* if vend_prod_num < -1 then list vendor_product ids + vendor pages, =-1
+ * list only vendor_product ids, else list pages for that vend_prod_num */
+void
+svpd_enumerate_vendor(int vend_prod_num)
+{
+    bool seen;
+    const struct svpd_vp_name_t * vpp;
+    const struct svpd_values_name_t * vnp;
+
+    if (vend_prod_num < 0) {
+        for (seen = false, vpp = vp_arr; vpp->acron; ++vpp) {
+            if (vpp->name) {
+                if (! seen) {
+                    printf("\nVendor/product identifiers:\n");
+                    seen = true;
+                }
+                printf("  %-10s %d      %s\n", vpp->acron,
+                       vpp->vend_prod_num, vpp->name);
+            }
+        }
+    }
+    if (-1 == vend_prod_num)
+        return;
+    for (seen = false, vnp = vendor_vpd_pg; vnp->acron; ++vnp) {
+        if ((vend_prod_num >= 0) && (vend_prod_num != vnp->subvalue))
+            continue;
+        if (vnp->name) {
+            if (! seen) {
+                printf("\nVendor specific VPD pages:\n");
+                seen = true;
+            }
+            printf("  %-10s 0x%02x,%d      %s\n", vnp->acron,
+                   vnp->value, vnp->subvalue, vnp->name);
+        }
+    }
+}
+
+/* mxlen is command line --maxlen=LEN option (def: 0) or -1 for a VPD page
+ * with a short length (1 byte). Returns 0 for success. */
+int     /* global: use by sg_vpd_vendor.c */
+vpd_fetch_page(int sg_fd, uint8_t * rp, int page, int mxlen, bool qt,
+               int vb, int * rlenp)
+{
+    int res, resid, rlen, len, n;
+
+    if (sg_fd < 0) {
+        len = sg_get_unaligned_be16(rp + 2) + 4;
+        if (vb && (len > mxlen))
+            pr2serr("warning: VPD page's length (%d) > bytes in --inhex=FN "
+                    "file (%d)\n",  len , mxlen);
+        if (rlenp)
+            *rlenp = (len < mxlen) ? len : mxlen;
+        return 0;
+    }
+    if (mxlen > MX_ALLOC_LEN) {
+        pr2serr("--maxlen=LEN too long: %d > %d\n", mxlen, MX_ALLOC_LEN);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    n = (mxlen > 0) ? mxlen : DEF_ALLOC_LEN;
+    res = sg_ll_inquiry_v2(sg_fd, true, page, rp, n, DEF_PT_TIMEOUT, &resid,
+                           ! qt, vb);
+    if (res)
+        return res;
+    rlen = n - resid;
+    if (rlen < 4) {
+        pr2serr("VPD response too short (len=%d)\n", rlen);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    if (page != rp[1]) {
+        pr2serr("invalid VPD response; probably a STANDARD INQUIRY "
+                "response\n");
+        n = (rlen < 32) ? rlen : 32;
+        if (vb) {
+            pr2serr("First %d bytes of bad response\n", n);
+            hex2stderr(rp, n, 0);
+        }
+        return SG_LIB_CAT_MALFORMED;
+    } else if ((0x80 == page) && (0x2 == rp[2]) && (0x2 == rp[3])) {
+        /* could be a Unit Serial number VPD page with a very long
+         * length of 4+514 bytes; more likely standard response for
+         * SCSI-2, RMB=1 and a response_data_format of 0x2. */
+        pr2serr("invalid Unit Serial Number VPD response; probably a "
+                "STANDARD INQUIRY response\n");
+        return SG_LIB_CAT_MALFORMED;
+    }
+    if (mxlen < 0)
+        len = rp[3] + 4;
+    else
+        len = sg_get_unaligned_be16(rp + 2) + 4;
+    if (len <= rlen) {
+        if (rlenp)
+            *rlenp = len;
+        return 0;
+    } else if (mxlen) {
+        if (rlenp)
+            *rlenp = rlen;
+        return 0;
+    }
+    if (len > MX_ALLOC_LEN) {
+        pr2serr("response length too long: %d > %d\n", len, MX_ALLOC_LEN);
+        return SG_LIB_CAT_MALFORMED;
+    } else {
+        res = sg_ll_inquiry_v2(sg_fd, true, page, rp, len, DEF_PT_TIMEOUT,
+                               &resid, ! qt, vb);
+        if (res)
+            return res;
+        rlen = len - resid;
+        /* assume it is well behaved: hence page and len still same */
+        if (rlenp)
+            *rlenp = rlen;
+        return 0;
+    }
+}
+
+sgj_opaque_p
+sg_vpd_js_hdr(sgj_state * jsp, sgj_opaque_p jop, const char * name,
+              const uint8_t * vpd_hdrp)
+{
+    int pdt = vpd_hdrp[0] & PDT_MASK;
+    int pqual = (vpd_hdrp[0] & 0xe0) >> 5;
+    int pn = vpd_hdrp[1];
+    const char * pdt_str;
+    sgj_opaque_p jo2p = sgj_snake_named_subobject_r(jsp, jop, name);
+    char d[64];
+
+    pdt_str = sg_get_pdt_str(pdt, sizeof(d), d);
+    sgj_js_nv_ihexstr(jsp, jo2p, "peripheral_qualifier",
+                      pqual, NULL, pqual_str(pqual));
+    sgj_js_nv_ihexstr(jsp, jo2p, "peripheral_device_type",
+                      pdt, NULL, pdt_str);
+    sgj_js_nv_ihex(jsp, jo2p, "page_code", pn);
+    return jo2p;
+}
+
+const char *
+pqual_str(int pqual)
+{
+    switch (pqual) {
+    case 0:
+        return "LU accessible";
+    case 1:
+        return "LU temporarily unavailable";
+    case 3:
+        return "LU not accessible via this port";
+    default:
+        return "value reserved by T10";
+    }
+}
+
+static const char * network_service_type_arr[] =
+{
+    "unspecified",
+    "storage configuration service",
+    "diagnostics",
+    "status",
+    "logging",
+    "code download",
+    "copy service",
+    "administrative configuration service",
+    "reserved[0x8]", "reserved[0x9]",
+    "reserved[0xa]", "reserved[0xb]", "reserved[0xc]", "reserved[0xd]",
+    "reserved[0xe]", "reserved[0xf]", "reserved[0x10]", "reserved[0x11]",
+    "reserved[0x12]", "reserved[0x13]", "reserved[0x14]", "reserved[0x15]",
+    "reserved[0x16]", "reserved[0x17]", "reserved[0x18]", "reserved[0x19]",
+    "reserved[0x1a]", "reserved[0x1b]", "reserved[0x1c]", "reserved[0x1d]",
+    "reserved[0x1e]", "reserved[0x1f]",
+};
+
+/* VPD_MAN_NET_ADDR     0x85 ["mna"] */
+void
+decode_net_man_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                   sgj_opaque_p jap)
+{
+    int k, bump, na_len, assoc, nst;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    const uint8_t * bp;
+    const char * assoc_str;
+    const char * nst_str;
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("Management network addresses VPD page length too short=%d\n",
+                len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        assoc = (bp[0] >> 5) & 0x3;
+        assoc_str = sg_get_desig_assoc_str(assoc);
+        nst = bp[0] & 0x1f;
+        nst_str = network_service_type_arr[nst];
+        sgj_pr_hr(jsp, "  %s, Service type: %s\n", assoc_str, nst_str);
+        na_len = sg_get_unaligned_be16(bp + 2);
+        if (jsp->pr_as_json) {
+            jo2p = sgj_new_unattached_object_r(jsp);
+            sgj_js_nv_ihexstr(jsp, jo2p, "association", assoc, NULL,
+                              assoc_str);
+            sgj_js_nv_ihexstr(jsp, jo2p, "service_type", nst, NULL,
+                              nst_str);
+            sgj_js_nv_s_len(jsp, jo2p, "network_address",
+                            (const char *)(bp + 4), na_len);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+        }
+        if (na_len > 0) {
+            if (op->do_hex > 1) {
+                sgj_pr_hr(jsp, "    Network address:\n");
+                hex2stdout((bp + 4), na_len, 0);
+            } else
+                sgj_pr_hr(jsp, "    %s\n", bp + 4);
+        }
+        bump = 4 + na_len;
+        if ((k + bump) > len) {
+            pr2serr("Management network addresses VPD page, short "
+                    "descriptor length=%d, left=%d\n", bump, (len - k));
+            return;
+        }
+    }
+}
+
+/* VPD_EXT_INQ    Extended Inquiry VPD ["ei"] */
+void
+decode_x_inq_vpd(const uint8_t * b, int len, bool protect, struct opts_t * op,
+                 sgj_opaque_p jop)
+{
+    bool do_long_nq = op->do_long && (! op->do_quiet);
+    int n;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    const char * cp;
+    const char * np;
+    const char * nex_p;
+    char d[128];
+    static const int dlen = sizeof(d);
+
+    if (len < 7) {
+        pr2serr("Extended INQUIRY data VPD page length too short=%d\n", len);
+        return;
+    }
+    if (op->do_hex) {
+        hex2stdout(b, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (do_long_nq || jsp->pr_as_json) {
+        n = (b[4] >> 6) & 0x3;
+        if (1 == n)
+            cp = "before final WRITE BUFFER";
+        else if (2 == n)
+            cp = "after power on or hard reset";
+        else {
+            cp = "none";
+            d[0] = '\0';
+        }
+        if (cp[0])
+            snprintf(d, dlen, " [%s]", cp);
+        sgj_pr_hr(jsp, "  ACTIVATE_MICROCODE=%d%s\n", n, d);
+        sgj_js_nv_ihexstr(jsp, jop, "activate_microcode", n, NULL, cp);
+        n = (b[4] >> 3) & 0x7;
+        if (protect) {
+            switch (n)
+            {
+            case 0:
+                cp = "protection type 1 supported";
+                break;
+            case 1:
+                cp = "protection types 1 and 2 supported";
+                break;
+            case 2:
+                cp = "protection type 2 supported";
+                break;
+            case 3:
+                cp = "protection types 1 and 3 supported";
+                break;
+            case 4:
+                cp = "protection type 3 supported";
+                break;
+            case 5:
+                cp = "protection types 2 and 3 supported";
+                break;
+            case 6:
+                cp = "see Supported block lengths and protection types "
+                     "VPD page";
+                break;
+            case 7:
+                cp = "protection types 1, 2 and 3 supported";
+                break;
+            }
+        } else if (op->protect_not_sure) {
+            cp = "Unsure because unable to read PROTECT bit in standard "
+                 "INQUIRY response";
+            d[0] = '\0';
+        } else {
+            cp = "none";
+            d[0] = '\0';
+        }
+        if (cp[0])
+            snprintf(d, dlen, " [%s]", cp);
+        sgj_pr_hr(jsp, "  SPT=%d%s\n", n, d);
+        sgj_js_nv_ihexstr_nex(jsp, jop, "spt", n, false, NULL,
+                              cp, "Supported Protection Type");
+        sgj_haj_vi_nex(jsp, jop, 2, "GRD_CHK", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[4] & 0x4), false, "guard check");
+        sgj_haj_vi_nex(jsp, jop, 2, "APP_CHK", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[4] & 0x2), false, "application tag check");
+        sgj_haj_vi_nex(jsp, jop, 2, "REF_CHK", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[4] & 0x1), false, "reference tag check");
+        sgj_haj_vi_nex(jsp, jop, 2, "UASK_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[5] & 0x20), false, "Unit Attention "
+                       "condition Sense Key specific data Supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "GROUP_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[5] & 0x10), false, "grouping function supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "PRIOR_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[5] & 0x8), false, "priority supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "HEADSUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[5] & 0x4), false, "head of queue supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "ORDSUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[5] & 0x2), false, "ordered (task attribute) "
+                       "supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "SIMPSUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[5] & 0x1), false, "simple (task attribute) "
+                       "supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "WU_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[6] & 0x8), false, "Write uncorrectable "
+                       "supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "CRD_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[6] & 0x4), false, "Correction disable "
+                       "supported (obsolete SPC-5)");
+        sgj_haj_vi_nex(jsp, jop, 2, "NV_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[6] & 0x2), false, "Nonvolatile cache "
+                       "supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "V_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[6] & 0x1), false, "Volatile cache supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "NO_PI_CHK", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[7] & 0x20), false, "No protection "
+                       "information checking");        /* spc5r02 */
+        sgj_haj_vi_nex(jsp, jop, 2, "P_I_I_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[7] & 0x10), false, "Protection information "
+                       "interval supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "LUICLR", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[7] & 0x1), false, "Logical unit I_T nexus clear");
+        np = "LU_COLL_TYPE";
+        n = (b[8] >> 5) & 0x7;
+        nex_p = "Logical unit collection type";
+        if (jsp && (jsp->pr_string)) {
+            switch (n) {
+            case 0:
+                cp = "not reported";
+                break;
+            case 1:
+                cp = "Conglomerate";
+                break;
+            case 2:
+                cp = "Logical unit group";
+                break;
+            default:
+                cp = rsv_s;
+                break;
+            }
+            jo2p = sgj_haj_subo_r(jsp, jop, 2, np, SGJ_SEP_EQUAL_NO_SPACE,
+                                  n, false);
+            sgj_js_nv_s(jsp, jo2p, mn_s, cp);
+            if (jsp->pr_name_ex)
+                sgj_js_nv_s(jsp, jo2p, "abbreviated_name_expansion", nex_p);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, np, SGJ_SEP_EQUAL_NO_SPACE, n,
+                           true, nex_p);
+
+        sgj_haj_vi_nex(jsp, jop, 2, "R_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[8] & 0x10), false, "Referrals supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "RTD_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[8] & 0x8), false,
+                       "Revert to defaults supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "HSSRELEF", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[8] & 0x2), false,
+                       "History snapshots release effects");
+        sgj_haj_vi_nex(jsp, jop, 2, "CBCS", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[8] & 0x1), false, "Capability-based command "
+                       "security (obsolete SPC-5)");
+        sgj_haj_vi(jsp, jop, 2, "Multi I_T nexus microcode download",
+                   SGJ_SEP_EQUAL_NO_SPACE, b[9] & 0xf, true);
+        sgj_haj_vi(jsp, jop, 2, "Extended self-test completion minutes",
+                   SGJ_SEP_EQUAL_NO_SPACE,
+                   sg_get_unaligned_be16(b + 10), true);
+        sgj_haj_vi_nex(jsp, jop, 2, "POA_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[12] & 0x80), false,
+                       "Power on activation supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "HRA_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[12] & 0x40), false,
+                       "Hard reset activation supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "VSA_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[12] & 0x20), false,
+                       "Vendor specific activation supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "DMS_VALID", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[12] & 0x10), false,
+                       "Download microcode support byte valid");
+        sgj_haj_vi(jsp, jop, 2, "Maximum supported sense data length",
+                   SGJ_SEP_EQUAL_NO_SPACE, b[13], true);
+        sgj_haj_vi_nex(jsp, jop, 2, "IBS", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[14] & 0x80), false, "Implicit bind supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "IAS", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[14] & 0x40), false,
+                       "Implicit affiliation supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "SAC", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[14] & 0x4), false,
+                       "Set affiliation command supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "NRD1", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[14] & 0x2), false,
+                       "No redirect one supported (BIND)");
+        sgj_haj_vi_nex(jsp, jop, 2, "NRD0", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[14] & 0x1), false,
+                       "No redirect zero supported (BIND)");
+        sgj_haj_vi(jsp, jop, 2, "Maximum inquiry change logs",
+                   SGJ_SEP_EQUAL_NO_SPACE,
+                   sg_get_unaligned_be16(b + 15), true);
+        sgj_haj_vi(jsp, jop, 2, "Maximum mode page change logs",
+                   SGJ_SEP_EQUAL_NO_SPACE,
+                   sg_get_unaligned_be16(b + 17), true);
+        sgj_haj_vi_nex(jsp, jop, 2, "DM_MD_4", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[19] & 0x80), false,
+                       "Download microcode mode 4 supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "DM_MD_5", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[19] & 0x40), false,
+                       "Download microcode mode 5 supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "DM_MD_6", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[19] & 0x20), false,
+                       "Download microcode mode 6 supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "DM_MD_7", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[19] & 0x10), false,
+                       "Download microcode mode 7 supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "DM_MD_D", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[19] & 0x8), false,
+                       "Download microcode mode 0xd supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "DM_MD_E", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[19] & 0x4), false,
+                       "Download microcode mode 0xe supported");
+        sgj_haj_vi_nex(jsp, jop, 2, "DM_MD_F", SGJ_SEP_EQUAL_NO_SPACE,
+                       !!(b[19] & 0x2), false,
+                       "Download microcode mode 0xf supported");
+        if (do_long_nq || (! jsp->pr_out_hr))
+            return;
+    }
+    sgj_pr_hr(jsp, "  ACTIVATE_MICROCODE=%d SPT=%d GRD_CHK=%d APP_CHK=%d "
+              "REF_CHK=%d\n", ((b[4] >> 6) & 0x3), ((b[4] >> 3) & 0x7),
+              !!(b[4] & 0x4), !!(b[4] & 0x2), !!(b[4] & 0x1));
+    sgj_pr_hr(jsp, "  UASK_SUP=%d GROUP_SUP=%d PRIOR_SUP=%d HEADSUP=%d "
+              "ORDSUP=%d SIMPSUP=%d\n", !!(b[5] & 0x20), !!(b[5] & 0x10),
+              !!(b[5] & 0x8), !!(b[5] & 0x4), !!(b[5] & 0x2), !!(b[5] & 0x1));
+    sgj_pr_hr(jsp, "  WU_SUP=%d [CRD_SUP=%d] NV_SUP=%d V_SUP=%d\n",
+              !!(b[6] & 0x8), !!(b[6] & 0x4), !!(b[6] & 0x2), !!(b[6] & 0x1));
+    sgj_pr_hr(jsp, "  NO_PI_CHK=%d P_I_I_SUP=%d LUICLR=%d\n", !!(b[7] & 0x20),
+              !!(b[7] & 0x10), !!(b[7] & 0x1));
+    /* RTD_SUP added in spc5r11, LU_COLL_TYPE added in spc5r09,
+     * HSSRELEF added in spc5r02; CBCS obsolete in spc5r01 */
+    sgj_pr_hr(jsp, "  LU_COLL_TYPE=%d R_SUP=%d RTD_SUP=%d HSSRELEF=%d "
+              "[CBCS=%d]\n", (b[8] >> 5) & 0x7, !!(b[8] & 0x10),
+              !!(b[8] & 0x8), !!(b[8] & 0x2), !!(b[8] & 0x1));
+    sgj_pr_hr(jsp, "  Multi I_T nexus microcode download=%d\n", b[9] & 0xf);
+    sgj_pr_hr(jsp, "  Extended self-test completion minutes=%d\n",
+              sg_get_unaligned_be16(b + 10));    /* spc4r27 */
+    sgj_pr_hr(jsp, "  POA_SUP=%d HRA_SUP=%d VSA_SUP=%d DMS_VALID=%d\n",
+              !!(b[12] & 0x80), !!(b[12] & 0x40), !!(b[12] & 0x20),
+              !!(b[12] & 0x10));                   /* spc5r20 */
+    sgj_pr_hr(jsp, "  Maximum supported sense data length=%d\n",
+              b[13]); /* spc4r34 */
+    sgj_pr_hr(jsp, "  IBS=%d IAS=%d SAC=%d NRD1=%d NRD0=%d\n",
+              !!(b[14] & 0x80), !!(b[14] & 0x40), !!(b[14] & 0x4),
+              !!(b[14] & 0x2), !!(b[14] & 0x1));  /* added in spc5r09 */
+    sgj_pr_hr(jsp, "  Maximum inquiry change logs=%u\n",
+              sg_get_unaligned_be16(b + 15));        /* spc5r17 */
+    sgj_pr_hr(jsp, "  Maximum mode page change logs=%u\n",
+              sg_get_unaligned_be16(b + 17));        /* spc5r17 */
+    sgj_pr_hr(jsp, "  DM_MD_4=%d DM_MD_5=%d DM_MD_6=%d DM_MD_7=%d\n",
+              !!(b[19] & 0x80), !!(b[19] & 0x40), !!(b[19] & 0x20),
+              !!(b[19] & 0x10));                     /* spc5r20 */
+    sgj_pr_hr(jsp, "  DM_MD_D=%d DM_MD_E=%d DM_MD_F=%d\n",
+              !!(b[19] & 0x8), !!(b[19] & 0x4), !!(b[19] & 0x2));
+}
+
+/* VPD_SOFTW_INF_ID   0x84 */
+void
+decode_softw_inf_id(const uint8_t * buff, int len, struct opts_t * op,
+                    sgj_opaque_p jap)
+{
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jop;
+    uint64_t ieee_id;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    len -= 4;
+    buff += 4;
+    for ( ; len > 5; len -= 6, buff += 6) {
+        ieee_id = sg_get_unaligned_be48(buff + 0);
+        sgj_pr_hr(jsp, "    IEEE identifier: 0x%" PRIx64 "\n", ieee_id);
+        if (jsp->pr_as_json) {
+            jop = sgj_new_unattached_object_r(jsp);
+            sgj_js_nv_ihex(jsp, jop, "ieee_identifier", ieee_id);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jop);
+        }
+   }
+}
+
+static const char * mode_page_policy_arr[] =
+{
+    "shared",
+    "per target port",
+    "per initiator port",
+    "per I_T nexus",
+};
+
+/* VPD_MODE_PG_POLICY  0x87 ["mpp"] */
+void
+decode_mode_policy_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                       sgj_opaque_p jap)
+{
+    int k, n, bump, ppc, pspc;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    const uint8_t * bp;
+    char b[128];
+    static const int blen = sizeof(b);
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 1 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("Mode page policy VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        bump = 4;
+        if ((k + bump) > len) {
+            pr2serr("Mode page policy VPD page, short "
+                    "descriptor length=%d, left=%d\n", bump, (len - k));
+            return;
+        }
+        if (op->do_hex > 1)
+            hex2stdout(bp, 4, 1);
+        else {
+            n = 0;
+            ppc =  (bp[0] & 0x3f);
+            pspc = bp[1];
+            n = sg_scnpr(b + n, blen - n, "  Policy page code: 0x%x", ppc);
+            if (pspc)
+                n += sg_scnpr(b + n, blen - n, ",  subpage code: 0x%x", pspc);
+            sgj_pr_hr(jsp, "%s\n", b);
+            if ((0 == k) && (0x3f == (0x3f & bp[0])) && (0xff == bp[1]))
+                sgj_pr_hr(jsp, "  therefore the policy applies to all modes "
+                          "pages and subpages\n");
+            sgj_pr_hr(jsp, "    MLUS=%d,  Policy: %s\n", !!(bp[2] & 0x80),
+                      mode_page_policy_arr[bp[2] & 0x3]);
+            if (jsp->pr_as_json) {
+                jo2p = sgj_new_unattached_object_r(jsp);
+                sgj_js_nv_ihex(jsp, jo2p, "policy_page_code", ppc);
+                sgj_js_nv_ihex(jsp, jo2p, "policy_subpage_code", pspc);
+                sgj_js_nv_ihex_nex(jsp, jo2p, "mlus", !!(bp[2] & 0x80), false,
+                                   "Multiple logical units share");
+                sgj_js_nv_ihexstr(jsp, jo2p, "mode_page_policy", bp[2] & 0x3,
+                                  NULL, mode_page_policy_arr[bp[2] & 0x3]);
+                sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            }
+        }
+    }
+}
+
+/* VPD_POWER_CONDITION 0x8a ["pc"] */
+void
+decode_power_condition(const uint8_t * buff, int len, struct opts_t * op,
+                       sgj_opaque_p jop)
+{
+    sgj_state * jsp = &op->json_st;
+
+    if (len < 18) {
+        pr2serr("Power condition VPD page length too short=%d\n", len);
+        return;
+    }
+    if (op->do_hex) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    sgj_pr_hr(jsp, "  Standby_y=%d Standby_z=%d Idle_c=%d Idle_b=%d "
+              "Idle_a=%d\n", !!(buff[4] & 0x2), !!(buff[4] & 0x1),
+              !!(buff[5] & 0x4), !!(buff[5] & 0x2), !!(buff[5] & 0x1));
+    if (jsp->pr_as_json) {
+        sgj_js_nv_ihex(jsp, jop, "standby_y", !!(buff[4] & 0x2));
+        sgj_js_nv_ihex(jsp, jop, "standby_z", !!(buff[4] & 0x1));
+        sgj_js_nv_ihex(jsp, jop, "idle_c", !!(buff[5] & 0x4));
+        sgj_js_nv_ihex(jsp, jop, "idle_b", !!(buff[5] & 0x2));
+        sgj_js_nv_ihex(jsp, jop, "idle_a", !!(buff[5] & 0x1));
+    }
+    sgj_haj_vi_nex(jsp, jop, 2, "Stopped condition recovery time",
+                   SGJ_SEP_SPACE_1, sg_get_unaligned_be16(buff + 6),
+                   true, "unit: millisecond");
+    sgj_haj_vi_nex(jsp, jop, 2, "Standby_z condition recovery time",
+                   SGJ_SEP_SPACE_1, sg_get_unaligned_be16(buff + 8),
+                   true, "unit: millisecond");
+    sgj_haj_vi_nex(jsp, jop, 2, "Standby_y condition recovery time",
+                   SGJ_SEP_SPACE_1, sg_get_unaligned_be16(buff + 10),
+                   true, "unit: millisecond");
+    sgj_haj_vi_nex(jsp, jop, 2, "Idle_a condition recovery time",
+                   SGJ_SEP_SPACE_1, sg_get_unaligned_be16(buff + 12),
+                   true, "unit: millisecond");
+    sgj_haj_vi_nex(jsp, jop, 2, "Idle_b condition recovery time",
+                   SGJ_SEP_SPACE_1, sg_get_unaligned_be16(buff + 14),
+                   true, "unit: millisecond");
+    sgj_haj_vi_nex(jsp, jop, 2, "Idle_c condition recovery time",
+                   SGJ_SEP_SPACE_1, sg_get_unaligned_be16(buff + 16),
+                   true, "unit: millisecond");
+}
+
+int
+filter_json_dev_ids(uint8_t * buff, int len, int m_assoc, struct opts_t * op,
+                    sgj_opaque_p jap)
+{
+    int u, off, i_len;
+    sgj_opaque_p jo2p;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+
+    off = -1;
+    while ((u = sg_vpd_dev_id_iter(buff, len, &off, m_assoc, -1, -1)) == 0) {
+        bp = buff + off;
+        i_len = bp[3];
+        if ((off + i_len + 4) > len) {
+            pr2serr("    VPD page error: designator length longer than\n"
+                    "     remaining response length=%d\n", (len - off));
+            return SG_LIB_CAT_MALFORMED;
+        }
+        jo2p = sgj_new_unattached_object_r(jsp);
+        sgj_js_designation_descriptor(jsp, jo2p, bp, i_len + 4);
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+    if (-2 == u) {
+        pr2serr("VPD page error: short designator around offset %d\n", off);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    return 0;
+}
+
+/* VPD_ATA_INFO    0x89 ["ai"] */
+void
+decode_ata_info_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                    sgj_opaque_p jop)
+{
+    bool do_long_nq = op->do_long && (! op->do_quiet);
+    int num, is_be, cc, n;
+    sgj_state * jsp = &op->json_st;
+    const char * cp;
+    const char * ata_transp;
+    char b[512];
+    char d[80];
+    static const int blen = sizeof(b);
+    static const int dlen = sizeof(d);
+    static const char * sat_vip = "SAT Vendor identification";
+    static const char * sat_pip = "SAT Product identification";
+    static const char * sat_prlp = "SAT Product revision level";
+
+    if (len < 36) {
+        pr2serr("ATA information VPD page length too short=%d\n", len);
+        return;
+    }
+    if (op->do_hex && (2 != op->do_hex)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    memcpy(b, buff + 8, 8);
+    b[8] = '\0';
+    sgj_pr_hr(jsp, "  %s: %s\n", sat_vip, b);
+    memcpy(b, buff + 16, 16);
+    b[16] = '\0';
+    sgj_pr_hr(jsp, "  %s: %s\n", sat_pip, b);
+    memcpy(b, buff + 32, 4);
+    b[4] = '\0';
+    sgj_pr_hr(jsp, "  %s: %s\n", sat_prlp, b);
+    if (len < 56)
+        return;
+    ata_transp = (0x34 == buff[36]) ? "SATA" : "PATA";
+    if (do_long_nq) {
+        sgj_pr_hr(jsp, "  Device signature [%s] (in hex):\n", ata_transp);
+        hex2stdout(buff + 36, 20, 0);
+    } else
+        sgj_pr_hr(jsp, "  Device signature indicates %s transport\n",
+                  ata_transp);
+    cc = buff[56];      /* 0xec for IDENTIFY DEVICE and 0xa1 for IDENTIFY
+                         * PACKET DEVICE (obsolete) */
+    n = sg_scnpr(b, blen, "  Command code: 0x%x\n", cc);
+    if (len < 60)
+        return;
+    if (0xec == cc)
+        cp = null_s;
+    else if (0xa1 == cc)
+        cp = "PACKET ";
+    else
+        cp = NULL;
+    is_be = sg_is_big_endian();
+    if (cp) {
+        n += sg_scnpr(b + n, blen - n, "  ATA command IDENTIFY %sDEVICE "
+                      "response summary:\n", cp);
+        num = sg_ata_get_chars((const unsigned short *)(buff + 60), 27, 20,
+                               is_be, d);
+        d[num] = '\0';
+        n += sg_scnpr(b + n, blen - n, "    model: %s\n", d);
+        num = sg_ata_get_chars((const unsigned short *)(buff + 60), 10, 10,
+                               is_be, d);
+        d[num] = '\0';
+        n += sg_scnpr(b + n, blen - n, "    serial number: %s\n", d);
+        num = sg_ata_get_chars((const unsigned short *)(buff + 60), 23, 4,
+                               is_be, d);
+        d[num] = '\0';
+        n += sg_scnpr(b + n, blen - n, "    firmware revision: %s\n", d);
+        sgj_pr_hr(jsp, "%s", b);
+        if (do_long_nq)
+            sgj_pr_hr(jsp, "  ATA command IDENTIFY %sDEVICE response in "
+                      "hex:\n", cp);
+    } else if (do_long_nq)
+        sgj_pr_hr(jsp, "  ATA command 0x%x got following response:\n",
+                  (unsigned int)cc);
+    if (jsp->pr_as_json) {
+        sgj_convert_to_snake_name(sat_vip, d, dlen);
+        sgj_js_nv_s_len(jsp, jop, d, (const char *)(buff + 8), 8);
+        sgj_convert_to_snake_name(sat_pip, d, dlen);
+        sgj_js_nv_s_len(jsp, jop, d, (const char *)(buff + 16), 16);
+        sgj_convert_to_snake_name(sat_prlp, d, dlen);
+        sgj_js_nv_s_len(jsp, jop, d, (const char *)(buff + 32), 4);
+        sgj_js_nv_hex_bytes(jsp, jop, "ata_device_signature", buff + 36, 20);
+        sgj_js_nv_ihex(jsp, jop, "command_code", buff[56]);
+        sgj_js_nv_s(jsp, jop, "ata_identify_device_data_example",
+                    "sg_vpd -p ai -HHH /dev/sdc | hdparm --Istdin");
+    }
+    if (len < 572)
+        return;
+    if (2 == op->do_hex)
+        hex2stdout((buff + 60), 512, 0);
+    else if (do_long_nq)
+        dWordHex((const unsigned short *)(buff + 60), 256, 0, is_be);
+}
+
+/* VPD_SCSI_FEATURE_SETS  0x92  ["sfs"] */
+void
+decode_feature_sets_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                        sgj_opaque_p jap)
+{
+    int k, bump;
+    uint16_t sf_code;
+    bool found;
+    const uint8_t * bp;
+    sgj_opaque_p jo2p;
+    sgj_state * jsp = &op->json_st;
+    char b[256];
+    char d[80];
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 1 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("SCSI Feature sets VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 8;
+    bp = buff + 8;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        sf_code = sg_get_unaligned_be16(bp);
+        bump = 2;
+        if ((k + bump) > len) {
+            pr2serr("SCSI Feature sets, short descriptor length=%d, "
+                    "left=%d\n", bump, (len - k));
+            return;
+        }
+        if (2 == op->do_hex)
+            hex2stdout(bp + 8, 2, 1);
+        else if (op->do_hex > 2)
+            hex2stdout(bp, 2, 1);
+        else {
+             sg_scnpr(b, sizeof(b), "    %s",
+                      sg_get_sfs_str(sf_code, -2, sizeof(d), d, &found,
+                                     op->verbose));
+            if (op->verbose == 1)
+                sgj_pr_hr(jsp, "%s [0x%x]\n", b, (unsigned int)sf_code);
+            else if (op->verbose > 1)
+                sgj_pr_hr(jsp, "%s [0x%x] found=%s\n", b,
+                          (unsigned int)sf_code, found ? "true" : "false");
+            else
+                sgj_pr_hr(jsp, "%s\n", b);
+            sgj_js_nv_ihexstr(jsp, jo2p, "feature_set_code", sf_code, NULL,
+                              d);
+            if (jsp->verbose)
+                sgj_js_nv_b(jsp, jo2p, "meaning_is_match", found);
+        }
+        sgj_js_nv_o(jsp, jap, NULL, jo2p);
+    }
+}
+
+static const char * constituent_type_arr[] = {
+    "Reserved",
+    "Virtual tape library",
+    "Virtual tape drive",
+    "Direct access block device",
+};
+
+/* VPD_DEVICE_CONSTITUENTS   0x8b ["dc"] */
+void
+decode_dev_constit_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                       sgj_opaque_p jap, recurse_vpd_decodep fp)
+{
+    uint16_t constit_type;
+    int k, j, res, bump, csd_len;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p, jo3p, ja2p;
+    const uint8_t * bp;
+    char b[256];
+    char d[64];
+    static const int blen = sizeof(b);
+    static const int dlen = sizeof(d);
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("page length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0, j = 0; k < len; k += bump, bp += bump, ++j) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        if (j > 0)
+            sgj_pr_hr(jsp, "\n");
+        sgj_pr_hr(jsp, "  Constituent descriptor %d:\n", j + 1);
+        if ((k + 36) > len) {
+            pr2serr("short descriptor length=36, left=%d\n", (len - k));
+            sgj_js_nv_o(jsp, jap, NULL, jo2p);
+            return;
+        }
+        constit_type = sg_get_unaligned_be16(bp + 0);
+        if (constit_type >= SG_ARRAY_SIZE(constituent_type_arr))
+            sgj_pr_hr(jsp,"    Constituent type: unknown [0x%x]\n",
+                      constit_type);
+        else
+            sgj_pr_hr(jsp, "    Constituent type: %s [0x%x]\n",
+                      constituent_type_arr[constit_type], constit_type);
+        sg_scnpr(b, blen, "    Constituent device type: ");
+        if (0xff == bp[2])
+            sgj_pr_hr(jsp, "%sUnknown [0xff]\n", b);
+        else if (bp[2] >= 0x20)
+            sgj_pr_hr(jsp, "%s%s [0x%x]\n", b, rsv_s, bp[2]);
+        else
+            sgj_pr_hr(jsp, "%s%s [0x%x]\n", b,
+                   sg_get_pdt_str(PDT_MASK & bp[2], dlen, d), bp[2]);
+        snprintf(b, blen, "%.8s", bp + 4);
+        sgj_pr_hr(jsp, "    %s: %s\n", t10_vendor_id_hr, b);
+        sgj_js_nv_s(jsp, jo2p, t10_vendor_id_js, b);
+        snprintf(b, blen, "%.16s", bp + 12);
+        sgj_pr_hr(jsp, "    %s: %s\n", product_id_hr, b);
+        sgj_js_nv_s(jsp, jo2p, product_id_js, b);
+        snprintf(b, blen, "%.4s", bp + 28);
+        sgj_pr_hr(jsp, "    %s: %s\n", product_rev_lev_hr, b);
+        sgj_js_nv_s(jsp, jo2p, product_rev_lev_js, b);
+        csd_len = sg_get_unaligned_be16(bp + 34);
+        bump = 36 + csd_len;
+        if ((k + bump) > len) {
+            pr2serr("short descriptor length=%d, left=%d\n", bump, (len - k));
+            sgj_js_nv_o(jsp, jap, NULL, jo2p);
+            return;
+        }
+        if (csd_len > 0) {
+            int m, q, cs_bump;
+            uint8_t cs_type;
+            uint8_t cs_len;
+            const uint8_t * cs_bp;
+
+            sgj_pr_hr(jsp, "    Constituent specific descriptors:\n");
+            ja2p = sgj_named_subarray_r(jsp, jo2p,
+                                "constituent_specific_descriptor_list");
+            for (m = 0, q = 0, cs_bp = bp + 36; m < csd_len;
+                 m += cs_bump, ++q, cs_bp += cs_bump) {
+                jo3p = sgj_new_unattached_object_r(jsp);
+                cs_type = cs_bp[0];
+                cs_len = sg_get_unaligned_be16(cs_bp + 2);
+                cs_bump = cs_len + 4;
+                sgj_js_nv_ihex(jsp, jo3p, "constituent_specific_type",
+                               cs_type);
+                if (1 == cs_type) {     /* VPD page */
+                    int off = cs_bp + 4 - buff;
+
+                    sgj_pr_hr(jsp, "      Constituent specific VPD page "
+                              "%d:\n", q + 1);
+                    /* SPC-5 says these shall _not_ themselves be Device
+                     *  Constituent VPD pages. So no infinite recursion. */
+                    res = (*fp)(op, jo3p, off);
+                    if (res)
+                        pr2serr("%s: recurse_vpd_decode() failed, res=%d\n",
+                                __func__, res);
+                } else {
+                    if (0xff == cs_type)
+                        sgj_pr_hr(jsp, "      Vendor specific data (in "
+                                  "hex):\n");
+                    else
+                        sgj_pr_hr(jsp, "      %s [0x%x] specific data (in "
+                                  "hex):\n", rsv_s, cs_type);
+                    if (jsp->pr_as_json)
+                        sgj_js_nv_hex_bytes(jsp, jo3p,
+                                            "constituent_specific_data_hex",
+                                            cs_bp + 4, cs_len);
+                    else
+                        hex2stdout(cs_bp + 4, cs_len, 0 /* plus ASCII */);
+                }
+                sgj_js_nv_o(jsp, ja2p, NULL, jo3p);
+            }   /* end of Constituent specific descriptor loop */
+        }
+        sgj_js_nv_o(jsp, jap, NULL, jo2p);
+    }   /* end Constituent descriptor loop */
+}
+
+/* VPD_CFA_PROFILE_INFO  0x8c ["cfa"] */
+void
+decode_cga_profile_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                       sgj_opaque_p jap)
+{
+    int k;
+    uint32_t u;
+    sgj_state * jsp = &op->json_st;
+    const uint8_t * bp;
+    sgj_opaque_p jo2p;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += 4, bp += 4) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        sgj_haj_vi(jsp, jo2p, 0, "CGA profile supported",
+                   SGJ_SEP_COLON_1_SPACE, bp[0], true);
+        u = sg_get_unaligned_be16(bp + 2);
+        sgj_haj_vi_nex(jsp, jo2p, 2, "Sequential write data size",
+                       SGJ_SEP_COLON_1_SPACE, u, true, "unit: LB");
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+/* Assume index is less than 16 */
+static const char * sg_ansi_version_arr[16] =
+{
+    "no conformance claimed",
+    "SCSI-1",           /* obsolete, ANSI X3.131-1986 */
+    "SCSI-2",           /* obsolete, ANSI X3.131-1994 */
+    "SPC",              /* withdrawn, ANSI INCITS 301-1997 */
+    "SPC-2",            /* ANSI INCITS 351-2001, ISO/IEC 14776-452 */
+    "SPC-3",            /* ANSI INCITS 408-2005, ISO/IEC 14776-453 */
+    "SPC-4",            /* ANSI INCITS 513-2015 */
+    "SPC-5",            /* ANSI INCITS 502-2020 */
+    "ecma=1, [8h]",
+    "ecma=1, [9h]",
+    "ecma=1, [Ah]",
+    "ecma=1, [Bh]",
+    "reserved [Ch]",
+    "reserved [Dh]",
+    "reserved [Eh]",
+    "reserved [Fh]",
+};
+
+static const char *
+hot_pluggable_str(int hp)
+{
+    switch (hp) {
+    case 0:
+        return "No information";
+    case 1:
+        return "target device designed to be removed from SCSI domain";
+    case 2:
+        return "target device not designed to be removed from SCSI domain";
+    default:
+        return "value reserved by T10";
+    }
+}
+
+static const char *
+tpgs_str(int tpgs)
+{
+    switch (tpgs) {
+    case 1:
+        return "only implicit asymmetric logical unit access";
+    case 2:
+        return "only explicit asymmetric logical unit access";
+    case 3:
+        return "both explicit and implicit asymmetric logical unit access";
+    case 0:
+    default:
+        return ns_s;
+    }
+}
+
+sgj_opaque_p
+std_inq_decode_js(const uint8_t * b, int len, struct opts_t * op,
+                  sgj_opaque_p jop)
+{
+    int tpgs;
+    int pqual = (b[0] & 0xe0) >> 5;
+    int pdt = b[0] & PDT_MASK;
+    int hp = (b[1] >> 4) & 0x3;
+    int ver = b[2];
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    char c[256];
+    static const int clen = sizeof(c);
+
+    jo2p = sgj_named_subobject_r(jsp, jop, "standard_inquiry_data_format");
+    sgj_js_nv_ihexstr(jsp, jo2p, "peripheral_qualifier", pqual, NULL,
+                      pqual_str(pqual));
+    sgj_js_nv_ihexstr(jsp, jo2p, "peripheral_device_type", pdt, NULL,
+                      sg_get_pdt_str(pdt, clen, c));
+    sgj_js_nv_ihex_nex(jsp, jo2p, "rmb", !!(b[1] & 0x80), false,
+                       "Removable Medium Bit");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "lu_cong", !!(b[1] & 0x40), false,
+                       "Logical Unit Conglomerate");
+    sgj_js_nv_ihexstr(jsp, jo2p, "hot_pluggable", hp, NULL,
+                      hot_pluggable_str(hp));
+    snprintf(c, clen, "%s", (ver > 0xf) ? "old or reserved version code" :
+                                          sg_ansi_version_arr[ver]);
+    sgj_js_nv_ihexstr(jsp, jo2p, "version", ver, NULL, c);
+    sgj_js_nv_ihex_nex(jsp, jo2p, "aerc", !!(b[3] & 0x80), false,
+                       "Asynchronous Event Reporting Capability (obsolete "
+                       "SPC-3)");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "trmtsk", !!(b[3] & 0x40), false,
+                       "Terminate Task (obsolete SPC-2)");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "normaca", !!(b[3] & 0x20), false,
+                       "Normal ACA (Auto Contingent Allegiance)");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "hisup", !!(b[3] & 0x10), false,
+                       "Hierarchial Support");
+    sgj_js_nv_ihex(jsp, jo2p, "response_data_format", b[3] & 0xf);
+    sgj_js_nv_ihex_nex(jsp, jo2p, "sccs", !!(b[5] & 0x80), false,
+                       "SCC (SCSI Storage Commands) Supported");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "acc", !!(b[5] & 0x40), false,
+                       "Access Commands Coordinator (obsolete SPC-5)");
+    tpgs = (b[5] >> 4) & 0x3;
+    sgj_js_nv_ihexstr_nex(jsp, jo2p, "tpgs", tpgs, false, NULL,
+                          tpgs_str(tpgs), "Target Port Group Support");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "3pc", !!(b[5] & 0x8), false,
+                       "Third Party Copy");
+    sgj_js_nv_ihex(jsp, jo2p, "protect", !!(b[5] & 0x1));
+    /* Skip SPI specific flags which have been obsolete for a while) */
+    sgj_js_nv_ihex_nex(jsp, jo2p, "bque", !!(b[6] & 0x80), false,
+                       "Basic task management model (obsolete SPC-4)");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "encserv", !!(b[6] & 0x40), false,
+                       "Enclousure Services supported");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "multip", !!(b[6] & 0x10), false,
+                       "Multiple SCSI port");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "mchngr", !!(b[6] & 0x8), false,
+                       "Medium changer (obsolete SPC-4)");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "reladr", !!(b[7] & 0x80), false,
+                       "Relative Addressing (obsolete in SPC-4)");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "linked", !!(b[7] & 0x8), false,
+                       "Linked Commands (obsolete in SPC-4)");
+    sgj_js_nv_ihex_nex(jsp, jo2p, "cmdque", !!(b[7] & 0x2), false,
+                       "Command Management Model (command queuing)");
+    if (len < 16)
+        return jo2p;
+    snprintf(c, clen, "%.8s", b + 8);
+    sgj_js_nv_s(jsp, jo2p, t10_vendor_id_js, c);
+    if (len < 32)
+        return jo2p;
+    snprintf(c, clen, "%.16s", b + 16);
+    sgj_js_nv_s(jsp, jo2p, product_id_js, c);
+    if (len < 36)
+        return jo2p;
+    snprintf(c, clen, "%.4s", b + 32);
+    sgj_js_nv_s(jsp, jo2p, product_rev_lev_js, c);
+    return jo2p;
+}
+
+static const char * power_unit_arr[] =
+{
+    "Gigawatts",
+    "Megawatts",
+    "Kilowatts",
+    "Watts",
+    "Milliwatts",
+    "Microwatts",
+    "Unit reserved",
+    "Unit reserved",
+};
+
+/* VPD_POWER_CONSUMPTION  0x8d  ["psm"] */
+void
+decode_power_consumption(const uint8_t * buff, int len, struct opts_t * op,
+                         sgj_opaque_p jap)
+{
+    int k, bump, pcmp_id, pcmp_unit;
+    unsigned int pcmp_val;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    const uint8_t * bp;
+    char b[128];
+    static const int blen = sizeof(b);
+    static const char * pcmp = "power_consumption";
+    static const char * pci = "Power consumption identifier";
+    static const char * mpc = "Maximum power consumption";
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 1 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        bump = 4;
+        if ((k + bump) > len) {
+            pr2serr("short descriptor length=%d, left=%d\n", bump,
+                    (len - k));
+            return;
+        }
+        if (op->do_hex > 1)
+            hex2stdout(bp, 4, 1);
+        else {
+            jo2p = sgj_new_unattached_object_r(jsp);
+            pcmp_id = bp[0];
+            pcmp_unit = 0x7 & bp[1];
+            pcmp_val = sg_get_unaligned_be16(bp + 2);
+            if (jsp->pr_as_json) {
+                sgj_convert_to_snake_name(pci, b, blen);
+                sgj_js_nv_ihex(jsp, jo2p, b, pcmp_id);
+                snprintf(b, blen, "%s_units", pcmp);
+                sgj_js_nv_ihexstr(jsp, jo2p, b, pcmp_unit, NULL,
+                                  power_unit_arr[pcmp_unit]);
+                snprintf(b, blen, "%s_value", pcmp);
+                sgj_js_nv_ihex(jsp, jo2p, b, pcmp_val);
+            }
+            snprintf(b, blen, "  %s: 0x%x", pci, pcmp_id);
+            if (pcmp_val >= 1000 && pcmp_unit > 0)
+                sgj_pr_hr(jsp, "%s    %s: %d.%03d %s\n", b, mpc,
+                          pcmp_val / 1000, pcmp_val % 1000,
+                          power_unit_arr[pcmp_unit - 1]); /* up one unit */
+            else
+                sgj_pr_hr(jsp, "%s    %s: %u %s\n", b, mpc, pcmp_val,
+                          power_unit_arr[pcmp_unit]);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+        }
+    }
+}
+
+
+/* VPD_BLOCK_LIMITS    0xb0 ["bl"] */
+void
+decode_block_limits_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                        sgj_opaque_p jop)
+{
+    int wsnz, ugavalid;
+    uint32_t u;
+    uint64_t ull;
+    sgj_state * jsp = &op->json_st;
+    char b[144];
+    static const int blen = sizeof(b);
+    static const char * mcawl = "Maximum compare and write length";
+    static const char * otlg = "Optimal transfer length granularity";
+    static const char * cni = "command not implemented";
+    static const char * ul = "unlimited";
+    static const char * mtl = "Maximum transfer length";
+    static const char * otl = "Optimal transfer length";
+    static const char * mpl = "Maximum prefetch length";
+    static const char * mulc = "Maximum unmap LBA count";
+    static const char * mubdc = "Maximum unmap block descriptor count";
+    static const char * oug = "Optimal unmap granularity";
+    static const char * ugav = "Unmap granularity alignment valid";
+    static const char * uga = "Unmap granularity alignment";
+    static const char * mwsl = "Maximum write same length";
+    static const char * matl = "Maximum atomic transfer length";
+    static const char * aa = "Atomic alignment";
+    static const char * atlg = "Atomic transfer length granularity";
+    static const char * matlwab = "Maximum atomic transfer length with "
+                                  "atomic boundary";
+    static const char * mabs = "Maximum atomic boundary size";
+
+    if (len < 16) {
+        pr2serr("page length too short=%d\n", len);
+        return;
+    }
+    wsnz = !!(buff[4] & 0x1);
+    sgj_pr_hr(jsp, "  Write same non-zero (WSNZ): %d\n", wsnz);
+    sgj_js_nv_ihex_nex(jsp, jop, "wsnz", wsnz, false,
+                "Write Same Non-Zero (number of LBs must be > 0)");
+    u = buff[5];
+    if (0 == u) {
+        sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", mcawl, cni);
+        sgj_convert_to_snake_name(mcawl, b, blen);
+        sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, cni);
+    } else
+        sgj_haj_vi_nex(jsp, jop, 2, mcawl, SGJ_SEP_COLON_1_SPACE, u,
+                       true, "unit: LB");
+
+    u = sg_get_unaligned_be16(buff + 6);
+    if (0 == u) {
+        sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", otlg, nr_s);
+        sgj_convert_to_snake_name(otlg, b, blen);
+        sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, nr_s);
+    } else
+        sgj_haj_vi_nex(jsp, jop, 2, otlg, SGJ_SEP_COLON_1_SPACE, u,
+                       true, "unit: LB");
+
+    u = sg_get_unaligned_be32(buff + 8);
+    if (0 == u) {
+        sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", mtl, nr_s);
+        sgj_convert_to_snake_name(mtl, b, blen);
+        sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, nr_s);
+    } else
+        sgj_haj_vi_nex(jsp, jop, 2, mtl, SGJ_SEP_COLON_1_SPACE, u,
+                       true, "unit: LB");
+
+    u = sg_get_unaligned_be32(buff + 12);
+    if (0 == u) {
+        sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", otl, nr_s);
+        sgj_convert_to_snake_name(otl, b, blen);
+        sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, nr_s);
+    } else
+        sgj_haj_vi_nex(jsp, jop, 2, otl, SGJ_SEP_COLON_1_SPACE, u,
+                       true, "unit: LB");
+    if (len > 19) {     /* added in sbc3r09 */
+        u = sg_get_unaligned_be32(buff + 16);
+        if (0 == u) {
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", mpl, nr_s);
+            sgj_convert_to_snake_name(mpl, b, blen);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, nr_s);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, mpl, SGJ_SEP_COLON_1_SPACE, u,
+                           true, "unit: LB");
+    }
+    if (len > 27) {     /* added in sbc3r18 */
+        u = sg_get_unaligned_be32(buff + 20);
+        sgj_convert_to_snake_name(mulc, b, blen);
+        if (0 == u) {
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", mulc, cni);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, cni);
+        } else if (0xffffffff == u) {
+            sgj_pr_hr(jsp, "  %s: %s blocks\n", ul, mulc);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, ul);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, mulc, SGJ_SEP_COLON_1_SPACE, u,
+                           true, "unit: LB");
+
+        u = sg_get_unaligned_be32(buff + 24);
+        sgj_convert_to_snake_name(mulc, b, blen);
+        if (0 == u) {
+            sgj_pr_hr(jsp, "  %s: 0 block descriptors [%s]\n", mubdc, cni);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, cni);
+        } else if (0xffffffff == u) {
+            sgj_pr_hr(jsp, "  %s: %s block descriptors\n", ul, mubdc);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, ul);
+        } else
+            sgj_haj_vi(jsp, jop, 2, mubdc, SGJ_SEP_COLON_1_SPACE,
+                       u, true);
+    }
+    if (len > 35) {     /* added in sbc3r19 */
+        u = sg_get_unaligned_be32(buff + 28);
+        if (0 == u) {
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", oug, nr_s);
+            sgj_convert_to_snake_name(oug, b, blen);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, nr_s);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, oug, SGJ_SEP_COLON_1_SPACE, u,
+                           true, "unit: LB");
+
+        ugavalid = !!(buff[32] & 0x80);
+        sgj_pr_hr(jsp, "  %s: %s\n", ugav, ugavalid ? "true" : "false");
+        sgj_js_nv_i(jsp, jop, ugav, ugavalid);
+        if (ugavalid) {
+            u = 0x7fffffff & sg_get_unaligned_be32(buff + 32);
+            sgj_haj_vi_nex(jsp, jop, 2, uga, SGJ_SEP_COLON_1_SPACE, u,
+                           true, "unit: LB");
+        }
+    }
+    if (len > 43) {     /* added in sbc3r26 */
+        ull = sg_get_unaligned_be64(buff + 36);
+        if (0 == ull) {
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", mwsl, nr_s);
+            sgj_convert_to_snake_name(mwsl, b, blen);
+            sgj_js_nv_ihexstr(jsp, jop, b, ull, NULL, nr_s);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, mwsl, SGJ_SEP_COLON_1_SPACE,
+                           ull, true, "unit: LB");
+    }
+    if (len > 47) {     /* added in sbc4r02 */
+        u = sg_get_unaligned_be32(buff + 44);
+        if (0 == u) {
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", matl, nr_s);
+            sgj_convert_to_snake_name(matl, b, blen);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, nr_s);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, matl, SGJ_SEP_COLON_1_SPACE,
+                           u, true, "unit: LB");
+
+        u = sg_get_unaligned_be32(buff + 48);
+        if (0 == u) {
+            static const char * uawp = "unaligned atomic writes permitted";
+
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", aa, uawp);
+            sgj_convert_to_snake_name(aa, b, blen);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, uawp);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, aa, SGJ_SEP_COLON_1_SPACE,
+                           u, true, "unit: LB");
+
+        u = sg_get_unaligned_be32(buff + 52);
+        if (0 == u) {
+            static const char * ngr = "no granularity requirement";
+
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", atlg, ngr);
+            sgj_convert_to_snake_name(atlg, b, blen);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, ngr);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, aa, SGJ_SEP_COLON_1_SPACE,
+                           u, true, "unit: LB");
+    }
+    if (len > 56) {
+        u = sg_get_unaligned_be32(buff + 56);
+        if (0 == u) {
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", matlwab, nr_s);
+            sgj_convert_to_snake_name(matlwab, b, blen);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, nr_s);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, matlwab, SGJ_SEP_COLON_1_SPACE,
+                           u, true, "unit: LB");
+
+        u = sg_get_unaligned_be32(buff + 60);
+        if (0 == u) {
+            static const char * cowa1b = "can only write atomic 1 block";
+
+            sgj_pr_hr(jsp, "  %s: 0 blocks [%s]\n", mabs, cowa1b);
+            sgj_convert_to_snake_name(mabs, b, blen);
+            sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, cowa1b);
+        } else
+            sgj_haj_vi_nex(jsp, jop, 2, mabs, SGJ_SEP_COLON_1_SPACE,
+                           u, true, "unit: LB");
+    }
+}
+
+static const char * product_type_arr[] =
+{
+    "Not specified",
+    "CFast",
+    "CompactFlash",
+    "MemoryStick",
+    "MultiMediaCard",
+    "Secure Digital Card (SD)",
+    "XQD",
+    "Universal Flash Storage Card (UFS)",
+};
+
+/* ZONED field here replaced by ZONED BLOCK DEVICE EXTENSION field in the
+ * Zoned Block Device Characteristics VPD page. The new field includes
+ * Zone Domains and Realms (see ZBC-2) */
+static const char * bdc_zoned_strs[] = {
+    nr_s,
+    "host-aware",
+    "host-managed",
+    rsv_s,
+};
+
+/* VPD_BLOCK_DEV_CHARS    0xb1 ["bdc"] */
+void
+decode_block_dev_ch_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                        sgj_opaque_p jop)
+{
+    int zoned;
+    unsigned int u, k;
+    sgj_state * jsp = &op->json_st;
+    const char * cp;
+    char b[144];
+    static const char * mrr_j = "medium_rotation_rate";
+    static const char * mrr_h = "Medium rotation rate";
+    static const char * nrm = "Non-rotating medium (e.g. solid state)";
+    static const char * pt_j = "product_type";
+
+    if (len < 64) {
+        pr2serr("page length too short=%d\n", len);
+        return;
+    }
+    u = sg_get_unaligned_be16(buff + 4);
+    if (0 == u) {
+        sgj_pr_hr(jsp, "  %s is %s\n", mrr_h, nr_s);
+        sgj_js_nv_ihexstr(jsp, jop, mrr_j, 0, NULL, nr_s);
+    } else if (1 == u) {
+        sgj_pr_hr(jsp, "  %s\n", nrm);
+        sgj_js_nv_ihexstr(jsp, jop, mrr_j, 1, NULL, nrm);
+    } else if ((u < 0x401) || (0xffff == u)) {
+        sgj_pr_hr(jsp, "  %s [0x%x]\n", rsv_s, u);
+        sgj_js_nv_ihexstr(jsp, jop, mrr_j, u, NULL, rsv_s);
+    } else {
+        sgj_js_nv_ihex_nex(jsp, jop, mrr_j, u, true,
+                           "unit: rpm; nominal rotation rate");
+    }
+    u = buff[6];
+    k = SG_ARRAY_SIZE(product_type_arr);
+    if (u < k) {
+        sgj_pr_hr(jsp, "  %s: %s\n", "Product type", product_type_arr[u]);
+        sgj_js_nv_ihexstr(jsp, jop, pt_j, u, NULL, product_type_arr[u]);
+    } else {
+        sgj_pr_hr(jsp, "  %s: %s [0x%x]\n", "Product type",
+                  (u < 0xf0) ? rsv_s : vs_s, u);
+        sgj_js_nv_ihexstr(jsp, jop, pt_j, u, NULL, (u < 0xf0) ? rsv_s : vs_s);
+    }
+    sgj_haj_vi_nex(jsp, jop, 2, "WABEREQ", SGJ_SEP_EQUAL_NO_SPACE,
+                   (buff[7] >> 6) & 0x3, false,
+                   "Write After Block Erase REQuired");
+    sgj_haj_vi_nex(jsp, jop, 2, "WACEREQ", SGJ_SEP_EQUAL_NO_SPACE,
+                   (buff[7] >> 4) & 0x3, false,
+                   "Write After Cryptographic Erase REQuired");
+    u = buff[7] & 0xf;
+    switch (u) {
+    case 0:
+        strcpy(b, nr_s);
+        break;
+    case 1:
+        strcpy(b, "5.25 inch");
+        break;
+    case 2:
+        strcpy(b, "3.5 inch");
+        break;
+    case 3:
+        strcpy(b, "2.5 inch");
+        break;
+    case 4:
+        strcpy(b, "1.8 inch");
+        break;
+    case 5:
+        strcpy(b, "less then 1.8 inch");
+        break;
+    default:
+        strcpy(b, rsv_s);
+        break;
+    }
+    sgj_pr_hr(jsp, "  Nominal form factor: %s\n", b);
+    sgj_js_nv_ihexstr(jsp, jop, "nominal_forn_factor", u, NULL, b);
+    sgj_haj_vi_nex(jsp, jop, 2, "MACT", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(buff[8] & 0x40), false, "Multiple ACTuator");
+    zoned = (buff[8] >> 4) & 0x3;   /* added sbc4r04, obsolete sbc5r01 */
+    cp = bdc_zoned_strs[zoned];
+    sgj_pr_hr(jsp, "  ZONED=%d [%s]\n", zoned, cp);
+    sgj_js_nv_ihexstr_nex(jsp, jop, "zoned", zoned, false, NULL,
+                          cp, "Added in SBC-4, obsolete in SBC-5");
+    sgj_haj_vi_nex(jsp, jop, 2, "RBWZ", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(buff[8] & 0x4), false,
+                   "Background Operation Control Supported");
+    sgj_haj_vi_nex(jsp, jop, 2, "FUAB", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(buff[8] & 0x2), false,
+                   "Force Unit Access Behaviour");
+    sgj_haj_vi_nex(jsp, jop, 2, "VBULS", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(buff[8] & 0x1), false,
+                   "Verify Byte check Unmapped Lba Supported");
+    u = sg_get_unaligned_be32(buff + 12);
+    sgj_haj_vi_nex(jsp, jop, 2, "DEPOPULATION TIME", SGJ_SEP_COLON_1_SPACE,
+                   u, true, "unit: second");
+}
+
+static const char * prov_type_arr[8] = {
+    "not known or fully provisioned",
+    "resource provisioned",
+    "thin provisioned",
+    rsv_s,
+    rsv_s,
+    rsv_s,
+    rsv_s,
+    rsv_s,
+};
+
+/* VPD_LB_PROVISIONING   0xb2 ["lbpv"] */
+int
+decode_block_lb_prov_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                         sgj_opaque_p jop)
+{
+    unsigned int u, dp, pt, t_exp;
+    sgj_state * jsp = &op->json_st;
+    const char * cp;
+    char b[1024];
+    static const int blen = sizeof(b);
+    static const char * mp = "Minimum percentage";
+    static const char * tp = "Threshold percentage";
+    static const char * pgd = "Provisioning group descriptor";
+
+    if (len < 4) {
+        pr2serr("page too short=%d\n", len);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    t_exp = buff[4];
+    sgj_js_nv_ihexstr(jsp, jop, "threshold_exponent", t_exp, NULL,
+                      (0 == t_exp) ? ns_s : NULL);
+    sgj_haj_vi_nex(jsp, jop, 2, "LBPU", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(buff[5] & 0x80), false,
+                   "Logical Block Provisioning Unmap command supported");
+    sgj_haj_vi_nex(jsp, jop, 2, "LBPWS", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(buff[5] & 0x40), false, "Logical Block Provisioning "
+                   "Write Same (16) command supported");
+    sgj_haj_vi_nex(jsp, jop, 2, "LBPWS10", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(buff[5] & 0x20), false, "Logical Block Provisioning "
+                   "Write Same (10) command supported");
+    sgj_haj_vi_nex(jsp, jop, 2, "LBPRZ", SGJ_SEP_EQUAL_NO_SPACE,
+                   (0x7 & (buff[5] >> 2)), true,
+                   "Logical Block Provisioning Read Zero");
+    sgj_haj_vi_nex(jsp, jop, 2, "ANC_SUP", SGJ_SEP_EQUAL_NO_SPACE,
+                   !!(buff[5] & 0x2), false,
+                   "ANChor SUPported");
+    dp = !!(buff[5] & 0x1);
+    sgj_haj_vi_nex(jsp, jop, 2, "DP", SGJ_SEP_EQUAL_NO_SPACE,
+                   dp, false, "Descriptor Present");
+    u = 0x1f & (buff[6] >> 3);  /* minimum percentage */
+    if (0 == u)
+        sgj_pr_hr(jsp, "  %s: 0 [%s]\n", mp, nr_s);
+    else
+        sgj_pr_hr(jsp, "  %s: %u\n", mp, u);
+    sgj_convert_to_snake_name(mp, b, blen);
+    sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, (0 == u) ? nr_s : NULL);
+    pt = buff[6] & 0x7;
+    cp = prov_type_arr[pt];
+    if (pt > 2)
+        snprintf(b, blen, " [%u]", u);
+    else
+        b[0] = '\0';
+    sgj_pr_hr(jsp, "  Provisioning type: %s%s\n", cp, b);
+    sgj_js_nv_ihexstr(jsp, jop, "provisioning_type", pt, NULL, cp);
+    u = buff[7];        /* threshold percentage */
+    strcpy(b, tp);
+    if (0 == u)
+        sgj_pr_hr(jsp, "  %s: 0 [percentages %s]\n", b, ns_s);
+    else
+        sgj_pr_hr(jsp, "  %s: %u", b, u);
+    sgj_convert_to_snake_name(tp, b, blen);
+    sgj_js_nv_ihexstr(jsp, jop, b, u, NULL, (0 == u) ? ns_s : NULL);
+    if (dp && (len > 11)) {
+        int i_len;
+        const uint8_t * bp;
+        sgj_opaque_p jo2p;
+
+        bp = buff + 8;
+        i_len = bp[3];
+        if (0 == i_len) {
+            pr2serr("%s too short=%d\n", pgd, i_len);
+            return 0;
+        }
+        if (jsp->pr_as_json) {
+            jo2p = sgj_snake_named_subobject_r(jsp, jop, pgd);
+            sgj_js_designation_descriptor(jsp, jo2p, bp, i_len + 4);
+        }
+        sgj_pr_hr(jsp, "  %s:\n", pgd);
+        sg_get_designation_descriptor_str("    ", bp, i_len + 4, true,
+                                          op->do_long, blen, b);
+        if (jsp->pr_as_json && jsp->pr_out_hr)
+            sgj_js_str_out(jsp, b, strlen(b));
+        else
+            sgj_pr_hr(jsp, "%s", b);
+    }
+    return 0;
+}
+
+/* VPD_REFERRALS   0xb3 ["ref"] */
+void
+decode_referrals_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                     sgj_opaque_p jop)
+{
+    uint32_t u;
+    sgj_state * jsp = &op->json_st;
+    char b[64];
+
+    if (len < 16) {
+        pr2serr("Referrals VPD page length too short=%d\n", len);
+        return;
+    }
+    u = sg_get_unaligned_be32(buff + 8);
+    strcpy(b, "  User data segment size: ");
+    if (0 == u)
+        sgj_pr_hr(jsp, "%s0 [per sense descriptor]\n", b);
+    else
+        sgj_pr_hr(jsp, "%s%u\n", b, u);
+    sgj_js_nv_ihex(jsp, jop, "user_data_segment_size", u);
+    u = sg_get_unaligned_be32(buff + 12);
+    sgj_haj_vi(jsp, jop, 2, "User data segment multiplier",
+               SGJ_SEP_COLON_1_SPACE, u, true);
+}
+
+/* VPD_SUP_BLOCK_LENS  0xb4 ["sbl"] (added sbc4r01) */
+void
+decode_sup_block_lens_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                          sgj_opaque_p jap)
+{
+    int k;
+    unsigned int u;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+
+    if (len < 4) {
+        pr2serr("page length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += 8, bp += 8) {
+        if (jsp->pr_as_json)
+            jo2p = sgj_new_unattached_object_r(jsp);
+        u = sg_get_unaligned_be32(bp);
+        sgj_haj_vi(jsp, jo2p, 2, "Logical block length",
+                   SGJ_SEP_COLON_1_SPACE, u, true);
+        sgj_haj_vi_nex(jsp, jo2p, 4, "P_I_I_SUP",
+                       SGJ_SEP_COLON_1_SPACE, !!(bp[4] & 0x40), false,
+                       "Protection Information Interval SUPported");
+        sgj_haj_vi_nex(jsp, jo2p, 4, "NO_PI_CHK",
+                       SGJ_SEP_COLON_1_SPACE, !!(bp[4] & 0x8), false,
+                       "NO Protection Information CHecKing");
+        sgj_haj_vi_nex(jsp, jo2p, 4, "GRD_CHK", SGJ_SEP_COLON_1_SPACE,
+                       !!(bp[4] & 0x4), false, "GuaRD CHecK");
+        sgj_haj_vi_nex(jsp, jo2p, 4, "APP_CHK", SGJ_SEP_COLON_1_SPACE,
+                       !!(bp[4] & 0x2), false, "APPlication tag CHecK");
+        sgj_haj_vi_nex(jsp, jo2p, 4, "REF_CHK", SGJ_SEP_COLON_1_SPACE,
+                       !!(bp[4] & 0x1), false, "REFerence tag CHecK");
+        sgj_haj_vi_nex(jsp, jo2p, 4, "T3PS", SGJ_SEP_COLON_1_SPACE,
+                       !!(bp[5] & 0x8), false, "Type 3 Protection Supported");
+        sgj_haj_vi_nex(jsp, jo2p, 4, "T2PS", SGJ_SEP_COLON_1_SPACE,
+                       !!(bp[5] & 0x4), false, "Type 2 Protection Supported");
+        sgj_haj_vi_nex(jsp, jo2p, 4, "T1PS", SGJ_SEP_COLON_1_SPACE,
+                       !!(bp[5] & 0x2), false, "Type 1 Protection Supported");
+        sgj_haj_vi_nex(jsp, jo2p, 4, "T0PS", SGJ_SEP_COLON_1_SPACE,
+                       !!(bp[5] & 0x1), false, "Type 0 Protection Supported");
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+/* VPD_BLOCK_DEV_C_EXTENS  0xb5 ["bdce"] (added sbc4r02) */
+void
+decode_block_dev_char_ext_vpd(const uint8_t * buff, int len,
+                              struct opts_t * op, sgj_opaque_p jop)
+{
+    bool b_active = false;
+    bool combined = false;
+    int n;
+    uint32_t u;
+    sgj_state * jsp = &op->json_st;
+    const char * utp = null_s;
+    const char * uup = null_s;
+    const char * uip = null_s;
+    char b[128];
+    static const int blen = sizeof(b);
+
+    if (len < 16) {
+        pr2serr("page length too short=%d\n", len);
+        return;
+    }
+    switch (buff[5]) {
+    case 1:
+        utp = "Combined writes and reads";
+        combined = true;
+        break;
+    case 2:
+        utp = "Writes only";
+        break;
+    case 3:
+        utp = "Separate writes and reads";
+        b_active = true;
+        break;
+    default:
+        utp = rsv_s;
+        break;
+    }
+    sgj_haj_vistr(jsp, jop, 2, "Utilization type", SGJ_SEP_COLON_1_SPACE,
+                  buff[5], true, utp);
+    switch (buff[6]) {
+    case 2:
+        uup = "megabytes";
+        break;
+    case 3:
+        uup = "gigabytes";
+        break;
+    case 4:
+        uup = "terabytes";
+        break;
+    case 5:
+        uup = "petabytes";
+        break;
+    case 6:
+        uup = "exabytes";
+        break;
+    default:
+        uup = rsv_s;
+        break;
+    }
+    sgj_haj_vistr(jsp, jop, 2, "Utilization units", SGJ_SEP_COLON_1_SPACE,
+                  buff[6], true, uup);
+    switch (buff[7]) {
+    case 0xa:
+        uip = "per day";
+        break;
+    case 0xe:
+        uip = "per year";
+        break;
+    default:
+        uip = rsv_s;
+        break;
+    }
+    sgj_haj_vistr(jsp, jop, 2, "Utilization interval", SGJ_SEP_COLON_1_SPACE,
+                  buff[7], true, uip);
+    u = sg_get_unaligned_be32(buff + 8);
+    sgj_haj_vistr(jsp, jop, 2, "Utilization B", SGJ_SEP_COLON_1_SPACE,
+                  u, true, (b_active ? NULL : rsv_s));
+    n = sg_scnpr(b, blen, "%s: ", "Designed utilization");
+    if (b_active)
+        n += sg_scnpr(b + n, blen - n, "%u %s for reads and ", u, uup);
+    u = sg_get_unaligned_be32(buff + 12);
+    sgj_haj_vi(jsp, jop, 2, "Utilization A", SGJ_SEP_COLON_1_SPACE, u, true);
+    n += sg_scnpr(b + n, blen - n, "%u %s for %swrites, %s", u, uup,
+                  combined ? "reads and " : null_s, uip);
+    sgj_pr_hr(jsp, "  %s\n", b);
+    if (jsp->pr_string)
+        sgj_js_nv_s(jsp, jop, "summary", b);
+}
+
+/* VPD_ZBC_DEV_CHARS 0xb6  ["zdbch"]  sbc or zbc [zbc2r04] */
+void
+decode_zbdch_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                 sgj_opaque_p jop)
+{
+    uint32_t u, pdt;
+    sgj_state * jsp = &op->json_st;
+    char b[128];
+    static const int blen = sizeof(b);
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (len < 64) {
+        pr2serr("Zoned block device characteristics VPD page length too "
+                "short=%d\n", len);
+        return;
+    }
+    pdt = PDT_MASK & buff[0];
+    sgj_pr_hr(jsp, "  Peripheral device type: %s\n",
+              sg_get_pdt_str(pdt, blen, b));
+
+    sgj_pr_hr(jsp, "  Zoned block device extension: ");
+    u = (buff[4] >> 4) & 0xf;
+    switch (u) {
+    case 0:
+        if (PDT_ZBC == (PDT_MASK & buff[0]))
+            strcpy(b, "host managed zoned block device");
+        else
+            strcpy(b, nr_s);
+        break;
+    case 1:
+        strcpy(b, "host aware zoned block device model");
+        break;
+    case 2:
+        strcpy(b, "Domains and realms zoned block device model");
+        break;
+    default:
+        strcpy(b, rsv_s);
+        break;
+    }
+    sgj_haj_vistr(jsp, jop, 2, "Zoned block device extension",
+                  SGJ_SEP_COLON_1_SPACE, u, true, b);
+    sgj_haj_vi_nex(jsp, jop, 2, "AAORB", SGJ_SEP_COLON_1_SPACE,
+                   !!(buff[4] & 0x2), false,
+                   "Activation Aligned On Realm Boundaries");
+    sgj_haj_vi_nex(jsp, jop, 2, "URSWRZ", SGJ_SEP_COLON_1_SPACE,
+                   !!(buff[4] & 0x1), false,
+                   "Unrestricted Read in Sequential Write Required Zone");
+    u = sg_get_unaligned_be32(buff + 8);
+    sgj_haj_vistr(jsp, jop, 2, "Optimal number of open sequential write "
+                  "preferred zones", SGJ_SEP_COLON_1_SPACE, u, true,
+                  (SG_LIB_UNBOUNDED_32BIT == u) ? nr_s : NULL);
+    u = sg_get_unaligned_be32(buff + 12);
+    sgj_haj_vistr(jsp, jop, 2, "Optimal number of non-sequentially "
+                  "written sequential write preferred zones",
+                  SGJ_SEP_COLON_1_SPACE, u, true,
+                  (SG_LIB_UNBOUNDED_32BIT == u) ? nr_s : NULL);
+    u = sg_get_unaligned_be32(buff + 16);
+    sgj_haj_vistr(jsp, jop, 2, "Maximum number of open sequential write "
+                  "required zones", SGJ_SEP_COLON_1_SPACE, u, true,
+                  (SG_LIB_UNBOUNDED_32BIT == u) ? nl_s : NULL);
+    u = buff[23] & 0xf;
+    switch (u) {
+    case 0:
+        strcpy(b, nr_s);
+        break;
+    case 1:
+        strcpy(b, "Zoned starting LBAs aligned using constant zone lengths");
+        break;
+    case 0x8:
+        strcpy(b, "Zoned starting LBAs potentially non-constant (as "
+                 "reported by REPORT ZONES)");
+        break;
+    default:
+        strcpy(b, rsv_s);
+        break;
+    }
+    sgj_haj_vistr(jsp, jop, 2, "Zoned alignment method",
+                  SGJ_SEP_COLON_1_SPACE, u, true, b);
+    sgj_haj_vi(jsp, jop, 2, "Zone starting LBA granularity",
+               SGJ_SEP_COLON_1_SPACE, sg_get_unaligned_be64(buff + 24), true);
+}
+
+/* VPD_BLOCK_LIMITS_EXT  0xb7 ["ble"] SBC */
+void
+decode_block_limits_ext_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                            sgj_opaque_p jop)
+{
+    uint32_t u;
+    sgj_state * jsp = &op->json_st;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (len < 12) {
+        pr2serr("page length too short=%d\n", len);
+        return;
+    }
+    u = sg_get_unaligned_be16(buff + 6);
+    sgj_haj_vistr(jsp, jop, 2, "Maximum number of streams",
+                  SGJ_SEP_COLON_1_SPACE, u, true,
+                  (0 == u) ? "Stream control not supported" : NULL);
+    u = sg_get_unaligned_be16(buff + 8);
+    sgj_haj_vi_nex(jsp, jop, 2, "Optimal stream write size",
+                   SGJ_SEP_COLON_1_SPACE, u, true, "unit: LB");
+    u = sg_get_unaligned_be32(buff + 10);
+    sgj_haj_vi_nex(jsp, jop, 2, "Stream granularity size",
+                   SGJ_SEP_COLON_1_SPACE, u, true,
+                   "unit: number of optimal stream write size blocks");
+    if (len < 28)
+        return;
+    u = sg_get_unaligned_be32(buff + 16);
+    sgj_haj_vistr_nex(jsp, jop, 2, "Maximum scattered LBA range transfer "
+                      "length", SGJ_SEP_COLON_1_SPACE, u, true,
+                      (0 == u ? nlr_s : NULL),
+                      "unit: LB (in a single LBA range descriptor)");
+    u = sg_get_unaligned_be16(buff + 22);
+    sgj_haj_vistr(jsp, jop, 2, "Maximum scattered LBA range descriptor "
+                  "count", SGJ_SEP_COLON_1_SPACE, u, true,
+                  (0 == u ? nlr_s : NULL));
+    u = sg_get_unaligned_be32(buff + 24);
+    sgj_haj_vistr_nex(jsp, jop, 2, "Maximum scattered transfer length",
+                      SGJ_SEP_COLON_1_SPACE, u, true,
+                      (0 == u ? nlr_s : NULL),
+                      "unit: LB (per single Write Scattered command)");
+}
+
+static const char * sch_type_arr[8] = {
+    rsv_s,
+    "non-zoned",
+    "host aware zoned",
+    "host managed zoned",
+    "zone domain and realms zoned",
+    rsv_s,
+    rsv_s,
+    rsv_s,
+};
+
+static char *
+get_zone_align_method(uint8_t val, char * b, int blen)
+{
+   assert(blen > 32);
+   switch (val) {
+    case 0:
+        strcpy(b, nr_s);
+        break;
+    case 1:
+        strcpy(b, "using constant zone lengths");
+        break;
+    case 8:
+        strcpy(b, "taking gap zones into account");
+        break;
+    default:
+        strcpy(b, rsv_s);
+        break;
+    }
+    return b;
+}
+
+/* VPD_FORMAT_PRESETS  0xb8 ["fp"] (added sbc4r18) */
+void
+decode_format_presets_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                          sgj_opaque_p jap)
+{
+    uint8_t sch_type;
+    int k;
+    uint32_t u;
+    uint64_t ul;
+    sgj_state * jsp = &op->json_st;
+    const uint8_t * bp;
+    sgj_opaque_p jo2p, jo3p;
+    const char * cp;
+    char b[128];
+    char d[64];
+    static const int blen = sizeof(b);
+    static const int dlen = sizeof(d);
+    static const char * llczp = "Low LBA conventional zones percentage";
+    static const char * hlczp = "High LBA conventional zones percentage";
+    static const char * ztzd = "Zone type for zone domain";
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += 64, bp += 64) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        sgj_haj_vi(jsp, jo2p, 2, "Preset identifier", SGJ_SEP_COLON_1_SPACE,
+                   sg_get_unaligned_be64(bp + 0), true);
+        sch_type = bp[4];
+        if (sch_type < 8) {
+            cp = sch_type_arr[sch_type];
+            if (rsv_s != cp)
+                snprintf(b, blen, "%s block device", cp);
+            else
+                snprintf(b, blen, "%s", cp);
+        } else
+            strcpy(b, rsv_s);
+        sgj_haj_vistr(jsp, jo2p, 4, "Schema type", SGJ_SEP_COLON_1_SPACE,
+                      sch_type, true, b);
+        sgj_haj_vi(jsp, jo2p, 4, "Logical blocks per physical block "
+                   "exponent", SGJ_SEP_COLON_1_SPACE,
+                   0xf & bp[7], true);
+        sgj_haj_vi_nex(jsp, jo2p, 4, "Logical block length",
+                       SGJ_SEP_COLON_1_SPACE, sg_get_unaligned_be32(bp + 8),
+                       true, "unit: byte");
+        sgj_haj_vi(jsp, jo2p, 4, "Designed last Logical Block Address",
+                   SGJ_SEP_COLON_1_SPACE,
+                   sg_get_unaligned_be64(bp + 16), true);
+        sgj_haj_vi_nex(jsp, jo2p, 4, "FMTPINFO", SGJ_SEP_COLON_1_SPACE,
+                       (bp[38] >> 6) & 0x3, false,
+                       "ForMaT Protection INFOrmation (see Format Unit)");
+        sgj_haj_vi(jsp, jo2p, 4, "Protection field usage",
+                   SGJ_SEP_COLON_1_SPACE, bp[38] & 0x7, false);
+        sgj_haj_vi(jsp, jo2p, 4, "Protection interval exponent",
+                   SGJ_SEP_COLON_1_SPACE, bp[39] & 0xf, true);
+        jo3p = sgj_named_subobject_r(jsp, jo2p,
+                                     "schema_type_specific_information");
+        switch (sch_type) {
+        case 2:
+            sgj_pr_hr(jsp, "    Defines zones for host aware device:\n");
+            u = bp[40 + 0];
+            sgj_pr_hr(jsp, "      %s: %u.%u %%\n", llczp, u / 10, u % 10);
+            sgj_convert_to_snake_name(llczp, b, blen);
+            sgj_js_nv_ihex_nex(jsp, jo3p, b, u, true, "unit: 1/10 of a "
+                               "percent");
+            u = bp[40 + 1];
+            sgj_pr_hr(jsp, "      %s: %u.%u %%\n", hlczp, u / 10, u % 10);
+            sgj_convert_to_snake_name(hlczp, b, blen);
+            sgj_js_nv_ihex_nex(jsp, jo3p, b, u, true, "unit: 1/10 of a "
+                               "percent");
+            u = sg_get_unaligned_be32(bp + 40 + 12);
+            sgj_haj_vistr(jsp, jo3p, 6, "Logical blocks per zone",
+                          SGJ_SEP_COLON_1_SPACE, u, true,
+                          (0 == u ? rsv_s : NULL));
+            break;
+        case 3:
+            sgj_pr_hr(jsp, "    Defines zones for host managed device:\n");
+            u = bp[40 + 0];
+            sgj_pr_hr(jsp, "      %s: %u.%u %%\n", llczp, u / 10, u % 10);
+            sgj_convert_to_snake_name(llczp, b, blen);
+            sgj_js_nv_ihex_nex(jsp, jo3p, b, u, true, "unit: 1/10 of a "
+                               "percent");
+            u = bp[40 + 1];
+            sgj_pr_hr(jsp, "      %s: %u.%u %%\n", hlczp, u / 10, u % 10);
+            sgj_convert_to_snake_name(hlczp, b, blen);
+            sgj_js_nv_ihex_nex(jsp, jo3p, b, u, true, "unit: 1/10 of a "
+                               "percent");
+            u = bp[40 + 3] & 0x7;
+            sgj_haj_vistr(jsp, jo3p, 6, "Designed zone alignment method",
+                           SGJ_SEP_COLON_1_SPACE, u, true,
+                           get_zone_align_method(u, d, dlen));
+            ul = sg_get_unaligned_be64(bp + 40 + 4);
+            sgj_haj_vi_nex(jsp, jo3p, 6, "Designed zone starting LBA "
+                           "granularity", SGJ_SEP_COLON_1_SPACE, ul, true,
+                           "unit: LB");
+            u = sg_get_unaligned_be32(bp + 40 + 12);
+            sgj_haj_vistr(jsp, jo3p, 6, "Logical blocks per zone",
+                          SGJ_SEP_COLON_1_SPACE, u, true,
+                          (0 == u ? rsv_s : NULL));
+            break;
+        case 4:
+            sgj_pr_hr(jsp, "    Defines zones for zone domains and realms "
+                      "device:\n");
+            snprintf(b, blen, "%s 0", ztzd);
+            u = bp[40 + 0];
+            sg_get_zone_type_str((u >> 4) & 0xf, dlen, d);
+            sgj_haj_vistr(jsp, jo3p, 6, b, SGJ_SEP_COLON_1_SPACE, u, true, d);
+            snprintf(b, blen, "%s 1", ztzd);
+            sg_get_zone_type_str(u & 0xf, dlen, d);
+            sgj_haj_vistr(jsp, jo3p, 6, b, SGJ_SEP_COLON_1_SPACE, u, true, d);
+
+            snprintf(b, blen, "%s 2", ztzd);
+            u = bp[40 + 1];
+            sg_get_zone_type_str((u >> 4) & 0xf, dlen, d);
+            sgj_haj_vistr(jsp, jo3p, 6, b, SGJ_SEP_COLON_1_SPACE, u, true, d);
+            snprintf(b, blen, "%s 3", ztzd);
+            sg_get_zone_type_str(u & 0xf, dlen, d);
+            sgj_haj_vistr(jsp, jo3p, 6, b, SGJ_SEP_COLON_1_SPACE, u, true, d);
+            u = bp[40 + 3] & 0x7;
+            sgj_haj_vistr(jsp, jo3p, 6, "Designed zone alignment method",
+                          SGJ_SEP_COLON_1_SPACE, u, true,
+                          get_zone_align_method(u, d, dlen));
+            ul = sg_get_unaligned_be64(bp + 40 + 4);
+            sgj_haj_vi_nex(jsp, jo3p, 6, "Designed zone starting LBA "
+                           "granularity", SGJ_SEP_COLON_1_SPACE, ul, true,
+                           "unit: LB");
+            u = sg_get_unaligned_be32(bp + 40 + 12);
+            sgj_haj_vistr(jsp, jo3p, 6, "Logical blocks per zone",
+                          SGJ_SEP_COLON_1_SPACE, u, true,
+                          (0 == u ? rsv_s : NULL));
+            ul = sg_get_unaligned_be64(bp + 40 + 16);
+            sgj_haj_vi_nex(jsp, jo3p, 6, "Designed zone maximum address",
+                           SGJ_SEP_COLON_1_SPACE, ul, true, "unit: LBA");
+            break;
+        default:
+            sgj_pr_hr(jsp, "    No schema type specific information\n");
+            break;
+        }
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+/* VPD_CON_POS_RANGE  0xb9 (added sbc5r01) */
+void
+decode_con_pos_range_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                         sgj_opaque_p jap)
+{
+    int k;
+    uint32_t u;
+    sgj_state * jsp = &op->json_st;
+    const uint8_t * bp;
+    sgj_opaque_p jo2p;
+
+    if (op->do_hex) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (len < 64) {
+        pr2serr("VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 64;
+    bp = buff + 64;
+    for (k = 0; k < len; k += 32, bp += 32) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        sgj_haj_vi(jsp, jo2p, 2, "LBA range number",
+                   SGJ_SEP_COLON_1_SPACE, bp[0], true);
+        u = bp[1];
+        sgj_haj_vistr(jsp, jo2p, 4, "Number of storage elements",
+                      SGJ_SEP_COLON_1_SPACE, u, true, (0 == u ? nr_s : NULL));
+        sgj_haj_vi(jsp, jo2p, 4, "Starting LBA", SGJ_SEP_COLON_1_SPACE,
+                   sg_get_unaligned_be64(bp + 8), true);
+        sgj_haj_vi(jsp, jo2p, 4, "Number of LBAs", SGJ_SEP_COLON_1_SPACE,
+                   sg_get_unaligned_be64(bp + 16), true);
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+/* This is xcopy(LID4) related: "ROD" == Representation Of Data
+ * Used by VPD_3PARTY_COPY   0x8f ["tpc"] */
+static void
+decode_rod_descriptor(const uint8_t * buff, int len, struct opts_t * op,
+                      sgj_opaque_p jap)
+{
+    uint8_t pdt;
+    uint32_t u;
+    int k, bump;
+    uint64_t ull;
+    const uint8_t * bp = buff;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p;
+    char b[80];
+    static const int blen = sizeof(b);
+    static const char * ab_pdt = "abnormal use of 'pdt'";
+
+    for (k = 0; k < len; k += bump, bp += bump) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        bump = sg_get_unaligned_be16(bp + 2) + 4;
+        pdt = 0x1f & bp[0];
+        u = (bp[0] >> 5) & 0x7;
+        sgj_js_nv_i(jsp, jo2p, "descriptor_format", u);
+        if (0 != u) {
+            sgj_pr_hr(jsp, "  Unhandled descriptor (format %u, device type "
+                      "%u)\n", u, pdt);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            break;
+        }
+        switch (pdt) {
+        case 0:
+            /* Block ROD device type specific descriptor */
+            sgj_js_nv_ihexstr_nex(jsp, jo2p, "peripheral_device_type",
+                                  pdt, false, NULL, "Block ROD device "
+                                  "type specific descriptor", ab_pdt);
+            sgj_haj_vi_nex(jsp, jo2p, 4, "Optimal block ROD length "
+                           "granularity", SGJ_SEP_COLON_1_SPACE,
+                           sg_get_unaligned_be16(bp + 6), true, "unit: LB");
+            ull = sg_get_unaligned_be64(bp + 8);
+            sgj_haj_vi(jsp, jo2p, 4, "Maximum bytes in block ROD",
+                       SGJ_SEP_COLON_1_SPACE, ull, true);
+            ull = sg_get_unaligned_be64(bp + 16);
+            sgj_haj_vistr(jsp, jo2p, 4, "Optimal Bytes in block ROD "
+                          "transfer", SGJ_SEP_COLON_1_SPACE, ull, true,
+                          (SG_LIB_UNBOUNDED_64BIT == ull) ? nl_s : NULL);
+            ull = sg_get_unaligned_be64(bp + 24);
+            sgj_haj_vistr(jsp, jo2p, 4, "Optimal Bytes to token per "
+                          "segment", SGJ_SEP_COLON_1_SPACE, ull, true,
+                          (SG_LIB_UNBOUNDED_64BIT == ull) ? nl_s : NULL);
+            ull = sg_get_unaligned_be64(bp + 32);
+            sgj_haj_vistr(jsp, jo2p, 4, "Optimal Bytes from token per "
+                          "segment", SGJ_SEP_COLON_1_SPACE, ull, true,
+                          (SG_LIB_UNBOUNDED_64BIT == ull) ? nl_s : NULL);
+            break;
+        case 1:
+            /* Stream ROD device type specific descriptor */
+            sgj_js_nv_ihexstr_nex(jsp, jo2p, "peripheral_device_type",
+                                  pdt, false, NULL, "Stream ROD device "
+                                  "type specific descriptor", ab_pdt);
+            ull = sg_get_unaligned_be64(bp + 8);
+            sgj_haj_vi(jsp, jo2p, 4, "Maximum bytes in stream ROD",
+                       SGJ_SEP_COLON_1_SPACE, ull, true);
+            ull = sg_get_unaligned_be64(bp + 16);
+            snprintf(b, blen, "  Optimal Bytes in stream ROD transfer: ");
+            if (SG_LIB_UNBOUNDED_64BIT == ull)
+                sgj_pr_hr(jsp, "%s-1 [no limit]\n", b);
+            else
+                sgj_pr_hr(jsp, "%s%" PRIu64 "\n", b, ull);
+            break;
+        case 3:
+            /* Copy manager ROD device type specific descriptor */
+            sgj_js_nv_ihexstr_nex(jsp, jo2p, "peripheral_device_type",
+                                  pdt, false, NULL, "Copy manager ROD "
+                                  "device type specific descriptor",
+                                  ab_pdt);
+            sgj_pr_hr(jsp, "  Maximum Bytes in processor ROD: %" PRIu64 "\n",
+                      sg_get_unaligned_be64(bp + 8));
+            ull = sg_get_unaligned_be64(bp + 16);
+            snprintf(b, blen, "  Optimal Bytes in processor ROD transfer: ");
+            if (SG_LIB_UNBOUNDED_64BIT == ull)
+                sgj_pr_hr(jsp, "%s-1 [no limit]\n", b);
+            else
+                sgj_pr_hr(jsp, "%s%" PRIu64 "\n", b, ull);
+            break;
+        default:
+            sgj_js_nv_ihexstr(jsp, jo2p, "peripheral_device_type",
+                              pdt, NULL, "unknown");
+            break;
+        }
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+struct tpc_desc_type {
+    uint8_t code;
+    const char * name;
+};
+
+static struct tpc_desc_type tpc_desc_arr[] = {
+    {0x0, "block -> stream"},
+    {0x1, "stream -> block"},
+    {0x2, "block -> block"},
+    {0x3, "stream -> stream"},
+    {0x4, "inline -> stream"},
+    {0x5, "embedded -> stream"},
+    {0x6, "stream -> discard"},
+    {0x7, "verify CSCD"},
+    {0x8, "block<o> -> stream"},
+    {0x9, "stream -> block<o>"},
+    {0xa, "block<o> -> block<o>"},
+    {0xb, "block -> stream & application_client"},
+    {0xc, "stream -> block & application_client"},
+    {0xd, "block -> block & application_client"},
+    {0xe, "stream -> stream&application_client"},
+    {0xf, "stream -> discard&application_client"},
+    {0x10, "filemark -> tape"},
+    {0x11, "space -> tape"},            /* obsolete: spc5r02 */
+    {0x12, "locate -> tape"},           /* obsolete: spc5r02 */
+    {0x13, "<i>tape -> <i>tape"},
+    {0x14, "register persistent reservation key"},
+    {0x15, "third party persistent reservation source I_T nexus"},
+    {0x16, "<i>block -> <i>block"},
+    {0x17, "positioning -> tape"},      /* this and next added spc5r02 */
+    {0x18, "<loi>tape -> <loi>tape"},   /* loi: logical object identifier */
+    {0xbe, "ROD <- block range(n)"},
+    {0xbf, "ROD <- block range(1)"},
+    {0xe0, "CSCD: FC N_Port_Name"},
+    {0xe1, "CSCD: FC N_Port_ID"},
+    {0xe2, "CSCD: FC N_Port_ID with N_Port_Name, checking"},
+    {0xe3, "CSCD: Parallel interface: I_T"},
+    {0xe4, "CSCD: Identification Descriptor"},
+    {0xe5, "CSCD: IPv4"},
+    {0xe6, "CSCD: Alias"},
+    {0xe7, "CSCD: RDMA"},
+    {0xe8, "CSCD: IEEE 1394 EUI-64"},
+    {0xe9, "CSCD: SAS SSP"},
+    {0xea, "CSCD: IPv6"},
+    {0xeb, "CSCD: IP copy service"},
+    {0xfe, "CSCD: ROD"},
+    {0xff, "CSCD: extension"},
+    {0x0, NULL},
+};
+
+static const char *
+get_tpc_desc_name(uint8_t code)
+{
+    const struct tpc_desc_type * dtp;
+
+    for (dtp = tpc_desc_arr; dtp->name; ++dtp) {
+        if (code == dtp->code)
+            return dtp->name;
+    }
+    return "";
+}
+
+struct tpc_rod_type {
+    uint32_t type;
+    const char * name;
+};
+
+static struct tpc_rod_type tpc_rod_arr[] = {
+    {0x0, "copy manager internal"},
+    {0x10000, "access upon reference"},
+    {0x800000, "point in time copy - default"},
+    {0x800001, "point in time copy - change vulnerable"},
+    {0x800002, "point in time copy - persistent"},
+    {0x80ffff, "point in time copy - any"},
+    {0xffff0001, "block device zero"},
+    {0x0, NULL},
+};
+
+static const char *
+get_tpc_rod_name(uint32_t rod_type)
+{
+    const struct tpc_rod_type * rtp;
+
+    for (rtp = tpc_rod_arr; rtp->name; ++rtp) {
+        if (rod_type == rtp->type)
+            return rtp->name;
+    }
+    return "";
+}
+
+struct cscd_desc_id_t {
+    uint16_t id;
+    const char * name;
+};
+
+static struct cscd_desc_id_t cscd_desc_id_arr[] = {
+    /* only values higher than 0x7ff are listed */
+    {0xc000, "copy src or dst null LU, pdt=0"},
+    {0xc001, "copy src or dst null LU, pdt=1"},
+    {0xf800, "copy src or dst in ROD token"},
+    {0xffff, "copy src or dst is copy manager LU"},
+    {0x0, NULL},
+};
+
+static const char *
+get_cscd_desc_id_name(uint16_t cscd_desc_id)
+{
+    const struct cscd_desc_id_t * cdip;
+
+    for (cdip = cscd_desc_id_arr; cdip->name; ++cdip) {
+        if (cscd_desc_id == cdip->id)
+            return cdip->name;
+    }
+    return "";
+}
+
+static const char *
+get_tpc_desc_type_s(uint32_t desc_type)
+{
+    switch(desc_type) {
+    case 0:
+        return "Block Device ROD Limits";
+    case 1:
+        return "Supported Commands";
+    case 4:
+        return "Parameter Data";
+    case 8:
+        return "Supported Descriptors";
+    case 0xc:
+        return "Supported CSCD Descriptor IDs";
+    case 0xd:
+        return "Copy Group Identifier";
+    case 0x106:
+        return "ROD Token Features";
+    case 0x108:
+        return "Supported ROD Token and ROD Types";
+    case 0x8001:
+        return "General Copy Operations";
+    case 0x9101:
+        return "Stream Copy Operations";
+    case 0xC001:
+        return "Held Data";
+    default:
+        if ((desc_type >= 0xE000) && (desc_type <= 0xEFFF))
+            return "Restricted";
+        else
+            return "Reserved";
+    }
+}
+
+/* VPD_3PARTY_COPY   3PC, third party copy  0x8f ["tpc"] */
+void
+decode_3party_copy_vpd(const uint8_t * buff, int len,
+                       struct opts_t * op, sgj_opaque_p jap)
+{
+    int j, k, m, bump, desc_type, desc_len, sa_len, pdt;
+    uint32_t u, v;
+    uint64_t ull;
+    const uint8_t * bp;
+    const char * cp;
+    const char * dtp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    sgj_opaque_p ja2p = NULL;
+    sgj_opaque_p jo3p = NULL;
+    char b[144];
+    static const int blen = sizeof(b);
+
+    if (len < 4) {
+        pr2serr("VPD page length too short=%d\n", len);
+        return;
+    }
+    if (3 == op->do_hex) {
+        hex2stdout(buff, len, -1);
+        return;
+    }
+    pdt = buff[0] & PDT_MASK;
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        desc_type = sg_get_unaligned_be16(bp);
+        desc_len = sg_get_unaligned_be16(bp + 2);
+        if (op->verbose)
+            sgj_pr_hr(jsp, "Descriptor type=%d [0x%x] , len %d\n", desc_type,
+                      desc_type, desc_len);
+        bump = 4 + desc_len;
+        if ((k + bump) > len) {
+            pr2serr("VPD page, short descriptor length=%d, left=%d\n", bump,
+                    (len - k));
+            break;
+        }
+        if (0 == desc_len)
+            goto skip;          /* continue plus attach jo2p */
+        if (2 == op->do_hex)
+            hex2stdout(bp + 4, desc_len, 1);
+        else if (op->do_hex > 2)
+            hex2stdout(bp, bump, 1);
+        else {
+            int csll;
+
+            dtp = get_tpc_desc_type_s(desc_type);
+            sgj_js_nv_ihexstr(jsp, jo2p, "third_party_copy_descriptor_type",
+                              desc_type, NULL, dtp);
+            sgj_js_nv_ihex(jsp, jo2p, "third_party_copy_descriptor_length",
+                           desc_len);
+
+            switch (desc_type) {
+            case 0x0000:    /* Required if POPULATE TOKEN (or friend) used */
+                sgj_pr_hr(jsp, " %s:\n", dtp);
+                u = sg_get_unaligned_be16(bp + 10);
+                sgj_haj_vistr(jsp, jo2p, 2, "Maximum range descriptors",
+                              SGJ_SEP_COLON_1_SPACE, u, true,
+                              (0 == u) ? nr_s : NULL);
+                u = sg_get_unaligned_be32(bp + 12);
+                if (0 == u)
+                    cp = nr_s;
+                else if (SG_LIB_UNBOUNDED_32BIT == u)
+                    cp = "No maximum given";
+                else
+                    cp = NULL;
+                sgj_haj_vistr_nex(jsp, jo2p, 2, "Maximum inactivity timeout",
+                                  SGJ_SEP_COLON_1_SPACE, u, true, cp,
+                                  "unit: second");
+                u = sg_get_unaligned_be32(bp + 16);
+                sgj_haj_vistr_nex(jsp, jo2p, 2, "Default inactivity timeout",
+                                  SGJ_SEP_COLON_1_SPACE, u, true,
+                                  (0 == u) ? nr_s : NULL, "unit: second");
+                ull = sg_get_unaligned_be64(bp + 20);
+                sgj_haj_vistr_nex(jsp, jo2p, 2, "Maximum token transfer size",
+                                  SGJ_SEP_COLON_1_SPACE, ull, true,
+                                  (0 == ull) ? nr_s : NULL, "unit: LB");
+                ull = sg_get_unaligned_be64(bp + 28);
+                sgj_haj_vistr_nex(jsp, jo2p, 2, "Optimal transfer count",
+                                  SGJ_SEP_COLON_1_SPACE, ull, true,
+                                  (0 == ull) ? nr_s : NULL, "unit: LB");
+                break;
+            case 0x0001:    /* Mandatory (SPC-4) */
+                sgj_pr_hr(jsp, " %s:\n", "Commands supported list");
+                ja2p = sgj_named_subarray_r(jsp, jo2p,
+                                            "commands_supported_list");
+                j = 0;
+                csll = bp[4];
+                if (csll >= desc_len) {
+                    pr2serr("Command supported list length (%d) >= "
+                            "descriptor length (%d), wrong so trim\n",
+                            csll, desc_len);
+                    csll = desc_len - 1;
+                }
+                while (j < csll) {
+                    uint8_t opc, sa;
+                    static const char * soc = "supported_operation_code";
+                    static const char * ssa = "supported_service_action";
+
+                    jo3p = NULL;
+                    opc = bp[5 + j];
+                    sa_len = bp[6 + j];
+                    for (m = 0; (m < sa_len) && ((j + m) < csll); ++m) {
+                        jo3p = sgj_new_unattached_object_r(jsp);
+                        sa = bp[7 + j + m];
+                        sg_get_opcode_sa_name(opc, sa, pdt, blen, b);
+                        sgj_pr_hr(jsp, "  %s\n", b);
+                        sgj_js_nv_s(jsp, jo3p, "name", b);
+                        sgj_js_nv_ihex(jsp, jo3p, soc, opc);
+                        sgj_js_nv_ihex(jsp, jo3p, ssa, sa);
+                        sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+                    }
+                    if (0 == sa_len) {
+                        jo3p = sgj_new_unattached_object_r(jsp);
+                        sg_get_opcode_name(opc, pdt, blen, b);
+                        sgj_pr_hr(jsp, "  %s\n", b);
+                        sgj_js_nv_s(jsp, jo3p, "name", b);
+                        sgj_js_nv_ihex(jsp, jo3p, soc, opc);
+                        sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+                    } else if (m < sa_len)
+                        pr2serr("Supported service actions list length (%d) "
+                                "is too large\n", sa_len);
+                    j += m + 2;
+                }
+                break;
+            case 0x0004:
+                sgj_pr_hr(jsp, " %s:\n", dtp);
+                sgj_haj_vi(jsp, jo2p, 2, "Maximum CSCD descriptor count",
+                           SGJ_SEP_COLON_1_SPACE,
+                           sg_get_unaligned_be16(bp + 8), true);
+                sgj_haj_vi(jsp, jo2p, 2, "Maximum segment descriptor count",
+                           SGJ_SEP_COLON_1_SPACE,
+                           sg_get_unaligned_be16(bp + 10), true);
+                sgj_haj_vi(jsp, jo2p, 2, "Maximum descriptor list length",
+                           SGJ_SEP_COLON_1_SPACE,
+                           sg_get_unaligned_be32(bp + 12), true);
+                sgj_haj_vi(jsp, jo2p, 2, "Maximum inline data length",
+                           SGJ_SEP_COLON_1_SPACE,
+                           sg_get_unaligned_be32(bp + 17), true);
+                break;
+            case 0x0008:
+                sgj_pr_hr(jsp, " Supported descriptors:\n");
+                ja2p = sgj_named_subarray_r(jsp, jo2p,
+                                            "supported_descriptor_list");
+                for (j = 0; j < bp[4]; j++) {
+                    bool found_name;
+
+                    jo3p = sgj_new_unattached_object_r(jsp);
+                    u = bp[5 + j];
+                    cp = get_tpc_desc_name(u);
+                    found_name = (strlen(cp) > 0);
+                    if (found_name)
+                        sgj_pr_hr(jsp, "  %s [0x%x]\n", cp, u);
+                    else
+                        sgj_pr_hr(jsp, "  0x%x\n", u);
+                    sgj_js_nv_s(jsp, jo3p, "name", found_name ? cp : nr_s);
+                    sgj_js_nv_ihex(jsp, jo3p, "code", u);
+                    sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+                }
+                break;
+            case 0x000C:
+                sgj_pr_hr(jsp, " Supported CSCD IDs (above 0x7ff):\n");
+                ja2p = sgj_named_subarray_r(jsp, jo2p, "supported_cscd_"
+                                            "descriptor_id_list");
+                v = sg_get_unaligned_be16(bp + 4);
+                for (j = 0; j < (int)v; j += 2) {
+                    bool found_name;
+
+                    jo3p = sgj_new_unattached_object_r(jsp);
+                    u = sg_get_unaligned_be16(bp + 6 + j);
+                    cp = get_cscd_desc_id_name(u);
+                    found_name = (strlen(cp) > 0);
+                    if (found_name)
+                        sgj_pr_hr(jsp, "  %s [0x%04x]\n", cp, u);
+                    else
+                        sgj_pr_hr(jsp, "  0x%04x\n", u);
+                    sgj_js_nv_s(jsp, jo3p, "name", found_name ? cp : nr_s);
+                    sgj_js_nv_ihex(jsp, jo3p, "id", u);
+                    sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+                }
+                break;
+            case 0x000D:
+                sgj_pr_hr(jsp, " Copy group identifier:\n");
+                u = bp[4];
+                sg_t10_uuid_desig2str(bp + 5, u, 1 /* c_set */, false,
+                                      true, NULL, blen, b);
+                sgj_pr_hr(jsp, "  Locally assigned UUID: %s", b);
+                sgj_js_nv_s(jsp, jo2p, "locally_assigned_uuid", b);
+                break;
+            case 0x0106:
+                sgj_pr_hr(jsp, " ROD token features:\n");
+                sgj_haj_vi(jsp, jo2p, 2, "Remote tokens",
+                           SGJ_SEP_COLON_1_SPACE, bp[4] & 0x0f, true);
+                u = sg_get_unaligned_be32(bp + 16);
+                sgj_pr_hr(jsp, "  Minimum token lifetime: %u seconds\n", u);
+                sgj_js_nv_ihex_nex(jsp, jo2p, "minimum_token_lifetime", u,
+                                   true, "unit: second");
+                u = sg_get_unaligned_be32(bp + 20);
+                sgj_pr_hr(jsp, "  Maximum token lifetime: %u seconds\n", u);
+                sgj_js_nv_ihex_nex(jsp, jo2p, "maximum_token_lifetime", u,
+                                   true, "unit: second");
+                u = sg_get_unaligned_be32(bp + 24);
+                sgj_haj_vi_nex(jsp, jo2p, 2, "Maximum token inactivity "
+                               "timeout", SGJ_SEP_COLON_1_SPACE, u,
+                               true, "unit: second");
+                u = sg_get_unaligned_be16(bp + 46);
+                ja2p = sgj_named_subarray_r(jsp, jo2p,
+                    "rod_device_type_specific_features_descriptor_list");
+                decode_rod_descriptor(bp + 48, u, op, ja2p);
+                break;
+            case 0x0108:
+                sgj_pr_hr(jsp, " Supported ROD token and ROD types:\n");
+                ja2p = sgj_named_subarray_r(jsp, jo2p, "rod_type_"
+                                            "descriptor_list");
+                for (j = 0; j < sg_get_unaligned_be16(bp + 6); j+= 64) {
+                    bool found_name;
+
+                    jo3p = sgj_new_unattached_object_r(jsp);
+                    u = sg_get_unaligned_be32(bp + 8 + j);
+                    cp = get_tpc_rod_name(u);
+                    found_name = (strlen(cp) > 0);
+                    if (found_name > 0)
+                        sgj_pr_hr(jsp, "  ROD type: %s [0x%x]\n", cp, u);
+                    else
+                        sgj_pr_hr(jsp, "  ROD type: 0x%x\n", u);
+                    sgj_js_nv_ihexstr(jsp, jo3p, "rod_type", u, NULL,
+                                      found_name ? cp : NULL);
+                    u = bp[8 + j + 4];
+                    sgj_pr_hr(jsp, "    ECPY_INT: %s\n",
+                              (u & 0x80) ? y_s : n_s);
+                    sgj_js_nv_ihex_nex(jsp, jo3p, "ecpy_int", !!(0x80 & u),
+                                       false, "Extended CoPY INTernal rods");
+                    sgj_pr_hr(jsp, "    Token in: %s\n",
+                              (u & 0x2) ? y_s : n_s);
+                    sgj_js_nv_i(jsp, jo3p, "token_in", !!(0x2 & u));
+                    sgj_pr_hr(jsp, "    Token out: %s\n",
+                              (u & 0x1) ? y_s : n_s);
+                    sgj_js_nv_i(jsp, jo3p, "token_out", !!(0x2 & u));
+                    u = sg_get_unaligned_be16(bp + 8 + j + 6);
+                    sgj_haj_vi(jsp, jo3p, 4, "Preference indicator",
+                               SGJ_SEP_COLON_1_SPACE, u, true);
+                    sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+                }
+                break;
+            case 0x8001:    /* Mandatory (SPC-4) */
+                sgj_pr_hr(jsp, " General copy operations:\n");
+                u = sg_get_unaligned_be32(bp + 4);
+                sgj_haj_vi(jsp, jo2p, 2, "Total concurrent copies",
+                           SGJ_SEP_COLON_1_SPACE, u, true);
+                u = sg_get_unaligned_be32(bp + 8);
+                sgj_haj_vi(jsp, jo2p, 2, "Maximum identified concurrent "
+                           "copies", SGJ_SEP_COLON_1_SPACE, u, true);
+                u = sg_get_unaligned_be32(bp + 12);
+                sgj_haj_vi_nex(jsp, jo2p, 2, "Maximum segment length",
+                               SGJ_SEP_COLON_1_SPACE, u, true, "unit: byte");
+                u = bp[16];     /* field is power of 2 */
+                sgj_haj_vi_nex(jsp, jo2p, 2, "Data segment granularity",
+                               SGJ_SEP_COLON_1_SPACE, u, true,
+                               "unit: 2^val LB");
+                u = bp[17];     /* field is power of 2 */
+                sgj_haj_vi_nex(jsp, jo2p, 2, "Inline data granularity",
+                               SGJ_SEP_COLON_1_SPACE, u, true,
+                               "unit: 2^val LB");
+                break;
+            case 0x9101:
+                sgj_pr_hr(jsp, " Stream copy operations:\n");
+                u = sg_get_unaligned_be32(bp + 4);
+                sgj_haj_vi_nex(jsp, jo2p, 2, "Maximum stream device transfer "
+                               "size", SGJ_SEP_COLON_1_SPACE, u, true,
+                               "unit: byte");
+                break;
+            case 0xC001:
+                sgj_pr_hr(jsp, " Held data:\n");
+                u = sg_get_unaligned_be32(bp + 4);
+                sgj_haj_vi_nex(jsp, jo2p, 2, "Held data limit",
+                               SGJ_SEP_COLON_1_SPACE, u, true,
+                               "unit: byte; (lower limit: minimum)");
+                sgj_haj_vi_nex(jsp, jo2p, 2, "Held data granularity",
+                               SGJ_SEP_COLON_1_SPACE, bp[8], true,
+                               "unit: 2^val byte");
+                break;
+            default:
+                pr2serr("Unexpected type=%d\n", desc_type);
+                hex2stderr(bp, bump, 1);
+                break;
+            }
+        }
+skip:
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+        jo2p = NULL;
+    }
+    if (jo2p)
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+}
+
+/* VPD_PROTO_LU  0x90 ["pslu"] */
+void
+decode_proto_lu_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                    sgj_opaque_p jap)
+{
+    int k, bump, rel_port, desc_len, proto;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    char b[128];
+    static const int blen = sizeof(b);
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 1 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        rel_port = sg_get_unaligned_be16(bp);
+        sgj_haj_vi(jsp, jo2p, 2, "Relative port",
+                   SGJ_SEP_COLON_1_SPACE, rel_port, true);
+        proto = bp[2] & 0xf;
+        sg_get_trans_proto_str(proto, blen, b);
+        sgj_haj_vistr(jsp, jo2p, 4, "Protocol identifier",
+                      SGJ_SEP_COLON_1_SPACE, proto, false, b);
+        desc_len = sg_get_unaligned_be16(bp + 6);
+        bump = 8 + desc_len;
+        if ((k + bump) > len) {
+            pr2serr("Protocol-specific logical unit information VPD page, "
+                    "short descriptor length=%d, left=%d\n", bump, (len - k));
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            return;
+        }
+        if (0 == desc_len)
+            goto again;
+        if (2 == op->do_hex) {
+            hex2stdout(bp + 8, desc_len, 1);
+            goto again;
+        }
+        switch (proto) {
+        case TPROTO_SAS:
+            sgj_haj_vi(jsp, jo2p, 2, "TLR control supported",
+                       SGJ_SEP_COLON_1_SPACE, !!(bp[8] & 0x1), false);
+            break;
+        default:
+            pr2serr("Unexpected proto=%d\n", proto);
+            hex2stderr(bp, bump, 1);
+            break;
+        }
+again:
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+/* VPD_PROTO_PORT  0x91 ["pspo"] */
+void
+decode_proto_port_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                      sgj_opaque_p jap)
+{
+    bool pds, ssp_pers;
+    int k, j, bump, rel_port, desc_len, proto, phy;
+    const uint8_t * bp;
+    const uint8_t * pidp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    sgj_opaque_p ja2p = NULL;
+    sgj_opaque_p jo3p = NULL;
+    char b[128];
+    static const int blen = sizeof(b);
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 1 : -1);
+        return;
+    }
+    if (len < 4) {
+        pr2serr("VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 4;
+    bp = buff + 4;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        rel_port = sg_get_unaligned_be16(bp);
+        sgj_haj_vi(jsp, jo2p, 2, "Relative port",
+                   SGJ_SEP_COLON_1_SPACE, rel_port, true);
+        proto = bp[2] & 0xf;
+        sg_get_trans_proto_str(proto, blen, b);
+        sgj_haj_vistr(jsp, jo2p, 4, "Protocol identifier",
+                      SGJ_SEP_COLON_1_SPACE, proto, false, b);
+        desc_len = sg_get_unaligned_be16(bp + 6);
+        bump = 8 + desc_len;
+        if ((k + bump) > len) {
+            pr2serr("VPD page, short descriptor length=%d, left=%d\n",
+                    bump, (len - k));
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            return;
+        }
+        if (0 == desc_len)
+            goto again;
+        if (2 == op->do_hex) {
+            hex2stdout(bp + 8, desc_len, 1);
+            goto again;
+        }
+        switch (proto) {
+        case TPROTO_SAS:    /* page added in spl3r02 */
+            pds = !!(bp[3] & 0x1);
+            sgj_pr_hr(jsp, "    power disable supported (pwr_d_s)=%d\n", pds);
+            sgj_js_nv_ihex_nex(jsp, jo2p, "pwr_d_s", pds, false,
+                       "PoWeR Disable Supported");
+            ja2p = sgj_named_subarray_r(jsp, jo2p,
+                                    "sas_phy_information_descriptor_list");
+            pidp = bp + 8;
+            for (j = 0; j < desc_len; j += 4, pidp += 4) {
+                jo3p = sgj_new_unattached_object_r(jsp);
+                phy = pidp[1];
+                ssp_pers = !!(0x1 & pidp[2]);
+                sgj_pr_hr(jsp, "      phy id=%d, SSP persistent capable=%d\n",
+                          phy, ssp_pers);
+                sgj_js_nv_ihex(jsp, jo3p, "phy_identifier", phy);
+                sgj_js_nv_i(jsp, jo3p, "ssp_persistent_capable", ssp_pers);
+                sgj_js_nv_o(jsp, ja2p, NULL /* name */, jo3p);
+            }
+            break;
+        default:
+            pr2serr("Unexpected proto=%d\n", proto);
+            hex2stderr(bp, bump, 1);
+            break;
+        }
+again:
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+/* VPD_LB_PROTECTION 0xb5 (SSC)  [added in ssc5r02a] */
+void
+decode_lb_protection_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                         sgj_opaque_p jap)
+{
+    int k, bump;
+    const uint8_t * bp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+
+    if ((1 == op->do_hex) || (op->do_hex > 2)) {
+        hex2stdout(buff, len, (1 == op->do_hex) ? 0 : -1);
+        return;
+    }
+    if (len < 8) {
+        pr2serr("VPD page length too short=%d\n", len);
+        return;
+    }
+    len -= 8;
+    bp = buff + 8;
+    for (k = 0; k < len; k += bump, bp += bump) {
+        jo2p = sgj_new_unattached_object_r(jsp);
+        bump = 1 + bp[0];
+        sgj_pr_hr(jsp, "  method: %d, info_len: %d, LBP_W_C=%d, LBP_R_C=%d, "
+                  "RBDP_C=%d\n", bp[1], 0x3f & bp[2], !!(0x80 & bp[3]),
+                  !!(0x40 & bp[3]), !!(0x20 & bp[3]));
+        sgj_js_nv_ihex(jsp, jo2p, "logical_block_protection_method", bp[1]);
+        sgj_js_nv_ihex_nex(jsp, jo2p,
+                           "logical_block_protection_information_length",
+                           0x3f & bp[2], true, "unit: byte");
+        sgj_js_nv_ihex_nex(jsp, jo2p, "lbp_w_c", !!(0x80 & bp[3]), false,
+                           "Logical Blocks Protected during Write supported");
+        sgj_js_nv_ihex_nex(jsp, jo2p, "lbp_r_c", !!(0x40 & bp[3]), false,
+                           "Logical Blocks Protected during Read supported");
+        sgj_js_nv_ihex_nex(jsp, jo2p, "rbdp_c", !!(0x20 & bp[3]), false,
+                           "Recover Buffered Data Protected supported");
+        if ((k + bump) > len) {
+            pr2serr("Logical block protection VPD page, short "
+                    "descriptor length=%d, left=%d\n", bump, (len - k));
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+            return;
+        }
+        sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+    }
+}
+
+/* VPD_TA_SUPPORTED  0xb2 ["tas"] */
+void
+decode_tapealert_supported_vpd(const uint8_t * buff, int len,
+                               struct opts_t * op, sgj_opaque_p jop)
+{
+    bool have_ta_strs = !! sg_lib_tapealert_strs[0];
+    int k, mod, div, n;
+    unsigned int supp;
+    sgj_state * jsp = &op->json_st;
+    char b[144];
+    char d[64];
+    static const int blen = sizeof(b);
+
+    if (len < 12) {
+        pr2serr("VPD page length too short=%d\n", len);
+        return;
+    }
+    b[0] ='\0';
+    for (k = 1, n = 0; k < 0x41; ++k) {
+        mod = ((k - 1) % 8);
+        div = (k - 1) / 8;
+        supp = !! (buff[4 + div] & (1 << (7 - mod)));
+        if (jsp->pr_as_json) {
+            snprintf(d, sizeof(d), "flag%02xh", k);
+            if (have_ta_strs)
+                sgj_js_nv_ihex_nex(jsp, jop, d, supp, false,
+                                   sg_lib_tapealert_strs[k]);
+            else
+                sgj_js_nv_i(jsp, jop, d, supp);
+        }
+        if (0 == mod) {
+            if (div > 0) {
+                sgj_pr_hr(jsp, "%s\n", b);
+                n = 0;
+            }
+            n += sg_scnpr(b + n, blen - n, "  Flag%02Xh: %d", k, supp);
+        } else
+            n += sg_scnpr(b + n, blen - n, "  %02Xh: %d", k, supp);
+    }
+    sgj_pr_hr(jsp, "%s\n", b);
+}
+
+/*
+ * Some of the vendor specific VPD pages are common as well. So place them here
+ * to save on code duplication.
+ */
+
+static const char * lun_state_arr[] =
+{
+    "LUN not bound or LUN_Z report",
+    "LUN bound, but not owned by this SP",
+    "LUN bound and owned by this SP",
+};
+
+static const char * ip_mgmt_arr[] =
+{
+    "No IP access",
+    "Reserved (undefined)",
+    "via IPv4",
+    "via IPv6",
+};
+
+static const char * sp_arr[] =
+{
+    "SP A",
+    "SP B",
+};
+
+static const char * lun_op_arr[] =
+{
+    "Normal operations",
+    "I/O Operations being rejected, SP reboot or NDU in progress",
+};
+
+static const char * failover_mode_arr[] =
+{
+    "Legacy mode 0",
+    "Unknown mode (1)",
+    "Unknown mode (2)",
+    "Unknown mode (3)",
+    "Active/Passive (PNR) mode 1",
+    "Unknown mode (5)",
+    "Active/Active (ALUA) mode 4",
+    "Unknown mode (7)",
+    "Legacy mode 2",
+    "Unknown mode (9)",
+    "Unknown mode (10)",
+    "Unknown mode (11)",
+    "Unknown mode (12)",
+    "Unknown mode (13)",
+    "AIX Active/Passive (PAR) mode 3",
+    "Unknown mode (15)",
+};
+
+/* VPD_UPR_EMC,VPD_V_UPR_EMC  0xc0  ["upr","upr"] */
+void
+decode_upr_vpd_c0_emc(uint8_t * buff, int len, struct opts_t * op,
+                      sgj_opaque_p jop)
+{
+    uint8_t uc;
+    int k, n, ip_mgmt, vpp80, lun_z;
+    sgj_state * jsp = &op->json_st;
+    const char * cp;
+    const char * c2p;
+    char b[256];
+    static const int blen = sizeof(b);
+
+    if (len < 3) {
+        pr2serr("EMC upr VPD page [0xc0]: length too short=%d\n", len);
+        return;
+    }
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    if (buff[9] != 0x00) {
+        pr2serr("Unsupported page revision %d, decoding not possible.\n",
+                buff[9]);
+        return;
+    }
+    for (k = 0, n = 0; k < 16; ++k)
+        n += sg_scnpr(b + n, blen - n, "%02x", buff[10 + k]);
+    sgj_haj_vs(jsp, jop, 2, "LUN WWN", SGJ_SEP_COLON_1_SPACE, b);
+    snprintf(b, blen, "%.*s", buff[49], buff + 50);
+    sgj_haj_vs(jsp, jop, 2, "Array Serial Number", SGJ_SEP_COLON_1_SPACE, b);
+
+    if (buff[4] > 0x02)
+       snprintf(b, blen, "Unknown (%x)", buff[4]);
+    else
+       snprintf(b, blen, "%s", lun_state_arr[buff[4]]);
+    sgj_haj_vistr(jsp, jop, 2, "LUN State", SGJ_SEP_COLON_1_SPACE,
+                  buff[4], true, b);
+
+    uc = buff[8];
+    n = 0;
+    if (uc > 0x01)
+       n += sg_scnpr(b + n, blen - n, "Unknown SP (%x)", uc);
+    else
+       n += sg_scnpr(b + n, blen - n, "%s", sp_arr[uc]);
+    sgj_js_nv_ihexstr(jsp, jop, "path_connects_to", uc, NULL, b);
+    n += sg_scnpr(b + n, blen - n, ", Port Number: %u", buff[7]);
+    sgj_pr_hr(jsp, "  This path connects to: %s\n", b);
+    sgj_js_nv_ihex(jsp, jop, "port_number", buff[7]);
+
+    if (buff[5] > 0x01)
+           snprintf(b, blen, "Unknown (%x)\n", buff[5]);
+    else
+           snprintf(b, blen, "%s\n", sp_arr[buff[5]]);
+    sgj_haj_vistr(jsp, jop, 2, "Default owner", SGJ_SEP_COLON_1_SPACE,
+                  buff[5], true, b);
+
+    cp = (buff[6] & 0x40) ? "supported" : "not supported";
+    sgj_pr_hr(jsp, "  NO_ATF: %s, Access Logix: %s\n",
+              buff[6] & 0x80 ? "set" : "not set", cp);
+    sgj_js_nv_i(jsp, jop, "no_atf", !! (buff[6] & 0x80));
+    sgj_js_nv_istr(jsp, jop, "access_logix", !! (buff[6] & 0x40),
+                   NULL, cp);
+
+    ip_mgmt = (buff[6] >> 4) & 0x3;
+    cp = ip_mgmt_arr[ip_mgmt];
+    sgj_pr_hr(jsp, "  SP IP Management Mode: %s\n", cp);
+    sgj_js_nv_istr(jsp, jop, "sp_ip_management_mode", !! ip_mgmt,
+                   NULL, cp);
+    if (ip_mgmt == 2) {
+        snprintf(b, blen, "%u.%u.%u.%u", buff[44], buff[45], buff[46],
+                 buff[47]);
+        sgj_pr_hr(jsp, "  SP IPv4 address: %s\n", b);
+        sgj_js_nv_s(jsp, jop, "sp_ipv4_address", b);
+    } else if (ip_mgmt == 3) {
+        printf("  SP IPv6 address: ");
+        n = 0;
+        for (k = 0; k < 16; ++k)
+            n += sg_scnpr(b + n, blen - n, "%02x", buff[32 + k]);
+        sgj_pr_hr(jsp, "  SP IPv6 address: %s\n", b);
+        sgj_js_nv_hex_bytes(jsp, jop, "sp_ipv6_address", buff + 32, 16);
+    }
+
+    k = buff[28] & 0x0f;
+    sgj_pr_hr(jsp, "  System Type: %x, Failover mode: %s\n",
+              buff[27], failover_mode_arr[k]);
+    sgj_js_nv_ihex(jsp, jop, "system_type", buff[27]);
+    sgj_js_nv_ihexstr(jsp, jop, "failover_mode", k, NULL,
+                      failover_mode_arr[k]);
+
+    vpp80 = buff[30] & 0x08;
+    lun_z = buff[30] & 0x04;
+    cp = vpp80 ? "array serial#" : "LUN serial#";
+    c2p = lun_z ? "Set to 1" : "Unknown";
+    sgj_pr_hr(jsp, "  Inquiry VPP 0x80 returns: %s, Arraycommpath: %s\n",
+              cp, c2p);
+    sgj_js_nv_istr(jsp, jop, "inquiry_vpp_0x80_returns", !! vpp80, NULL, cp);
+    sgj_js_nv_istr(jsp, jop, "arraycommpath", !! lun_z, NULL, c2p);
+
+    cp = buff[48] > 1 ? "undefined" : lun_op_arr[buff[48]];
+    sgj_pr_hr(jsp, "  Lun operations: %s\n", cp);
+    sgj_js_nv_istr(jsp, jop, "lun_operations", 0x1 & buff[48], NULL, cp);
+
+    return;
+}
+
+/*  VPD_RDAC_VERS,VPD_V_SVER_RDAC  0xc2 ["rdac_vers", "swr4"] */
+void
+decode_rdac_vpd_c2(uint8_t * buff, int len, struct opts_t * op,
+                   sgj_opaque_p jop)
+{
+    int i, n, v, r, m, p, d, y, num_part;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    sgj_opaque_p jap = NULL;
+    // const char * cp;
+    // const char * c2p;
+    char b[256];
+    static const int blen = sizeof(b);
+    char part[5];
+
+    if (len < 3) {
+        pr2serr("Software Version VPD page length too short=%d\n", len);
+        return;
+    }
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    if (buff[4] != 's' && buff[5] != 'w' && buff[6] != 'r') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding not possible.\n",
+                buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    snprintf(b, blen, "%02x.%02x.%02x", buff[8], buff[9], buff[10]);
+    sgj_haj_vs(jsp, jop, 2, "Software Version", SGJ_SEP_COLON_1_SPACE, b);
+    snprintf(b, blen, "%02d/%02d/%02d\n", buff[11], buff[12], buff[13]);
+    sgj_haj_vs(jsp, jop, 2, "Software Date", SGJ_SEP_COLON_1_SPACE, b);
+    n = 0;
+    n += sg_scnpr(b + n, blen - n, "  Features:");
+    if (buff[14] & 0x01)
+        n += sg_scnpr(b + n, blen - n, " Dual Active,");
+    if (buff[14] & 0x02)
+        n += sg_scnpr(b + n, blen - n, " Series 3,");
+    if (buff[14] & 0x04)
+        n += sg_scnpr(b + n, blen - n, " Multiple Sub-enclosures,");
+    if (buff[14] & 0x08)
+        n += sg_scnpr(b + n, blen - n, " DCE/DRM/DSS/DVE,");
+    if (buff[14] & 0x10)
+        n += sg_scnpr(b + n, blen - n, " Asymmetric Logical Unit Access,");
+    sgj_pr_hr(jsp, "%s\n", b);
+    if (jsp->pr_as_json) {
+        jo2p = sgj_snake_named_subobject_r(jsp, jop, "features");
+        sgj_js_nv_i(jsp, jo2p, "dual_active", !! (buff[14] & 0x01));
+        sgj_js_nv_i(jsp, jo2p, "series_3", !! (buff[14] & 0x02));
+        sgj_js_nv_i(jsp, jo2p, "multiple_sub_enclosures",
+                    !! (buff[14] & 0x04));
+        sgj_js_nv_i(jsp, jo2p, "dcm_drm_dss_dve", !! (buff[14] & 0x08));
+        sgj_js_nv_i(jsp, jo2p, "asymmetric_logical_unit_access",
+                    !! (buff[14] & 0x10));
+    }
+    sgj_haj_vi(jsp, jop, 2, "Maximum number of LUNS",
+               SGJ_SEP_COLON_1_SPACE, buff[15], true);
+
+    num_part = (len - 12) / 16;
+    n = 16;
+    printf("  Partitions: %d\n", num_part);
+    sgj_haj_vi(jsp, jop, 2, "Partitions", SGJ_SEP_COLON_1_SPACE, num_part,
+               true);
+    if (num_part > 0)
+        jap = sgj_named_subarray_r(jsp, jop, "partition_list");
+    for (i = 0; i < num_part; i++) {
+        memset(part,0, 5);
+        memcpy(part, &buff[n], 4);
+        sgj_pr_hr(jsp, "    Name: %s\n", part);
+        if (jsp->pr_as_json) {
+            jo2p = sgj_new_unattached_object_r(jsp);
+            sgj_js_nv_s(jsp, jo2p, "name", part);
+        }
+        n += 4;
+        v = buff[n++];
+        r = buff[n++];
+        m = buff[n++];
+        p = buff[n++];
+        snprintf(b, blen, "%d.%d.%d.%d", v, r, m, p);
+        sgj_pr_hr(jsp, "    Version: %s\n", b);
+        if (jsp->pr_as_json)
+            sgj_js_nv_s(jsp, jo2p, "version", b);
+        m = buff[n++];
+        d = buff[n++];
+        y = buff[n++];
+        snprintf(b, blen, "%d/%d/%d\n", m, d, y);
+        sgj_pr_hr(jsp, "    Date: %s\n", b);
+        if (jsp->pr_as_json) {
+            sgj_js_nv_s(jsp, jo2p, "date", b);
+            sgj_js_nv_o(jsp, jap, NULL /* name */, jo2p);
+        }
+
+        n += 5;
+    }
+    return;
+}
+
+static char *
+decode_rdac_vpd_c9_aas_s(uint8_t aas, char * b, int blen)
+{
+    // snprintf("  Asymmetric Access State:");
+    switch(aas & 0x0F) {
+        case 0x0:
+            snprintf(b, blen, "Active/Optimized");
+            break;
+        case 0x1:
+            snprintf(b, blen, "Active/Non-Optimized");
+            break;
+        case 0x2:
+            snprintf(b, blen, "Standby");
+            break;
+        case 0x3:
+            snprintf(b, blen, "Unavailable");
+            break;
+        case 0xE:
+            snprintf(b, blen, "Offline");
+            break;
+        case 0xF:
+            snprintf(b, blen, "Transitioning");
+            break;
+        default:
+            snprintf(b, blen, "(unknown)");
+            break;
+    }
+    return b;
+}
+
+static char *
+decode_rdac_vpd_c9_vs_s(uint8_t vendor, char * b, int blen)
+{
+    // printf("  Vendor Specific Field:");
+    switch(vendor) {
+        case 0x01:
+            snprintf(b, blen, "Operating normally");
+            break;
+        case 0x02:
+            snprintf(b, blen, "Non-responsive to queries");
+            break;
+        case 0x03:
+            snprintf(b, blen, "Controller being held in reset");
+            break;
+        case 0x04:
+            snprintf(b, blen, "Performing controller firmware download (1st "
+                   "controller)");
+            break;
+        case 0x05:
+            snprintf(b, blen, "Performing controller firmware download (2nd "
+                   "controller)");
+            break;
+        case 0x06:
+            snprintf(b, blen,
+                     "Quiesced as a result of an administrative request");
+            break;
+        case 0x07:
+            snprintf(b, blen,
+                     "Service mode as a result of an administrative request");
+            break;
+        case 0xFF:
+            snprintf(b, blen, "Details are not available");
+            break;
+        default:
+            snprintf(b, blen, "(unknown)");
+            break;
+    }
+    return b;
+}
+
+/*  VPD_RDAC_VAC,VPD_V_VAC_RDAC  0xc9 ["rdac_vac", "vac1"] */
+void
+decode_rdac_vpd_c9(uint8_t * buff, int len, struct opts_t * op,
+                   sgj_opaque_p jop)
+{
+    bool vav;
+    int n, n_hold;
+    sgj_state * jsp = &op->json_st;
+    char b[196];
+    static const int blen = sizeof(b);
+
+    if (len < 3) {
+        pr2serr("Volume Access Control VPD page length too short=%d\n", len);
+        return;
+    }
+    if (op->do_hex) {
+        hex2stdout(buff, len, no_ascii_4hex(op));
+        return;
+    }
+    if (buff[4] != 'v' && buff[5] != 'a' && buff[6] != 'c') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding "
+                "not possible.\n" , buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    if (buff[7] != '1') {
+        pr2serr("Invalid page version '%c' (should be 1)\n", buff[7]);
+    }
+    n = ((buff[8] & 0xE0) == 0xE0 );
+    if (n) {
+        sgj_pr_hr(jsp, "  IOShipping (ALUA): Enabled\n");
+        sgj_js_nv_ihexstr_nex(jsp, jop, "ioshipping", n, true, NULL,
+                              "Enabled",
+                              "a.k.a. ALUA (Asymmetric Logical Unit Access)");
+    } else {
+        n = 0;
+        n = snprintf(b, blen, "  AVT:");
+        n_hold = n;
+        if (buff[8] & 0x80) {
+            n += sg_scnpr(b + n, blen - n, " Enabled");
+            if (buff[8] & 0x40)
+                n += sg_scnpr(b + n, blen - n, " (Allow reads on sector 0)");
+            sgj_pr_hr(jsp, "%s\n", b);
+            sgj_js_nv_ihexstr(jsp, jop, "avt", buff[8], NULL, b + n_hold);
+
+        } else {
+            sgj_pr_hr(jsp, "%s: Disabled\n", b);
+            sgj_js_nv_ihexstr(jsp, jop, "avt", buff[8], NULL, "Disabled");
+        }
+    }
+    vav = !! (0x1 & buff[8]);
+    sgj_haj_vistr(jsp, jop, 2, "Volume access via", SGJ_SEP_COLON_1_SPACE,
+                  (int)vav, false,
+                  (vav ? "primary controller" : "alternate controller"));
+
+    if (buff[8] & 0x08) {
+        n = buff[15] & 0xf;
+        // printf("  Path priority: %d ", n);
+        switch (n) {
+        case 0x1:
+            snprintf(b, blen, "(preferred path)");
+            break;
+        case 0x2:
+            snprintf(b, blen, "(secondary path)");
+            break;
+        default:
+            snprintf(b, blen, "(unknown)");
+            break;
+        }
+        sgj_haj_vistr(jsp, jop, 2, "Path priority", SGJ_SEP_COLON_1_SPACE, n,
+                      true, b);
+
+        // printf("  Preferred Path Auto Changeable:");
+        n = buff[14] & 0x3C;
+        switch (n) {
+        case 0x14:
+            snprintf(b, blen, "No (User Disabled and Host Type Restricted)");
+            break;
+        case 0x18:
+            snprintf(b, blen, "No (User Disabled)");
+            break;
+        case 0x24:
+            snprintf(b, blen, "No (Host Type Restricted)");
+            break;
+        case 0x28:
+            snprintf(b, blen, "Yes");
+            break;
+        default:
+            snprintf(b, blen, "(Unknown)");
+            break;
+        }
+        sgj_haj_vistr(jsp, jop, 2, "Preferred path auto changeable",
+                      SGJ_SEP_COLON_1_SPACE, n, true, b);
+
+        n = buff[14] & 0x03;
+        // printf("  Implicit Failback:");
+        switch (n) {
+        case 0x1:
+            snprintf(b, blen, "Disabled");
+            break;
+        case 0x2:
+            snprintf(b, blen, "Enabled");
+            break;
+        default:
+            snprintf(b, blen, "(Unknown)");
+            break;
+        }
+        sgj_haj_vistr(jsp, jop, 2, "Implicit failback",
+                      SGJ_SEP_COLON_1_SPACE, n, false, b);
+    } else {
+        n = buff[9] & 0xf;
+        // printf("  Path priority: %d ", buff[9] & 0xf);
+        switch (n) {
+        case 0x1:
+            snprintf(b, blen, "(preferred path)");
+            break;
+        case 0x2:
+            snprintf(b, blen, "(secondary path)");
+            break;
+        default:
+            snprintf(b, blen, "(unknown)");
+            break;
+        }
+        sgj_haj_vistr(jsp, jop, 2, "Path priority",
+                      SGJ_SEP_COLON_1_SPACE, n, false, b);
+    }
+
+    n = !! (buff[8] & 0x80);
+    sgj_haj_vi(jsp, jop, 2, "Target port group present",
+               SGJ_SEP_COLON_1_SPACE, n, false);
+    if (n) {
+        sgj_opaque_p jo2p = NULL;
+        sgj_opaque_p jo3p = NULL;
+        static const char * tpg_s = "Target port group data";
+        static const char * aas_s = "Asymmetric access state";
+        static const char * vsf_s = "Vendor specific field";
+        char d1[80];
+        char d2[80];
+
+        sgj_pr_hr(jsp, "  Target Port Group Data (This controller):\n");
+        decode_rdac_vpd_c9_aas_s(buff[10], d1, sizeof(d1));
+        decode_rdac_vpd_c9_vs_s(buff[11], d2, sizeof(d2));
+        sgj_pr_hr(jsp, "    %s: %s\n", aas_s, d1);
+        sgj_pr_hr(jsp, "    %s: %s\n", vsf_s, d2);
+        if (jsp->pr_as_json) {
+            jo2p = sgj_snake_named_subobject_r(jsp, jop, tpg_s);
+            jo3p = sgj_snake_named_subobject_r(jsp, jo2p, "this_controller");
+            sgj_convert_to_snake_name(aas_s, b, blen);
+            sgj_js_nv_ihexstr(jsp, jo3p, b, buff[10], NULL, d1);
+            sgj_convert_to_snake_name(vsf_s, b, blen);
+            sgj_js_nv_ihexstr(jsp, jo3p, b, buff[11], NULL, d2);
+        }
+        sgj_pr_hr(jsp, " Target Port Group Data (Alternate controller):\n");
+        // decode_rdac_vpd_c9_rtpg_data(buff[12], buff[13]);
+
+        decode_rdac_vpd_c9_aas_s(buff[12], d1, sizeof(d1));
+        decode_rdac_vpd_c9_vs_s(buff[13], d2, sizeof(d2));
+        sgj_pr_hr(jsp, "    %s: %s\n", aas_s, d1);
+        sgj_pr_hr(jsp, "    %s: %s\n", vsf_s, d2);
+        if (jsp->pr_as_json) {
+            jo2p = sgj_snake_named_subobject_r(jsp, jop, tpg_s);
+            jo3p = sgj_snake_named_subobject_r(jsp, jo2p,
+                                               "alternate_controller");
+            sgj_convert_to_snake_name(aas_s, b, blen);
+            sgj_js_nv_ihexstr(jsp, jo3p, b, buff[12], NULL, d1);
+            sgj_convert_to_snake_name(vsf_s, b, blen);
+            sgj_js_nv_ihexstr(jsp, jo3p, b, buff[13], NULL, d2);
+        }
+    }
+}
diff --git a/src/sg_vpd_common.h b/src/sg_vpd_common.h
new file mode 100644
index 0000000..1313433
--- /dev/null
+++ b/src/sg_vpd_common.h
@@ -0,0 +1,294 @@
+#ifndef SG_VPD_H
+#define SG_VPD_H
+
+/*
+ * Copyright (c) 2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/* This is a common header file for the sg_inq and sg_vpd utilities */
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_pr2serr.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* standard VPD pages, in ascending page number order */
+#define VPD_SUPPORTED_VPDS 0x0
+#define VPD_UNIT_SERIAL_NUM 0x80
+#define VPD_IMP_OP_DEF 0x81             /* obsolete in SPC-2 */
+#define VPD_ASCII_OP_DEF 0x82           /* obsolete in SPC-2 */
+#define VPD_DEVICE_ID 0x83
+#define VPD_SOFTW_INF_ID 0x84
+#define VPD_MAN_NET_ADDR 0x85
+#define VPD_EXT_INQ 0x86                /* Extended Inquiry */
+#define VPD_MODE_PG_POLICY 0x87
+#define VPD_SCSI_PORTS 0x88
+#define VPD_ATA_INFO 0x89
+#define VPD_POWER_CONDITION 0x8a
+#define VPD_DEVICE_CONSTITUENTS 0x8b
+#define VPD_CFA_PROFILE_INFO 0x8c
+#define VPD_POWER_CONSUMPTION  0x8d
+#define VPD_3PARTY_COPY 0x8f            /* 3PC, XCOPY, SPC-5, SBC-4 */
+#define VPD_PROTO_LU 0x90
+#define VPD_PROTO_PORT 0x91
+#define VPD_SCSI_FEATURE_SETS 0x92      /* spc5r11 */
+#define VPD_BLOCK_LIMITS 0xb0           /* SBC-3 */
+#define VPD_SA_DEV_CAP 0xb0             /* SSC-3 */
+#define VPD_OSD_INFO 0xb0               /* OSD */
+#define VPD_BLOCK_DEV_CHARS 0xb1        /* SBC-3 */
+#define VPD_MAN_ASS_SN 0xb1             /* SSC-3, ADC-2 */
+#define VPD_SECURITY_TOKEN 0xb1         /* OSD */
+#define VPD_TA_SUPPORTED 0xb2           /* SSC-3 */
+#define VPD_LB_PROVISIONING 0xb2        /* SBC-3 */
+#define VPD_REFERRALS 0xb3              /* SBC-3 */
+#define VPD_AUTOMATION_DEV_SN 0xb3      /* SSC-3 */
+#define VPD_SUP_BLOCK_LENS 0xb4         /* sbc4r01 */
+#define VPD_DTDE_ADDRESS 0xb4           /* SSC-4 */
+#define VPD_BLOCK_DEV_C_EXTENS 0xb5     /* sbc4r02 */
+#define VPD_LB_PROTECTION 0xb5          /* SSC-5 */
+#define VPD_ZBC_DEV_CHARS 0xb6          /* zbc-r01b */
+#define VPD_BLOCK_LIMITS_EXT 0xb7       /* sbc4r08 */
+#define VPD_FORMAT_PRESETS 0xb8         /* sbc4r18 */
+#define VPD_CON_POS_RANGE 0xb9          /* sbc5r01 */
+#define VPD_NOPE_WANT_STD_INQ -2        /* request for standard inquiry */
+
+/* vendor/product identifiers */
+#define VPD_VP_SEAGATE 0
+#define VPD_VP_RDAC 1
+#define VPD_VP_EMC 2
+#define VPD_VP_DDS 3
+#define VPD_VP_HP3PAR 4
+#define VPD_VP_IBM_LTO 5
+#define VPD_VP_HP_LTO 6
+#define VPD_VP_WDC_HITACHI 7
+#define VPD_VP_NVME 8
+#define VPD_VP_SG 9     /* this package/library as a vendor */
+
+
+/* vendor VPD pages */
+#define VPD_V_HIT_PG3 0x3
+#define VPD_V_HP3PAR 0xc0
+#define VPD_V_FIRM_SEA  0xc0
+#define VPD_V_UPR_EMC  0xc0
+#define VPD_V_HVER_RDAC  0xc0
+#define VPD_V_FVER_DDS 0xc0
+#define VPD_V_FVER_LTO 0xc0
+#define VPD_V_DCRL_LTO 0xc0
+#define VPD_V_DATC_SEA  0xc1
+#define VPD_V_FVER_RDAC  0xc1
+#define VPD_V_HVER_LTO 0xc1
+#define VPD_V_DSN_LTO 0xc1
+#define VPD_V_JUMP_SEA 0xc2
+#define VPD_V_SVER_RDAC 0xc2
+#define VPD_V_PCA_LTO 0xc2
+#define VPD_V_DEV_BEH_SEA 0xc3
+#define VPD_V_FEAT_RDAC 0xc3
+#define VPD_V_MECH_LTO 0xc3
+#define VPD_V_SUBS_RDAC 0xc4
+#define VPD_V_HEAD_LTO 0xc4
+#define VPD_V_ACI_LTO 0xc5
+#define VPD_V_DUCD_LTO 0xc7
+#define VPD_V_EDID_RDAC 0xc8
+#define VPD_V_MPDS_LTO 0xc8
+#define VPD_V_VAC_RDAC 0xc9
+#define VPD_V_RVSI_RDAC 0xca
+#define VPD_V_SAID_RDAC 0xd0
+#define VPD_V_HIT_PG_D1 0xd1
+#define VPD_V_HIT_PG_D2 0xd2
+
+#ifndef SG_NVME_VPD_NICR
+#define SG_NVME_VPD_NICR 0xde   /* NVME Identify Controller Response */
+#endif
+
+#define DEF_ALLOC_LEN 252
+#define MX_ALLOC_LEN (0xc000 + 0x80)
+#define DEF_PT_TIMEOUT  60       /* 60 seconds */
+
+enum sg_vpd_invoker_e {
+    SG_VPD_INV_NONE = 0,
+    SG_VPD_INV_SG_INQ,
+    SG_VPD_INV_SG_VPD,
+};
+
+/* This structure holds the union of options available in sg_inq and sg_vpd */
+struct opts_t {
+    enum sg_vpd_invoker_e invoker;  /* indicates if for sg_inq or sg_vpd */
+    bool do_all;                /* sg_vpd */
+    bool do_ata;                /* sg_inq */
+    bool do_decode;             /* sg_inq */
+    bool do_descriptors;        /* sg_inq */
+    bool do_enum;               /* sg_enum */
+    bool do_export;             /* sg_inq */
+    bool do_force;              /* sg_inq + sg_vpd */
+    bool do_only; /* sg_inq: --only after stdinq: don't fetch VPD page 0x80 */
+    bool do_quiet;              /* sg_vpd */
+    bool examine_given;         /* sg_vpd */
+    bool page_given;            /* sg_inq + sg_vpd */
+    bool possible_nvme;         /* sg_inq */
+    bool protect_not_sure;      /* sg_vpd */
+    bool verbose_given;         /* sg_inq + sg_vpd */
+    bool version_given;         /* sg_inq + sg_vpd */
+    bool do_vpd;                /* sg_inq */
+    bool std_inq_a_valid;       /* sg_inq + sg_vpd */
+#ifdef SG_SCSI_STRINGS
+    bool opt_new;               /* sg_inq */
+#endif
+    int do_block;               /* do_block */
+    int do_cmddt;               /* sg_inq */
+    int do_help;                /* sg_inq */
+    int do_hex;                 /* sg_inq + sg_vpd */
+    int do_ident;               /* sg_vpd */
+    int do_long;                /* sg_inq[int] + sg_vpd[bool] */
+    int do_raw;                 /* sg_inq + sg_vpd */
+    int do_vendor;              /* sg_inq */
+    int examine;                /* sg_vpd */
+    int maxlen;                 /* sg_inq[was: resp_len] + sg_vpd */
+    int num_pages;              /* sg_inq */
+    int page_pdt;               /* sg_inq */
+    int vend_prod_num;          /* sg_vpd */
+    int verbose;                /* sg_inq + sg_vpd */
+    int vpd_pn;                 /* sg_vpd */
+    const char * device_name;   /* sg_inq + sg_vpd */
+    const char * page_str;      /* sg_inq + sg_vpd */
+    const char * inhex_fn;      /* sg_inq + sg_vpd */
+    const char * sinq_inraw_fn; /* sg_inq + sg_vpd */
+    const char * vend_prod;     /* sg_vpd */
+    sgj_state json_st;
+    uint8_t std_inq_a[36];
+};
+
+struct svpd_values_name_t {
+    int value;       /* VPD page number */
+    int subvalue;    /* to differentiate if value+pdt are not unique */
+    int pdt;         /* peripheral device type id, -1 is the default */
+                     /* (all or not applicable) value */
+    const char * acron;
+    const char * name;
+};
+
+struct svpd_vp_name_t {
+    int vend_prod_num;       /* vendor/product identifier */
+    const char * acron;
+    const char * name;
+};
+
+typedef int (*recurse_vpd_decodep)(struct opts_t *, sgj_opaque_p jop, int off);
+
+
+sgj_opaque_p sg_vpd_js_hdr(sgj_state * jsp, sgj_opaque_p jop,
+                           const char * name, const uint8_t * vpd_hdrp);
+void decode_net_man_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                        sgj_opaque_p jap);
+void decode_x_inq_vpd(const uint8_t * b, int len, bool protect,
+                      struct opts_t * op, sgj_opaque_p jop);
+void decode_softw_inf_id(const uint8_t * buff, int len, struct opts_t * op,
+                         sgj_opaque_p jap);
+void decode_mode_policy_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                            sgj_opaque_p jap);
+void decode_cga_profile_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                       sgj_opaque_p jap);
+void decode_power_condition(const uint8_t * buff, int len, struct opts_t * op,
+                            sgj_opaque_p jop);
+int filter_json_dev_ids(uint8_t * buff, int len, int m_assoc,
+                        struct opts_t * op, sgj_opaque_p jap);
+void decode_ata_info_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                        sgj_opaque_p jop);
+void decode_feature_sets_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                             sgj_opaque_p jap);
+void decode_dev_constit_vpd(const uint8_t * buff, int len,
+                            struct opts_t * op, sgj_opaque_p jap,
+                            recurse_vpd_decodep fp);
+sgj_opaque_p std_inq_decode_js(const uint8_t * b, int len,
+                               struct opts_t * op, sgj_opaque_p jop);
+void decode_power_consumption(const uint8_t * buff, int len,
+                              struct opts_t * op, sgj_opaque_p jap);
+void decode_block_limits_vpd(const uint8_t * buff, int len,
+                             struct opts_t * op, sgj_opaque_p jop);
+void decode_block_dev_ch_vpd(const uint8_t * buff, int len,
+                             struct opts_t * op, sgj_opaque_p jop);
+int decode_block_lb_prov_vpd(const uint8_t * buff, int len,
+                             struct opts_t * op, sgj_opaque_p jop);
+void decode_referrals_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                          sgj_opaque_p jop);
+void decode_sup_block_lens_vpd(const uint8_t * buff, int len,
+                               struct opts_t * op, sgj_opaque_p jap);
+void decode_block_dev_char_ext_vpd(const uint8_t * buff, int len,
+                                   struct opts_t * op, sgj_opaque_p jop);
+void decode_zbdch_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                      sgj_opaque_p jop);
+void decode_block_limits_ext_vpd(const uint8_t * buff, int len,
+                                 struct opts_t * op, sgj_opaque_p jop);
+void decode_format_presets_vpd(const uint8_t * buff, int len,
+                               struct opts_t * op, sgj_opaque_p jap);
+void decode_con_pos_range_vpd(const uint8_t * buff, int len,
+                              struct opts_t * op, sgj_opaque_p jap);
+void decode_3party_copy_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                            sgj_opaque_p jap);
+void
+decode_proto_lu_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                    sgj_opaque_p jap);
+void
+decode_proto_port_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                      sgj_opaque_p jap);
+void
+decode_lb_protection_vpd(const uint8_t * buff, int len, struct opts_t * op,
+                         sgj_opaque_p jap);
+void
+decode_tapealert_supported_vpd(const uint8_t * buff, int len,
+                               struct opts_t * op, sgj_opaque_p jop);
+/* Share some vendor specific VPD pages as well */
+void
+decode_upr_vpd_c0_emc(uint8_t * buff, int len, struct opts_t * op,
+                      sgj_opaque_p jop);
+void
+decode_rdac_vpd_c2(uint8_t * buff, int len, struct opts_t * op,
+                   sgj_opaque_p jop);
+void
+decode_rdac_vpd_c9(uint8_t * buff, int len, struct opts_t * op,
+                   sgj_opaque_p jop);
+
+const char * pqual_str(int pqual);
+int no_ascii_4hex(const struct opts_t * op);
+
+void svpd_enumerate_vendor(int vend_prod_num);
+int svpd_count_vendor_vpds(int vpd_pn, int vend_prod_num);
+int svpd_decode_vendor(int sg_fd, struct opts_t * op, sgj_opaque_p jop,
+                       int off);
+const struct svpd_values_name_t * svpd_find_vendor_by_acron(const char * ap);
+int svpd_find_vp_num_by_acron(const char * vp_ap);
+const struct svpd_values_name_t * svpd_find_vendor_by_num(int page_num,
+                                                          int vend_prod_num);
+int vpd_fetch_page(int sg_fd, uint8_t * rp, int page, int mxlen,
+                   bool qt, int vb, int * rlenp);
+void dup_sanity_chk(int sz_opts_t, int sz_values_name_t);
+
+extern uint8_t * rsp_buff;
+extern const char * t10_vendor_id_hr;
+extern const char * t10_vendor_id_js;
+extern const char * product_id_hr;
+extern const char * product_id_js;
+extern const char * product_rev_lev_hr;
+extern const char * product_rev_lev_js;
+extern struct svpd_vp_name_t vp_arr[];
+extern struct svpd_values_name_t vendor_vpd_pg[];
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* end of SG_VPD_H */
diff --git a/src/sg_vpd_vendor.c b/src/sg_vpd_vendor.c
new file mode 100644
index 0000000..156dd83
--- /dev/null
+++ b/src/sg_vpd_vendor.c
@@ -0,0 +1,1296 @@
+/*
+ * Copyright (c) 2006-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifndef SG_LIB_MINGW
+#include <time.h>
+#endif
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+#include "sg_vpd_common.h"
+
+/* This is a companion file to sg_vpd.c . It contains logic to output and
+   decode vendor specific VPD pages
+
+   This program fetches Vital Product Data (VPD) pages from the given
+   device and outputs it as directed. VPD pages are obtained via a
+   SCSI INQUIRY command. Most of the data in this program is obtained
+   from the SCSI SPC-4 document at https://www.t10.org .
+
+   Acknowledgments:
+      - Lars Marowsky-Bree <lmb at suse dot de> contributed Unit Path Report
+        VPD page decoding for EMC CLARiiON devices [20041016]
+      - Hannes Reinecke <hare at suse dot de> contributed RDAC vendor
+        specific VPD pages [20060421]
+      - Jonathan McDowell <noodles at hp dot com> contributed HP/3PAR InServ
+        VPD page [0xc0] containing volume information [20110922]
+
+*/
+
+/* vendor/product identifiers */
+#define VPD_VP_SEAGATE 0
+#define VPD_VP_RDAC 1
+#define VPD_VP_EMC 2
+#define VPD_VP_DDS 3
+#define VPD_VP_HP3PAR 4
+#define VPD_VP_IBM_LTO 5
+#define VPD_VP_HP_LTO 6
+#define VPD_VP_WDC_HITACHI 7
+#define VPD_VP_NVME 8
+#define VPD_VP_SG 9     /* this package/library as a vendor */
+
+
+/* vendor VPD pages */
+#define VPD_V_HIT_PG3 0x3
+#define VPD_V_HP3PAR 0xc0
+#define VPD_V_FIRM_SEA  0xc0
+#define VPD_V_UPR_EMC  0xc0
+#define VPD_V_HVER_RDAC  0xc0
+#define VPD_V_FVER_DDS 0xc0
+#define VPD_V_FVER_LTO 0xc0
+#define VPD_V_DCRL_LTO 0xc0
+#define VPD_V_DATC_SEA  0xc1
+#define VPD_V_FVER_RDAC  0xc1
+#define VPD_V_HVER_LTO 0xc1
+#define VPD_V_DSN_LTO 0xc1
+#define VPD_V_JUMP_SEA 0xc2
+#define VPD_V_SVER_RDAC 0xc2
+#define VPD_V_PCA_LTO 0xc2
+#define VPD_V_DEV_BEH_SEA 0xc3
+#define VPD_V_FEAT_RDAC 0xc3
+#define VPD_V_MECH_LTO 0xc3
+#define VPD_V_SUBS_RDAC 0xc4
+#define VPD_V_HEAD_LTO 0xc4
+#define VPD_V_ACI_LTO 0xc5
+#define VPD_V_DUCD_LTO 0xc7
+#define VPD_V_EDID_RDAC 0xc8
+#define VPD_V_MPDS_LTO 0xc8
+#define VPD_V_VAC_RDAC 0xc9
+#define VPD_V_RVSI_RDAC 0xca
+#define VPD_V_SAID_RDAC 0xd0
+#define VPD_V_HIT_PG_D1 0xd1
+#define VPD_V_HIT_PG_D2 0xd2
+
+
+#define DEF_ALLOC_LEN 252
+#define MX_ALLOC_LEN (0xc000 + 0x80)
+
+void
+dup_sanity_chk(int sz_opts_t, int sz_values_name_t)
+{
+    const size_t my_sz_opts_t = sizeof(struct opts_t);
+    const size_t my_sz_values_name_t = sizeof(struct svpd_values_name_t);
+
+    if (sz_opts_t != (int)my_sz_opts_t)
+        pr2serr(">>> struct opts_t differs in size from sg_vpd.c [%d != "
+                "%d]\n", (int)my_sz_opts_t, sz_opts_t);
+    if (sz_values_name_t != (int)my_sz_values_name_t)
+        pr2serr(">>> struct svpd_values_name_t differs in size from "
+                "sg_vpd.c [%d != %d]\n", (int)my_sz_values_name_t,
+                sz_values_name_t);
+}
+
+static bool
+is_like_pdt(int actual_pdt, const struct svpd_values_name_t * vnp)
+{
+    if (actual_pdt == vnp->pdt)
+        return true;
+    if (PDT_DISK == vnp->pdt) {
+        switch (actual_pdt) {
+        case PDT_DISK:
+        case PDT_RBC:
+        case PDT_PROCESSOR:
+        case PDT_SAC:
+        case PDT_ZBC:
+            return true;
+        default:
+            return false;
+        }
+    } else if (PDT_TAPE == vnp->pdt) {
+        switch (actual_pdt) {
+        case PDT_TAPE:
+        case PDT_MCHANGER:
+        case PDT_ADC:
+            return true;
+        default:
+            return false;
+        }
+    } else
+        return false;
+}
+
+static const struct svpd_values_name_t *
+svpd_get_v_detail(int page_num, int vend_prod_num, int pdt)
+{
+    const struct svpd_values_name_t * vnp;
+    int vp, ty;
+
+    vp = (vend_prod_num < 0) ? 1 : 0;
+    ty = (pdt < 0) ? 1 : 0;
+    for (vnp = vendor_vpd_pg; vnp->acron; ++vnp) {
+        if ((page_num == vnp->value) &&
+            (vp || (vend_prod_num == vnp->subvalue)) &&
+            (ty || is_like_pdt(pdt, vnp)))
+            return vnp;
+    }
+#if 0
+    if (! ty)
+        return svpd_get_v_detail(page_num, vend_prod_num, -1);
+    if (! vp)
+        return svpd_get_v_detail(page_num, -1, pdt);
+#endif
+    return NULL;
+}
+
+const struct svpd_values_name_t *
+svpd_find_vendor_by_num(int page_num, int vend_prod_num)
+{
+    const struct svpd_values_name_t * vnp;
+
+    for (vnp = vendor_vpd_pg; vnp->acron; ++vnp) {
+        if ((page_num == vnp->value) &&
+            ((vend_prod_num < 0) || (vend_prod_num == vnp->subvalue)))
+            return vnp;
+    }
+    return NULL;
+}
+
+const struct svpd_values_name_t *
+svpd_find_vendor_by_acron(const char * ap)
+{
+    const struct svpd_values_name_t * vnp;
+
+    for (vnp = vendor_vpd_pg; vnp->acron; ++vnp) {
+        if (0 == strcmp(vnp->acron, ap))
+            return vnp;
+    }
+    return NULL;
+}
+
+int
+svpd_count_vendor_vpds(int vpd_pn, int vend_prod_num)
+{
+    const struct svpd_values_name_t * vnp;
+    int matches;
+
+    for (vnp = vendor_vpd_pg, matches = 0; vnp->acron; ++vnp) {
+        if ((vpd_pn == vnp->value) && vnp->name) {
+            if ((vend_prod_num < 0) || (vend_prod_num == vnp->subvalue)) {
+                if (0 == matches)
+                    printf("Matching vendor specific VPD pages:\n");
+                ++matches;
+                printf("  %-10s 0x%02x,%d      %s\n", vnp->acron,
+                       vnp->value, vnp->subvalue, vnp->name);
+            }
+        }
+    }
+    return matches;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+static void
+decode_vpd_c0_hp3par(uint8_t * buff, int len)
+{
+    int rev;
+    long offset;
+
+    if (len < 24) {
+        pr2serr("HP/3PAR vendor specific VPD page length too short=%d\n",
+                len);
+        return;
+    }
+
+    rev = buff[4];
+    printf("  Page revision: %d\n", rev);
+
+    printf("  Volume type: %s\n", (buff[5] & 0x01) ? "tpvv" :
+            (buff[5] & 0x02) ? "snap" : "base");
+    printf("  Reclaim supported: %s\n", (buff[5] & 0x04) ? "yes" : "no");
+    printf("  ATS supported: %s\n", (buff[5] & 0x10) ? "yes" : "no");
+    printf("  XCopy supported: %s\n", (buff[5] & 0x20) ? "yes" : "no");
+
+    if (rev > 3) {
+        printf("  VV ID: %" PRIu64 "\n", sg_get_unaligned_be64(buff + 28));
+        offset = 44;
+        printf("  Volume name: %s\n", &buff[offset]);
+
+        printf("  Domain ID: %d\n", sg_get_unaligned_be32(buff + 36));
+
+        offset += sg_get_unaligned_be32(buff + offset - 4) + 4;
+        printf("  Domain Name: %s\n", &buff[offset]);
+
+        offset += sg_get_unaligned_be32(buff + offset - 4) + 4;
+        printf("  User CPG: %s\n", &buff[offset]);
+
+        offset += sg_get_unaligned_be32(buff + offset - 4) + 4;
+        printf("  Snap CPG: %s\n", &buff[offset]);
+
+        offset += sg_get_unaligned_be32(buff + offset - 4);
+
+        printf("  VV policies: %s,%s,%s,%s\n",
+                (buff[offset + 3] & 0x01) ? "stale_ss" : "no_stale_ss",
+                (buff[offset + 3] & 0x02) ? "one_host" : "no_one_host",
+                (buff[offset + 3] & 0x04) ? "tp_bzero" : "no_tp_bzero",
+                (buff[offset + 3] & 0x08) ? "zero_detect" : "no_zero_detect");
+
+    }
+
+    if (buff[5] & 0x04) {
+        printf("  Allocation unit: %d\n", sg_get_unaligned_be32(buff + 8));
+
+        printf("  Data pool size: %" PRIu64 "\n",
+               sg_get_unaligned_be64(buff + 12));
+
+        printf("  Space allocated: %" PRIu64 "\n",
+               sg_get_unaligned_be64(buff + 20));
+    }
+    return;
+}
+
+
+static void
+decode_firm_vpd_c0_sea(uint8_t * buff, int len)
+{
+    if (len < 28) {
+        pr2serr("Seagate firmware numbers VPD page length too short=%d\n",
+                len);
+        return;
+    }
+    if (28 == len) {
+        printf("  SCSI firmware release number: %.8s\n", buff + 4);
+        printf("  Servo ROM release number: %.8s\n", buff + 20);
+    } else {
+        printf("  SCSI firmware release number: %.8s\n", buff + 4);
+        printf("  Servo ROM release number: %.8s\n", buff + 12);
+        printf("  SAP block point numbers (major/minor): %.8s\n", buff + 20);
+        if (len < 36)
+            return;
+        printf("  Servo firmware release date: %.4s\n", buff + 28);
+        printf("  Servo ROM release date: %.4s\n", buff + 32);
+        if (len < 44)
+            return;
+        printf("  SAP firmware release number: %.8s\n", buff + 36);
+        if (len < 52)
+            return;
+        printf("  SAP firmware release date: %.4s\n", buff + 44);
+        printf("  SAP firmware release year: %.4s\n", buff + 48);
+        if (len < 60)
+            return;
+        printf("  SAP manufacturing key: %.4s\n", buff + 52);
+        printf("  Servo firmware product family and product family "
+               "member: %.4s\n", buff + 56);
+    }
+}
+
+static void
+decode_date_code_vpd_c1_sea(uint8_t * buff, int len)
+{
+    if (len < 20) {
+        pr2serr("Seagate Data code VPD page length too short=%d\n",
+                len);
+        return;
+    }
+    printf("  ETF log (mmddyyyy): %.8s\n", buff + 4);
+    printf("  Compile date code (mmddyyyy): %.8s\n", buff + 12);
+}
+
+static void
+decode_dev_beh_vpd_c3_sea(uint8_t * buff, int len)
+{
+    if (len < 25) {
+        pr2serr("Seagate Device behaviour VPD page length too short=%d\n",
+                len);
+        return;
+    }
+    printf("  Version number: %d\n", buff[4]);
+    printf("  Behaviour code: %d\n", buff[5]);
+    printf("  Behaviour code version number: %d\n", buff[6]);
+    printf("  ASCII family number: %.16s\n", buff + 7);
+    printf("  Number of interleaves: %d\n", buff[23]);
+    printf("  Default number of cache segments: %d\n", buff[24]);
+}
+
+static void
+decode_rdac_vpd_c0(uint8_t * buff, int len)
+{
+    int memsize;
+    char name[65];
+
+    if (len < 3) {
+        pr2serr("Hardware Version VPD page length too short=%d\n", len);
+        return;
+    }
+    if (buff[4] != 'h' && buff[5] != 'w' && buff[6] != 'r') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding not possible.\n",
+                buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    printf("  Number of channels: %x\n", buff[8]);
+    memsize = sg_get_unaligned_be16(buff + 10);
+    printf("  Processor Memory Size: %d\n", memsize);
+    memset(name, 0, 65);
+    memcpy(name, buff + 16, 64);
+    printf("  Board Name: %s\n", name);
+    memset(name, 0, 65);
+    memcpy(name, buff + 80, 16);
+    printf("  Board Part Number: %s\n", name);
+    memset(name, 0, 65);
+    memcpy(name, buff + 96, 12);
+    printf("  Schematic Number: %s\n", name);
+    memset(name, 0, 65);
+    memcpy(name, buff + 108, 4);
+    printf("  Schematic Revision Number: %s\n", name);
+    memset(name, 0, 65);
+    memcpy(name, buff + 112, 16);
+    printf("  Board Serial Number: %s\n", name);
+    memset(name, 0, 65);
+    memcpy(name, buff + 144, 8);
+    printf("  Date of Manufacture: %s\n", name);
+    memset(name, 0, 65);
+    memcpy(name, buff + 152, 2);
+    printf("  Board Revision: %s\n", name);
+    memset(name, 0, 65);
+    memcpy(name, buff + 154, 4);
+    printf("  Board Identifier: %s\n", name);
+
+    return;
+}
+
+static void
+decode_rdac_vpd_c1(uint8_t * buff, int len)
+{
+    int i, n, v, r, m, p, d, y, num_part;
+    char part[5];
+
+    if (len < 3) {
+        pr2serr("Firmware Version VPD page length too short=%d\n", len);
+        return;
+    }
+    if (buff[4] != 'f' && buff[5] != 'w' && buff[6] != 'r') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding not possible.\n",
+                buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    printf("  Firmware Version: %02x.%02x.%02x\n", buff[8], buff[9], buff[10]);
+    printf("  Firmware Date: %02d/%02d/%02d\n", buff[11], buff[12], buff[13]);
+
+    num_part = (len - 12) / 16;
+    n = 16;
+    printf("  Partitions: %d\n", num_part);
+    for (i = 0; i < num_part; i++) {
+        memset(part,0, 5);
+        memcpy(part, &buff[n], 4);
+        printf("    Name: %s\n", part);
+        n += 4;
+        v = buff[n++];
+        r = buff[n++];
+        m = buff[n++];
+        p = buff[n++];
+        printf("    Version: %d.%d.%d.%d\n", v, r, m, p);
+        m = buff[n++];
+        d = buff[n++];
+        y = buff[n++];
+        printf("    Date: %d/%d/%d\n", m, d, y);
+
+        n += 5;
+    }
+
+    return;
+}
+
+static void
+decode_rdac_vpd_c3(uint8_t * buff, int len)
+{
+    if (len < 0x2c) {
+        pr2serr("Feature parameters VPD page length too short=%d\n", len);
+        return;
+    }
+    if (buff[4] != 'p' && buff[5] != 'r' && buff[6] != 'm') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding not possible.\n",
+                buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    printf("  Maximum number of drives per LUN: %d\n", buff[8]);
+    printf("  Maximum number of hot spare drives: %d\n", buff[9]);
+    printf("  UTM: %s\n", buff[11] & 0x80?"enabled":"disabled");
+    if ((buff[11] & 0x80))
+        printf("    UTM LUN: %02x\n", buff[11] & 0x7f);
+    printf("  Persistent Reservations Bus Reset Support: %s\n",
+           (buff[12] & 0x01) ? "enabled" : "disabled");
+    return;
+}
+
+static void
+decode_rdac_vpd_c4(uint8_t * buff, int len)
+{
+    char subsystem_id[17];
+    char subsystem_rev[5];
+    char slot_id[3];
+
+    if (len < 0x1c) {
+        pr2serr("Subsystem identifier VPD page length too short=%d\n", len);
+        return;
+    }
+    if (buff[4] != 's' && buff[5] != 'u' && buff[6] != 'b') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding not possible.\n",
+                buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    memset(subsystem_id, 0, 17);
+    memcpy(subsystem_id, &buff[8], 16);
+    memset(subsystem_rev, 0, 5);
+    memcpy(subsystem_rev, &buff[24], 4);
+    slot_id[0] = buff[28];
+    slot_id[1] = buff[29];
+    slot_id[2] = 0;
+
+    printf("  Subsystem ID: %s\n  Subsystem Revision: %s",
+           subsystem_id, subsystem_rev);
+    if (!strcmp(subsystem_rev, "10.0"))
+        printf(" (Board ID 4884)\n");
+    else if (!strcmp(subsystem_rev, "12.0"))
+        printf(" (Board ID 5884)\n");
+    else if (!strcmp(subsystem_rev, "13.0"))
+        printf(" (Board ID 2882)\n");
+    else if (!strcmp(subsystem_rev, "13.1"))
+        printf(" (Board ID 2880)\n");
+    else if (!strcmp(subsystem_rev, "14.0"))
+        printf(" (Board ID 2822)\n");
+    else if (!strcmp(subsystem_rev, "15.0"))
+        printf(" (Board ID 6091)\n");
+    else if (!strcmp(subsystem_rev, "16.0"))
+        printf(" (Board ID 3992)\n");
+    else if (!strcmp(subsystem_rev, "16.1"))
+        printf(" (Board ID 3991)\n");
+    else if (!strcmp(subsystem_rev, "17.0"))
+        printf(" (Board ID 1331)\n");
+    else if (!strcmp(subsystem_rev, "17.1"))
+        printf(" (Board ID 1332)\n");
+    else if (!strcmp(subsystem_rev, "17.3"))
+        printf(" (Board ID 1532)\n");
+    else if (!strcmp(subsystem_rev, "17.4"))
+        printf(" (Board ID 1932)\n");
+    else if (!strcmp(subsystem_rev, "42.0"))
+        printf(" (Board ID 26x0)\n");
+    else if (!strcmp(subsystem_rev, "43.0"))
+        printf(" (Board ID 498x)\n");
+    else if (!strcmp(subsystem_rev, "44.0"))
+        printf(" (Board ID 548x)\n");
+    else if (!strcmp(subsystem_rev, "45.0"))
+        printf(" (Board ID 5501)\n");
+    else if (!strcmp(subsystem_rev, "46.0"))
+        printf(" (Board ID 2701)\n");
+    else if (!strcmp(subsystem_rev, "47.0"))
+        printf(" (Board ID 5601)\n");
+    else
+        printf(" (Board ID unknown)\n");
+
+    printf("  Slot ID: %s\n", slot_id);
+
+    return;
+}
+
+static void
+convert_binary_to_ascii(uint8_t * src, uint8_t * dst,  int len)
+{
+    int i;
+
+    for (i = 0; i < len; i++) {
+        sprintf((char *)(dst+2*i), "%02x", *(src+i));
+    }
+}
+
+static void
+decode_rdac_vpd_c8(uint8_t * buff, int len)
+{
+    int i;
+#ifndef SG_LIB_MINGW
+    time_t tstamp;
+#endif
+    char *c;
+    char label[61];
+    int label_len;
+    char uuid[33];
+    int uuid_len;
+    uint8_t port_id[128];
+    int n;
+
+    if (len < 0xab) {
+        pr2serr("Extended Device Identification VPD page length too "
+                "short=%d\n", len);
+        return;
+    }
+    if (buff[4] != 'e' && buff[5] != 'd' && buff[6] != 'i') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding not possible.\n",
+                buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+
+    uuid_len = buff[11];
+
+    for (i = 0, c = uuid; i < uuid_len; i++) {
+        sprintf(c,"%02x",buff[12 + i]);
+        c += 2;
+    }
+
+    printf("  Volume Unique Identifier: %s\n", uuid);
+#ifndef SG_LIB_MINGW
+    tstamp = sg_get_unaligned_be32(buff + 24);
+    printf("    Creation Number: %d, Timestamp: %s",
+           sg_get_unaligned_be16(buff + 22), ctime(&tstamp));
+#else
+    printf("    Creation Number: %d, Timestamp value: %u",
+           sg_get_unaligned_be16(buff + 22),
+           sg_get_unaligned_be32(buff + 24));
+#endif
+    memset(label, 0, 61);
+    label_len = buff[28];
+    for(i = 0; i < (label_len - 1); ++i)
+        *(label + i) = buff[29 + (2 * i) + 1];
+    printf("  Volume User Label: %s\n", label);
+
+    uuid_len = buff[89];
+
+    for (i = 0, c = uuid; i < uuid_len; i++) {
+        sprintf(c,"%02x",buff[90 + i]);
+        c += 2;
+    }
+
+    printf("  Storage Array Unique Identifier: %s\n", uuid);
+    memset(label, 0, 61);
+    label_len = buff[106];
+    for(i = 0; i < (label_len - 1); ++i)
+        *(label + i) = buff[107 + (2 * i) + 1];
+    printf("  Storage Array User Label: %s\n", label);
+
+    for (i = 0, c = uuid; i < 8; i++) {
+        sprintf(c,"%02x",buff[167 + i]);
+        c += 2;
+    }
+
+    printf("  Logical Unit Number: %s\n", uuid);
+
+    /* Initiator transport ID */
+    if ( buff[10] & 0x01 ) {
+        memset(port_id, 0, 128);
+        printf("  Transport Protocol: ");
+        switch (buff[175] & 0x0F) {
+        case TPROTO_FCP: /* FC */
+            printf("FC\n");
+            convert_binary_to_ascii(&buff[183], port_id, 8);
+            n = 199;
+            break;
+        case TPROTO_SRP: /* SRP */
+            printf("SRP\n");
+            convert_binary_to_ascii(&buff[183], port_id, 8);
+            n = 199;
+            break;
+        case TPROTO_ISCSI: /* iSCSI */
+            printf("iSCSI\n");
+            n = sg_get_unaligned_be32(buff + 177);
+            memcpy(port_id, &buff[179], n);
+            n = 179 + n;
+            break;
+        case TPROTO_SAS: /* SAS */
+            printf("SAS\n");
+            convert_binary_to_ascii(&buff[179], port_id, 8);
+            n = 199;
+            break;
+        default:
+            return; /* Can't continue decoding, so return */
+        }
+
+        printf("  Initiator Port Identifier: %s\n", port_id);
+        if ( buff[10] & 0x02 ) {
+            memset(port_id, 0, 128);
+            memcpy(port_id, &buff[n], 8);
+            printf("  Supplemental Vendor ID: %s\n", port_id);
+        }
+    }
+
+    return;
+}
+
+#if 0
+static void
+decode_rdac_vpd_c9_rtpg_data(uint8_t aas, uint8_t vendor)
+{
+    printf("  Asymmetric Access State:");
+    switch(aas & 0x0F) {
+    case 0x0:
+        printf(" Active/Optimized");
+        break;
+    case 0x1:
+        printf(" Active/Non-Optimized");
+        break;
+    case 0x2:
+        printf(" Standby");
+        break;
+    case 0x3:
+        printf(" Unavailable");
+        break;
+    case 0xE:
+        printf(" Offline");
+        break;
+    case 0xF:
+        printf(" Transitioning");
+        break;
+    default:
+        printf(" (unknown)");
+        break;
+    }
+    printf("\n");
+
+    printf("  Vendor Specific Field:");
+    switch(vendor) {
+    case 0x01:
+        printf(" Operating normally");
+        break;
+    case 0x02:
+        printf(" Non-responsive to queries");
+        break;
+    case 0x03:
+        printf(" Controller being held in reset");
+        break;
+    case 0x04:
+        printf(" Performing controller firmware download (1st controller)");
+        break;
+    case 0x05:
+        printf(" Performing controller firmware download (2nd controller)");
+        break;
+    case 0x06:
+        printf(" Quiesced as a result of an administrative request");
+        break;
+    case 0x07:
+        printf(" Service mode as a result of an administrative request");
+        break;
+    case 0xFF:
+        printf(" Details are not available");
+        break;
+    default:
+        printf(" (unknown)");
+        break;
+    }
+    printf("\n");
+}
+
+static void
+decode_rdac_vpd_c9(uint8_t * buff, int len)
+{
+    if (len < 3) {
+        pr2serr("Volume Access Control VPD page length too short=%d\n", len);
+        return;
+    }
+    if (buff[4] != 'v' && buff[5] != 'a' && buff[6] != 'c') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding not possible.\n",
+                buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    if (buff[7] != '1') {
+        pr2serr("Invalid page version '%c' (should be 1)\n", buff[7]);
+    }
+    if ( (buff[8] & 0xE0) == 0xE0 ) {
+        printf("  IOShipping (ALUA): Enabled\n");
+    } else {
+        printf("  AVT:");
+        if (buff[8] & 0x80) {
+            printf(" Enabled");
+            if (buff[8] & 0x40)
+                printf(" (Allow reads on sector 0)");
+            printf("\n");
+        } else {
+            printf(" Disabled\n");
+        }
+    }
+    printf("  Volume Access via: ");
+    if (buff[8] & 0x01)
+        printf("primary controller\n");
+    else
+        printf("alternate controller\n");
+
+    if (buff[8] & 0x08) {
+        printf("  Path priority: %d ", buff[15] & 0xf);
+        switch(buff[15] & 0xf) {
+        case 0x1:
+            printf("(preferred path)\n");
+            break;
+        case 0x2:
+            printf("(secondary path)\n");
+            break;
+        default:
+            printf("(unknown)\n");
+            break;
+        }
+
+        printf("  Preferred Path Auto Changeable:");
+        switch(buff[14] & 0x3C) {
+        case 0x14:
+            printf(" No (User Disabled and Host Type Restricted)\n");
+            break;
+        case 0x18:
+            printf(" No (User Disabled)\n");
+            break;
+        case 0x24:
+            printf(" No (Host Type Restricted)\n");
+            break;
+        case 0x28:
+            printf(" Yes\n");
+            break;
+        default:
+            printf(" (Unknown)\n");
+            break;
+        }
+
+        printf("  Implicit Failback:");
+        switch(buff[14] & 0x03) {
+        case 0x1:
+            printf(" Disabled\n");
+            break;
+        case 0x2:
+            printf(" Enabled\n");
+            break;
+        default:
+            printf(" (Unknown)\n");
+            break;
+        }
+    } else {
+        printf("  Path priority: %d ", buff[9] & 0xf);
+        switch(buff[9] & 0xf) {
+        case 0x1:
+            printf("(preferred path)\n");
+            break;
+        case 0x2:
+            printf("(secondary path)\n");
+            break;
+        default:
+            printf("(unknown)\n");
+            break;
+        }
+    }
+
+
+    if (buff[8] & 0x80) {
+        printf(" Target Port Group Data (This controller):\n");
+        decode_rdac_vpd_c9_rtpg_data(buff[10], buff[11]);
+
+        printf(" Target Port Group Data (Alternate controller):\n");
+        decode_rdac_vpd_c9_rtpg_data(buff[12], buff[13]);
+    }
+}
+#endif
+
+static void
+decode_rdac_vpd_ca(uint8_t * buff, int len)
+{
+    int i;
+
+    if (len < 16) {
+        pr2serr("Replicated Volume Source Identifier VPD page length too "
+                "short=%d\n", len);
+        return;
+    }
+    if (buff[4] != 'r' && buff[5] != 'v' && buff[6] != 's') {
+        pr2serr("Invalid page identifier %c%c%c%c, decoding not possible.\n",
+                buff[4], buff[5], buff[6], buff[7]);
+        return;
+    }
+    if (buff[8] & 0x01) {
+        printf("  Snapshot Volume\n");
+        printf("  Base Volume WWID: ");
+        for (i = 0; i < 16; i++)
+            printf("%02x", buff[10 + i]);
+        printf("\n");
+    } else if (buff[8] & 0x02) {
+        printf("  Copy Target Volume\n");
+        printf("  Source Volume WWID: ");
+        for (i = 0; i < 16; i++)
+            printf("%02x", buff[10 + i]);
+        printf("\n");
+    } else
+        printf(" Neither a snapshot nor a copy target volume\n");
+
+    return;
+}
+
+static void
+decode_rdac_vpd_d0(uint8_t * buff, int len)
+{
+    int i;
+
+    if (len < 20) {
+        pr2serr("Storage Array World Wide Name VPD page length too "
+                "short=%d\n", len);
+        return;
+    }
+    printf("  Storage Array WWN: ");
+    for (i = 0; i < 16; i++)
+        printf("%02x", buff[8 + i]);
+    printf("\n");
+
+    return;
+}
+
+
+static void
+decode_dds_vpd_c0(uint8_t * buff, int len)
+{
+    char firmware_rev[25];
+    char build_date[43];
+    char hw_conf[21];
+    char fw_conf[21];
+
+    if (len < 0xb3) {
+        pr2serr("Vendor-Unique Firmware revision page invalid length=%d\n",
+                len);
+        return;
+    }
+    memset(firmware_rev, 0x0, 25);
+    memcpy(firmware_rev, &buff[5], 24);
+
+    printf("  %s\n", firmware_rev);
+
+    memset(build_date, 0x0, 43);
+    memcpy(build_date, &buff[30], 42);
+
+    printf("  %s\n", build_date);
+
+    memset(hw_conf, 0x0, 21);
+    memcpy(hw_conf, &buff[73], 20);
+    printf("  %s\n", hw_conf);
+
+    memset(fw_conf, 0x0, 21);
+    memcpy(fw_conf, &buff[94], 20);
+    printf("  %s\n", fw_conf);
+    return;
+}
+
+static void
+decode_hp_lto_vpd_cx(uint8_t * buff, int len, int page)
+{
+    char str[32];
+    const char *comp = NULL;
+
+    if (len < 0x5c) {
+        pr2serr("Driver Component Revision Levels page invalid length=%d\n",
+                len);
+        return;
+    }
+    switch (page) {
+        case 0xc0:
+            comp = "Firmware";
+            break;
+        case 0xc1:
+            comp = "Hardware";
+            break;
+        case 0xc2:
+            comp = "PCA";
+            break;
+        case 0xc3:
+            comp = "Mechanism";
+            break;
+        case 0xc4:
+            comp = "Head Assy";
+            break;
+        case 0xc5:
+            comp = "ACI";
+            break;
+    }
+    if (!comp) {
+        pr2serr("Driver Component Revision Level invalid page=0x%02x\n",
+                page);
+        return;
+    }
+
+    memset(str, 0x0, 32);
+    memcpy(str, &buff[4], 26);
+    printf("  %s\n", str);
+
+    memset(str, 0x0, 32);
+    memcpy(str, &buff[30], 19);
+    printf("  %s\n", str);
+
+    memset(str, 0x0, 32);
+    memcpy(str, &buff[49], 24);
+    printf("  %s\n", str);
+
+    memset(str, 0x0, 32);
+    memcpy(str, &buff[73], 23);
+    printf("  %s\n", str);
+    return;
+}
+
+static void
+decode_ibm_lto_dcrl(uint8_t * buff, int len)
+{
+    if (len < 0x2b) {
+        pr2serr("Driver Component Revision Levels page (IBM LTO) invalid "
+                "length=%d\n", len);
+        return;
+    }
+    printf("  Code name: %.12s\n", buff + 4);
+    printf("  Time (hhmmss): %.7s\n", buff + 16);
+    printf("  Date (yyyymmdd): %.8s\n", buff + 23);
+    printf("  Platform: %.12s\n", buff + 31);
+}
+
+static void
+decode_ibm_lto_dsn(uint8_t * buff, int len)
+{
+    if (len < 0x1c) {
+        pr2serr("Driver Serial Numbers page (IBM LTO) invalid "
+                "length=%d\n", len);
+        return;
+    }
+    printf("  Manufacturing serial number: %.12s\n", buff + 4);
+    printf("  Reported serial number: %.12s\n", buff + 16);
+}
+
+static void
+decode_vpd_3_hit(uint8_t * b, int blen)
+{
+    uint16_t plen = sg_get_unaligned_be16(b + 2);
+
+    if ((plen < 184) || (blen < 184)) {
+        pr2serr("Hitachi VPD page 0x3 length (%u) shorter than %u\n",
+                plen + 4, 184 + 4);
+        return;
+    }
+    printf("  ASCII uCode Identifier: %.12s\n", b + 24);
+    printf("  ASCII servo P/N: %.4s\n", b + 36);
+    printf("  Major Version: %.2s\n", b + 40);
+    printf("  Minor Version: %.2s\n", b + 42);
+    printf("  User Count: %.4s\n", b + 44);
+    printf("  Build Number: %.4s\n", b + 48);
+    printf("  Build Date String: %.32s\n", b + 52);
+    printf("  Product ID: %.8s\n", b + 84);
+    printf("  Interface ID: %.8s\n", b + 92);
+    printf("  Code Type: %.8s\n", b + 100);
+    printf("  User Name: %.12s\n", b + 108);
+    printf("  Machine Name: %.16s\n", b + 120);
+    printf("  Directory Name: %.32s\n", b + 136);
+    printf("  Operating state: %u\n", sg_get_unaligned_be32(b + 168));
+    printf("  Functional Mode: %u\n", sg_get_unaligned_be32(b + 172));
+    printf("  Degraded Reason: %u\n", sg_get_unaligned_be32(b + 176));
+    printf("  Broken Reason: %u\n", sg_get_unaligned_be32(b + 180));
+    printf("  Code Mode: %u\n", sg_get_unaligned_be32(b + 184));
+    printf("  Revision: %.4s\n", b + 188);
+}
+
+static void
+decode_vpd_d1_hit(uint8_t * b, int blen)
+{
+    uint16_t plen = sg_get_unaligned_be16(b + 2);
+
+    if ((plen < 80) || (blen < 80)) {
+        pr2serr("Hitachi VPD page 0xd1 length (%u) shorter than %u\n",
+                plen + 4, 80 + 4);
+        return;
+    }
+    printf("  ASCII Media Disk Definition: %.16s\n", b + 4);
+    printf("  ASCII Motor Serial Number: %.16s\n", b + 20);
+    printf("  ASCII Flex Assembly Serial Number: %.16s\n", b + 36);
+    printf("  ASCII Actuator Serial Number: %.16s\n", b + 52);
+    printf("  ASCII Device Enclosure Serial Number: %.16s\n", b + 68);
+}
+
+static void
+decode_vpd_d2_hit(uint8_t * b, int blen)
+{
+    uint16_t plen = sg_get_unaligned_be16(b + 2);
+
+    if ((plen < 52) || (blen < 52)) {
+        pr2serr("Hitachi VPD page 0xd2 length (%u) shorter than %u\n",
+                plen + 4, 52 + 4);
+        return;
+    }
+    if ((blen - 4) == 120) {
+        printf("  HDC Version: %.*s\n", b[4], b + 5);
+        printf("  Card Serial Number: %.*s\n", b[24], b + 25);
+        printf("  NAND Flash Version: %.*s\n", b[44], b + 45);
+        printf("  Card Assembly Part Number: %.*s\n", b[64], b + 65);
+        printf("  Second Card Serial Number: %.*s\n", b[84], b + 85);
+        printf("  Second Card Assembly Part Number: %.*s\n", b[104], b + 105);
+    } else {
+        printf("  ASCII HDC Version: %.16s\n", b + 5);
+        printf("  ASCII Card Serial Number: %.16s\n", b + 22);
+        printf("  ASCII Card Assembly Part Number: %.16s\n", b + 39);
+    }
+}
+
+/* Returns 0 if successful, see sg_ll_inquiry() plus SG_LIB_CAT_OTHER for
+   unsupported page */
+int
+svpd_decode_vendor(int sg_fd, struct opts_t * op, sgj_opaque_p jop, int off)
+{
+    bool hex0 = (0 == op->do_hex);
+    bool as_json;
+    int len, pdt, plen, pn;
+    int alloc_len = op->maxlen;
+    int res = 0;
+    const struct svpd_values_name_t * vnp;
+    sgj_state * jsp = &op->json_st;
+    sgj_opaque_p jo2p = NULL;
+    uint8_t * rp;
+    char name[80];
+
+    as_json = jsp->pr_as_json;
+    pn = op->vpd_pn;
+    switch (pn) {       /* VPD codes that we support vendor pages for */
+    case 0x3:
+    case 0xc0:
+    case 0xc1:
+    case 0xc2:
+    case 0xc3:
+    case 0xc4:
+    case 0xc5:
+    case 0xc8:
+    case 0xc9:
+    case 0xca:
+    case 0xd0:
+    case 0xd1:
+    case 0xd2:
+    case 0xde:
+        break;
+    default:    /* not known so return prior to fetching page */
+        return SG_LIB_CAT_OTHER;
+    }
+    rp = rsp_buff + off;
+    if (sg_fd >= 0) {
+        if (0 == alloc_len)
+            alloc_len = DEF_ALLOC_LEN;
+    }
+    res = vpd_fetch_page(sg_fd, rp, pn, alloc_len, op->do_quiet, op->verbose,
+                         &len);
+    if (res) {
+        pr2serr("Vendor VPD page=0x%x  failed to fetch\n", pn);
+        return res;
+    }
+    pdt = rp[0] & PDT_MASK;
+    vnp = svpd_get_v_detail(pn, op->vend_prod_num, pdt);
+    if (vnp && vnp->name)
+        snprintf(name, sizeof(name), "%s", vnp->name);
+    else
+        snprintf(name, sizeof(name) - 1, "Vendor VPD page=0x%x", pn);
+    if ((! op->do_raw) && (! op->do_quiet) && (op->do_hex < 3))
+        sgj_pr_hr(jsp, "%s VPD Page:\n", name);
+    if (op->do_raw)
+        dStrRaw(rp, len);
+    else {
+        switch(pn) {
+        case 0x3:
+            if (hex0 && (VPD_VP_WDC_HITACHI == op->vend_prod_num))
+                decode_vpd_3_hit(rp, len);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xc0:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_SEAGATE == op->vend_prod_num)
+                decode_firm_vpd_c0_sea(rp, len);
+            else if (VPD_VP_EMC == op->vend_prod_num) {
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop,
+                                         "Unit serial number VPD page", rp);
+                decode_upr_vpd_c0_emc(rp, len, op, jo2p);
+            } else if (VPD_VP_HP3PAR == op->vend_prod_num)
+                decode_vpd_c0_hp3par(rp, len);
+            else if (VPD_VP_RDAC == op->vend_prod_num)
+                decode_rdac_vpd_c0(rp, len);
+            else if (VPD_VP_DDS == op->vend_prod_num)
+                decode_dds_vpd_c0(rp, len);
+            else if (VPD_VP_IBM_LTO == op->vend_prod_num)
+                decode_ibm_lto_dcrl(rp, len);
+            else if (VPD_VP_HP_LTO == op->vend_prod_num)
+                decode_hp_lto_vpd_cx(rp, len, pn);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xc1:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_SEAGATE == op->vend_prod_num)
+                decode_date_code_vpd_c1_sea(rp, len);
+            else if (VPD_VP_RDAC == op->vend_prod_num)
+                decode_rdac_vpd_c1(rp, len);
+            else if (VPD_VP_IBM_LTO == op->vend_prod_num)
+                decode_ibm_lto_dsn(rp, len);
+            else if (VPD_VP_HP_LTO == op->vend_prod_num)
+                decode_hp_lto_vpd_cx(rp, len, pn);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xc2:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_RDAC == op->vend_prod_num) {
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop,
+                                         "Software version VPD page", rp);
+                decode_rdac_vpd_c2(rp, len, op, jo2p);
+            } else if (VPD_VP_HP_LTO == op->vend_prod_num)
+                decode_hp_lto_vpd_cx(rp, len, pn);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xc3:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_SEAGATE == op->vend_prod_num)
+                decode_dev_beh_vpd_c3_sea(rp, len);
+            else if (VPD_VP_RDAC == op->vend_prod_num)
+                decode_rdac_vpd_c3(rp, len);
+            else if (VPD_VP_HP_LTO == op->vend_prod_num)
+                decode_hp_lto_vpd_cx(rp, len, pn);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xc4:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_RDAC == op->vend_prod_num)
+                decode_rdac_vpd_c4(rp, len);
+            else if (VPD_VP_HP_LTO == op->vend_prod_num)
+                decode_hp_lto_vpd_cx(rp, len, pn);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xc5:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_HP_LTO == op->vend_prod_num)
+                decode_hp_lto_vpd_cx(rp, len, pn);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xc8:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_RDAC == op->vend_prod_num)
+                decode_rdac_vpd_c8(rp, len);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xc9:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_RDAC == op->vend_prod_num) {
+                if (as_json)
+                    jo2p = sg_vpd_js_hdr(jsp, jop,
+                                         "Volume access control VPD page", rp);
+                decode_rdac_vpd_c9(rp, len, op, jo2p);
+            } else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xca:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_RDAC == op->vend_prod_num)
+                decode_rdac_vpd_ca(rp, len);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xd0:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_RDAC == op->vend_prod_num)
+                decode_rdac_vpd_d0(rp, len);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xd1:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_WDC_HITACHI == op->vend_prod_num)
+                decode_vpd_d1_hit(rp, len);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case 0xd2:
+            if (! hex0)
+                hex2stdout(rp, len, no_ascii_4hex(op));
+            else if (VPD_VP_WDC_HITACHI == op->vend_prod_num)
+                decode_vpd_d2_hit(rp, len);
+            else
+                res = SG_LIB_CAT_OTHER;
+            break;
+        case SG_NVME_VPD_NICR:          /* 0xde */
+            if (VPD_VP_SG != op->vend_prod_num) {
+                res = SG_LIB_CAT_OTHER;
+                break;
+            }
+            /* NVMe: Identify Controller data structure (CNS 01h) */
+            plen = sg_get_unaligned_be16(rp + 2) + 4;
+            if (plen > len) {   /* fetch the whole page */
+                res = vpd_fetch_page(sg_fd, rp, pn, plen,
+                                     op->do_quiet, op->verbose, &len);
+                if (res) {
+                    pr2serr("Vendor VPD page=0x%x  failed to fetch\n", pn);
+                    return res;
+                }
+            }
+            if (len < 16) {
+                pr2serr("%s expected to be > 15 bytes long (got: %d)\n",
+                        name, len);
+                break;
+            } else {
+                int n = len - 16;
+                const char * np = "NVMe Identify Controller Response VPD page";
+                /* NVMe: Identify Controller data structure (CNS 01h) */
+                const char * ep = "(sg3_utils)";
+
+                if (n > 4096) {
+                    pr2serr("NVMe Identify response expected to be "
+                            "<= 4096 bytes (got: %d)\n", n);
+                    break;
+                }
+                if (op->do_hex < 3)
+                    sgj_pr_hr(jsp, "VPD INQUIRY: %s %s\n", np, ep);
+                if (op->do_hex)
+                    hex2stdout(rp, len, no_ascii_4hex(op));
+                else if (jsp->pr_as_json) {
+                    jo2p = sg_vpd_js_hdr(jsp, jop, np, rp);
+                    sgj_js_nv_hex_bytes(jsp, jo2p, "response_bytes",
+                                        rp + 16, n);
+                } else
+                    hex2stdout(rp + 16, n, 1);
+            }
+            break;
+        default:
+            res = SG_LIB_CAT_OTHER;
+        }
+    }
+    if (res && op->verbose)
+        pr2serr("%s: can't decode pn=0x%x, vend_prod_num=%d\n", __func__,
+                pn, op->vend_prod_num);
+    return res;
+}
diff --git a/src/sg_wr_mode.c b/src/sg_wr_mode.c
new file mode 100644
index 0000000..b2dff40
--- /dev/null
+++ b/src/sg_wr_mode.c
@@ -0,0 +1,648 @@
+/*
+ * Copyright (c) 2004-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <getopt.h>
+#include <ctype.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ * This program writes the given mode page contents to the corresponding
+ * mode page on the given device.
+ */
+
+static const char * version_str = "1.27 20210610";
+
+#define ME "sg_wr_mode: "
+
+#define MX_ALLOC_LEN 2048
+#define SHORT_ALLOC_LEN 252
+
+#define EBUFF_SZ 256
+
+
+static struct option long_options[] = {
+        {"contents", required_argument, 0, 'c'},
+        {"dbd", no_argument, 0, 'd'},
+        {"force", no_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"len", required_argument, 0, 'l'},
+        {"mask", required_argument, 0, 'm'},
+        {"page", required_argument, 0, 'p'},
+        {"rtd", no_argument, 0, 'R'},
+        {"save", no_argument, 0, 's'},
+        {"six", no_argument, 0, '6'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_wr_mode [--contents=H,H...] [--dbd] [--force] "
+            "[--help]\n"
+            "                  [--len=10|6] [--mask=M,M...] "
+            "[--page=PG_H[,SPG_H]]\n"
+            "                  [--rtd] [--save] [--six] [--verbose] "
+            "[--version]\n"
+            "                  DEVICE\n"
+            "  where:\n"
+            "    --contents=H,H... | -c H,H...    comma separated string "
+            "of hex numbers\n"
+            "                                     that is mode page contents "
+            "to write\n"
+            "    --contents=- | -c -   read stdin for mode page contents"
+            " to write\n"
+            "    --dbd | -d            disable block descriptors (DBD bit"
+            " in cdb)\n"
+            "    --force | -f          force the contents to be written\n"
+            "    --help | -h           print out usage message\n"
+            "    --len=10|6 | -l 10|6    use 10 byte (def) or 6 byte "
+            "variants of\n"
+            "                            SCSI MODE SENSE/SELECT commands\n"
+            "    --mask=M,M... | -m M,M...   comma separated "
+            "string of hex\n"
+            "                                numbers that mask contents"
+            " to write\n"
+            "    --page=PG_H | -p PG_H     page_code to be written (in hex)\n"
+            "    --page=PG_H,SPG_H | -p PG_H,SPG_H    page and subpage code "
+            "to be\n"
+            "                                         written (in hex)\n"
+            "    --rtd | -R            set RTD bit (revert to defaults) in "
+            "cdb\n"
+            "    --save | -s           set 'save page' (SP) bit; default "
+            "don't so\n"
+            "                          only 'current' values changed\n"
+            "    --six | -6            do SCSI MODE SENSE/SELECT(6) "
+            "commands\n"
+            "    --verbose | -v        increase verbosity\n"
+            "    --version | -V        print version string and exit\n\n"
+            "writes given mode page with SCSI MODE SELECT (10 or 6) "
+            "command\n");
+}
+
+
+/* Read hex numbers from command line or stdin. On the command line can
+ * either be comma or space separated list. Space separated list need to be
+ * quoted. For stdin (indicated by *inp=='-') there should be either
+ * one entry per line, a comma separated list or space separated list.
+ * Returns 0 if ok, or sg3_utils error code if error. */
+static int
+build_mode_page(const char * inp, uint8_t * mp_arr, int * mp_arr_len,
+                int max_arr_len)
+{
+    int in_len, k, j, m;
+    unsigned int h;
+    const char * lcp;
+    char * cp;
+    char * c2p;
+
+    if ((NULL == inp) || (NULL == mp_arr) ||
+        (NULL == mp_arr_len))
+        return SG_LIB_LOGIC_ERROR;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *mp_arr_len = 0;
+    if ('-' == inp[0]) {        /* read from stdin */
+        bool split_line;
+        int off = 0;
+        char carry_over[4];
+        char line[512];
+
+        carry_over[0] = 0;
+        for (j = 0; j < 512; ++j) {
+            if (NULL == fgets(line, sizeof(line), stdin))
+                break;
+            in_len = strlen(line);
+            if (in_len > 0) {
+                if ('\n' == line[in_len - 1]) {
+                    --in_len;
+                    line[in_len] = '\0';
+                    split_line = false;
+                } else
+                    split_line = true;
+            }
+            if (in_len < 1) {
+                carry_over[0] = 0;
+                continue;
+            }
+            if (carry_over[0]) {
+                if (isxdigit((uint8_t)line[0])) {
+                    carry_over[1] = line[0];
+                    carry_over[2] = '\0';
+                    if (1 == sscanf(carry_over, "%x", &h))
+                        mp_arr[off - 1] = h;       /* back up and overwrite */
+                    else {
+                        pr2serr("%s: carry_over error ['%s'] around line "
+                                "%d\n", __func__, carry_over, j + 1);
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    lcp = line + 1;
+                    --in_len;
+                } else
+                    lcp = line;
+                carry_over[0] = 0;
+            } else
+                lcp = line;
+            m = strspn(lcp, " \t");
+            if (m == in_len)
+                continue;
+            lcp += m;
+            in_len -= m;
+            if ('#' == *lcp)
+                continue;
+            k = strspn(lcp, "0123456789aAbBcCdDeEfF ,\t");
+            if ((k < in_len) && ('#' != lcp[k])) {
+                pr2serr("%s: syntax error at line %d, pos %d\n", __func__,
+                        j + 1, m + k + 1);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            for (k = 0; k < 1024; ++k) {
+                if (1 == sscanf(lcp, "%x", &h)) {
+                    if (h > 0xff) {
+                        pr2serr("%s: hex number larger than 0xff in line %d, "
+                                "pos %d\n", __func__, j + 1,
+                                (int)(lcp - line + 1));
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    if (split_line && (1 == strlen(lcp))) {
+                        /* single trailing hex digit might be a split pair */
+                        carry_over[0] = *lcp;
+                    }
+                    if ((off + k) >= max_arr_len) {
+                        pr2serr("%s: array length exceeded\n", __func__);
+                        return SG_LIB_SYNTAX_ERROR;
+                    }
+                    mp_arr[off + k] = h;
+                    lcp = strpbrk(lcp, " ,\t");
+                    if (NULL == lcp)
+                        break;
+                    lcp += strspn(lcp, " ,\t");
+                    if ('\0' == *lcp)
+                        break;
+                } else {
+                    if ('#' == *lcp) {
+                        --k;
+                        break;
+                    }
+                    pr2serr("%s: error in line %d, at pos %d\n", __func__,
+                            j + 1, (int)(lcp - line + 1));
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            off += (k + 1);
+        }
+        *mp_arr_len = off;
+    } else {        /* hex string on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfF, ");
+        if (in_len != k) {
+            pr2serr("%s: error at pos %d\n", __func__, k + 1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        for (k = 0; k < max_arr_len; ++k) {
+            if (1 == sscanf(lcp, "%x", &h)) {
+                if (h > 0xff) {
+                    pr2serr("%s: hex number larger than 0xff at pos %d\n",
+                            __func__, (int)(lcp - inp + 1));
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                mp_arr[k] = h;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("%s: error at pos %d\n", __func__,
+                        (int)(lcp - inp + 1));
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+        *mp_arr_len = k + 1;
+        if (k == max_arr_len) {
+            pr2serr("%s: array length exceeded\n", __func__);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+/* Read hex numbers from command line (comma separated list).
+ * Can also be (single) space separated list but needs to be quoted on the
+ * command line. Returns 0 if ok, or 1 if error. */
+static int
+build_mask(const char * inp, uint8_t * mask_arr, int * mask_arr_len,
+           int max_arr_len)
+{
+    int in_len, k;
+    unsigned int h;
+    const char * lcp;
+    char * cp;
+    char * c2p;
+
+    if ((NULL == inp) || (NULL == mask_arr) ||
+        (NULL == mask_arr_len))
+        return 1;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *mask_arr_len = 0;
+    if ('-' == inp[0]) {        /* read from stdin */
+        pr2serr("'--mask' does not accept input from stdin\n");
+        return 1;
+    } else {        /* hex string on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfF, ");
+        if (in_len != k) {
+            pr2serr("%s: error at pos %d\n", __func__, k + 1);
+            return 1;
+        }
+        for (k = 0; k < max_arr_len; ++k) {
+            if (1 == sscanf(lcp, "%x", &h)) {
+                if (h > 0xff) {
+                    pr2serr("%s: hex number larger than 0xff at pos %d\n",
+                            __func__, (int)(lcp - inp + 1));
+                    return 1;
+                }
+                mask_arr[k] = h;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("%s: error at pos %d\n", __func__,
+                        (int)(lcp - inp + 1));
+                return 1;
+            }
+        }
+        *mask_arr_len = k + 1;
+        if (k == max_arr_len) {
+            pr2serr("%s: array length exceeded\n", __func__);
+            return 1;
+        }
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool dbd = false;
+    bool force = false;
+    bool got_contents = false;
+    bool got_mask = false;
+    bool mode_6 = false;        /* so default is mode_10 */
+    bool rtd = false;   /* added in spc5r11 */
+    bool save = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, num, alloc_len, off, pdt, k, md_len, hdr_len, bd_len;
+    int mask_in_len;
+    int sg_fd = -1;
+    int pg_code = -1;
+    int sub_pg_code = 0;
+    int verbose = 0;
+    int read_in_len = 0;
+    int ret = 0;
+    unsigned u, uu;
+    const char * device_name = NULL;
+    uint8_t read_in[MX_ALLOC_LEN];
+    uint8_t mask_in[MX_ALLOC_LEN];
+    uint8_t ref_md[MX_ALLOC_LEN];
+    char ebuff[EBUFF_SZ];
+    char errStr[128];
+    char b[80];
+    struct sg_simple_inquiry_resp inq_data;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "6c:dfhl:m:p:RsvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case '6':
+            mode_6 = true;
+            break;
+        case 'c':
+            memset(read_in, 0, sizeof(read_in));
+            if ((ret = build_mode_page(optarg, read_in, &read_in_len,
+                                       sizeof(read_in)))) {
+                pr2serr("bad argument to '--contents='\n");
+                return ret;
+            }
+            got_contents = true;
+            break;
+        case 'd':
+            dbd = true;
+            break;
+        case 'f':
+            force = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'l':
+            num = sscanf(optarg, "%d", &res);
+            if ((1 == num) && ((6 == res) || (10 == res)))
+                mode_6 = (6 == res);
+            else {
+                pr2serr("length (of cdb) must be 6 or 10\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'm':
+            memset(mask_in, 0xff, sizeof(mask_in));
+            if (0 != build_mask(optarg, mask_in, &mask_in_len,
+                                sizeof(mask_in))) {
+                pr2serr("bad argument to '--mask'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            got_mask = true;
+            break;
+        case 'p':
+           if (NULL == strchr(optarg, ',')) {
+                num = sscanf(optarg, "%x", &u);
+                if ((1 != num) || (u > 62)) {
+                    pr2serr("Bad hex page code value after '--page' "
+                            "switch\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                pg_code = u;
+            } else if (2 == sscanf(optarg, "%x,%x", &u, &uu)) {
+                if (uu > 254) {
+                    pr2serr("Bad hex sub page code value after '--page' "
+                            "switch\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                pg_code = u;
+                sub_pg_code = uu;
+            } else {
+                pr2serr("Bad hex page code, subpage code sequence after "
+                        "'--page' switch\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'R':
+            rtd = true;
+            break;
+        case 's':
+            save = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((pg_code < 0) && (! rtd)) {
+        pr2serr("need page code (see '--page=')\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (got_mask && force) {
+        pr2serr("cannot use both '--force' and '--mask'\n\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+    if (rtd)
+        goto revert_to_defaults;
+
+    if (0 == sg_simple_inquiry(sg_fd, &inq_data, false, verbose))
+        pdt = inq_data.peripheral_type;
+    else
+        pdt = PDT_UNKNOWN;
+
+    /* do MODE SENSE to fetch current values */
+    memset(ref_md, 0, MX_ALLOC_LEN);
+    snprintf(errStr, sizeof(errStr), "MODE SENSE (%d): ", mode_6 ? 6 : 10);
+    alloc_len = mode_6 ? SHORT_ALLOC_LEN : MX_ALLOC_LEN;
+    if (mode_6)
+        res = sg_ll_mode_sense6(sg_fd, dbd, 0 /*current */, pg_code,
+                                sub_pg_code, ref_md, alloc_len, true,
+                                verbose);
+     else
+        res = sg_ll_mode_sense10(sg_fd, false /* llbaa */, dbd,
+                                 0 /* current */, pg_code, sub_pg_code,
+                                 ref_md, alloc_len, true, verbose);
+    ret = res;
+    if (res) {
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("%snot supported, try '--len=%d'\n", errStr,
+                    (mode_6 ? 10 : 6));
+        else {
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("%s%s\n", errStr, b);
+        }
+        goto fini;
+    }
+    off = sg_mode_page_offset(ref_md, alloc_len, mode_6, ebuff, EBUFF_SZ);
+    if (off < 0) {
+        pr2serr("%s%s\n", errStr, ebuff);
+        goto fini;
+    }
+    md_len = sg_msense_calc_length(ref_md, alloc_len, mode_6, &bd_len);
+    if (md_len < 0) {
+        pr2serr("%ssg_msense_calc_length() failed\n", errStr);
+        goto fini;
+    }
+    hdr_len = mode_6 ? 4 : 8;
+    if (got_contents) {
+        if (read_in_len < 2) {
+            pr2serr("contents length=%d too short\n", read_in_len);
+            goto fini;
+        }
+        ref_md[0] = 0;  /* mode data length reserved for mode select */
+        if (! mode_6)
+            ref_md[1] = 0;    /* mode data length reserved for mode select */
+        if (0 == pdt)   /* for disks mask out DPOFUA bit */
+            ref_md[mode_6 ? 2 : 3] &= 0xef;
+        if (md_len > alloc_len) {
+            pr2serr("mode data length=%d exceeds allocation length=%d\n",
+                    md_len, alloc_len);
+            goto fini;
+        }
+        if (got_mask) {
+            for (k = 0; k < (md_len - off); ++k) {
+                if ((0x0 == mask_in[k]) || (k > read_in_len))
+                   read_in[k] = ref_md[off + k];
+                else if (mask_in[k] < 0xff) {
+                   c = (ref_md[off + k] & (0xff & ~mask_in[k]));
+                   read_in[k] = (c | (read_in[k] & mask_in[k]));
+                }
+            }
+            read_in_len = md_len - off;
+        }
+        if (! force) {
+            if ((! (ref_md[off] & 0x80)) && save) {
+                pr2serr("PS bit in existing mode page indicates that it is "
+                        "not saveable\n    but '--save' option given\n");
+                goto fini;
+            }
+            read_in[0] &= 0x7f; /* mask out PS bit, reserved in mode select */
+            if ((md_len - off) != read_in_len) {
+                pr2serr("contents length=%d but reference mode page "
+                        "length=%d\n", read_in_len, md_len - off);
+                goto fini;
+            }
+            if (pg_code != (read_in[0] & 0x3f)) {
+                pr2serr("contents page_code=0x%x but reference "
+                        "page_code=0x%x\n", (read_in[0] & 0x3f), pg_code);
+                goto fini;
+            }
+            if ((read_in[0] & 0x40) != (ref_md[off] & 0x40)) {
+                pr2serr("contents flags subpage but reference page does not "
+                        "(or vice versa)\n");
+                goto fini;
+            }
+            if ((read_in[0] & 0x40) && (read_in[1] != sub_pg_code)) {
+                pr2serr("contents subpage_code=0x%x but reference "
+                        "sub_page_code=0x%x\n", read_in[1], sub_pg_code);
+                goto fini;
+            }
+        } else
+            md_len = off + read_in_len; /* force length */
+
+        memcpy(ref_md + off, read_in, read_in_len);
+        if (mode_6)
+            res = sg_ll_mode_select6_v2(sg_fd, true /* PF */, rtd, save,
+                                        ref_md, md_len, true, verbose);
+        else
+            res = sg_ll_mode_select10_v2(sg_fd, true /* PF */, rtd, save,
+                                         ref_md, md_len, true, verbose);
+        ret = res;
+        if (res)
+            goto fini;
+    } else {
+        printf(">>> No contents given, so show current mode page data:\n");
+        printf("  header:\n");
+        hex2stdout(ref_md, hdr_len, -1);
+        if (bd_len) {
+            printf("  block descriptor(s):\n");
+            hex2stdout(ref_md + hdr_len, bd_len, -1);
+        } else
+            printf("  << no block descriptors >>\n");
+        printf("  mode page:\n");
+        hex2stdout(ref_md + off, md_len - off, -1);
+    }
+    ret = 0;
+    goto fini;
+
+revert_to_defaults:
+    if (verbose)
+        pr2serr("Doing MODE SELECT(%d) with revert to defaults (RTD) set "
+                "and SP=%d\n", mode_6 ? 6 : 10, !! save);
+    if (mode_6)
+        res = sg_ll_mode_select6_v2(sg_fd, false /* PF */, true /* rtd */,
+                                    save, NULL, 0, true, verbose);
+    else
+        res = sg_ll_mode_select10_v2(sg_fd, false /* PF */, true /* rtd */,
+                                     save, NULL, 0, true, verbose);
+    ret = res;
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_wr_mode failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_write_buffer.c b/src/sg_write_buffer.c
new file mode 100644
index 0000000..2b84323
--- /dev/null
+++ b/src/sg_write_buffer.c
@@ -0,0 +1,597 @@
+/*
+ * Copyright (c) 2006-2021 Luben Tuikov and Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <errno.h>
+#include <string.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+#include "sg_pt.h"      /* needed for scsi_pt_win32_direct() */
+#endif
+#endif
+
+/*
+ * This utility issues the SCSI WRITE BUFFER command to the given device.
+ */
+
+static const char * version_str = "1.30 20210610";    /* spc6r05 */
+
+#define ME "sg_write_buffer: "
+#define DEF_XFER_LEN (8 * 1024 * 1024)
+#define EBUFF_SZ 256
+
+#define WRITE_BUFFER_CMD 0x3b
+#define WRITE_BUFFER_CMDLEN 10
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT 300      /* 300 seconds, 5 minutes */
+
+static struct option long_options[] = {
+        {"bpw", required_argument, 0, 'b'},
+        {"dry-run", no_argument, 0, 'd'},
+        {"dry_run", no_argument, 0, 'd'},
+        {"help", no_argument, 0, 'h'},
+        {"id", required_argument, 0, 'i'},
+        {"in", required_argument, 0, 'I'},
+        {"length", required_argument, 0, 'l'},
+        {"mode", required_argument, 0, 'm'},
+        {"offset", required_argument, 0, 'o'},
+        {"read-stdin", no_argument, 0, 'r'},
+        {"read_stdin", no_argument, 0, 'r'},
+        {"raw", no_argument, 0, 'r'},
+        {"skip", required_argument, 0, 's'},
+        {"specific", required_argument, 0, 'S'},
+        {"timeout", required_argument, 0, 't' },
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_write_buffer [--bpw=CS] [--dry-run] [--help] [--id=ID] "
+            "[--in=FILE]\n"
+            "                       [--length=LEN] [--mode=MO] "
+            "[--offset=OFF]\n"
+            "                       [--read-stdin] [--skip=SKIP] "
+            "[--specific=MS]\n"
+            "                       [--timeout=TO] [--verbose] [--version] "
+            "DEVICE\n"
+            "  where:\n"
+            "    --bpw=CS|-b CS         CS is chunk size: bytes per write "
+            "buffer\n"
+            "                           command (def: 0 -> as many as "
+            "possible)\n"
+            "    --dry-run|-d           skip WRITE BUFFER commands, do "
+            "everything else\n"
+            "    --help|-h              print out usage message then exit\n"
+            "    --id=ID|-i ID          buffer identifier (0 (default) to "
+            "255)\n"
+            "    --in=FILE|-I FILE      read from FILE ('-I -' read "
+            "from stdin)\n"
+            "    --length=LEN|-l LEN    length in bytes to write; may be "
+            "deduced from\n"
+            "                           FILE\n"
+            "    --mode=MO|-m MO        write buffer mode, MO is number or "
+            "acronym\n"
+            "                           (def: 0 -> 'combined header and "
+            "data' (obs))\n"
+            "    --offset=OFF|-o OFF    buffer offset (unit: bytes, def: 0)\n"
+            "    --read-stdin|-r        read from stdin (same as '-I -')\n"
+            "    --skip=SKIP|-s SKIP    bytes in file FILE to skip before "
+            "reading\n"
+            "    --specific=MS|-S MS    mode specific value; 3 bit field "
+            "(0 to 7)\n"
+            "    --timeout=TO|-t TO     command timeout in seconds (def: "
+            "300)\n"
+            "    --verbose|-v           increase verbosity\n"
+            "    --version|-V           print version string and exit\n\n"
+            "Performs one or more SCSI WRITE BUFFER commands. Use '-m xxx' "
+            "to list\navailable modes. A chunk size of 4 KB ('--bpw=4k') "
+            "seems to work well.\nExample: sg_write_buffer -b 4k -I xxx.lod "
+            "-m 7 /dev/sg3\n"
+          );
+
+}
+
+#define MODE_HEADER_DATA        0
+#define MODE_VENDOR             1
+#define MODE_DATA               2
+#define MODE_DNLD_MC            4
+#define MODE_DNLD_MC_SAVE       5
+#define MODE_DNLD_MC_OFFS       6
+#define MODE_DNLD_MC_OFFS_SAVE  7
+#define MODE_ECHO_BUFFER        0x0A
+#define MODE_DNLD_MC_EV_OFFS_DEFER 0x0D
+#define MODE_DNLD_MC_OFFS_DEFER 0x0E
+#define MODE_ACTIVATE_MC        0x0F
+#define MODE_EN_EX_ECHO         0x1A
+#define MODE_DIS_EX             0x1B
+#define MODE_DNLD_ERR_HISTORY   0x1C
+
+
+struct mode_s {
+        const char *mode_string;
+        int   mode;
+        const char *comment;
+};
+
+static struct mode_s mode_arr[] = {
+        {"hd",         MODE_HEADER_DATA, "combined header and data "
+                "(obsolete)"},
+        {"vendor",     MODE_VENDOR,    "vendor specific"},
+        {"data",       MODE_DATA,      "data"},
+        {"dmc",        MODE_DNLD_MC,   "download microcode and activate"},
+        {"dmc_save",   MODE_DNLD_MC_SAVE, "download microcode, save and "
+                "activate"},
+        {"dmc_offs",   MODE_DNLD_MC_OFFS, "download microcode with offsets "
+                "and activate"},
+        {"dmc_offs_save", MODE_DNLD_MC_OFFS_SAVE, "download microcode with "
+                "offsets, save and\n\t\t\t\tactivate"},
+        {"echo",       MODE_ECHO_BUFFER, "write data to echo buffer"},
+        {"dmc_offs_ev_defer", MODE_DNLD_MC_EV_OFFS_DEFER, "download "
+                "microcode with offsets, select\n\t\t\t\tactivation event, "
+                "save and defer activation"},
+        {"dmc_offs_defer", MODE_DNLD_MC_OFFS_DEFER, "download microcode "
+                "with offsets, save and\n\t\t\t\tdefer activation"},
+        {"activate_mc", MODE_ACTIVATE_MC, "activate deferred microcode"},
+        {"en_ex",      MODE_EN_EX_ECHO, "enable expander communications "
+                "protocol and\n\t\t\t\techo buffer (obsolete)"},
+        {"dis_ex",     MODE_DIS_EX, "disable expander communications "
+                "protocol\n\t\t\t\t(obsolete)"},
+        {"deh",        MODE_DNLD_ERR_HISTORY, "download application client "
+                "error history "},
+        {NULL, 0, NULL},
+};
+
+static void
+print_modes(void)
+{
+    const struct mode_s * mp;
+
+    pr2serr("The modes parameter argument can be numeric (hex or decimal)\n"
+            "or symbolic:\n");
+    for (mp = mode_arr; mp->mode_string; ++mp) {
+        pr2serr(" %2d (0x%02x)  %-18s%s\n", mp->mode, mp->mode,
+                mp->mode_string, mp->comment);
+    }
+    pr2serr("\nAdditionally '--bpw=<val>,act' does a activate deferred "
+            "microcode after\nsuccessful dmc_offs_defer and "
+            "dmc_offs_ev_defer mode downloads.\n");
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool bpw_then_activate = false;
+    bool dry_run = false;
+    bool got_stdin = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    bool wb_len_given = false;
+    int infd, res, c, len, k, n;
+    int sg_fd = -1;
+    int bpw = 0;
+    int do_help = 0;
+    int ret = 0;
+    int verbose = 0;
+    int wb_id = 0;
+    int wb_len = 0;
+    int wb_mode = 0;
+    int wb_offset = 0;
+    int wb_skip = 0;
+    int wb_timeout = DEF_PT_TIMEOUT;
+    int wb_mspec = 0;
+    const char * device_name = NULL;
+    const char * file_name = NULL;
+    uint8_t * dop = NULL;
+    uint8_t * read_buf = NULL;
+    uint8_t * free_dop = NULL;
+    char * cp;
+    const struct mode_s * mp;
+    char ebuff[EBUFF_SZ];
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "b:dhi:I:l:m:o:rs:S:t:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            bpw = sg_get_num(optarg);
+            if (bpw < 0) {
+                pr2serr("argument to '--bpw' should be in a positive "
+                        "number\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if ((cp = strchr(optarg, ','))) {
+                if (0 == strncmp("act", cp + 1, 3))
+                    bpw_then_activate = true;
+            }
+            break;
+        case 'd':
+            dry_run = true;
+            break;
+        case 'h':
+        case '?':
+            ++do_help;
+            break;
+        case 'i':
+            wb_id = sg_get_num(optarg);
+            if ((wb_id < 0) || (wb_id > 255)) {
+                pr2serr("argument to '--id' should be in the range 0 to "
+                        "255\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'I':
+            file_name = optarg;
+            break;
+        case 'l':
+            wb_len = sg_get_num(optarg);
+            if (wb_len < 0) {
+                pr2serr("bad argument to '--length'\n");
+                return SG_LIB_SYNTAX_ERROR;
+             }
+             wb_len_given = true;
+             break;
+        case 'm':
+            if (isdigit((uint8_t)*optarg)) {
+                wb_mode = sg_get_num(optarg);
+                if ((wb_mode < 0) || (wb_mode > 31)) {
+                    pr2serr("argument to '--mode' should be in the range 0 "
+                            "to 31\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            } else {
+                len = strlen(optarg);
+                for (mp = mode_arr; mp->mode_string; ++mp) {
+                    if (0 == strncmp(mp->mode_string, optarg, len)) {
+                        wb_mode = mp->mode;
+                        break;
+                    }
+                }
+                if (! mp->mode_string) {
+                    print_modes();
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }
+            break;
+        case 'o':
+           wb_offset = sg_get_num(optarg);
+           if (wb_offset < 0) {
+                pr2serr("bad argument to '--offset'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'r':       /* --read-stdin and --raw (previous name) */
+            file_name = "-";
+            break;
+        case 's':
+           wb_skip = sg_get_num(optarg);
+           if (wb_skip < 0) {
+                pr2serr("bad argument to '--skip'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'S':
+            wb_mspec = sg_get_num(optarg);
+            if ((wb_mspec < 0) || (wb_mspec > 7)) {
+                pr2serr("expected argument to '--specific' to be 0 to 7\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 't':
+            wb_timeout = sg_get_num(optarg);
+            if (wb_timeout < 0) {
+                pr2serr("Invalid argument to '--timeout'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (do_help) {
+        if (do_help > 1) {
+            usage();
+            pr2serr("\n");
+            print_modes();
+        } else
+            usage();
+        return 0;
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if ((wb_len > 0) && (bpw > wb_len)) {
+        pr2serr("trim chunk size (CS) to be the same as LEN\n");
+        bpw = wb_len;
+    }
+
+#ifdef SG_LIB_WIN32
+#ifdef SG_LIB_WIN32_DIRECT
+    if (verbose > 4)
+        pr2serr("Initial win32 SPT interface state: %s\n",
+                scsi_pt_win32_spt_state() ? "direct" : "indirect");
+    scsi_pt_win32_direct(SG_LIB_WIN32_DIRECT /* SPT pt interface */);
+#endif
+#endif
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+    if (file_name || (wb_len > 0)) {
+        if (0 == wb_len)
+            wb_len = DEF_XFER_LEN;
+        dop = sg_memalign(wb_len, 0, &free_dop, false);
+        if (NULL == dop) {
+            pr2serr(ME "out of memory\n");
+            ret = sg_convert_errno(ENOMEM);
+            goto err_out;
+        }
+        memset(dop, 0xff, wb_len);
+        if (file_name) {
+            got_stdin = (0 == strcmp(file_name, "-"));
+            if (got_stdin) {
+                if (wb_skip > 0) {
+                    pr2serr("Can't skip on stdin\n");
+                    ret = SG_LIB_FILE_ERROR;
+                    goto err_out;
+                }
+                infd = STDIN_FILENO;
+            } else {
+                if ((infd = open(file_name, O_RDONLY)) < 0) {
+                    ret = sg_convert_errno(errno);
+                    snprintf(ebuff, EBUFF_SZ,
+                             ME "could not open %s for reading", file_name);
+                    perror(ebuff);
+                    goto err_out;
+                } else if (sg_set_binary_mode(infd) < 0)
+                    perror("sg_set_binary_mode");
+                if (wb_skip > 0) {
+                    if (lseek(infd, wb_skip, SEEK_SET) < 0) {
+                        ret = sg_convert_errno(errno);
+                        snprintf(ebuff,  EBUFF_SZ, ME "couldn't skip to "
+                                 "required position on %s", file_name);
+                        perror(ebuff);
+                        close(infd);
+                        goto err_out;
+                    }
+                }
+            }
+            if (infd == STDIN_FILENO) {
+                if (NULL == (read_buf = (uint8_t *)malloc(DEF_XFER_LEN))) {
+                    pr2serr(ME "out of memory\n");
+                    ret = SG_LIB_SYNTAX_ERROR;
+                    goto err_out;
+                }
+                res = read(infd, read_buf, DEF_XFER_LEN);
+                if (res < 0) {
+                    snprintf(ebuff, EBUFF_SZ, ME "couldn't read from STDIN");
+                    perror(ebuff);
+                    ret = SG_LIB_FILE_ERROR;
+                    goto err_out;
+                }
+                char * pch;
+                int val = 0;
+                res = 0;
+                pch = strtok((char*)read_buf, ",. \n\t");
+                while (pch != NULL) {
+                    val = sg_get_num_nomult(pch);
+                    if (val >= 0 && val < 255) {
+                        dop[res] = val;
+                        res++;
+                    } else {
+                        pr2serr("Data read from STDIO is wrong.\nPlease "
+                                "input the data a byte at a time, the bytes "
+                                "should be separated\nby either space, or "
+                                "',' ( or by '.'), and the value per byte "
+                                "should\nbe between 0~255. Hexadecimal "
+                                "numbers should be preceded by either '0x' "
+                                "or\n'OX' (or have a trailing 'h' or "
+                                "'H').\n");
+                        ret = SG_LIB_SYNTAX_ERROR;
+                        goto err_out;
+                    }
+                    pch = strtok(NULL, ",. \n\t");
+                }
+            } else {
+                res = read(infd, dop, wb_len);
+                if (res < 0) {
+                    ret = sg_convert_errno(errno);
+                    snprintf(ebuff, EBUFF_SZ, ME "couldn't read from %s",
+                             file_name);
+                    perror(ebuff);
+                    if (! got_stdin)
+                        close(infd);
+                    goto err_out;
+                }
+            }
+            if (res < wb_len) {
+                if (wb_len_given) {
+                    pr2serr("tried to read %d bytes from %s, got %d bytes\n",
+                            wb_len, file_name, res);
+                    pr2serr("pad with 0xff bytes and continue\n");
+                } else {
+                    if (verbose) {
+                        pr2serr("tried to read %d bytes from %s, got %d "
+                                "bytes\n", wb_len, file_name, res);
+                        pr2serr("will write %d bytes", res);
+                        if ((bpw > 0) && (bpw < wb_len))
+                            pr2serr(", %d bytes per WRITE BUFFER command\n",
+                                    bpw);
+                        else
+                            pr2serr("\n");
+                    }
+                    wb_len = res;
+                }
+            }
+            if (! got_stdin)
+                close(infd);
+        }
+    }
+
+    res = 0;
+    if (bpw > 0) {
+        for (k = 0; k < wb_len; k += n) {
+            n = wb_len - k;
+            if (n > bpw)
+                n = bpw;
+            if (verbose)
+                pr2serr("sending write buffer, mode=0x%x, mspec=%d, id=%d, "
+                        " offset=%d, len=%d\n", wb_mode, wb_mspec, wb_id,
+                        wb_offset + k, n);
+            if (dry_run) {
+                if (verbose)
+                    pr2serr("skipping WRITE BUFFER command due to "
+                            "--dry-run\n");
+                res = 0;
+            } else
+                res = sg_ll_write_buffer_v2(sg_fd, wb_mode, wb_mspec, wb_id,
+                                            wb_offset + k, dop + k, n,
+                                            wb_timeout, true, verbose);
+            if (res)
+                break;
+        }
+        if (bpw_then_activate) {
+            if (verbose)
+                pr2serr("sending Activate deferred microcode [0xf]\n");
+            if (dry_run) {
+                if (verbose)
+                    pr2serr("skipping WRITE BUFFER(ACTIVATE) command due to "
+                            "--dry-run\n");
+                res = 0;
+            } else
+                res = sg_ll_write_buffer_v2(sg_fd, MODE_ACTIVATE_MC,
+                                            0 /* buffer_id */,
+                                            0 /* buffer_offset */, 0,
+                                            NULL, 0, wb_timeout, true,
+                                            verbose);
+        }
+    } else {
+        if (verbose)
+            pr2serr("sending single write buffer, mode=0x%x, mpsec=%d, "
+                    "id=%d, offset=%d, len=%d\n", wb_mode, wb_mspec, wb_id,
+                    wb_offset, wb_len);
+        if (dry_run) {
+            if (verbose)
+                pr2serr("skipping WRITE BUFFER(all in one) command due to "
+                        "--dry-run\n");
+            res = 0;
+        } else
+            res = sg_ll_write_buffer_v2(sg_fd, wb_mode, wb_mspec, wb_id,
+                                        wb_offset, dop, wb_len, wb_timeout,
+                                        true, verbose);
+    }
+    if (0 != res) {
+        char b[80];
+
+        ret = res;
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("Write buffer failed: %s\n", b);
+    }
+
+err_out:
+    if (free_dop)
+        free(free_dop);
+    if (read_buf)
+        free(read_buf);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_write_buffer failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_write_long.c b/src/sg_write_long.c
new file mode 100644
index 0000000..af323cc
--- /dev/null
+++ b/src/sg_write_long.c
@@ -0,0 +1,331 @@
+/* A utility program for the Linux OS SCSI subsystem.
+ *  Copyright (C) 2004-2018 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program issues the SCSI command WRITE LONG to a given SCSI device.
+ * It sends the command with the logical block address passed as the lba
+ * argument, and the transfer length set to the xfer_len argument. the
+ * buffer to be written to the device filled with 0xff, this buffer includes
+ * the sector data and the ECC bytes.
+ *
+ * This code was contributed by Saeed Bishara
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.21 20180723";
+
+
+#define MAX_XFER_LEN (15 * 1024)
+
+#define ME "sg_write_long: "
+
+#define EBUFF_SZ 512
+
+static struct option long_options[] = {
+        {"16", no_argument, 0, 'S'},
+        {"cor_dis", no_argument, 0, 'c'},
+        {"cor-dis", no_argument, 0, 'c'},
+        {"help", no_argument, 0, 'h'},
+        {"in", required_argument, 0, 'i'},
+        {"lba", required_argument, 0, 'l'},
+        {"pblock", no_argument, 0, 'p'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"wr_uncor", no_argument, 0, 'w'},
+        {"wr-uncor", no_argument, 0, 'w'},
+        {"xfer_len", required_argument, 0, 'x'},
+        {"xfer-len", required_argument, 0, 'x'},
+        {0, 0, 0, 0},
+};
+
+
+
+static void
+usage()
+{
+  pr2serr("Usage: sg_write_long [--16] [--cor_dis] [--help] [--in=IF] "
+          "[--lba=LBA]\n"
+          "                     [--pblock] [--verbose] [--version] "
+          "[--wr_uncor]\n"
+          "                     [--xfer_len=BTL] DEVICE\n"
+          "  where:\n"
+          "    --16|-S              do WRITE LONG(16) (default: 10)\n"
+          "    --cor_dis|-c         set correction disabled bit\n"
+          "    --help|-h            print out usage message\n"
+          "    --in=IF|-i IF        input from file called IF (default: "
+          "use\n"
+          "                         0xff bytes as fill)\n"
+          "    --lba=LBA|-l LBA     logical block address "
+          "(default: 0)\n"
+          "    --pblock|-p          physical block (default: logical "
+          "block)\n"
+          "    --verbose|-v         increase verbosity\n"
+          "    --version|-V         print version string then exit\n"
+          "    --wr_uncor|-w        set an uncorrectable error (no "
+          "data transferred)\n"
+          "    --xfer_len=BTL|-x BTL    byte transfer length (< 10000) "
+          "(default:\n"
+          "                             520 bytes)\n\n"
+          "Performs a SCSI WRITE LONG (10 or 16) command. Writes a single "
+          "block\nincluding associated ECC data. That data may be obtained "
+          "from the\nSCSI READ LONG command. See the sg_read_long utility.\n"
+          );
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool do_16 = false;
+    bool cor_dis = false;
+    bool got_stdin;
+    bool pblock = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    bool wr_uncor = false;
+    int res, c, infd, offset;
+    int sg_fd = -1;
+    int xfer_len = 520;
+    int ret = 1;
+    int verbose = 0;
+    int64_t ll;
+    uint64_t llba = 0;
+    const char * device_name = NULL;
+    uint8_t * writeLongBuff = NULL;
+    void * rawp = NULL;
+    uint8_t * free_rawp = NULL;
+    const char * ten_or;
+    char file_name[256];
+    char b[80];
+    char ebuff[EBUFF_SZ];
+
+    memset(file_name, 0, sizeof file_name);
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "chi:l:pSvVwx:", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            cor_dis = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+            strncpy(file_name, optarg, sizeof(file_name) - 1);
+            file_name[sizeof(file_name) - 1] = '\0';
+            break;
+        case 'l':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            llba = (uint64_t)ll;
+            break;
+        case 'p':
+            pblock = true;
+            break;
+        case 'S':
+            do_16 = true;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        case 'w':
+            wr_uncor = true;
+            break;
+        case 'x':
+            xfer_len = sg_get_num(optarg);
+            if (-1 == xfer_len) {
+                pr2serr("bad argument to '--xfer_len'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (wr_uncor)
+        xfer_len = 0;
+    else if (xfer_len >= MAX_XFER_LEN) {
+        pr2serr("xfer_len (%d) is out of range ( < %d)\n", xfer_len,
+                MAX_XFER_LEN);
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        if (verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    if (wr_uncor) {
+        if ('\0' != file_name[0])
+            pr2serr(">>> warning: when '--wr_uncor' given '-in=' is "
+                    "ignored\n");
+    } else {
+        if (NULL == (rawp = sg_memalign(MAX_XFER_LEN, 0, &free_rawp, false))) {
+            pr2serr(ME "out of memory\n");
+            ret = sg_convert_errno(ENOMEM);
+            goto err_out;
+        }
+        writeLongBuff = (uint8_t *)rawp;
+        memset(rawp, 0xff, MAX_XFER_LEN);
+        if (file_name[0]) {
+            got_stdin = (0 == strcmp(file_name, "-"));
+            if (got_stdin) {
+                infd = STDIN_FILENO;
+                if (sg_set_binary_mode(STDIN_FILENO) < 0)
+                    perror("sg_set_binary_mode");
+            } else {
+                if ((infd = open(file_name, O_RDONLY)) < 0) {
+                    snprintf(ebuff, EBUFF_SZ,
+                             ME "could not open %s for reading", file_name);
+                    perror(ebuff);
+                    goto err_out;
+                } else if (sg_set_binary_mode(infd) < 0)
+                    perror("sg_set_binary_mode");
+            }
+            res = read(infd, writeLongBuff, xfer_len);
+            if (res < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "couldn't read from %s",
+                         file_name);
+                perror(ebuff);
+                if (! got_stdin)
+                    close(infd);
+                goto err_out;
+            }
+            if (res < xfer_len) {
+                pr2serr("tried to read %d bytes from %s, got %d bytes\n",
+                        xfer_len, file_name, res);
+                pr2serr("pad with 0xff bytes and continue\n");
+            }
+            if (! got_stdin)
+                close(infd);
+        }
+    }
+    if (verbose)
+        pr2serr(ME "issue write long to device %s\n\t\txfer_len= %d (0x%x), "
+                "lba=%" PRIu64 " (0x%" PRIx64 ")\n    cor_dis=%d, "
+                "wr_uncor=%d, pblock=%d\n", device_name, xfer_len, xfer_len,
+                llba, llba, (int)cor_dis, (int)wr_uncor, (int)pblock);
+
+    ten_or = do_16 ? "16" : "10";
+    if (do_16)
+        res = sg_ll_write_long16(sg_fd, cor_dis, wr_uncor, pblock, llba,
+                                 writeLongBuff, xfer_len, &offset, true,
+                                 verbose);
+    else
+        res = sg_ll_write_long10(sg_fd, cor_dis, wr_uncor, pblock,
+                                 (unsigned int)llba, writeLongBuff, xfer_len,
+                                 &offset, true, verbose);
+    ret = res;
+    switch (res) {
+    case 0:
+        break;
+    case SG_LIB_CAT_ILLEGAL_REQ_WITH_INFO:
+        pr2serr("<<< device indicates 'xfer_len' should be %d >>>\n",
+                xfer_len - offset);
+        break;
+    default:
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("  SCSI WRITE LONG (%s): %s\n", ten_or, b);
+        break;
+    }
+
+err_out:
+    if (free_rawp)
+        free(free_rawp);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_write_long failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_write_same.c b/src/sg_write_same.c
new file mode 100644
index 0000000..bfdf815
--- /dev/null
+++ b/src/sg_write_same.c
@@ -0,0 +1,678 @@
+/*
+ * Copyright (c) 2009-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.34 20220127";
+
+
+#define ME "sg_write_same: "
+
+#define WRITE_SAME10_OP 0x41
+#define WRITE_SAME16_OP 0x93
+#define VARIABLE_LEN_OP 0x7f
+#define WRITE_SAME32_SA 0xd
+#define WRITE_SAME32_ADD 0x18
+#define WRITE_SAME10_LEN 10
+#define WRITE_SAME16_LEN 16
+#define WRITE_SAME32_LEN 32
+#define RCAP10_RESP_LEN 8
+#define RCAP16_RESP_LEN 32
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_TIMEOUT_SECS 60
+#define DEF_WS_CDB_SIZE WRITE_SAME10_LEN
+#define DEF_WS_NUMBLOCKS 1
+#define MAX_XFER_LEN (64 * 1024)
+#define EBUFF_SZ 512
+
+#ifndef UINT32_MAX
+#define UINT32_MAX ((uint32_t)-1)
+#endif
+
+static struct option long_options[] = {
+    {"10", no_argument, 0, 'R'},
+    {"16", no_argument, 0, 'S'},
+    {"32", no_argument, 0, 'T'},
+    {"anchor", no_argument, 0, 'a'},
+    {"ff", no_argument, 0, 'f'},
+    {"grpnum", required_argument, 0, 'g'},
+    {"help", no_argument, 0, 'h'},
+    {"in", required_argument, 0, 'i'},
+    {"lba", required_argument, 0, 'l'},
+    {"lbdata", no_argument, 0, 'L'},
+    {"ndob", no_argument, 0, 'N'},
+    {"num", required_argument, 0, 'n'},
+    {"pbdata", no_argument, 0, 'P'},
+    {"timeout", required_argument, 0, 't'},
+    {"unmap", no_argument, 0, 'U'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {"wrprotect", required_argument, 0, 'w'},
+    {"xferlen", required_argument, 0, 'x'},
+    {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool anchor;
+    bool ff;
+    bool ndob;
+    bool lbdata;
+    bool pbdata;
+    bool unmap;
+    bool verbose_given;
+    bool version_given;
+    bool want_ws10;
+    int grpnum;
+    int numblocks;
+    int timeout;
+    int verbose;
+    int wrprotect;
+    int xfer_len;
+    int pref_cdb_size;
+    uint64_t lba;
+    char ifilename[256];
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_write_same [--10] [--16] [--32] [--anchor] "
+            "[-ff] [--grpnum=GN]\n"
+            "                     [--help] [--in=IF] [--lba=LBA] [--lbdata] "
+            "[--ndob]\n"
+            "                     [--num=NUM] [--pbdata] [--timeout=TO] "
+            "[--unmap]\n"
+            "                     [--verbose] [--version] [--wrprotect=WRP] "
+            "[xferlen=LEN]\n"
+            "                     DEVICE\n"
+            "  where:\n"
+            "    --10|-R              send WRITE SAME(10) (even if '--unmap' "
+            "is given)\n"
+            "    --16|-S              send WRITE SAME(16) (def: 10 unless "
+            "'--unmap' given,\n"
+            "                         LBA+NUM > 32 bits, or NUM > 65535; "
+            "then def 16)\n"
+            "    --32|-T              send WRITE SAME(32) (def: 10 or 16)\n"
+            "    --anchor|-a          set ANCHOR field in cdb\n"
+            "    --ff|-f              use buffer of 0xff bytes for fill "
+            "(def: 0x0 bytes)\n"
+            "    --grpnum=GN|-g GN    GN is group number field (def: 0)\n"
+            "    --help|-h            print out usage message\n"
+            "    --in=IF|-i IF        IF is file to fetch one block of data "
+            "from (use LEN\n"
+            "                         bytes or whole file). Block written to "
+            "DEVICE\n"
+            "    --lba=LBA|-l LBA     LBA is the logical block address to "
+            "start (def: 0)\n"
+            "    --lbdata|-L          set LBDATA bit (obsolete)\n"
+            "    --ndob|-N            set NDOB (no data-out buffer) bit in "
+            "cdb\n"
+            "    --num=NUM|-n NUM     NUM is number of logical blocks to "
+            "write (def: 1)\n"
+            "                         [Beware NUM==0 may mean: 'rest of "
+            "device']\n"
+            "    --pbdata|-P          set PBDATA bit (obsolete)\n"
+            "    --timeout=TO|-t TO    command timeout (unit: seconds) (def: "
+            "60)\n"
+            "    --unmap|-U           set UNMAP bit\n"
+            "    --verbose|-v         increase verbosity\n"
+            "    --version|-V         print version string then exit\n"
+            "    --wrprotect=WPR|-w WPR    WPR is the WRPROTECT field value "
+            "(def: 0)\n"
+            "    --xferlen=LEN|-x LEN    LEN is number of bytes from IF to "
+            "send to\n"
+            "                            DEVICE (def: IF file length)\n\n"
+            "Performs a SCSI WRITE SAME (10, 16 or 32) command. NDOB bit is "
+            "only\nsupported by the 16 and 32 byte variants. When set the "
+            "specified blocks\nwill be filled with zeros or the "
+            "'provisioning initialization pattern'\nas indicated by the "
+            "LBPRZ field. As a precaution one of the '--in=',\n'--lba=' or "
+            "'--num=' options is required.\nAnother implementation of WRITE "
+            "SAME is found in the sg_write_x utility.\n"
+            );
+}
+
+static int
+do_write_same(int sg_fd, const struct opts_t * op, const void * dataoutp,
+              int * act_cdb_lenp)
+{
+    int ret, res, sense_cat, cdb_len;
+    uint64_t llba;
+    uint8_t ws_cdb[WRITE_SAME32_LEN] SG_C_CPP_ZERO_INIT;
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    cdb_len = op->pref_cdb_size;
+    if (WRITE_SAME10_LEN == cdb_len) {
+        llba = op->lba + op->numblocks;
+        if ((op->numblocks > 0xffff) || (llba > UINT32_MAX) ||
+            op->ndob || (op->unmap && (! op->want_ws10))) {
+            cdb_len = WRITE_SAME16_LEN;
+            if (op->verbose) {
+                const char * cp = "use WRITE SAME(16) instead of 10 byte "
+                                  "cdb";
+
+                if (op->numblocks > 0xffff)
+                    pr2serr("%s since blocks exceed 65535\n", cp);
+                else if (llba > UINT32_MAX)
+                    pr2serr("%s since LBA may exceed 32 bits\n", cp);
+                else
+                    pr2serr("%s due to ndob or unmap settings\n", cp);
+            }
+        }
+    }
+    if (act_cdb_lenp)
+        *act_cdb_lenp = cdb_len;
+    switch (cdb_len) {
+    case WRITE_SAME10_LEN:
+        ws_cdb[0] = WRITE_SAME10_OP;
+        ws_cdb[1] = ((op->wrprotect & 0x7) << 5);
+        /* ANCHOR + UNMAP not allowed for WRITE_SAME10 in sbc3r24+r25 but
+         * a proposal has been made to allow it. Anticipate approval. */
+        if (op->anchor)
+            ws_cdb[1] |= 0x10;
+        if (op->unmap)
+            ws_cdb[1] |= 0x8;
+        if (op->pbdata)
+            ws_cdb[1] |= 0x4;
+        if (op->lbdata)
+            ws_cdb[1] |= 0x2;
+        sg_put_unaligned_be32((uint32_t)op->lba, ws_cdb + 2);
+        ws_cdb[6] = (op->grpnum & GRPNUM_MASK);
+        sg_put_unaligned_be16((uint16_t)op->numblocks, ws_cdb + 7);
+        break;
+    case WRITE_SAME16_LEN:
+        ws_cdb[0] = WRITE_SAME16_OP;
+        ws_cdb[1] = ((op->wrprotect & 0x7) << 5);
+        if (op->anchor)
+            ws_cdb[1] |= 0x10;
+        if (op->unmap)
+            ws_cdb[1] |= 0x8;
+        if (op->pbdata)
+            ws_cdb[1] |= 0x4;
+        if (op->lbdata)
+            ws_cdb[1] |= 0x2;
+        if (op->ndob)
+            ws_cdb[1] |= 0x1;
+        sg_put_unaligned_be64(op->lba, ws_cdb + 2);
+        sg_put_unaligned_be32((uint32_t)op->numblocks, ws_cdb + 10);
+        ws_cdb[14] = (op->grpnum & GRPNUM_MASK);
+        break;
+    case WRITE_SAME32_LEN:
+        ws_cdb[0] = VARIABLE_LEN_OP;
+        ws_cdb[6] = (op->grpnum & GRPNUM_MASK);
+        ws_cdb[7] = WRITE_SAME32_ADD;
+        sg_put_unaligned_be16((uint16_t)WRITE_SAME32_SA, ws_cdb + 8);
+        ws_cdb[10] = ((op->wrprotect & 0x7) << 5);
+        if (op->anchor)
+            ws_cdb[10] |= 0x10;
+        if (op->unmap)
+            ws_cdb[10] |= 0x8;
+        if (op->pbdata)
+            ws_cdb[10] |= 0x4;
+        if (op->lbdata)
+            ws_cdb[10] |= 0x2;
+        if (op->ndob)
+            ws_cdb[10] |= 0x1;
+        sg_put_unaligned_be64(op->lba, ws_cdb + 12);
+        sg_put_unaligned_be32((uint32_t)op->numblocks, ws_cdb + 28);
+        break;
+    default:
+        pr2serr("do_write_same: bad cdb length %d\n", cdb_len);
+        return -1;
+    }
+
+    if (op->verbose > 1) {
+        char b[128];
+
+        pr2serr("    Write same(%d) cdb: %s\n", cdb_len,
+                sg_get_command_str(ws_cdb, cdb_len, false, sizeof(b), b));
+        pr2serr("    Data-out buffer length=%d\n", op->xfer_len);
+    }
+    if ((op->verbose > 3) && (op->xfer_len > 0)) {
+        pr2serr("    Data-out buffer contents:\n");
+        hex2stderr((const uint8_t *)dataoutp, op->xfer_len, 1);
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("Write same(%d): out of memory\n", cdb_len);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, ws_cdb, cdb_len);
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, (uint8_t *)dataoutp, op->xfer_len);
+    res = do_scsi_pt(ptvp, sg_fd, op->timeout, op->verbose);
+    ret = sg_cmds_process_resp(ptvp, "Write same", res, true /*noisy */,
+                               op->verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD:
+            {
+                bool valid;
+                int slen;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                if (valid)
+                    pr2serr("Medium or hardware error starting at lba=%"
+                            PRIu64 " [0x%" PRIx64 "]\n", ull, ull);
+            }
+            ret = sense_cat;
+            break;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            if (op->verbose)
+                sg_print_command_len(ws_cdb, cdb_len);
+            /* FALL THROUGH */
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool got_stdin = false;
+    bool if_given = false;
+    bool lba_given = false;
+    bool num_given = false;
+    bool prot_en;
+    int res, c, infd, act_cdb_len, vb, err;
+    int sg_fd = -1;
+    int ret = -1;
+    uint32_t block_size;
+    int64_t ll;
+    const char * device_name = NULL;
+    struct opts_t * op;
+    uint8_t * wBuff = NULL;
+    uint8_t * free_wBuff = NULL;
+    char ebuff[EBUFF_SZ];
+    char b[80];
+    uint8_t resp_buff[RCAP16_RESP_LEN];
+    struct opts_t opts;
+    struct stat a_stat;
+
+    op = &opts;
+    memset(op, 0, sizeof(opts));
+    op->numblocks = DEF_WS_NUMBLOCKS;
+    op->pref_cdb_size = DEF_WS_CDB_SIZE;
+    op->timeout = DEF_TIMEOUT_SECS;
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "afg:hi:l:Ln:NPRSt:TUvVw:x:",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            op->anchor = true;
+            break;
+        case 'f':
+            op->ff = true;
+            break;
+        case 'g':
+            op->grpnum = sg_get_num(optarg);
+            if ((op->grpnum < 0) || (op->grpnum > 63))  {
+                pr2serr("bad argument to '--grpnum'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+            strncpy(op->ifilename, optarg, sizeof(op->ifilename) - 1);
+            op->ifilename[sizeof(op->ifilename) - 1] = '\0';
+            if_given = true;
+            break;
+        case 'l':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->lba = (uint64_t)ll;
+            lba_given = true;
+            break;
+        case 'L':
+            op->lbdata = true;
+            break;
+        case 'n':
+            op->numblocks = sg_get_num(optarg);
+            if (op->numblocks < 0)  {
+                pr2serr("bad argument to '--num'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            num_given = true;
+            break;
+        case 'N':
+            op->ndob = true;
+            break;
+        case 'P':
+            op->pbdata = true;
+            break;
+        case 'R':
+            op->want_ws10 = true;
+            break;
+        case 'S':
+            if (DEF_WS_CDB_SIZE != op->pref_cdb_size) {
+                pr2serr("only one '--10', '--16' or '--32' please\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->pref_cdb_size = 16;
+            break;
+        case 't':
+            op->timeout = sg_get_num(optarg);
+            if (op->timeout < 0)  {
+                pr2serr("bad argument to '--timeout'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'T':
+            if (DEF_WS_CDB_SIZE != op->pref_cdb_size) {
+                pr2serr("only one '--10', '--16' or '--32' please\n");
+                return SG_LIB_CONTRADICT;
+            }
+            op->pref_cdb_size = 32;
+            break;
+        case 'U':
+            op->unmap = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'w':
+            op->wrprotect = sg_get_num(optarg);
+            if ((op->wrprotect < 0) || (op->wrprotect > 7))  {
+                pr2serr("bad argument to '--wrprotect'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'x':
+            op->xfer_len = sg_get_num(optarg);
+            if (op->xfer_len < 0) {
+                pr2serr("bad argument to '--xferlen'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (op->want_ws10 && (DEF_WS_CDB_SIZE != op->pref_cdb_size)) {
+        pr2serr("only one '--10', '--16' or '--32' please\n");
+        return SG_LIB_CONTRADICT;
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    vb = op->verbose;
+
+    if ((! if_given) && (! lba_given) && (! num_given)) {
+        pr2serr("As a precaution, one of '--in=', '--lba=' or '--num=' is "
+                "required\n");
+        return SG_LIB_CONTRADICT;
+    }
+
+    if (op->ndob) {
+        if (if_given) {
+            pr2serr("Can't have both --ndob and '--in='\n");
+            return SG_LIB_CONTRADICT;
+        }
+        if (0 != op->xfer_len) {
+            pr2serr("With --ndob only '--xferlen=0' (or not given) is "
+                    "acceptable\n");
+            return SG_LIB_CONTRADICT;
+        }
+    } else if (op->ifilename[0]) {
+        got_stdin = (0 == strcmp(op->ifilename, "-"));
+        if (! got_stdin) {
+            memset(&a_stat, 0, sizeof(a_stat));
+            if (stat(op->ifilename, &a_stat) < 0) {
+                err = errno;
+                if (vb)
+                    pr2serr("unable to stat(%s): %s\n", op->ifilename,
+                            safe_strerror(err));
+                return sg_convert_errno(err);
+            }
+            if (op->xfer_len <= 0)
+                op->xfer_len = (int)a_stat.st_size;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, vb);
+    if (sg_fd < 0) {
+        if (op->verbose)
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto err_out;
+    }
+
+    if (! op->ndob) {
+        prot_en = false;
+        if (0 == op->xfer_len) {
+            res = sg_ll_readcap_16(sg_fd, false /* pmi */, 0 /* llba */,
+                                   resp_buff, RCAP16_RESP_LEN, true,
+                                   (vb ? (vb - 1): 0));
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Read capacity(16) unit attention, try again\n");
+                res = sg_ll_readcap_16(sg_fd, false, 0, resp_buff,
+                                       RCAP16_RESP_LEN, true,
+                                       (vb ? (vb - 1): 0));
+            }
+            if (0 == res) {
+                if (vb > 3)
+                    hex2stderr(resp_buff, RCAP16_RESP_LEN, 1);
+                block_size = sg_get_unaligned_be32(resp_buff + 8);
+                prot_en = !!(resp_buff[12] & 0x1);
+                op->xfer_len = block_size;
+                if (prot_en && (op->wrprotect > 0))
+                    op->xfer_len += 8;
+            } else if ((SG_LIB_CAT_INVALID_OP == res) ||
+                       (SG_LIB_CAT_ILLEGAL_REQ == res)) {
+                if (vb)
+                    pr2serr("Read capacity(16) not supported, try Read "
+                            "capacity(10)\n");
+                res = sg_ll_readcap_10(sg_fd, false /* pmi */, 0 /* lba */,
+                                       resp_buff, RCAP10_RESP_LEN, true,
+                                       (vb ? (vb - 1): 0));
+                if (0 == res) {
+                    if (vb > 3)
+                        hex2stderr(resp_buff, RCAP10_RESP_LEN, 1);
+                    block_size = sg_get_unaligned_be32(resp_buff + 4);
+                    op->xfer_len = block_size;
+                } else {
+                    sg_get_category_sense_str(res, sizeof(b), b, vb);
+                    pr2serr("Read capacity(10): %s\n", b);
+                    pr2serr("Unable to calculate block size\n");
+                }
+            } else if (vb) {
+                sg_get_category_sense_str(res, sizeof(b), b, vb);
+                pr2serr("Read capacity(16): %s\n", b);
+                pr2serr("Unable to calculate block size\n");
+            }
+        }
+        if (op->xfer_len < 1) {
+            pr2serr("unable to deduce block size, please give '--xferlen=' "
+                    "argument\n");
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto err_out;
+        }
+        if (op->xfer_len > MAX_XFER_LEN) {
+            pr2serr("'--xferlen=%d is out of range ( want <= %d)\n",
+                    op->xfer_len, MAX_XFER_LEN);
+            ret = SG_LIB_SYNTAX_ERROR;
+            goto err_out;
+        }
+        wBuff = (uint8_t *)sg_memalign(op->xfer_len, 0, &free_wBuff, false);
+        if (NULL == wBuff) {
+            pr2serr("unable to allocate %d bytes of memory with "
+                    "sg_memalign()\n", op->xfer_len);
+            ret = sg_convert_errno(ENOMEM);
+            goto err_out;
+        }
+        if (op->ff)
+            memset(wBuff, 0xff, op->xfer_len);
+        if (op->ifilename[0]) {
+            if (got_stdin) {
+                infd = STDIN_FILENO;
+                if (sg_set_binary_mode(STDIN_FILENO) < 0)
+                    perror("sg_set_binary_mode");
+            } else {
+                if ((infd = open(op->ifilename, O_RDONLY)) < 0) {
+                    ret = sg_convert_errno(errno);
+                    snprintf(ebuff, EBUFF_SZ, ME "could not open %.400s for "
+                             "reading", op->ifilename);
+                    perror(ebuff);
+                    goto err_out;
+                } else if (sg_set_binary_mode(infd) < 0)
+                    perror("sg_set_binary_mode");
+            }
+            res = read(infd, wBuff, op->xfer_len);
+            if (res < 0) {
+                ret = sg_convert_errno(errno);
+                snprintf(ebuff, EBUFF_SZ, ME "couldn't read from %.400s",
+                         op->ifilename);
+                perror(ebuff);
+                if (! got_stdin)
+                    close(infd);
+                goto err_out;
+            }
+            if (res < op->xfer_len) {
+                pr2serr("tried to read %d bytes from %s, got %d bytes\n",
+                        op->xfer_len, op->ifilename, res);
+                pr2serr("  so pad with 0x0 bytes and continue\n");
+            }
+            if (! got_stdin)
+                close(infd);
+        } else {
+            if (vb)
+                pr2serr("Default data-out buffer set to %d zeros\n",
+                        op->xfer_len);
+            if (prot_en && (op->wrprotect > 0)) {
+               /* default for protection is 0xff, rest get 0x0 */
+                memset(wBuff + op->xfer_len - 8, 0xff, 8);
+                if (vb)
+                    pr2serr(" ... apart from last 8 bytes which are set to "
+                            "0xff\n");
+            }
+        }
+    }
+
+    ret = do_write_same(sg_fd, op, wBuff, &act_cdb_len);
+    if (ret) {
+        sg_get_category_sense_str(ret, sizeof(b), b, vb);
+        pr2serr("Write same(%d): %s\n", act_cdb_len, b);
+    }
+
+err_out:
+    if (free_wBuff)
+        free(free_wBuff);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == op->verbose) {
+        if (! sg_if_can2stderr("sg_write_same failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_write_verify.c b/src/sg_write_verify.c
new file mode 100644
index 0000000..ee6b12b
--- /dev/null
+++ b/src/sg_write_verify.c
@@ -0,0 +1,634 @@
+/*
+ * Copyright (c) 2014-2022 Douglas Gilbert
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * This program issues the SCSI command WRITE AND VERIFY to a given SCSI
+ * device. It sends the command with the logical block address passed as the
+ * LBA argument, for the given number of blocks. The number of bytes sent is
+ * supplied separately, either by the size of the given file (IF) or
+ * explicitly with ILEN.
+ *
+ * This code was contributed by Bruno Goncalves
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <limits.h>
+#include <getopt.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.21 20220127";
+
+
+#define ME "sg_write_verify: "
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+
+#define WRITE_VERIFY10_CMD      0x2e
+#define WRITE_VERIFY10_CMDLEN   10
+#define WRITE_VERIFY16_CMD      0x8e
+#define WRITE_VERIFY16_CMDLEN   16
+
+#define WRPROTECT_MASK  (0x7)
+#define WRPROTECT_SHIFT (5)
+
+#define DEF_TIMEOUT_SECS 60
+
+
+static struct option long_options[] = {
+    {"16", no_argument, 0, 'S'},
+    {"bytchk", required_argument, 0, 'b'},
+    {"dpo", no_argument, 0, 'd'},
+    {"group", required_argument, 0, 'g'},
+    {"help", no_argument, 0, 'h'},
+    {"ilen", required_argument, 0, 'I'},
+    {"in", required_argument, 0, 'i'},
+    {"lba", required_argument, 0, 'l'},
+    {"num", required_argument, 0, 'n'},
+    {"repeat", no_argument, 0, 'R'},
+    {"timeout", required_argument, 0, 't'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {"wrprotect", required_argument, 0, 'w'},
+    {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_write_verify [--16] [--bytchk=BC] [--dpo] [--group=GN] "
+            "[--help]\n"
+            "                       [--ilen=IL] [--in=IF] --lba=LBA "
+            "[--num=NUM]\n"
+            "                       [--repeat] [--timeout=TO] [--verbose] "
+            "[--version]\n"
+            "                       [--wrprotect=WPR] DEVICE\n"
+            "  where:\n"
+            "    --16|-S              do WRITE AND VERIFY(16) (default: 10)\n"
+            "    --bytchk=BC|-b BC    set BYTCHK field (default: 0)\n"
+            "    --dpo|-d             set DPO bit (default: 0)\n"
+            "    --group=GN|-g GN     GN is group number (default: 0)\n"
+            "    --help|-h            print out usage message\n"
+            "    --ilen=IL| -I IL     input (file) length in bytes, becomes "
+            "data-out\n"
+            "                         buffer length (def: deduced from IF "
+            "size)\n"
+            "    --in=IF|-i IF        IF is a file containing the data to "
+            "be written\n"
+            "    --lba=LBA|-l LBA     LBA of the first block to write "
+            "and verify;\n"
+            "                         no default, must be given\n"
+            "    --num=NUM|-n NUM     logical blocks to write and verify "
+            "(def: 1)\n"
+            "    --repeat|-R          while IF still has data to read, send "
+            "another\n"
+            "                         command, bumping LBA with up to NUM "
+            "blocks again\n"
+            "    --timeout=TO|-t TO   command timeout in seconds (def: 60)\n"
+            "    --verbose|-v         increase verbosity\n"
+            "    --version|-V         print version string then exit\n"
+            "    --wrprotect|-w WPR   WPR is the WRPROTECT field value "
+            "(def: 0)\n\n"
+            "Performs a SCSI WRITE AND VERIFY (10 or 16) command on DEVICE, "
+            "startings\nat LBA for NUM logical blocks. More commands "
+            "performed only if '--repeat'\noption given. Data to be written "
+            "is fetched from the IF file.\n"
+         );
+}
+
+/* Invokes a SCSI WRITE AND VERIFY according with CDB. Returns 0 -> success,
+ * various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+run_scsi_transaction(int sg_fd, const uint8_t *cdbp, int cdb_len,
+                     uint8_t *dop, int do_len, int timeout,
+                     bool noisy, int verbose)
+{
+    int res, sense_cat, ret;
+    struct sg_pt_base * ptvp;
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    char b[32];
+
+    snprintf(b, sizeof(b), "Write and verify(%d)", cdb_len);
+    if (verbose) {
+        char d[128];
+
+        pr2serr("    %s cdb: %s\n", b,
+                sg_get_command_str(cdbp, cdb_len, false, sizeof(d), d));
+        if ((verbose > 2) && dop && do_len) {
+            pr2serr("    Data out buffer [%d bytes]:\n", do_len);
+            hex2stderr(dop, do_len, -1);
+        }
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", b);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, cdbp, cdb_len);
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_out(ptvp, dop, do_len);
+    res = do_scsi_pt(ptvp, sg_fd, timeout, verbose);
+    ret = sg_cmds_process_resp(ptvp, b, res, noisy, verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD:    /* write or verify failed */
+            {
+                bool valid;
+                int slen;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                if (valid)
+                    pr2serr("Medium or hardware error starting at lba=%"
+                            PRIu64 " [0x%" PRIx64 "]\n", ull, ull);
+            }
+            ret = sense_cat;
+            break;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            if (verbose)
+                sg_print_command_len(cdbp, cdb_len);
+            /* FALL THROUGH */
+        case SG_LIB_CAT_PROTECTION:     /* PI failure */
+        case SG_LIB_CAT_MISCOMPARE:     /* only in bytchk=1 case */
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Invokes a SCSI WRITE AND VERIFY (10) command (SBC). Returns 0 -> success,
+* various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_write_verify10(int sg_fd, int wrprotect, bool dpo, int bytchk,
+                     unsigned int lba, int num_lb, int group,
+                     uint8_t *dop, int do_len, int timeout,
+                     bool noisy, int verbose)
+{
+    int ret;
+    uint8_t wv_cdb[WRITE_VERIFY10_CMDLEN];
+
+    memset(wv_cdb, 0, WRITE_VERIFY10_CMDLEN);
+    wv_cdb[0] = WRITE_VERIFY10_CMD;
+    wv_cdb[1] = ((wrprotect & WRPROTECT_MASK) << WRPROTECT_SHIFT);
+    if (dpo)
+        wv_cdb[1] |= 0x10;
+    if (bytchk)
+       wv_cdb[1] |= ((bytchk & 0x3) << 1);
+
+    sg_put_unaligned_be32((uint32_t)lba, wv_cdb + 2);
+    wv_cdb[6] = group & GRPNUM_MASK;
+    sg_put_unaligned_be16((uint16_t)num_lb, wv_cdb + 7);
+    ret = run_scsi_transaction(sg_fd, wv_cdb, sizeof(wv_cdb), dop, do_len,
+                               timeout, noisy, verbose);
+    return ret;
+}
+
+/* Invokes a SCSI WRITE AND VERIFY (16) command (SBC). Returns 0 -> success,
+* various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_write_verify16(int sg_fd, int wrprotect, bool dpo, int bytchk,
+                     uint64_t llba, int num_lb, int group, uint8_t *dop,
+                     int do_len, int timeout, bool noisy, int verbose)
+{
+    int ret;
+    uint8_t wv_cdb[WRITE_VERIFY16_CMDLEN];
+
+
+    memset(wv_cdb, 0, sizeof(wv_cdb));
+    wv_cdb[0] = WRITE_VERIFY16_CMD;
+    wv_cdb[1] = ((wrprotect & WRPROTECT_MASK) << WRPROTECT_SHIFT);
+    if (dpo)
+        wv_cdb[1] |= 0x10;
+    if (bytchk)
+        wv_cdb[1] |= ((bytchk & 0x3) << 1);
+
+    sg_put_unaligned_be64(llba, wv_cdb + 2);
+    sg_put_unaligned_be32((uint32_t)num_lb, wv_cdb + 10);
+    wv_cdb[14] = group & GRPNUM_MASK;
+    ret = run_scsi_transaction(sg_fd, wv_cdb, sizeof(wv_cdb), dop, do_len,
+                               timeout, noisy, verbose);
+    return ret;
+}
+
+/* Returns file descriptor ( >= 0) if successful. Else a negated sg3_utils
+ * error code is returned. */
+static int
+open_if(const char * fn, int got_stdin)
+{
+    int fd, err;
+
+    if (got_stdin)
+        fd = STDIN_FILENO;
+    else {
+        fd = open(fn, O_RDONLY);
+        if (fd < 0) {
+            err = errno;
+            pr2serr(ME "open error: %s: %s\n", fn, safe_strerror(err));
+            return -sg_convert_errno(err);
+        }
+    }
+    if (sg_set_binary_mode(fd) < 0) {
+        perror("sg_set_binary_mode");
+        return -SG_LIB_FILE_ERROR;
+    }
+    return fd;
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool do_16 = false;
+    bool dpo = false;
+    bool first_time;
+    bool given_do_16 = false;
+    bool has_filename = false;
+    bool lba_given = false;
+    bool repeat = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int sg_fd, res, c, n;
+    int bytchk = 0;
+    int group = 0;
+    int ilen = -1;
+    int ifd = -1;
+    int b_p_lb = 512;
+    int ret = 1;
+    int timeout = DEF_TIMEOUT_SECS;
+    int tnum_lb_wr = 0;
+    int verbose = 0;
+    int wrprotect = 0;
+    uint32_t num_lb = 1;
+    uint32_t snum_lb = 1;
+    uint64_t llba = 0;
+    int64_t ll;
+    uint8_t * wvb = NULL;
+    uint8_t * wrkBuff = NULL;
+    uint8_t * free_wrkBuff = NULL;
+    const char * device_name = NULL;
+    const char * ifnp;
+    char cmd_name[32];
+
+    ifnp = "";          /* keep MinGW quiet */
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "b:dg:hi:I:l:n:RSt:w:vV", long_options,
+                       &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            /* Only bytchk=0 and =1 are meaningful for this command in
+             * sbc4r02 (not =2 nor =3) but that may change in the future. */
+            bytchk = sg_get_num(optarg);
+            if ((bytchk < 0) || (bytchk > 3))  {
+                pr2serr("argument to '--bytchk' expected to be 0 to 3\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'd':
+            dpo = true;
+            break;
+        case 'g':
+            group = sg_get_num(optarg);
+            if ((group < 0) || (group > 63))  {
+                pr2serr("argument to '--group' expected to be 0 to 63\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'i':
+            ifnp = optarg;
+            has_filename = true;
+            break;
+        case 'I':
+            ilen = sg_get_num(optarg);
+            if (-1 == ilen) {
+                pr2serr("bad argument to '--ilen'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'l':
+            if (lba_given) {
+                pr2serr("must have one and only one '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            ll = sg_get_llnum(optarg);
+            if (ll < 0) {
+                pr2serr("bad argument to '--lba'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            llba = (uint64_t)ll;
+            lba_given = true;
+            break;
+        case 'n':
+            n = sg_get_num(optarg);
+            if (-1 == n) {
+                pr2serr("bad argument to '--num'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            num_lb = (uint32_t)n;
+            break;
+        case 'R':
+            repeat = true;
+            break;
+        case 'S':
+            do_16 = true;
+            given_do_16 = true;
+            break;
+        case 't':
+            timeout = sg_get_num(optarg);
+            if (timeout < 1) {
+                pr2serr("bad argument to '--timeout'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        case 'w':
+            wrprotect = sg_get_num(optarg);
+            if ((wrprotect < 0) || (wrprotect > 7))  {
+                pr2serr("wrprotect (%d) is out of range ( < %d)\n", wrprotect,
+                        7);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+       }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "version: %s\n", version_str);
+        return 0;
+    }
+
+    if (NULL == device_name) {
+        pr2serr("Missing device name!\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (! lba_given) {
+        pr2serr("need a --lba=LBA option\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (repeat) {
+        if (! has_filename) {
+            pr2serr("with '--repeat' need '--in=IF' option\n");
+            usage();
+            return SG_LIB_CONTRADICT;
+        }
+        if (ilen < 1) {
+            pr2serr("with '--repeat' need '--ilen=ILEN' option\n");
+            usage();
+            return SG_LIB_CONTRADICT;
+        } else {
+            b_p_lb = ilen / num_lb;
+            if (b_p_lb < 64) {
+                pr2serr("calculated %d bytes per logical block, too small\n",
+                        b_p_lb);
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        ret = sg_convert_errno(-sg_fd);
+        pr2serr(ME "open error: %s: %s\n", device_name, safe_strerror(-sg_fd));
+        goto err_out;
+    }
+
+    if ((! do_16) && (llba > UINT_MAX))
+        do_16 = true;
+    if ((! do_16) && (num_lb > 0xffff))
+        do_16 = true;
+    snprintf(cmd_name, sizeof(cmd_name), "Write and verify(%d)",
+             (do_16 ? 16 : 10));
+    if (verbose && (! given_do_16) && do_16)
+        pr2serr("Switching to %s because LBA or NUM too large\n", cmd_name);
+    if (verbose) {
+        pr2serr("Issue %s to device %s\n\tilen=%d", cmd_name, device_name,
+                ilen);
+        if (ilen > 0)
+            pr2serr(" [0x%x]", ilen);
+        pr2serr(", lba=%" PRIu64 " [0x%" PRIx64 "]\n\twrprotect=%d, dpo=%d, "
+                "bytchk=%d, group=%d, repeat=%d\n", llba, llba, wrprotect,
+                (int)dpo, bytchk, group, (int)repeat);
+    }
+
+    first_time = true;
+    do {
+        if (first_time) {
+            //If a file with data to write has been provided
+            if (has_filename) {
+                struct stat a_stat;
+
+                if ((1 == strlen(ifnp)) && ('-' == ifnp[0])) {
+                    ifd = STDIN_FILENO;
+                    ifnp = "<stdin>";
+                    if (verbose > 1)
+                        pr2serr("Reading input data from stdin\n");
+                } else {
+                    ifd = open_if(ifnp, 0);
+                    if (ifd < 0) {
+                        ret = -ifd;
+                        goto err_out;
+                    }
+                }
+                if (ilen < 1) {
+                    if (fstat(ifd, &a_stat) < 0) {
+                        pr2serr("Could not fstat(%s)\n", ifnp);
+                        goto err_out;
+                    }
+                    if (! S_ISREG(a_stat.st_mode)) {
+                        pr2serr("Cannot determine IF size, please give "
+                                "'--ilen='\n");
+                        goto err_out;
+                    }
+                    ilen = (int)a_stat.st_size;
+                    if (ilen < 1) {
+                        pr2serr("%s file size too small\n", ifnp);
+                        goto err_out;
+                    } else if (verbose)
+                        pr2serr("Using file size of %d bytes\n", ilen);
+                }
+                if (NULL == (wrkBuff = (uint8_t *)sg_memalign(ilen, 0,
+                                           &free_wrkBuff, verbose > 3))) {
+                    pr2serr(ME "out of memory\n");
+                    ret = sg_convert_errno(ENOMEM);
+                    goto err_out;
+                }
+                wvb = (uint8_t *)wrkBuff;
+                res = read(ifd, wvb, ilen);
+                if (res < 0) {
+                    pr2serr("Could not read from %s", ifnp);
+                    goto err_out;
+                }
+                if (res < ilen) {
+                    pr2serr("Read only %d bytes (expected %d) from %s\n", res,
+                            ilen, ifnp);
+                    if (repeat)
+                        pr2serr("Will scale subsequent pieces when "
+                                "repeat=true, but this is first\n");
+                    goto err_out;
+                }
+            } else {
+                if (ilen < 1) {
+                    if (verbose)
+                        pr2serr("Default write length to %d*%d=%d bytes\n",
+                                num_lb, 512, 512 * num_lb);
+                    ilen = 512 * num_lb;
+                }
+                if (NULL == (wrkBuff = (uint8_t *)sg_memalign(ilen, 0,
+                                           &free_wrkBuff, verbose > 3))) {
+                    pr2serr(ME "out of memory\n");
+                    ret = sg_convert_errno(ENOMEM);
+                    goto err_out;
+                }
+                wvb = (uint8_t *)wrkBuff;
+                /* Not sure about this: default contents to 0xff bytes */
+                memset(wrkBuff, 0xff, ilen);
+            }
+            first_time = false;
+            snum_lb = num_lb;
+        } else {    /* repeat=true, first_time=false, must be reading file */
+            llba += snum_lb;
+            res = read(ifd, wvb, ilen);
+            if (res < 0) {
+                pr2serr("Could not read from %s", ifnp);
+                goto err_out;
+            } else {
+                if (verbose > 1)
+                pr2serr("Subsequent read from %s got %d bytes\n", ifnp, res);
+                if (0 == res)
+                    break;
+                if (res < ilen) {
+                    snum_lb = (uint32_t)(res / b_p_lb);
+                    n = res % b_p_lb;
+                    if (0 != n)
+                        pr2serr(">>> warning: ignoring last %d bytes of %s\n",
+                                n, ifnp);
+                    if (snum_lb < 1)
+                        break;
+                }
+            }
+        }
+        if (do_16)
+            res = sg_ll_write_verify16(sg_fd, wrprotect, dpo, bytchk, llba,
+                                       snum_lb, group, wvb, ilen, timeout,
+                                       verbose > 0, verbose);
+        else
+            res = sg_ll_write_verify10(sg_fd, wrprotect, dpo, bytchk,
+                                       (unsigned int)llba, snum_lb, group,
+                                       wvb, ilen, timeout, verbose > 0,
+                                       verbose);
+        ret = res;
+        if (repeat && (0 == ret))
+            tnum_lb_wr += snum_lb;
+        if (ret || (snum_lb != num_lb))
+            break;
+    } while (repeat);
+
+err_out:
+    if (repeat)
+        pr2serr("%d [0x%x] logical blocks written, in total\n", tnum_lb_wr,
+                tnum_lb_wr);
+    if (free_wrkBuff)
+        free(free_wrkBuff);
+    if ((ifd >= 0) && (STDIN_FILENO != ifd))
+        close(ifd);
+    res = sg_cmds_close_device(sg_fd);
+    if (res < 0) {
+        pr2serr("close error: %s\n", safe_strerror(-res));
+        if (0 == ret)
+            ret = sg_convert_errno(-res);
+    }
+    if (ret && (0 == verbose)) {
+        if (! sg_if_can2stderr("sg_write_verify failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_write_x.c b/src/sg_write_x.c
new file mode 100644
index 0000000..89c9125
--- /dev/null
+++ b/src/sg_write_x.c
@@ -0,0 +1,2678 @@
+/*
+ * Copyright (c) 2017-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * The utility can send six variants of the SCSI WRITE command: (normal)
+ * WRITE(16 or 32), WRITE ATOMIC(16 or 32), ORWRITE(16 or 32),
+ * WRITE SAME(16 or 32), WRITE SCATTERED (16 or 32) or WRITE
+ * STREAM(16 or 32).
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <limits.h>
+#include <ctype.h>
+#include <sys/types.h>  /* needed for lseek() */
+#include <sys/stat.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.31 20220217";
+
+/* Protection Information refers to 8 bytes of extra information usually
+ * associated with each logical block and is often abbreviated to PI while
+ * its fields: reference-tag (4 bytes), application-tag (2 bytes) and
+ * tag-mask (2 bytes) are often abbreviated to RT, AT and TM respectively.
+ * And the LBA Range Descriptor associated with the WRITE SCATTERED command
+ * is abbreviated to RD. A degenerate RD is one where length components,
+ ( and perhaps the LBA, are zero; it is not illegal according to T10 but are
+ * a little tricky to handle when scanning and little extra information
+ * is provided. */
+
+#define ORWRITE16_OP 0x8b
+#define WRITE_16_OP 0x8a
+#define WRITE_ATOMIC16_OP 0x9c
+#define WRITE_SAME16_OP 0x93
+#define SERVICE_ACTION_OUT_16_OP 0x9f   /* WRITE SCATTERED (16) uses this */
+#define WRITE_SCATTERED16_SA 0x12
+#define WRITE_STREAM16_OP 0x9a
+#define VARIABLE_LEN_OP 0x7f
+#define ORWRITE32_SA 0xe
+#define WRITE_32_SA 0xb
+#define WRITE_ATOMIC32_SA 0xf
+#define WRITE_SAME_SA 0xd
+#define WRITE_SCATTERED32_SA 0x11
+#define WRITE_STREAM32_SA 0x10
+#define WRITE_X_16_LEN 16
+#define WRITE_X_32_LEN 32
+#define WRITE_X_32_ADD 0x18
+#define RCAP10_RESP_LEN 8
+#define RCAP16_RESP_LEN 32
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_TIMEOUT_SECS 120    /* might need more for large NUM */
+#define DEF_WR_NUMBLOCKS 0      /* do nothing; for safety */
+#define DEF_RT 0xffffffff
+#define DEF_AT 0xffff
+#define DEF_TM 0xffff
+#define EBUFF_SZ 256
+
+#define MAX_NUM_ADDR 128
+
+#ifndef UINT32_MAX
+#define UINT32_MAX ((uint32_t)-1)
+#endif
+#ifndef UINT16_MAX
+#define UINT16_MAX ((uint16_t)-1)
+#endif
+
+static struct option long_options[] = {
+    {"32", no_argument, 0, '3'},
+    {"16", no_argument, 0, '6'},
+    {"app-tag", required_argument, 0, 'a'},
+    {"app_tag", required_argument, 0, 'a'},
+    {"atomic", required_argument, 0, 'A'},
+    {"bmop", required_argument, 0, 'B'},
+    {"bs", required_argument, 0, 'b'},
+    {"combined", required_argument, 0, 'c'},
+    {"dld", required_argument, 0, 'D'},
+    {"dpo", no_argument, 0, 'd'},
+    {"dry-run", no_argument, 0, 'x'},
+    {"dry_run", no_argument, 0, 'x'},
+    {"fua", no_argument, 0, 'f'},
+    {"grpnum", required_argument, 0, 'g'},
+    {"generation", required_argument, 0, 'G'},
+    {"help", no_argument, 0, 'h'},
+    {"in", required_argument, 0, 'i'},
+    {"lba", required_argument, 0, 'l'},
+    {"normal", no_argument, 0, 'N'},
+    {"num", required_argument, 0, 'n'},
+    {"offset", required_argument, 0, 'o'},
+    {"or", no_argument, 0, 'O'},
+    {"quiet", no_argument, 0, 'Q'},
+    {"ref-tag", required_argument, 0, 'r'},
+    {"ref_tag", required_argument, 0, 'r'},
+    {"same", required_argument, 0, 'M'},
+    {"scat-file", required_argument, 0, 'q'},
+    {"scat_file", required_argument, 0, 'q'},
+    {"scat-raw", no_argument, 0, 'R'},
+    {"scat_raw", no_argument, 0, 'R'},
+    {"scattered", required_argument, 0, 'S'},
+    {"stream", required_argument, 0, 'T'},
+    {"strict", no_argument, 0, 's'},
+    {"tag-mask", required_argument, 0, 't'},
+    {"tag_mask", required_argument, 0, 't'},
+    {"timeout", required_argument, 0, 'I'},
+    {"unmap", required_argument, 0, 'u'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {"wrprotect", required_argument, 0, 'w'},
+    {0, 0, 0, 0},
+};
+
+struct opts_t {
+    bool do_16;                 /* default when --32 not given */
+    bool do_32;
+    bool do_anchor;             /* from  --unmap=U_A , bit 1; WRITE SAME */
+    bool do_atomic;             /* selects  WRITE ATOMIC(16 or 32) */
+                                /*  --atomic=AB  AB --> .atomic_boundary */
+    bool do_combined;           /* -c DOF --> .scat_lbdof */
+    bool do_or;                 /* -O  ORWRITE(16 or 32) */
+    bool do_quiet;              /* -Q  suppress some messages */
+    bool do_scat_raw;
+    bool do_same;               /* -M  WRITE SAME(16 or 32) */
+                                /*  --same=NDOB  NDOB --> .ndob */
+    bool do_scattered;          /* -S  WRITE SCATTERED(16 or 32) */
+                                /*  --scattered=RD  RD --> .scat_num_lbard */
+    bool do_stream;             /* -T  WRITE STREAM(16 or 32) */
+                                /*  --stream=ID  ID --> .str_id */
+    bool do_unmap;              /* from --unmap=U_A , bit 0; WRITE SAME */
+    bool do_write_normal;       /* -N  WRITE (16 or 32) */
+    bool expect_pi_do;          /* expect protection information (PI) which
+                                 * is 8 bytes long following each logical
+                                 * block in the data out buffer. */
+    bool dpo;                   /* "Disable Page Out" bit field */
+    bool fua;           /* "Force Unit Access" bit field */
+    bool ndob;          /* "No Data-Out Buffer" from --same=NDOB */
+    bool verbose_given;
+    bool version_given;
+    int dld;            /* "Duration Limit Descriptor" bit mask; bit 0 -->
+                         * DLD0, bit 1 --> DLD1, bit 2 --> DLD2
+                         * only WRITE(16) and WRITE SCATTERED(16) */
+    int dry_run;        /* temporary write when used more than once */
+    int grpnum;         /* "Group Number", 0 to 0x3f (GRPNUM_MASK) */
+    int help;
+    int pi_type;        /* -1: unknown: 0: type 0 (none): 1: type 1 */
+    int strict;         /* > 0, report then exit on questionable meta data */
+    int timeout;        /* timeout (in seconds) to abort SCSI commands */
+    int verbose;        /* incremented for each -v */
+    int wrprotect;      /* is ORPROTECT field for ORWRITE */
+    uint8_t bmop;       /* bit mask operators for ORWRITE(32) */
+    uint8_t pgp;        /* previous generation processing for ORWRITE(32) */
+    uint16_t app_tag;   /* part of protection information (def: 0xffff) */
+    uint16_t atomic_boundary;   /* when 0 atomic write spans given length */
+    uint16_t scat_lbdof; /* by construction this must be >= 1 */
+    uint16_t scat_num_lbard;    /* RD from --scattered=RD, number of LBA
+                                 * Range Descriptors */
+    uint16_t str_id;    /* (stream ID) is for WRITE STREAM */
+    uint16_t tag_mask;  /* part of protection information (def: 0xffff) */
+    uint32_t bs;        /* logical block size (def: 0). 0 implies use READ
+                         * CAPACITY(10 or 16) to determine */
+    uint32_t bs_pi_do;  /* logical block size plus PI, if any. This value is
+                         * used as the actual block size */
+    uint32_t if_dlen;   /* bytes to read after .if_offset from .if_name,
+                         * if 0 given, read rest of .if_name */
+    uint32_t numblocks; /* defaults to 0, number of blocks (of user data) to
+                         * write */
+    uint32_t orw_eog;   /* from --generation=EOG,NOG (first argument) */
+    uint32_t orw_nog;   /* from --generation=EOG,NOG (for ORWRITE) */
+    uint32_t ref_tag;   /* part of protection information (def: 0xffffffff) */
+    uint64_t lba;       /* "Logical Block Address", for non-scattered use */
+    uint64_t if_offset; /* byte offset in .if_name to start reading */
+    uint64_t tot_lbs;   /* from READ CAPACITY */
+    ssize_t xfer_bytes;     /* derived value: bs_pi_do * numblocks */
+                            /* for WRITE SCATTERED .xfer_bytes < do_len */
+    const char * device_name;
+    const char * if_name;       /* from --in=IF */
+    const char * scat_filename; /* from --scat-file=SF */
+    const char * cmd_name;      /* e.g. 'Write atomic' */
+    char cdb_name[24];          /* e.g. 'Write atomic(16)' */
+};
+
+static const char * xx_wr_fname = "sg_write_x.bin";
+static const uint32_t lbard_sz = 32;
+static const char * lbard_str = "LBA range descriptor";
+
+
+static void
+usage(int do_help)
+{
+    if (do_help < 2) {
+        pr2serr("Usage:\n"
+            "sg_write_x [--16] [--32] [--app-tag=AT] [--atomic=AB] "
+            "[--bmop=OP,PGP]\n"
+            "           [--bs=BS] [--combined=DOF] [--dld=DLD] [--dpo] "
+            "[--dry-run]\n"
+            "           [--fua] [--generation=EOG,NOG] [--grpnum=GN] "
+            "[--help] --in=IF\n"
+            "           [--lba=LBA,LBA...] [--normal] [--num=NUM,NUM...]\n"
+            "           [--offset=OFF[,DLEN]] [--or] [--quiet] "
+            "[--ref-tag=RT]\n"
+            "           [--same=NDOB] [--scat-file=SF] [--scat-raw] "
+            "[--scattered=RD]\n"
+            "           [--stream=ID] [--strict] [--tag-mask=TM] "
+            "[--timeout=TO]\n"
+            "           [--unmap=U_A] [--verbose] [--version] "
+            "[--wrprotect=WRP]\n"
+            "           DEVICE\n");
+        if (1 != do_help) {
+            pr2serr("\nOr the corresponding short option usage:\n"
+                "sg_write_x [-6] [-3] [-a AT] [-A AB] [-B OP,PGP] [-b BS] "
+                "[-c DOF] [-D DLD]\n"
+                "           [-d] [-x] [-f] [-G EOG,NOG] [-g GN] [-h] -i IF "
+                "[-l LBA,LBA...]\n"
+                "           [-N] [-n NUM,NUM...] [-o OFF[,DLEN]] [-O] [-Q] "
+                "[-r RT] [-M NDOB]\n"
+                "           [-q SF] [-R] [-S RD] [-T ID] [-s] [-t TM] [-I TO] "
+                "[-u U_A] [-v]\n"
+                "           [-V] [-w WPR] DEVICE\n"
+                   );
+            pr2serr("\nUse '-h' or '--help' for more help\n");
+            return;
+        }
+        pr2serr("  where:\n"
+            "    --16|-6            send 16 byte cdb variant (this is "
+            "default action)\n"
+            "    --32|-3            send 32 byte cdb variant of command "
+            "(def: 16 byte)\n"
+            "    --app-tag=AT|-a AT    expected application tag field "
+            "(def: 0xffff)\n"
+            "    --atomic=AB|-A AB    send WRITE ATOMIC command with AB "
+            "being its\n"
+            "                         Atomic Boundary field (0 to 0xffff)\n"
+            "    --bmop=OP,PGP|-p OP,PGP    set BMOP field to OP and "
+            " Previous\n"
+            "                               Generation Processing field "
+            "to PGP\n"
+            "    --bs=BS|-b BS      block size (def: use READ CAPACITY), "
+            "if power of\n"
+            "                       2: logical block size, otherwise: "
+            "actual block size\n"
+            "    --combined=DOF|-c DOF    scatter list and data combined "
+            "for WRITE\n"
+            "                             SCATTERED, data starting at "
+            "offset DOF which\n"
+            "                             has units of sizeof(LB+PI); "
+            "sizeof(PI)=8n or 0\n"
+            "    --dld=DLD|-D DLD    set duration limit descriptor (dld) "
+            "bits (def: 0)\n"
+            "    --dpo|-d           set DPO (disable page out) field "
+            "(def: clear)\n"
+            "    --dry-run|-x       exit just before sending SCSI write "
+            "command\n"
+            "    --fua|-f           set FUA (force unit access) field "
+            "(def: clear)\n"
+            "    --generation=EOG,NOG    set Expected ORWgeneration field "
+            "to EOG\n"
+            "        |-G EOG,NOG         and New ORWgeneration field to "
+            "NOG\n"
+            );
+        pr2serr(
+            "    --grpnum=GN|-g GN    GN is group number field (def: 0, "
+            "range: 0 to 31)\n"
+            "    --help|-h          use multiple times for different "
+            "usage messages\n"
+            "    --in=IF|-i IF      IF is file to fetch NUM blocks of "
+            "data from.\n"
+            "                       Blocks written to DEVICE. 1 or no "
+            "blocks read\n"
+            "                       in the case of WRITE SAME\n"
+            "    --lba=LBA,LBA...     list of LBAs (Logical Block Addresses) "
+            "to start\n"
+            "        |-l LBA,LBA...   writes (def: --lba=0). Alternative is "
+            "--scat-file=SF\n"
+            "    --normal|-N        send 'normal' WRITE command (default "
+            "when no other\n"
+            "                       command option given)\n"
+            "    --num=NUM,NUM...     NUM is number of logical blocks to "
+            "write (def:\n"
+            "        |-n NUM,NUM...   --num=0). Number of block sent is "
+            "sum of NUMs\n"
+            "    --offset=OFF[,DLEN]    OFF is byte offset in IF to start "
+            "reading from\n"
+            "        |-o OFF[,DLEN]     (def: 0), then read DLEN bytes(def: "
+            "rest of IF)\n"
+            "    --or|-O            send ORWRITE command\n"
+            "    --quiet|-Q         suppress some informational messages\n"
+            "    --ref-tag=RT|-r RT     expected reference tag field (def: "
+            "0xffffffff)\n"
+            "    --same=NDOB|-M NDOB    send WRITE SAME command. NDOB (no "
+            "data out buffer)\n"
+            "                           can be either 0 (do send buffer) or "
+            "1 (don't)\n"
+            "    --scat-file=SF|-q SF    file containing LBA, NUM pairs, "
+            "see manpage\n"
+            "    --scat-raw|-R      read --scat_file=SF as binary (def: "
+            "ASCII hex)\n"
+            "    --scattered=RD|-S RD    send WRITE SCATTERED command with "
+            "RD range\n"
+            "                            descriptors (RD can be 0 when "
+            "--combined= given)\n"
+            "    --stream=ID|-T ID    send WRITE STREAM command with its "
+            "STR_ID\n"
+            "                         field set to ID\n"
+            "    --strict|-s        exit if read less than requested from "
+            "IF ;\n"
+            "                       require variety of WRITE to be given "
+            "as option\n"
+            "    --tag-mask=TM|-t TM    tag mask field (def: 0xffff)\n"
+            "    --timeout=TO|-I TO    command timeout (unit: seconds) "
+            "(def: 120)\n"
+            "    --unmap=U_A|-u U_A    0 clears both UNMAP and ANCHOR bits "
+            "(default),\n"
+            "                          1 sets UNMAP, 2 sets ANCHOR, 3 sets "
+            "both\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string then exit\n"
+            "    --wrprotect=WPR|-w WPR    WPR is the WRPROTECT field "
+            "value (def: 0)\n\n"
+            "Performs a SCSI WRITE (normal), ORWRITE, WRITE ATOMIC, WRITE "
+            "SAME, WRITE\nSCATTERED, or WRITE STREAM command. A 16 or 32 "
+            "byte cdb variant can be\nselected. The --in=IF option (data to "
+            "be written) is required apart from\nwhen --same=1 (i.e. when "
+            "NDOB is set). If no WRITE variant option is given\nthen, in "
+            "the absence of --strict, a (normal) WRITE is performed. Only "
+            "WRITE\nSCATTERED uses multiple LBAs and NUMs, or a SF file "
+            "with multiple pairs.\nThe --num=NUM field defaults to 0 (do "
+            "nothing) for safety. Using '-h'\nmultiple times shows the "
+            "applicable options for each command variant.\n"
+            );
+    } else if (2 == do_help) {
+        printf("WRITE ATOMIC (16 or 32) applicable options:\n"
+            "  sg_write_x --atomic=AB --in=IF [--16] [--32] [--app-tag=AT] "
+            "[--bs=BS]\n"
+            "             [--dpo] [--fua] [--grpnum=GN] [--lba=LBA] "
+            "[--num=NUM]\n"
+            "             [--offset=OFF[,DLEN]] [--ref-tag=RT] [--strict] "
+            "[--tag-mask=TM]\n"
+            "             [--timeout=TO] [--wrprotect=WRP] DEVICE\n"
+            "\n"
+            "normal WRITE (32) applicable options:\n"
+            "  sg_write_x --normal --in=IF --32 [--app-tag=AT] [--bs=BS] "
+            "[--dpo] [--fua]\n"
+            "             [--grpnum=GN] [--lba=LBA] [--num=NUM] "
+            "[--offset=OFF[,DLEN]]\n"
+            "             [--ref-tag=RT] [--strict] [--tag-mask=TM] "
+            "[--timeout=TO]\n"
+            "             [--wrprotect=WRP] DEVICE\n"
+            "\n"
+            "normal WRITE (16) applicable options:\n"
+            "  sg_write_x --normal --in=IF [--16] [--bs=BS] [--dld=DLD] "
+            "[--dpo] [--fua]\n"
+            "            [--grpnum=GN] [--lba=LBA] [--num=NUM] "
+            "[--offset=OFF[,DLEN]]\n"
+            "            [--strict] [--timeout=TO] [--verbose] "
+            "[--wrprotect=WRP] DEVICE\n"
+            "\n"
+            "ORWRITE (32) applicable options:\n"
+            "  sg_write_x --or --in=IF --32 [--bmop=OP,PGP] [--bs=BS] "
+            "[--dpo] [--fua]\n"
+            "             [--generation=EOG,NOG] [--grpnum=GN] [--lba=LBA] "
+            "[--num=NUM]\n"
+            "             [--offset=OFF{,DLEN]] [--strict] [--timeout=TO]\n"
+            "             [--wrprotect=ORP] DEVICE\n"
+            "\n"
+            "ORWRITE (16) applicable options:\n"
+            "  sg_write_x --or --in=IF [--16] [--bs=BS] [--dpo] [--fua] "
+            "[--grpnum=GN]\n"
+            "             [--lba=LBA] [--num=NUM] [--offset=OFF[,DLEN]] "
+            "[--strict]\n"
+            "             [--timeout=TO] [--wrprotect=ORP] DEVICE\n"
+            "\n"
+              );
+    } else if (3 == do_help) {
+        printf("WRITE SAME (32) applicable options:\n"
+            "  sg_write_x --same=NDOB --32 [--app-tag=AT] [--bs=BS] "
+            "[--grpnum=GN]\n"
+            "             [--in=IF] [--lba=LBA] [--num=NUM] "
+            "[--offset=OFF[,DLEN]]\n"
+            "             [--ref-tag=RT] [--strict] [--tag-mask=TM] "
+            "[--timeout=TO]\n"
+            "             [--unmap=U_A] [--wrprotect=WRP] DEVICE\n"
+            "\n"
+            "WRITE SCATTERED (32) applicable options:\n"
+            "  sg_write_x --scattered --in=IF --32 [--app-tag=AT] "
+            "[--bs=BS]\n"
+            "             [--combined=DOF] [--dpo] [--fua] [--grpnum=GN]\n"
+            "             [--lba=LBA,LBA...] [--num=NUM,NUM...] "
+            "[--offset=OFF[,DLEN]]\n"
+            "             [--ref-tag=RT] [--scat-file=SF] [--scat-raw] "
+            "[--strict]\n"
+            "             [--tag-mask=TM] [--timeout=TO] [--wrprotect=WRP] "
+            "DEVICE\n"
+            "\n"
+            "WRITE SCATTERED (16) applicable options:\n"
+            "  sg_write_x --scattered --in=IF [--bs=BS] [--combined=DOF] "
+            "[--dld=DLD]\n"
+            "             [--dpo] [--fua] [--grpnum=GN] [--lba=LBA,LBA...]\n"
+            "             [--num=NUM,NUM...] [--offset=OFF[,DLEN]] "
+            "[--scat-raw]\n"
+            "             [--scat-file=SF] [--strict] [--timeout=TO] "
+            "[--wrprotect=WRP]\n"
+            "             DEVICE\n"
+            "\n"
+            "WRITE STREAM (32) applicable options:\n"
+            "  sg_write_x --stream=ID --in=IF --32 [--app-tag=AT] "
+            "[--bs=BS] [--dpo]\n"
+            "             [--fua] [--grpnum=GN] [--lba=LBA] [--num=NUM]\n"
+            "             [--offset=OFF[,DLEN]] [--ref-tag=RT] [--strict] "
+            "[--tag-mask=TM]\n"
+            "             [--timeout=TO] [--verbose] [--wrprotect=WRP] "
+            "DEVICE\n"
+            "\n"
+            "WRITE STREAM (16) applicable options:\n"
+            "  sg_write_x --stream=ID --in=IF [--16] [--bs=BS] [--dpo] "
+            "[--fua]\n"
+            "             [--grpnum=GN] [--lba=LBA] [--num=NUM] "
+            "[--offset=OFF[,DLEN]]\n"
+            "             [--strict] [--timeout=TO] [--wrprotect=WRP] "
+            "DEVICE\n"
+            "\n"
+              );
+    } else {
+        printf("Notes:\n"
+            " - all 32 byte cdb variants, apart from ORWRITE(32), need type "
+            "1, 2, or 3\n"
+            "   protection information active on the DEVICE\n"
+            " - all commands can take one or more --verbose (-v) options "
+            "and/or the\n"
+            "   --dry-run option\n"
+            " - all WRITE X commands will accept --scat-file=SF and "
+            "optionally --scat-raw\n"
+            "   options but only the first lba,num pair is used (any "
+            "more are ignored)\n"
+            " - when '--rscat-aw --scat-file=SF' are used then the binary "
+            "format expected in\n"
+            "   SF is as defined for the WRITE SCATTERED commands. "
+            "That is 32 bytes\n"
+            "   of zeros followed by the first LBA range descriptor "
+            "followed by the\n"
+            "   second LBA range descriptor, etc. Each LBA range "
+            "descriptor is 32 bytes\n"
+            "   long with an 8 byte LBA at offset 0 and a 4 byte "
+            "number_of_logical_\n"
+            "   blocks at offset 8 (both big endian). The 'pad' following "
+            "the last LBA\n"
+            "   range descriptor does not need to be given\n"
+            " - WRITE SCATTERED(32) additionally has expected initial "
+            "LB reference tag,\n"
+            "   application tag and LB application tag mask fields in the "
+            "LBA range\n"
+            "   descriptor. If --strict is given then all reserved fields "
+            "are checked\n"
+            "   for zeros, an error is generated for non zero bytes.\n"
+            " - when '--lba=LBA,LBA...' is used on commands other than "
+            "WRITE SCATTERED\n"
+            "   then only the first LBA value is used.\n"
+            " - when '--num=NUM,NUM...' is used on commands other than "
+            "WRITE SCATTERED\n"
+            "   then only the first NUM value is used.\n"
+            " - whenever '--lba=LBA,LBA...' is used then "
+            "'--num=NUM,NUM...' should\n"
+            "   also be used. Also they should have the same number of "
+            "elements.\n"
+              );
+    }
+}
+
+/* Returns 0 if successful, else sg3_utils error code. */
+static int
+bin_read(int fd, uint8_t * up, uint32_t len, const char * fname)
+{
+    int res, err;
+
+    res = read(fd, up, len);
+    if (res < 0) {
+        err = errno;
+        pr2serr("Error doing read of %s file: %s\n", fname,
+                safe_strerror(err));
+        return sg_convert_errno(err);
+    }
+    if ((uint32_t)res < len) {
+        pr2serr("Short (%u) read of %s file, wanted %u\n", (unsigned int)res,
+                fname, len);
+        return SG_LIB_FILE_ERROR;
+    }
+    return 0;
+}
+
+/* Returns true if num_of_f_chars of ASCII 'f' or 'F' characters are found
+ * in sequence. Any leading "0x" or "0X" is ignored; otherwise false is
+ * returned (and the comparison stops when the first mismatch is found).
+ * For example a sequence of 'f' characters in a null terminated C string
+ * that is two characters shorter than the requested num_of_f_chars will
+ * compare the null character in the string with 'f', find them unequal,
+ * stop comparing and return false. */
+static bool
+all_ascii_f_s(const char * cp, int num_of_f_chars)
+{
+    if ((NULL == cp) || (num_of_f_chars < 1))
+        return false;   /* define degenerate cases */
+    if (('0' == cp[0]) && (('x' == cp[1]) || ('X' == cp[1])))
+        cp += 2;
+    for ( ; num_of_f_chars >= 0 ; --num_of_f_chars, ++cp) {
+        if ('F' != toupper((uint8_t)*cp))
+            return false;
+    }
+    return true;
+}
+
+/* Read numbers (up to 64 bits in size) from command line (comma (or
+ * (single) space) separated list). Assumed decimal unless prefixed
+ * by '0x', '0X' or contains trailing 'h' or 'H' (which indicate hex).
+ * Returns 0 if ok, or 1 if error. */
+static int
+build_lba_arr(const char * inp, uint64_t * lba_arr, uint32_t * lba_arr_len,
+              int max_arr_len)
+{
+    int in_len, k;
+    int64_t ll;
+    const char * lcp;
+    char * cp;
+    char * c2p;
+
+    if ((NULL == inp) || (NULL == lba_arr) ||
+        (NULL == lba_arr_len))
+        return 1;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *lba_arr_len = 0;
+    if ('-' == inp[0]) {        /* read from stdin */
+        pr2serr("'--lba' cannot be read from stdin\n");
+        return 1;
+    } else {        /* list of numbers (default decimal) on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfFhHxXiIkKmMgGtTpP, ");
+        if (in_len != k) {
+            pr2serr("build_lba_arr: error at pos %d\n", k + 1);
+            return 1;
+        }
+        for (k = 0; k < max_arr_len; ++k) {
+            ll = sg_get_llnum(lcp);
+            if (-1 != ll) {
+                lba_arr[k] = (uint64_t)ll;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("build_lba_arr: error at pos %d\n",
+                        (int)(lcp - inp + 1));
+                return 1;
+            }
+        }
+        *lba_arr_len = (uint32_t)(k + 1);
+        if (k == max_arr_len) {
+            pr2serr("build_lba_arr: array length exceeded\n");
+            return 1;
+        }
+    }
+    return 0;
+}
+
+/* Read numbers (up to 32 bits in size) from command line (comma (or
+ * (single) space) separated list). Assumed decimal unless prefixed
+ * by '0x', '0X' or contains trailing 'h' or 'H' (which indicate hex).
+ * Returns 0 if ok, else a sg3_utils error code is returned. */
+static int
+build_num_arr(const char * inp, uint32_t * num_arr, uint32_t * num_arr_len,
+              int max_arr_len)
+{
+    int in_len, k;
+    const char * lcp;
+    int64_t ll;
+    char * cp;
+    char * c2p;
+
+    if ((NULL == inp) || (NULL == num_arr) ||
+        (NULL == num_arr_len))
+        return SG_LIB_LOGIC_ERROR;
+    lcp = inp;
+    in_len = strlen(inp);
+    if (0 == in_len)
+        *num_arr_len = 0;
+    if ('-' == inp[0]) {        /* read from stdin */
+        pr2serr("'--len' cannot be read from stdin\n");
+        return SG_LIB_SYNTAX_ERROR;
+    } else {        /* list of numbers (default decimal) on command line */
+        k = strspn(inp, "0123456789aAbBcCdDeEfFhHxXiIkKmMgGtTpP, ");
+        if (in_len != k) {
+            pr2serr("%s: error at pos %d\n", __func__, k + 1);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        for (k = 0; k < max_arr_len; ++k) {
+            ll = sg_get_llnum(lcp);
+            if (-1 != ll) {
+                if (ll > UINT32_MAX) {
+                    pr2serr("%s: number exceeds 32 bits at pos %d\n",
+                            __func__, (int)(lcp - inp + 1));
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                num_arr[k] = (uint32_t)ll;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp)
+                    cp = c2p;
+                if (NULL == cp)
+                    break;
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                pr2serr("%s: error at pos %d\n", __func__,
+                        (int)(lcp - inp + 1));
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+        *num_arr_len = (uint32_t)(k + 1);
+        if (k == max_arr_len) {
+            pr2serr("%s: array length exceeded\n", __func__);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    return 0;
+}
+
+/* Tries to parse LBA,NUM[,RT,AP,TM] on one line, comma separated. Returns
+ * 0 if parsed ok, else 999 if nothing parsed, else error (currently always
+ * SG_LIB_SYNTAX_ERROR). If protection information fields not given, then
+ * default values are given (i.e. all 0xff bytes). Ignores all spaces and
+ * tabs and everything after '#' on lcp (assumed to be an ASCII line that
+ * is null terminated). If successful and 'up' is non NULL then writes a
+ * LBA range descriptor starting at 'up'. */
+static int
+parse_scat_pi_line(const char * lcp, uint8_t * up, uint32_t * sum_num)
+{
+    bool ok;
+    int k;
+    int64_t ll;
+    const char * cp;
+    const char * bp;
+    char c[1024];
+
+    bp = c;
+    cp = strchr(lcp, '#');
+    lcp += strspn(lcp, " \t");
+    if (('\0' == *lcp) || (cp && (lcp >= cp)))
+        return 999;   /* blank line or blank prior to first '#' */
+    if (cp) {   /* copy from first non whitespace ... */
+        memcpy(c, lcp, cp - lcp);  /* ... to just prior to first '#' */
+        c[cp - lcp] = '\0';
+    } else {
+        /* ... to end of line, including null */
+        snprintf(c, sizeof(c), "%s", lcp);
+    }
+    ll = sg_get_llnum(bp);
+    ok = ((-1 != ll) || all_ascii_f_s(bp, 16));
+    if (! ok) {
+        pr2serr("%s: error reading LBA (first) item on ", __func__);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (up)
+        sg_put_unaligned_be64((uint64_t)ll, up + 0);
+    ok = false;
+    cp = strchr(bp, ',');
+    if (cp) {
+        bp = cp + 1;
+        if (*bp) {
+            ll = sg_get_llnum(bp);
+            if (-1 != ll)
+                ok = true;
+        }
+    }
+    if ((! ok) || (ll > UINT32_MAX)) {
+        pr2serr("%s: error reading NUM (second) item on ", __func__);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (up)
+        sg_put_unaligned_be32((uint32_t)ll, up + 8);
+    if (sum_num)
+        *sum_num += (uint32_t)ll;
+    /* now for 3 PI items */
+    for (k = 0; k < 3; ++k) {
+        ok = true;
+        cp = strchr(bp, ',');
+        if (NULL == cp)
+            break;
+        bp = cp + 1;
+        if (*bp) {
+            cp += strspn(bp, " \t");
+            if ('\0' == *cp)
+                break;
+            else if (',' == *cp) {
+                if (0 == k)
+                    ll = DEF_RT;
+                else
+                    ll = DEF_AT; /* DEF_AT and DEF_TM have same value */
+            } else {
+                ll = sg_get_llnum(bp);
+                if (-1 == ll)
+                    ok = false;
+            }
+        }
+        if (! ok) {
+            pr2serr("%s: error reading item %d NUM item on ", __func__,
+                    k + 3);
+            break;
+        }
+        switch (k) {
+        case 0:
+            if (ll > UINT32_MAX) {
+                pr2serr("%s: error with item 3, >0xffffffff; on ", __func__);
+                ok = false;
+            } else if (up)
+                sg_put_unaligned_be32((uint32_t)ll, up + 12);
+            break;
+        case 1:
+            if (ll > UINT16_MAX) {
+                pr2serr("%s: error with item 4, >0xffff; on ", __func__);
+                ok = false;
+            } else if (up)
+                sg_put_unaligned_be16((uint16_t)ll, up + 16);
+            break;
+        case 2:
+            if (ll > UINT16_MAX) {
+                pr2serr("%s: error with item 5, >0xffff; on ", __func__);
+                ok = false;
+            } else if (up)
+                sg_put_unaligned_be16((uint16_t)ll, up + 18);
+            break;
+        }
+        if (! ok)
+            break;
+    }
+    if (! ok)
+        return SG_LIB_SYNTAX_ERROR;
+    for ( ; k < 3; ++k) {
+        switch (k) {
+        case 0:
+            if (up)
+                sg_put_unaligned_be32((uint32_t)DEF_RT, up + 12);
+            break;
+        case 1:
+            if (up)
+                sg_put_unaligned_be16((uint16_t)DEF_AT, up + 16);
+            break;
+        case 2:
+            if (up)
+                sg_put_unaligned_be16((uint16_t)DEF_TM, up + 18);
+            break;
+        }
+    }
+    return 0;
+}
+
+/* Read pairs or quintets from a scat_file and places them in a T10 scatter
+ * list array is built starting at at t10_scat_list_out (i.e. as per T10 the
+ * first 32 bytes are zeros followed by the first LBA range descriptor (also
+ * 32 bytes long) then the second LBA range descriptor, etc. The pointer
+ * t10_scat_list_out may be NULL in which case the T10 list array is not
+ * built but all other operations take place; this can be useful for sizing
+ * how large the area holding that list needs to be. The max_list_blen may
+ * also be 0. If do_16 is true then only LBA,NUM pairs are expected,
+ * loosely formatted with numbers found alternating between LBA and NUM, with
+ * an even number of elements required overall. If do_16 is false then a
+ * stricter format for quintets is expected: each non comment line should
+ * contain: LBA,NUM[,RT,AT,TM] . If RT,AT,TM are not given then they assume
+ * their defaults (i.e. 0xffffffff, 0xffff, 0xffff). Each number (64 bits for
+ * the LBA, 32 bits for NUM and RT, 16 bit for AT and TM) may be a comma,
+ * space or tab separated list. Assumed decimal unless prefixed by '0x', '0X'
+ * or contains trailing 'h' or 'H' (which indicate hex). Returns 0 if ok,
+ * else error number. If ok also yields the number of LBA range descriptors
+ * written in num_scat_elems and the sum of NUM elements found. Note that
+ * sum_num is not initialized to 0. If parse_one is true then exits
+ * after one LBA range descriptor is decoded. */
+static int
+build_t10_scat(const char * scat_fname, bool do_16, bool parse_one,
+               uint8_t * t10_scat_list_out, uint16_t * num_scat_elems,
+               uint32_t * sum_num, uint32_t max_list_blen)
+{
+    bool have_stdin = false;
+    bool del_fp = false;
+    bool bit0, ok;
+    int off = 0;
+    int in_len, k, j, m, n, res, err;
+    int64_t ll;
+    char * lcp;
+    uint8_t * up = t10_scat_list_out;
+    FILE * fp = NULL;
+    char line[1024];
+
+    if (up) {
+        if (max_list_blen < 64) {
+            pr2serr("%s: t10_scat_list_out is too short\n", __func__);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        memset(up, 0, max_list_blen);
+    }
+    n = lbard_sz;
+
+    have_stdin = ((1 == strlen(scat_fname)) && ('-' == scat_fname[0]));
+    if (have_stdin) {
+        fp = stdin;
+        scat_fname = "<stdin>";
+    } else {
+        fp = fopen(scat_fname, "r");
+        if (NULL == fp) {
+            err = errno;
+            pr2serr("%s: unable to open %s: %s\n", __func__, scat_fname,
+                    safe_strerror(err));
+            return sg_convert_errno(err);
+        }
+        del_fp = true;
+    }
+    for (j = 0; j < 1024; ++j) {/* loop over lines in file */
+        if ((max_list_blen > 0) && ((n + lbard_sz) > max_list_blen))
+            goto fini;
+        if (NULL == fgets(line, sizeof(line), fp))
+            break;
+        // could improve with carry_over logic if sizeof(line) too small
+        in_len = strlen(line);
+        if (in_len > 0) {
+            if ('\n' == line[in_len - 1]) {
+                --in_len;
+                line[in_len] = '\0';
+            }
+        }
+        if (in_len < 1)
+            continue;
+        lcp = line;
+        m = strspn(lcp, " \t");
+        if (m == in_len)
+            continue;
+        lcp += m;
+        in_len -= m;
+        if ('#' == *lcp)    /* Comment? If so skip rest of line */
+            continue;
+        k = strspn(lcp, "0123456789aAbBcCdDeEfFhHxXiIkKmMgGtTpP ,\t");
+        if ((k < in_len) && ('#' != lcp[k])) {
+            pr2serr("%s: syntax error in %s at line %d, pos %d\n",
+                    __func__, scat_fname, j + 1, m + k + 1);
+            goto bad_exit;
+        }
+        if (! do_16) {
+            res = parse_scat_pi_line(lcp, up ? (up + n) : up, sum_num);
+            if (999 == res)
+                ;
+            else if (0 == res) {
+                n += lbard_sz;
+                if (parse_one)
+                    goto fini;
+            } else {
+                if (SG_LIB_CAT_NOT_READY == res)
+                    goto bad_mem_exit;
+                pr2serr("line %d in %s\n", j + 1, scat_fname);
+                goto bad_exit;
+            }
+            continue;
+        }
+        for (k = 0; k < 1024; ++k) {
+            ll = sg_get_llnum(lcp);
+            ok = ((-1 != ll) || all_ascii_f_s(lcp, 16));
+            if (ok) {
+                bit0 = !! (0x1 & (off + k));
+                if (bit0) {
+                    if (ll > UINT32_MAX) {
+                        pr2serr("%s: number exceeds 32 bits in line %d, at "
+                                "pos %d of %s\n", __func__, j + 1,
+                                (int)(lcp - line + 1), scat_fname);
+                        goto bad_exit;
+                    }
+                    if (up)
+                        sg_put_unaligned_be32((uint32_t)ll, up + n + 8);
+                    if (sum_num)
+                        *sum_num += (uint32_t)ll;
+                    n += lbard_sz;  /* skip to next LBA range descriptor */
+                    if (parse_one)
+                        goto fini;
+                } else {
+                    if (up)
+                        sg_put_unaligned_be64((uint64_t)ll, up + n + 0);
+                }
+                lcp = strpbrk(lcp, " ,\t");
+                if (NULL == lcp)
+                    break;
+                lcp += strspn(lcp, " ,\t");
+                if ('\0' == *lcp)
+                    break;
+            } else {        /* no valid number found */
+                if ('#' == *lcp) {
+                    --k;
+                    break;
+                }
+                pr2serr("%s: error on line %d, at pos %d\n", __func__, j + 1,
+                        (int)(lcp - line + 1));
+                goto bad_exit;
+            }
+        }   /* inner for loop(k) over line elements */
+        off += (k + 1);
+    }       /* outer for loop(j) over lines */
+    if (do_16 && (0x1 & off)) {
+        pr2serr("%s: expect LBA,NUM pairs but decoded odd number\n  from "
+                "%s\n", __func__, scat_fname);
+        goto bad_exit;
+    }
+fini:
+    *num_scat_elems = (n / lbard_sz) - 1;
+    if (del_fp)
+        fclose(fp);
+    return 0;
+bad_exit:
+    if (del_fp)
+        fclose(fp);
+    return SG_LIB_SYNTAX_ERROR;
+bad_mem_exit:
+    if (del_fp)
+        fclose(fp);
+    return SG_LIB_CAT_NOT_READY;        /* flag output buffer too small */
+}
+
+static bool
+is_pi_default(const struct opts_t * op)
+{
+    return ((DEF_AT == op->app_tag) && (DEF_RT == op->ref_tag) &&
+            (DEF_TM == op->tag_mask));
+}
+
+/* Given a t10 parameter list header (32 zero bytes) for WRITE SCATTERED
+ * (16 or 32) followed by n RDs with a total length of at least
+ * max_lbrds_blen bytes, find "n" and increment where num_lbard points
+ * n times. Further get the LBA length component from each RD and add each
+ * length into where sum_num points. Note: the caller probably wants to zero
+ * where num_lbard and sum_num point before invoking this function. If all
+ * goes well return true, else false. If a degenerate RD is detected then
+ * if 'RD' (from --scattered=RD) is 0 then stop looking for further RDs;
+ * otherwise keep going. Currently overlapping LBA range descriptors are no
+ * checked for. If op->strict > 0 then the first 32 bytes are checked for
+ * zeros; any non-zero bytes will report to stderr, stop the check and
+ * return false. If op->strict > 0 then the trailing 20 or 12 bytes (only
+ * 12 if RT, AT and TM fields (for PI) are present) are checked for zeros;
+ * any non-zero bytes cause the same action as the previous check. If
+ * the number of RDs (when 'RD' from --scattered=RD > 0) is greater than
+ * the number of RDs found then a report is sent to stderr and if op->strict
+ * > 0 then returns false, else returns true.  */
+static bool
+check_lbrds(const uint8_t * up, uint32_t max_lbrds_blen,
+            const struct opts_t * op, uint16_t * num_lbard,
+            uint32_t * sum_num)
+{
+    bool ok;
+    int k, j, n;
+    const int max_lbrd_start = max_lbrds_blen - lbard_sz;
+    int vb = op->verbose;
+
+    if (op->strict) {
+        if (max_lbrds_blen < lbard_sz) {
+            pr2serr("%s: %ss too short (%d < 32)\n", __func__, lbard_str,
+                    max_lbrds_blen);
+            return false;
+        }
+        if (! sg_all_zeros(up, lbard_sz)) {
+            pr2serr("%s: first 32 bytes of WRITE SCATTERED data-out buffer "
+                    "should be zero.\nFound non-zero byte.\n", __func__);
+            return false;
+        }
+    }
+    if (max_lbrds_blen < (2 * lbard_sz)) {
+        *num_lbard = 0;
+        return true;
+    }
+    n = op->scat_num_lbard ? (int)op->scat_num_lbard : -1;
+    for (k = lbard_sz, j = 0; k < max_lbrd_start; k += lbard_sz, ++j) {
+        if ((n < 0) && sg_all_zeros(up + k + 0, 12)) { /* degenerate LBA */
+            if (vb)   /* ... range descriptor terminator if --scattered=0 */
+                pr2serr("%s: degenerate %s stops scan at k=%d (num_rds=%d)\n",
+                        __func__, lbard_str, k, j);
+            break;
+        }
+        *sum_num += sg_get_unaligned_be32(up + k + 8);
+        *num_lbard += 1;
+        if (op->strict) {
+            ok = true;
+            if (op->wrprotect) {
+                if (! sg_all_zeros(up + k + 20, 12))
+                    ok = false;
+            } else if (! sg_all_zeros(up + k + 12, 20))
+                ok = false;
+            if (! ok) {
+                pr2serr("%s: %s %d non zero in reserved fields\n", __func__,
+                        lbard_str, (k / lbard_sz) - 1);
+                return false;
+            }
+        }
+        if (n >= 0) {
+            if (--n <= 0)
+                break;
+        }
+    }
+    if ((k < max_lbrd_start) && op->strict) { /* check pad all zeros */
+        k += lbard_sz;
+        j = max_lbrds_blen - k;
+        if (! sg_all_zeros(up + k, j)) {
+            pr2serr("%s: pad (%d bytes) following %ss is non zero\n",
+                    __func__, j, lbard_str);
+            return false;
+        }
+    }
+    if (vb > 2)
+        pr2serr("%s: about to return true, num_lbard=%u, sum_num=%u "
+                "[k=%d, n=%d]\n", __func__, *num_lbard, *sum_num, k, n);
+    return true;
+}
+
+static int
+sum_num_lbards(const uint8_t * up, int num_lbards)
+{
+    int sum = 0;
+    int k, n;
+
+    for (k = 0, n = lbard_sz; k < num_lbards; ++k, n += lbard_sz)
+        sum += sg_get_unaligned_be32(up + n + 8);
+    return sum;
+}
+
+/* Returns 0 if successful, else sg3_utils error code. */
+static int
+do_write_x(int sg_fd, const void * dataoutp, int dout_len,
+           const struct opts_t * op)
+{
+    int k, ret, res, sense_cat, cdb_len, vb, err;
+    uint8_t x_cdb[WRITE_X_32_LEN];        /* use for both lengths */
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_pt_base * ptvp;
+
+    memset(x_cdb, 0, sizeof(x_cdb));
+    vb = op->verbose;
+    cdb_len = op->do_16 ? WRITE_X_16_LEN : WRITE_X_32_LEN;
+    if (16 == cdb_len) {
+        if (! op->do_scattered)
+            sg_put_unaligned_be64(op->lba, x_cdb + 2);
+        x_cdb[14] = (op->grpnum & GRPNUM_MASK);
+    } else {
+        x_cdb[0] = VARIABLE_LEN_OP;
+        x_cdb[6] = (op->grpnum & GRPNUM_MASK);
+        x_cdb[7] = WRITE_X_32_ADD;
+        if (! op->do_scattered)
+            sg_put_unaligned_be64(op->lba, x_cdb + 12);
+    }
+    if (op->do_write_normal) {
+        if (16 == cdb_len)  {
+            x_cdb[0] = WRITE_16_OP;
+            x_cdb[1] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[1] |= 0x10;
+            if (op->fua)
+                x_cdb[1] |= 0x8;
+            if (op->dld) {
+                if (op->dld & 1)
+                    x_cdb[14] |= 0x40;
+                if (op->dld & 2)
+                    x_cdb[14] |= 0x80;
+                if (op->dld & 4)
+                    x_cdb[1] |= 0x1;
+            }
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 10);
+        } else {        /* 32 byte WRITE */
+            sg_put_unaligned_be16((uint16_t)WRITE_32_SA, x_cdb + 8);
+            x_cdb[10] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[10] |= 0x10;
+            if (op->fua)
+                x_cdb[10] |= 0x8;
+            if (op->dld) {      /* added in sbc4r19 */
+                if (op->dld & 1)
+                    x_cdb[11] |= 0x1;
+                if (op->dld & 2)
+                    x_cdb[11] |= 0x2;
+                if (op->dld & 4)
+                    x_cdb[11] |= 0x4;
+            }
+            sg_put_unaligned_be32(op->ref_tag, x_cdb + 20);
+            sg_put_unaligned_be16(op->app_tag, x_cdb + 24);
+            sg_put_unaligned_be16(op->tag_mask, x_cdb + 26);
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 28);
+        }
+    } else if (op->do_atomic) {
+        if (16 == cdb_len)  {
+            if (op->numblocks > UINT16_MAX) {
+                pr2serr("Need WRITE ATOMIC(32) since blocks exceed 65535\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            x_cdb[0] = WRITE_ATOMIC16_OP;
+            x_cdb[1] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[1] |= 0x10;
+            if (op->fua)
+                x_cdb[1] |= 0x8;
+            sg_put_unaligned_be16(op->atomic_boundary, x_cdb + 10);
+            sg_put_unaligned_be16((uint16_t)op->numblocks, x_cdb + 12);
+        } else {        /* 32 byte WRITE ATOMIC */
+            sg_put_unaligned_be16(op->atomic_boundary, x_cdb + 4);
+            sg_put_unaligned_be16((uint16_t)WRITE_ATOMIC32_SA, x_cdb + 8);
+            x_cdb[10] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[10] |= 0x10;
+            if (op->fua)
+                x_cdb[10] |= 0x8;
+            sg_put_unaligned_be32(op->ref_tag, x_cdb + 20);
+            sg_put_unaligned_be16(op->app_tag, x_cdb + 24);
+            sg_put_unaligned_be16(op->tag_mask, x_cdb + 26);
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 28);
+        }
+    } else if (op->do_or) {     /* ORWRITE(16 or 32) */
+        if (16 == cdb_len) {
+            x_cdb[0] = ORWRITE16_OP;
+            x_cdb[1] = ((op->wrprotect & 0x7) << 5);  /* actually ORPROTECT */
+            if (op->dpo)
+                x_cdb[1] |= 0x10;
+            if (op->fua)
+                x_cdb[1] |= 0x8;
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 10);
+        } else {
+            x_cdb[2] = op->bmop;
+            x_cdb[3] = op->pgp;
+            sg_put_unaligned_be16((uint16_t)ORWRITE32_SA, x_cdb + 8);
+            x_cdb[10] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[10] |= 0x10;
+            if (op->fua)
+                x_cdb[10] |= 0x8;
+            sg_put_unaligned_be32(op->orw_eog, x_cdb + 20);
+            sg_put_unaligned_be32(op->orw_nog, x_cdb + 24);
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 28);
+        }
+    } else if (op->do_same) {
+        if (16 == cdb_len) {
+            x_cdb[0] = WRITE_SAME16_OP;
+            x_cdb[1] = ((op->wrprotect & 0x7) << 5);
+            if (op->do_anchor)
+                x_cdb[1] |= 0x10;
+            if (op->do_unmap)
+                x_cdb[1] |= 0x8;
+            if (op->ndob)
+                x_cdb[1] |= 0x1;
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 10);
+        } else {
+            sg_put_unaligned_be16((uint16_t)WRITE_SAME_SA, x_cdb + 8);
+            x_cdb[10] = ((op->wrprotect & 0x7) << 5);
+            if (op->do_anchor)
+                x_cdb[10] |= 0x10;
+            if (op->do_unmap)
+                x_cdb[10] |= 0x8;
+            if (op->ndob)
+                x_cdb[10] |= 0x1;
+            /* Expected initial logical block reference tag */
+            sg_put_unaligned_be32(op->ref_tag, x_cdb + 20);
+            sg_put_unaligned_be16(op->app_tag, x_cdb + 24);
+            sg_put_unaligned_be16(op->tag_mask, x_cdb + 26);
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 28);
+        }
+    } else if (op->do_scattered) {
+        if (16 == cdb_len) {
+            x_cdb[0] = SERVICE_ACTION_OUT_16_OP;
+            x_cdb[1] = WRITE_SCATTERED16_SA;
+            x_cdb[2] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[2] |= 0x10;
+            if (op->fua)
+                x_cdb[2] |= 0x8;
+            if (op->dld) {
+                if (op->dld & 1)
+                    x_cdb[14] |= 0x40;
+                if (op->dld & 2)
+                    x_cdb[14] |= 0x80;
+                if (op->dld & 4)
+                    x_cdb[2] |= 0x1;
+            }
+            sg_put_unaligned_be16(op->scat_lbdof, x_cdb + 4);
+            sg_put_unaligned_be16(op->scat_num_lbard, x_cdb + 8);
+            /* Spec says Buffer Transfer Length field (BTL) is the number
+             * of (user) Logical Blocks in the data-out buffer and that BTL
+             * may be 0. So the total data-out buffer length in bytes is:
+             *      (scat_lbdof + numblocks) * actual_block_size   */
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 10);
+        } else {
+            sg_put_unaligned_be16((uint16_t)WRITE_SCATTERED32_SA, x_cdb + 8);
+            x_cdb[10] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[10] |= 0x10;
+            if (op->fua)
+                x_cdb[10] |= 0x8;
+            sg_put_unaligned_be16(op->scat_lbdof, x_cdb + 12);
+            sg_put_unaligned_be16(op->scat_num_lbard, x_cdb + 16);
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 28);
+            /* ref_tag, app_tag and tag_mask placed in scatter list */
+        }
+    } else if (op->do_stream) {
+        if (16 == cdb_len) {
+            x_cdb[0] = WRITE_STREAM16_OP;
+            x_cdb[1] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[1] |= 0x10;
+            if (op->fua)
+                x_cdb[1] |= 0x8;
+            sg_put_unaligned_be16(op->str_id, x_cdb + 10);
+            sg_put_unaligned_be16((uint16_t)op->numblocks, x_cdb + 12);
+        } else {
+            sg_put_unaligned_be16(op->str_id, x_cdb + 4);
+            sg_put_unaligned_be16((uint16_t)WRITE_STREAM32_SA, x_cdb + 8);
+            x_cdb[10] = ((op->wrprotect & 0x7) << 5);
+            if (op->dpo)
+                x_cdb[10] |= 0x10;
+            if (op->fua)
+                x_cdb[10] |= 0x8;
+            sg_put_unaligned_be32(op->ref_tag, x_cdb + 20);
+            sg_put_unaligned_be16(op->app_tag, x_cdb + 24);
+            sg_put_unaligned_be16(op->tag_mask, x_cdb + 26);
+            sg_put_unaligned_be32(op->numblocks, x_cdb + 28);
+        }
+    } else {
+        pr2serr("%s: bad cdb name or length (%d)\n", __func__, cdb_len);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (vb > 1) {
+        char b[128];
+
+        pr2serr("    %s cdb: %s\n", op->cdb_name,
+                sg_get_command_str(x_cdb, cdb_len, false, sizeof(b), b));
+    }
+    if (op->do_scattered && (vb > 2) && (dout_len > 31)) {
+        uint32_t sod_off = op->bs_pi_do * op->scat_lbdof;
+        const uint8_t * up = (const uint8_t *)dataoutp;
+
+        pr2serr("    %s scatter list, number of %ss: %u\n", op->cdb_name,
+                lbard_str, op->scat_num_lbard);
+        pr2serr("      byte offset of data_to_write: %u, dout_len: %d\n",
+                sod_off, dout_len);
+        up += lbard_sz;       /* step over parameter list header */
+        for (k = 0; k < (int)op->scat_num_lbard; ++k, up += lbard_sz) {
+            pr2serr("        desc %d: LBA=0x%" PRIx64 " numblocks=%" PRIu32
+                    "%s", k, sg_get_unaligned_be64(up + 0),
+                    sg_get_unaligned_be32(up + 8), (op->do_16 ? "\n" : " "));
+            if (op->do_32)
+                pr2serr("rt=0x%x at=0x%x tm=0x%x\n",
+                        sg_get_unaligned_be32(up + 12),
+                        sg_get_unaligned_be16(up + 16),
+                        sg_get_unaligned_be16(up + 18));
+            if ((uint32_t)(((k + 2) * lbard_sz) + 20) > sod_off) {
+                pr2serr("Warning: possible clash of descriptor %u with "
+                        "data_to_write\n", k);
+                if (op->strict > 1)
+                    return SG_LIB_FILE_ERROR;
+            }
+        }
+    }
+    if ((vb > 3) && (dout_len > 0)) {
+        if ((dout_len > 1024) && (vb < 7)) {
+            pr2serr("    Data-out buffer contents (first 1024 of %u "
+                    "bytes):\n", dout_len);
+            hex2stdout((const uint8_t *)dataoutp, 1024, 1);
+            pr2serr("    Above: dout's first 1024 of %u bytes [%s]\n",
+                    dout_len, op->cdb_name);
+        } else {
+            pr2serr("    Data-out buffer contents (length=%u):\n", dout_len);
+            hex2stderr((const uint8_t *)dataoutp, (int)dout_len, 1);
+        }
+    }
+    if (op->dry_run) {
+        if (vb)
+            pr2serr("Exit just before sending %s due to --dry-run\n",
+                    op->cdb_name);
+        if (op->dry_run > 1) {
+            int w_fd;
+
+            w_fd = open(xx_wr_fname, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+            if (w_fd < 0) {
+                err = errno;
+                perror(xx_wr_fname);
+                return sg_convert_errno(err);
+            }
+            res = write(w_fd, dataoutp, dout_len);
+            if (res < 0) {
+                err = errno;
+                perror(xx_wr_fname);
+                close(w_fd);
+                return sg_convert_errno(err);
+            }
+            close(w_fd);
+            printf("Wrote %u bytes to %s", dout_len, xx_wr_fname);
+            if (op->do_scattered)
+                printf(", LB data offset: %u\nNumber of %ss: %u\n",
+                       op->scat_lbdof, lbard_str, op->scat_num_lbard);
+            else
+                printf("\n");
+        }
+        return 0;
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", op->cdb_name);
+        return sg_convert_errno(ENOMEM);
+    }
+    set_scsi_pt_cdb(ptvp, x_cdb, cdb_len);
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    if (dout_len > 0)
+        set_scsi_pt_data_out(ptvp, (uint8_t *)dataoutp, dout_len);
+    else if (vb && (! op->ndob))
+        pr2serr("%s:  dout_len==0, so empty dout buffer\n",
+                op->cdb_name);
+    res = do_scsi_pt(ptvp, sg_fd, op->timeout, vb);
+    ret = sg_cmds_process_resp(ptvp, op->cdb_name, res, true /*noisy */, vb,
+                               &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD:
+            {
+                bool valid;
+                int slen;
+                uint64_t ull = 0;
+
+                slen = get_scsi_pt_sense_len(ptvp);
+                valid = sg_get_sense_info_fld(sense_b, slen, &ull);
+                if (valid) {
+                    pr2serr("Medium or hardware error starting at ");
+                    if (op->do_scattered) {
+                        if (0 == ull)
+                            pr2serr("%s=<not reported>\n", lbard_str);
+                        else
+                            pr2serr("%s=%" PRIu64 " (origin 0)\n", lbard_str,
+                                    ull - 1);
+                        if (sg_get_sense_cmd_spec_fld(sense_b, slen, &ull)) {
+                            if (0 == ull)
+                                pr2serr("  Number of successfully written "
+                                        "%ss is 0 or not reported\n",
+                                        lbard_str);
+                            else
+                                pr2serr("  Number of successfully written "
+                                        "%ss is %u\n", lbard_str,
+                                        (uint32_t)ull);
+                        }
+                    } else
+                        pr2serr("lba=%" PRIu64 " [0x%" PRIx64 "]\n", ull,
+                                ull);
+                }
+            }
+            ret = sense_cat;
+            break;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            if (vb)
+                sg_print_command_len(x_cdb, cdb_len);
+            ret = sense_cat;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+/* Returns 0 if successful, else sg3_utils error code. */
+static int
+do_read_capacity(int sg_fd, struct opts_t *op)
+{
+    bool prot_en = false;
+    int res;
+    int vb = op->verbose;
+    char b[80];
+    uint8_t resp_buff[RCAP16_RESP_LEN];
+
+    res = sg_ll_readcap_16(sg_fd, false /* pmi */, 0 /* llba */, resp_buff,
+                           RCAP16_RESP_LEN, true, (vb ? (vb - 1): 0));
+    if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+        pr2serr("Read capacity(16) unit attention, try again\n");
+        res = sg_ll_readcap_16(sg_fd, false, 0, resp_buff, RCAP16_RESP_LEN,
+                               true, (vb ? (vb - 1): 0));
+    }
+    if (0 == res) {
+        uint32_t pi_len = 0;
+
+        if (vb > 3) {
+            pr2serr("Read capacity(16) response:\n");
+            hex2stderr(resp_buff, RCAP16_RESP_LEN, 1);
+        }
+        op->bs = sg_get_unaligned_be32(resp_buff + 8);
+        op->tot_lbs = sg_get_unaligned_be64(resp_buff + 0) + 1;
+        prot_en = !!(resp_buff[12] & 0x1);
+        if (prot_en) {
+            uint32_t pi_exp;
+
+            op->pi_type = ((resp_buff[12] >> 1) & 0x7) + 1;
+            pi_exp = 0xf & (resp_buff[13] >> 4);
+            pi_len = 8 * (1 << pi_exp);
+            if (op->wrprotect > 0) {
+                op->bs_pi_do = op->bs + pi_len;
+                if (vb > 1)
+                    pr2serr("  For data out buffer purposes the effective "
+                            "block size is %u (lb size\n  is %u) because "
+                            "PROT_EN=1, PI_EXP=%u and WRPROTECT>0\n", op->bs,
+                            pi_exp, op->bs_pi_do);
+           }
+        } else {    /* device formatted to PI type 0 (i.e. none) */
+            op->pi_type = 0;
+            if (op->wrprotect > 0) {
+                if (vb)
+                    pr2serr("--wrprotect (%d) expects PI but %s says it "
+                            "has none\n", op->wrprotect, op->device_name);
+                if (op->strict)
+                    return SG_LIB_FILE_ERROR;
+                else if (vb)
+                    pr2serr("  ... continue but could be dangerous\n");
+            }
+        }
+        if (vb) {
+            uint8_t d[2];
+
+            pr2serr("Read capacity(16) response fields:\n");
+            pr2serr("  Last_LBA=0x%" PRIx64 "  LB size: %u (with PI: "
+                    "%u) bytes  p_type=%u\n", op->tot_lbs - 1,
+                    op->bs, op->bs + (prot_en ? pi_len : 0),
+                    ((resp_buff[12] >> 1) & 0x7));
+            pr2serr("  prot_en=%u [PI type=%u] p_i_exp=%u  lbppb_exp=%u  "
+                    "lbpme,rz=%u,", prot_en, op->pi_type,
+                    ((resp_buff[13] >> 4) & 0xf), (resp_buff[13] & 0xf),
+                    !!(resp_buff[14] & 0x80));
+            memcpy(d, resp_buff + 14, 2);
+            d[0] &= 0x3f;
+            pr2serr("%u  low_ali_lba=%u\n", !!(resp_buff[14] & 0x40),
+                    sg_get_unaligned_be16(d));
+        }
+    } else if ((SG_LIB_CAT_INVALID_OP == res) ||
+               (SG_LIB_CAT_ILLEGAL_REQ == res)) {
+        if (vb)
+            pr2serr("Read capacity(16) not supported, try Read "
+                    "capacity(10)\n");
+        res = sg_ll_readcap_10(sg_fd, false /* pmi */, 0 /* lba */,
+                               resp_buff, RCAP10_RESP_LEN, true,
+                               (vb ? (vb - 1): 0));
+        if (0 == res) {
+            if (vb > 3) {
+                pr2serr("Read capacity(10) response:\n");
+                hex2stderr(resp_buff, RCAP10_RESP_LEN, 1);
+            }
+            op->tot_lbs = sg_get_unaligned_be32(resp_buff + 0) + 1;
+            op->bs = sg_get_unaligned_be32(resp_buff + 4);
+        } else {
+            strcpy(b,"OS error");
+            if (res > 0)
+                sg_get_category_sense_str(res, sizeof(b), b, vb);
+            else
+                snprintf(b, sizeof(b), "error: %d", res);
+            pr2serr("Read capacity(10): %s\n", b);
+            pr2serr("Unable to calculate block size\n");
+            return (res > 0) ? res : SG_LIB_FILE_ERROR;
+        }
+    } else {
+        if (vb) {
+            strcpy(b,"OS error");
+            if (res > 0)
+                sg_get_category_sense_str(res, sizeof(b), b, vb);
+            pr2serr("Read capacity(16): %s\n", b);
+            pr2serr("Unable to calculate block size\n");
+        }
+        return (res > 0) ? res : SG_LIB_FILE_ERROR;
+    }
+    op->bs_pi_do = op->expect_pi_do ? (op->bs + 8) : op->bs;
+    return 0;
+}
+
+#define WANT_ZERO_EXIT 9999
+static const char * const opt_long_ctl_str =
+    "36a:A:b:B:c:dD:Efg:G:hi:I:l:M:n:No:Oq:Qr:RsS:t:T:u:vVw:x";
+
+/* command line processing, options and arguments. Returns 0 if ok,
+ * returns WANT_ZERO_EXIT so upper level yields an exist status of zero.
+ * Other return values (mainly SG_LIB_SYNTAX_ERROR) indicate errors. */
+static int
+parse_cmd_line(struct opts_t *op, int argc, char *argv[],
+               const char ** lba_opp, const char ** num_opp)
+{
+    bool fail_if_strict = false;
+    int c, j;
+    int64_t ll;
+    const char * cp;
+
+    while (1) {
+        int opt_ind = 0;
+
+        c = getopt_long(argc, argv, opt_long_ctl_str, long_options, &opt_ind);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case '3':       /* same as --32 */
+            op->do_32 = true;
+            break;
+        case '6':       /* same as --16 */
+            op->do_16 = true;
+            break;
+        case 'a':
+            j = sg_get_num(optarg);
+            if ((j < 0) || (j > (int)UINT16_MAX)) {
+                pr2serr("bad argument to '--app-tag='. Expect 0 to 0xffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->app_tag = (uint16_t)j;
+            break;
+        case 'A':
+            j = sg_get_num(optarg);
+            if ((j < 0) || (j > (int)UINT16_MAX)) {
+                pr2serr("bad argument to '--atomic='. Expect 0 to 0xffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->atomic_boundary = (uint16_t)j;
+            op->do_atomic = true;
+            op->cmd_name = "Write atomic";
+            break;
+        case 'b':                        /* logical block size in bytes */
+            j = sg_get_num(optarg); /* 0 -> look up with READ CAPACITY */
+            if ((j < 0) || (j > (1 << 28))) {
+                pr2serr("bad argument to '--bs='. Expect 0 or greater\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (j > 0) {
+                int k;
+                int m = j;
+                int highest_ind;
+
+                if (j < 512) {
+                    pr2serr("warning: --bs=BS value is < 512 which seems too "
+                            "small, continue\n");
+                    fail_if_strict = true;
+                }
+                if (0 != (j % 8)) {
+                    pr2serr("warning: --bs=BS value is not a multiple of 8, "
+                            "unexpected, continue\n");
+                    fail_if_strict = true;
+                }
+                for (k = 0, highest_ind = 0; k < 28; ++ k, m >>= 1) {
+                    if (1 & m)
+                        highest_ind = k;
+                }       /* loop should get log_base2(j) */
+                k = 1 << highest_ind;
+                if (j == k) {   /* j is a power of two; actual and logical
+                                 * block size is assumed to be the same */
+                    op->bs = (uint32_t)j;
+                    op->bs_pi_do = op->bs;
+                } else {  /* j is not power_of_two, use as actual LB size */
+                    op->bs = (uint32_t)k;       /* power_of_two less than j */
+                    op->bs_pi_do = (uint32_t)j;
+                }
+            } else {    /* j==0, let READCAP sort this out */
+                op->bs = 0;
+                op->bs_pi_do = 0;
+            }
+            break;
+        case 'B':       /* --bmop=OP,PGP (for ORWRITE(32)) */
+            j = sg_get_num(optarg);
+            if ((j < 0) || (j > 7)) {
+                pr2serr("bad first argument to '--bmop='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->bmop = (uint8_t)j;
+            if ((cp = strchr(optarg, ','))) {
+                j = sg_get_num(cp + 1);
+                if ((j < 0) || (j > 15)) {
+                    pr2serr("bad second argument to '--bmop='\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->pgp = (uint8_t)j;
+            }
+            break;
+        case 'c':       /* --combined=DOF for W SCATTERED, DOF: data offset */
+            j = sg_get_num(optarg);
+            if (j < 0) {
+                pr2serr("bad argument to '--combined='. Expect 0 to "
+                        "0x7fffffff\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->scat_lbdof = (uint16_t)j;
+            op->do_combined = true;
+            break;
+        case 'd':
+            op->dpo = true;
+            break;
+        case 'D':
+            op->dld = sg_get_num(optarg);
+            if ((op->dld < 0) || (op->dld > 7))  {
+                pr2serr("bad argument to '--dld=', expect 0 to 7 "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'f':
+            op->fua = true;
+            break;
+        case 'g':
+            op->grpnum = sg_get_num(optarg);
+            if ((op->grpnum < 0) || (op->grpnum > 63))  {
+                pr2serr("bad argument to '--grpnum'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'G':       /* --generation=EOG,NOG */
+            ll = sg_get_llnum(optarg);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("bad first argument to '--generation='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->orw_eog = (uint32_t)ll;
+            if ((cp = strchr(optarg, ','))) {
+                ll = sg_get_llnum(cp + 1);
+                if ((ll < 0) || (ll > UINT32_MAX)) {
+                    pr2serr("bad second argument to '--generation='\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->orw_nog = (uint32_t)ll;
+            } else {
+                pr2serr("need two arguments with --generation=EOG,NOG and "
+                        "they must be comma separated\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'h':
+            ++op->help;
+            break;
+        case '?':
+            pr2serr("\n");
+            usage((op->help > 0) ? op->help : 0);
+            return SG_LIB_SYNTAX_ERROR;
+        case 'i':
+            op->if_name = optarg;
+            break;
+        case 'I':
+            op->timeout = sg_get_num(optarg);
+            if (op->timeout < 0)  {
+                pr2serr("bad argument to '--timeout='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'l':
+            if (*lba_opp) {
+                pr2serr("only expect '--lba=' option once\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            *lba_opp = optarg;
+            break;
+        case 'M':               /* WRITE SAME */
+            j = sg_get_num(optarg);
+            if ((j < 0) || (j > 1))  {
+                pr2serr("bad argument to '--same', expect 0 or 1\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->ndob = (bool)j;
+            op->do_same = true;
+            op->cmd_name = "Write same";
+            break;
+        case 'n':
+            if (*num_opp) {
+                pr2serr("only expect '--num=' option once\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            *num_opp = optarg;
+            break;
+        case 'N':
+            op->do_write_normal = true;
+            op->cmd_name = "Write";
+            break;
+        case 'o':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad first argument to '--offset='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->if_offset = (uint64_t)ll;
+            if ((cp = strchr(optarg, ','))) {
+                ll = sg_get_llnum(cp + 1);
+                if (-1 == ll) {
+                    pr2serr("bad second argument to '--offset='\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                if (ll > UINT32_MAX) {
+                    pr2serr("bad second argument to '--offset=', cannot "
+                            "exceed 32 bits\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                op->if_dlen = (uint32_t)ll;
+            }
+            break;
+        case 'O':
+            op->do_or = true;
+            op->cmd_name = "Orwrite";
+            break;
+        case 'q':
+            op->scat_filename = optarg;
+            break;
+        case 'Q':
+            op->do_quiet = true;
+            break;
+        case 'R':
+            op->do_scat_raw = true;
+            break;
+        case 'r':               /* same as --ref-tag= */
+            ll = sg_get_llnum(optarg);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("bad argument to '--ref-tag='. Expect 0 to "
+                        "0xffffffff inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->ref_tag = (uint32_t)ll;
+            break;
+        case 's':
+            ++op->strict;
+            break;
+        case 'S':
+            j = sg_get_num(optarg);
+            if ((j < 0) || (j > (int)UINT16_MAX)) {
+                pr2serr("bad argument to '--scattered='. Expect 0 to 0xffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->scat_num_lbard = (uint16_t)j;
+            op->do_scattered = true;
+            op->cmd_name = "Write scattered";
+            break;
+        case 't':               /* same as --tag-mask= */
+            j = sg_get_num(optarg);
+            if ((j < 0) || (j > (int)UINT16_MAX)) {
+                pr2serr("bad argument to '--tag-mask='. Expect 0 to 0xffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->tag_mask = (uint16_t)j;
+            break;
+        case 'T':               /* WRITE STREAM */
+            j = sg_get_num(optarg);
+            if ((j < 0) || (j > (int)UINT16_MAX)) {
+                pr2serr("bad argument to '--stream=', expect 0 to 65535\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->str_id = (uint16_t)j;
+            op->do_stream = true;
+            op->cmd_name = "Write stream";
+            break;
+        case 'u':               /* WRITE SAME, UNMAP and ANCHOR bit */
+            j = sg_get_num(optarg);
+            if ((j < 0) || (j > 3)) {
+                pr2serr("bad argument to '--unmap=', expect 0 to "
+                        "3\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->do_unmap = !!(1 & j);
+            op->do_anchor = !!(2 & j);
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'w':       /* WRPROTECT field (or ORPROTECT for ORWRITE) */
+            op->wrprotect = sg_get_num(optarg);
+            if ((op->wrprotect < 0) || (op->wrprotect > 7))  {
+                pr2serr("bad argument to '--wrprotect'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->expect_pi_do = (op->wrprotect > 0);
+            break;
+        case 'x':
+            ++op->dry_run;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage((op->help > 0) ? op->help : 0);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
+            usage((op->help > 0) ? op->help : 0);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (op->strict && fail_if_strict)
+        return SG_LIB_SYNTAX_ERROR;
+    return 0;
+}
+
+static int
+process_scattered(int sg_fd, int infd, uint32_t if_len, uint32_t if_rlen,
+                  int sfr_fd, uint32_t sf_len, uint64_t * addr_arr,
+                  uint32_t addr_arr_len, uint32_t * num_arr,
+                  uint16_t num_lbard, uint32_t sum_num, struct opts_t * op)
+{
+    int k, n, ret;
+    int vb = op->verbose;
+    uint32_t d, dd, nn, do_len;
+    uint8_t * up = NULL;
+    uint8_t * free_up = NULL;
+    char b[80];
+
+    if (op->do_combined) {      /* --combined=DOF (.scat_lbdof) */
+        if (op->scat_lbdof > 0)
+            d = op->scat_lbdof * op->bs_pi_do;
+        else if (op->scat_num_lbard > 0) {
+            d = lbard_sz * (1 + op->scat_num_lbard);
+            if (0 != (d % op->bs_pi_do))
+                d = ((d / op->bs_pi_do) + 1) * op->bs_pi_do;
+        } else if (if_len > 0) {
+            d = if_len;
+            if (0 != (d % op->bs_pi_do))
+                d = ((d / op->bs_pi_do) + 1) * op->bs_pi_do;
+        } else {
+            pr2serr("With --combined= if DOF, RD are 0 and IF has an "
+                    "unknown length\nthen give up\n");
+            return SG_LIB_CONTRADICT;
+        }
+        up = sg_memalign(d, 0, &free_up, false);
+        if (NULL == up) {
+            pr2serr("unable to allocate aligned memory for "
+                    "scatterlist+data\n");
+            return sg_convert_errno(ENOMEM);
+        }
+        ret = bin_read(infd, up, ((if_len < d) ? if_len : d), "IF c1");
+        if (ret)
+            goto finii;
+        if (! check_lbrds(up, d, op, &num_lbard, &sum_num))
+            goto file_err_outt;
+        if ((op->scat_num_lbard > 0) && (op->scat_num_lbard != num_lbard)) {
+            bool rd_gt = (op->scat_num_lbard > num_lbard);
+
+            if (rd_gt || op->strict || vb) {
+                pr2serr("RD (%u) %s number of %ss (%u) found in IF\n",
+                        op->scat_num_lbard, (rd_gt ? ">" : "<"), lbard_str,
+                        num_lbard);
+                if (rd_gt)
+                    goto file_err_outt;
+                else if (op->strict)
+                    goto file_err_outt;
+            }
+            num_lbard = op->scat_num_lbard;
+            sum_num = sum_num_lbards(up, op->scat_num_lbard);
+        } else
+            op->scat_num_lbard = num_lbard;
+        dd = lbard_sz * (num_lbard + 1);
+        if (0 != (dd % op->bs_pi_do))
+            dd = ((dd / op->bs_pi_do) + 1) * op->bs_pi_do; /* round up */
+        nn = op->scat_lbdof * op->bs_pi_do;
+        if (dd != nn) {
+            bool dd_gt = (dd > nn);
+
+            if (dd_gt) {
+                pr2serr("%s: Cannot fit %ss (%u) in given LB data offset "
+                        "(%u)\n", __func__, lbard_str, num_lbard,
+                        op->scat_lbdof);
+                goto file_err_outt;
+            }
+            if (vb || op->strict)
+                pr2serr("%s: empty blocks before LB data offset (%u), could "
+                        "be okay\n", __func__, op->scat_lbdof);
+            if (op->strict) {
+                pr2serr("Exiting due to --strict; perhaps try again with "
+                        "--combined=%u\n", dd / op->bs_pi_do);
+                goto file_err_outt;
+            }
+            dd = nn;
+        }
+        dd += (sum_num * op->bs_pi_do);
+        if (dd > d) {
+            uint8_t * u2p;
+            uint8_t * free_u2p;
+
+            if (dd != if_len) {
+                bool dd_gt = (dd > if_len);
+
+                if (dd_gt || op->strict || vb) {
+                    pr2serr("Calculated dout length (%u) %s bytes available "
+                            "in IF (%u)\n", dd, (dd_gt ? ">" : "<"), if_len);
+                    if (dd_gt)
+                        goto file_err_outt;
+                    else if (op->strict)
+                        goto file_err_outt;
+                }
+            }
+            u2p = sg_memalign(dd, 0, &free_u2p, false);
+            if (NULL == u2p) {
+                pr2serr("unable to allocate memory for final "
+                        "scatterlist+data\n");
+                ret = sg_convert_errno(ENOMEM);
+                goto finii;
+            }
+            memcpy(u2p, up, d);
+            free(free_up);
+            up = u2p;
+            free_up = free_u2p;
+            ret = bin_read(infd, up + d, dd - d, "IF c2");
+            if (ret)
+                goto finii;
+        }
+        do_len = dd;
+        op->numblocks = sum_num;
+        op->xfer_bytes = sum_num * op->bs_pi_do;
+        goto do_io;
+    }
+
+    /* other than do_combined, so --scat-file= or --lba= */
+    if (addr_arr_len > 0)
+        num_lbard = addr_arr_len;
+
+    if (op->scat_filename && (! op->do_scat_raw)) {
+        d = lbard_sz * (num_lbard + 1);
+        nn = d;
+        op->scat_lbdof = d / op->bs_pi_do;
+        if (0 != (d % op->bs_pi_do))  /* if not multiple, round up */
+            op->scat_lbdof += 1;
+        dd = op->scat_lbdof * op->bs_pi_do;
+        d = sum_num * op->bs_pi_do;
+        do_len = dd + d;
+        /* zeroed data-out buffer for SL+DATA */
+        up = sg_memalign(do_len, 0, &free_up, false);
+        if (NULL == up) {
+            pr2serr("unable to allocate aligned memory for "
+                    "scatterlist+data\n");
+            return sg_convert_errno(ENOMEM);
+        }
+        num_lbard = 0;
+        sum_num = 0;
+        nn = (nn > lbard_sz) ? nn : (op->scat_lbdof *  op->bs_pi_do);
+        ret = build_t10_scat(op->scat_filename, op->do_16, ! op->do_scattered,
+                             up, &num_lbard, &sum_num, nn);
+        if (ret)
+            goto finii;
+        /* Calculate number of bytes to read from IF (place in 'd') */
+        d = sum_num * op->bs_pi_do;
+        if (op->if_dlen > d) {
+            if (op->strict || vb) {
+                pr2serr("DLEN > than bytes implied by sum of scatter "
+                        "list NUMs (%u)\n", d);
+                if (vb > 1)
+                    pr2serr("  num_lbard=%u, sum_num=%u actual_bs=%u",
+                            num_lbard, sum_num, op->bs_pi_do);
+                if (op->strict)
+                    goto file_err_outt;
+            }
+        } else if ((op->if_dlen > 0) && (op->if_dlen < d))
+            d = op->if_dlen;
+        if ((if_rlen > 0) && (if_rlen != d)) {
+            bool readable_lt = (if_rlen < d);
+
+            if (vb)
+                pr2serr("readable length (%u) of IF %s bytes implied by "
+                        "sum of\nscatter list NUMs (%u) and DLEN\n",
+                        (uint32_t)if_rlen,
+                        readable_lt ? "<" : ">", d);
+            if (op->strict) {
+                if ((op->strict > 1) || (! readable_lt))
+                    goto file_err_outt;
+            }
+            if (readable_lt)
+                d = if_rlen;
+        }
+        if (0 != (d % op->bs_pi_do)) {
+            if (vb || (op->strict > 1)) {
+                pr2serr("Calculated data-out length (0x%x) not a "
+                        "multiple of BS (%u", d, op->bs);
+                if (op->bs != op->bs_pi_do)
+                    pr2serr(" + %d(PI)", (int)op->bs_pi_do - (int)op->bs);
+                if (op->strict > 1) {
+                    pr2serr(")\nexiting ...\n");
+                    goto file_err_outt;
+                } else
+                    pr2serr(")\nzero pad and continue ...\n");
+            }
+        }
+        ret = bin_read(infd, up + (op->scat_lbdof * op->bs_pi_do), d,
+                       "IF 3");
+        if (ret)
+            goto finii;
+        do_len = ((op->scat_lbdof + sum_num) * op->bs_pi_do);
+        op->numblocks = sum_num;
+        op->xfer_bytes = sum_num * op->bs_pi_do;
+        /* dout for scattered write with ASCII scat_file ready */
+    } else if (op->do_scat_raw) {
+        bool if_len_gt = false;
+
+        /* guessing game for length of buffer */
+        if (op->scat_num_lbard > 0) {
+            dd = (op->scat_num_lbard + 1) * lbard_sz;
+            if (sf_len < dd) {
+                pr2serr("SF not long enough (%u bytes) to provide RD "
+                        "(%u) %ss\n", sf_len, dd, lbard_str);
+                goto file_err_outt;
+            }
+            nn = dd / op->bs_pi_do;
+            if (0 != (dd % op->bs_pi_do))
+                nn +=1;
+            dd = nn * op->bs_pi_do;
+        } else
+            dd = op->bs_pi_do;      /* guess */
+        if (if_len > 0) {
+            nn = if_len / op->bs_pi_do;
+            if (0 != (if_len % op->bs_pi_do))
+                nn += 1;
+            d = nn * op->bs_pi_do;
+        } else
+            d = op->bs_pi_do;      /* guess one LB */
+        /* zero data-out buffer for SL+DATA */
+        nn = dd + d;
+        up = sg_memalign(nn, 0, &free_up, false);
+        if (NULL == up) {
+            pr2serr("unable to allocate aligned memory for "
+                    "scatterlist+data\n");
+            ret = sg_convert_errno(ENOMEM);
+            goto finii;
+        }
+        ret = bin_read(sfr_fd, up, sf_len, "SF");
+        if (ret)
+            goto finii;
+        if (! check_lbrds(up, dd, op, &num_lbard, &sum_num))
+            goto file_err_outt;
+        if (num_lbard != op->scat_num_lbard) {
+            pr2serr("Try again with --scattered=%u\n", num_lbard);
+            goto file_err_outt;
+        }
+        if ((sum_num * op->bs_pi_do) > d) {
+            uint8_t * u2p;
+            uint8_t * free_u2p;
+
+            d = sum_num * op->bs_pi_do;
+            nn = dd + d;
+            u2p = sg_memalign(nn, 0, &free_u2p, false);
+            if (NULL == u2p) {
+                pr2serr("unable to allocate memory for final "
+                        "scatterlist+data\n");
+                ret = sg_convert_errno(ENOMEM);
+                goto finii;
+            }
+            memcpy(u2p, up, dd);
+            free(free_up);
+            up = u2p;
+            free_up = free_u2p;
+        }
+        if ((if_len != (nn - d)) && (op->strict || vb)) {
+            if_len_gt = (if_len > (nn - d));
+            pr2serr("IF length (%u) %s 'sum_num' bytes (%u), ", if_len,
+                    (if_len_gt ? ">" : "<"), nn - d);
+            if (op->strict > 1) {
+                pr2serr("exiting (strict=%d)\n", op->strict);
+                goto file_err_outt;
+            } else
+                pr2serr("continuing ...\n");
+        }
+        ret = bin_read(infd, up + d, (if_len_gt ? nn - d : if_len), "IF 4");
+        if (ret)
+            goto finii;
+        do_len = (num_lbard + sum_num) * op->bs_pi_do;
+        op->numblocks = sum_num;
+        op->xfer_bytes = sum_num * op->bs_pi_do;
+    } else if (addr_arr_len > 0) {  /* build RDs for --lba= --num= */
+        if ((op->scat_num_lbard > 0) && (op->scat_num_lbard > addr_arr_len)) {
+            pr2serr("%s: number given to --scattered= (%u) exceeds number of "
+                    "--lba= elements (%u)\n", __func__, op->scat_num_lbard,
+                    addr_arr_len);
+            return SG_LIB_CONTRADICT;
+        }
+        d = lbard_sz * (num_lbard + 1);
+        op->scat_lbdof = d / op->bs_pi_do;
+        if (0 != (d % op->bs_pi_do))  /* if not multiple, round up */
+            op->scat_lbdof += 1;
+        for (sum_num = 0, k = 0; k < (int)addr_arr_len; ++k)
+            sum_num += num_arr[k];
+        do_len = ((op->scat_lbdof + sum_num) * op->bs_pi_do);
+        up = sg_memalign(do_len, 0, &free_up, false);
+        if (NULL == up) {
+            pr2serr("unable to allocate aligned memory for "
+                    "scatterlist+data\n");
+            ret = sg_convert_errno(ENOMEM);
+            goto finii;
+        }
+        for (n = lbard_sz, k = 0; k < (int)addr_arr_len; ++k,
+             n += lbard_sz) {
+            sg_put_unaligned_be64(addr_arr[k], up + n + 0);
+            sg_put_unaligned_be32(num_arr[k], up + n + 8);
+            if (op->do_32) {
+                if (0 == k) {
+                    sg_put_unaligned_be32(op->ref_tag, up + n + 12);
+                    sg_put_unaligned_be16(op->app_tag, up + n + 16);
+                    sg_put_unaligned_be16(op->tag_mask, up + n + 18);
+                } else {
+                    sg_put_unaligned_be32((uint32_t)DEF_RT, up + n + 12);
+                    sg_put_unaligned_be16((uint16_t)DEF_AT, up + n + 16);
+                    sg_put_unaligned_be16((uint16_t)DEF_TM, up + n + 18);
+                }
+            }
+        }
+        op->numblocks = sum_num;
+    } else {
+        pr2serr("How did we get here??\n");
+        goto syntax_err_outt;
+    }
+do_io:
+    ret = do_write_x(sg_fd, up, do_len, op);
+    if (ret) {
+        strcpy(b,"OS error");
+        if (ret > 0)
+            sg_get_category_sense_str(ret, sizeof(b), b, vb);
+        pr2serr("%s: %s\n", op->cdb_name, b);
+    }
+    goto finii;
+
+syntax_err_outt:
+    ret = SG_LIB_SYNTAX_ERROR;
+    goto finii;
+file_err_outt:
+    ret = SG_LIB_FILE_ERROR;
+finii:
+    if (free_up)
+        free(free_up);
+    return ret;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool got_stdin = false;
+    bool got_stat = false;
+    bool if_reg_file = false;
+    int n, err, vb;
+    int infd = -1;
+    int sg_fd = -1;
+    int sfr_fd = -1;
+    int ret = -1;
+    uint32_t nn, addr_arr_len, num_arr_len;     /* --lba= */
+    uint32_t do_len = 0;
+    uint16_t num_lbard = 0;
+    uint32_t if_len = 0;    /* after accounting for OFF,DLEN and moving file
+                             * file pointer to OFF, is bytes available in IF */
+    uint32_t sf_len = 0;
+    uint32_t sum_num = 0;
+    ssize_t res;
+    off_t if_readable_len = 0;  /* similar to if_len but doesn't take DLEN
+                                 * into account */
+    struct opts_t * op;
+    const char * lba_op = NULL;
+    const char * num_op = NULL;
+    uint8_t * up = NULL;
+    uint8_t * free_up = NULL;
+    char ebuff[EBUFF_SZ];
+    char b[80];
+    uint64_t addr_arr[MAX_NUM_ADDR];
+    uint32_t num_arr[MAX_NUM_ADDR];
+    struct stat if_stat, sf_stat;
+    struct opts_t opts SG_C_CPP_ZERO_INIT;
+
+    op = &opts;
+    memset(&if_stat, 0, sizeof(if_stat));
+    memset(&sf_stat, 0, sizeof(sf_stat));
+    op->numblocks = DEF_WR_NUMBLOCKS;
+    op->pi_type = -1;           /* Protection information type unknown */
+    op->ref_tag = DEF_RT;       /* first 4 bytes of 8 byte protection info */
+    op->app_tag = DEF_AT;       /* 2 bytes of protection information */
+    op->tag_mask = DEF_TM;      /* final 2 bytes of protection information */
+    op->timeout = DEF_TIMEOUT_SECS;
+
+    /* Process command line */
+    ret = parse_cmd_line(op, argc, argv, &lba_op, &num_op);
+    if (ret) {
+        if (WANT_ZERO_EXIT == ret)
+            return 0;
+        return ret;
+    }
+    if (op->help > 0) {
+        usage(op->help);
+        return 0;
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr("sg_write_x version: %s\n", version_str);
+        return WANT_ZERO_EXIT;
+    }
+
+    vb = op->verbose;
+    /* sanity checks */
+    if ((! op->do_16) && (! op->do_32)) {
+        op->do_16 = true;
+        if (vb > 1)
+            pr2serr("Since neither --16 nor --32 given, choose --16\n");
+    } else if (op->do_16 && op->do_32) {
+        op->do_16 = false;
+        if (vb > 1)
+            pr2serr("Since both --16 and --32 given, choose --32\n");
+    }
+    n = (int)op->do_atomic + (int)op->do_write_normal + (int)op->do_or +
+        (int)op->do_same + (int)op->do_scattered + (int)op->do_stream;
+    if (n > 1) {
+        pr2serr("Can only select one command; so only one of --atomic, "
+                "--normal, --or,\n--same=, --scattered= or --stream=\n") ;
+        return SG_LIB_CONTRADICT;
+    } else if (n < 1) {
+        if (op->strict) {
+            pr2serr("With --strict won't default to a normal WRITE, add "
+                    "--normal\n");
+            return SG_LIB_CONTRADICT;
+        } else {
+            op->do_write_normal = true;
+            op->cmd_name = "Write";
+            if (vb)
+                pr2serr("No command selected so choose 'normal' WRITE\n");
+        }
+    }
+    snprintf(op->cdb_name, sizeof(op->cdb_name), "%s(%d)", op->cmd_name,
+             (op->do_16 ? 16 : 32));
+    if (op->do_combined) {
+        if (! op->do_scattered) {
+            pr2serr("--combined=DOF only allowed with --scattered=RD (i.e. "
+                    "only with\nWRITE SCATTERED command)\n");
+            return SG_LIB_CONTRADICT;
+        }
+        if (op->scat_filename) {
+            pr2serr("Ambiguous: got --combined=DOF and --scat-file=SF .\n"
+                    "Give one, the other or neither\n");
+            return SG_LIB_CONTRADICT;
+        }
+        if (lba_op || num_op) {
+            pr2serr("--scattered=RD --combined=DOF does not use --lba= or "
+                    "--num=\nPlease remove.\n");
+            return SG_LIB_CONTRADICT;
+        }
+        if (op->do_scat_raw) {
+            pr2serr("Ambiguous: don't expect --combined=DOF and --scat-raw\n"
+                    "Give one or the other\n");
+            return SG_LIB_CONTRADICT;
+        }
+    }
+    if ((NULL == op->scat_filename) && op->do_scat_raw) {
+        pr2serr("--scat-raw only applies to the --scat-file=SF option\n"
+                "--scat-raw without the --scat-file=SF option is an "
+                "error\n");
+        return SG_LIB_CONTRADICT;
+    }
+    n = (!! op->scat_filename) + (!! (lba_op || num_op)) +
+        (!! op->do_combined);
+    if (n > 1) {
+        pr2serr("want one and only one of: (--lba=LBA and/or --num=NUM), or\n"
+                "--scat-file=SF, or --combined=DOF\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (op->scat_filename && (1 == strlen(op->scat_filename)) &&
+        ('-' == op->scat_filename[0])) {
+        pr2serr("don't accept '-' (implying stdin) as a filename in "
+                "--scat-file=SF\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (vb && op->do_16 && (! is_pi_default(op)))
+        pr2serr("--app-tag=, --ref-tag= and --tag-mask= options ignored "
+                "with 16 byte commands\n");
+
+    /* examine .if_name . Open, move to .if_offset, calculate length that we
+     * want to read. */
+    if (! op->ndob) {           /* as long as --same=1 is not active */
+        if_len = op->if_dlen;   /* from --offset=OFF,DLEN; defaults to 0 */
+        if (NULL == op->if_name) {
+            pr2serr("Need --if=FN option to be given, exiting.\n");
+            if (vb > 1)
+                pr2serr("To write zeros use --in=/dev/zero\n");
+            pr2serr("\n");
+            usage((op->help > 0) ? op->help : 0);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if ((1 == strlen(op->if_name)) && ('-' == op->if_name[0])) {
+            got_stdin = true;
+            infd = STDIN_FILENO;
+            if (sg_set_binary_mode(STDIN_FILENO) < 0) {
+                perror("sg_set_binary_mode");
+                return SG_LIB_FILE_ERROR;
+            }
+        } else {
+            if ((infd = open(op->if_name, O_RDONLY)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ, "could not open %s for reading",
+                         op->if_name);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+            if (sg_set_binary_mode(infd) < 0) {
+                perror("sg_set_binary_mode");
+                return SG_LIB_FILE_ERROR;
+            }
+            if (fstat(infd, &if_stat) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ, "could not fstat %s", op->if_name);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+            got_stat = true;
+            if (S_ISREG(if_stat.st_mode)) {
+                if_reg_file = true;
+                if_readable_len = if_stat.st_size;
+                if (0 == if_len)
+                    if_len = if_readable_len;
+            }
+        }
+        if (got_stat && if_readable_len &&
+            ((int64_t)op->if_offset >= (if_readable_len - 1))) {
+            pr2serr("Offset (%" PRIu64 ") is at or beyond IF byte length (%"
+                    PRIu64 ")\n", op->if_offset, (uint64_t)if_readable_len);
+            goto file_err_out;
+        }
+        if (op->if_offset > 0) {
+            off_t off = op->if_offset;
+            off_t h = if_readable_len;
+
+            if (if_reg_file) {
+                /* lseek() won't work with stdin, pipes or sockets, etc */
+                if (lseek(infd, off, SEEK_SET) < 0) {
+                    err = errno;
+                    snprintf(ebuff,  EBUFF_SZ, "couldn't offset to required "
+                            "position on %s", op->if_name);
+                    perror(ebuff);
+                    ret = sg_convert_errno(err);
+                    goto err_out;
+                }
+                if_readable_len -= op->if_offset;
+                if (if_readable_len <= 0) {
+                    pr2serr("--offset [0x%" PRIx64 "] at or beyond file "
+                            "length[0x%" PRIx64 "]\n",
+                            (uint64_t)op->if_offset, (uint64_t)h);
+                    goto file_err_out;
+                }
+                if (op->strict && ((off_t)op->if_dlen > if_readable_len)) {
+                    pr2serr("after accounting for OFF, DLEN exceeds %s "
+                            "remaining length (%u bytes)\n",
+                            op->if_name, (uint32_t)if_readable_len);
+                    goto file_err_out;
+                }
+                if_len = (uint32_t)((if_readable_len < (off_t)if_len) ?
+                                if_readable_len : (off_t)if_len);
+                if (vb > 2)
+                    pr2serr("Moved IF byte pointer to %u, if_len=%u, "
+                            "if_readable_len=%u\n", (uint32_t)op->if_offset,
+                            if_len, (uint32_t)if_readable_len);
+            } else {
+                if (vb)
+                    pr2serr("--offset=OFF ignored when IF is stdin, pipe, "
+                            "socket, etc\nDLEN, if given, is used\n");
+            }
+        }
+    }
+    /* Check device name has been given */
+    if (NULL == op->device_name) {
+        pr2serr("missing device name!\n");
+        usage((op->help > 0) ? op->help : 0);
+        goto syntax_err_out;
+    }
+
+    /* Open device file, do READ CAPACITY(16, maybe 10) if no BS */
+    sg_fd = sg_cmds_open_device(op->device_name, false /* rw */, vb);
+    if (sg_fd < 0) {
+        if (op->verbose)
+            pr2serr("open error: %s: %s\n", op->device_name,
+                    safe_strerror(-sg_fd));
+        ret = sg_convert_errno(-sg_fd);
+        goto fini;
+    }
+    if (0 == op->bs) {  /* ask DEVICE about logical/actual block size */
+        ret = do_read_capacity(sg_fd, op);
+        if (ret)
+            goto err_out;
+    }
+    if ((0 == op->bs_pi_do) || (0 == op->bs)) {
+        pr2serr("Logic error, need block size by now\n");
+        goto syntax_err_out;
+    }
+    if (! op->ndob) {
+        if (0 != (if_len % op->bs_pi_do)) {
+            if (op->strict > 1) {
+                pr2serr("Error: number of bytes to read from IF [%u] is "
+                        "not a multiple\nblock size %u (including "
+                        "protection information)\n", (unsigned int)if_len,
+                        op->bs_pi_do);
+                goto file_err_out;
+            }
+            if (op->strict || vb)
+                pr2serr("Warning: number of bytes to read from IF [%u] is "
+                        "not a multiple\nof actual block size %u; pad with "
+                        "zeros\n", (unsigned int)if_len, op->bs_pi_do);
+        }
+    }
+
+    /* decode --lba= and --num= options */
+    memset(addr_arr, 0, sizeof(addr_arr));
+    memset(num_arr, 0, sizeof(num_arr));
+    addr_arr_len = 0;
+    num_arr_len = 0;
+    if (lba_op) {
+        if (0 != build_lba_arr(lba_op, addr_arr, &addr_arr_len,
+                               MAX_NUM_ADDR)) {
+            pr2serr("bad argument to '--lba'\n");
+            goto syntax_err_out;
+        }
+    }
+    if (num_op) {
+        if (0 != build_num_arr(num_op, num_arr, &num_arr_len,
+                               MAX_NUM_ADDR)) {
+            pr2serr("bad argument to '--num'\n");
+            goto err_out;
+        }
+    }
+    if (((addr_arr_len > 1) && (addr_arr_len != num_arr_len)) ||
+        ((0 == addr_arr_len) && (num_arr_len > 1))) {
+        /* allow all combinations of 0 or 1 element --lba= with 0 or 1
+         * element --num=, otherwise this error ... */
+        pr2serr("need same number of arguments to '--lba=' and '--num=' "
+                    "options\n");
+        ret = SG_LIB_CONTRADICT;
+        goto err_out;
+    }
+    if ((0 == addr_arr_len) && (1 == num_arr_len)) {
+        if (num_arr[0] > 0) {
+            pr2serr("won't write %u blocks without an explicit --lba= "
+                    "option\n", num_arr[0]);
+            goto syntax_err_out;
+        }
+        addr_arr_len = 1;  /* allow --num=0 without --lba= since it is safe */
+    }
+    /* Everything can use a SF, except --same=1 (when op->ndob==true) */
+    if (op->scat_filename) {
+        if (stat(op->scat_filename, &sf_stat) < 0) {
+            err = errno;
+            pr2serr("Unable to stat(%s) as SF: %s\n", op->scat_filename,
+                    safe_strerror(err));
+            ret = sg_convert_errno(err);
+            goto err_out;
+        }
+        if (op->do_scat_raw) {
+            if (! S_ISREG(sf_stat.st_mode)) {
+                pr2serr("Expect scatter file to be a regular file\n");
+                goto file_err_out;
+            }
+            sf_len = sf_stat.st_size;
+            sfr_fd = open(op->scat_filename, O_RDONLY);
+            if (sfr_fd < 0) {
+                err = errno;
+                pr2serr("Failed to open %s for raw read: %s\n",
+                        op->scat_filename, safe_strerror(err));
+                ret = sg_convert_errno(err);
+                goto err_out;
+            }
+            if (sg_set_binary_mode(sfr_fd) < 0) {
+                perror("sg_set_binary_mode");
+                goto file_err_out;
+            }
+        } else { /* scat_file should contain ASCII hex, preliminary parse */
+            nn = (op->scat_num_lbard > 0) ?
+                                lbard_sz * (1 + op->scat_num_lbard) : 0;
+            ret = build_t10_scat(op->scat_filename, op->do_16,
+                                 ! op->do_scattered, NULL, &num_lbard,
+                                 &sum_num, nn);
+            if (ret)
+                goto err_out;
+            if ((op->scat_num_lbard > 0) &&
+                (num_lbard != op->scat_num_lbard)) {
+                bool rd_gt = (op->scat_num_lbard > num_lbard);
+
+                if (rd_gt || op->strict || vb) {
+                    pr2serr("RD (%u) %s number of %ss (%u) found in SF\n",
+                            op->scat_num_lbard, (rd_gt ? ">" : "<"),
+                            lbard_str, num_lbard);
+                    if (rd_gt)
+                        goto file_err_out;
+                    else if (op->strict)
+                        goto file_err_out;
+                }
+            }
+        }
+    }
+
+    if (op->do_scattered) {
+        ret = process_scattered(sg_fd, infd, if_len, if_readable_len, sfr_fd,
+                                sf_len, addr_arr, addr_arr_len, num_arr,
+                                num_lbard, sum_num, op);
+        goto fini;
+    }
+
+    /* other than scattered */
+    if (addr_arr_len > 0) {
+        op->lba = addr_arr[0];
+        op->numblocks = num_arr[0];
+        if (vb && (addr_arr_len > 1))
+            pr2serr("warning: %d LBA,number_of_blocks pairs found, only "
+                    "taking first\n", addr_arr_len);
+    } else if (op->scat_filename && (! op->do_scat_raw)) {
+        uint8_t upp[96];
+
+        sum_num = 0;
+        ret = build_t10_scat(op->scat_filename, op->do_16,
+                             true /* parse one */, upp, &num_lbard,
+                             &sum_num, sizeof(upp));
+        if (ret)
+            goto err_out;
+        if (vb && (num_lbard > 1))
+            pr2serr("warning: %d LBA,number_of_blocks pairs found, only "
+                    "taking first\n", num_lbard);
+        if (vb > 2)
+            pr2serr("after build_t10_scat, num_lbard=%u, sum_num=%u\n",
+                    num_lbard, sum_num);
+        if (1 != num_lbard) {
+            pr2serr("Unable to decode one LBA range descriptor from %s\n",
+                    op->scat_filename);
+            goto file_err_out;
+        }
+        op->lba = sg_get_unaligned_be64(upp + 32 + 0);
+        op->numblocks = sg_get_unaligned_be32(upp + 32 + 8);
+        if (op->do_32) {
+            op->ref_tag = sg_get_unaligned_be32(upp + 32 + 12);
+            op->app_tag = sg_get_unaligned_be16(upp + 32 + 16);
+            op->tag_mask = sg_get_unaligned_be16(upp + 32 + 18);
+        }
+    } else if (op->do_scat_raw) {
+        uint8_t upp[64];
+
+        if (sf_len < (2 * lbard_sz)) {
+            pr2serr("raw scatter file must be at least 64 bytes long "
+                    "(length: %u)\n", sf_len);
+            goto file_err_out;
+        }
+        ret = bin_read(sfr_fd, upp, sizeof(upp), "SF");
+        if (ret)
+            goto err_out;
+        if (! check_lbrds(upp, sizeof(upp), op, &num_lbard, &sum_num))
+            goto file_err_out;
+        if (1 != num_lbard) {
+            pr2serr("No %ss found in SF (num=%u)\n", lbard_str, num_lbard);
+            goto file_err_out;
+        }
+        op->lba = sg_get_unaligned_be64(upp + 16);
+        op->numblocks = sg_get_unaligned_be32(upp + 16 + 8);
+        do_len = sum_num * op->bs_pi_do;
+        op->xfer_bytes = do_len;
+    } else {
+        pr2serr("No LBA or number_of_blocks given, try using --lba= and "
+                "--num=\n");
+        goto syntax_err_out;
+    }
+    if (op->do_same)
+        op->xfer_bytes = op->ndob ? 0 : op->bs_pi_do;
+    else    /* WRITE, ORWRITE, WRITE ATOMIC or WRITE STREAM */
+        op->xfer_bytes = op->numblocks * op->bs_pi_do;
+    do_len = op->xfer_bytes;
+
+    if (do_len > 0) {
+        /* fill allocated buffer with zeros */
+        up = sg_memalign(do_len, 0, &free_up, false);
+        if (NULL == up) {
+            pr2serr("unable to allocate %u bytes of memory\n", do_len);
+            ret = sg_convert_errno(ENOMEM);
+            goto err_out;
+        }
+        ret = bin_read(infd, up, ((if_len < do_len) ? if_len : do_len),
+                       "IF 5");
+        if (ret)
+            goto fini;
+    } else
+        up = NULL;
+
+    ret = do_write_x(sg_fd, up, do_len, op);
+    if (ret && (! op->do_quiet)) {
+        strcpy(b,"OS error");
+        if (ret > 0)
+            sg_get_category_sense_str(ret, sizeof(b), b, vb);
+        pr2serr("%s: %s\n", op->cdb_name, b);
+    }
+    goto fini;
+
+syntax_err_out:
+    ret = SG_LIB_SYNTAX_ERROR;
+    goto err_out;
+file_err_out:
+    ret = SG_LIB_FILE_ERROR;
+err_out:
+fini:
+    if (free_up)
+        free(free_up);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            if (! op->do_quiet)
+                pr2serr("sg_fd close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = SG_LIB_FILE_ERROR;
+        }
+    }
+    if (sfr_fd >= 0) {
+        if (close(sfr_fd) < 0) {
+            if (! op->do_quiet)
+                perror("sfr_fd close error");
+            if (0 == ret)
+                ret = SG_LIB_FILE_ERROR;
+        }
+    }
+    if ((! got_stdin) && (infd >= 0)) {
+        if (close(infd) < 0) {
+            if (! op->do_quiet)
+                perror("infd close error");
+            if (0 == ret)
+                ret = SG_LIB_FILE_ERROR;
+        }
+    }
+    if ((0 == op->verbose) && (! op->do_quiet)) {
+        if (! sg_if_can2stderr("sg_write_x failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_xcopy.c b/src/sg_xcopy.c
new file mode 100644
index 0000000..39ad83c
--- /dev/null
+++ b/src/sg_xcopy.c
@@ -0,0 +1,1934 @@
+/* A utility program for copying files. Similar to 'dd' but using
+ * the 'Extended Copy' command.
+ *
+ *  Copyright (c) 2011-2022 Hannes Reinecke, SUSE Labs
+ *
+ *  Largely taken from 'sg_dd', which has the
+ *
+ *  Copyright (C) 1999 - 2010 D. Gilbert and P. Allworth
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is a specialisation of the Unix "dd" command in which
+ * either the input or the output file is a scsi generic device, raw
+ * device, a block device or a normal file. The block size ('bs') is
+ * assumed to be 512 if not given. This program complains if 'ibs' or
+ * 'obs' are given with a value that differs from 'bs' (or the default 512).
+ * If 'if' is not given or 'if=-' then stdin is assumed. If 'of' is
+ * not given or 'of=-' then stdout assumed.
+ *
+ * A non-standard argument "bpt" (blocks per transfer) is added to control
+ * the maximum number of blocks in each transfer. The default value is 128.
+ * For example if "bs=512" and "bpt=32" then a maximum of 32 blocks (16 KiB
+ * in this case) is transferred to or from the sg device in a single SCSI
+ * command.
+ *
+ * This version is designed for the Linux kernel 2.4, 2.6, 3, 4 and 5 series.
+ */
+
+#define _XOPEN_SOURCE 600
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <string.h>
+#include <signal.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/file.h>
+#include <sys/sysmacros.h>
+#ifndef major
+#include <sys/types.h>
+#endif
+#include <linux/major.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_cmds_extra.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "0.73 20220118";
+
+#define ME "sg_xcopy: "
+
+#define STR_SZ 1024
+#define INOUTF_SZ 512
+#define EBUFF_SZ 1024
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BLOCKS_PER_TRANSFER 128
+#define MAX_BLOCKS_PER_TRANSFER 65535
+
+#define DEF_MODE_RESP_LEN 252
+#define RW_ERR_RECOVERY_MP 1
+#define CACHING_MP 8
+#define CONTROL_MP 0xa
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define READ_CAP_REPLY_LEN 8
+#define RCAP16_REPLY_LEN 32
+
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs == 60 seconds */
+
+#ifndef UINT32_MAX
+#define UINT32_MAX ((uint32_t)-1)
+#endif
+
+#ifndef RAW_MAJOR
+#define RAW_MAJOR 255   /*unlikely value */
+#endif
+
+#define SG_LIB_FLOCK_ERR 90
+
+/* In SPC-4 the cdb opcodes have more generic names */
+#define THIRD_PARTY_COPY_OUT_CMD 0x83
+#define THIRD_PARTY_COPY_IN_CMD 0x84
+
+/* Third party copy IN (opcode 0x84) and OUT (opcode 0x83) command service
+ * actions */
+#define SA_XCOPY_LID1           0x0     /* OUT, originate */
+#define SA_XCOPY_LID4           0x1     /* OUT, originate */
+#define SA_POP_TOK              0x10    /* OUT, originate */
+#define SA_WR_USING_TOK         0x11    /* OUT, originate */
+#define SA_COPY_ABORT           0x1C    /* OUT, abort */
+#define SA_COPY_STATUS_LID1     0x0     /* IN, retrieve */
+#define SA_COPY_DATA_LID1       0x1     /* IN, retrieve */
+#define SA_COPY_OP_PARAMS       0x3     /* IN, retrieve */
+#define SA_COPY_FAIL_DETAILS    0x4     /* IN, retrieve */
+#define SA_COPY_STATUS_LID4     0x5     /* IN, retrieve */
+#define SA_COPY_DATA_LID4       0x6     /* IN, retrieve */
+#define SA_ROD_TOK_INFO         0x7     /* IN, retrieve */
+#define SA_ALL_ROD_TOKS         0x8     /* IN, retrieve */
+
+#define DEF_3PC_OUT_TIMEOUT (10 * 60)   /* is 10 minutes enough? */
+#define DEF_GROUP_NUM 0x0
+
+#define VPD_DEVICE_ID 0x83
+#define VPD_3PARTY_COPY 0x8f
+
+#define FT_OTHER 1              /* filetype is probably normal */
+#define FT_SG 2                 /* filetype is sg or bsg char device */
+#define FT_RAW 4                /* filetype is raw char device */
+#define FT_DEV_NULL 8           /* either "/dev/null" or "." as filename */
+#define FT_ST 16                /* filetype is st char device (tape) */
+#define FT_BLOCK 32             /* filetype is block device */
+#define FT_FIFO 64              /* filetype is a fifo (name pipe) */
+#define FT_ERROR 128            /* couldn't "stat" file */
+
+#define TD_FC_WWPN 1
+#define TD_FC_PORT 2
+#define TD_FC_WWPN_AND_PORT 4
+#define TD_SPI 8
+#define TD_VPD 16
+#define TD_IPV4 32
+#define TD_ALIAS 64
+#define TD_RDMA 128
+#define TD_FW 256
+#define TD_SAS 512
+#define TD_IPV6 1024
+#define TD_IP_COPY_SERVICE 2048
+#define TD_ROD 4096
+
+#define XCOPY_TO_SRC "XCOPY_TO_SRC"
+#define XCOPY_TO_DST "XCOPY_TO_DST"
+#define DEF_XCOPY_SRC0_DST1 1
+
+#define DEV_NULL_MINOR_NUM 3
+
+#define MIN_RESERVED_SIZE 8192
+
+#define MAX_UNIT_ATTENTIONS 10
+#define MAX_ABORTED_CMDS 256
+
+static int64_t dd_count = -1;
+static int64_t in_full = 0;
+static int in_partial = 0;
+static int64_t out_full = 0;
+static int out_partial = 0;
+
+static bool do_time = false;
+static bool start_tm_valid = false;
+static bool xcopy_flag_cat = false;
+static bool xcopy_flag_dc = false;
+static bool xcopy_flag_fco = false;     /* fast copy only, spc5r20 */
+static int blk_sz = 0;
+static int list_id_usage = -1;
+static int priority = 1;
+static int verbose = 0;
+static struct timeval start_tm;
+
+
+struct xcopy_fp_t {
+    bool append;
+    bool excl;
+    bool flock;
+    bool pad;     /* Data descriptor PAD bit (residual data treatment) */
+    bool xcopy_given;
+    int sect_sz;
+    int sg_type, sg_fd;
+    int pdt;     /* Peripheral device type */
+    dev_t devno;
+    uint32_t min_bytes;
+    uint32_t max_bytes;
+    int64_t num_sect;
+    char fname[INOUTF_SZ];
+};
+
+static struct xcopy_fp_t ixcf;
+static struct xcopy_fp_t oxcf;
+
+static const char * read_cap_str = "Read capacity";
+static const char * rec_copy_op_params_str = "Receive copy operating "
+                                             "parameters";
+
+static void calc_duration_throughput(int contin);
+
+
+static void
+install_handler(int sig_num, void (*sig_handler) (int sig))
+{
+    struct sigaction sigact;
+    sigaction (sig_num, NULL, &sigact);
+    if (sigact.sa_handler != SIG_IGN)
+    {
+        sigact.sa_handler = sig_handler;
+        sigemptyset (&sigact.sa_mask);
+        sigact.sa_flags = 0;
+        sigaction (sig_num, &sigact, NULL);
+    }
+}
+
+static void
+print_stats(const char * str)
+{
+    if (0 != dd_count)
+        pr2serr("  remaining block count=%" PRId64 "\n", dd_count);
+    pr2serr("%s%" PRId64 "+%d records in\n", str, in_full - in_partial,
+            in_partial);
+    pr2serr("%s%" PRId64 "+%d records out\n", str, out_full - out_partial,
+            out_partial);
+}
+
+static void
+interrupt_handler(int sig)
+{
+    struct sigaction sigact;
+
+    sigact.sa_handler = SIG_DFL;
+    sigemptyset(&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigaction(sig, &sigact, NULL);
+    pr2serr("Interrupted by signal,");
+    if (do_time)
+        calc_duration_throughput(0);
+    print_stats("");
+    kill(getpid (), sig);
+}
+
+static void
+siginfo_handler(int sig)
+{
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    pr2serr("Progress report, continuing ...\n");
+    if (do_time)
+        calc_duration_throughput(1);
+    print_stats("  ");
+}
+
+static bool bsg_major_checked = false;
+static int bsg_major = 0;
+
+static void
+find_bsg_major(void)
+{
+    const char * proc_devices = "/proc/devices";
+    FILE *fp;
+    char a[128];
+    char b[128];
+    char * cp;
+    int n;
+
+    if (NULL == (fp = fopen(proc_devices, "r"))) {
+        if (verbose)
+            pr2serr("fopen %s failed: %s\n", proc_devices, strerror(errno));
+        return;
+    }
+    while ((cp = fgets(b, sizeof(b), fp))) {
+        if ((1 == sscanf(b, "%126s", a)) &&
+            (0 == memcmp(a, "Character", 9)))
+            break;
+    }
+    while (cp && (cp = fgets(b, sizeof(b), fp))) {
+        if (2 == sscanf(b, "%d %126s", &n, a)) {
+            if (0 == strcmp("bsg", a)) {
+                bsg_major = n;
+                break;
+            }
+        } else
+            break;
+    }
+    if (verbose > 5) {
+        if (cp)
+            pr2serr("found bsg_major=%d\n", bsg_major);
+        else
+            pr2serr("found no bsg char device in %s\n", proc_devices);
+    }
+    fclose(fp);
+}
+
+/* Returns a file descriptor on success (0 or greater), -1 for an open
+ * error, -2 for a standard INQUIRY problem. */
+static int
+open_sg(struct xcopy_fp_t * fp, int vb)
+{
+    int devmajor, devminor, offset;
+    struct sg_simple_inquiry_resp sir;
+    char ebuff[EBUFF_SZ];
+    int len, res;
+
+    devmajor = major(fp->devno);
+    devminor = minor(fp->devno);
+
+    if (fp->sg_type & FT_SG) {
+        snprintf(ebuff, EBUFF_SZ, "%.500s", fp->fname);
+    } else if (fp->sg_type & FT_BLOCK || fp->sg_type & FT_OTHER) {
+        int fd;
+
+        snprintf(ebuff, EBUFF_SZ, "/sys/dev/block/%d:%d/partition",
+                 devmajor, devminor);
+        if ((fd = open(ebuff, O_RDONLY)) >= 0) {
+            ebuff[EBUFF_SZ - 1] = '\0';
+            len = read(fd, ebuff, EBUFF_SZ - 1);
+            if (len < 0) {
+                perror("read partition");
+            } else {
+                offset = strtoul(ebuff, NULL, 10);
+                devminor -= offset;
+            }
+            close(fd);
+        }
+        snprintf(ebuff, EBUFF_SZ, "/dev/block/%d:%d", devmajor, devminor);
+    } else {
+        snprintf(ebuff, EBUFF_SZ, "/dev/char/%d:%d", devmajor, devminor);
+    }
+    fp->sg_fd = sg_cmds_open_device(ebuff, false /* rw mode */, vb);
+    if (fp->sg_fd < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 ME "could not open %s device %d:%d for sg",
+                 fp->sg_type & FT_BLOCK ? "block" : "char",
+                 devmajor, devminor);
+        perror(ebuff);
+        return -sg_convert_errno(-fp->sg_fd);
+    }
+    if (sg_simple_inquiry(fp->sg_fd, &sir, false, vb)) {
+        pr2serr("INQUIRY failed on %s\n", ebuff);
+        res = sg_cmds_close_device(fp->sg_fd);
+        if (res < 0)
+            pr2serr("sg_cmds_close_device() failed as well\n");
+        fp->sg_fd = -1;
+        return -1;
+    }
+
+    fp->pdt = sir.peripheral_type;
+    if (vb)
+        pr2serr("    %s: %.8s  %.16s  %.4s  [pdt=%d, 3pc=%d]\n", fp->fname,
+                sir.vendor, sir.product, sir.revision, fp->pdt,
+                !! (0x8 & sir.byte_5));
+
+    return fp->sg_fd;
+}
+
+static int
+dd_filetype(struct xcopy_fp_t * fp)
+{
+    struct stat st;
+    size_t len = strlen(fp->fname);
+
+    if ((1 == len) && ('.' == fp->fname[0]))
+        return FT_DEV_NULL;
+    if (stat(fp->fname, &st) < 0)
+        return FT_ERROR;
+    if (S_ISCHR(st.st_mode)) {
+        fp->devno = st.st_rdev;
+        /* major() and minor() defined in sys/sysmacros.h */
+        if ((MEM_MAJOR == major(st.st_rdev)) &&
+            (DEV_NULL_MINOR_NUM == minor(st.st_rdev)))
+            return FT_DEV_NULL;
+        if (RAW_MAJOR == major(st.st_rdev))
+            return FT_RAW;
+        if (SCSI_GENERIC_MAJOR == major(st.st_rdev))
+            return FT_SG;
+        if (SCSI_TAPE_MAJOR == major(st.st_rdev))
+            return FT_ST;
+        if (! bsg_major_checked) {
+            bsg_major_checked = true;
+            find_bsg_major();
+        }
+        if (bsg_major == (int)major(st.st_rdev))
+            return FT_SG;
+    } else if (S_ISBLK(st.st_mode)) {
+        fp->devno = st.st_rdev;
+        return FT_BLOCK;
+    } else if (S_ISFIFO(st.st_mode)) {
+        fp->devno = st.st_dev;
+        return FT_FIFO;
+    }
+    fp->devno = st.st_dev;
+    return FT_OTHER | FT_BLOCK;
+}
+
+
+static char *
+dd_filetype_str(int ft, char * buff)
+{
+    int off = 0;
+
+    if (FT_DEV_NULL & ft)
+        off += sg_scnpr(buff + off, 32, "null device ");
+    if (FT_SG & ft)
+        off += sg_scnpr(buff + off, 32, "SCSI generic (sg) device ");
+    if (FT_BLOCK & ft)
+        off += sg_scnpr(buff + off, 32, "block device ");
+    if (FT_FIFO & ft)
+        off += sg_scnpr(buff + off, 32, "fifo (named pipe) ");
+    if (FT_ST & ft)
+        off += sg_scnpr(buff + off, 32, "SCSI tape device ");
+    if (FT_RAW & ft)
+        off += sg_scnpr(buff + off, 32, "raw device ");
+    if (FT_OTHER & ft)
+        off += sg_scnpr(buff + off, 32, "other (perhaps ordinary file) ");
+    if (FT_ERROR & ft)
+        sg_scnpr(buff + off, 32, "unable to 'stat' file ");
+    return buff;
+}
+
+static int
+simplified_ft(const struct xcopy_fp_t * xfp)
+{
+    int ftype = xfp->sg_type;
+
+    switch (ftype) {
+    case FT_BLOCK:
+    case FT_ST:
+    case FT_OTHER:      /* typically regular file */
+    case FT_DEV_NULL:
+    case FT_FIFO:
+    case FT_ERROR:
+        return ftype;
+    default:
+        if (FT_SG & ftype) {
+            if ((0 == xfp->pdt) || (0xe == xfp->pdt)) /* D-A or RBC */
+            return FT_BLOCK;
+        else if (0x1 == xfp->pdt)
+            return FT_ST;
+        }
+        return FT_OTHER;
+    }
+}
+
+static int
+seg_desc_from_dd_type(int in_ft, int in_off, int out_ft, int out_off)
+{
+    int desc_type = -1;
+
+    switch (in_ft) {
+    case FT_BLOCK:
+        switch (out_ft) {
+        case FT_ST:
+            if (out_off)
+                break;
+
+            if (in_off)
+                desc_type = 0x8;
+            else
+                desc_type = 0;
+            break;
+        case FT_BLOCK:
+            if (in_off || out_off)
+                desc_type = 0xA;
+            else
+                desc_type = 2;
+            break;
+        default:
+            break;
+        }
+        break;
+    case FT_ST:
+        if (in_off)
+            break;
+
+        switch (out_ft) {
+        case FT_ST:
+            if (!out_off) {
+                desc_type = 3;
+                break;
+            }
+            break;
+        case FT_BLOCK:
+            if (out_off)
+                desc_type = 9;
+            else
+                desc_type = 3;
+            break;
+        case FT_DEV_NULL:
+            desc_type = 6;
+            break;
+        default:
+            break;
+        }
+        break;
+    default:
+        break;
+    }
+
+    return desc_type;
+}
+
+static void
+usage(int n_help)
+{
+    if (n_help < 2)
+        goto primary_help;
+    else
+        goto secondary_help;
+
+primary_help:
+    pr2serr("Usage: "
+            "sg_xcopy [app=0|1] [bpt=BPT] [bs=BS] [cat=0|1] [conv=CONV]\n"
+            "                [count=COUNT] [dc=0|1] [ibs=BS]\n"
+            "                [id_usage=hold|discard|disable] [if=IFILE] "
+            "[iflag=FLAGS]\n"
+            "                [list_id=ID] [obs=BS] [of=OFILE] "
+            "[oflag=FLAGS] [prio=PRIO]\n"
+            "                [seek=SEEK] [skip=SKIP] [time=0|1] "
+            "[verbose=VERB]\n"
+            "                [--help] [--on_dst|--on_src] [--verbose] "
+            "[--version]\n\n"
+            "  where:\n"
+            "    app         if argument is 1 then open OFILE in append "
+            "mode\n"
+            "    bpt         is blocks_per_transfer (default: 128)\n"
+            "    bs          block size (default is 512)\n");
+    pr2serr("    cat         xcopy segment descriptor CAT bit (default: "
+            "0)\n"
+            "    conv        ignored\n"
+            "    count       number of blocks to copy (def: device size)\n"
+            "    dc          xcopy segment descriptor DC bit (default: 0)\n"
+            "    fco         xcopy segment descriptor FCO bit (default: 0)\n"
+            "    ibs         input block size (if given must be same as "
+            "'bs=')\n"
+            "    id_usage    sets list_id_usage field to hold (0), "
+            "discard (2) or\n"
+            "                disable (3)\n"
+            "    if          file or device to read from (def: stdin)\n"
+            "    iflag       comma separated list of flags applying to "
+            "IFILE\n"
+            "    list_id     sets list_id field to ID (default: 1 or 0)\n"
+            "    obs         output block size (if given must be same as "
+            "'bs=')\n"
+            "    of          file or device to write to (def: stdout), "
+            "OFILE of '.'\n");
+    pr2serr("                treated as /dev/null\n"
+            "    oflag       comma separated list of flags applying to "
+            "OFILE\n"
+            "    prio        set xcopy priority field to PRIO (def: 1)\n"
+            "    seek        block position to start writing to OFILE\n"
+            "    skip        block position to start reading from IFILE\n"
+            "    time        0->no timing(def), 1->time plus calculate "
+            "throughput\n"
+            "    verbose     0->quiet(def), 1->some noise, 2->more noise, "
+            "etc\n"
+            "    --help|-h   print out this usage message then exit\n"
+            "    --on_dst    send XCOPY command to OFILE\n"
+            "    --on_src    send XCOPY command to IFILE\n"
+            "    --verbose|-v   same action as verbose=1\n"
+            "    --version|-V   print version information then exit\n\n"
+            "Copy from IFILE to OFILE, similar to dd command; "
+            "but using the SCSI\nEXTENDED COPY (XCOPY(LID1)) command. For "
+            "list of flags, use '-hh'.\n");
+    return;
+
+secondary_help:
+    pr2serr("FLAGS:\n"
+            "  append (o)     open OFILE in append mode\n"
+            "  excl           open corresponding device with O_EXCL\n"
+            "  flock          call flock(LOCK_EX|LOCK_NB)\n"
+            "  null           does nothing, placeholder\n"
+            "  pad            set xcopy data descriptor PAD bit on\n"
+            "                 corresponding device\n"
+            "  xcopy          send XCOPY command to corresponding device\n"
+            "\n"
+            "ENVIRONMENT VARIABLES:\n"
+            "  XCOPY_TO_DST   send XCOPY command to OFILE (destination) "
+            "if no other\n"
+            "                 indication\n"
+            "  XCOPY_TO_SRC   send XCOPY command to IFILE (source)\n"
+           );
+}
+
+static int
+scsi_encode_seg_desc(uint8_t *seg_desc, int seg_desc_type,
+                     int64_t num_blk, uint64_t src_lba, uint64_t dst_lba)
+{
+    int seg_desc_len = 0;
+
+    seg_desc[0] = (uint8_t)seg_desc_type;
+    seg_desc[1] = 0x0;
+    if (xcopy_flag_cat)
+        seg_desc[1] |= 0x1;
+    if (xcopy_flag_dc)
+        seg_desc[1] |= 0x2;
+    if (xcopy_flag_fco)
+        seg_desc[1] |= 0x4;
+    if (seg_desc_type == 0x02) {
+        seg_desc_len = 0x18;
+        seg_desc[4] = 0;
+        seg_desc[5] = 0; /* Source target index */
+        seg_desc[7] = 1; /* Destination target index */
+        sg_put_unaligned_be16(num_blk, seg_desc + 10);
+        sg_put_unaligned_be64(src_lba, seg_desc + 12);
+        sg_put_unaligned_be64(dst_lba, seg_desc + 20);
+    }
+    sg_put_unaligned_be16(seg_desc_len, seg_desc + 2);
+    return seg_desc_len + 4;
+}
+
+static int
+scsi_extended_copy(int sg_fd, uint8_t list_id,
+                   uint8_t *src_desc, int src_desc_len,
+                   uint8_t *dst_desc, int dst_desc_len,
+                   int seg_desc_type, int64_t num_blk,
+                   uint64_t src_lba, uint64_t dst_lba)
+{
+    uint8_t xcopyBuff[256];
+    int desc_offset = 16;
+    int seg_desc_len;
+    int verb, res;
+    char b[80];
+
+    verb = (verbose > 1) ? (verbose - 2) : 0;
+    memset(xcopyBuff, 0, 256);
+    xcopyBuff[0] = list_id;
+    xcopyBuff[1] = (list_id_usage << 3) | priority;
+    xcopyBuff[2] = 0;
+    xcopyBuff[3] = src_desc_len + dst_desc_len; /* Two target descriptors */
+    memcpy(xcopyBuff + desc_offset, src_desc, src_desc_len);
+    desc_offset += src_desc_len;
+    memcpy(xcopyBuff + desc_offset, dst_desc, dst_desc_len);
+    desc_offset += dst_desc_len;
+    seg_desc_len = scsi_encode_seg_desc(xcopyBuff + desc_offset,
+                                        seg_desc_type, num_blk,
+                                        src_lba, dst_lba);
+    xcopyBuff[11] = seg_desc_len; /* One segment descriptor */
+    desc_offset += seg_desc_len;
+    /* set noisy so if a UA happens it will be printed to stderr */
+    res = sg_ll_3party_copy_out(sg_fd, SA_XCOPY_LID1, list_id,
+                                DEF_GROUP_NUM, DEF_3PC_OUT_TIMEOUT,
+                                xcopyBuff, desc_offset, true, verb);
+    if (res) {
+        sg_get_category_sense_str(res, sizeof(b), b, verb);
+        pr2serr("Xcopy(LID1): %s\n", b);
+    }
+    return res;
+}
+
+/* Return of 0 -> success, see sg_ll_read_capacity*() otherwise */
+static int
+scsi_read_capacity(struct xcopy_fp_t *xfp)
+{
+    int res;
+    unsigned int ui;
+    uint8_t rcBuff[RCAP16_REPLY_LEN];
+    int verb;
+    char b[80];
+
+    verb = (verbose ? verbose - 1: 0);
+    res = sg_ll_readcap_10(xfp->sg_fd, false /* pmi */, 0, rcBuff,
+                           READ_CAP_REPLY_LEN, true, verb);
+    if (0 != res) {
+        sg_get_category_sense_str(res, sizeof(b), b, verb);
+        pr2serr("Read capacity(10): %s\n", b);
+        return res;
+    }
+
+    if ((0xff == rcBuff[0]) && (0xff == rcBuff[1]) && (0xff == rcBuff[2]) &&
+        (0xff == rcBuff[3])) {
+        uint64_t ls;
+
+        res = sg_ll_readcap_16(xfp->sg_fd, false /* pmi */, 0, rcBuff,
+                               RCAP16_REPLY_LEN, true, verb);
+        if (0 != res) {
+            sg_get_category_sense_str(res, sizeof(b), b, verb);
+            pr2serr("Read capacity(16): %s\n", b);
+            return res;
+        }
+        ls = sg_get_unaligned_be64(rcBuff + 0);
+        xfp->num_sect = (int64_t)(ls + 1);
+        xfp->sect_sz = sg_get_unaligned_be32(rcBuff + 8);
+    } else {
+        ui = sg_get_unaligned_be32(rcBuff + 0);
+        /* take care not to sign extend values > 0x7fffffff */
+        xfp->num_sect = (int64_t)ui + 1;
+        xfp->sect_sz = sg_get_unaligned_be32(rcBuff + 4);
+    }
+    if (verbose)
+        pr2serr("    %s: number of blocks=%" PRId64 " [0x%" PRIx64 "], block "
+                "size=%d\n", xfp->fname, xfp->num_sect, xfp->num_sect,
+                xfp->sect_sz);
+    return 0;
+}
+
+static int
+scsi_operating_parameter(struct xcopy_fp_t *xfp, int is_target)
+{
+    bool valid = false;
+    int res, ftype, snlid, verb;
+    uint32_t rcBuffLen = 256, len, n, td_list = 0;
+    uint32_t num, max_target_num, max_segment_num, max_segment_len;
+    uint32_t max_desc_len, max_inline_data, held_data_limit;
+    uint8_t rcBuff[256];
+    char b[80];
+
+    verb = (verbose ? verbose - 1: 0);
+    ftype = xfp->sg_type;
+    if (FT_SG & ftype) {
+        if ((0 == xfp->pdt) || (0xe == xfp->pdt)) /* direct-access or RBC */
+            ftype |= FT_BLOCK;
+        else if (0x1 == xfp->pdt)
+            ftype |= FT_ST;
+    }
+    res = sg_ll_receive_copy_results(xfp->sg_fd, SA_COPY_OP_PARAMS, 0, rcBuff,
+                                     rcBuffLen, true, verb);
+    if (0 != res) {
+        sg_get_category_sense_str(res, sizeof(b), b, verb);
+        pr2serr("Xcopy operating parameters: %s\n", b);
+        return -res;
+    }
+
+    len = sg_get_unaligned_be32(rcBuff + 0);
+    if (len > rcBuffLen) {
+        pr2serr("  <<report len %d > %d too long for internal buffer, output "
+                "truncated\n", len, rcBuffLen);
+    }
+    if (verbose > 2) {
+        pr2serr("\nOutput response in hex:\n");
+        hex2stderr(rcBuff, len, 1);
+    }
+    snlid = rcBuff[4] & 0x1;
+    max_target_num = sg_get_unaligned_be16(rcBuff + 8);
+    max_segment_num = sg_get_unaligned_be16(rcBuff + 10);
+    max_desc_len = sg_get_unaligned_be32(rcBuff + 12);
+    max_segment_len = sg_get_unaligned_be32(rcBuff + 16);
+    xfp->max_bytes = max_segment_len ? max_segment_len : UINT32_MAX;
+    max_inline_data = sg_get_unaligned_be32(rcBuff + 20);
+    if (verbose) {
+        pr2serr(" >> %s response:\n", rec_copy_op_params_str);
+        pr2serr("    Support No List IDentifier (SNLID): %d\n", snlid);
+        pr2serr("    Maximum target descriptor count: %u\n",
+                (unsigned int)max_target_num);
+        pr2serr("    Maximum segment descriptor count: %u\n",
+                (unsigned int)max_segment_num);
+        pr2serr("    Maximum descriptor list length: %u\n",
+                (unsigned int)max_desc_len);
+        pr2serr("    Maximum segment length: %u\n",
+                (unsigned int)max_segment_len);
+        pr2serr("    Maximum inline data length: %u\n",
+                (unsigned int)max_inline_data);
+    }
+    held_data_limit = sg_get_unaligned_be32(rcBuff + 24);
+    if (list_id_usage < 0) {
+        if (!held_data_limit)
+            list_id_usage = 2;
+        else
+            list_id_usage = 0;
+    }
+    if (verbose) {
+        pr2serr("    Held data limit: %u (list_id_usage: %d)\n",
+                (unsigned int)held_data_limit, list_id_usage);
+        num = sg_get_unaligned_be32(rcBuff + 28);
+        pr2serr("    Maximum stream device transfer size: %u\n",
+                (unsigned int)num);
+        pr2serr("    Maximum concurrent copies: %u\n", rcBuff[36]);
+        if (rcBuff[37] > 30)
+            pr2serr("    Data segment granularity: 2**%u bytes\n",
+                    rcBuff[37]);
+        else
+            pr2serr("    Data segment granularity: %u bytes\n",
+                    1 << rcBuff[37]);
+        if (rcBuff[38] > 30)
+            pr2serr("    Inline data granularity: 2**%u bytes\n", rcBuff[38]);
+        else
+            pr2serr("    Inline data granularity: %u bytes\n",
+                    1 << rcBuff[38]);
+        if (rcBuff[39] > 30)
+            pr2serr("    Held data granularity: 2**%u bytes\n",
+                    1 << rcBuff[39]);
+        else
+            pr2serr("    Held data granularity: %u bytes\n", 1 << rcBuff[39]);
+
+        pr2serr("    Implemented descriptor list:\n");
+    }
+    xfp->min_bytes = 1 << rcBuff[37];
+
+    for (n = 0; n < rcBuff[43]; n++) {
+        switch(rcBuff[44 + n]) {
+        case 0x00: /* copy block to stream device */
+            if (!is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (is_target && (ftype & FT_ST))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy Block to Stream device\n");
+            break;
+        case 0x01: /* copy stream to block device */
+            if (!is_target && (ftype & FT_ST))
+                valid = true;
+            if (is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy Stream to Block device\n");
+            break;
+        case 0x02: /* copy block to block device */
+            if (!is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy Block to Block device\n");
+            break;
+        case 0x03: /* copy stream to stream device */
+            if (!is_target && (ftype & FT_ST))
+                valid = true;
+            if (is_target && (ftype & FT_ST))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy Stream to Stream device\n");
+            break;
+        case 0x04: /* copy inline data to stream device */
+            if (!is_target && (ftype & FT_OTHER))
+                valid = true;
+            if (is_target && (ftype & FT_ST))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy inline data to Stream device\n");
+            break;
+        case 0x05: /* copy embedded data to stream device */
+            if (!is_target && (ftype & FT_OTHER))
+                valid = true;
+            if (is_target && (ftype & FT_ST))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy embedded data to Stream device\n");
+            break;
+        case 0x06: /* Read from stream device and discard */
+            if (!is_target && (ftype & FT_ST))
+                valid = true;
+            if (is_target && (ftype & FT_DEV_NULL))
+                valid = true;
+            if (verbose)
+                pr2serr("        Read from stream device and discard\n");
+            break;
+        case 0x07: /* Verify block or stream device operation */
+            if (!is_target && (ftype & (FT_ST | FT_BLOCK)))
+                valid = true;
+            if (is_target && (ftype & (FT_ST | FT_BLOCK)))
+                valid = true;
+            if (verbose)
+                pr2serr("        Verify block or stream device operation\n");
+            break;
+        case 0x08: /* copy block device with offset to stream device */
+            if (!is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (is_target && (ftype & FT_ST))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy block device with offset to stream "
+                       "device\n");
+            break;
+        case 0x09: /* copy stream device to block device with offset */
+            if (!is_target && (ftype & FT_ST))
+                valid = true;
+            if (is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy stream device to block device with "
+                       "offset\n");
+            break;
+        case 0x0a: /* copy block device with offset to block device with
+                    * offset */
+            if (!is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy block device with offset to block "
+                       "device with offset\n");
+            break;
+        case 0x0b: /* copy block device to stream device and hold data */
+            if (!is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (is_target && (ftype & FT_ST))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy block device to stream device and hold "
+                       "data\n");
+            break;
+        case 0x0c: /* copy stream device to block device and hold data */
+            if (!is_target && (ftype & FT_ST))
+                valid = true;
+            if (is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy stream device to block device and hold "
+                       "data\n");
+            break;
+        case 0x0d: /* copy block device to block device and hold data */
+            if (!is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (is_target && (ftype & FT_BLOCK))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy block device to block device and hold "
+                       "data\n");
+            break;
+        case 0x0e: /* copy stream device to stream device and hold data */
+            if (!is_target && (ftype & FT_ST))
+                valid = true;
+            if (is_target && (ftype & FT_ST))
+                valid = true;
+            if (verbose)
+                pr2serr("        Copy block device to block device and hold "
+                       "data\n");
+            break;
+        case 0x0f: /* read from stream device and hold data */
+            if (!is_target && (ftype & FT_ST))
+                valid = true;
+            if (is_target && (ftype & FT_DEV_NULL))
+                valid = true;
+            if (verbose)
+                pr2serr("        Read from stream device and hold data\n");
+            break;
+        case 0xe0: /* FC N_Port_Name */
+            if (verbose)
+                pr2serr("        FC N_Port_Name target descriptor\n");
+            td_list |= TD_FC_WWPN;
+            break;
+        case 0xe1: /* FC Port_ID */
+            if (verbose)
+                pr2serr("        FC Port_ID target descriptor\n");
+            td_list |= TD_FC_PORT;
+            break;
+        case 0xe2: /* FC N_Port_ID with N_Port_Name checking */
+            if (verbose)
+                pr2serr("        FC N_Port_ID with N_Port_Name target "
+                       "descriptor\n");
+            td_list |= TD_FC_WWPN_AND_PORT;
+            break;
+        case 0xe3: /* Parallel Interface T_L  */
+            if (verbose)
+                pr2serr("        SPI T_L target descriptor\n");
+            td_list |= TD_SPI;
+            break;
+        case 0xe4: /* identification descriptor */
+            if (verbose)
+                pr2serr("        Identification target descriptor\n");
+            td_list |= TD_VPD;
+            break;
+        case 0xe5: /* IPv4  */
+            if (verbose)
+                pr2serr("        IPv4 target descriptor\n");
+            td_list |= TD_IPV4;
+            break;
+        case 0xe6: /* Alias */
+            if (verbose)
+                pr2serr("        Alias target descriptor\n");
+            td_list |= TD_ALIAS;
+            break;
+        case 0xe7: /* RDMA */
+            if (verbose)
+                pr2serr("        RDMA target descriptor\n");
+            td_list |= TD_RDMA;
+            break;
+        case 0xe8: /* FireWire */
+            if (verbose)
+                pr2serr("        IEEE 1394 target descriptor\n");
+            td_list |= TD_FW;
+            break;
+        case 0xe9: /* SAS */
+            if (verbose)
+                pr2serr("        SAS target descriptor\n");
+            td_list |= TD_SAS;
+            break;
+        case 0xea: /* IPv6 */
+            if (verbose)
+                pr2serr("        IPv6 target descriptor\n");
+            td_list |= TD_IPV6;
+            break;
+        case 0xeb: /* IP Copy Service */
+            if (verbose)
+                pr2serr("        IP Copy Service target descriptor\n");
+            td_list |= TD_IP_COPY_SERVICE;
+            break;
+        case 0xfe: /* ROD */
+            if (verbose)
+                pr2serr("        ROD target descriptor\n");
+            td_list |= TD_ROD;
+            break;
+        default:
+            pr2serr(">> Unhandled target descriptor 0x%02x\n",
+                    rcBuff[44 + n]);
+            break;
+        }
+    }
+    if (! valid) {
+        pr2serr(">> no matching target descriptor supported\n");
+        td_list = 0;
+    }
+    return td_list;
+}
+
+static void
+decode_designation_descriptor(const uint8_t * bp, int i_len)
+{
+    char c[2048];
+
+    sg_get_designation_descriptor_str(NULL, bp, i_len, 1, verbose,
+                                      sizeof(c), c);
+    pr2serr("%s", c);
+}
+
+static int
+desc_from_vpd_id(int sg_fd, uint8_t *desc, int desc_len,
+                 unsigned int block_size, bool pad)
+{
+    int res, verb;
+    uint8_t rcBuff[256], *bp, *best = NULL;
+    unsigned int len = 254;
+    int off = -1, i_len, best_len = 0, assoc, desig, f_desig = 0;
+    char b[80];
+
+    verb = (verbose ? verbose - 1: 0);
+    memset(rcBuff, 0xff, len);
+    res = sg_ll_inquiry(sg_fd, false, true /* evpd */, VPD_DEVICE_ID, rcBuff,
+                        4, true, verb);
+    if (0 != res) {
+        if (SG_LIB_CAT_ILLEGAL_REQ == res)
+            pr2serr("Device identification VPD page not found\n");
+        else {
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("VPD inquiry (Device ID): %s\n", b);
+            pr2serr("   try again with '-vv'\n");
+        }
+        return res;
+    } else if (rcBuff[1] != VPD_DEVICE_ID) {
+        pr2serr("invalid VPD response\n");
+        return SG_LIB_CAT_MALFORMED;
+    }
+    len = sg_get_unaligned_be16(rcBuff + 2) + 4;
+    res = sg_ll_inquiry(sg_fd, false, true, VPD_DEVICE_ID, rcBuff, len, true,
+                        verb);
+    if (0 != res) {
+        sg_get_category_sense_str(res, sizeof(b), b, verbose);
+        pr2serr("VPD inquiry (Device ID): %s\n", b);
+        return res;
+    } else if (rcBuff[1] != VPD_DEVICE_ID) {
+        pr2serr("invalid VPD response\n");
+        return SG_LIB_CAT_MALFORMED;
+    }
+    if (verbose > 2) {
+        pr2serr("Output response in hex:\n");
+        hex2stderr(rcBuff, len, 1);
+    }
+
+    while (sg_vpd_dev_id_iter(rcBuff + 4, len - 4, &off, 0, -1, -1) == 0) {
+        bp = rcBuff + 4 + off;
+        i_len = bp[3];
+        if (((unsigned int)off + i_len + 4) > len) {
+            pr2serr("    VPD page error: designator length %d longer "
+                    "than\n     remaining response length=%d\n", i_len,
+                    (len - off));
+            return SG_LIB_CAT_MALFORMED;
+        }
+        assoc = ((bp[1] >> 4) & 0x3);
+        desig = (bp[1] & 0xf);
+        if (verbose > 2)
+            pr2serr("    Desc %d: assoc %u desig %u len %d\n", off, assoc,
+                    desig, i_len);
+        /* Identification descriptor's Designator length must be <= 20. */
+        if (i_len > 20)
+            continue;
+        if (desig == /*NAA=*/3) {
+            best = bp;
+            best_len = i_len;
+            break;
+        }
+        if (desig == /*EUI64=*/2) {
+            if (!best || f_desig < 2) {
+                best = bp;
+                best_len = i_len;
+                f_desig = 2;
+            }
+        } else if (desig == /*T10*/1) {
+            if (!best || f_desig == 0) {
+                best = bp;
+                best_len = i_len;
+                f_desig = desig;
+            }
+        } else if (desig == /*vend.spec.=*/0) {
+            if (!best) {
+                best = bp;
+                best_len = i_len;
+                f_desig = desig;
+            }
+        }
+    }
+    if (best) {
+        if (verbose)
+            decode_designation_descriptor(best, best_len);
+        if (best_len + 4 < desc_len) {
+            memset(desc, 0, 32);
+            desc[0] = 0xe4; /* Identification Descriptor */
+            memcpy(desc + 4, best, best_len + 4);
+            desc[4] &= 0x0f; /* code set */
+            desc[5] &= 0x3f; /* association and designator type */
+            if (pad)
+                desc[28] = 0x4;
+            sg_put_unaligned_be24((uint32_t)block_size, desc + 29);
+            if (verbose > 3) {
+                pr2serr("Descriptor in hex (bs %d):\n", block_size);
+                hex2stderr(desc, 32, 1);
+            }
+            return 32;
+        }
+        return  best_len + 8;
+    }
+    return 0;
+}
+
+static void
+calc_duration_throughput(int contin)
+{
+    struct timeval end_tm, res_tm;
+    double a, b;
+    int64_t blks;
+
+    if (start_tm_valid && (start_tm.tv_sec || start_tm.tv_usec)) {
+        blks = (in_full > out_full) ? in_full : out_full;
+        gettimeofday(&end_tm, NULL);
+        res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+        res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+        if (res_tm.tv_usec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_usec += 1000000;
+        }
+        a = res_tm.tv_sec;
+        a += (0.000001 * res_tm.tv_usec);
+        b = (double)blk_sz * blks;
+        pr2serr("time to transfer data%s: %d.%06d secs",
+                (contin ? " so far" : ""), (int)res_tm.tv_sec,
+                (int)res_tm.tv_usec);
+        if ((a > 0.00001) && (b > 511))
+            pr2serr(" at %.2f MB/sec\n", b / (a * 1000000.0));
+        else
+            pr2serr("\n");
+    }
+}
+
+/* Process arguments given to 'iflag=" or 'oflag=" options. Returns 0
+ * on success, 1 on error. */
+static int
+process_flags(const char * arg, struct xcopy_fp_t * fp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff) - 1);
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no flag found\n");
+        return 1;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "append"))
+            fp->append = true;
+        else if (0 == strcmp(cp, "excl"))
+            fp->excl = true;
+        else if (0 == strcmp(cp, "flock"))
+            fp->flock = true;
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else if (0 == strcmp(cp, "pad"))
+            fp->pad = true;
+        else if (0 == strcmp(cp, "xcopy"))
+            fp->xcopy_given = true;   /* for ddpt compatibility */
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return 1;
+        }
+        cp = np;
+    } while (cp);
+    return 0;
+}
+
+/* Returns open input file descriptor (>= 0) or a negative value
+ * (-SG_LIB_FILE_ERROR or -SG_LIB_CAT_OTHER) if error.
+ */
+static int
+open_if(struct xcopy_fp_t * ifp, int vb)
+{
+    int infd = -1, flags, fl, res, err;
+    char ebuff[EBUFF_SZ];
+
+    ifp->sg_type = dd_filetype(ifp);
+
+    if (vb)
+        pr2serr(" >> Input file type: %s, devno %d:%d\n",
+                dd_filetype_str(ifp->sg_type, ebuff),
+                major(ifp->devno), minor(ifp->devno));
+    if (FT_ERROR & ifp->sg_type) {
+        pr2serr(ME "unable access %s\n", ifp->fname);
+        return -SG_LIB_FILE_ERROR;
+    }
+    flags = O_NONBLOCK;
+    if (ifp->excl)
+        flags |= O_EXCL;
+    fl = O_RDWR;
+    if ((infd = open(ifp->fname, fl | flags)) < 0) {
+        fl = O_RDONLY;
+        if ((infd = open(ifp->fname, fl | flags)) < 0) {
+            err = errno;
+            snprintf(ebuff, EBUFF_SZ,
+                     ME "could not open %.500s for sg reading", ifp->fname);
+            perror(ebuff);
+            return -sg_convert_errno(err);
+        }
+    }
+    if (vb)
+        pr2serr("        open input(sg_io), flags=0x%x\n", fl | flags);
+
+    if (ifp->flock) {
+        res = flock(infd, LOCK_EX | LOCK_NB);
+        if (res < 0) {
+            close(infd);
+            snprintf(ebuff, EBUFF_SZ, ME "flock(LOCK_EX | LOCK_NB) on %.500s "
+                     "failed", ifp->fname);
+            perror(ebuff);
+            return -SG_LIB_FLOCK_ERR;
+        }
+    }
+    return infd;
+}
+
+/* Returns open output file descriptor (>= 0), -1 for don't
+ * bother opening (e.g. /dev/null), or a more negative value
+ * (-SG_LIB_FILE_ERROR or -SG_LIB_CAT_OTHER) if error.
+ */
+static int
+open_of(struct xcopy_fp_t * ofp, int vb)
+{
+    int outfd, flags, res, err;
+    char ebuff[EBUFF_SZ];
+
+    ofp->sg_type = dd_filetype(ofp);
+    if (vb)
+        pr2serr(" >> Output file type: %s, devno %d:%d\n",
+                dd_filetype_str(ofp->sg_type, ebuff),
+                major(ofp->devno), minor(ofp->devno));
+
+    if (!(FT_DEV_NULL & ofp->sg_type)) {
+        flags = O_RDWR | O_NONBLOCK;
+        if (ofp->excl)
+            flags |= O_EXCL;
+        if (ofp->append)
+            flags |= O_APPEND;
+        if ((outfd = open(ofp->fname, flags)) < 0) {
+            err = errno;
+            snprintf(ebuff, EBUFF_SZ,
+                     ME "could not open %.500s for sg writing", ofp->fname);
+            perror(ebuff);
+            return -sg_convert_errno(err);
+        }
+        if (vb)
+            pr2serr("        open output(sg_io), flags=0x%x\n", flags);
+    } else
+        outfd = -1; /* don't bother opening */
+    if ((outfd >= 0) && ofp->flock) {
+        res = flock(outfd, LOCK_EX | LOCK_NB);
+        if (res < 0) {
+            close(outfd);
+            snprintf(ebuff, EBUFF_SZ, ME "flock(LOCK_EX | LOCK_NB) on %.500s "
+                     "failed", ofp->fname);
+            perror(ebuff);
+            return -SG_LIB_FLOCK_ERR;
+        }
+    }
+    return outfd;
+}
+
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool bpt_given = false;
+    bool list_id_given = false;
+    bool on_src = false;
+    bool on_src_dst_given = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, k, n, keylen, infd, outfd, xcopy_fd;
+    int blocks = 0;
+    int bpt = DEF_BLOCKS_PER_TRANSFER;
+    int dst_desc_len;
+    int ibs = 0;
+    int num_help = 0;
+    int num_xcopy = 0;
+    int obs = 0;
+    int ret = 0;
+    int seg_desc_type;
+    int src_desc_len;
+    int64_t skip = 0;
+    int64_t seek = 0;
+    uint8_t list_id = 1;
+    char * key;
+    char * buf;
+    char str[STR_SZ];
+    uint8_t src_desc[256];
+    uint8_t dst_desc[256];
+
+    ixcf.fname[0] = '\0';
+    oxcf.fname[0] = '\0';
+    ixcf.num_sect = -1;
+    oxcf.num_sect = -1;
+
+    if (argc < 2) {
+        pr2serr("Won't default both IFILE to stdin _and_ OFILE to stdout\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_CONTRADICT;
+    }
+
+    for (k = 1; k < argc; k++) {
+        if (argv[k]) {
+            strncpy(str, argv[k], STR_SZ - 1);
+            str[STR_SZ - 1] = '\0';
+        } else
+            continue;
+        for (key = str, buf = key; *buf && *buf != '=';)
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        keylen = (int)strlen(key);
+        if (0 == strncmp(key, "app", 3)) {
+            ixcf.append = !! sg_get_num(buf);
+            oxcf.append = ixcf.append;
+        } else if (0 == strcmp(key, "bpt")) {
+            bpt = sg_get_num(buf);
+            if (-1 == bpt) {
+                pr2serr(ME "bad argument to 'bpt='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            bpt_given = true;
+        } else if (0 == strcmp(key, "bs")) {
+            blk_sz = sg_get_num(buf);
+            if (-1 == blk_sz) {
+                pr2serr(ME "bad argument to 'bs='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "list_id")) {
+            ret = sg_get_num(buf);
+            if (-1 == ret || ret > 0xff) {
+                pr2serr(ME "bad argument to 'list_id='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            list_id = (ret & 0xff);
+            list_id_given = true;
+        } else if (0 == strcmp(key, "id_usage")) {
+            if (!strncmp(buf, "hold", 4))
+                list_id_usage = 0;
+            else if (!strncmp(buf, "discard", 7))
+                list_id_usage = 2;
+            else if (!strncmp(buf, "disable", 7))
+                list_id_usage = 3;
+            else {
+                pr2serr(ME "bad argument to 'id_usage='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "conv"))
+            pr2serr(ME ">>> ignoring all 'conv=' arguments\n");
+        else if (0 == strcmp(key, "count")) {
+            if (0 != strcmp("-1", buf)) {
+                dd_count = sg_get_llnum(buf);
+                if (-1LL == dd_count) {
+                    pr2serr(ME "bad argument to 'count='\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }   /* treat 'count=-1' as calculate count (same as not given) */
+        } else if (0 == strcmp(key, "prio")) {
+            priority = sg_get_num(buf);
+        } else if (0 == strcmp(key, "cat")) {
+            n = sg_get_num(buf);
+            if (n < 0 || n > 1) {
+                pr2serr(ME "bad argument to 'cat='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            xcopy_flag_cat = !! n;
+        } else if (0 == strcmp(key, "dc")) {
+            n = sg_get_num(buf);
+            if (n < 0 || n > 1) {
+                pr2serr(ME "bad argument to 'dc='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            xcopy_flag_dc = !! n;
+        } else if (0 == strcmp(key, "fco")) {
+            n = sg_get_num(buf);
+            if (n < 0 || n > 1) {
+                pr2serr(ME "bad argument to 'fco='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            xcopy_flag_fco = !! n;
+        } else if (0 == strcmp(key, "ibs")) {
+            ibs = sg_get_num(buf);
+        } else if (strcmp(key, "if") == 0) {
+            if ('\0' != ixcf.fname[0]) {
+                pr2serr("Second IFILE argument??\n");
+                return SG_LIB_CONTRADICT;
+            } else {
+                memcpy(ixcf.fname, buf, INOUTF_SZ - 1);
+                ixcf.fname[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (0 == strcmp(key, "iflag")) {
+            if (process_flags(buf, &ixcf)) {
+                pr2serr(ME "bad argument to 'iflag='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "obs")) {
+            obs = sg_get_num(buf);
+        } else if (strcmp(key, "of") == 0) {
+            if ('\0' != oxcf.fname[0]) {
+                pr2serr("Second OFILE argument??\n");
+                return SG_LIB_CONTRADICT;
+            } else {
+                memcpy(oxcf.fname, buf, INOUTF_SZ - 1);
+                oxcf.fname[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (0 == strcmp(key, "oflag")) {
+            if (process_flags(buf, &oxcf)) {
+                pr2serr(ME "bad argument to 'oflag='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "seek")) {
+            seek = sg_get_llnum(buf);
+            if (-1LL == seek) {
+                pr2serr(ME "bad argument to 'seek='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "skip")) {
+            skip = sg_get_llnum(buf);
+            if (-1LL == skip) {
+                pr2serr(ME "bad argument to 'skip='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "time"))
+            do_time = !! sg_get_num(buf);
+        else if (0 == strncmp(key, "verb", 4))
+            verbose = sg_get_num(buf);
+        /* look for long options that start with '--' */
+        else if (0 == strncmp(key, "--help", 6))
+            ++num_help;
+        else if (0 == strncmp(key, "--on_dst", 8)) {
+            on_src = false;
+            if (on_src_dst_given) {
+                pr2serr("Syntax error - either specify --on_src OR "
+                        "--on_dst\n");
+                pr2serr("For more information use '--help'\n");
+                return SG_LIB_CONTRADICT;
+            }
+            on_src_dst_given = true;
+        } else if (0 == strncmp(key, "--on_src", 8)) {
+            on_src = true;
+            if (on_src_dst_given) {
+                pr2serr("Syntax error - either specify --on_src OR "
+                        "--on_dst\n");
+                pr2serr("For more information use '--help'\n");
+                return SG_LIB_CONTRADICT;
+            }
+            on_src_dst_given = true;
+        } else if (0 == strncmp(key, "--verb", 6)) {
+            verbose_given = true;
+            verbose += 1;
+        } else if (0 == strncmp(key, "--vers", 6))
+            version_given = true;
+        else if (0 == strncmp(key, "--xcopy", 7))
+            ;   /* ignore; for compatibility with ddpt */
+        /* look for short options that start with a single '-', they can be
+         * concaternated (e.g. '-vvvV') */
+        else if ((keylen > 1) && ('-' == key[0]) && ('-' != key[1])) {
+            res = 0;
+            n = num_chs_in_str(key + 1, keylen - 1, 'h');
+            num_help += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'v');
+            verbose += n;
+            if (n > 0)
+                verbose_given = true;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'V');
+            if (n > 0)
+                version_given = true;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'x');
+            /* accept and ignore; for compatibility with ddpt */
+            res += n;
+            if (res < (keylen - 1)) {
+                pr2serr(ME "Unrecognised short option in '%s', try "
+                        "'--help'\n", key);
+                if (0 == num_help)
+                    return -1;
+            }
+        } else {
+            pr2serr("Unrecognized option '%s'\n", key);
+            if (num_help)
+                usage(num_help);
+            else
+                pr2serr("For more information use '--help'\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (num_help) {
+        usage(num_help);
+        return 0;
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME "%s\n", version_str);
+        return 0;
+    }
+
+    if (! on_src_dst_given) {
+        if (ixcf.xcopy_given == oxcf.xcopy_given) {
+            char * csp;
+            char * cdp;
+
+            csp = getenv(XCOPY_TO_SRC);
+            cdp = getenv(XCOPY_TO_DST);
+            if ((!! csp) == (!! cdp)) {
+#if DEF_XCOPY_SRC0_DST1 == 0
+                on_src = true;
+#else
+                on_src = false;
+#endif
+            } else if (csp)
+                on_src = true;
+            else
+                on_src = false;
+        } else if (ixcf.xcopy_given)
+            on_src = true;
+        else
+            on_src = false;
+    }
+    if (verbose > 1)
+        pr2serr(" >>> Extended Copy(LID1) command will be sent to %s device "
+                "[%s]\n", (on_src ? "src" : "dst"),
+                (on_src ? ixcf.fname : oxcf.fname));
+
+    if ((ibs && blk_sz && (ibs != blk_sz)) ||
+        (obs && blk_sz && (obs != blk_sz))) {
+        pr2serr("If 'ibs' or 'obs' given must be same as 'bs'\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (blk_sz && !ibs)
+        ibs = blk_sz;
+    if (blk_sz && !obs)
+        obs = blk_sz;
+
+    if ((skip < 0) || (seek < 0)) {
+        pr2serr("skip and seek cannot be negative\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (oxcf.append && (seek > 0)) {
+        pr2serr("Can't use both append and seek switches\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (bpt < 1) {
+        pr2serr("bpt must be greater than 0\n");
+        return SG_LIB_SYNTAX_ERROR;
+    } else if (bpt > MAX_BLOCKS_PER_TRANSFER) {
+        pr2serr("bpt must be less than or equal to %d\n",
+                MAX_BLOCKS_PER_TRANSFER);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (list_id_usage == 3) { /* list_id usage disabled */
+        if (! list_id_given)
+            list_id = 0;
+        if (list_id) {
+            pr2serr("list_id disabled by id_usage flag\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    if (verbose > 1)
+        pr2serr(" >>> " ME " if=%s skip=%" PRId64 " of=%s seek=%" PRId64
+                " count=%" PRId64 "\n", ixcf.fname, skip, oxcf.fname, seek,
+                dd_count);
+    install_handler(SIGINT, interrupt_handler);
+    install_handler(SIGQUIT, interrupt_handler);
+    install_handler(SIGPIPE, interrupt_handler);
+    install_handler(SIGUSR1, siginfo_handler);
+
+    ixcf.pdt = -1;
+    oxcf.pdt = -1;
+    if (ixcf.fname[0] && ('-' != ixcf.fname[0])) {
+        infd = open_if(&ixcf, verbose);
+        if (infd < 0)
+            return -infd;
+    } else {
+        pr2serr("stdin not acceptable for IFILE\n");
+        return SG_LIB_FILE_ERROR;
+    }
+
+    if (oxcf.fname[0] && ('-' != oxcf.fname[0])) {
+        outfd = open_of(&oxcf, verbose);
+        if (outfd < -1)
+            return -outfd;
+    } else {
+        pr2serr("stdout not acceptable for OFILE\n");
+        return SG_LIB_FILE_ERROR;
+    }
+
+    res = open_sg(&ixcf, verbose);
+    if (res < 0) {
+        if (-1 == res)
+            return SG_LIB_FILE_ERROR;
+        else
+            return SG_LIB_CAT_OTHER;
+    }
+    res = open_sg(&oxcf, verbose);
+    if (res < 0) {
+        if (-1 == res)
+            return SG_LIB_FILE_ERROR;
+        else
+            return SG_LIB_CAT_OTHER;
+    }
+
+    if ((STDIN_FILENO == infd) && (STDOUT_FILENO == outfd)) {
+        pr2serr("Can't have both 'if' as stdin _and_ 'of' as stdout\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_CONTRADICT;
+    }
+
+    res = scsi_read_capacity(&ixcf);
+    if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+        pr2serr("Unit attention (%s in), continuing\n", read_cap_str);
+        res = scsi_read_capacity(&ixcf);
+    } else if (SG_LIB_CAT_ABORTED_COMMAND == res) {
+        pr2serr("Aborted command (%s in), continuing\n", read_cap_str);
+        res = scsi_read_capacity(&ixcf);
+    }
+    if (0 != res) {
+        if (res == SG_LIB_CAT_INVALID_OP)
+            pr2serr("%s command not supported on %s\n", read_cap_str,
+                    ixcf.fname);
+        else if (res == SG_LIB_CAT_NOT_READY)
+            pr2serr("%s failed on %s - not ready\n", read_cap_str,
+                    ixcf.fname);
+        else
+            pr2serr("Unable to %s on %s\n", read_cap_str, ixcf.fname);
+        ixcf.num_sect = -1;
+    } else if (ibs && ixcf.sect_sz != ibs) {
+        pr2serr(">> warning: block size on %s confusion: "
+                "ibs=%d, device claims=%d\n", ixcf.fname, ibs, ixcf.sect_sz);
+    }
+    if (skip && ixcf.num_sect < skip) {
+        pr2serr("argument to 'skip=' exceeds device size (max %" PRId64 ")\n",
+                ixcf.num_sect);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    res = scsi_read_capacity(&oxcf);
+    if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+        pr2serr("Unit attention (%s out), continuing\n", read_cap_str);
+        res = scsi_read_capacity(&oxcf);
+    } else if (SG_LIB_CAT_ABORTED_COMMAND == res) {
+        pr2serr("Aborted command (%s out), continuing\n", read_cap_str);
+        res = scsi_read_capacity(&oxcf);
+    }
+    if (0 != res) {
+        if (res == SG_LIB_CAT_INVALID_OP)
+            pr2serr("%s command not supported on %s\n", read_cap_str,
+                    oxcf.fname);
+        else
+            pr2serr("Unable to %s on %s\n", read_cap_str, oxcf.fname);
+        oxcf.num_sect = -1;
+    } else if (obs && obs != oxcf.sect_sz) {
+        pr2serr(">> warning: block size on %s confusion: obs=%d, device "
+                "claims=%d\n", oxcf.fname, obs, oxcf.sect_sz);
+    }
+    if (seek && oxcf.num_sect < seek) {
+        pr2serr("argument to 'seek=' exceeds device size (max %" PRId64 ")\n",
+                oxcf.num_sect);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((dd_count < 0) || ((verbose > 0) && (0 == dd_count))) {
+        if (xcopy_flag_dc == 0) {
+            dd_count = ixcf.num_sect - skip;
+            if ((dd_count * ixcf.sect_sz) >
+                ((oxcf.num_sect - seek) * oxcf.sect_sz))
+                dd_count = (oxcf.num_sect - seek) * oxcf.sect_sz /
+                           ixcf.sect_sz;
+        } else {
+            dd_count = oxcf.num_sect - seek;
+            if ((dd_count * oxcf.sect_sz) >
+                ((ixcf.num_sect - skip) * ixcf.sect_sz))
+                dd_count = (ixcf.num_sect - skip) * ixcf.sect_sz /
+                           oxcf.sect_sz;
+        }
+    } else {
+        int64_t dd_bytes;
+
+        if (xcopy_flag_dc)
+            dd_bytes = dd_count * oxcf.sect_sz;
+        else
+            dd_bytes = dd_count * ixcf.sect_sz;
+
+        if (dd_bytes > ixcf.num_sect * ixcf.sect_sz) {
+            pr2serr("access beyond end of source device (max %" PRId64 ")\n",
+                    ixcf.num_sect);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if (dd_bytes > oxcf.num_sect * oxcf.sect_sz) {
+            pr2serr("access beyond end of target device (max %" PRId64 ")\n",
+                    oxcf.num_sect);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    res = scsi_operating_parameter(&ixcf, 0);
+    if (res < 0) {
+        if (SG_LIB_CAT_UNIT_ATTENTION == -res) {
+            pr2serr("Unit attention (%s), continuing\n",
+                    rec_copy_op_params_str);
+            res = scsi_operating_parameter(&ixcf, 0);
+        }
+        if (-res == SG_LIB_CAT_INVALID_OP) {
+            pr2serr("%s command not supported on %s\n",
+                    rec_copy_op_params_str, ixcf.fname);
+            ret = sg_convert_errno(EINVAL);
+            goto fini;
+        } else if (-res == SG_LIB_CAT_NOT_READY)
+            pr2serr("%s failed on %s - not ready\n",
+                    rec_copy_op_params_str, ixcf.fname);
+        else {
+            pr2serr("Unable to %s on %s\n", rec_copy_op_params_str,
+                    ixcf.fname);
+            ret = -res;
+            goto fini;
+        }
+    } else if (res == 0) {
+        ret = SG_LIB_CAT_INVALID_OP;
+        goto fini;
+    }
+
+    if (res & TD_VPD) {
+        if (verbose)
+            pr2serr("  >> using VPD identification for source %s\n",
+                    ixcf.fname);
+        src_desc_len = desc_from_vpd_id(ixcf.sg_fd, src_desc,
+                                 sizeof(src_desc), ixcf.sect_sz, ixcf.pad);
+        if (src_desc_len > (int)sizeof(src_desc)) {
+            pr2serr("source descriptor too large (%d bytes)\n", res);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto fini;
+        }
+    } else {
+        ret = SG_LIB_CAT_INVALID_OP;
+        goto fini;
+    }
+
+    res = scsi_operating_parameter(&oxcf, 1);
+    if (res < 0) {
+        if (SG_LIB_CAT_UNIT_ATTENTION == -res) {
+            pr2serr("Unit attention (%s), continuing\n",
+                    rec_copy_op_params_str);
+            res = scsi_operating_parameter(&oxcf, 1);
+        }
+        if (-res == SG_LIB_CAT_INVALID_OP) {
+            pr2serr("%s command not supported on %s\n",
+                    rec_copy_op_params_str, oxcf.fname);
+            ret = sg_convert_errno(EINVAL);
+            goto fini;
+        } else if (-res == SG_LIB_CAT_NOT_READY)
+            pr2serr("%s failed on %s - not ready\n",
+                    rec_copy_op_params_str, oxcf.fname);
+        else {
+            pr2serr("Unable to %s on %s\n", rec_copy_op_params_str,
+                    oxcf.fname);
+            ret = -res;
+            goto fini;
+        }
+    } else if (res == 0) {
+        ret = SG_LIB_CAT_INVALID_OP;
+        goto fini;
+    }
+
+    if (res & TD_VPD) {
+        if (verbose)
+            pr2serr("  >> using VPD identification for destination %s\n",
+                    oxcf.fname);
+        dst_desc_len = desc_from_vpd_id(oxcf.sg_fd, dst_desc,
+                                 sizeof(dst_desc), oxcf.sect_sz, oxcf.pad);
+        if (dst_desc_len > (int)sizeof(dst_desc)) {
+            pr2serr("destination descriptor too large (%d bytes)\n", res);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto fini;
+        }
+    } else {
+        ret = SG_LIB_CAT_INVALID_OP;
+        goto fini;
+    }
+
+    if (dd_count < 0) {
+        pr2serr("Couldn't calculate count, please give one\n");
+        return SG_LIB_CAT_OTHER;
+    }
+
+    if (dd_count < (ixcf.min_bytes / (uint32_t)ixcf.sect_sz)) {
+        pr2serr("not enough data to read (min %" PRIu32 " bytes)\n",
+                oxcf.min_bytes);
+        return SG_LIB_CAT_OTHER;
+    }
+    if (dd_count < (oxcf.min_bytes / (uint32_t)oxcf.sect_sz)) {
+        pr2serr("not enough data to write (min %" PRIu32 " bytes)\n",
+                oxcf.min_bytes);
+        return SG_LIB_CAT_OTHER;
+    }
+
+    if (bpt_given) {
+        if (xcopy_flag_dc) {
+            if ((uint32_t)(bpt * oxcf.sect_sz) > oxcf.max_bytes) {
+                pr2serr("bpt too large (max %" PRIu32 " blocks)\n",
+                        oxcf.max_bytes / (uint32_t)oxcf.sect_sz);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else {
+            if ((uint32_t)(bpt * ixcf.sect_sz) > ixcf.max_bytes) {
+                pr2serr("bpt too large (max %" PRIu32 " blocks)\n",
+                        ixcf.max_bytes / (uint32_t)ixcf.sect_sz);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+    } else {
+        uint32_t r;
+
+        if (xcopy_flag_dc)
+            r = oxcf.max_bytes / (uint32_t)oxcf.sect_sz;
+        else
+            r = ixcf.max_bytes / (uint32_t)ixcf.sect_sz;
+        bpt = (r > MAX_BLOCKS_PER_TRANSFER) ? MAX_BLOCKS_PER_TRANSFER : r;
+    }
+
+    seg_desc_type = seg_desc_from_dd_type(simplified_ft(&ixcf), 0,
+                                          simplified_ft(&oxcf), 0);
+
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+        start_tm_valid = true;
+    }
+
+    if (verbose)
+        pr2serr("Start of loop, count=%" PRId64 ", bpt=%d, lba_in=%" PRId64
+                ", lba_out=%" PRId64 "\n", dd_count, bpt, skip, seek);
+
+    xcopy_fd = (on_src) ? infd : outfd;
+
+    while (dd_count > 0) {
+        if (dd_count > bpt)
+            blocks = bpt;
+        else
+            blocks = dd_count;
+        res = scsi_extended_copy(xcopy_fd, list_id, src_desc, src_desc_len,
+                                 dst_desc, dst_desc_len, seg_desc_type,
+                                 blocks, skip, seek);
+        if (res != 0)
+            break;
+        in_full += blocks;
+        skip += blocks;
+        seek += blocks;
+        dd_count -= blocks;
+        num_xcopy++;
+    }
+
+    if (do_time)
+        calc_duration_throughput(0);
+    if (res)
+        pr2serr("sg_xcopy: failed with error %d (%" PRId64 " blocks left)\n",
+                res, dd_count);
+    else
+        pr2serr("sg_xcopy: %" PRId64 " blocks, %d command%s\n", in_full,
+                num_xcopy, ((num_xcopy > 1) ? "s" : ""));
+    ret = res;
+
+fini:
+    /* file handles not explicitly closed; let process cleanup do that */
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_xcopy failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_z_act_query.c b/src/sg_z_act_query.c
new file mode 100644
index 0000000..372c27c
--- /dev/null
+++ b/src/sg_z_act_query.c
@@ -0,0 +1,639 @@
+/*
+ * Copyright (c) 2014-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ *
+ * This program issues either a SCSI ZONE ACTIVATE command or a ZONE QUERY
+ * command to the given SCSI device. Based on zbc2r12.pdf .
+ */
+
+static const char * version_str = "1.04 20220729";
+
+#define SG_ZBC_IN_CMDLEN 16
+#define Z_ACTIVATE_SA 0x8
+#define Z_QUERY_SA 0x9
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT 60       /* 60 seconds */
+#define DEF_ALLOC_LEN 8192
+#define Z_ACT_DESC_LEN 32
+#define MAX_ACT_QUERY_BUFF_LEN (16 * 1024 * 1024)
+
+struct opts_t {
+    bool do_all;
+    bool do_activate;
+    bool do_force;
+    bool do_query;
+    bool do_raw;
+    bool maxlen_given;
+    uint8_t other_zdid;
+    uint16_t max_alloc;
+    uint16_t num_zones;
+    int hex_count;
+    int vb;
+    uint64_t st_lba;    /* Zone ID */
+    const char * device_name;
+    const char * inhex_fn;
+};
+
+static struct option long_options[] = {
+        {"activate", no_argument, 0, 'A'},
+        {"all", no_argument, 0, 'a'},
+        {"force", no_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"hex", no_argument, 0, 'H'},
+        {"in", required_argument, 0, 'i'},      /* silent, same as --inhex= */
+        {"inhex", required_argument, 0, 'i'},
+        {"maxlen", required_argument, 0, 'm'},
+        {"num", required_argument, 0, 'n'},
+        {"other", required_argument, 0, 'o'},
+        {"query", no_argument, 0, 'q'},
+        {"raw", no_argument, 0, 'r'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"zone", required_argument, 0, 'z'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_z_act_query [--activate] [--all] [--force] [--help] "
+            "[--hex]\n"
+            "                      [--inhex=FN] [--maxlen=LEN] [--num=ZS] "
+            "[--other=ZDID]\n"
+            "                      [--query] [--raw] [--verbose] "
+            "[--version]\n"
+            "                      [--zone=ID] DEVICE\n");
+    pr2serr("  where:\n"
+            "    --activate|-A      do ZONE ACTIVATE command (def: ZONE "
+            "QUERY)\n"
+            "    --all|-a           sets the ALL flag in the cdb\n"
+            "    --force|-f         bypass some sanity checks\n"
+            "    --help|-h          print out usage message\n"
+            "    --hex|-H           print out response in hexadecimal\n"
+            "    --inhex=FN|-i FN    decode contents of FN, ignore DEVICE\n"
+            "    --maxlen=LEN|-m LEN    LEN place in cdb's allocation "
+            "length field\n"
+            "                           (def: 8192 (bytes))\n"
+            "    --num=ZS|-n ZS     ZS is the number of zones and is placed "
+            "in the cdb;\n"
+            "                       default value is 1, ignored if --all "
+            "given\n"
+            "    --other=ZDID|-o ZDID    ZDID is placed in Other zone domain "
+            "ID field\n"
+            "    --query|-q         do ZONE QUERY command (def: ZONE "
+            "QUERY)\n"
+            "    --raw|-r           output response in binary, or if "
+            "--inhex=FN is\n"
+            "                       given, then FN's contents are binary\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n"
+            "    --zone=ID|-z ID    ID is the starting LBA of the zone "
+            "(def: 0)\n\n"
+            "Performs either a SCSI ZONE ACTIVATE command, or a ZONE QUERY "
+            "command.\nArguments to options are decimal by default, for hex "
+            "use a leading '0x'\nor a trailing 'h'. The default action is to "
+            "send a ZONE QUERY command.\n");
+}
+
+/* Invokes a ZBC IN command (with either a ZONE ACTIVATE or a ZONE QUERY
+ * service action).  Return of 0 -> success, various SG_LIB_CAT_* positive
+ * values or -1 -> other errors */
+static int
+sg_ll_zone_act_query(int sg_fd, const struct opts_t * op, void * resp,
+                     int * residp)
+{
+    uint8_t sa = op->do_activate ? Z_ACTIVATE_SA : Z_QUERY_SA;
+    int ret, res, sense_cat;
+    struct sg_pt_base * ptvp;
+    uint8_t zi_cdb[SG_ZBC_IN_CMDLEN] =
+          {SG_ZBC_IN, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    char b[64];
+
+    zi_cdb[1] = 0x1f & sa;
+    if (op->do_all)
+        zi_cdb[1] |= 0x80;
+
+    sg_put_unaligned_be64(op->st_lba, zi_cdb + 2);
+    sg_put_unaligned_be16(op->num_zones, zi_cdb + 10);
+    sg_put_unaligned_be16(op->max_alloc, zi_cdb + 12);
+    zi_cdb[14] = op->other_zdid;
+    sg_get_opcode_sa_name(zi_cdb[0], sa, -1, sizeof(b), b);
+    if (op->vb) {
+        char d[128];
+
+        pr2serr("    %s cdb: %s\n", b,
+                sg_get_command_str(zi_cdb, SG_ZBC_IN_CMDLEN, false,
+                                   sizeof(d), d));
+    }
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", b);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, zi_cdb, sizeof(zi_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, op->max_alloc);
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, op->vb);
+    ret = sg_cmds_process_resp(ptvp, b, res, true /* noisy */,
+                               op->vb, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    if (residp)
+        *residp = get_scsi_pt_resid(ptvp);
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+static const char *
+zone_condition_str(int zc, char * b, int blen, int vb)
+{
+    const char * cp;
+
+    if (NULL == b)
+        return "zone_condition_str: NULL ptr)";
+    switch (zc) {
+    case 0:
+        cp = "Not write pointer";
+        break;
+    case 1:
+        cp = "Empty";
+        break;
+    case 2:
+        cp = "Implicitly opened";
+        break;
+    case 3:
+        cp = "Explicitly opened";
+        break;
+    case 4:
+        cp = "Closed";
+        break;
+    case 5:
+        cp = "Inactive";
+        break;
+    case 0xd:
+        cp = "Read only";
+        break;
+    case 0xe:
+        cp = "Full";
+        break;
+    case 0xf:
+        cp = "Offline";
+        break;
+    default:
+        cp = NULL;
+        break;
+    }
+    if (cp) {
+        if (vb)
+            snprintf(b, blen, "%s [0x%x]", cp, zc);
+        else
+            snprintf(b, blen, "%s", cp);
+    } else
+        snprintf(b, blen, "Reserved [0x%x]", zc);
+    return b;
+}
+
+/* The allocation length field in each cdb cannot be less than 64 but the
+ * transport could still trim the response. */
+static int
+decode_z_act_query(const uint8_t * ziBuff, int act_len, uint32_t zar_len,
+                   const struct opts_t * op)
+{
+    uint8_t zt;
+    int k, zc, num_desc;
+    const uint8_t * bp;
+    char b[80];
+
+    if ((uint32_t)act_len < zar_len) {
+        num_desc = (act_len >= 64) ? ((act_len - 64) / Z_ACT_DESC_LEN) : 0;
+        if (act_len == op->max_alloc) {
+            if (op->maxlen_given)
+                pr2serr("response length [%u bytes] may be constrained by "
+                        "given --maxlen value, try increasing\n", zar_len);
+            else
+                pr2serr("perhaps --maxlen=%u needs to be used\n", zar_len);
+        } else if (op->inhex_fn)
+            pr2serr("perhaps %s has been truncated\n", op->inhex_fn);
+    } else
+        num_desc = (zar_len - 64) / Z_ACT_DESC_LEN;
+    if (act_len <= 8)
+        return 0;
+    if (0x80 & ziBuff[8]) {
+        printf("  Nz_valid=1\n");
+        if (act_len > 19)
+            printf("    Number of zones: %u\n",
+                   sg_get_unaligned_be32(ziBuff + 16));
+    } else
+        printf("  Nz_valid=0\n");
+    if (0x40 & ziBuff[8]) {
+        printf("  Ziwup_valid=1\n");
+        if (act_len > 31)
+            printf("    Zone ID with unmet prerequisite: 0x%" PRIx64 "\n",
+                   sg_get_unaligned_be64(ziBuff + 24));
+    } else
+        printf("  Ziwup_valid=0\n");
+    printf("  Activated=%d\n", (0x1 & ziBuff[8]));
+    if (act_len <= 9)
+        return 0;
+    printf("  Unmet prerequisites:\n");
+    if (0 == ziBuff[9])
+        printf("    none\n");
+    else {
+        if (0x40 & ziBuff[9])
+             printf("    security\n");
+        if (0x20 & ziBuff[9])
+             printf("    mult domn\n");
+        if (0x10 & ziBuff[9])
+             printf("    rlm rstct\n");
+        if (0x8 & ziBuff[9])
+             printf("    mult ztyp\n");
+        if (0x4 & ziBuff[9])
+             printf("    rlm align\n");
+        if (0x2 & ziBuff[9])
+             printf("    not empty\n");
+        if (0x1 & ziBuff[9])
+             printf("    not inact\n");
+    }
+    if (act_len <= 10)
+        return 0;
+    printf("  Other zone domain ID: %u\n", ziBuff[10]);
+    if (act_len <= 11)
+        return 0;
+    printf("  All: %d\n", (0x1 & ziBuff[11]));
+
+    if (((uint32_t)act_len < zar_len) &&
+        ((num_desc * Z_ACT_DESC_LEN) + 64 > act_len)) {
+        pr2serr("Skip due to truncated response, try using --num= to a "
+                "value less than %d\n", num_desc);
+        return SG_LIB_CAT_MALFORMED;
+    }
+    for (k = 0, bp = ziBuff + 64; k < num_desc; ++k, bp += Z_ACT_DESC_LEN) {
+        printf("  Zone activation descriptor: %d\n", k);
+        if (op->hex_count) {
+            hex2stdout(bp, Z_ACT_DESC_LEN, -1);
+            continue;
+        }
+        zt = bp[0] & 0xf;
+        zc = (bp[1] >> 4) & 0xf;
+        printf("    Zone type: %s\n", sg_get_zone_type_str(zt, sizeof(b),
+               b));
+        printf("    Zone condition: %s\n", zone_condition_str(zc, b,
+               sizeof(b), op->vb));
+        printf("    Zone domain ID: %u\n", bp[2]);
+        printf("    Zone range size: %" PRIu64 "\n",
+               sg_get_unaligned_be64(bp + 8));
+        printf("    Starting zone locator: 0x%" PRIx64 "\n",
+               sg_get_unaligned_be64(bp + 16));
+    }
+    return 0;
+}
+
+static void
+dStrRaw(const uint8_t * str, int len)
+{
+    int k;
+
+    for (k = 0; k < len; ++k)
+        printf("%c", str[k]);
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool no_final_msg = false;
+    bool version_given = false;
+    int res, c, n, in_len, rlen, act_len;
+    int sg_fd = -1;
+    int resid = 0;
+    int verbose = 0;
+    int ret = 0;
+    uint32_t zar_len, zarr_len;
+    int64_t ll;
+    uint8_t * ziBuff = NULL;
+    uint8_t * free_zibp = NULL;
+    const char * sa_name;
+    char b[80];
+    struct opts_t opts SG_C_CPP_ZERO_INIT;
+    struct opts_t * op = &opts;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "aAfhHi:m:n:o:qrvVz:", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+            op->do_all = true;
+            break;
+        case 'A':
+            op->do_activate = true;
+            break;
+        case 'f':
+            op->do_force = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++op->hex_count;
+            break;
+        case 'i':
+            op->inhex_fn = optarg;
+            break;
+        case 'm':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 0xffff)) {
+                pr2serr("--maxlen= expects an argument between 0 and 0xffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->maxlen_given = true;
+            op->max_alloc = (uint16_t)n;
+            break;
+        case 'n':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 0xffff)) {
+                pr2serr("--num=ZS expects an argument between 0 and 0xffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->num_zones = (uint16_t)n;
+            break;
+        case 'o':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 0xff)) {
+                pr2serr("--other=ZDID expects an argument between 0 and 0xff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->other_zdid = (uint8_t)n;
+            break;
+        case 'q':
+            op->do_query = true;
+            break;
+        case 'r':
+            op->do_raw = true;
+            break;
+        case 'v':
+            ++op->vb;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        case 'z':
+            if ((2 == strlen(optarg)) && (0 == memcmp("-1", optarg, 2))) {
+                op->st_lba = UINT64_MAX;
+                break;
+            }
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--zone=ID'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            op->st_lba = (uint64_t)ll;  /* Zone ID is starting LBA */
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == op->device_name) {
+            op->device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if ((! op->do_all) && (0 == op->num_zones))
+        op->num_zones = 1;
+    if (op->do_activate && op->do_query){
+        pr2serr("only one of these options: --activate and --query may be "
+                "given\n\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    sa_name = op->do_activate ? "Zone activate" : "Zone query";
+    if (op->device_name && op->inhex_fn) {
+        pr2serr("ignoring DEVICE, best to give DEVICE or --inhex=FN, but "
+                "not both\n");
+        op->device_name = NULL;
+    }
+    if (op->max_alloc < 4) {
+        if (op->max_alloc > 0)
+            pr2serr("Won't accept --maxlen= of 1, 2 or 3, using %d "
+                    "instead\n", DEF_ALLOC_LEN);
+        op->max_alloc = DEF_ALLOC_LEN;
+    }
+    ziBuff = (uint8_t *)sg_memalign(op->max_alloc, 0, &free_zibp, op->vb > 3);
+    if (NULL == ziBuff) {
+        pr2serr("unable to sg_memalign %d bytes\n", op->max_alloc);
+        return sg_convert_errno(ENOMEM);
+    }
+
+    if (NULL == op->device_name) {
+        if (op->inhex_fn) {
+            if ((ret = sg_f2hex_arr(op->inhex_fn, op->do_raw, false, ziBuff,
+                                    &in_len, op->max_alloc))) {
+                if (SG_LIB_LBA_OUT_OF_RANGE == ret) {
+                    no_final_msg = true;
+                    pr2serr("... decode what we have, --maxlen=%d needs to "
+                            "be increased\n", op->max_alloc);
+                } else
+                    goto the_end;
+            }
+            if (verbose > 2)
+                pr2serr("Read %d [0x%x] bytes of user supplied data\n",
+                        in_len, in_len);
+            if (op->do_raw)
+                op->do_raw = false;    /* can interfere on decode */
+            if (in_len < 4) {
+                pr2serr("--inhex=%s only decoded %d bytes (needs 4 at "
+                        "least)\n", op->inhex_fn, in_len);
+                ret = SG_LIB_SYNTAX_ERROR;
+                goto the_end;
+            }
+            res = 0;
+            goto start_response;
+        } else {
+            pr2serr("missing device name!\n\n");
+            usage();
+            ret = SG_LIB_FILE_ERROR;
+            no_final_msg = true;
+            goto the_end;
+        }
+    } else
+        in_len = 0;
+
+    if (op->do_raw) {
+        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
+            perror("sg_set_binary_mode");
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    sg_fd = sg_cmds_open_device(op->device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        int err = -sg_fd;
+        if (verbose)
+            pr2serr("open error: %s: %s\n", op->device_name,
+                    safe_strerror(err));
+        ret = sg_convert_errno(err);
+        goto the_end;
+    }
+
+    res = sg_ll_zone_act_query(sg_fd, op, ziBuff, &resid);
+    ret = res;
+    if (res) {
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("%s command not supported\n", sa_name);
+        else {
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("%s command: %s\n", sa_name, b);
+        }
+    }
+
+start_response:
+    if (0 == res) {
+        if ((resid < 0) || (resid > op->max_alloc)) {
+            pr2serr("Unexpected resid=%d\n", resid);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto the_end;
+        }
+        rlen = op->inhex_fn ? in_len : (op->max_alloc - resid);
+        if (rlen < 4) {
+            pr2serr("Decoded response length (%d) too short\n", rlen);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto the_end;
+        }
+        zar_len = sg_get_unaligned_be32(ziBuff + 0) + 64;
+        zarr_len = sg_get_unaligned_be32(ziBuff + 4) + 64;
+        if ((zar_len > MAX_ACT_QUERY_BUFF_LEN) ||
+            (zarr_len > MAX_ACT_QUERY_BUFF_LEN) || (zarr_len > zar_len)) {
+            if (! op->do_force) {
+                pr2serr("zar or zarr length [%u/%u bytes] seems wild, use "
+                        "--force override\n", zar_len, zarr_len);
+                return SG_LIB_CAT_MALFORMED;
+            }
+        }
+        if (zarr_len > (uint32_t)rlen) {
+            pr2serr("zarr response length is %u bytes, but system "
+                    "reports %d bytes received??\n", zarr_len, rlen);
+            if (op->do_force)
+                act_len = rlen;
+            else {
+                pr2serr("Exiting, use --force to override\n");
+                ret = SG_LIB_CAT_MALFORMED;
+                goto the_end;
+            }
+        } else
+            act_len = zarr_len;
+        if (op->do_raw) {
+            dStrRaw(ziBuff, act_len);
+            goto the_end;
+        }
+        if (op->hex_count && (2 != op->hex_count)) {
+            hex2stdout(ziBuff, act_len, ((1 == op->hex_count) ? 1 : -1));
+            goto the_end;
+        }
+        printf("%s response:\n", sa_name);
+        if (act_len < 64) {
+            pr2serr("Zone length [%d] too short (perhaps after truncation\n)",
+                    act_len);
+            ret = SG_LIB_CAT_MALFORMED;
+            goto the_end;
+        }
+        ret = decode_z_act_query(ziBuff, act_len, zar_len, op);
+    } else if (SG_LIB_CAT_INVALID_OP == res)
+        pr2serr("%s command not supported\n", sa_name);
+    else {
+        sg_get_category_sense_str(res, sizeof(b), b, op->vb);
+        pr2serr("%s command: %s\n", sa_name, b);
+    }
+
+the_end:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (free_zibp)
+        free(free_zibp);
+    if ((0 == verbose) && (! no_final_msg)) {
+        if (! sg_if_can2stderr("sg_z_act_query failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' "
+                    "or '-vv' for more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sg_zone.c b/src/sg_zone.c
new file mode 100644
index 0000000..6da6d0a
--- /dev/null
+++ b/src/sg_zone.c
@@ -0,0 +1,397 @@
+/*
+ * Copyright (c) 2014-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_lib_data.h"
+#include "sg_pt.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *
+ * This program issues one of the following SCSI commands:
+ *   - CLOSE ZONE
+ *   - FINISH ZONE
+ *   - OPEN ZONE
+ *   - REMOVE ELEMENT AND MODIFY ZONES
+ *   - SEQUENTIALIZE ZONE
+ */
+
+static const char * version_str = "1.18 20220609";
+
+#define SG_ZONING_OUT_CMDLEN 16
+#define CLOSE_ZONE_SA 0x1
+#define FINISH_ZONE_SA 0x2
+#define OPEN_ZONE_SA 0x3
+#define SEQUENTIALIZE_ZONE_SA 0x10
+#define REM_ELEM_MOD_ZONES_SA 0x1a      /* uses SERVICE ACTION IN(16) */
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define DEF_PT_TIMEOUT  60      /* 60 seconds */
+
+
+static struct option long_options[] = {
+        {"all", no_argument, 0, 'a'},
+        {"close", no_argument, 0, 'c'},
+        {"count", required_argument, 0, 'C'},
+        {"element", required_argument, 0, 'e'},
+        {"finish", no_argument, 0, 'f'},
+        {"help", no_argument, 0, 'h'},
+        {"open", no_argument, 0, 'o'},
+        {"quick", no_argument, 0, 'q'},
+        {"remove", no_argument, 0, 'r'},
+        {"reset-all", no_argument, 0, 'R'},     /* same as --all */
+        {"reset_all", no_argument, 0, 'R'},
+        {"sequentialize", no_argument, 0, 'S'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"zone", required_argument, 0, 'z'},
+        {0, 0, 0, 0},
+};
+
+/* Indexed by service action of opcode 0x94 (Zone out) unless noted */
+static const char * sa_name_arr[] = {
+    "no SA=0",                  /* 0x0 */
+    "Close zone",
+    "Finish zone",
+    "Open zone",
+    "-", "-", "-", "-",
+    "-",
+    "-", "-", "-", "-",
+    "-",
+    "-",
+    "-",
+    "Sequentialize zone",       /* 0x10 */
+    "-", "-", "-", "-",
+    "-", "-", "-", "-",
+    "-",
+    "Remove element and modify zones", /* service action in(16), 0x1a */
+};
+
+
+static void
+usage()
+{
+    pr2serr("Usage: "
+            "sg_zone  [--all] [--close] [--count=ZC] [--element=EID] "
+            "[--finish]\n"
+            "                [--help] [--open] [--quick] [--remove] "
+            "[--sequentialize]\n"
+            "                [--verbose] [--version] [--zone=ID] DEVICE\n");
+    pr2serr("  where:\n"
+            "    --all|-a           sets the ALL flag in the cdb\n"
+            "    --close|-c         issue CLOSE ZONE command\n"
+            "    --count=ZC|-C ZC    set zone count field (def: 0)\n"
+            "    --element=EID|-e EID    EID is the element identifier to "
+            "remove;\n"
+            "                            default is 0 which is an invalid "
+            "EID\n"
+            "    --finish|-f        issue FINISH ZONE command\n"
+            "    --help|-h          print out usage message\n"
+            "    --open|-o          issue OPEN ZONE command\n"
+            "    --quick|-q         bypass 15 second warn and wait "
+            "(for --remove)\n"
+            "    --remove|-r        issue REMOVE ELEMENT AND MODIFY ZONES "
+            "command\n"
+            "    --sequentialize|-S    issue SEQUENTIALIZE ZONE command\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n"
+            "    --zone=ID|-z ID    ID is the starting LBA of the zone "
+            "(def: 0)\n\n"
+            "Performs a SCSI OPEN ZONE, CLOSE ZONE, FINISH ZONE, "
+            "REMOVE ELEMENT AND\nMODIFY ZONES or SEQUENTIALIZE ZONE "
+            "command. Either --close, --finish,\n--open, --remove or "
+            "--sequentialize option needs to be given.\n");
+}
+
+/* Invokes the zone out command indicated by 'sa' (ZBC).  Return of 0
+ * -> success, various SG_LIB_CAT_* positive values or -1 -> other errors */
+static int
+sg_ll_zone_out(int sg_fd, int sa, uint64_t zid, uint16_t zc, bool all,
+               bool noisy, int verbose)
+{
+    int ret, res, sense_cat;
+    struct sg_pt_base * ptvp;
+    uint8_t zo_cdb[SG_ZONING_OUT_CMDLEN] =
+          {SG_ZONING_OUT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    char b[64];
+
+    zo_cdb[1] = 0x1f & sa;
+    if (REM_ELEM_MOD_ZONES_SA == sa) {  /* zid carries element identifier */
+        zo_cdb[0] = SG_SERVICE_ACTION_IN_16;    /* N.B. changing opcode */
+        sg_put_unaligned_be32((uint32_t)zid, zo_cdb + 10); /* element id */
+    } else {
+        sg_put_unaligned_be64(zid, zo_cdb + 2);
+        sg_put_unaligned_be16(zc, zo_cdb + 12);
+        if (all)
+            zo_cdb[14] = 0x1;
+    }
+    sg_get_opcode_sa_name(zo_cdb[0], sa, -1, sizeof(b), b);
+    if (verbose) {
+        char d[128];
+
+        pr2serr("    %s cdb: %s\n", b,
+                sg_get_command_str(zo_cdb, SG_ZONING_OUT_CMDLEN,
+                                   false, sizeof(d), d));
+    }
+
+    ptvp = construct_scsi_pt_obj();
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", b);
+        return -1;
+    }
+    set_scsi_pt_cdb(ptvp, zo_cdb, sizeof(zo_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, verbose);
+    ret = sg_cmds_process_resp(ptvp, b, res, noisy,
+                               verbose, &sense_cat);
+    if (-1 == ret) {
+        if (get_scsi_pt_transport_err(ptvp))
+            ret = SG_LIB_TRANSPORT_ERROR;
+        else
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptvp));
+    } else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else
+        ret = 0;
+    destruct_scsi_pt_obj(ptvp);
+    return ret;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool all = false;
+    bool close = false;
+    bool finish = false;
+    bool open = false;
+    bool quick = false;
+    bool reamz = false;
+    bool element_id_given = false;
+    bool sequentialize = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, c, n;
+    int sg_fd = -1;
+    int verbose = 0;
+    int ret = 0;
+    int sa = 0;
+    uint16_t zc = 0;
+    uint64_t zid = 0;
+    int64_t ll;
+    const char * device_name = NULL;
+    const char * sa_name;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "acC:e:fhoqrRSvVz:", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'a':
+        case 'R':
+            all = true;
+            break;
+        case 'c':
+            close = true;
+            sa = CLOSE_ZONE_SA;
+            break;
+        case 'C':
+            n = sg_get_num(optarg);
+            if ((n < 0) || (n > 0xffff)) {
+                pr2serr("--count= expects an argument between 0 and 0xffff "
+                        "inclusive\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            zc = (uint16_t)n;
+            break;
+        case 'e':
+            ll = sg_get_llnum(optarg);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("bad argument to '--element=EID'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (0 == ll)
+                pr2serr("Warning: 0 is an invalid element identifier\n");
+            zid = (uint64_t)ll;    /* putting element_id in zid */
+            element_id_given = true;
+            break;
+        case 'f':
+            finish = true;
+            sa = FINISH_ZONE_SA;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'o':
+            open = true;
+            sa = OPEN_ZONE_SA;
+            break;
+        case 'q':
+            quick = true;
+            break;
+        case 'r':
+            reamz = true;
+            sa = REM_ELEM_MOD_ZONES_SA;
+            break;
+        case 'S':
+            sequentialize = true;
+            sa = SEQUENTIALIZE_ZONE_SA;
+            break;
+        case 'v':
+            verbose_given = true;
+            ++verbose;
+            break;
+        case 'V':
+            version_given = true;
+            break;
+        case 'z':
+            ll = sg_get_llnum(optarg);
+            if (-1 == ll) {
+                pr2serr("bad argument to '--zone=ID'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            zid = (uint64_t)ll;
+            break;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == device_name) {
+            device_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                pr2serr("Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("version: %s\n", version_str);
+        return 0;
+    }
+
+    if (1 != ((int)close + (int)finish + (int)open + (int)sequentialize +
+              (int)reamz)) {
+        pr2serr("One, and only one, of these options needs to be given:\n"
+                "   --close, --finish, --open, --remove or --sequentialize "
+                "\n\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    if (element_id_given && (! reamz)) {
+        pr2serr("The --element=EID option should only be used with the "
+                "--remove option\n\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    sa_name = sa_name_arr[sa];
+
+    if (NULL == device_name) {
+        pr2serr("missing device name!\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, verbose);
+    if (sg_fd < 0) {
+        int err = -sg_fd;
+        if (verbose)
+            pr2serr("open error: %s: %s\n", device_name,
+                    safe_strerror(err));
+        ret = sg_convert_errno(err);
+        goto fini;
+    }
+    if (reamz && (! quick))
+        sg_warn_and_wait(sa_name_arr[REM_ELEM_MOD_ZONES_SA], device_name,
+                         false);
+
+    res = sg_ll_zone_out(sg_fd, sa, zid, zc, all, true, verbose);
+    ret = res;
+    if (res) {
+        if (SG_LIB_CAT_INVALID_OP == res)
+            pr2serr("%s command not supported\n", sa_name);
+        else {
+            char b[80];
+
+            sg_get_category_sense_str(res, sizeof(b), b, verbose);
+            pr2serr("%s command: %s\n", sa_name, b);
+        }
+    }
+
+fini:
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                ret = sg_convert_errno(-res);
+        }
+    }
+    if (0 == verbose) {
+        if (! sg_if_can2stderr("sg_zone failed: ", ret))
+            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
+                    "more information\n");
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sginfo.c b/src/sginfo.c
new file mode 100644
index 0000000..0937e68
--- /dev/null
+++ b/src/sginfo.c
@@ -0,0 +1,3999 @@
+/* * This program reads various mode pages and bits of other
+ * information from a scsi device and interprets the raw data for you
+ * with a report written to stdout.  Usage:
+ *
+ * ./sginfo [options] /dev/sg2 [replace parameters]
+ *
+ * Options are:
+ * -6    do 6 byte mode sense + select (default: 10 byte)
+ * -a    display all mode pages reported by the device: equivalent to '-t 63'.
+ * -A    display all mode pages and subpages reported by the device: equivalent
+ *       to '-t 63,255'.
+ * -c    access Cache control page.
+ * -C    access Control Page.
+ * -d    display defect lists (default format: index).
+ * -D    access disconnect-reconnect page.
+ * -e    access Read-Write error recovery page.
+ * -E    access Control Extension page.
+ * -f    access Format Device Page.
+ * -Farg defect list format (-Flogical, -flba64, -Fphysical, -Findex, -Fhead)
+ * -g    access rigid disk geometry page.
+ * -G    display only "grown" defect list (default format: index)
+ * -i    display information from Inquiry command.
+ * -I    access Informational Exceptions page.
+ * -l    list known scsi devices on the system [deprecated]
+ * -n    access notch parameters page.
+ * -N    Negate (stop) storing to saved page (active with -R)
+ * -P    access Power Condition Page.
+ * -r    list known raw scsi devices on the system
+ * -s    display serial number (from INQUIRY VPD page)
+ * -t <n[,spn]> access page number <n> [and subpage <spn>], try to decode
+ * -u <n[,spn]> access page number <n> [and subpage <spn>], output in hex
+ * -v    show this program's version number
+ * -V    access Verify Error Recovery Page.
+ * -T    trace commands (for debugging, double for more debug)
+ * -z    do a single fetch for mode pages (rather than double fetch)
+ *
+ * Only one of the following three options can be specified.
+ * None of these three implies the current values are returned.
+ * -m    Display modifiable fields instead of current values
+ * -M    Display manufacturer defaults instead of current values
+ * -S    Display saved defaults instead of current values
+ *
+ * -X    Display output values in a list.
+ * -R    Replace parameters - best used with -X
+ *
+ * Eric Youngdale - 11/1/93.  Version 1.0.
+ *
+ * Version 1.1: Ability to change parameters on cache page, support for
+ *  X front end.
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2, or (at your option)
+ *   any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Michael Weller (eowmob at exp-math dot uni-essen dot de)
+ *      11/23/94 massive extensions from 1.4a
+ *      08/23/97 fix problems with defect lists
+ *
+ * Douglas Gilbert (dgilbert at interlog dot com)
+ *      990628   port to sg .... (version 1.81)
+ *               up 4KB limit on defect list to 32KB
+ *               'sginfo -l' also shows sg devices and mapping to other
+ *                    scsi devices
+ *               'sginfo' commands can take either an sd, sr (scd), st
+ *                    or an sg device (all non-sg devices converted to a
+ *                    sg device)
+ *
+ *      001208   Add Kurt Garloff's "-uno" flag for displaying info
+ *               from a page number. [version 1.90]
+ *
+ * Kurt Garloff
+ *    20000715  allow displaying and modification of vendor specific pages
+ *                      (unformatted - @ hexdatafield)
+ *              accept vendor lengths for those pages
+ *              enabled page saving
+ *              cleaned parameter parsing a bit (it's still a terrible mess!)
+ *              Use sr (instead of scd) and sg%d (instead of sga,b,...) in -l
+ *                      and support much more devs in -l (incl. nosst)
+ *              Fix segfault in defect list (len=0xffff) and adapt formatting
+ *                      to large disks. Support up to 256kB defect lists with
+ *                      0xB7 (12byte) command if necessary and fallback to 0x37
+ *                      (10byte) in case of failure. Report truncation.
+ *              sizeof(buffer) (which is sizeof(char*) == 4 or 32 bit archs)
+ *                      was used incorrectly all over the place. Fixed.
+ *                                      [version 1.95]
+ * Douglas Gilbert (dgilbert at interlog dot com)
+ *    20020113  snprintf() type cleanup [version 1.96]
+ *    20021211  correct sginfo MODE_SELECT, protect against block devices
+ *              that answer sg's ioctls. [version 1.97]
+ *    20021228  scan for some "scd<n>" as well as "sr<n>" device names [1.98]
+ *    20021020  Update control page [1.99]
+ *
+ * Thomas Steudten (thomas at steudten dot com)
+ *    20040521  add -Fhead feature [version 2.04]
+ *
+ * Tim Hunt (tim at timhunt dot net)
+ *    20050427  increase number of mapped SCSI disks devices
+ *
+ * Dave Johnson (djj at ccv dot brown dot edu)
+ *    20051218  improve disk defect list handling
+ */
+
+
+/*
+ * N.B. This utility is in maintenance mode only. This means that serious
+ * bugs will be fixed but no new features or mode page changes will be
+ * added. Please use the sdparm utility.     D. Gilbert 20090316
+ */
+
+#define _XOPEN_SOURCE 500
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+static const char * version_str = "2.45 [20220425]";
+
+#include <stdio.h>
+#include <string.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <ctype.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <dirent.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_io_linux.h"
+
+
+static int glob_fd;
+static char *device_name;
+
+#define MAX_SG_DEVS 8192
+#define MAX_RESP6_SIZE 252
+#define MAX_RESP10_SIZE (4*1024)
+#define MAX_BUFFER_SIZE MAX_RESP10_SIZE
+
+#define INQUIRY_RESP_INITIAL_LEN 36
+#define MAX_INQFIELD_LEN 17
+
+#define MAX_HEADS 127
+#define HEAD_SORT_TOKEN 0x55
+
+#define SIZEOF_BUFFER (16*1024)
+#define SIZEOF_BUFFER1 (16*1024)
+static uint8_t cbuffer[SIZEOF_BUFFER];
+static uint8_t cbuffer1[SIZEOF_BUFFER1];
+static uint8_t cbuffer2[SIZEOF_BUFFER1];
+
+static char defect = 0;
+static char defectformat = 0x4;
+static char grown_defect = 0;
+static char negate_sp_bit = 0;
+static char replace = 0;
+static char serial_number = 0;
+static char x_interface = 0;
+static char single_fetch = 0;
+
+static char mode6byte = 0;      /* defaults to 10 byte mode sense + select */
+static char trace_cmd = 0;
+
+struct mpage_info {
+    int page;
+    int subpage;
+    int page_control;
+    int peri_type;
+    int inq_byte6;      /* EncServ and MChngr bits of interest */
+    int resp_len;
+};
+
+/* declarations of functions decoding known mode pages */
+static int common_disconnect_reconnect(struct mpage_info * mpi,
+                                       const char * prefix);
+static int common_control(struct mpage_info * mpi, const char * prefix);
+static int common_control_extension(struct mpage_info * mpi,
+                                    const char * prefix);
+static int common_proto_spec_lu(struct mpage_info * mpi, const char * prefix);
+static int common_proto_spec_port(struct mpage_info * mpi,
+                                  const char * prefix);
+static int common_proto_spec_port_sp1(struct mpage_info * mpi,
+                                      const char * prefix);
+static int common_proto_spec_port_sp2(struct mpage_info * mpi,
+                                      const char * prefix);
+static int common_power_condition(struct mpage_info * mpi,
+                                  const char * prefix);
+static int common_informational(struct mpage_info * mpi, const char * prefix);
+static int disk_error_recovery(struct mpage_info * mpi, const char * prefix);
+static int disk_format(struct mpage_info * mpi, const char * prefix);
+static int disk_verify_error_recovery(struct mpage_info * mpi,
+                                      const char * prefix);
+static int disk_geometry(struct mpage_info * mpi, const char * prefix);
+static int disk_notch_parameters(struct mpage_info * mpi, const char * prefix);
+static int disk_cache(struct mpage_info * mpi, const char * prefix);
+static int disk_xor_control(struct mpage_info * mpi, const char * prefix);
+static int disk_background(struct mpage_info * mpi, const char * prefix);
+static int optical_memory(struct mpage_info * mpi, const char * prefix);
+static int cdvd_error_recovery(struct mpage_info * mpi, const char * prefix);
+static int cdvd_mrw(struct mpage_info * mpi, const char * prefix);
+static int cdvd_write_param(struct mpage_info * mpi, const char * prefix);
+static int cdvd_audio_control(struct mpage_info * mpi, const char * prefix);
+static int cdvd_timeout(struct mpage_info * mpi, const char * prefix);
+static int cdvd_device_param(struct mpage_info * mpi, const char * prefix);
+static int cdvd_cache(struct mpage_info * mpi, const char * prefix);
+static int cdvd_mm_capab(struct mpage_info * mpi, const char * prefix);
+static int cdvd_feature(struct mpage_info * mpi, const char * prefix);
+static int tape_data_compression(struct mpage_info * mpi, const char * prefix);
+static int tape_dev_config(struct mpage_info * mpi, const char * prefix);
+static int tape_medium_part1(struct mpage_info * mpi, const char * prefix);
+static int tape_medium_part2_4(struct mpage_info * mpi, const char * prefix);
+static int ses_services_manag(struct mpage_info * mpi, const char * prefix);
+static int spi4_training_config(struct mpage_info * mpi, const char * prefix);
+static int spi4_negotiated(struct mpage_info * mpi, const char * prefix);
+static int spi4_report_xfer(struct mpage_info * mpi, const char * prefix);
+
+enum page_class {PC_COMMON, PC_DISK, PC_TAPE, PC_CDVD, PC_SES, PC_SMC};
+
+struct mpage_name_func {
+    int page;
+    int subpage;
+    enum page_class pg_class;
+    const char * name;
+    int (*func)(struct mpage_info *, const char *);
+};
+
+#define MP_LIST_PAGES 0x3f
+#define MP_LIST_SUBPAGES 0xff
+
+static struct mpage_name_func mpage_common[] =
+{
+    { 0, 0, PC_COMMON, "Vendor (non-page format)", NULL},
+    { 2, 0, PC_COMMON, "Disconnect-Reconnect", common_disconnect_reconnect},
+    { 9, 0, PC_COMMON, "Peripheral device (obsolete)", NULL},
+    { 0xa, 0, PC_COMMON, "Control", common_control},
+    { 0xa, 1, PC_COMMON, "Control Extension", common_control_extension},
+    { 0x15, 0, PC_COMMON, "Extended", NULL},
+    { 0x16, 0, PC_COMMON, "Extended, device-type specific", NULL},
+    { 0x18, 0, PC_COMMON, "Protocol specific lu", common_proto_spec_lu},
+    { 0x19, 0, PC_COMMON, "Protocol specific port", common_proto_spec_port},
+    { 0x19, 1, PC_COMMON, "Protocol specific port, subpage 1 overload",
+      common_proto_spec_port_sp1},
+    { 0x19, 2, PC_COMMON, "Protocol specific port, subpage 2 overload",
+      common_proto_spec_port_sp2},
+/*    { 0x19, 2, PC_COMMON, "SPI-4 Saved Training configuration",
+        spi4_training_config}, */
+    { 0x19, 3, PC_COMMON, "SPI-4 Negotiated Settings", spi4_negotiated},
+    { 0x19, 4, PC_COMMON, "SPI-4 Report transfer capabilities",
+      spi4_report_xfer},
+    { 0x1a, 0, PC_COMMON, "Power Condition", common_power_condition},
+    { 0x1c, 0, PC_COMMON, "Informational Exceptions", common_informational},
+    { MP_LIST_PAGES, 0, PC_COMMON, "Return all pages", NULL},
+};
+static const int mpage_common_len = sizeof(mpage_common) /
+                                    sizeof(mpage_common[0]);
+
+static struct mpage_name_func mpage_disk[] =
+{
+    { 1, 0, PC_DISK, "Read-Write Error Recovery", disk_error_recovery},
+    { 3, 0, PC_DISK, "Format Device", disk_format},
+    { 4, 0, PC_DISK, "Rigid Disk Geometry", disk_geometry},
+    { 5, 0, PC_DISK, "Flexible Disk", NULL},
+    { 6, 0, PC_DISK, "Optical memory", optical_memory},
+    { 7, 0, PC_DISK, "Verify Error Recovery", disk_verify_error_recovery},
+    { 8, 0, PC_DISK, "Caching", disk_cache},
+    { 0xa, 0xf1, PC_DISK, "Parallel ATA control (SAT)", NULL},
+    { 0xb, 0, PC_DISK, "Medium Types Supported", NULL},
+    { 0xc, 0, PC_DISK, "Notch and Partition", disk_notch_parameters},
+    { 0x10, 0, PC_DISK, "XOR control", disk_xor_control},
+    { 0x1c, 1, PC_DISK, "Background control", disk_background},
+};
+static const int mpage_disk_len = sizeof(mpage_disk) / sizeof(mpage_disk[0]);
+
+static struct mpage_name_func mpage_cdvd[] =
+{
+    { 1, 0, PC_CDVD, "Read-Write Error Recovery (cdvd)",
+      cdvd_error_recovery},
+    { 3, 0, PC_CDVD, "MRW", cdvd_mrw},
+    { 5, 0, PC_CDVD, "Write parameters", cdvd_write_param},
+    { 8, 0, PC_CDVD, "Caching", cdvd_cache},
+    { 0xd, 0, PC_CDVD, "CD device parameters", cdvd_device_param},
+    { 0xe, 0, PC_CDVD, "CD audio control", cdvd_audio_control},
+    { 0x18, 0, PC_CDVD, "Feature set support & version", cdvd_feature},
+    { 0x1a, 0, PC_CDVD, "Power Condition", common_power_condition},
+    { 0x1c, 0, PC_CDVD, "Fault/failure reporting control",
+      common_informational},
+    { 0x1d, 0, PC_CDVD, "Time-out & protect", cdvd_timeout},
+    { 0x2a, 0, PC_CDVD, "MM capabilities & mechanical status", cdvd_mm_capab},
+};
+static const int mpage_cdvd_len = sizeof(mpage_cdvd) / sizeof(mpage_cdvd[0]);
+
+static struct mpage_name_func mpage_tape[] =
+{
+    { 1, 0, PC_TAPE, "Read-Write Error Recovery", disk_error_recovery},
+    { 0xf, 0, PC_TAPE, "Data compression", tape_data_compression},
+    { 0x10, 0, PC_TAPE, "Device configuration", tape_dev_config},
+    { 0x10, 1, PC_TAPE, "Device configuration extension", NULL},
+    { 0x11, 0, PC_TAPE, "Medium partition(1)", tape_medium_part1},
+    { 0x12, 0, PC_TAPE, "Medium partition(2)", tape_medium_part2_4},
+    { 0x13, 0, PC_TAPE, "Medium partition(3)", tape_medium_part2_4},
+    { 0x14, 0, PC_TAPE, "Medium partition(4)", tape_medium_part2_4},
+    { 0x1c, 0, PC_TAPE, "Informational Exceptions", common_informational},
+    { 0x1d, 0, PC_TAPE, "Medium configuration", NULL},
+};
+static const int mpage_tape_len = sizeof(mpage_tape) / sizeof(mpage_tape[0]);
+
+static struct mpage_name_func mpage_ses[] =
+{
+    { 0x14, 0, PC_SES, "Enclosure services management", ses_services_manag},
+};
+static const int mpage_ses_len = sizeof(mpage_ses) / sizeof(mpage_ses[0]);
+
+static struct mpage_name_func mpage_smc[] =
+{
+    { 0x1d, 0, PC_SMC, "Element address assignment", NULL},
+    { 0x1e, 0, PC_SMC, "Transport geometry parameters", NULL},
+    { 0x1f, 0, PC_SMC, "Device capabilities", NULL},
+    { 0x1f, 1, PC_SMC, "Extended device capabilities", NULL},
+};
+static const int mpage_smc_len = sizeof(mpage_smc) / sizeof(mpage_smc[0]);
+
+
+#define MAXPARM 64
+
+static int next_parameter;
+static int n_replacement_values;
+static uint64_t replacement_values[MAXPARM];
+static char is_hex[MAXPARM];
+
+#define SMODE_SENSE 0x1a
+#define SMODE_SENSE_10 0x5a
+#define SMODE_SELECT 0x15
+#define SMODE_SELECT_10 0x55
+
+#define MPHEADER6_LEN 4
+#define MPHEADER10_LEN 8
+
+
+/* forward declarations */
+static void usage(const char *);
+static void dump(void *buffer, unsigned int length);
+
+#define DXFER_NONE        0
+#define DXFER_FROM_DEVICE 1
+#define DXFER_TO_DEVICE   2
+
+
+struct scsi_cmnd_io
+{
+    uint8_t * cmnd;       /* ptr to SCSI command block (cdb) */
+    size_t  cmnd_len;           /* number of bytes in SCSI command */
+    int dxfer_dir;              /* DXFER_NONE, DXFER_FROM_DEVICE, or
+                                   DXFER_TO_DEVICE */
+    uint8_t * dxferp;     /* ptr to outgoing/incoming data */
+    size_t dxfer_len;           /* bytes to be transferred to/from dxferp */
+};
+
+#define SENSE_BUFF_LEN   64
+#define CMD_TIMEOUT   60000 /* 60,000 milliseconds (60 seconds) */
+#define EBUFF_SZ   512
+
+
+#define GENERAL_ERROR           1
+#define UNKNOWN_OPCODE          2
+#define BAD_CDB_FIELD           3
+#define UNSUPPORTED_PARAM       4
+#define DEVICE_ATTENTION        5
+#define DEVICE_NOT_READY        6
+
+#define DECODE_FAILED_TRY_HEX   9999
+
+/* Returns 0 -> ok, 1 -> general error, 2 -> unknown opcode,
+   3 -> unsupported field in cdb, 4 -> unsupported param in data-in */
+static int
+do_scsi_io(struct scsi_cmnd_io * sio)
+{
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr;
+    struct sg_scsi_sense_hdr ssh;
+    int res;
+
+    memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sio->cmnd_len;
+    io_hdr.mx_sb_len = sizeof(sense_b);
+    if (DXFER_NONE == sio->dxfer_dir)
+        io_hdr.dxfer_direction = SG_DXFER_NONE;
+    else
+        io_hdr.dxfer_direction = (DXFER_TO_DEVICE == sio->dxfer_dir) ?
+                                SG_DXFER_TO_DEV : SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = sio->dxfer_len;
+    io_hdr.dxferp = sio->dxferp;
+    io_hdr.cmdp = sio->cmnd;
+    io_hdr.sbp = sense_b;
+    io_hdr.timeout = CMD_TIMEOUT;
+
+    if (trace_cmd) {
+        printf("  cdb:");
+        dump(sio->cmnd, sio->cmnd_len);
+    }
+    if ((trace_cmd > 1) && (DXFER_TO_DEVICE == sio->dxfer_dir)) {
+        printf("  additional data:\n");
+        dump(sio->dxferp, sio->dxfer_len);
+    }
+
+    if (ioctl(glob_fd, SG_IO, &io_hdr) < 0) {
+        perror("do_scsi_cmd: SG_IO error");
+        return GENERAL_ERROR;
+    }
+    res = sg_err_category3(&io_hdr);
+    switch (res) {
+    case SG_LIB_CAT_RECOVERED:
+        sg_chk_n_print3("do_scsi_cmd, continuing", &io_hdr, true);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+        __attribute__((fallthrough));
+        /* FALL THROUGH */
+#endif
+#endif
+    case SG_LIB_CAT_CLEAN:
+        return 0;
+    default:
+        if (trace_cmd) {
+            char ebuff[EBUFF_SZ];
+
+            snprintf(ebuff, EBUFF_SZ, "do_scsi_io: opcode=0x%x", sio->cmnd[0]);
+            sg_chk_n_print3(ebuff, &io_hdr, true);
+        }
+        if (sg_normalize_sense(&io_hdr, &ssh)) {
+            if (ILLEGAL_REQUEST == ssh.sense_key) {
+                if (0x20 == ssh.asc)
+                    return UNKNOWN_OPCODE;
+                else if (0x24 == ssh.asc)
+                    return BAD_CDB_FIELD;
+                else if (0x26 == ssh.asc)
+                    return UNSUPPORTED_PARAM;
+            } else if (UNIT_ATTENTION == ssh.sense_key)
+                return DEVICE_ATTENTION;
+            else if (NOT_READY == ssh.sense_key)
+                return DEVICE_NOT_READY;
+        }
+        return GENERAL_ERROR;
+    }
+}
+
+struct mpage_name_func * get_mpage_info(int page_no, int subpage_no,
+                                    struct mpage_name_func * mpp, int elems)
+{
+    int k;
+
+    for (k = 0; k < elems; ++k, ++mpp) {
+        if ((mpp->page == page_no) && (mpp->subpage == subpage_no))
+            return mpp;
+        if (mpp->page > page_no)
+            break;
+    }
+    return NULL;
+}
+
+enum page_class get_page_class(struct mpage_info * mpi)
+{
+    switch (mpi->peri_type)
+    {
+    case 0:
+    case 4:
+    case 7:
+    case 0xe:   /* should be RBC */
+        return PC_DISK;
+    case 1:
+    case 2:
+        return PC_TAPE;
+    case 8:
+        return PC_SMC;
+    case 5:
+        return PC_CDVD;
+    case 0xd:
+        return PC_SES;
+    default:
+        return PC_COMMON;
+    }
+}
+
+struct mpage_name_func * get_mpage_name_func(struct mpage_info * mpi)
+{
+    struct mpage_name_func * mpf = NULL;
+
+    switch (get_page_class(mpi))
+    {
+    case PC_DISK:
+        mpf = get_mpage_info(mpi->page, mpi->subpage, mpage_disk,
+                             mpage_disk_len);
+        break;
+    case PC_CDVD:
+        mpf = get_mpage_info(mpi->page, mpi->subpage, mpage_cdvd,
+                             mpage_cdvd_len);
+        break;
+    case PC_TAPE:
+        mpf = get_mpage_info(mpi->page, mpi->subpage, mpage_tape,
+                             mpage_tape_len);
+        break;
+    case PC_SES:
+        mpf = get_mpage_info(mpi->page, mpi->subpage, mpage_ses,
+                             mpage_ses_len);
+        break;
+    case PC_SMC:
+        mpf = get_mpage_info(mpi->page, mpi->subpage, mpage_smc,
+                             mpage_smc_len);
+        break;
+    case PC_COMMON:
+        /* picked up it catch all next */
+        break;
+    }
+    if (NULL == mpf) {
+        if ((PC_SES != get_page_class(mpi)) && (mpi->inq_byte6 & 0x40)) {
+            /* check for attached enclosure services processor */
+            mpf = get_mpage_info(mpi->page, mpi->subpage, mpage_ses,
+                                 mpage_ses_len);
+        }
+        if ((PC_SMC != get_page_class(mpi)) && (mpi->inq_byte6 & 0x8)) {
+            /* check for attached medium changer device */
+            mpf = get_mpage_info(mpi->page, mpi->subpage, mpage_smc,
+                                 mpage_smc_len);
+        }
+    }
+    if (NULL == mpf)
+        mpf = get_mpage_info(mpi->page, mpi->subpage, mpage_common,
+                             mpage_common_len);
+    return mpf;
+}
+
+
+static char unkn_page_str[64];
+
+static const char *
+get_page_name(struct mpage_info * mpi)
+{
+    struct mpage_name_func * mpf;
+
+    if (MP_LIST_PAGES == mpi->page) {
+        if (MP_LIST_SUBPAGES == mpi->subpage)
+            return "List supported pages and subpages";
+        else
+            return "List supported pages";
+    }
+    mpf = get_mpage_name_func(mpi);
+    if ((NULL == mpf) || (NULL == mpf->name)) {
+        if (mpi->subpage)
+            snprintf(unkn_page_str, sizeof(unkn_page_str),
+                     "page number=0x%x, subpage number=0x%x",
+                     mpi->page, mpi->subpage);
+        else
+            snprintf(unkn_page_str, sizeof(unkn_page_str),
+                     "page number=0x%x", mpi->page);
+        return unkn_page_str;
+    }
+    return mpf->name;
+}
+
+static void
+dump(void *buffer, unsigned int length)
+{
+    unsigned int i;
+
+    printf("    ");
+    for (i = 0; i < length; i++) {
+#if 0
+        if (((uint8_t *) buffer)[i] > 0x20)
+            printf(" %c ", (unsigned int) ((uint8_t *) buffer)[i]);
+        else
+#endif
+            printf("%02x ", (unsigned int) ((uint8_t *) buffer)[i]);
+        if ((i % 16 == 15) && (i < (length - 1))) {
+            printf("\n    ");
+        }
+    }
+    printf("\n");
+
+}
+
+static int
+getnbyte(const uint8_t *pnt, int nbyte)
+{
+    unsigned int result;
+    int i;
+
+    if (nbyte > 4)
+        fprintf(stderr, "getnbyte() limited to 32 bits, nbyte=%d\n", nbyte);
+    result = 0;
+    for (i = 0; i < nbyte; i++)
+        result = (result << 8) | (pnt[i] & 0xff);
+    return result;
+}
+
+static int64_t
+getnbyte_ll(const uint8_t *pnt, int nbyte)
+{
+    int64_t result;
+    int i;
+
+    if (nbyte > 8)
+        fprintf(stderr, "getnbyte_ll() limited to 64 bits, nbyte=%d\n",
+                nbyte);
+    result = 0;
+    for (i = 0; i < nbyte; i++)
+        result = (result << 8) + (pnt[i] & 0xff);
+    return result;
+}
+
+static int
+putnbyte(uint8_t *pnt, unsigned int value,
+                    unsigned int nbyte)
+{
+    int i;
+
+    for (i = nbyte - 1; i >= 0; i--) {
+        pnt[i] = value & 0xff;
+        value = value >> 8;
+    }
+    return 0;
+}
+
+#define REASON_SZ 128
+
+static void
+check_parm_type(int i)
+{
+    char reason[REASON_SZ];
+
+    if (i == 1 && is_hex[next_parameter] != 1) {
+        snprintf(reason, REASON_SZ,
+                 "simple number (pos %i) instead of @ hexdatafield: %"
+                 PRIu64 , next_parameter, replacement_values[next_parameter]);
+        usage(reason);
+    }
+    if (i != 1 && is_hex[next_parameter]) {
+        snprintf(reason, REASON_SZ,
+                 "@ hexdatafield (pos %i) instead of a simple number: %"
+                 PRIu64 , next_parameter, replacement_values[next_parameter]);
+        usage(reason);
+    }
+}
+
+static void
+bitfield(uint8_t *pageaddr, const char * text, int mask, int shift)
+{
+    if (x_interface && replace) {
+        check_parm_type(0);
+        *pageaddr = (*pageaddr & ~(mask << shift)) |
+            ((replacement_values[next_parameter++] & mask) << shift);
+    } else if (x_interface)
+        printf("%d ", (*pageaddr >> shift) & mask);
+    else
+        printf("%-35s%d\n", text, (*pageaddr >> shift) & mask);
+}
+
+#if 0
+static void
+notbitfield(uint8_t *pageaddr, char * text, int mask,
+                        int shift)
+{
+    if (modifiable) {
+        bitfield(pageaddr, text, mask, shift);
+        return;
+    }
+    if (x_interface && replace) {
+        check_parm_type(0);
+        *pageaddr = (*pageaddr & ~(mask << shift)) |
+            (((!replacement_values[next_parameter++]) & mask) << shift);
+    } else if (x_interface)
+        printf("%d ", !((*pageaddr >> shift) & mask));
+    else
+        printf("%-35s%d\n", text, !((*pageaddr >> shift) & mask));
+}
+#endif
+
+static void
+intfield(uint8_t * pageaddr, int nbytes, const char * text)
+{
+    if (x_interface && replace) {
+        check_parm_type(0);
+        putnbyte(pageaddr, replacement_values[next_parameter++], nbytes);
+    } else if (x_interface)
+        printf("%d ", getnbyte(pageaddr, nbytes));
+    else
+        printf("%-35s%d\n", text, getnbyte(pageaddr, nbytes));
+}
+
+static void
+hexfield(uint8_t * pageaddr, int nbytes, const char * text)
+{
+    if (x_interface && replace) {
+        check_parm_type(0);
+        putnbyte(pageaddr, replacement_values[next_parameter++], nbytes);
+    } else if (x_interface)
+        printf("%d ", getnbyte(pageaddr, nbytes));
+    else
+        printf("%-35s0x%x\n", text, getnbyte(pageaddr, nbytes));
+}
+
+static void
+hexdatafield(uint8_t * pageaddr, int nbytes, const char * text)
+{
+    if (x_interface && replace) {
+        uint8_t *ptr;
+        unsigned tmp;
+
+        /* Though in main we ensured that a @string has the right format,
+           we have to check that we are working on a @ hexdata field */
+
+        check_parm_type(1);
+
+        ptr = (uint8_t *) (unsigned long)
+              (replacement_values[next_parameter++]);
+        ptr++;                  /* Skip @ */
+
+        while (*ptr) {
+            if (!nbytes)
+                goto illegal;
+            tmp = (*ptr >= 'a') ? (*ptr - 'a' + 'A') : *ptr;
+            tmp -= (tmp >= 'A') ? 'A' - 10 : '0';
+
+            *pageaddr = tmp << 4;
+            ptr++;
+
+            tmp = (*ptr >= 'a') ? (*ptr - 'a' + 'A') : *ptr;
+            tmp -= (tmp >= 'A') ? 'A' - 10 : '0';
+
+            *pageaddr++ += tmp;
+            ptr++;
+            nbytes--;
+        }
+
+        if (nbytes) {
+          illegal:
+            fputs("sginfo: incorrect number of bytes in @hexdatafield.\n",
+                  stdout);
+            exit(2);
+        }
+    } else if (x_interface) {
+        putchar('@');
+        while (nbytes-- > 0)
+            printf("%02x", *pageaddr++);
+        putchar(' ');
+    } else {
+        printf("%-35s0x", text);
+        while (nbytes-- > 0)
+            printf("%02x", *pageaddr++);
+        putchar('\n');
+    }
+}
+
+
+/* Offset into mode sense (6 or 10 byte) response that actual mode page
+ * starts at (relative to resp[0]). Returns -1 if problem */
+static int
+modePageOffset(const uint8_t * resp, int len, int modese_6)
+{
+    int bd_len;
+    int resp_len = 0;
+    int offset = -1;
+
+    if (resp) {
+        if (modese_6) {
+            resp_len = resp[0] + 1;
+            bd_len = resp[3];
+            offset = bd_len + MPHEADER6_LEN;
+        } else {
+            resp_len = (resp[0] << 8) + resp[1] + 2;
+            bd_len = (resp[6] << 8) + resp[7];
+            /* LongLBA doesn't change this calculation */
+            offset = bd_len + MPHEADER10_LEN;
+        }
+        if ((offset + 2) > len) {
+            printf("modePageOffset: raw_curr too small, offset=%d "
+                   "resp_len=%d bd_len=%d\n", offset, resp_len, bd_len);
+            offset = -1;
+        } else if ((offset + 2) > resp_len) {
+            printf("modePageOffset: response length too short, resp_len=%d"
+                   " offset=%d bd_len=%d\n", resp_len, offset, bd_len);
+            offset = -1;
+        }
+    }
+    return offset;
+}
+
+/* Reads mode (sub-)page via 6 byte MODE SENSE, returns 0 if ok */
+static int
+get_mode_page6(struct mpage_info * mpi, int dbd, uint8_t * resp,
+               int sngl_fetch)
+{
+    int status, off;
+    uint8_t cmd[6];
+    struct scsi_cmnd_io sci;
+    int initial_len = (sngl_fetch ? MAX_RESP6_SIZE : 4);
+
+    memset(resp, 0, 4);
+    cmd[0] = SMODE_SENSE;       /* MODE SENSE (6) */
+    cmd[1] = 0x00 | (dbd ? 0x8 : 0); /* disable block descriptors bit */
+    cmd[2] = (mpi->page_control << 6) | mpi->page;
+    cmd[3] = mpi->subpage;      /* subpage code */
+    cmd[4] = initial_len;
+    cmd[5] = 0x00;              /* control */
+
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_FROM_DEVICE;
+    sci.dxfer_len = initial_len;
+    sci.dxferp = resp;
+    status = do_scsi_io(&sci);
+    if (status) {
+        if (mpi->subpage)
+            fprintf(stdout, ">>> Unable to read %s mode page 0x%x, subpage "
+                    "0x%x [mode_sense_6]\n", get_page_name(mpi), mpi->page,
+                    mpi->subpage);
+        else
+            fprintf(stdout, ">>> Unable to read %s mode page (0x%x) "
+                    "[mode_sense_6]\n", get_page_name(mpi), mpi->page);
+        return status;
+    }
+    mpi->resp_len = resp[0] + 1;
+    if (sngl_fetch) {
+        if (trace_cmd > 1) {
+            off = modePageOffset(resp, mpi->resp_len, 1);
+            if (off >= 0) {
+                printf("  cdb response:\n");
+                dump(resp, mpi->resp_len);
+            }
+        }
+        return status;
+    }
+
+    cmd[4] = mpi->resp_len;
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_FROM_DEVICE;
+    sci.dxfer_len = mpi->resp_len;
+    sci.dxferp = resp;
+    status = do_scsi_io(&sci);
+    if (status) {
+        if (mpi->subpage)
+            fprintf(stdout, ">>> Unable to read %s mode page 0x%x, subpage "
+                    "0x%x [mode_sense_6]\n", get_page_name(mpi), mpi->page,
+                    mpi->subpage);
+        else
+            fprintf(stdout, ">>> Unable to read %s mode page (0x%x) "
+                    "[mode_sense_6]\n", get_page_name(mpi), mpi->page);
+    } else if (trace_cmd > 1) {
+        off = modePageOffset(resp, mpi->resp_len, 1);
+        if (off >= 0) {
+            printf("  cdb response:\n");
+            dump(resp, mpi->resp_len);
+        }
+    }
+    return status;
+}
+
+/* Reads mode (sub-)page via 10 byte MODE SENSE, returns 0 if ok */
+static int
+get_mode_page10(struct mpage_info * mpi, int llbaa, int dbd,
+                uint8_t * resp, int sngl_fetch)
+{
+    int status, off;
+    uint8_t cmd[10];
+    struct scsi_cmnd_io sci;
+    int initial_len = (sngl_fetch ? MAX_RESP10_SIZE : 4);
+
+    memset(resp, 0, 4);
+    cmd[0] = SMODE_SENSE_10;     /* MODE SENSE (10) */
+    cmd[1] = 0x00 | (llbaa ? 0x10 : 0) | (dbd ? 0x8 : 0);
+    cmd[2] = (mpi->page_control << 6) | mpi->page;
+    cmd[3] = mpi->subpage;
+    cmd[4] = 0x00;              /* (reserved) */
+    cmd[5] = 0x00;              /* (reserved) */
+    cmd[6] = 0x00;              /* (reserved) */
+    cmd[7] = (initial_len >> 8) & 0xff;
+    cmd[8] = initial_len & 0xff;
+    cmd[9] = 0x00;              /* control */
+
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_FROM_DEVICE;
+    sci.dxfer_len = initial_len;
+    sci.dxferp = resp;
+    status = do_scsi_io(&sci);
+    if (status) {
+        if (mpi->subpage)
+            fprintf(stdout, ">>> Unable to read %s mode page 0x%x, subpage "
+                    "0x%x [mode_sense_10]\n", get_page_name(mpi), mpi->page,
+                    mpi->subpage);
+        else {
+            fprintf(stdout, ">>> Unable to read %s mode page (0x%x) "
+                    "[mode_sense_10]\n", get_page_name(mpi), mpi->page);
+            return status;
+        }
+    }
+    mpi->resp_len = (resp[0] << 8) + resp[1] + 2;
+    if (sngl_fetch) {
+        if (trace_cmd > 1) {
+            off = modePageOffset(resp, mpi->resp_len, 0);
+            if (off >= 0) {
+                printf("  cdb response:\n");
+                dump(resp, mpi->resp_len);
+            }
+        }
+        return status;
+    }
+
+    cmd[7] = (mpi->resp_len >> 8) & 0xff;
+    cmd[8] = (mpi->resp_len & 0xff);
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_FROM_DEVICE;
+    sci.dxfer_len = mpi->resp_len;
+    sci.dxferp = resp;
+    status = do_scsi_io(&sci);
+    if (status) {
+        if (mpi->subpage)
+            fprintf(stdout, ">>> Unable to read %s mode page 0x%x, subpage "
+                    "0x%x [mode_sense_10]\n", get_page_name(mpi), mpi->page,
+                    mpi->subpage);
+        else
+            fprintf(stdout, ">>> Unable to read %s mode page (0x%x) "
+                    "[mode_sense_10]\n", get_page_name(mpi), mpi->page);
+    } else if (trace_cmd > 1) {
+        off = modePageOffset(resp, mpi->resp_len, 0);
+        if (off >= 0) {
+            printf("  cdb response:\n");
+            dump(resp, mpi->resp_len);
+        }
+    }
+    return status;
+}
+
+static int
+get_mode_page(struct mpage_info * mpi, int dbd, uint8_t * resp)
+{
+    int res;
+
+    if (mode6byte)
+        res = get_mode_page6(mpi, dbd, resp, single_fetch);
+    else
+        res = get_mode_page10(mpi, 0, dbd, resp, single_fetch);
+    if (UNKNOWN_OPCODE == res)
+        fprintf(stdout, ">>>>> Try command again with%s '-6' "
+                "argument\n", (mode6byte ? "out the" : " a"));
+    else if (mpi->subpage && (BAD_CDB_FIELD == res))
+        fprintf(stdout, ">>>>> device doesn't seem to support "
+                "subpages\n");
+    else if (DEVICE_ATTENTION == res)
+        fprintf(stdout, ">>>>> device reports UNIT ATTENTION, check it or"
+                " just try again\n");
+    else if (DEVICE_NOT_READY == res)
+        fprintf(stdout, ">>>>> device NOT READY, does it need media?\n");
+    return res;
+}
+
+/* Contents should point to the mode parameter header that we obtained
+   in a prior read operation.  This way we do not have to work out the
+   format of the beast. Assume 0 or 1 block descriptors. */
+static int
+put_mode_page6(struct mpage_info * mpi, const uint8_t * msense6_resp,
+               int sp_bit)
+{
+    int status;
+    int bdlen, resplen;
+    uint8_t cmd[6];
+    struct scsi_cmnd_io sci;
+
+    bdlen = msense6_resp[3];
+    resplen = msense6_resp[0] + 1;
+
+    cmd[0] = SMODE_SELECT;
+    cmd[1] = 0x10 | (sp_bit ? 1 : 0); /* always set PF bit */
+    cmd[2] = 0x00;
+    cmd[3] = 0x00;              /* (reserved) */
+    cmd[4] = resplen;           /* parameter list length */
+    cmd[5] = 0x00;              /* (reserved) */
+
+    memcpy(cbuffer1, msense6_resp, resplen);
+    cbuffer1[0] = 0;            /* Mask off the mode data length
+                                   - reserved field */
+    cbuffer1[2] = 0;            /* device-specific parameter is not defined
+                                   and/or reserved for mode select */
+
+#if 0   /* leave block descriptor alone */
+    if (bdlen > 0) {
+        memset(cbuffer1 + MPHEADER6_LEN, 0, 4);  /* clear 'number of blocks'
+                                                   for DAD device */
+        cbuffer1[MPHEADER6_LEN + 4] = 0; /* clear DAD density code. Why? */
+        /* leave DAD block length */
+    }
+#endif
+    cbuffer1[MPHEADER6_LEN + bdlen] &= 0x7f;   /* Mask PS bit */
+
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_TO_DEVICE;
+    sci.dxfer_len = resplen;
+    sci.dxferp = cbuffer1;
+    status = do_scsi_io(&sci);
+    if (status) {
+        if (mpi->subpage)
+            fprintf(stdout, ">>> Unable to store %s mode page 0x%x,"
+                    " subpage 0x%x [msel_6]\n", get_page_name(mpi),
+                    mpi->page, mpi->subpage);
+        else
+            fprintf(stdout, ">>> Unable to store %s mode page 0x%x [msel_6]\n",
+                    get_page_name(mpi), mpi->page);
+    }
+    return status;
+}
+
+/* Contents should point to the mode parameter header that we obtained
+   in a prior read operation.  This way we do not have to work out the
+   format of the beast. Assume 0 or 1 block descriptors. */
+static int
+put_mode_page10(struct mpage_info * mpi, const uint8_t * msense10_resp,
+                int sp_bit)
+{
+    int status;
+    int bdlen, resplen;
+    uint8_t cmd[10];
+    struct scsi_cmnd_io sci;
+
+    bdlen = (msense10_resp[6] << 8) + msense10_resp[7];
+    resplen = (msense10_resp[0] << 8) + msense10_resp[1] + 2;
+
+    cmd[0] = SMODE_SELECT_10;
+    cmd[1] = 0x10 | (sp_bit ? 1 : 0); /* always set PF bit */
+    cmd[2] = 0x00;              /* (reserved) */
+    cmd[3] = 0x00;              /* (reserved) */
+    cmd[4] = 0x00;              /* (reserved) */
+    cmd[5] = 0x00;              /* (reserved) */
+    cmd[6] = 0x00;              /* (reserved) */
+    cmd[7] = (resplen >> 8) & 0xff;
+    cmd[8] = resplen & 0xff;
+    cmd[9] = 0x00;              /* (reserved) */
+
+    memcpy(cbuffer1, msense10_resp, resplen);
+    cbuffer1[0] = 0;            /* Mask off the mode data length */
+    cbuffer1[1] = 0;            /* Mask off the mode data length */
+    cbuffer1[3] = 0;            /* device-specific parameter is not defined
+                                   and/or reserved for mode select */
+#if 0   /* leave block descriptor alone */
+    if (bdlen > 0) {
+        memset(cbuffer1 + MPHEADER10_LEN, 0, 4);  /* clear 'number of blocks'
+                                                    for DAD device */
+        cbuffer1[MPHEADER10_LEN + 4] = 0; /* clear DAD density code. Why? */
+        /* leave DAD block length */
+    }
+#endif
+    cbuffer1[MPHEADER10_LEN + bdlen] &= 0x7f;   /* Mask PS bit */
+
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_TO_DEVICE;
+    sci.dxfer_len = resplen;
+    sci.dxferp = cbuffer1;
+    status = do_scsi_io(&sci);
+    if (status) {
+        if (mpi->subpage)
+            fprintf(stdout, ">>> Unable to store %s mode page 0x%x,"
+                    " subpage 0x%x [msel_10]\n", get_page_name(mpi),
+                    mpi->page, mpi->subpage);
+        else
+            fprintf(stdout, ">>> Unable to store %s mode page 0x%x "
+                    "[msel_10]\n", get_page_name(mpi), mpi->page);
+    }
+    return status;
+}
+
+static int
+put_mode_page(struct mpage_info * mpi, const uint8_t * msense_resp)
+{
+    if (mode6byte)
+        return put_mode_page6(mpi, msense_resp, ! negate_sp_bit);
+    else
+        return put_mode_page10(mpi, msense_resp, ! negate_sp_bit);
+}
+
+static int
+setup_mode_page(struct mpage_info * mpi, int nparam, uint8_t * buff,
+                uint8_t ** o_pagestart)
+{
+    int status, offset, rem_pglen;
+    uint8_t * pgp;
+
+    status = get_mode_page(mpi, 0, buff);
+    if (status) {
+        printf("\n");
+        return status;
+    }
+    offset = modePageOffset(buff, mpi->resp_len, mode6byte);
+    if (offset < 0) {
+        fprintf(stdout, "mode page=0x%x has bad page format\n", mpi->page);
+        fprintf(stdout, "   perhaps '-z' switch may help\n");
+        return -1;
+    }
+    pgp = buff + offset;
+    *o_pagestart = pgp;
+    rem_pglen = (0x40 & pgp[0]) ? ((pgp[2] << 8) + pgp[3]) : pgp[1];
+
+    if (x_interface && replace) {
+        if ((nparam && (n_replacement_values != nparam)) ||
+            ((! nparam) && (n_replacement_values != rem_pglen))) {
+            fprintf(stdout, "Wrong number of replacement values (%i instead "
+                    "of %i)\n", n_replacement_values,
+                    nparam ? nparam : rem_pglen);
+            return 1;
+        }
+        next_parameter = 1;
+    }
+    return 0;
+}
+
+static int
+get_protocol_id(int port_not_lu, uint8_t * buff, int * proto_idp,
+                int * offp)
+{
+    int status, off, proto_id, spf;
+    struct mpage_info mp_i;
+    char b[64];
+
+    memset(&mp_i, 0, sizeof(mp_i));
+    mp_i.page = (port_not_lu ? 0x19 : 0x18);
+    /* N.B. getting port or lu specific mode page (not subpage) */
+    status = get_mode_page(&mp_i, 0, buff);
+    if (status)
+        return status;
+    off = modePageOffset(buff, mp_i.resp_len, mode6byte);
+    if (off < 0)
+        return off;
+    spf = (buff[off] & 0x40) ? 1 : 0;  /* subpages won't happen here */
+    proto_id = buff[off + (spf ? 5 : 2)] & 0xf;
+    if (trace_cmd > 0)
+        printf("Protocol specific %s, protocol_id=%s\n",
+               (port_not_lu ? "port" : "lu"),
+               sg_get_trans_proto_str(proto_id, sizeof(b), b));
+    if (proto_idp)
+        *proto_idp = proto_id;
+    if (offp)
+        *offp = off;
+    return 0;
+}
+
+static int
+disk_geometry(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 9, cbuffer, &pagestart);
+    if (status)
+        return status;
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------------------\n");
+    };
+    intfield(pagestart + 2, 3, "Number of cylinders");
+    intfield(pagestart + 5, 1, "Number of heads");
+    intfield(pagestart + 6, 3, "Starting cyl. write precomp");
+    intfield(pagestart + 9, 3, "Starting cyl. reduced current");
+    intfield(pagestart + 12, 2, "Device step rate");
+    intfield(pagestart + 14, 3, "Landing Zone Cylinder");
+    bitfield(pagestart + 17, "RPL", 3, 0);
+    intfield(pagestart + 18, 1, "Rotational Offset");
+    intfield(pagestart + 20, 2, "Rotational Rate");
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+common_disconnect_reconnect(struct mpage_info * mpi,
+                                       const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 11, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("------------------------------------\n");
+    };
+    intfield(pagestart + 2, 1, "Buffer full ratio");
+    intfield(pagestart + 3, 1, "Buffer empty ratio");
+    intfield(pagestart + 4, 2, "Bus Inactivity Limit (SAS: 100us)");
+    intfield(pagestart + 6, 2, "Disconnect Time Limit");
+    intfield(pagestart + 8, 2, "Connect Time Limit (SAS: 100us)");
+    intfield(pagestart + 10, 2, "Maximum Burst Size");
+    bitfield(pagestart + 12, "EMDP", 1, 7);
+    bitfield(pagestart + 12, "Fair Arbitration (fcp:faa,fab,fac)", 0x7, 4);
+    bitfield(pagestart + 12, "DIMM", 1, 3);
+    bitfield(pagestart + 12, "DTDC", 0x7, 0);
+    intfield(pagestart + 14, 2, "First Burst Size");
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+
+}
+
+static int
+common_control(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 21, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------\n");
+    }
+    bitfield(pagestart + 2, "TST", 0x7, 5);
+    bitfield(pagestart + 2, "TMF_ONLY", 1, 4);
+    bitfield(pagestart + 2, "D_SENSE", 1, 2);
+    bitfield(pagestart + 2, "GLTSD", 1, 1);
+    bitfield(pagestart + 2, "RLEC", 1, 0);
+    bitfield(pagestart + 3, "Queue Algorithm Modifier", 0xf, 4);
+    bitfield(pagestart + 3, "QErr", 0x3, 1);
+    bitfield(pagestart + 3, "DQue [obsolete]", 1, 0);
+    bitfield(pagestart + 4, "TAS", 1, 7);
+    bitfield(pagestart + 4, "RAC", 1, 6);
+    bitfield(pagestart + 4, "UA_INTLCK_CTRL", 0x3, 4);
+    bitfield(pagestart + 4, "SWP", 1, 3);
+    bitfield(pagestart + 4, "RAERP [obs.]", 1, 2);
+    bitfield(pagestart + 4, "UAAERP [obs.]", 1, 1);
+    bitfield(pagestart + 4, "EAERP [obs.]", 1, 0);
+    bitfield(pagestart + 5, "ATO", 1, 7);
+    bitfield(pagestart + 5, "TAS", 1, 6);
+    bitfield(pagestart + 5, "AUTOLOAD MODE", 0x7, 0);
+    intfield(pagestart + 6, 2, "Ready AER Holdoff Period [obs.]");
+    intfield(pagestart + 8, 2, "Busy Timeout Period");
+    intfield(pagestart + 10, 2, "Extended self-test completion time");
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+common_control_extension(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 4, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode subpage (0x%x,0x%x)\n", get_page_name(mpi), mpi->page,
+               mpi->subpage);
+        printf("--------------------------------------------\n");
+    }
+    bitfield(pagestart + 4, "TCMOS", 1, 2);
+    bitfield(pagestart + 4, "SCSIP", 1, 1);
+    bitfield(pagestart + 4, "IALUAE", 1, 0);
+    bitfield(pagestart + 5, "Initial Priority", 0xf, 0);
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+common_informational(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 10, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "PERF", 1, 7);
+    bitfield(pagestart + 2, "EBF", 1, 5);
+    bitfield(pagestart + 2, "EWASC", 1, 4);
+    bitfield(pagestart + 2, "DEXCPT", 1, 3);
+    bitfield(pagestart + 2, "TEST", 1, 2);
+    bitfield(pagestart + 2, "EBACKERR", 1, 1);
+    bitfield(pagestart + 2, "LOGERR", 1, 0);
+    bitfield(pagestart + 3, "MRIE", 0xf, 0);
+    intfield(pagestart + 4, 4, "Interval Timer");
+    intfield(pagestart + 8, 4, "Report Count");
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+disk_error_recovery(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 14, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "AWRE", 1, 7);
+    bitfield(pagestart + 2, "ARRE", 1, 6);
+    bitfield(pagestart + 2, "TB", 1, 5);
+    bitfield(pagestart + 2, "RC", 1, 4);
+    bitfield(pagestart + 2, "EER", 1, 3);
+    bitfield(pagestart + 2, "PER", 1, 2);
+    bitfield(pagestart + 2, "DTE", 1, 1);
+    bitfield(pagestart + 2, "DCR", 1, 0);
+    intfield(pagestart + 3, 1, "Read Retry Count");
+    intfield(pagestart + 4, 1, "Correction Span");
+    intfield(pagestart + 5, 1, "Head Offset Count");
+    intfield(pagestart + 6, 1, "Data Strobe Offset Count");
+    intfield(pagestart + 8, 1, "Write Retry Count");
+    intfield(pagestart + 10, 2, "Recovery Time Limit (ms)");
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+cdvd_error_recovery(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 10, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("------------------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "AWRE", 1, 7);
+    bitfield(pagestart + 2, "ARRE", 1, 6);
+    bitfield(pagestart + 2, "TB", 1, 5);
+    bitfield(pagestart + 2, "RC", 1, 4);
+    bitfield(pagestart + 2, "PER", 1, 2);
+    bitfield(pagestart + 2, "DTE", 1, 1);
+    bitfield(pagestart + 2, "DCR", 1, 0);
+    intfield(pagestart + 3, 1, "Read Retry Count");
+    bitfield(pagestart + 7, "EMCDR", 3, 0);
+    intfield(pagestart + 8, 1, "Write Retry Count");
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+cdvd_mrw(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 1, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("------------------------------------------------\n");
+    }
+    bitfield(pagestart + 3, "LBA space", 1, 0);
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+disk_notch_parameters(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 6, cbuffer, &pagestart);
+    if (status) {
+        fprintf(stdout, "Special case: only give 6 fields to '-XR' since"
+                " 'Pages Notched' is unchangeable\n");
+        return status;
+    }
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------------------\n");
+    };
+    bitfield(pagestart + 2, "Notched Drive", 1, 7);
+    bitfield(pagestart + 2, "Logical or Physical Notch", 1, 6);
+    intfield(pagestart + 4, 2, "Max # of notches");
+    intfield(pagestart + 6, 2, "Active Notch");
+    if (pagestart[2] & 0x40) {
+        intfield(pagestart + 8, 4, "Starting Boundary");
+        intfield(pagestart + 12, 4, "Ending Boundary");
+    } else {           /* Hex is more meaningful for physical notches */
+        hexfield(pagestart + 8, 4, "Starting Boundary");
+        hexfield(pagestart + 12, 4, "Ending Boundary");
+    }
+
+    if (x_interface && !replace) {
+#if 1
+        ;       /* do nothing, skip this field */
+#else
+        if (1 == mpi->page_control)     /* modifiable */
+            printf("0");
+        else
+            printf("0x%8.8x%8.8x", getnbyte(pagestart + 16, 4),
+                   getnbyte(pagestart + 20, 4));
+#endif
+    };
+    if (!x_interface)
+        printf("Pages Notched                      %8.8x %8.8x\n",
+               getnbyte(pagestart + 16, 4), getnbyte(pagestart + 20, 4));
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static const char *
+formatname(int format)
+{
+    switch(format) {
+        case 0x0: return "logical block addresses (32 bit)";
+        case 0x3: return "logical block addresses (64 bit)";
+        case 0x4: return "bytes from index [Cyl:Head:Off]\n"
+                "Offset -1 marks whole track as bad.\n";
+        case 0x5: return "physical blocks [Cyl:Head:Sect]\n"
+                "Sector -1 marks whole track as bad.\n";
+    }
+    return "Weird, unknown format";
+}
+
+static int
+read_defect_list(int grown_only)
+{
+    int i, len, reallen, table, k, defect_format;
+    int status = 0;
+    int header = 1;
+    int sorthead = 0;
+    uint8_t cmd[10];
+    uint8_t cmd12[12];
+    uint8_t *df = NULL;
+    uint8_t *bp = NULL;
+    uint8_t *heapp = NULL;
+    unsigned int  *headsp = NULL;
+    int trunc;
+    struct scsi_cmnd_io sci;
+
+    if (defectformat == HEAD_SORT_TOKEN) {
+        defectformat = 0x04;
+        sorthead = 1;
+        headsp = (unsigned int *)calloc(MAX_HEADS, sizeof(unsigned int));
+        if (headsp == NULL) {
+           perror("malloc failed");
+           return status;
+        }
+    }
+    for (table = grown_only; table < 2; table++) {
+        if (heapp) {
+            free(heapp);
+            heapp = NULL;
+        }
+        bp = cbuffer;
+        memset(bp, 0, 4);
+        trunc = 0;
+        reallen = -1;
+
+        cmd[0] = 0x37;          /* READ DEFECT DATA (10) */
+        cmd[1] = 0x00;
+        cmd[2] = (table ? 0x08 : 0x10) | defectformat;  /*  List, Format */
+        cmd[3] = 0x00;          /* (reserved) */
+        cmd[4] = 0x00;          /* (reserved) */
+        cmd[5] = 0x00;          /* (reserved) */
+        cmd[6] = 0x00;          /* (reserved) */
+        cmd[7] = 0x00;          /* Alloc len */
+        cmd[8] = 0x04;          /* Alloc len (size finder) */
+        cmd[9] = 0x00;          /* control */
+
+        sci.cmnd = cmd;
+        sci.cmnd_len = sizeof(cmd);
+        sci.dxfer_dir = DXFER_FROM_DEVICE;
+        sci.dxfer_len = 4;
+        sci.dxferp = bp;
+        i = do_scsi_io(&sci);
+        if (i) {
+            fprintf(stdout, ">>> Unable to read %s defect data.\n",
+                    (table ? "grown (GLIST)" : "primary (PLIST)"));
+            status |= i;
+            continue;
+        }
+        if (trace_cmd > 1) {
+            printf("  cdb response:\n");
+            dump(bp, 4);
+        }
+        /*
+         * Check validity of response:
+         * bp[0] reserved, must be zero
+         * bp[1] bits 7-5 reserved, must be zero
+         * bp[1] bits 4-3 should match table requested
+         */
+        if (0 != bp[0] || (table ? 0x08 : 0x10) != (bp[1] & 0xf8)) {
+            fprintf(stdout, ">>> Invalid header for %s defect list.\n",
+                    (table ? "grown (GLIST)" : "primary (PLIST)"));
+            status |= 1;
+            continue;
+        }
+        if (header) {
+            printf("Defect Lists\n"
+                   "------------\n");
+            header = 0;
+        }
+        len = (bp[2] << 8) + bp[3];
+        if (len < 0xfff8)
+            reallen = len;
+        else {
+            /*
+             * List length is at or over capacity of READ DEFECT DATA (10)
+             * Try to get actual length with READ DEFECT DATA (12)
+             */
+            bp = cbuffer;
+            memset(bp, 0, 8);
+            cmd12[0] = 0xB7;          /* READ DEFECT DATA (12) */
+            cmd12[1] = (table ? 0x08 : 0x10) | defectformat;/*  List, Format */
+            cmd12[2] = 0x00;          /* (reserved) */
+            cmd12[3] = 0x00;          /* (reserved) */
+            cmd12[4] = 0x00;          /* (reserved) */
+            cmd12[5] = 0x00;          /* (reserved) */
+            cmd12[6] = 0x00;          /* Alloc len */
+            cmd12[7] = 0x00;          /* Alloc len */
+            cmd12[8] = 0x00;          /* Alloc len */
+            cmd12[9] = 0x08;          /* Alloc len (size finder) */
+            cmd12[10] = 0x00;         /* reserved */
+            cmd12[11] = 0x00;         /* control */
+
+            sci.cmnd = cmd12;
+            sci.cmnd_len = sizeof(cmd12);
+            sci.dxfer_dir = DXFER_FROM_DEVICE;
+            sci.dxfer_len = 8;
+            sci.dxferp = bp;
+            i = do_scsi_io(&sci);
+            if (i) {
+                if (trace_cmd) {
+                    fprintf(stdout, ">>> No 12 byte command support, "
+                            "but list is too long for 10 byte version.\n"
+                            "List will be truncated at 8191 elements\n");
+                }
+                goto trytenbyte;
+            }
+            if (trace_cmd > 1) {
+                printf("  cdb response:\n");
+                dump(bp, 8);
+            }
+            /*
+             * Check validity of response:
+             *    bp[0], bp[2] and bp[3] reserved, must be zero
+             *    bp[1] bits 7-5 reserved, must be zero
+             *    bp[1] bits 4-3 should match table we requested
+             */
+            if (0 != bp[0] || 0 != bp[2] || 0 != bp[3] ||
+                    ((table ? 0x08 : 0x10) != (bp[1] & 0xf8))) {
+                if (trace_cmd)
+                    fprintf(stdout,
+                            ">>> Invalid header for %s defect list.\n",
+                            (table ? "grown (GLIST)" : "primary (PLIST)"));
+                goto trytenbyte;
+            }
+            len = (bp[4] << 24) + (bp[5] << 16) + (bp[6] << 8) + bp[7];
+            reallen = len;
+        }
+
+        if (len > 0) {
+            k = len + 8;              /* length of defect list + header */
+            if (k > (int)sizeof(cbuffer)) {
+                heapp = (uint8_t *)malloc(k);
+
+                if (len > 0x80000 && NULL == heapp) {
+                    len = 0x80000;      /* go large: 512 KB */
+                    k = len + 8;
+                    heapp = (uint8_t *)malloc(k);
+                }
+                if (heapp != NULL)
+                    bp = heapp;
+            }
+            if (len > 0xfff0 && heapp != NULL) {
+                cmd12[0] = 0xB7;          /* READ DEFECT DATA (12) */
+                cmd12[1] = (table ? 0x08 : 0x10) | defectformat;
+                                                /*  List, Format */
+                cmd12[2] = 0x00;          /* (reserved) */
+                cmd12[3] = 0x00;          /* (reserved) */
+                cmd12[4] = 0x00;          /* (reserved) */
+                cmd12[5] = 0x00;          /* (reserved) */
+                cmd12[6] = 0x00;          /* Alloc len */
+                cmd12[7] = (k >> 16) & 0xff;     /* Alloc len */
+                cmd12[8] = (k >> 8) & 0xff;      /* Alloc len */
+                cmd12[9] = (k & 0xff);    /* Alloc len */
+                cmd12[10] = 0x00;         /* reserved */
+                cmd12[11] = 0x00;         /* control */
+
+                sci.cmnd = cmd12;
+                sci.cmnd_len = sizeof(cmd12);
+                sci.dxfer_dir = DXFER_FROM_DEVICE;
+                sci.dxfer_len = k;
+                sci.dxferp = bp;
+                i = do_scsi_io(&sci);
+                if (i)
+                    goto trytenbyte;
+                if (trace_cmd > 1) {
+                    printf("  cdb response:\n");
+                    dump(bp, 8);
+                }
+                reallen = (bp[4] << 24) + (bp[5] << 16) + (bp[6] << 8) +
+                          bp[7];
+                if (reallen > len) {
+                    trunc = 1;
+                }
+                df = (uint8_t *) (bp + 8);
+            }
+            else {
+trytenbyte:
+                if (len > 0xfff8) {
+                    len = 0xfff8;
+                    trunc = 1;
+                }
+                k = len + 4;            /* length of defect list + header */
+                if (k > (int)sizeof(cbuffer) && NULL == heapp) {
+                    heapp = (uint8_t *)malloc(k);
+                    if (heapp != NULL)
+                        bp = heapp;
+                }
+                if (k > (int)sizeof(cbuffer) && NULL == heapp) {
+                    bp = cbuffer;
+                    k = sizeof(cbuffer);
+                    len = k - 4;
+                    trunc = 1;
+                }
+                cmd[0] = 0x37;          /* READ DEFECT DATA (10) */
+                cmd[1] = 0x00;
+                cmd[2] = (table ? 0x08 : 0x10) | defectformat;
+                                        /*  List, Format */
+                cmd[3] = 0x00;          /* (reserved) */
+                cmd[4] = 0x00;          /* (reserved) */
+                cmd[5] = 0x00;          /* (reserved) */
+                cmd[6] = 0x00;          /* (reserved) */
+                cmd[7] = (k >> 8);      /* Alloc len */
+                cmd[8] = (k & 0xff);    /* Alloc len */
+                cmd[9] = 0x00;          /* control */
+
+                sci.cmnd = cmd;
+                sci.cmnd_len = sizeof(cmd);
+                sci.dxfer_dir = DXFER_FROM_DEVICE;
+                sci.dxfer_len = k;
+                sci.dxferp = bp;
+                i = do_scsi_io(&sci);
+                df = (uint8_t *) (bp + 4);
+            }
+        }
+        if (i) {
+            fprintf(stdout, ">>> Unable to read %s defect data.\n",
+                    (table ? "grown (GLIST)" : "primary (PLIST)"));
+            status |= i;
+            continue;
+        }
+        else {
+            if (table && !status && !sorthead)
+                printf("\n");
+            defect_format = (bp[1] & 0x7);
+            if (-1 == reallen) {
+                printf("at least ");
+                reallen = len;
+            }
+            printf("%d entries (%d bytes) in %s table.\n",
+                   reallen / ((0 == defect_format) ? 4 : 8), reallen,
+                   table ? "grown (GLIST)" : "primary (PLIST)");
+            if (!sorthead)
+                printf("Format (%x) is: %s\n", defect_format,
+                   formatname(defect_format));
+            i = 0;
+            switch (defect_format) {
+            case 4:     /* bytes from index */
+                while (len > 0) {
+                    snprintf((char *)cbuffer1, 40, "%6d:%3u:%8d",
+                             getnbyte(df, 3), df[3], getnbyte(df + 4, 4));
+                    if (sorthead == 0)
+                        printf("%19s", (char *)cbuffer1);
+                    else
+                        if (df[3] < MAX_HEADS) headsp[df[3]]++;
+                    len -= 8;
+                    df += 8;
+                    i++;
+                    if (i >= 4 && !sorthead) {
+                        printf("\n");
+                        i = 0;
+                    }
+                    else if (!sorthead) printf("|");
+                }
+                break;
+            case 5:     /* physical sector */
+                while (len > 0) {
+                    snprintf((char *)cbuffer1, 40, "%6d:%2u:%5d",
+                             getnbyte(df, 3),
+                             df[3], getnbyte(df + 4, 4));
+                    if (sorthead == 0)
+                        printf("%15s", (char *)cbuffer1);
+                    else
+                        if (df[3] < MAX_HEADS) headsp[df[3]]++;
+                    len -= 8;
+                    df += 8;
+                    i++;
+                    if (i >= 5 && !sorthead) {
+                        printf("\n");
+                        i = 0;
+                    }
+                    else if (!sorthead) printf("|");
+                }
+                break;
+            case 0:     /* lba (32 bit) */
+                while (len > 0) {
+                    printf("%10d", getnbyte(df, 4));
+                    len -= 4;
+                    df += 4;
+                    i++;
+                    if (i >= 7) {
+                        printf("\n");
+                        i = 0;
+                    }
+                    else
+                        printf("|");
+                }
+                break;
+            case 3:     /* lba (64 bit) */
+                while (len > 0) {
+                    printf("%15" PRId64 , getnbyte_ll(df, 8));
+                    len -= 8;
+                    df += 8;
+                    i++;
+                    if (i >= 5) {
+                        printf("\n");
+                        i = 0;
+                    }
+                    else
+                        printf("|");
+                }
+                break;
+            default:
+                printf("unknown defect list format: %d\n", defect_format);
+                break;
+            }
+            if (i && !sorthead)
+                printf("\n");
+        }
+        if (trunc)
+                printf("[truncated]\n");
+    }
+    if (heapp) {
+        free(heapp);
+        heapp = NULL;
+    }
+    if (sorthead) {
+        printf("Format is: [head:# entries for this head in list]\n\n");
+        for (i=0; i<MAX_HEADS; i++) {
+            if (headsp[i] > 0) {
+               printf("%3d: %u\n", i, headsp[i]);
+            }
+        }
+        free(headsp);
+    }
+    printf("\n");
+    return status;
+}
+
+static int
+disk_cache(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 21, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------\n");
+    };
+    bitfield(pagestart + 2, "Initiator Control", 1, 7);
+    bitfield(pagestart + 2, "ABPF", 1, 6);
+    bitfield(pagestart + 2, "CAP", 1, 5);
+    bitfield(pagestart + 2, "DISC", 1, 4);
+    bitfield(pagestart + 2, "SIZE", 1, 3);
+    bitfield(pagestart + 2, "Write Cache Enabled", 1, 2);
+    bitfield(pagestart + 2, "MF", 1, 1);
+    bitfield(pagestart + 2, "Read Cache Disabled", 1, 0);
+    bitfield(pagestart + 3, "Demand Read Retention Priority", 0xf, 4);
+    bitfield(pagestart + 3, "Demand Write Retention Priority", 0xf, 0);
+    intfield(pagestart + 4, 2, "Disable Pre-fetch Transfer Length");
+    intfield(pagestart + 6, 2, "Minimum Pre-fetch");
+    intfield(pagestart + 8, 2, "Maximum Pre-fetch");
+    intfield(pagestart + 10, 2, "Maximum Pre-fetch Ceiling");
+    bitfield(pagestart + 12, "FSW", 1, 7);
+    bitfield(pagestart + 12, "LBCSS", 1, 6);
+    bitfield(pagestart + 12, "DRA", 1, 5);
+    bitfield(pagestart + 12, "NV_DIS", 1, 0);
+    intfield(pagestart + 13, 1, "Number of Cache Segments");
+    intfield(pagestart + 14, 2, "Cache Segment size");
+    intfield(pagestart + 17, 3, "Non-Cache Segment size");
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+disk_format(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 13, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------------\n");
+    };
+    intfield(pagestart + 2, 2, "Tracks per Zone");
+    intfield(pagestart + 4, 2, "Alternate sectors per zone");
+    intfield(pagestart + 6, 2, "Alternate tracks per zone");
+    intfield(pagestart + 8, 2, "Alternate tracks per lu");
+    intfield(pagestart + 10, 2, "Sectors per track");
+    intfield(pagestart + 12, 2, "Data bytes per physical sector");
+    intfield(pagestart + 14, 2, "Interleave");
+    intfield(pagestart + 16, 2, "Track skew factor");
+    intfield(pagestart + 18, 2, "Cylinder skew factor");
+    bitfield(pagestart + 20, "Supports Soft Sectoring", 1, 7);
+    bitfield(pagestart + 20, "Supports Hard Sectoring", 1, 6);
+    bitfield(pagestart + 20, "Removable Medium", 1, 5);
+    bitfield(pagestart + 20, "Surface", 1, 4);
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+
+}
+
+static int
+disk_verify_error_recovery(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 7, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "EER", 1, 3);
+    bitfield(pagestart + 2, "PER", 1, 2);
+    bitfield(pagestart + 2, "DTE", 1, 1);
+    bitfield(pagestart + 2, "DCR", 1, 0);
+    intfield(pagestart + 3, 1, "Verify Retry Count");
+    intfield(pagestart + 4, 1, "Verify Correction Span (bits)");
+    intfield(pagestart + 10, 2, "Verify Recovery Time Limit (ms)");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+#if 0
+static int
+peripheral_device_page(struct mpage_info * mpi, const char * prefix)
+{
+    static char *idents[] =
+    {
+        "X3.131: Small Computer System Interface",
+        "X3.91M-1987: Storage Module Interface",
+        "X3.170: Enhanced Small Device Interface",
+        "X3.130-1986; X3T9.3/87-002: IPI-2",
+        "X3.132-1987; X3.147-1988: IPI-3"
+    };
+    int status;
+    unsigned ident;
+    uint8_t *pagestart;
+    char *name;
+
+    status = setup_mode_page(mpi, 2, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("---------------------------------\n");
+    };
+
+#if 0
+    dump(pagestart, 20);
+    pagestart[1] += 2;          /*TEST */
+    cbuffer[8] += 2;             /*TEST */
+#endif
+
+    ident = getnbyte(pagestart + 2, 2);
+    if (ident < (sizeof(idents) / sizeof(char *)))
+         name = idents[ident];
+    else if (ident < 0x8000)
+        name = "Reserved";
+    else
+        name = "Vendor Specific";
+
+#ifdef DPG_CHECK_THIS_OUT
+    bdlen = pagestart[1] - 6;
+    if (bdlen < 0)
+        bdlen = 0;
+    else {
+        status = setup_mode_page(mpi, 2, cbuffer, &bdlen,
+                                 &pagestart);
+        if (status)
+            return status;
+    }
+
+    hexfield(pagestart + 2, 2, "Interface Identifier");
+    if (!x_interface) {
+        for (ident = 0; ident < 35; ident++)
+            putchar(' ');
+        puts(name);
+    }
+    hexdatafield(pagestart + 8, bdlen, "Vendor Specific Data");
+#endif
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    if (x_interface)
+        puts(name);
+    return 0;
+}
+#endif
+
+static int
+common_power_condition(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 4, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("--------------------------------\n");
+    }
+    bitfield(pagestart + 3, "Idle", 1, 1);
+    bitfield(pagestart + 3, "Standby", 1, 0);
+    intfield(pagestart + 4, 4, "Idle Condition counter (100ms)");
+    intfield(pagestart + 8, 4, "Standby Condition counter (100ms)");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+disk_xor_control(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 5, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("--------------------------------\n");
+    }
+    bitfield(pagestart + 2, "XORDS", 1, 1);
+    intfield(pagestart + 4, 4, "Maximum XOR write size");
+    intfield(pagestart + 12, 4, "Maximum regenerate size");
+    intfield(pagestart + 16, 4, "Maximum rebuild transfer size");
+    intfield(pagestart + 22, 2, "Rebuild delay");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+disk_background(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 4, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode subpage (0x%x,0x%x)\n", get_page_name(mpi), mpi->page,
+               mpi->subpage);
+        printf("--------------------------------------------\n");
+    }
+    bitfield(pagestart + 4, "Enable background medium scan", 1, 0);
+    bitfield(pagestart + 5, "Enable pre-scan", 1, 0);
+    intfield(pagestart + 6, 2, "BMS interval time (hour)");
+    intfield(pagestart + 8, 2, "Pre-scan timeout value (hour)");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+optical_memory(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 1, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("--------------------------------\n");
+    }
+    bitfield(pagestart + 2, "RUBR", 1, 0);
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+cdvd_write_param(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 20, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("--------------------------------\n");
+    }
+    bitfield(pagestart + 2, "BUFE", 1, 6);
+    bitfield(pagestart + 2, "LS_V", 1, 5);
+    bitfield(pagestart + 2, "Test Write", 1, 4);
+    bitfield(pagestart + 2, "Write Type", 0xf, 0);
+    bitfield(pagestart + 3, "MultiSession", 3, 6);
+    bitfield(pagestart + 3, "FP", 1, 5);
+    bitfield(pagestart + 3, "Copy", 1, 4);
+    bitfield(pagestart + 3, "Track Mode", 0xf, 0);
+    bitfield(pagestart + 4, "Data Block type", 0xf, 0);
+    intfield(pagestart + 5, 1, "Link size");
+    bitfield(pagestart + 7, "Initiator app. code", 0x3f, 0);
+    intfield(pagestart + 8, 1, "Session Format");
+    intfield(pagestart + 10, 4, "Packet size");
+    intfield(pagestart + 14, 2, "Audio Pause Length");
+    hexdatafield(pagestart + 16, 16, "Media Catalog number");
+    hexdatafield(pagestart + 32, 16, "Int. standard recording code");
+    hexdatafield(pagestart + 48, 1, "Subheader byte 1");
+    hexdatafield(pagestart + 49, 1, "Subheader byte 2");
+    hexdatafield(pagestart + 50, 1, "Subheader byte 3");
+    hexdatafield(pagestart + 51, 1, "Subheader byte 4");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+cdvd_audio_control(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 10, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("--------------------------------\n");
+    }
+    bitfield(pagestart + 2, "IMMED", 1, 2);
+    bitfield(pagestart + 2, "SOTC", 1, 1);
+    bitfield(pagestart + 8, "CDDA out port 0, channel select", 0xf, 0);
+    intfield(pagestart + 9, 1, "Channel port 0 volume");
+    bitfield(pagestart + 10, "CDDA out port 1, channel select", 0xf, 0);
+    intfield(pagestart + 11, 1, "Channel port 1 volume");
+    bitfield(pagestart + 12, "CDDA out port 2, channel select", 0xf, 0);
+    intfield(pagestart + 13, 1, "Channel port 2 volume");
+    bitfield(pagestart + 14, "CDDA out port 3, channel select", 0xf, 0);
+    intfield(pagestart + 15, 1, "Channel port 3 volume");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+cdvd_timeout(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 6, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------------------\n");
+    }
+    bitfield(pagestart + 4, "G3Enable", 1, 3);
+    bitfield(pagestart + 4, "TMOE", 1, 2);
+    bitfield(pagestart + 4, "DISP", 1, 1);
+    bitfield(pagestart + 4, "SWPP", 1, 0);
+    intfield(pagestart + 6, 2, "Group 1 minimum time-out");
+    intfield(pagestart + 8, 2, "Group 2 minimum time-out");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+cdvd_device_param(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 3, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("------------------------------------\n");
+    }
+    bitfield(pagestart + 3, "Inactivity timer multiplier", 0xf, 0);
+    intfield(pagestart + 4, 2, "MSF-S units per MSF_M unit");
+    intfield(pagestart + 6, 2, "MSF-F units per MSF_S unit");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+/* This is not a standard t10.org MMC mode page (it is now "protocol specific
+   lu" mode page). This definition was found in Hitachi GF-2050/GF-2055
+   DVD-RAM drive SCSI reference manual. */
+static int
+cdvd_feature(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 12, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("----------------------------------------------\n");
+    }
+    intfield(pagestart + 2, 2, "DVD feature set");
+    intfield(pagestart + 4, 2, "CD audio");
+    intfield(pagestart + 6, 2, "Embedded changer");
+    intfield(pagestart + 8, 2, "Packet SMART");
+    intfield(pagestart + 10, 2, "Persistent prevent(MESN)");
+    intfield(pagestart + 12, 2, "Event status notification");
+    intfield(pagestart + 14, 2, "Digital output");
+    intfield(pagestart + 16, 2, "CD sequential recordable");
+    intfield(pagestart + 18, 2, "DVD sequential recordable");
+    intfield(pagestart + 20, 2, "Random recordable");
+    intfield(pagestart + 22, 2, "Key management");
+    intfield(pagestart + 24, 2, "Partial recorded CD media read");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+cdvd_mm_capab(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 49, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "DVD-RAM read", 1, 5);
+    bitfield(pagestart + 2, "DVD-R read", 1, 4);
+    bitfield(pagestart + 2, "DVD-ROM read", 1, 3);
+    bitfield(pagestart + 2, "Method 2", 1, 2);
+    bitfield(pagestart + 2, "CD-RW read", 1, 1);
+    bitfield(pagestart + 2, "CD-R read", 1, 0);
+    bitfield(pagestart + 3, "DVD-RAM write", 1, 5);
+    bitfield(pagestart + 3, "DVD-R write", 1, 4);
+    bitfield(pagestart + 3, "DVD-ROM write", 1, 3);
+    bitfield(pagestart + 3, "Test Write", 1, 2);
+    bitfield(pagestart + 3, "CD-RW write", 1, 1);
+    bitfield(pagestart + 3, "CD-R write", 1, 0);
+    bitfield(pagestart + 4, "BUF", 1, 7);
+    bitfield(pagestart + 4, "MultiSession", 1, 6);
+    bitfield(pagestart + 4, "Mode 2 Form 2", 1, 5);
+    bitfield(pagestart + 4, "Mode 2 Form 1", 1, 4);
+    bitfield(pagestart + 4, "Digital port (2)", 1, 3);
+    bitfield(pagestart + 4, "Digital port (1)", 1, 2);
+    bitfield(pagestart + 4, "Composite", 1, 1);
+    bitfield(pagestart + 4, "Audio play", 1, 0);
+    bitfield(pagestart + 5, "Read bar code", 1, 7);
+    bitfield(pagestart + 5, "UPC", 1, 6);
+    bitfield(pagestart + 5, "ISRC", 1, 5);
+    bitfield(pagestart + 5, "C2 pointers supported", 1, 4);
+    bitfield(pagestart + 5, "R-W de-interleaved & corrected", 1, 3);
+    bitfield(pagestart + 5, "R-W supported", 1, 2);
+    bitfield(pagestart + 5, "CD-DA stream is accurate", 1, 1);
+    bitfield(pagestart + 5, "CD-DA commands supported", 1, 0);
+    bitfield(pagestart + 6, "Loading mechanism type", 7, 5);
+    bitfield(pagestart + 6, "Eject (individual or magazine)", 1, 3);
+    bitfield(pagestart + 6, "Prevent jumper", 1, 2);
+    bitfield(pagestart + 6, "Lock state", 1, 1);
+    bitfield(pagestart + 6, "Lock", 1, 0);
+    bitfield(pagestart + 7, "R-W in lead-in", 1, 5);
+    bitfield(pagestart + 7, "Side change capable", 1, 4);
+    bitfield(pagestart + 7, "S/W slot selection", 1, 3);
+    bitfield(pagestart + 7, "Changer supports disc present", 1, 2);
+    bitfield(pagestart + 7, "Separate channel mute", 1, 1);
+    bitfield(pagestart + 7, "Separate volume levels", 1, 0);
+    intfield(pagestart + 10, 2, "number of volume level supported");
+    intfield(pagestart + 12, 2, "Buffer size supported");
+    bitfield(pagestart + 17, "Length", 3, 4);
+    bitfield(pagestart + 17, "LSBF", 1, 3);
+    bitfield(pagestart + 17, "RCK", 1, 2);
+    bitfield(pagestart + 17, "BCKF", 1, 1);
+    intfield(pagestart + 22, 2, "Copy management revision supported");
+    bitfield(pagestart + 27, "Rotation control selected", 3, 0);
+    intfield(pagestart + 28, 2, "Current write speed selected");
+    intfield(pagestart + 30, 2, "# of lu speed performance tables");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+cdvd_cache(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 2, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("-----------------------\n");
+    };
+    bitfield(pagestart + 2, "Write Cache Enabled", 1, 2);
+    bitfield(pagestart + 2, "Read Cache Disabled", 1, 0);
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+tape_data_compression(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 6, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "DCE", 1, 7);
+    bitfield(pagestart + 2, "DCC", 1, 6);
+    bitfield(pagestart + 3, "DDE", 1, 7);
+    bitfield(pagestart + 3, "RED", 3, 5);
+    intfield(pagestart + 4, 4, "Compression algorithm");
+    intfield(pagestart + 8, 4, "Decompression algorithm");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+tape_dev_config(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 25, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "CAF", 1, 5);
+    bitfield(pagestart + 2, "Active format", 0x1f, 0);
+    intfield(pagestart + 3, 1, "Active partition");
+    intfield(pagestart + 4, 1, "Write object cbuffer full ratio");
+    intfield(pagestart + 5, 1, "Read object cbuffer full ratio");
+    intfield(pagestart + 6, 2, "Wire delay time");
+    bitfield(pagestart + 8, "OBR", 1, 7);
+    bitfield(pagestart + 8, "LOIS", 1, 6);
+    bitfield(pagestart + 8, "RSMK", 1, 5);
+    bitfield(pagestart + 8, "AVC", 1, 4);
+    bitfield(pagestart + 8, "SOCF", 3, 2);
+    bitfield(pagestart + 8, "ROBO", 1, 1);
+    bitfield(pagestart + 8, "REW", 1, 0);
+    intfield(pagestart + 9, 1, "Gap size");
+    bitfield(pagestart + 10, "EOD defined", 7, 5);
+    bitfield(pagestart + 10, "EEG", 1, 4);
+    bitfield(pagestart + 10, "SEW", 1, 3);
+    bitfield(pagestart + 10, "SWP", 1, 2);
+    bitfield(pagestart + 10, "BAML", 1, 1);
+    bitfield(pagestart + 10, "BAM", 1, 0);
+    intfield(pagestart + 11, 3, "Object cbuffer size at early warning");
+    intfield(pagestart + 14, 1, "Select data compression algorithm");
+    bitfield(pagestart + 15, "ASOCWP", 1, 2);
+    bitfield(pagestart + 15, "PERSWO", 1, 1);
+    bitfield(pagestart + 15, "PRMWP", 1, 0);
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+tape_medium_part1(struct mpage_info * mpi, const char * prefix)
+{
+    int status, off, len;
+    uint8_t *pagestart;
+
+    /* variable length mode page, need to know its response length */
+    status = get_mode_page(mpi, 0, cbuffer);
+    if (status)
+        return status;
+    off = modePageOffset(cbuffer, mpi->resp_len, mode6byte);
+    if (off < 0)
+        return off;
+    len = mpi->resp_len - off;
+
+    status = setup_mode_page(mpi, 12 + ((len - 10) / 2), cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    intfield(pagestart + 2, 1, "Maximum additional partitions");
+    intfield(pagestart + 3, 1, "Additional partitions defined");
+    bitfield(pagestart + 4, "FDP", 1, 7);
+    bitfield(pagestart + 4, "SDP", 1, 6);
+    bitfield(pagestart + 4, "IDP", 1, 5);
+    bitfield(pagestart + 4, "PSUM", 3, 3);
+    bitfield(pagestart + 4, "POFM", 1, 2);
+    bitfield(pagestart + 4, "CLEAR", 1, 1);
+    bitfield(pagestart + 4, "ADDP", 1, 0);
+    intfield(pagestart + 5, 1, "Medium format recognition");
+    bitfield(pagestart + 6, "Partition units", 0xf, 0);
+    intfield(pagestart + 8, 2, "Partition size");
+
+    for (off = 10; off < len; off += 2)
+        intfield(pagestart + off, 2, "Partition size");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+tape_medium_part2_4(struct mpage_info * mpi, const char * prefix)
+{
+    int status, off, len;
+    uint8_t *pagestart;
+
+    /* variable length mode page, need to know its response length */
+    status = get_mode_page(mpi, 0, cbuffer);
+    if (status)
+        return status;
+    off = modePageOffset(cbuffer, mpi->resp_len, mode6byte);
+    if (off < 0)
+        return off;
+    len = mpi->resp_len - off;
+
+    status = setup_mode_page(mpi, 1 + ((len - 4) / 2), cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    intfield(pagestart + 2, 2, "Partition size");
+
+    for (off = 4; off < len; off += 2)
+        intfield(pagestart + off, 2, "Partition size");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+ses_services_manag(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 2, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", get_page_name(mpi), mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    bitfield(pagestart + 5, "ENBLTC", 1, 0);
+    intfield(pagestart + 6, 2, "Maximum time to completion (100 ms units)");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+fcp_proto_spec_lu(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 1, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", "Fibre Channel logical unit",
+               mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    bitfield(pagestart + 3, "EPDC", 1, 0);
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+sas_proto_spec_lu(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 1, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", "SAS logical unit", mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "Transport Layer Retries", 1, 4);
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+common_proto_spec_lu(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    int proto_id = 0;
+
+    status = get_protocol_id(0, cbuffer, &proto_id, NULL);
+    if (status)
+        return status;
+    if (0 == proto_id)
+        return fcp_proto_spec_lu(mpi, prefix);
+    else if (6 == proto_id)
+        return sas_proto_spec_lu(mpi, prefix);
+    else
+        return DECODE_FAILED_TRY_HEX;
+}
+
+static int
+fcp_proto_spec_port(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 10, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", "Fibre Channel port control",
+               mpi->page);
+        printf("----------------------------------------------------\n");
+    }
+    bitfield(pagestart + 3, "DTFD", 1, 7);
+    bitfield(pagestart + 3, "PLPB", 1, 6);
+    bitfield(pagestart + 3, "DDIS", 1, 5);
+    bitfield(pagestart + 3, "DLM", 1, 4);
+    bitfield(pagestart + 3, "RHA", 1, 3);
+    bitfield(pagestart + 3, "ALWI", 1, 2);
+    bitfield(pagestart + 3, "DTIPE", 1, 1);
+    bitfield(pagestart + 3, "DTOLI", 1, 0);
+    bitfield(pagestart + 6, "RR_TOV units", 7, 0);
+    intfield(pagestart + 7, 1, "Resource recovery time-out");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+spi4_proto_spec_port(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 1, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", "SPI-4 port control", mpi->page);
+        printf("-----------------------------------\n");
+    }
+    intfield(pagestart + 4, 2, "Synchronous transfer time-out");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+/* Protocol specific mode page for SAS, short format (subpage 0) */
+static int
+sas_proto_spec_port(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 3, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode page (0x%x)\n", "SAS SSP port control", mpi->page);
+        printf("-------------------------------------\n");
+    }
+    bitfield(pagestart + 2, "Ready LED meaning", 0x1, 4);
+    intfield(pagestart + 4, 2, "I_T Nexus Loss time");
+    intfield(pagestart + 6, 2, "Initiator response time-out");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+common_proto_spec_port(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    int proto_id = 0;
+
+    status = get_protocol_id(1, cbuffer, &proto_id, NULL);
+    if (status)
+        return status;
+    if (0 == proto_id)
+        return fcp_proto_spec_port(mpi, prefix);
+    else if (1 == proto_id)
+        return spi4_proto_spec_port(mpi, prefix);
+    else if (6 == proto_id)
+        return sas_proto_spec_port(mpi, prefix);
+    else
+        return DECODE_FAILED_TRY_HEX;
+}
+
+static int
+spi4_margin_control(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 5, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode subpage (0x%x,0x%x)\n", "SPI-4 Margin control",
+               mpi->page, mpi->subpage);
+        printf("--------------------------------------------\n");
+    }
+    bitfield(pagestart + 5, "Protocol identifier", 0xf, 0);
+    bitfield(pagestart + 7, "Driver Strength", 0xf, 4);
+    bitfield(pagestart + 8, "Driver Asymmetry", 0xf, 4);
+    bitfield(pagestart + 8, "Driver Precompensation", 0xf, 0);
+    bitfield(pagestart + 9, "Driver Slew rate", 0xf, 4);
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+/* Protocol specific mode page for SAS, phy control + discover (subpage 1) */
+static int
+sas_phy_control_discover(struct mpage_info * mpi, const char * prefix)
+{
+    int status, off, num_phys, k;
+    uint8_t *pagestart;
+    uint8_t *p;
+
+   /* variable length mode page, need to know its response length */
+    status = get_mode_page(mpi, 0, cbuffer);
+    if (status)
+        return status;
+    off = modePageOffset(cbuffer, mpi->resp_len, mode6byte);
+    if (off < 0)
+        return off;
+    num_phys = cbuffer[off + 7];
+
+    status = setup_mode_page(mpi,  1 + (16 * num_phys), cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode subpage (0x%x,0x%x)\n", "SAS Phy Control and "
+               "Discover", mpi->page, mpi->subpage);
+        printf("--------------------------------------------\n");
+    }
+    intfield(pagestart + 7, 1, "Number of phys");
+    for (k = 0, p = pagestart + 8; k < num_phys; ++k, p += 48) {
+        intfield(p + 1, 1, "Phy Identifier");
+        bitfield(p + 4, "Attached Device type", 0x7, 4);
+        bitfield(p + 5, "Negotiated Logical Link rate", 0xf, 0);
+        bitfield(p + 6, "Attached SSP Initiator port", 0x1, 3);
+        bitfield(p + 6, "Attached STP Initiator port", 0x1, 2);
+        bitfield(p + 6, "Attached SMP Initiator port", 0x1, 1);
+        bitfield(p + 7, "Attached SSP Target port", 0x1, 3);
+        bitfield(p + 7, "Attached STP Target port", 0x1, 2);
+        bitfield(p + 7, "Attached SMP Target port", 0x1, 1);
+        hexdatafield(p + 8, 8, "SAS address");
+        hexdatafield(p + 16, 8, "Attached SAS address");
+        intfield(p + 24, 1, "Attached Phy identifier");
+        bitfield(p + 32, "Programmed Min Physical Link rate", 0xf, 4);
+        bitfield(p + 32, "Hardware Min Physical Link rate", 0xf, 0);
+        bitfield(p + 33, "Programmed Max Physical Link rate", 0xf, 4);
+        bitfield(p + 33, "Hardware Max Physical Link rate", 0xf, 0);
+    }
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+
+static int
+common_proto_spec_port_sp1(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    int proto_id = 0;
+
+    status = get_protocol_id(1, cbuffer, &proto_id, NULL);
+    if (status)
+        return status;
+    if (1 == proto_id)
+        return spi4_margin_control(mpi, prefix);
+    else if (6 == proto_id)
+        return sas_phy_control_discover(mpi, prefix);
+    else
+        return DECODE_FAILED_TRY_HEX;
+}
+
+static int
+spi4_training_config(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 27, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode subpage (0x%x,0x%x)\n", "training configuration",
+               mpi->page, mpi->subpage);
+        printf("----------------------------------------------------------\n");
+    }
+    hexdatafield(pagestart + 10, 4, "DB(0) value");
+    hexdatafield(pagestart + 14, 4, "DB(1) value");
+    hexdatafield(pagestart + 18, 4, "DB(2) value");
+    hexdatafield(pagestart + 22, 4, "DB(3) value");
+    hexdatafield(pagestart + 26, 4, "DB(4) value");
+    hexdatafield(pagestart + 30, 4, "DB(5) value");
+    hexdatafield(pagestart + 34, 4, "DB(6) value");
+    hexdatafield(pagestart + 38, 4, "DB(7) value");
+    hexdatafield(pagestart + 42, 4, "DB(8) value");
+    hexdatafield(pagestart + 46, 4, "DB(9) value");
+    hexdatafield(pagestart + 50, 4, "DB(10) value");
+    hexdatafield(pagestart + 54, 4, "DB(11) value");
+    hexdatafield(pagestart + 58, 4, "DB(12) value");
+    hexdatafield(pagestart + 62, 4, "DB(13) value");
+    hexdatafield(pagestart + 66, 4, "DB(14) value");
+    hexdatafield(pagestart + 70, 4, "DB(15) value");
+    hexdatafield(pagestart + 74, 4, "P_CRCA value");
+    hexdatafield(pagestart + 78, 4, "P1 value");
+    hexdatafield(pagestart + 82, 4, "BSY value");
+    hexdatafield(pagestart + 86, 4, "SEL value");
+    hexdatafield(pagestart + 90, 4, "RST value");
+    hexdatafield(pagestart + 94, 4, "REQ value");
+    hexdatafield(pagestart + 98, 4, "ACK value");
+    hexdatafield(pagestart + 102, 4, "ATN value");
+    hexdatafield(pagestart + 106, 4, "C/D value");
+    hexdatafield(pagestart + 110, 4, "I/O value");
+    hexdatafield(pagestart + 114, 4, "MSG value");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+/* SAS(2) SSP, shared protocol specific port mode subpage (subpage 2) */
+static int
+sas_shared_spec_port(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 1, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode subpage (0x%x,0x%x)\n", "SAS SSP shared protocol "
+               "specific port", mpi->page, mpi->subpage);
+        printf("-----------------------------------------------------\n");
+    }
+    intfield(pagestart + 6, 2, "Power loss timeout(ms)");
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+common_proto_spec_port_sp2(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    int proto_id = 0;
+
+    status = get_protocol_id(1, cbuffer, &proto_id, NULL);
+    if (status)
+        return status;
+    if (1 == proto_id)
+        return spi4_training_config(mpi, prefix);
+    else if (6 == proto_id)
+        return sas_shared_spec_port(mpi, prefix);
+    else
+        return DECODE_FAILED_TRY_HEX;
+}
+
+static int
+spi4_negotiated(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 7, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode subpage (0x%x,0x%x)\n", get_page_name(mpi), mpi->page,
+               mpi->subpage);
+        printf("--------------------------------------------\n");
+    }
+    intfield(pagestart + 6, 1, "Transfer period");
+    intfield(pagestart + 8, 1, "REQ/ACK offset");
+    intfield(pagestart + 9, 1, "Transfer width exponent");
+    bitfield(pagestart + 10, "Protocol option bits", 0x7f, 0);
+    bitfield(pagestart + 11, "Transceiver mode", 3, 2);
+    bitfield(pagestart + 11, "Sent PCOMP_EN", 1, 1);
+    bitfield(pagestart + 11, "Received PCOMP_EN", 1, 0);
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static int
+spi4_report_xfer(struct mpage_info * mpi, const char * prefix)
+{
+    int status;
+    uint8_t *pagestart;
+
+    status = setup_mode_page(mpi, 4, cbuffer, &pagestart);
+    if (status)
+        return status;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (!x_interface && !replace) {
+        printf("%s mode subpage (0x%x,0x%x)\n", get_page_name(mpi), mpi->page,
+               mpi->subpage);
+        printf("--------------------------------------------\n");
+    }
+    intfield(pagestart + 6, 1, "Minimum transfer period factor");
+    intfield(pagestart + 8, 1, "Maximum REQ/ACK offset");
+    intfield(pagestart + 9, 1, "Maximum transfer width exponent");
+    bitfield(pagestart + 10, "Protocol option bits supported", 0xff, 0);
+
+    if (x_interface && replace)
+        return put_mode_page(mpi, cbuffer);
+    else
+        printf("\n");
+    return 0;
+}
+
+static void
+print_hex_page(struct mpage_info * mpi, const char * prefix,
+               uint8_t *pagestart, int off, int len)
+{
+    int k;
+    const char * pg_name;
+
+    if (prefix[0])
+        printf("%s", prefix);
+    if (! x_interface) {
+        pg_name = get_page_name(mpi);
+        if (mpi->subpage) {
+            if (pg_name && (unkn_page_str != pg_name))
+                printf("mode page: 0x%02x  subpage: 0x%02x   [%s]\n",
+                       mpi->page, mpi->subpage, pg_name);
+            else
+                printf("mode page: 0x%02x  subpage: 0x%02x\n", mpi->page,
+                   mpi->subpage);
+            printf("------------------------------\n");
+        } else {
+            if (pg_name && (unkn_page_str != pg_name))
+                printf("mode page: 0x%02x   [%s]\n", mpi->page,
+                       pg_name);
+            else
+                printf("mode page: 0x%02x\n", mpi->page);
+            printf("---------------\n");
+        }
+    }
+    for (k = off; k < len; k++)
+    {
+        char nm[8];
+
+        snprintf(nm, sizeof(nm), "0x%02x", (unsigned char)k);
+        hexdatafield(pagestart + k, 1, nm);
+    }
+    printf("\n");
+}
+
+static int
+do_user_page(struct mpage_info * mpi, int decode_in_hex)
+{
+    int status = 0;
+    int len, off, res, done;
+    int offset = 0;
+    uint8_t *pagestart;
+    char prefix[96];
+    struct mpage_info local_mp_i;
+    struct mpage_name_func * mpf;
+    int multiple = ((MP_LIST_PAGES == mpi->page) ||
+                    (MP_LIST_SUBPAGES == mpi->subpage));
+
+    if (replace && multiple) {
+        printf("Can't list all (sub)pages and use replace (-R) together\n");
+        return 1;
+    }
+    status = get_mode_page(mpi, 0, cbuffer2);
+    if (status) {
+        printf("\n");
+        return status;
+    } else {
+        offset = modePageOffset(cbuffer2, mpi->resp_len, mode6byte);
+        if (offset < 0) {
+            fprintf(stdout, "mode page=0x%x has bad page format\n",
+                    mpi->page);
+            fprintf(stdout, "   perhaps '-z' switch may help\n");
+            return -1;
+        }
+        pagestart = cbuffer2 + offset;
+    }
+
+    memset(&local_mp_i, 0, sizeof(local_mp_i));
+    local_mp_i.page_control = mpi->page_control;
+    local_mp_i.peri_type = mpi->peri_type;
+    local_mp_i.inq_byte6 = mpi->inq_byte6;
+    local_mp_i.resp_len = mpi->resp_len;
+
+    do {
+        local_mp_i.page = (pagestart[0] & 0x3f);
+        local_mp_i.subpage = (pagestart[0] & 0x40) ? pagestart[1] : 0;
+        if(0 == local_mp_i.page) { /* page==0 vendor (unknown) format */
+            off = 0;
+            len = mpi->resp_len - offset;  /* should be last listed page */
+        } else if (local_mp_i.subpage) {
+            off = 4;
+            len = (pagestart[2] << 8) + pagestart[3] + 4;
+        } else {
+            off = 2;
+            len = pagestart[1] + 2;
+        }
+
+        prefix[0] = '\0';
+        done = 0;
+        if ((! decode_in_hex) && ((mpf = get_mpage_name_func(&local_mp_i))) &&
+            mpf->func) {
+            if (multiple && x_interface && !replace) {
+                if (local_mp_i.subpage)
+                    snprintf(prefix, sizeof(prefix), "sginfo -t 0x%x,0x%x"
+                             " -XR %s ", local_mp_i.page, local_mp_i.subpage,
+                             device_name);
+                else
+                    snprintf(prefix, sizeof(prefix), "sginfo -t 0x%x -XR %s ",
+                             local_mp_i.page, device_name);
+            }
+            res = mpf->func(&local_mp_i, prefix);
+            if (DECODE_FAILED_TRY_HEX != res) {
+                done = 1;
+                status |= res;
+            }
+        }
+        if (! done) {
+            if (x_interface && replace)
+                return put_mode_page(&local_mp_i, cbuffer2);
+            else {
+                if (multiple && x_interface && !replace) {
+                    if (local_mp_i.subpage)
+                        snprintf(prefix, sizeof(prefix), "sginfo -u 0x%x,0x%x"
+                                 " -XR %s ", local_mp_i.page,
+                                 local_mp_i.subpage, device_name);
+                    else
+                        snprintf(prefix, sizeof(prefix), "sginfo -u 0x%x -XR "
+                                 "%s ", local_mp_i.page, device_name);
+                }
+                print_hex_page(&local_mp_i, prefix, pagestart, off, len);
+            }
+        }
+        offset += len;
+        pagestart = cbuffer2 + offset;
+    } while (multiple && (offset < mpi->resp_len));
+    return status;
+}
+
+static void
+inqfieldname(uint8_t *deststr, const uint8_t *srcbuf, int maxlen)
+{
+        int i;
+
+        memset(deststr, '\0', MAX_INQFIELD_LEN);
+        for (i = maxlen - 1; i >= 0 && isspace(srcbuf[i]); --i)
+                ;
+        memcpy(deststr, srcbuf, i + 1);
+}
+
+static int
+do_inquiry(int * peri_type, int * resp_byte6, int inquiry_verbosity)
+{
+    int status;
+    uint8_t cmd[6];
+    uint8_t fieldname[MAX_INQFIELD_LEN];
+    uint8_t *pagestart;
+    struct scsi_cmnd_io sci;
+
+    memset(cbuffer, 0, INQUIRY_RESP_INITIAL_LEN);
+    cbuffer[0] = 0x7f;
+
+    cmd[0] = 0x12;              /* INQUIRY */
+    cmd[1] = 0x00;              /* evpd=0 */
+    cmd[2] = 0x00;              /* page code = 0 */
+    cmd[3] = 0x00;              /* (reserved) */
+    cmd[4] = INQUIRY_RESP_INITIAL_LEN;      /* allocation length */
+    cmd[5] = 0x00;              /* control */
+
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_FROM_DEVICE;
+    sci.dxfer_len = INQUIRY_RESP_INITIAL_LEN;
+    sci.dxferp = cbuffer;
+    status = do_scsi_io(&sci);
+    if (status) {
+        printf("Error doing INQUIRY (1)\n");
+        return status;
+    }
+    if (trace_cmd > 1) {
+        printf("  inquiry response:\n");
+        dump(cbuffer, INQUIRY_RESP_INITIAL_LEN);
+    }
+    pagestart = cbuffer;
+    if (peri_type)
+        *peri_type = pagestart[0] & 0x1f;
+    if (resp_byte6)
+        *resp_byte6 = pagestart[6];
+    if (0 == inquiry_verbosity)
+        return 0;
+    if ((pagestart[4] + 5) < INQUIRY_RESP_INITIAL_LEN) {
+        printf("INQUIRY response too short: expected 36 bytes, got %d\n",
+               pagestart[4] + 5);
+        return -EINVAL;
+    }
+
+    if (!x_interface && !replace) {
+        printf("INQUIRY response (cmd: 0x12)\n");
+        printf("----------------------------\n");
+    };
+    bitfield(pagestart + 0, "Device Type", 0x1f, 0);
+    if (2 == inquiry_verbosity) {
+        bitfield(pagestart + 0, "Peripheral Qualifier", 0x7, 5);
+        bitfield(pagestart + 1, "Removable", 1, 7);
+        bitfield(pagestart + 2, "Version", 0xff, 0);
+        bitfield(pagestart + 3, "NormACA", 1, 5);
+        bitfield(pagestart + 3, "HiSup", 1, 4);
+        bitfield(pagestart + 3, "Response Data Format", 0xf, 0);
+        bitfield(pagestart + 5, "SCCS", 1, 7);
+        bitfield(pagestart + 5, "ACC", 1, 6);
+        bitfield(pagestart + 5, "ALUA", 3, 4);
+        bitfield(pagestart + 5, "3PC", 1, 3);
+        bitfield(pagestart + 5, "Protect", 1, 0);
+        bitfield(pagestart + 6, "BQue", 1, 7);
+        bitfield(pagestart + 6, "EncServ", 1, 6);
+        bitfield(pagestart + 6, "MultiP", 1, 4);
+        bitfield(pagestart + 6, "MChngr", 1, 3);
+        bitfield(pagestart + 6, "Addr16", 1, 0);
+        bitfield(pagestart + 7, "Relative Address", 1, 7);
+        bitfield(pagestart + 7, "Wide bus 16", 1, 5);
+        bitfield(pagestart + 7, "Synchronous neg.", 1, 4);
+        bitfield(pagestart + 7, "Linked Commands", 1, 3);
+        bitfield(pagestart + 7, "Command Queueing", 1, 1);
+    }
+    if (x_interface)
+        printf("\n");
+
+    inqfieldname(fieldname, pagestart + 8, 8);
+    printf("%s%s\n", (!x_interface ? "Vendor:                    " : ""),
+           fieldname);
+
+    inqfieldname(fieldname, pagestart + 16, 16);
+    printf("%s%s\n", (!x_interface ? "Product:                   " : ""),
+           fieldname);
+
+    inqfieldname(fieldname, pagestart + 32, 4);
+    printf("%s%s\n", (!x_interface ? "Revision level:            " : ""),
+           fieldname);
+
+    printf("\n");
+    return status;
+
+}
+
+static int
+do_serial_number(void)
+{
+    int status, pagelen;
+    uint8_t cmd[6];
+    uint8_t *pagestart;
+    struct scsi_cmnd_io sci;
+    const uint8_t serial_vpd = 0x80;
+    const uint8_t supported_vpd = 0x0;
+
+    /* check supported VPD pages + unit serial number well formed */
+    cmd[0] = 0x12;              /* INQUIRY */
+    cmd[1] = 0x01;              /* evpd=1 */
+    cmd[2] = supported_vpd;
+    cmd[3] = 0x00;              /* (reserved) */
+    cmd[4] = 0x04;              /* allocation length */
+    cmd[5] = 0x00;              /* control */
+
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_FROM_DEVICE;
+    sci.dxfer_len = 4;
+    sci.dxferp = cbuffer;
+    status = do_scsi_io(&sci);
+    if (status) {
+        printf("No serial number (error doing INQUIRY, supported VPDs)\n\n");
+        return status;
+    }
+    if (! ((supported_vpd == cbuffer[1]) && (0 == cbuffer[2]))) {
+        printf("No serial number (bad format for supported VPDs)\n\n");
+        return -1;
+    }
+
+    cmd[0] = 0x12;              /* INQUIRY */
+    cmd[1] = 0x01;              /* evpd=1 */
+    cmd[2] = serial_vpd;
+    cmd[3] = 0x00;              /* (reserved) */
+    cmd[4] = 0x04;              /* allocation length */
+    cmd[5] = 0x00;              /* control */
+
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_FROM_DEVICE;
+    sci.dxfer_len = 4;
+    sci.dxferp = cbuffer;
+    status = do_scsi_io(&sci);
+    if (status) {
+        printf("No serial number (error doing INQUIRY, serial number)\n\n");
+        return status;
+    }
+    if (! ((serial_vpd == cbuffer[1]) && (0 == cbuffer[2]))) {
+        printf("No serial number (bad format for serial number)\n\n");
+        return -1;
+    }
+
+    pagestart = cbuffer;
+
+    pagelen = 4 + pagestart[3];
+
+    cmd[0] = 0x12;              /* INQUIRY */
+    cmd[1] = 0x01;              /* evpd=1 */
+    cmd[2] = serial_vpd;
+    cmd[3] = 0x00;              /* (reserved) */
+    cmd[4] = (uint8_t)pagelen; /* allocation length */
+    cmd[5] = 0x00;              /* control */
+
+    sci.cmnd = cmd;
+    sci.cmnd_len = sizeof(cmd);
+    sci.dxfer_dir = DXFER_FROM_DEVICE;
+    sci.dxfer_len = pagelen;
+    sci.dxferp = cbuffer;
+    status = do_scsi_io(&sci);
+    if (status) {
+        printf("No serial number (error doing INQUIRY, serial number)\n\n");
+        return status;
+    }
+    if (trace_cmd > 1) {
+        printf("  inquiry (vpd page 0x80) response:\n");
+        dump(cbuffer, pagelen);
+    }
+
+    pagestart[pagestart[3] + 4] = '\0';
+    printf("Serial Number '%s'\n\n", pagestart + 4);
+    return status;
+}
+
+
+typedef struct sg_map {
+    int bus;
+    int channel;
+    int target_id;
+    int lun;
+    char * dev_name;
+} Sg_map;
+
+typedef struct my_scsi_idlun
+{
+    int mux4;
+    int host_unique_id;
+
+} My_scsi_idlun;
+
+#define MDEV_NAME_SZ 256
+
+static void
+make_dev_name(char * fname, int k, int do_numeric)
+{
+    char buff[MDEV_NAME_SZ];
+    size_t len;
+
+    strncpy(fname, "/dev/sg", MDEV_NAME_SZ);
+    fname[MDEV_NAME_SZ - 1] = '\0';
+    len = strlen(fname);
+    if (do_numeric)
+        snprintf(fname + len, MDEV_NAME_SZ - len, "%d", k);
+    else {
+        if (k <= 26) {
+            buff[0] = 'a' + (char)k;
+            buff[1] = '\0';
+            strcat(fname, buff);
+        }
+        else
+            strcat(fname, "xxxx");
+    }
+}
+
+
+static Sg_map sg_map_arr[MAX_SG_DEVS + 1];
+
+#define MAX_HOLES 4
+
+/* Print out a list of the known devices on the system */
+static void
+show_devices(int raw)
+{
+    int k, j, fd, err, bus, n;
+    My_scsi_idlun m_idlun;
+    char name[MDEV_NAME_SZ];
+    char dev_name[MDEV_NAME_SZ + 6];
+    char ebuff[EBUFF_SZ];
+    int do_numeric = 1;
+    int max_holes = MAX_HOLES;
+    DIR *dir_ptr;
+    struct dirent *entry;
+    char *tmpptr;
+
+    dir_ptr=opendir("/dev");
+    if ( dir_ptr == NULL ) {
+        perror("/dev");
+        exit(1);
+    }
+
+    j=0;
+    while ( (entry=readdir(dir_ptr)) != NULL ) {
+        switch(entry->d_type) {
+        case DT_LNK:
+        case DT_CHR:
+        case DT_BLK:
+                break;
+        default:
+                continue;
+        }
+
+        switch(entry->d_name[0]) {
+        case 's':
+        case 'n':
+                break;
+        default:
+                continue;
+        }
+
+        if ( strncmp("sg",entry->d_name,2) == 0 ) {
+                continue;
+        }
+        if ( strncmp("sd",entry->d_name,2) == 0 ) {
+            continue;
+        }
+        if ( isdigit(entry->d_name[strlen(entry->d_name)-1]) ) {
+            continue;
+        }
+        if ( strncmp("snapshot",entry->d_name,8) == 0 ) {
+                continue;
+        }
+
+        snprintf(dev_name, sizeof(dev_name),"/dev/%s",entry->d_name);
+
+        fd = open(dev_name, O_RDONLY | O_NONBLOCK);
+        if (fd < 0)
+            continue;
+        err = ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, &(sg_map_arr[j].bus));
+        if (err < 0) {
+#if 0
+            snprintf(ebuff, EBUFF_SZ,
+                     "SCSI(1) ioctl on %s failed", dev_name);
+            perror(ebuff);
+#endif
+            close(fd);
+            continue;
+        }
+        err = ioctl(fd, SCSI_IOCTL_GET_IDLUN, &m_idlun);
+        if (err < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                     "SCSI(2) ioctl on %s failed", dev_name);
+            perror(ebuff);
+            close(fd);
+            continue;
+        }
+        sg_map_arr[j].channel = (m_idlun.mux4 >> 16) & 0xff;
+        sg_map_arr[j].lun = (m_idlun.mux4 >> 8) & 0xff;
+        sg_map_arr[j].target_id = m_idlun.mux4 & 0xff;
+        n = strlen(dev_name);
+        /* memory leak ... no free()s for this malloc() */
+        tmpptr = (char *)malloc(n + 1);
+        snprintf(tmpptr, n + 1, "%.*s", n, dev_name);
+        /* strncpy(tmpptr,dev_name,strlen(dev_name)+1); */
+
+        sg_map_arr[j].dev_name = tmpptr;
+#if 0
+        printf("[scsi%d ch=%d id=%d lun=%d %s] ", sg_map_arr[j].bus,
+        sg_map_arr[j].channel, sg_map_arr[j].target_id, sg_map_arr[j].lun,
+        sg_map_arr[j].dev_name);
+#endif
+        printf("%s ", dev_name);
+        close(fd);
+        if (++j >= MAX_SG_DEVS)
+            break;
+    }
+    closedir(dir_ptr);
+
+    printf("\n"); /* <<<<<<<<<<<<<<<<<<<<< */
+    for (k = 0; k < MAX_SG_DEVS; k++) {
+        if ( raw ) {
+                sprintf(name,"/dev/raw/raw%d",k);
+                fd = open(name, O_RDWR | O_NONBLOCK);
+                if (fd < 0) {
+                        continue;
+                }
+        }
+        else {
+                make_dev_name(name, k, do_numeric);
+                fd = open(name, O_RDWR | O_NONBLOCK);
+        if (fd < 0) {
+            if ((ENOENT == errno) && (0 == k)) {
+                do_numeric = 0;
+                make_dev_name(name, k, do_numeric);
+                fd = open(name, O_RDWR | O_NONBLOCK);
+            }
+            if (fd < 0) {
+                if (EBUSY == errno)
+                    continue;   /* step over if O_EXCL already on it */
+                else {
+#if 0
+                    snprintf(ebuff, EBUFF_SZ,
+                             "open on %s failed (%d)", name, errno);
+                    perror(ebuff);
+#endif
+                    if (max_holes-- > 0)
+                        continue;
+                    else
+                        break;
+                }
+            }
+        }
+        }
+        max_holes = MAX_HOLES;
+        err = ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, &bus);
+        if (err < 0) {
+            if ( ! raw ) {
+                snprintf(ebuff, EBUFF_SZ, "SCSI(3) ioctl on %s failed", name);
+                perror(ebuff);
+            }
+            close(fd);
+            continue;
+        }
+        err = ioctl(fd, SCSI_IOCTL_GET_IDLUN, &m_idlun);
+        if (err < 0) {
+            if ( ! raw ) {
+                snprintf(ebuff, EBUFF_SZ, "SCSI(3) ioctl on %s failed", name);
+                perror(ebuff);
+            }
+            close(fd);
+            continue;
+        }
+#if 0
+        printf("[scsi%d ch=%d id=%d lun=%d %s]", bus,
+               (m_idlun.mux4 >> 16) & 0xff, m_idlun.mux4 & 0xff,
+               (m_idlun.mux4 >> 8) & 0xff, name);
+#endif
+        for (j = 0; sg_map_arr[j].dev_name; ++j) {
+            if ((bus == sg_map_arr[j].bus) &&
+                ((m_idlun.mux4 & 0xff) == sg_map_arr[j].target_id) &&
+                (((m_idlun.mux4 >> 16) & 0xff) == sg_map_arr[j].channel) &&
+                (((m_idlun.mux4 >> 8) & 0xff) == sg_map_arr[j].lun)) {
+                printf("%s [=%s  scsi%d ch=%d id=%d lun=%d]\n", name,
+                       sg_map_arr[j].dev_name, bus,
+                       ((m_idlun.mux4 >> 16) & 0xff), m_idlun.mux4 & 0xff,
+                       ((m_idlun.mux4 >> 8) & 0xff));
+                break;
+            }
+        }
+        if (NULL == sg_map_arr[j].dev_name)
+            printf("%s [scsi%d ch=%d id=%d lun=%d]\n", name, bus,
+                   ((m_idlun.mux4 >> 16) & 0xff), m_idlun.mux4 & 0xff,
+                   ((m_idlun.mux4 >> 8) & 0xff));
+        close(fd);
+    }
+    printf("\n");
+}
+
+#define DEVNAME_SZ 256
+
+static int
+open_sg_io_dev(char * devname)
+{
+    int fd, fdrw, err, bus, bbus, k, v;
+    My_scsi_idlun m_idlun, mm_idlun;
+    int do_numeric = 1;
+    char name[DEVNAME_SZ];
+    struct stat a_st;
+    int block_dev = 0;
+
+    strncpy(name, devname, DEVNAME_SZ);
+    name[DEVNAME_SZ - 1] = '\0';
+    fd = open(name, O_RDONLY | O_NONBLOCK);
+    if (fd < 0)
+        return fd;
+    if ((ioctl(fd, SG_GET_VERSION_NUM, &v) >= 0) && (v >= 30000)) {
+        fdrw = open(name, O_RDWR | O_NONBLOCK);
+        if (fdrw >= 0) {
+            close(fd);
+            return fdrw;
+        }
+        return fd;
+    }
+    if (fstat(fd, &a_st) < 0) {
+        fprintf(stderr, "could do fstat() on fd ??\n");
+        close(fd);
+        return -9999;
+    }
+    if (S_ISBLK(a_st.st_mode))
+        block_dev = 1;
+
+    if (block_dev || (ioctl(fd, SG_GET_TIMEOUT, 0) < 0)) {
+        err = ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, &bus);
+        if (err < 0) {
+            fprintf(stderr, "A device name that understands SCSI commands "
+                    "is required\n");
+            close(fd);
+            return -9999;
+        }
+        err = ioctl(fd, SCSI_IOCTL_GET_IDLUN, &m_idlun);
+        if (err < 0) {
+            fprintf(stderr, "A SCSI device name is required(2)\n");
+            close(fd);
+            return -9999;
+        }
+        close(fd);
+
+        for (k = 0; k < MAX_SG_DEVS; k++) {
+            make_dev_name(name, k, do_numeric);
+            fd = open(name, O_RDWR | O_NONBLOCK);
+            if (fd < 0) {
+                if ((ENOENT == errno) && (0 == k)) {
+                    do_numeric = 0;
+                    make_dev_name(name, k, do_numeric);
+                    fd = open(name, O_RDWR | O_NONBLOCK);
+                }
+                if (fd < 0) {
+                    if (EBUSY == errno)
+                        continue;   /* step over if O_EXCL already on it */
+                    else
+                        break;
+                }
+            }
+            err = ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, &bbus);
+            if (err < 0) {
+                perror("sg ioctl failed");
+                close(fd);
+                fd = -9999;
+            }
+            err = ioctl(fd, SCSI_IOCTL_GET_IDLUN, &mm_idlun);
+            if (err < 0) {
+                perror("sg ioctl failed");
+                close(fd);
+                fd = -9999;
+            }
+            if ((bus == bbus) &&
+                ((m_idlun.mux4 & 0xff) == (mm_idlun.mux4 & 0xff)) &&
+                (((m_idlun.mux4 >> 8) & 0xff) ==
+                                        ((mm_idlun.mux4 >> 8) & 0xff)) &&
+                (((m_idlun.mux4 >> 16) & 0xff) ==
+                                        ((mm_idlun.mux4 >> 16) & 0xff)))
+                break;
+            else {
+                close(fd);
+                fd = -9999;
+            }
+        }
+    }
+    if (fd >= 0) {
+        if ((ioctl(fd, SG_GET_VERSION_NUM, &v) < 0) || (v < 30000)) {
+            fprintf(stderr, "requires lk 2.4 (sg driver), lk 2.6 or lk 3 "
+                    "series\n");
+            close(fd);
+            return -9999;
+        }
+        close(fd);
+        return open(name, O_RDWR | O_NONBLOCK);
+    }
+    else
+        return fd;
+}
+
+static void
+usage(const char *errtext)
+{
+    if (errtext)
+        fprintf(stderr, "Error: sginfo: %s\n", errtext);
+    fprintf(stderr, "Usage: sginfo [-options] [device] "
+            "[replacement_values]\n");
+    fputs("\tAllowed options are:\n"
+          "\t-6    Do 6 byte mode sense and select commands (def: 10 "
+          "bytes).\n"
+          "\t-a    Display inquiry info, serial # and all mode pages.\n"
+          "\t-A    Similar to '-a' but displays all subpages as well.\n"
+          "\t-c    Access Caching Page.\n"
+          "\t-C    Access Control Mode Page.\n"
+          "\t-d    Display defect lists (default format: index).\n"
+          "\t-D    Access Disconnect-Reconnect Page.\n"
+          "\t-e    Access Read-Write Error Recovery page.\n"
+          "\t-E    Access Control Extension page.\n"
+          "\t-f    Access Format Device Page.\n"
+          "\t-Farg Format of the defect list:\n"
+          "\t\t-Flogical  - logical block addresses (32 bit)\n"
+          "\t\t-Flba64    - logical block addresses (64 bit)\n"
+          "\t\t-Fphysical - physical blocks\n"
+          "\t\t-Findex    - defect bytes from index\n"
+          "\t\t-Fhead     - sort by head\n", stdout);
+    fputs("\t-g    Access Rigid Disk Drive Geometry Page.\n"
+          "\t-G    Display 'grown' defect list (default format: index).\n"
+          "\t-i    Display information from INQUIRY command.\n"
+          "\t-I    Access Informational Exception page.\n"
+          "\t-l    List known scsi devices on the system [DEPRECATED]\n"
+          "\t-n    Access Notch and Partition Page.\n"
+          "\t-N    Negate (stop) storing to saved page (active with -R).\n"
+          "\t-P    Access Power Condition Page.\n"
+          "\t-r    List known raw scsi devices on the system\n"
+          "\t-s    Display serial number (from INQUIRY VPD page).\n"
+          "\t-t<pn[,sp]> Access mode page <pn> [subpage <sp>] and decode.\n"
+          "\t-T    Trace commands (for debugging, double for more)\n"
+          "\t-u<pn[,sp]> Access mode page <pn> [subpage <sp>], output in hex\n"
+          "\t-v    Show version number\n"
+          "\t-V    Access Verify Error Recovery Page.\n"
+          "\t-z    single fetch mode pages (rather than double fetch)\n"
+          "\n", stdout);
+    fputs("\tOnly one of the following three options can be specified.\n"
+   "\tNone of these three implies the current values are returned.\n", stdout);
+    fputs("\t-m    Access modifiable fields instead of current values\n"
+          "\t-M    Access manufacturer defaults instead of current values\n"
+          "\t-S    Access saved defaults instead of current values\n\n"
+          "\t-X    Use list (space separated values) rather than table.\n"
+    "\t-R    Replace parameters - best used with -X (expert use only)\n"
+    "\t      [replacement parameters placed after device on command line]\n\n",
+    stdout);
+    printf("\t      sginfo version: %s; See man page for more details.\n",
+           version_str);
+    exit(2);
+}
+
+int main(int argc, char *argv[])
+{
+    int k, j, n;
+    unsigned int unum, unum2;
+    int decode_in_hex = 0;
+    char c;
+    char * cp;
+    int status = 0;
+    long tmp;
+    struct mpage_info mp_i;
+    int inquiry_verbosity = 0;
+    int show_devs = 0, show_raw = 0;
+    int found = 0;
+
+    if (argc < 2)
+        usage(NULL);
+    memset(&mp_i, 0, sizeof(mp_i));
+    while ((k = getopt(argc, argv, "6aAcCdDeEfgGiIlmMnNPrRsSTvVXzF:t:u:")) !=
+           EOF) {
+        c = (char)k;
+        switch (c) {
+        case '6':
+            mode6byte = 1;
+            break;
+        case 'a':
+            inquiry_verbosity = 1;
+            serial_number = 1;
+            mp_i.page = MP_LIST_PAGES;
+            break;
+        case 'A':
+            inquiry_verbosity = 1;
+            serial_number = 1;
+            mp_i.page = MP_LIST_PAGES;
+            mp_i.subpage = MP_LIST_SUBPAGES;
+            break;
+        case 'c':
+            mp_i.page = 0x8;
+            break;
+        case 'C':
+            mp_i.page = 0xa;
+            break;
+        case 'd':
+            defect = 1;
+            break;
+        case 'D':
+            mp_i.page = 0x2;
+            break;
+        case 'e':
+            mp_i.page = 0x1;
+            break;
+        case 'E':
+            mp_i.page = 0xa;
+            mp_i.subpage = 0x1;
+            break;
+        case 'f':
+            mp_i.page = 0x3;
+            break;
+        case 'F':
+            if (!strcasecmp(optarg, "logical"))
+                defectformat = 0x0;
+            else if (!strcasecmp(optarg, "lba64"))
+                defectformat = 0x3;
+            else if (!strcasecmp(optarg, "physical"))
+                defectformat = 0x5;
+            else if (!strcasecmp(optarg, "index"))
+                defectformat = 0x4;
+            else if (!strcasecmp(optarg, "head"))
+                defectformat = HEAD_SORT_TOKEN;
+            else
+                usage("Illegal -F parameter, must be one of logical, "
+                      "physical, index or head");
+            break;
+        case 'g':
+            mp_i.page = 0x4;
+            break;
+        case 'G':
+            grown_defect = 1;
+            break;
+        case 'i':       /* just vendor, product and revision for '-i -i' */
+            inquiry_verbosity = (2 == inquiry_verbosity) ? 1 : 2;
+            break;
+        case 'I':
+            mp_i.page = 0x1c;
+            break;
+        case 'l':
+            show_devs = 1;
+            break;
+        case 'm': /* modifiable page control */
+            if (0 == mp_i.page_control)
+                mp_i.page_control = 1;
+            else
+                usage("can only have one of 'm', 'M' and 'S'");
+            break;
+        case 'M': /* manufacturer's==default page control */
+            if (0 == mp_i.page_control)
+                mp_i.page_control = 2;
+            else
+                usage("can only have one of 'M', 'm' and 'S'");
+            break;
+        case 'n':
+            mp_i.page = 0xc;
+            break;
+        case 'N':
+            negate_sp_bit = 1;
+            break;
+        case 'P':
+            mp_i.page = 0x1a;
+            break;
+        case 'r':
+            show_raw = 1;
+            break;
+        case 'R':
+            replace = 1;
+            break;
+        case 's':
+            serial_number = 1;
+            break;
+        case 'S': /* saved page control */
+            if (0 == mp_i.page_control)
+                mp_i.page_control = 3;
+            else
+                usage("can only have one of 'S', 'm' and 'M'");
+            break;
+        case 'T':
+            trace_cmd++;
+            break;
+        case 't':
+        case 'u':
+            if ('u' == c)
+                decode_in_hex = 1;
+            while (' ' == *optarg)
+                optarg++;
+            if ('0' == *optarg) {
+                unum = 0;
+                unum2 = 0;
+                j = sscanf(optarg, "0x%x,0x%x", &unum, &unum2);
+                mp_i.page = unum;
+                if (1 == j) {
+                    cp = strchr(optarg, ',');
+                    if (cp && (1 == sscanf(cp, ",%d", &mp_i.subpage)))
+                        j = 2;
+                } else
+                    mp_i.subpage = unum2;
+            } else
+                j = sscanf(optarg, "%d,%d", &mp_i.page, &mp_i.subpage);
+            if (1 == j)
+                mp_i.subpage = 0;
+            else if (j < 1)
+                usage("argument following '-u' should be of form "
+                      "<pg>[,<subpg>]");
+            if ((mp_i.page < 0) || (mp_i.page > MP_LIST_PAGES) ||
+                (mp_i.subpage < 0) || (mp_i.subpage > MP_LIST_SUBPAGES))
+                usage("mode pages range from 0 .. 63, subpages from "
+                      "1 .. 255");
+            found = 1;
+            break;
+        case 'v':
+            fprintf(stdout, "sginfo version: %s\n", version_str);
+            return 0;
+        case 'V':
+            mp_i.page = 0x7;
+            break;
+        case 'X':
+            x_interface = 1;
+            break;
+        case 'z':
+            single_fetch = 1;
+            break;
+        case '?':
+            usage("Unknown option");
+            break;
+        default:
+            fprintf(stdout, "Unknown option '-%c' (ascii 0x%02x)\n", c, c);
+            usage("bad option");
+        }
+    }
+
+    if (replace && !x_interface)
+        usage("-R requires -X");
+    if (replace && mp_i.page_control)
+        usage("-R not allowed for -m, -M or -S");
+    if (x_interface && replace && ((MP_LIST_PAGES == mp_i.page) ||
+                        (MP_LIST_SUBPAGES == mp_i.subpage)))
+        usage("-XR can be used only with exactly one page.");
+
+    if (replace && (3 != mp_i.page_control)) {
+        memset (is_hex, 0, 32);
+        for (j = 1; j < argc - optind; j++) {
+            if (strncmp(argv[optind + j], "0x", 2) == 0) {
+                char *pnt = argv[optind + j] + 2;
+                replacement_values[j] = 0;
+        /* This is a kluge, but we can handle 64 bit quantities this way. */
+                while (*pnt) {
+                    if (*pnt >= 'a' && *pnt <= 'f')
+                        *pnt -= 32;
+                    replacement_values[j] = (replacement_values[j] << 4) |
+                        (*pnt > '9' ? (*pnt - 'A' + 10) : (*pnt - '0'));
+                    pnt++;
+                }
+                continue;
+            }
+            if (argv[optind + j][0] == '@') {
+        /*Ensure that this string contains an even number of hex-digits */
+                int len = strlen(argv[optind + j] + 1);
+
+                if ((len & 1) || (len != (int)strspn(argv[optind + j] + 1,
+                                                "0123456789ABCDEFabcdef")))
+                            usage("Odd number of chars or non-hex digit in "
+                                  "@hexdatafield");
+
+                replacement_values[j] = (unsigned long) argv[optind + j];
+                is_hex[j] = 1;
+                continue;
+            }
+            /* Using a tmp here is silly but the most clean approach */
+            n = sscanf(argv[optind + j], "%ld", &tmp);
+            replacement_values[j] = ((1 == n) ? tmp : 0);
+        }
+        n_replacement_values = argc - optind - 1;
+    }
+    if (show_devs) {
+        show_devices(0);
+        exit(0);
+    }
+    if (show_raw) {
+        show_devices(1);
+        exit(0);
+    }
+    if (optind >= argc)
+        usage("no device name given");
+    glob_fd = open_sg_io_dev(device_name = argv[optind]);
+    if (glob_fd < 0) {
+        if (-9999 == glob_fd)
+            fprintf(stderr, "Couldn't find sg device corresponding to %s\n",
+                    device_name);
+        else {
+            perror("sginfo(open)");
+            fprintf(stderr, "file=%s, or no corresponding sg device found\n",
+                    device_name);
+            fprintf(stderr, "Is sg driver loaded?\n");
+        }
+        exit(1);
+    }
+
+#if 0
+    if (!x_interface)
+        printf("\n");
+#endif
+    if (! (found || mp_i.page || mp_i.subpage || inquiry_verbosity ||
+           serial_number)) {
+        if (trace_cmd > 0)
+            fprintf(stdout, "nothing selected so do a short INQUIRY\n");
+        inquiry_verbosity = 1;
+    }
+
+    status |= do_inquiry(&mp_i.peri_type, &mp_i.inq_byte6,
+                         inquiry_verbosity);
+    if (serial_number)
+        do_serial_number();     /* ignore error */
+    if (mp_i.page > 0)
+        status |= do_user_page(&mp_i, decode_in_hex);
+    if (defect)
+        status |= read_defect_list(0);
+    if (grown_defect)
+        status |= read_defect_list(1);
+
+    return status ? 1 : 0;
+}
diff --git a/src/sgm_dd.c b/src/sgm_dd.c
new file mode 100644
index 0000000..8c9723a
--- /dev/null
+++ b/src/sgm_dd.c
@@ -0,0 +1,1474 @@
+/* A utility program for copying files. Specialised for "files" that
+ * represent devices that understand the SCSI command set.
+ *
+ * Copyright (C) 1999 - 2022 D. Gilbert and P. Allworth
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+
+   This program is a specialisation of the Unix "dd" command in which
+   either the input or the output file is a scsi generic device. The block
+   size ('bs') is assumed to be 512 if not given. This program complains if
+   'ibs' or 'obs' are given with a value that differs from 'bs' (or the
+   default, 512). If 'if' is not given or 'if=-' then stdin is assumed.
+   If 'of' is not given or 'of=-' then stdout assumed.
+
+   A non-standard argument "bpt" (blocks per transfer) is added to control
+   the maximum number of blocks in each transfer. The default value is 128.
+   For example if "bs=512" and "bpt=32" then a maximum of 32 blocks (16 KiB
+   in this case) is transferred to or from the sg device in a single SCSI
+   command.
+
+   This version uses memory-mapped transfers (i.e. mmap() call from the user
+   space) to speed transfers. If both sides of copy are sg devices
+   then only the read side will be mmap-ed, while the write side will
+   use normal IO.
+
+   This version is designed for the Linux kernel 2.4, 2.6, 3, 4 and 5 series.
+*/
+
+#define _XOPEN_SOURCE 600
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <signal.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/mman.h>
+#include <sys/sysmacros.h>
+#ifndef major
+#include <sys/types.h>
+#endif
+#include <linux/major.h>        /* for MEM_MAJOR, SCSI_GENERIC_MAJOR, etc */
+#include <linux/fs.h>           /* for BLKSSZGET and friends */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "1.19 20220118";
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BLOCKS_PER_TRANSFER 128
+#define DEF_BLOCKS_PER_2048TRANSFER 32
+#define DEF_SCSI_CDBSZ 10
+#define MAX_SCSI_CDBSZ 16
+#define MAX_BPT_VALUE (1 << 24)         /* used for maximum bs as well */
+#define MAX_COUNT_SKIP_SEEK (1LL << 48) /* coverity wants upper bound */
+
+#define ME "sgm_dd: "
+
+
+#ifndef SG_FLAG_MMAP_IO
+#define SG_FLAG_MMAP_IO 4
+#endif
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define READ_CAP_REPLY_LEN 8
+#define RCAP16_REPLY_LEN 32
+
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs == 60 seconds */
+
+#ifndef RAW_MAJOR
+#define RAW_MAJOR 255   /*unlikely value */
+#endif
+
+#define FT_OTHER 1              /* filetype other than one of following */
+#define FT_SG 2                 /* filetype is sg char device */
+#define FT_RAW 4                /* filetype is raw char device */
+#define FT_DEV_NULL 8           /* either "/dev/null" or "." as filename */
+#define FT_ST 16                /* filetype is st char device (tape) */
+#define FT_BLOCK 32             /* filetype is a block device */
+#define FT_ERROR 64             /* couldn't "stat" file */
+
+#define DEV_NULL_MINOR_NUM 3
+
+#define MIN_RESERVED_SIZE 8192
+
+static int sum_of_resids = 0;
+
+static int64_t dd_count = -1;
+static int64_t req_count = 0;
+static int64_t in_full = 0;
+static int in_partial = 0;
+static int64_t out_full = 0;
+static int out_partial = 0;
+static int verbose = 0;
+static int dry_run = 0;
+static int progress = 0;        /* accept --progress or -p, does nothing */
+
+static bool do_time = false;
+static bool start_tm_valid = false;
+static struct timeval start_tm;
+static int blk_sz = 0;
+static uint32_t glob_pack_id = 0;       /* pre-increment */
+
+static const char * sg_allow_dio = "/sys/module/sg/parameters/allow_dio";
+
+struct flags_t {
+    bool append;
+    bool dio;
+    bool direct;
+    bool dpo;
+    bool dsync;
+    bool excl;
+    bool fua;
+};
+
+
+static void
+install_handler(int sig_num, void (*sig_handler) (int sig))
+{
+    struct sigaction sigact;
+    sigaction (sig_num, NULL, &sigact);
+    if (sigact.sa_handler != SIG_IGN)
+    {
+        sigact.sa_handler = sig_handler;
+        sigemptyset (&sigact.sa_mask);
+        sigact.sa_flags = 0;
+        sigaction (sig_num, &sigact, NULL);
+    }
+}
+
+static void
+print_stats()
+{
+    if (0 != dd_count)
+        pr2serr("  remaining block count=%" PRId64 "\n", dd_count);
+    pr2serr("%" PRId64 "+%d records in\n", in_full - in_partial, in_partial);
+    pr2serr("%" PRId64 "+%d records out\n", out_full - out_partial,
+            out_partial);
+}
+
+static void
+calc_duration_throughput(bool contin)
+{
+    double a, b;
+    struct timeval end_tm, res_tm;
+
+    if (start_tm_valid && (start_tm.tv_sec || start_tm.tv_usec)) {
+        gettimeofday(&end_tm, NULL);
+        res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+        res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+        if (res_tm.tv_usec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_usec += 1000000;
+        }
+        a = res_tm.tv_sec;
+        a += (0.000001 * res_tm.tv_usec);
+        b = (double)blk_sz * (req_count - dd_count);
+        pr2serr("time to transfer data%s: %d.%06d secs",
+                (contin ? " so far" : ""), (int)res_tm.tv_sec,
+                (int)res_tm.tv_usec);
+        if ((a > 0.00001) && (b > 511))
+            pr2serr(" at %.2f MB/sec\n", b / (a * 1000000.0));
+        else
+            pr2serr("\n");
+    }
+}
+
+static void
+interrupt_handler(int sig)
+{
+    struct sigaction sigact;
+
+    sigact.sa_handler = SIG_DFL;
+    sigemptyset (&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigaction (sig, &sigact, NULL);
+    pr2serr("Interrupted by signal,");
+    print_stats ();
+    if (do_time)
+        calc_duration_throughput(false);
+    kill (getpid (), sig);
+}
+
+static void
+siginfo_handler(int sig)
+{
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    pr2serr("Progress report, continuing ...\n");
+    print_stats();
+    if (do_time)
+        calc_duration_throughput(true);
+}
+
+static int
+dd_filetype(const char * filename)
+{
+    size_t len = strlen(filename);
+    struct stat st;
+
+    if ((1 == len) && ('.' == filename[0]))
+        return FT_DEV_NULL;
+    if (stat(filename, &st) < 0)
+        return FT_ERROR;
+    if (S_ISCHR(st.st_mode)) {
+        if ((MEM_MAJOR == major(st.st_rdev)) &&
+            (DEV_NULL_MINOR_NUM == minor(st.st_rdev)))
+            return FT_DEV_NULL;
+        if (RAW_MAJOR == major(st.st_rdev))
+            return FT_RAW;
+        if (SCSI_GENERIC_MAJOR == major(st.st_rdev))
+            return FT_SG;
+        if (SCSI_TAPE_MAJOR == major(st.st_rdev))
+            return FT_ST;
+    } else if (S_ISBLK(st.st_mode))
+        return FT_BLOCK;
+    return FT_OTHER;
+}
+
+static char *
+dd_filetype_str(int ft, char * buff)
+{
+    int off = 0;
+
+    if (FT_DEV_NULL & ft)
+        off += sg_scnpr(buff + off, 32, "null device ");
+    if (FT_SG & ft)
+        off += sg_scnpr(buff + off, 32, "SCSI generic (sg) device ");
+    if (FT_BLOCK & ft)
+        off += sg_scnpr(buff + off, 32, "block device ");
+    if (FT_ST & ft)
+        off += sg_scnpr(buff + off, 32, "SCSI tape device ");
+    if (FT_RAW & ft)
+        off += sg_scnpr(buff + off, 32, "raw device ");
+    if (FT_OTHER & ft)
+        off += sg_scnpr(buff + off, 32, "other (perhaps ordinary file) ");
+    if (FT_ERROR & ft)
+        sg_scnpr(buff + off, 32, "unable to 'stat' file ");
+    return buff;
+}
+
+static void
+usage()
+{
+    pr2serr("Usage: sgm_dd  [bs=BS] [count=COUNT] [ibs=BS] [if=IFILE]"
+            " [iflag=FLAGS]\n"
+            "               [obs=BS] [of=OFILE] [oflag=FLAGS] "
+            "[seek=SEEK] [skip=SKIP]\n"
+            "               [--help] [--version]\n\n");
+    pr2serr("               [bpt=BPT] [cdbsz=6|10|12|16] [dio=0|1] "
+            "[fua=0|1|2|3]\n"
+            "               [sync=0|1] [time=0|1] [verbose=VERB] "
+            "[--dry-run] [--verbose]\n\n"
+            "  where:\n"
+            "    bpt         is blocks_per_transfer (default is 128)\n"
+            "    bs          must be device logical block size (default "
+            "512)\n"
+            "    cdbsz       size of SCSI READ or WRITE cdb (default is 10)\n"
+            "    count       number of blocks to copy (def: device size)\n"
+            "    dio         0->indirect IO on write, 1->direct IO on write\n"
+            "                (only when read side is sg device (using mmap))\n"
+            "    fua         force unit access: 0->don't(def), 1->OFILE, "
+            "2->IFILE,\n"
+            "                3->OFILE+IFILE\n"
+            "    if          file or device to read from (def: stdin)\n");
+    pr2serr("    iflag       comma separated list from: [direct,dpo,dsync,"
+            "excl,fua,\n"
+            "                null]\n"
+            "    of          file or device to write to (def: stdout), "
+            "OFILE of '.'\n"
+            "                treated as /dev/null\n"
+            "    oflag       comma separated list from: [append,dio,direct,"
+            "dpo,dsync,\n"
+            "                excl,fua,null]\n"
+            "    seek        block position to start writing to OFILE\n"
+            "    skip        block position to start reading from IFILE\n"
+            "    sync        0->no sync(def), 1->SYNCHRONIZE CACHE on OFILE "
+            "after copy\n"
+            "    time        0->no timing(def), 1->time plus calculate "
+            "throughput\n"
+            "    verbose     0->quiet(def), 1->some noise, 2->more noise, "
+            "etc\n"
+            "    --dry-run|-d    prepare but bypass copy/read\n"
+            "    --help|-h       print usage message then exit\n"
+            "    --verbose|-v    increase verbosity\n"
+            "    --version|-V    print version information then exit\n\n"
+            "Copy from IFILE to OFILE, similar to dd command\n"
+            "specialized for SCSI devices for which mmap-ed IO attempted\n");
+}
+
+/* Return of 0 -> success, see sg_ll_read_capacity*() otherwise */
+static int
+scsi_read_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+    int res, verb;
+    unsigned int ui;
+    uint8_t rcBuff[RCAP16_REPLY_LEN];
+
+    verb = (verbose ? verbose - 1: 0);
+    res = sg_ll_readcap_10(sg_fd, 0, 0, rcBuff, READ_CAP_REPLY_LEN, false,
+                           verb);
+    if (0 != res)
+        return res;
+
+    if ((0xff == rcBuff[0]) && (0xff == rcBuff[1]) && (0xff == rcBuff[2]) &&
+        (0xff == rcBuff[3])) {
+
+        res = sg_ll_readcap_16(sg_fd, 0, 0, rcBuff, RCAP16_REPLY_LEN, false,
+                               verb);
+        if (0 != res)
+            return res;
+        *num_sect = sg_get_unaligned_be64(rcBuff + 0) + 1;
+        *sect_sz = sg_get_unaligned_be32(rcBuff + 8);
+    } else {
+        ui = sg_get_unaligned_be32(rcBuff + 0);
+        /* take care not to sign extend values > 0x7fffffff */
+        *num_sect = (int64_t)ui + 1;
+        *sect_sz = sg_get_unaligned_be32(rcBuff + 4);
+    }
+    if (verb)
+        pr2serr("      number of blocks=%" PRId64 " [0x%" PRIx64 "], block "
+                "size=%d\n", *num_sect, *num_sect, *sect_sz);
+    return 0;
+}
+
+/* Return of 0 -> success, -1 -> failure. BLKGETSIZE64, BLKGETSIZE and */
+/* BLKSSZGET macros problematic (from <linux/fs.h> or <sys/mount.h>). */
+static int
+read_blkdev_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+#ifdef BLKSSZGET
+    if ((ioctl(sg_fd, BLKSSZGET, sect_sz) < 0) && (*sect_sz > 0)) {
+        perror("BLKSSZGET ioctl error");
+        return -1;
+    } else {
+ #ifdef BLKGETSIZE64
+        uint64_t ull;
+
+        if (ioctl(sg_fd, BLKGETSIZE64, &ull) < 0) {
+
+            perror("BLKGETSIZE64 ioctl error");
+            return -1;
+        }
+        *num_sect = ((int64_t)ull / (int64_t)*sect_sz);
+        if (verbose > 1)
+            pr2serr("      [bgs64] number of blocks=%" PRId64 " [0x%" PRIx64
+                    "], logical block size=%d\n", *num_sect, *num_sect,
+                    *sect_sz);
+ #else
+        unsigned long ul;
+
+        if (ioctl(sg_fd, BLKGETSIZE, &ul) < 0) {
+            perror("BLKGETSIZE ioctl error");
+            return -1;
+        }
+        *num_sect = (int64_t)ul;
+        if (verbose > 1)
+            pr2serr("      [bgs] number of blocks=%" PRId64 " [0x%" PRIx64
+                    "], logical block size=%d\n", *num_sect, *num_sect,
+                    *sect_sz);
+ #endif
+    }
+    return 0;
+#else
+    if (verbose)
+        pr2serr("      BLKSSZGET+BLKGETSIZE ioctl not available\n");
+    *num_sect = 0;
+    *sect_sz = 0;
+    return -1;
+#endif
+}
+
+static int
+sg_build_scsi_cdb(uint8_t * cdbp, int cdb_sz, unsigned int blocks,
+                  int64_t start_block, bool write_true, bool fua, bool dpo)
+{
+    int sz_ind;
+    int rd_opcode[] = {0x8, 0x28, 0xa8, 0x88};
+    int wr_opcode[] = {0xa, 0x2a, 0xaa, 0x8a};
+
+    memset(cdbp, 0, cdb_sz);
+    if (dpo)
+        cdbp[1] |= 0x10;
+    if (fua)
+        cdbp[1] |= 0x8;
+    switch (cdb_sz) {
+    case 6:
+        sz_ind = 0;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be24(0x1fffff & start_block, cdbp + 1);
+        cdbp[4] = (256 == blocks) ? 0 : (uint8_t)blocks;
+        if (blocks > 256) {
+            pr2serr(ME "for 6 byte commands, maximum number of blocks is "
+                    "256\n");
+            return 1;
+        }
+        if ((start_block + blocks - 1) & (~0x1fffff)) {
+            pr2serr(ME "for 6 byte commands, can't address blocks beyond "
+                    "%d\n", 0x1fffff);
+            return 1;
+        }
+        if (dpo || fua) {
+            pr2serr(ME "for 6 byte commands, neither dpo nor fua bits "
+                    "supported\n");
+            return 1;
+        }
+        break;
+    case 10:
+        sz_ind = 1;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be16((uint16_t)blocks, cdbp + 7);
+        if (blocks & (~0xffff)) {
+            pr2serr(ME "for 10 byte commands, maximum number of blocks is "
+                    "%d\n", 0xffff);
+            return 1;
+        }
+        break;
+    case 12:
+        sz_ind = 2;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 6);
+        break;
+    case 16:
+        sz_ind = 3;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be64((uint64_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 10);
+        break;
+    default:
+        pr2serr(ME "expected cdb size of 6, 10, 12, or 16 but got %d\n",
+                cdb_sz);
+        return 1;
+    }
+    return 0;
+}
+
+/* Returns 0 -> successful, various SG_LIB_CAT_* positive values,
+ * -2 -> recoverable (ENOMEM), -1 -> unrecoverable error */
+static int
+sg_read(int sg_fd, uint8_t * buff, int blocks, int64_t from_block,
+        int bs, int cdbsz, bool fua, bool dpo, bool do_mmap)
+{
+    bool print_cdb_after = false;
+    int res;
+    uint8_t rdCmd[MAX_SCSI_CDBSZ];
+    uint8_t senseBuff[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr;
+
+    if (sg_build_scsi_cdb(rdCmd, cdbsz, blocks, from_block, false, fua,
+                          dpo)) {
+        pr2serr(ME "bad rd cdb build, from_block=%" PRId64 ", blocks=%d\n",
+                from_block, blocks);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = cdbsz;
+    io_hdr.cmdp = rdCmd;
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = bs * blocks;
+    if (! do_mmap)
+        io_hdr.dxferp = buff;
+    io_hdr.mx_sb_len = SENSE_BUFF_LEN;
+    io_hdr.sbp = senseBuff;
+    io_hdr.timeout = DEF_TIMEOUT;
+    io_hdr.pack_id = (int)++glob_pack_id;
+    if (do_mmap)
+        io_hdr.flags |= SG_FLAG_MMAP_IO;
+    if (verbose > 2) {
+        char b[128];
+
+        pr2serr("    Read cdb: %s\n",
+                sg_get_command_str(rdCmd, cdbsz, false, sizeof(b), b));
+    }
+
+#if 1
+    while (((res = ioctl(sg_fd, SG_IO, &io_hdr)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        sleep(1);
+    if (res < 0) {
+        perror(ME "SG_IO error (sg_read)");
+        return -1;
+    }
+#else
+    while (((res = write(sg_fd, &io_hdr, sizeof(io_hdr))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    if (res < 0) {
+        if (ENOMEM == errno)
+            return -2;
+        perror("reading (wr) on sg device, error");
+        return -1;
+    }
+
+    while (((res = read(sg_fd, &io_hdr, sizeof(io_hdr))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    if (res < 0) {
+        perror("reading (rd) on sg device, error");
+        return -1;
+    }
+#endif
+    if (verbose > 2)
+        pr2serr("      duration=%u ms\n", io_hdr.duration);
+    res =  sg_err_category3(&io_hdr);
+    switch (res) {
+    case SG_LIB_CAT_CLEAN:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        sg_chk_n_print3("Reading, continuing", &io_hdr, verbose > 1);
+        break;
+    case SG_LIB_CAT_NOT_READY:
+    case SG_LIB_CAT_MEDIUM_HARD:
+        return res;
+    case SG_LIB_CAT_ILLEGAL_REQ:
+        if (verbose)
+            print_cdb_after = true;
+        /* FALL THROUGH */
+    case SG_LIB_CAT_ABORTED_COMMAND:
+    case SG_LIB_CAT_UNIT_ATTENTION:
+    default:
+        sg_chk_n_print3("reading", &io_hdr, verbose > 1);
+        if (print_cdb_after)
+            sg_print_command_len(rdCmd, cdbsz);
+        return res;
+    }
+    sum_of_resids += io_hdr.resid;
+#ifdef DEBUG
+    pr2serr("duration=%u ms\n", io_hdr.duration);
+#endif
+    return 0;
+}
+
+/* Returns 0 -> successful, various SG_LIB_CAT_* positive values,
+ * -2 -> recoverable (ENOMEM), -1 -> unrecoverable error */
+static int
+sg_write(int sg_fd, uint8_t * buff, int blocks, int64_t to_block,
+         int bs, int cdbsz, bool fua, bool dpo, bool do_mmap, bool * diop)
+{
+    bool print_cdb_after = false;
+    int res;
+    uint8_t wrCmd[MAX_SCSI_CDBSZ];
+    uint8_t senseBuff[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr SG_C_CPP_ZERO_INIT;
+
+    if (sg_build_scsi_cdb(wrCmd, cdbsz, blocks, to_block, true, fua, dpo)) {
+        pr2serr(ME "bad wr cdb build, to_block=%" PRId64 ", blocks=%d\n",
+                to_block, blocks);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = cdbsz;
+    io_hdr.cmdp = wrCmd;
+    io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
+    io_hdr.dxfer_len = bs * blocks;
+    if (! do_mmap)
+        io_hdr.dxferp = buff;
+    io_hdr.mx_sb_len = SENSE_BUFF_LEN;
+    io_hdr.sbp = senseBuff;
+    io_hdr.timeout = DEF_TIMEOUT;
+    io_hdr.pack_id = (int)++glob_pack_id;
+    if (do_mmap)
+        io_hdr.flags |= SG_FLAG_MMAP_IO;
+    else if (diop && *diop)
+        io_hdr.flags |= SG_FLAG_DIRECT_IO;
+    if (verbose > 2) {
+        char b[128];
+
+        pr2serr("    Write cdb: %s\n",
+                sg_get_command_str(wrCmd, cdbsz, false, sizeof(b), b));
+    }
+
+#if 1
+    while (((res = ioctl(sg_fd, SG_IO, &io_hdr)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        sleep(1);
+    if (res < 0) {
+        perror(ME "SG_IO error (sg_write)");
+        return -1;
+    }
+#else
+    while (((res = write(sg_fd, &io_hdr, sizeof(io_hdr))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    if (res < 0) {
+        if (ENOMEM == errno)
+            return -2;
+        perror("writing (wr) on sg device, error");
+        return -1;
+    }
+
+    while (((res = read(sg_fd, &io_hdr, sizeof(io_hdr))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    if (res < 0) {
+        perror("writing (rd) on sg device, error");
+        return -1;
+    }
+#endif
+    if (verbose > 2)
+        pr2serr("      duration=%u ms\n", io_hdr.duration);
+    res = sg_err_category3(&io_hdr);
+    switch (res) {
+    case SG_LIB_CAT_CLEAN:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        sg_chk_n_print3("Writing, continuing", &io_hdr, verbose > 1);
+        break;
+    case SG_LIB_CAT_NOT_READY:
+    case SG_LIB_CAT_MEDIUM_HARD:
+        return res;
+    case SG_LIB_CAT_ILLEGAL_REQ:
+        if (verbose)
+            print_cdb_after = true;
+        /* FALL THROUGH */
+    case SG_LIB_CAT_ABORTED_COMMAND:
+    case SG_LIB_CAT_UNIT_ATTENTION:
+    default:
+        sg_chk_n_print3("writing", &io_hdr, verbose > 1);
+        if (print_cdb_after)
+            sg_print_command_len(wrCmd, cdbsz);
+        return res;
+    }
+    if (diop && *diop &&
+        ((io_hdr.info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+        *diop = false;      /* flag that dio not done (completely) */
+    return 0;
+}
+
+static int
+process_flags(const char * arg, struct flags_t * fp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no flag found\n");
+        return 1;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "append"))
+            fp->append = true;
+        else if (0 == strcmp(cp, "dio"))
+            fp->dio = true;
+        else if (0 == strcmp(cp, "direct"))
+            fp->direct = true;
+        else if (0 == strcmp(cp, "dpo"))
+            fp->dpo = true;
+        else if (0 == strcmp(cp, "dsync"))
+            fp->dsync = true;
+        else if (0 == strcmp(cp, "excl"))
+            fp->excl = true;
+        else if (0 == strcmp(cp, "fua"))
+            fp->fua = true;
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return 1;
+        }
+        cp = np;
+    } while (cp);
+    return 0;
+}
+
+/* Returns the number of times 'ch' is found in string 's' given the
+ * string's length. */
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+
+#define STR_SZ 1024
+#define INOUTF_SZ 512
+#define EBUFF_SZ 768
+
+int
+main(int argc, char * argv[])
+{
+    bool bpt_given = false;
+    bool cdbsz_given = false;
+    bool do_coe = false;     /* dummy, just accept + ignore */
+    bool do_sync = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    int res, k, t, infd, outfd, blocks, n, flags, blocks_per, err, keylen;
+    int bpt = DEF_BLOCKS_PER_TRANSFER;
+    int ibs = 0;
+    int in_res_sz = 0;
+    int in_sect_sz;
+    int in_type = FT_OTHER;
+    int obs = 0;
+    int out_res_sz = 0;
+    int out_sect_sz;
+    int out_type = FT_OTHER;
+    int num_dio_not_done = 0;
+    int ret = 0;
+    int scsi_cdbsz_in = DEF_SCSI_CDBSZ;
+    int scsi_cdbsz_out = DEF_SCSI_CDBSZ;
+    size_t psz;
+    int64_t in_num_sect = -1;
+    int64_t out_num_sect = -1;
+    int64_t skip = 0;
+    int64_t seek = 0;
+    char * buf;
+    char * key;
+    uint8_t * wrkPos;
+    uint8_t * wrkBuff = NULL;
+    uint8_t * wrkMmap = NULL;
+    char inf[INOUTF_SZ];
+    char str[STR_SZ];
+    char outf[INOUTF_SZ];
+    char ebuff[EBUFF_SZ];
+    char b[80];
+    struct flags_t in_flags;
+    struct flags_t out_flags;
+
+#if defined(HAVE_SYSCONF) && defined(_SC_PAGESIZE)
+    psz = sysconf(_SC_PAGESIZE); /* POSIX.1 (was getpagesize()) */
+#else
+    psz = 4096;     /* give up, pick likely figure */
+#endif
+    inf[0] = '\0';
+    outf[0] = '\0';
+    memset(&in_flags, 0, sizeof(in_flags));
+    memset(&out_flags, 0, sizeof(out_flags));
+
+    for (k = 1; k < argc; k++) {
+        if (argv[k])
+            snprintf(str, STR_SZ, "%s", argv[k]);
+        else
+            continue;
+        for (key = str, buf = key; *buf && *buf != '=';)
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        keylen = strlen(key);
+        if (0 == strcmp(key,"bpt")) {
+            bpt = sg_get_num(buf);
+            if (-1 == bpt) {
+                pr2serr(ME "bad argument to 'bpt'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            bpt_given = true;
+        } else if (0 == strcmp(key,"bs")) {
+            blk_sz = sg_get_num(buf);
+            if (-1 == blk_sz) {
+                pr2serr(ME "bad argument to 'bs'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"cdbsz")) {
+            scsi_cdbsz_in = sg_get_num(buf);
+            if ((scsi_cdbsz_in < 6) || (scsi_cdbsz_in > 32)) {
+                pr2serr(ME "'cdbsz' expects 6, 10, 12, 16 or 32\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            scsi_cdbsz_out = scsi_cdbsz_in;
+            cdbsz_given = true;
+        } else if (0 == strcmp(key,"coe")) {
+            do_coe = !! sg_get_num(buf);   /* dummy, just accept + ignore */
+            if (do_coe) { ; }   /* unused, dummy to suppress warning */
+        } else if (0 == strcmp(key,"count")) {
+            if (0 != strcmp("-1", buf)) {
+                dd_count = sg_get_llnum(buf);
+                if ((dd_count < 0) || (dd_count > MAX_COUNT_SKIP_SEEK)) {
+                    pr2serr(ME "bad argument to 'count'\n");
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }   /* treat 'count=-1' as calculate count (same as not given) */
+        } else if (0 == strcmp(key,"dio"))
+            out_flags.dio = !! sg_get_num(buf);
+        else if (0 == strcmp(key,"fua")) {
+            n = sg_get_num(buf);
+            if (n & 1)
+                out_flags.fua = true;
+            if (n & 2)
+                in_flags.fua = true;
+        } else if (0 == strcmp(key,"ibs")) {
+            ibs = sg_get_num(buf);
+            if ((ibs < 0) || (ibs > MAX_BPT_VALUE)) {
+                pr2serr(ME "bad argument to 'ibs'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key,"if") == 0) {
+            if ('\0' != inf[0]) {
+                pr2serr("Second 'if=' argument??\n");
+                return SG_LIB_CONTRADICT;
+            } else {
+                memcpy(inf, buf, INOUTF_SZ);
+                inf[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (0 == strcmp(key, "iflag")) {
+            if (process_flags(buf, &in_flags)) {
+                pr2serr(ME "bad argument to 'iflag'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key,"of") == 0) {
+            if ('\0' != outf[0]) {
+                pr2serr("Second 'of=' argument??\n");
+                return SG_LIB_CONTRADICT;
+            } else {
+                memcpy(outf, buf, INOUTF_SZ);
+                outf[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (0 == strcmp(key, "oflag")) {
+            if (process_flags(buf, &out_flags)) {
+                pr2serr(ME "bad argument to 'oflag'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"obs")) {
+            obs = sg_get_num(buf);
+            if ((obs < 0) || (obs > MAX_BPT_VALUE)) {
+                pr2serr(ME "bad argument to 'obs'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"seek")) {
+            seek = sg_get_llnum(buf);
+            if ((seek < 0) || (seek > MAX_COUNT_SKIP_SEEK)) {
+                pr2serr(ME "bad argument to 'seek'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"skip")) {
+            skip = sg_get_llnum(buf);
+            if ((skip < 0) || (skip > MAX_COUNT_SKIP_SEEK)) {
+                pr2serr(ME "bad argument to 'skip'\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"sync"))
+            do_sync = !! sg_get_num(buf);
+        else if (0 == strcmp(key,"time"))
+            do_time = sg_get_num(buf);
+        else if (0 == strncmp(key, "verb", 4))
+            verbose = sg_get_num(buf);
+        else if ((keylen > 1) && ('-' == key[0]) && ('-' != key[1])) {
+            res = 0;
+            n = num_chs_in_str(key + 1, keylen - 1, 'd');
+            dry_run += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'h');
+            if (n > 0) {
+                usage();
+                return 0;
+            }
+            n = num_chs_in_str(key + 1, keylen - 1, 'p');
+            progress += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'v');
+            verbose += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'V');
+            if (n > 0)
+                version_given = true;
+            res += n;
+            if (res < (keylen - 1)) {
+                pr2serr("Unrecognised short option in '%s', try '--help'\n",
+                        key);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if ((0 == strncmp(key, "--dry-run", 9)) ||
+                 (0 == strncmp(key, "--dry_run", 9)))
+            ++dry_run;
+        else if ((0 == strncmp(key, "--help", 6)) ||
+                 (0 == strcmp(key, "-?"))) {
+            usage();
+            return 0;
+        } else if (0 == strncmp(key, "--prog", 6))
+            ++progress;
+        else if (0 == strncmp(key, "--verb", 6))
+            ++verbose;
+        else if (0 == strncmp(key, "--vers", 6))
+            version_given = true;
+        else {
+            pr2serr("Unrecognized option '%s'\n", key);
+            pr2serr("For more information use '--help'\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr(ME ": %s\n", version_str);
+        return 0;
+    }
+
+    if (blk_sz <= 0) {
+        blk_sz = DEF_BLOCK_SIZE;
+        pr2serr("Assume default 'bs' ((logical) block size) of %d bytes\n",
+                blk_sz);
+    }
+    if ((ibs && (ibs != blk_sz)) || (obs && (obs != blk_sz))) {
+        pr2serr("If 'ibs' or 'obs' given must be same as 'bs'\n");
+        usage();
+        return SG_LIB_CONTRADICT;
+    }
+    if ((skip < 0) || (seek < 0)) {
+        pr2serr("skip and seek cannot be negative\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (out_flags.append && (seek > 0)) {
+        pr2serr("Can't use both append and seek switches\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if ((bpt < 1) || (bpt > MAX_BPT_VALUE)) {
+        pr2serr("bpt must be > 0 and <= %d\n", MAX_BPT_VALUE);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    /* defaulting transfer size to 128*2048 for CD/DVDs is too large
+       for the block layer in lk 2.6 and results in an EIO on the
+       SG_IO ioctl. So reduce it in that case. */
+    if ((blk_sz >= 2048) && (! bpt_given))
+        bpt = DEF_BLOCKS_PER_2048TRANSFER;
+
+#ifdef DEBUG
+    pr2serr(ME "if=%s skip=%" PRId64 " of=%s seek=%" PRId64 " count=%" PRId64
+            "\n", inf, skip, outf, seek, dd_count);
+#endif
+    install_handler (SIGINT, interrupt_handler);
+    install_handler (SIGQUIT, interrupt_handler);
+    install_handler (SIGPIPE, interrupt_handler);
+    install_handler (SIGUSR1, siginfo_handler);
+
+    infd = STDIN_FILENO;
+    outfd = STDOUT_FILENO;
+    if (inf[0] && ('-' != inf[0])) {
+        in_type = dd_filetype(inf);
+        if (verbose > 1)
+            pr2serr(" >> Input file type: %s\n",
+                    dd_filetype_str(in_type, ebuff));
+
+        if (FT_ERROR == in_type) {
+            pr2serr(ME "unable to access %s\n", inf);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_ST == in_type) {
+            pr2serr(ME "unable to use scsi tape device %s\n", inf);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_SG == in_type) {
+            flags = O_RDWR | O_NONBLOCK;
+            if (in_flags.direct)
+                flags |= O_DIRECT;
+            if (in_flags.excl)
+                flags |= O_EXCL;
+            if (in_flags.dsync)
+                flags |= O_SYNC;
+            if ((infd = open(inf, flags)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ,
+                         ME "could not open %s for sg reading", inf);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+            res = ioctl(infd, SG_GET_VERSION_NUM, &t);
+            if ((res < 0) || (t < 30122)) {
+                pr2serr(ME "sg driver prior to 3.1.22\n");
+                return SG_LIB_FILE_ERROR;
+            }
+            in_res_sz = blk_sz * bpt;
+            if (0 != (in_res_sz % psz)) /* round up to next page */
+                in_res_sz = ((in_res_sz / psz) + 1) * psz;
+            if (ioctl(infd, SG_GET_RESERVED_SIZE, &t) < 0) {
+                err = errno;
+                perror(ME "SG_GET_RESERVED_SIZE error");
+                return sg_convert_errno(err);
+            }
+            if (t < MIN_RESERVED_SIZE)
+                t = MIN_RESERVED_SIZE;
+            if (in_res_sz > t) {
+                if (ioctl(infd, SG_SET_RESERVED_SIZE, &in_res_sz) < 0) {
+                    err = errno;
+                    perror(ME "SG_SET_RESERVED_SIZE error");
+                    return sg_convert_errno(err);
+                }
+            }
+            wrkMmap = (uint8_t *)mmap(NULL, in_res_sz,
+                                 PROT_READ | PROT_WRITE, MAP_SHARED, infd, 0);
+            if (MAP_FAILED == wrkMmap) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ,
+                         ME "error using mmap() on file: %s", inf);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+        } else {
+            flags = O_RDONLY;
+            if (in_flags.direct)
+                flags |= O_DIRECT;
+            if (in_flags.excl)
+                flags |= O_EXCL;
+            if (in_flags.dsync)
+                flags |= O_SYNC;
+            if ((infd = open(inf, flags)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ,
+                         ME "could not open %s for reading", inf);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+            else if (skip > 0) {
+                off64_t offset = skip;
+
+                offset *= blk_sz;       /* could exceed 32 bits here! */
+                if (lseek64(infd, offset, SEEK_SET) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, ME "couldn't skip to "
+                             "required position on %s", inf);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+                if (verbose > 1)
+                    pr2serr("  >> skip: lseek64 SEEK_SET, byte offset=0x%"
+                            PRIx64 "\n", (uint64_t)offset);
+            }
+        }
+    }
+
+    if (outf[0] && ('-' != outf[0])) {
+        out_type = dd_filetype(outf);
+        if (verbose > 1)
+            pr2serr(" >> Output file type: %s\n",
+                    dd_filetype_str(out_type, ebuff));
+
+        if (FT_ST == out_type) {
+            pr2serr(ME "unable to use scsi tape device %s\n", outf);
+            return SG_LIB_FILE_ERROR;
+        }
+        else if (FT_SG == out_type) {
+            flags = O_RDWR | O_NONBLOCK;
+            if (out_flags.direct)
+                flags |= O_DIRECT;
+            if (out_flags.excl)
+                flags |= O_EXCL;
+            if (out_flags.dsync)
+                flags |= O_SYNC;
+            if ((outfd = open(outf, flags)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ, ME "could not open %s for "
+                         "sg writing", outf);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+            res = ioctl(outfd, SG_GET_VERSION_NUM, &t);
+            if ((res < 0) || (t < 30122)) {
+                pr2serr(ME "sg driver prior to 3.1.22\n");
+                return SG_LIB_FILE_ERROR;
+            }
+            if (ioctl(outfd, SG_GET_RESERVED_SIZE, &t) < 0) {
+                err = errno;
+                perror(ME "SG_GET_RESERVED_SIZE error");
+                return sg_convert_errno(err);
+            }
+           if (t < MIN_RESERVED_SIZE)
+                t = MIN_RESERVED_SIZE;
+            out_res_sz = blk_sz * bpt;
+            if (out_res_sz > t) {
+                if (ioctl(outfd, SG_SET_RESERVED_SIZE, &out_res_sz) < 0) {
+                    err = errno;
+                    perror(ME "SG_SET_RESERVED_SIZE error");
+                    return sg_convert_errno(err);
+                }
+            }
+            if (NULL == wrkMmap) {
+                wrkMmap = (uint8_t *)mmap(NULL, out_res_sz,
+                                PROT_READ | PROT_WRITE, MAP_SHARED, outfd, 0);
+                if (MAP_FAILED == wrkMmap) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ,
+                             ME "error using mmap() on file: %s", outf);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+        }
+        else if (FT_DEV_NULL == out_type)
+            outfd = -1; /* don't bother opening */
+        else {
+            if (FT_RAW != out_type) {
+                flags = O_WRONLY | O_CREAT;
+                if (out_flags.direct)
+                    flags |= O_DIRECT;
+                if (out_flags.excl)
+                    flags |= O_EXCL;
+                if (out_flags.dsync)
+                    flags |= O_SYNC;
+                if (out_flags.append)
+                    flags |= O_APPEND;
+                if ((outfd = open(outf, flags, 0666)) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ,
+                             ME "could not open %s for writing", outf);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+            else {
+                if ((outfd = open(outf, O_WRONLY)) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, ME "could not open %s "
+                             "for raw writing", outf);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+            if (seek > 0) {
+                off64_t offset = seek;
+
+                offset *= blk_sz;       /* could exceed 32 bits here! */
+                if (lseek64(outfd, offset, SEEK_SET) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, ME "couldn't seek to "
+                             "required position on %s", outf);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+                if (verbose > 1)
+                    pr2serr("   >> seek: lseek64 SEEK_SET, byte offset=0x%"
+                            PRIx64 "\n", (uint64_t)offset);
+            }
+        }
+    }
+    if ((STDIN_FILENO == infd) && (STDOUT_FILENO == outfd)) {
+        pr2serr("Won't default both IFILE to stdin _and_ OFILE to as "
+                "stdout\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_CONTRADICT;
+    }
+    if (dd_count < 0) {
+        in_num_sect = -1;
+        if (FT_SG == in_type) {
+            res = scsi_read_capacity(infd, &in_num_sect, &in_sect_sz);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Unit attention(in), continuing\n");
+                res = scsi_read_capacity(infd, &in_num_sect, &in_sect_sz);
+            } else if (SG_LIB_CAT_ABORTED_COMMAND == res) {
+                pr2serr("Aborted command(in), continuing\n");
+                res = scsi_read_capacity(infd, &in_num_sect, &in_sect_sz);
+            }
+            if (0 != res) {
+                sg_get_category_sense_str(res, sizeof(b), b, verbose);
+                pr2serr("Read capacity (if=%s): %s\n", inf, b);
+                in_num_sect = -1;
+            }
+        } else if (FT_BLOCK == in_type) {
+            if (0 != read_blkdev_capacity(infd, &in_num_sect, &in_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", inf);
+                in_num_sect = -1;
+            }
+            if (blk_sz != in_sect_sz) {
+                pr2serr("logical block size on %s confusion; bs=%d, from "
+                        "device=%d\n", inf, blk_sz, in_sect_sz);
+                in_num_sect = -1;
+            }
+        }
+        if (in_num_sect > skip)
+            in_num_sect -= skip;
+
+        out_num_sect = -1;
+        if (FT_SG == out_type) {
+            res = scsi_read_capacity(outfd, &out_num_sect, &out_sect_sz);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Unit attention(out), continuing\n");
+                res = scsi_read_capacity(outfd, &out_num_sect, &out_sect_sz);
+            } else if (SG_LIB_CAT_ABORTED_COMMAND == res) {
+                pr2serr("Aborted command(out), continuing\n");
+                res = scsi_read_capacity(outfd, &out_num_sect, &out_sect_sz);
+            }
+            if (0 != res) {
+                sg_get_category_sense_str(res, sizeof(b), b, verbose);
+                pr2serr("Read capacity (of=%s): %s\n", inf, b);
+                out_num_sect = -1;
+            }
+        } else if (FT_BLOCK == out_type) {
+            if (0 != read_blkdev_capacity(outfd, &out_num_sect,
+                                          &out_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", outf);
+                out_num_sect = -1;
+            }
+            if (blk_sz != out_sect_sz) {
+                pr2serr("logical block size on %s confusion: bs=%d, from "
+                        "device=%d\n", outf, blk_sz, out_sect_sz);
+                out_num_sect = -1;
+            }
+        }
+        if (out_num_sect > seek)
+            out_num_sect -= seek;
+#ifdef DEBUG
+        pr2serr("Start of loop, count=%" PRId64 ", in_num_sect=%" PRId64 ", "
+                "out_num_sect=%" PRId64 "\n", dd_count, in_num_sect,
+                out_num_sect);
+#endif
+        if (in_num_sect > 0) {
+            if (out_num_sect > 0)
+                dd_count = (in_num_sect > out_num_sect) ? out_num_sect :
+                                                          in_num_sect;
+            else
+                dd_count = in_num_sect;
+        }
+        else
+            dd_count = out_num_sect;
+    }
+
+    if (dd_count < 0) {
+        pr2serr("Couldn't calculate count, please give one\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (! cdbsz_given) {
+        if ((FT_SG == in_type) && (MAX_SCSI_CDBSZ != scsi_cdbsz_in) &&
+            (((dd_count + skip) > UINT_MAX) || (bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'if')\n");
+            scsi_cdbsz_in = MAX_SCSI_CDBSZ;
+        }
+        if ((FT_SG == out_type) && (MAX_SCSI_CDBSZ != scsi_cdbsz_out) &&
+            (((dd_count + seek) > UINT_MAX) || (bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'of')\n");
+            scsi_cdbsz_out = MAX_SCSI_CDBSZ;
+        }
+    }
+
+    if (out_flags.dio && (FT_SG != in_type)) {
+        out_flags.dio = false;
+        pr2serr(">>> dio only performed on 'of' side when 'if' is an sg "
+                "device\n");
+    }
+    if (out_flags.dio) {
+        int fd;
+        char c;
+
+        if ((fd = open(sg_allow_dio, O_RDONLY)) >= 0) {
+            if (1 == read(fd, &c, 1)) {
+                if ('0' == c)
+                    pr2serr(">>> %s set to '0' but should be set to '1' for "
+                            "direct IO\n", sg_allow_dio);
+            }
+            close(fd);
+        }
+    }
+
+    if (wrkMmap) {
+        wrkPos = wrkMmap;
+    } else {
+        wrkPos = (uint8_t *)sg_memalign(blk_sz * bpt, 0, &wrkBuff,
+                                        verbose > 3);
+        if (NULL == wrkPos) {
+            pr2serr("Not enough user memory\n");
+            return sg_convert_errno(ENOMEM);
+        }
+    }
+
+    blocks_per = bpt;
+#ifdef DEBUG
+    pr2serr("Start of loop, count=%" PRId64 ", blocks_per=%d\n", dd_count,
+            blocks_per);
+#endif
+    if (dry_run > 0)
+        goto fini;
+
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+        start_tm_valid = true;
+    }
+    req_count = dd_count;
+
+    if (verbose && (dd_count > 0) && (! out_flags.dio) &&
+        (FT_SG == in_type) && (FT_SG == out_type))
+        pr2serr("Since both 'if' and 'of' are sg devices, only do mmap-ed "
+                "transfers on 'if'\n");
+
+    while (dd_count > 0) {
+        blocks = (dd_count > blocks_per) ? blocks_per : dd_count;
+        if (FT_SG == in_type) {
+            ret = sg_read(infd, wrkPos, blocks, skip, blk_sz, scsi_cdbsz_in,
+                          in_flags.fua, in_flags.dpo, true);
+            if ((SG_LIB_CAT_UNIT_ATTENTION == ret) ||
+                (SG_LIB_CAT_ABORTED_COMMAND == ret)) {
+                pr2serr("Unit attention or aborted command, continuing "
+                        "(r)\n");
+                ret = sg_read(infd, wrkPos, blocks, skip, blk_sz,
+                              scsi_cdbsz_in, in_flags.fua, in_flags.dpo,
+                              true);
+            }
+            if (0 != ret) {
+                pr2serr("sg_read failed, skip=%" PRId64 "\n", skip);
+                break;
+            }
+            else
+                in_full += blocks;
+        }
+        else {
+            while (((res = read(infd, wrkPos, blocks * blk_sz)) < 0) &&
+                   ((EINTR == errno) || (EAGAIN == errno) ||
+                    (EBUSY == errno)))
+                ;
+            if (verbose > 2)
+                pr2serr("read(unix): count=%d, res=%d\n", blocks * blk_sz,
+                        res);
+            if (ret < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "reading, skip=%" PRId64 " ",
+                         skip);
+                perror(ebuff);
+                ret = -1;
+                break;
+            }
+            else if (res < blocks * blk_sz) {
+                dd_count = 0;
+                blocks = res / blk_sz;
+                if ((res % blk_sz) > 0) {
+                    blocks++;
+                    in_partial++;
+                }
+            }
+            in_full += blocks;
+        }
+
+        if (0 == blocks)
+            break;      /* read nothing so leave loop */
+
+        if (FT_SG == out_type) {
+            bool dio_res = out_flags.dio;
+            bool do_mmap = (FT_SG != in_type);
+
+            ret = sg_write(outfd, wrkPos, blocks, seek, blk_sz, scsi_cdbsz_out,
+                           out_flags.fua, out_flags.dpo, do_mmap, &dio_res);
+            if ((SG_LIB_CAT_UNIT_ATTENTION == ret) ||
+                (SG_LIB_CAT_ABORTED_COMMAND == ret)) {
+                pr2serr("Unit attention or aborted command, continuing (w)\n");
+                dio_res = out_flags.dio;
+                ret = sg_write(outfd, wrkPos, blocks, seek, blk_sz,
+                               scsi_cdbsz_out, out_flags.fua, out_flags.dpo,
+                               do_mmap, &dio_res);
+            }
+            if (0 != ret) {
+                pr2serr("sg_write failed, seek=%" PRId64 "\n", seek);
+                break;
+            }
+            else {
+                out_full += blocks;
+                if (out_flags.dio && (! dio_res))
+                    num_dio_not_done++;
+            }
+        }
+        else if (FT_DEV_NULL == out_type)
+            out_full += blocks; /* act as if written out without error */
+        else {
+            while (((res = write(outfd, wrkPos, blocks * blk_sz)) < 0) &&
+                   ((EINTR == errno) || (EAGAIN == errno) ||
+                    (EBUSY == errno)))
+                ;
+            if (verbose > 2)
+                pr2serr("write(unix): count=%d, res=%d\n", blocks * blk_sz,
+                        res);
+            if (res < 0) {
+                snprintf(ebuff, EBUFF_SZ, ME "writing, seek=%" PRId64 " ",
+                         seek);
+                perror(ebuff);
+                break;
+            }
+            else if (res < blocks * blk_sz) {
+                pr2serr("output file probably full, seek=%" PRId64 " ", seek);
+                blocks = res / blk_sz;
+                out_full += blocks;
+                if ((res % blk_sz) > 0)
+                    out_partial++;
+                break;
+            }
+            else
+                out_full += blocks;
+        }
+        if (dd_count > 0)
+            dd_count -= blocks;
+        skip += blocks;
+        seek += blocks;
+    }
+
+    if (do_time)
+        calc_duration_throughput(false);
+    if (do_sync) {
+        if (FT_SG == out_type) {
+            pr2serr(">> Synchronizing cache on %s\n", outf);
+            res = sg_ll_sync_cache_10(outfd, 0, 0, 0, 0, 0, false, 0);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Unit attention(out), continuing\n");
+                res = sg_ll_sync_cache_10(outfd, 0, 0, 0, 0, 0, false, 0);
+            }
+            if (0 != res) {
+                sg_get_category_sense_str(res, sizeof(b), b, verbose);
+                pr2serr("Synchronize cache(out): %s\n", b);
+            }
+        }
+    }
+
+fini:
+    if (wrkBuff)
+        free(wrkBuff);
+    if ((STDIN_FILENO != infd) && (infd >= 0))
+        close(infd);
+    if ((STDOUT_FILENO != outfd) && (FT_DEV_NULL != out_type)) {
+        if (outfd >= 0)
+            close(outfd);
+    }
+    if ((0 != dd_count) && (0 == dry_run)) {
+        pr2serr("Some error occurred,");
+        if (0 == ret)
+            ret = SG_LIB_CAT_OTHER;
+    }
+    print_stats();
+    if (sum_of_resids)
+        pr2serr(">> Non-zero sum of residual counts=%d\n", sum_of_resids);
+    if (num_dio_not_done)
+        pr2serr(">> dio requested but _not_ done %d times\n",
+                num_dio_not_done);
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/src/sgp_dd.c b/src/sgp_dd.c
new file mode 100644
index 0000000..6a039f4
--- /dev/null
+++ b/src/sgp_dd.c
@@ -0,0 +1,2019 @@
+/* A utility program for copying files. Specialised for "files" that
+ * represent devices that understand the SCSI command set.
+ *
+ * Copyright (C) 1999 - 2022 D. Gilbert and P. Allworth
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is a specialisation of the Unix "dd" command in which
+ * one or both of the given files is a scsi generic device or a raw
+ * device. A logical block size ('bs') is assumed to be 512 if not given.
+ * This program complains if 'ibs' or 'obs' are given with some other value
+ * than 'bs'. If 'if' is not given or 'if=-' then stdin is assumed. If
+ * 'of' is not given or 'of=-' then stdout assumed.
+ *
+ * A non-standard argument "bpt" (blocks per transfer) is added to control
+ * the maximum number of blocks in each transfer. The default value is 128.
+ * For example if "bs=512" and "bpt=32" then a maximum of 32 blocks (16 KiB
+ * in this case) are transferred to or from the sg device in a single SCSI
+ * command.
+ *
+ * This version is designed for the Linux kernel 2.4, 2.6, 3, 4 and 5 series.
+ *
+ * sgp_dd is a Posix threads specialization of the sg_dd utility. Both
+ * sgp_dd and sg_dd only perform special tasks when one or both of the given
+ * devices belong to the Linux sg driver
+ */
+
+#define _XOPEN_SOURCE 600
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <pthread.h>
+#include <signal.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/mman.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#ifndef major
+#include <sys/types.h>
+#endif
+#include <sys/time.h>
+#include <linux/major.h>        /* for MEM_MAJOR, SCSI_GENERIC_MAJOR, etc */
+#include <linux/fs.h>           /* for BLKSSZGET and friends */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifdef __STDC_VERSION__
+#if __STDC_VERSION__ >= 201112L && defined(HAVE_STDATOMIC_H)
+#ifndef __STDC_NO_ATOMICS__
+
+#define HAVE_C11_ATOMICS
+#include <stdatomic.h>
+
+#endif
+#endif
+#endif
+
+#ifndef HAVE_C11_ATOMICS
+#warning "Don't have C11 Atomics, using mutex with pack_id"
+#endif
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+static const char * version_str = "5.84 20220118";
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BLOCKS_PER_TRANSFER 128
+#define DEF_BLOCKS_PER_2048TRANSFER 32
+#define DEF_SCSI_CDBSZ 10
+#define MAX_SCSI_CDBSZ 16
+#define MAX_BPT_VALUE (1 << 24)         /* used for maximum bs as well */
+#define MAX_COUNT_SKIP_SEEK (1LL << 48) /* coverity wants upper bound */
+
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define READ_CAP_REPLY_LEN 8
+#define RCAP16_REPLY_LEN 32
+
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs == 60 seconds */
+
+#define SGP_READ10 0x28
+#define SGP_WRITE10 0x2a
+#define DEF_NUM_THREADS 4
+#define MAX_NUM_THREADS 1024  /* was SG_MAX_QUEUE (16) but no longer applies */
+
+#ifndef RAW_MAJOR
+#define RAW_MAJOR 255   /*unlikely value */
+#endif
+
+#define FT_OTHER 1              /* filetype other than one of the following */
+#define FT_SG 2                 /* filetype is sg char device */
+#define FT_RAW 4                /* filetype is raw char device */
+#define FT_DEV_NULL 8           /* either "/dev/null" or "." as filename */
+#define FT_ST 16                /* filetype is st char device (tape) */
+#define FT_BLOCK 32             /* filetype is a block device */
+#define FT_ERROR 64             /* couldn't "stat" file */
+
+#define DEV_NULL_MINOR_NUM 3
+
+#define EBUFF_SZ 768
+
+#ifndef SG_FLAG_MMAP_IO
+#define SG_FLAG_MMAP_IO 4
+#endif
+
+#define STR_SZ 1024
+#define INOUTF_SZ 512
+
+
+struct flags_t {
+    bool append;
+    bool coe;
+    bool dio;
+    bool direct;
+    bool dpo;
+    bool dsync;
+    bool excl;
+    bool fua;
+    bool mmap;
+};
+
+struct opts_t
+{       /* one instance visible to all threads */
+    int infd;
+    int64_t skip;
+    int in_type;
+    int cdbsz_in;
+    struct flags_t in_flags;
+    int64_t in_blk;                 /* next block address to read */
+    int64_t in_count;               /* blocks remaining for next read */
+    int64_t in_rem_count;           /* count of remaining in blocks */
+    int in_partial;
+    pthread_mutex_t inout_mutex;
+    int outfd;
+    int64_t seek;
+    int out_type;
+    int cdbsz_out;
+    struct flags_t out_flags;
+    int64_t out_blk;                /* next block address to write */
+    int64_t out_count;              /* blocks remaining for next write */
+    int64_t out_rem_count;          /* count of remaining out blocks */
+    int out_partial;
+    pthread_cond_t out_sync_cv;
+    int bs;
+    int bpt;
+    int num_threads;
+    int dio_incomplete_count;
+    int sum_of_resids;
+    bool mmap_active;
+    int chkaddr;        /* check read data contains 4 byte, big endian block
+                         * addresses, once: check only 4 bytes per block */
+    int progress;       /* accept --progress or -p, does nothing */
+    int debug;
+    int dry_run;
+};
+
+struct thread_arg
+{       /* pointer to this argument passed to thread */
+    int id;
+    int64_t seek_skip;
+};
+
+typedef struct request_element
+{       /* one instance per worker thread */
+    bool wr;
+    bool in_stop;
+    bool in_err;
+    bool out_err;
+    bool use_no_dxfer;
+    int infd;
+    int outfd;
+    int64_t blk;
+    int num_blks;
+    uint8_t * buffp;
+    uint8_t * alloc_bp;
+    struct sg_io_hdr io_hdr;
+    uint8_t cdb[MAX_SCSI_CDBSZ];
+    uint8_t sb[SENSE_BUFF_LEN];
+    int bs;
+    int dio_incomplete_count;
+    int resid;
+    int cdbsz_in;
+    int cdbsz_out;
+    struct flags_t in_flags;
+    struct flags_t out_flags;
+    int debug;
+    uint32_t pack_id;
+} Rq_elem;
+
+static sigset_t signal_set;
+static pthread_t sig_listen_thread_id;
+
+static const char * sg_allow_dio = "/sys/module/sg/parameters/allow_dio";
+
+static void sg_in_operation(struct opts_t * clp, Rq_elem * rep);
+static void sg_out_operation(struct opts_t * clp, Rq_elem * rep,
+                             bool bump_out_blk);
+static void normal_in_operation(struct opts_t * clp, Rq_elem * rep,
+                                int blocks);
+static void normal_out_operation(struct opts_t * clp, Rq_elem * rep,
+                                 int blocks, bool bump_out_blk);
+static int sg_start_io(Rq_elem * rep);
+static int sg_finish_io(bool wr, Rq_elem * rep, pthread_mutex_t * a_mutp);
+
+#ifdef HAVE_C11_ATOMICS
+
+/* Assume initialized to 0, but want to start at 1, hence adding 1 in macro */
+static atomic_uint ascending_val;
+
+static atomic_uint num_eintr;
+static atomic_uint num_eagain;
+static atomic_uint num_ebusy;
+static atomic_bool exit_threads;
+
+#define GET_NEXT_PACK_ID(_v) (atomic_fetch_add(&ascending_val, _v) + (_v))
+
+#else
+
+static pthread_mutex_t av_mut = PTHREAD_MUTEX_INITIALIZER;
+static int ascending_val = 1;
+static volatile bool exit_threads;
+
+static unsigned int
+GET_NEXT_PACK_ID(unsigned int val)
+{
+    int res;
+
+    pthread_mutex_lock(&av_mut);
+    res = ascending_val;
+    ascending_val += val;
+    pthread_mutex_unlock(&av_mut);
+    return res;
+}
+
+#endif
+
+#define STRERR_BUFF_LEN 128
+
+static pthread_mutex_t strerr_mut = PTHREAD_MUTEX_INITIALIZER;
+
+static pthread_t threads[MAX_NUM_THREADS];
+static struct thread_arg thr_arg_a[MAX_NUM_THREADS];
+
+static bool shutting_down = false;
+static bool do_sync = false;
+static bool do_time = false;
+static struct opts_t my_opts;
+static struct timeval start_tm;
+static int64_t dd_count = -1;
+static int exit_status = 0;
+static char infn[INOUTF_SZ];
+static char outfn[INOUTF_SZ];
+
+static const char * my_name = "sgp_dd: ";
+
+
+static void
+calc_duration_throughput(int contin)
+{
+    struct timeval end_tm, res_tm;
+    double a, b;
+
+    gettimeofday(&end_tm, NULL);
+    res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+    res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+    if (res_tm.tv_usec < 0) {
+        --res_tm.tv_sec;
+        res_tm.tv_usec += 1000000;
+    }
+    a = res_tm.tv_sec;
+    a += (0.000001 * res_tm.tv_usec);
+    b = (double)my_opts.bs * (dd_count - my_opts.out_rem_count);
+    pr2serr("time to transfer data %s %d.%06d secs",
+            (contin ? "so far" : "was"), (int)res_tm.tv_sec,
+            (int)res_tm.tv_usec);
+    if ((a > 0.00001) && (b > 511))
+        pr2serr(", %.2f MB/sec\n", b / (a * 1000000.0));
+    else
+        pr2serr("\n");
+}
+
+static void
+print_stats(const char * str)
+{
+    int64_t infull, outfull;
+
+    if (0 != my_opts.out_rem_count)
+        pr2serr("  remaining block count=%" PRId64 "\n",
+                my_opts.out_rem_count);
+    infull = dd_count - my_opts.in_rem_count;
+    pr2serr("%s%" PRId64 "+%d records in\n", str,
+            infull - my_opts.in_partial, my_opts.in_partial);
+
+    outfull = dd_count - my_opts.out_rem_count;
+    pr2serr("%s%" PRId64 "+%d records out\n", str,
+            outfull - my_opts.out_partial, my_opts.out_partial);
+}
+
+static void
+interrupt_handler(int sig)
+{
+    struct sigaction sigact;
+
+    sigact.sa_handler = SIG_DFL;
+    sigemptyset(&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigaction(sig, &sigact, NULL);
+    pr2serr("Interrupted by signal,");
+    if (do_time)
+        calc_duration_throughput(0);
+    print_stats("");
+    kill(getpid (), sig);
+}
+
+static void
+siginfo_handler(int sig)
+{
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    pr2serr("Progress report, continuing ...\n");
+    if (do_time)
+        calc_duration_throughput(1);
+    print_stats("  ");
+}
+
+static void
+install_handler(int sig_num, void (*sig_handler) (int sig))
+{
+    struct sigaction sigact;
+    sigaction (sig_num, NULL, &sigact);
+    if (sigact.sa_handler != SIG_IGN)
+    {
+        sigact.sa_handler = sig_handler;
+        sigemptyset (&sigact.sa_mask);
+        sigact.sa_flags = 0;
+        sigaction (sig_num, &sigact, NULL);
+    }
+}
+
+#ifdef SG_LIB_ANDROID
+static void
+thread_exit_handler(int sig)
+{
+    pthread_exit(0);
+}
+#endif
+
+/* Make safe_strerror() thread safe */
+static char *
+tsafe_strerror(int code, char * ebp)
+{
+    int status;
+    char * cp;
+
+    status = pthread_mutex_lock(&strerr_mut);
+    if (0 != status) pr2serr("lock strerr_mut");
+    cp = safe_strerror(code);
+    strncpy(ebp, cp, STRERR_BUFF_LEN);
+    status = pthread_mutex_unlock(&strerr_mut);
+    if (0 != status) pr2serr("unlock strerr_mut");
+    ebp[STRERR_BUFF_LEN - 1] = '\0';
+    return ebp;
+}
+
+
+/* Following macro from D.R. Butenhof's POSIX threads book:
+ * ISBN 0-201-63392-2 . [Highly recommended book.] Changed __FILE__
+ * to __func__ */
+#define err_exit(code,text) do { \
+    char _strerr_buff[STRERR_BUFF_LEN + 1]; \
+    pr2serr("%s at \"%s\":%d: %s\n", \
+        text, __func__, __LINE__, tsafe_strerror(code, _strerr_buff)); \
+    exit(1); \
+    } while (0)
+
+
+static int
+dd_filetype(const char * filename)
+{
+    struct stat st;
+    size_t len = strlen(filename);
+
+    if ((1 == len) && ('.' == filename[0]))
+        return FT_DEV_NULL;
+    if (stat(filename, &st) < 0)
+        return FT_ERROR;
+    if (S_ISCHR(st.st_mode)) {
+        if ((MEM_MAJOR == major(st.st_rdev)) &&
+            (DEV_NULL_MINOR_NUM == minor(st.st_rdev)))
+            return FT_DEV_NULL;
+        if (RAW_MAJOR == major(st.st_rdev))
+            return FT_RAW;
+        if (SCSI_GENERIC_MAJOR == major(st.st_rdev))
+            return FT_SG;
+        if (SCSI_TAPE_MAJOR == major(st.st_rdev))
+            return FT_ST;
+    } else if (S_ISBLK(st.st_mode))
+        return FT_BLOCK;
+    return FT_OTHER;
+}
+
+static void
+usage()
+{
+    pr2serr("Usage: sgp_dd  [bs=BS] [count=COUNT] [ibs=BS] [if=IFILE]"
+            " [iflag=FLAGS]\n"
+            "               [obs=BS] [of=OFILE] [oflag=FLAGS] "
+            "[seek=SEEK] [skip=SKIP]\n"
+            "               [--help] [--version]\n\n");
+    pr2serr("               [bpt=BPT] [cdbsz=6|10|12|16] [coe=0|1] "
+            "[deb=VERB] [dio=0|1]\n"
+            "               [fua=0|1|2|3] [sync=0|1] [thr=THR] "
+            "[time=0|1] [verbose=VERB]\n"
+            "               [--dry-run] [--verbose]\n"
+            "  where:\n"
+            "    bpt         is blocks_per_transfer (default is 128)\n"
+            "    bs          must be device logical block size (default "
+            "512)\n"
+            "    cdbsz       size of SCSI READ or WRITE cdb (default is 10)\n"
+            "    coe         continue on error, 0->exit (def), "
+            "1->zero + continue\n"
+            "    count       number of blocks to copy (def: device size)\n"
+            "    deb         for debug, 0->none (def), > 0->varying degrees "
+            "of debug\n");
+    pr2serr("    dio         is direct IO, 1->attempt, 0->indirect IO (def)\n"
+            "    fua         force unit access: 0->don't(def), 1->OFILE, "
+            "2->IFILE,\n"
+            "                3->OFILE+IFILE\n"
+            "    if          file or device to read from (def: stdin)\n"
+            "    iflag       comma separated list from: [coe,dio,direct,dpo,"
+            "dsync,excl,\n"
+            "                fua,mmap,null]\n"
+            "    of          file or device to write to (def: stdout), "
+            "OFILE of '.'\n"
+            "                treated as /dev/null\n"
+            "    oflag       comma separated list from: [append,coe,dio,"
+            "direct,dpo,\n"
+            "                dsync,excl,fua,mmap,null]\n"
+            "    seek        block position to start writing to OFILE\n"
+            "    skip        block position to start reading from IFILE\n"
+            "    sync        0->no sync(def), 1->SYNCHRONIZE CACHE on OFILE "
+            "after copy\n"
+            "    thr         is number of threads, must be > 0, default 4, "
+            "max 1024\n"
+            "    time        0->no timing(def), 1->time plus calculate "
+            "throughput\n"
+            "    verbose     same as 'deb=VERB': increase verbosity\n"
+            "    --chkaddr|-c    check read data contains blk address\n"
+            "    --dry-run|-d    prepare but bypass copy/read\n"
+            "    --help|-h      output this usage message then exit\n"
+            "    --verbose|-v   increase verbosity of utility\n"
+            "    --version|-V   output version string then exit\n"
+            "Copy from IFILE to OFILE, similar to dd command\n"
+            "specialized for SCSI devices, uses multiple POSIX threads\n");
+}
+
+static int
+sgp_mem_mmap(int fd, int res_sz, uint8_t ** mmpp)
+{
+    int t;
+
+    if (ioctl(fd, SG_GET_RESERVED_SIZE, &t) < 0) {
+        perror("SG_GET_RESERVED_SIZE error");
+        return -1;
+    }
+    if (t < (int)sg_get_page_size())
+        t = sg_get_page_size();
+    if (res_sz > t) {
+        if (ioctl(fd, SG_SET_RESERVED_SIZE, &res_sz) < 0) {
+            perror("SG_SET_RESERVED_SIZE error");
+            return -1;
+        }
+    }
+    *mmpp = (uint8_t *)mmap(NULL, res_sz,
+                            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+    if (MAP_FAILED == *mmpp) {
+        perror("mmap() failed");
+        return -1;
+    }
+    return 0;
+}
+
+/* Return of 0 -> success, see sg_ll_read_capacity*() otherwise */
+static int
+scsi_read_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+    int res;
+    uint8_t rcBuff[RCAP16_REPLY_LEN];
+
+    res = sg_ll_readcap_10(sg_fd, 0, 0, rcBuff, READ_CAP_REPLY_LEN, false, 0);
+    if (0 != res)
+        return res;
+
+    if ((0xff == rcBuff[0]) && (0xff == rcBuff[1]) && (0xff == rcBuff[2]) &&
+        (0xff == rcBuff[3])) {
+
+        res = sg_ll_readcap_16(sg_fd, 0, 0, rcBuff, RCAP16_REPLY_LEN, false,
+                               0);
+        if (0 != res)
+            return res;
+        *num_sect = sg_get_unaligned_be64(rcBuff + 0) + 1;
+        *sect_sz = sg_get_unaligned_be32(rcBuff + 8);
+    } else {
+        /* take care not to sign extend values > 0x7fffffff */
+        *num_sect = (int64_t)sg_get_unaligned_be32(rcBuff + 0) + 1;
+        *sect_sz = sg_get_unaligned_be32(rcBuff + 4);
+    }
+    return 0;
+}
+
+/* Return of 0 -> success, -1 -> failure. BLKGETSIZE64, BLKGETSIZE and */
+/* BLKSSZGET macros problematic (from <linux/fs.h> or <sys/mount.h>). */
+static int
+read_blkdev_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+#ifdef BLKSSZGET
+    if ((ioctl(sg_fd, BLKSSZGET, sect_sz) < 0) && (*sect_sz > 0)) {
+        perror("BLKSSZGET ioctl error");
+        return -1;
+    } else {
+ #ifdef BLKGETSIZE64
+        uint64_t ull;
+
+        if (ioctl(sg_fd, BLKGETSIZE64, &ull) < 0) {
+
+            perror("BLKGETSIZE64 ioctl error");
+            return -1;
+        }
+        *num_sect = ((int64_t)ull / (int64_t)*sect_sz);
+ #else
+        unsigned long ul;
+
+        if (ioctl(sg_fd, BLKGETSIZE, &ul) < 0) {
+            perror("BLKGETSIZE ioctl error");
+            return -1;
+        }
+        *num_sect = (int64_t)ul;
+ #endif
+    }
+    return 0;
+#else
+    *num_sect = 0;
+    *sect_sz = 0;
+    return -1;
+#endif
+}
+
+static void *
+sig_listen_thread(void * v_clp)
+{
+    struct opts_t * clp = (struct opts_t *)v_clp;
+    int sig_number;
+
+    while (1) {
+        sigwait(&signal_set, &sig_number);
+        if (shutting_down)
+            break;
+        if (SIGINT == sig_number) {
+            pr2serr("%sinterrupted by SIGINT\n", my_name);
+#ifdef HAVE_C11_ATOMICS
+            atomic_store(&exit_threads, true);
+#else
+            exit_threads = true;
+#endif
+            pthread_cond_broadcast(&clp->out_sync_cv);
+        }
+    }
+    return NULL;
+}
+
+static void
+cleanup_in(void * v_clp)
+{
+    struct opts_t * clp = (struct opts_t *)v_clp;
+
+    pr2serr("thread cancelled while in mutex held\n");
+    pthread_mutex_unlock(&clp->inout_mutex);
+    pthread_cond_broadcast(&clp->out_sync_cv);
+}
+
+static void
+cleanup_out(void * v_clp)
+{
+    struct opts_t * clp = (struct opts_t *)v_clp;
+
+    pr2serr("thread cancelled while out mutex held\n");
+    pthread_mutex_unlock(&clp->inout_mutex);
+    pthread_cond_broadcast(&clp->out_sync_cv);
+}
+
+static int
+sg_prepare(int fd, int bs, int bpt)
+{
+    int res, t;
+
+    res = ioctl(fd, SG_GET_VERSION_NUM, &t);
+    if ((res < 0) || (t < 30000)) {
+        pr2serr("%ssg driver prior to 3.x.y\n", my_name);
+        return 1;
+    }
+    t = bs * bpt;
+    res = ioctl(fd, SG_SET_RESERVED_SIZE, &t);
+    if (res < 0)
+        perror("sgp_dd: SG_SET_RESERVED_SIZE error");
+    t = 1;
+    res = ioctl(fd, SG_SET_FORCE_PACK_ID, &t);
+    if (res < 0)
+        perror("sgp_dd: SG_SET_FORCE_PACK_ID error");
+    return 0;
+}
+
+static int
+sg_in_open(const char * fnp, struct flags_t * flagp, int bs, int bpt)
+{
+    int flags = O_RDWR;
+    int fd, err;
+    char ebuff[800];
+
+    if (flagp->direct)
+        flags |= O_DIRECT;
+    if (flagp->excl)
+        flags |= O_EXCL;
+    if (flagp->dsync)
+        flags |= O_SYNC;
+
+    if ((fd = open(fnp, flags)) < 0) {
+        err = errno;
+        snprintf(ebuff, EBUFF_SZ, "%scould not open %s for sg "
+                 "reading", my_name, fnp);
+        perror(ebuff);
+        return -sg_convert_errno(err);
+    }
+    if (sg_prepare(fd, bs, bpt)) {
+        close(fd);
+        return -SG_LIB_FILE_ERROR;
+    }
+    return fd;
+}
+
+static int
+sg_out_open(const char * fnp, struct flags_t * flagp, int bs, int bpt)
+{
+    int flags = O_RDWR;
+    int fd, err;
+    char ebuff[800];
+
+    if (flagp->direct)
+        flags |= O_DIRECT;
+    if (flagp->excl)
+        flags |= O_EXCL;
+    if (flagp->dsync)
+        flags |= O_SYNC;
+
+    if ((fd = open(fnp, flags)) < 0) {
+        err = errno;
+        snprintf(ebuff, EBUFF_SZ, "%scould not open %s for sg "
+                 "writing", my_name, fnp);
+        perror(ebuff);
+        return -sg_convert_errno(err);
+    }
+    if (sg_prepare(fd, bs, bpt)) {
+        close(fd);
+        return -SG_LIB_FILE_ERROR;
+    }
+    return fd;
+}
+
+static void *
+read_write_thread(void * v_tap)
+{
+    struct thread_arg * tap = (struct thread_arg *)v_tap;
+    struct opts_t * clp = &my_opts;
+    Rq_elem rel;
+    Rq_elem * rep = &rel;
+    volatile bool stop_after_write, bb;
+    bool enforce_write_ordering;
+    int sz, c_addr;
+    int64_t out_blk, out_count;
+    int64_t seek_skip = tap->seek_skip;
+    int blocks, status;
+
+    stop_after_write = false;
+    enforce_write_ordering = (FT_DEV_NULL != clp->out_type) &&
+                             (FT_SG != clp->out_type);
+    c_addr = clp->chkaddr;
+    memset(rep, 0, sizeof(*rep));
+    /* Following clp members are constant during lifetime of thread */
+    rep->bs = clp->bs;
+    if ((clp->num_threads > 1) && clp->mmap_active) {
+        /* sg devices need separate file descriptor */
+        if (clp->in_flags.mmap && (FT_SG == clp->in_type)) {
+            rep->infd = sg_in_open(infn, &clp->in_flags, rep->bs, clp->bpt);
+            if (rep->infd < 0) err_exit(-rep->infd, "error opening infn");
+        } else
+            rep->infd = clp->infd;
+        if (clp->out_flags.mmap && (FT_SG == clp->out_type)) {
+            rep->outfd = sg_out_open(outfn, &clp->out_flags, rep->bs,
+                                     clp->bpt);
+            if (rep->outfd < 0) err_exit(-rep->outfd, "error opening outfn");
+
+        } else
+            rep->outfd = clp->outfd;
+    } else {
+        rep->infd = clp->infd;
+        rep->outfd = clp->outfd;
+    }
+    sz = clp->bpt * rep->bs;
+    rep->debug = clp->debug;
+    rep->cdbsz_in = clp->cdbsz_in;
+    rep->cdbsz_out = clp->cdbsz_out;
+    rep->in_flags = clp->in_flags;
+    rep->out_flags = clp->out_flags;
+    rep->use_no_dxfer = (FT_DEV_NULL == clp->out_type);
+    if (clp->mmap_active) {
+        int fd = clp->in_flags.mmap ? rep->infd : rep->outfd;
+
+        status = sgp_mem_mmap(fd, sz, &rep->buffp);
+        if (status) err_exit(status, "sgp_mem_mmap() failed");
+    } else {
+        rep->buffp = sg_memalign(sz, 0 /* page align */, &rep->alloc_bp,
+                                 false);
+        if (NULL == rep->buffp)
+            err_exit(ENOMEM, "out of memory creating user buffers\n");
+    }
+
+    while(1) {
+        if ((rep->in_stop) || (rep->in_err) || (rep->out_err))
+            break;
+        status = pthread_mutex_lock(&clp->inout_mutex);
+        if (0 != status) err_exit(status, "lock inout_mutex");
+#ifdef HAVE_C11_ATOMICS
+        bb = atomic_load(&exit_threads);
+#else
+        bb = exit_threads;
+#endif
+        if (bb || (clp->in_count <= 0)) {
+            /* no more to do, exit loop then thread */
+            status = pthread_mutex_unlock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "unlock inout_mutex");
+            break;
+        }
+        blocks = (clp->in_count > clp->bpt) ? clp->bpt : clp->in_count;
+        rep->wr = false;
+        rep->blk = clp->in_blk;
+        rep->num_blks = blocks;
+        clp->in_blk += blocks;
+        clp->in_count -= blocks;
+        /* while we have this lock, find corresponding out_blk */
+        out_blk = rep->blk + seek_skip;
+        out_count = clp->out_count;
+        if (! enforce_write_ordering)
+            clp->out_blk += blocks;
+        clp->out_count -= blocks;
+        status = pthread_mutex_unlock(&clp->inout_mutex);
+        if (0 != status) err_exit(status, "unlock inout_mutex");
+
+        pthread_cleanup_push(cleanup_in, (void *)clp);
+        if (FT_SG == clp->in_type)
+            sg_in_operation(clp, rep);
+        else
+            normal_in_operation(clp, rep, blocks);
+        if (c_addr && (rep->bs > 3)) {
+            int k, j, off, num;
+            uint32_t addr = (uint32_t)rep->blk;
+
+            num = (1 == c_addr) ? 4 : (rep->bs - 3);
+            for (k = 0, off = 0; k < blocks; ++k, ++addr, off += rep->bs) {
+                for (j = 0; j < num; j += 4) {
+                    if (addr != sg_get_unaligned_be32(rep->buffp + off + j))
+                        break;
+                }
+                if (j < num)
+                    break;
+            }
+            if (k < blocks) {
+                pr2serr("%s: chkaddr failure at addr=0x%x\n", __func__, addr);
+                rep->in_err = true;
+            }
+        }
+        pthread_cleanup_pop(0);
+        if (rep->in_err) {
+            status = pthread_mutex_lock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "lock inout_mutex");
+            /* write-side not done, so undo changes to out_blk + out_count */
+            if (! enforce_write_ordering)
+                clp->out_blk -= blocks;
+            clp->out_count += blocks;
+            status = pthread_mutex_unlock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "unlock inout_mutex");
+            break;
+        }
+
+        if (enforce_write_ordering) {
+            status = pthread_mutex_lock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "lock inout_mutex");
+#ifdef HAVE_C11_ATOMICS
+            bb = atomic_load(&exit_threads);
+#else
+            bb = exit_threads;
+#endif
+            while ((! bb) && (out_blk != clp->out_blk)) {
+                /* if write would be out of sequence then wait */
+                pthread_cleanup_push(cleanup_out, (void *)clp);
+                status = pthread_cond_wait(&clp->out_sync_cv,
+                                           &clp->inout_mutex);
+                if (0 != status) err_exit(status, "cond out_sync_cv");
+                pthread_cleanup_pop(0);
+            }
+            status = pthread_mutex_unlock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "unlock inout_mutex");
+        }
+
+#ifdef HAVE_C11_ATOMICS
+        bb = atomic_load(&exit_threads);
+#else
+        bb = exit_threads;
+#endif
+        if (bb || (out_count <= 0))
+            break;
+
+        rep->wr = true;
+        rep->blk = out_blk;
+
+        if (0 == rep->num_blks) {
+            break;      /* read nothing so leave loop */
+        }
+
+        pthread_cleanup_push(cleanup_out, (void *)clp);
+        if (FT_SG == clp->out_type)
+            sg_out_operation(clp, rep, enforce_write_ordering);
+        else if (FT_DEV_NULL == clp->out_type) {
+            /* skip actual write operation */
+            clp->out_rem_count -= blocks;
+        }
+        else
+            normal_out_operation(clp, rep, blocks, enforce_write_ordering);
+        pthread_cleanup_pop(0);
+
+        if (enforce_write_ordering)
+            pthread_cond_broadcast(&clp->out_sync_cv);
+    } /* end of while loop */
+
+    if (rep->alloc_bp)
+        free(rep->alloc_bp);
+    if (rep->in_err || rep->out_err) {
+        stop_after_write = true;
+#ifdef HAVE_C11_ATOMICS
+        if (! atomic_load(&exit_threads))
+            atomic_store(&exit_threads, true);
+#else
+        if (! exit_threads)
+            exit_threads = true;
+#endif
+    }
+    pthread_cond_broadcast(&clp->out_sync_cv);
+    return (stop_after_write || rep->in_stop) ? NULL : clp;
+}
+
+static void
+normal_in_operation(struct opts_t * clp, Rq_elem * rep, int blocks)
+{
+    int res, status;
+    char strerr_buff[STRERR_BUFF_LEN + 1];
+
+    while (((res = read(rep->infd, rep->buffp, blocks * rep->bs)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno)))
+        ;
+    if (res < 0) {
+        if (rep->in_flags.coe) {
+            memset(rep->buffp, 0, rep->num_blks * rep->bs);
+            pr2serr(">> substituted zeros for in blk=%" PRId64 " for %d "
+                    "bytes, %s\n", rep->blk,
+                    rep->num_blks * rep->bs,
+                    tsafe_strerror(errno, strerr_buff));
+            res = rep->num_blks * rep->bs;
+        }
+        else {
+            pr2serr("error in normal read, %s\n",
+                    tsafe_strerror(errno, strerr_buff));
+            rep->in_stop = true;
+            rep->in_err = true;
+            return;
+        }
+    }
+    status = pthread_mutex_lock(&clp->inout_mutex);
+    if (0 != status) err_exit(status, "lock inout_mutex");
+    if (res < blocks * rep->bs) {
+        int o_blocks = blocks;
+
+        rep->in_stop = true;
+        blocks = res / rep->bs;
+        if ((res % rep->bs) > 0) {
+            blocks++;
+            clp->in_partial++;
+        }
+        /* Reverse out + re-apply blocks on clp */
+        clp->in_blk -= o_blocks;
+        clp->in_count += o_blocks;
+        rep->num_blks = blocks;
+        clp->in_blk += blocks;
+        clp->in_count -= blocks;
+    }
+    clp->in_rem_count -= blocks;
+    status = pthread_mutex_unlock(&clp->inout_mutex);
+    if (0 != status) err_exit(status, "lock inout_mutex");
+}
+
+static void
+normal_out_operation(struct opts_t * clp, Rq_elem * rep, int blocks,
+                     bool bump_out_blk)
+{
+    int res, status;
+    char strerr_buff[STRERR_BUFF_LEN + 1];
+
+    while (((res = write(rep->outfd, rep->buffp, rep->num_blks * rep->bs))
+            < 0) && ((EINTR == errno) || (EAGAIN == errno)))
+        ;
+    if (res < 0) {
+        if (rep->out_flags.coe) {
+            pr2serr(">> ignored error for out blk=%" PRId64 " for %d bytes, "
+                    "%s\n", rep->blk, rep->num_blks * rep->bs,
+                    tsafe_strerror(errno, strerr_buff));
+            res = rep->num_blks * rep->bs;
+        }
+        else {
+            pr2serr("error normal write, %s\n",
+                    tsafe_strerror(errno, strerr_buff));
+            rep->out_err = true;
+            return;
+        }
+    }
+    status = pthread_mutex_lock(&clp->inout_mutex);
+    if (0 != status) err_exit(status, "lock inout_mutex");
+    if (res < blocks * rep->bs) {
+        blocks = res / rep->bs;
+        if ((res % rep->bs) > 0) {
+            blocks++;
+            clp->out_partial++;
+        }
+        rep->num_blks = blocks;
+    }
+    clp->out_rem_count -= blocks;
+    if (bump_out_blk)
+        clp->out_blk += blocks;
+    status = pthread_mutex_unlock(&clp->inout_mutex);
+    if (0 != status) err_exit(status, "lock inout_mutex");
+}
+
+static int
+sg_build_scsi_cdb(uint8_t * cdbp, int cdb_sz, unsigned int blocks,
+                  int64_t start_block, bool write_true, bool fua, bool dpo)
+{
+    int rd_opcode[] = {0x8, 0x28, 0xa8, 0x88};
+    int wr_opcode[] = {0xa, 0x2a, 0xaa, 0x8a};
+    int sz_ind;
+
+    memset(cdbp, 0, cdb_sz);
+    if (dpo)
+        cdbp[1] |= 0x10;
+    if (fua)
+        cdbp[1] |= 0x8;
+    switch (cdb_sz) {
+    case 6:
+        sz_ind = 0;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be24(0x1fffff & start_block, cdbp + 1);
+        cdbp[4] = (256 == blocks) ? 0 : (uint8_t)blocks;
+        if (blocks > 256) {
+            pr2serr("%sfor 6 byte commands, maximum number of blocks is "
+                    "256\n", my_name);
+            return 1;
+        }
+        if ((start_block + blocks - 1) & (~0x1fffff)) {
+            pr2serr("%sfor 6 byte commands, can't address blocks beyond "
+                    "%d\n", my_name, 0x1fffff);
+            return 1;
+        }
+        if (dpo || fua) {
+            pr2serr("%sfor 6 byte commands, neither dpo nor fua bits "
+                    "supported\n", my_name);
+            return 1;
+        }
+        break;
+    case 10:
+        sz_ind = 1;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be16((uint16_t)blocks, cdbp + 7);
+        if (blocks & (~0xffff)) {
+            pr2serr("%sfor 10 byte commands, maximum number of blocks is "
+                    "%d\n", my_name, 0xffff);
+            return 1;
+        }
+        break;
+    case 12:
+        sz_ind = 2;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 6);
+        break;
+    case 16:
+        sz_ind = 3;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                               rd_opcode[sz_ind]);
+        sg_put_unaligned_be64((uint64_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 10);
+        break;
+    default:
+        pr2serr("%sexpected cdb size of 6, 10, 12, or 16 but got %d\n",
+                my_name, cdb_sz);
+        return 1;
+    }
+    return 0;
+}
+
+static void
+sg_in_operation(struct opts_t * clp, Rq_elem * rep)
+{
+    int res;
+    int status;
+
+    while (1) {
+        res = sg_start_io(rep);
+        if (1 == res)
+            err_exit(ENOMEM, "sg starting in command");
+        else if (res < 0) {
+            pr2serr("%sinputting to sg failed, blk=%" PRId64 "\n", my_name,
+                    rep->blk);
+            rep->in_stop = true;
+            rep->in_err = true;
+            return;
+        }
+        res = sg_finish_io(rep->wr, rep, &clp->inout_mutex);
+        switch (res) {
+        case SG_LIB_CAT_ABORTED_COMMAND:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            /* try again with same addr, count info */
+            /* now re-acquire in mutex for balance */
+            /* N.B. This re-read could now be out of read sequence */
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD:
+            if (0 == rep->in_flags.coe) {
+                pr2serr("error finishing sg in command (medium)\n");
+                if (exit_status <= 0)
+                    exit_status = res;
+                rep->in_stop = true;
+                rep->in_err = true;
+                return;
+            } else {
+                memset(rep->buffp, 0, rep->num_blks * rep->bs);
+                pr2serr(">> substituted zeros for in blk=%" PRId64 " for %d "
+                        "bytes\n", rep->blk, rep->num_blks * rep->bs);
+            }
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        case 0:
+            status = pthread_mutex_lock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "lock inout_mutex");
+            if (rep->dio_incomplete_count || rep->resid) {
+                clp->dio_incomplete_count += rep->dio_incomplete_count;
+                clp->sum_of_resids += rep->resid;
+            }
+            clp->in_rem_count -= rep->num_blks;
+            status = pthread_mutex_unlock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "unlock inout_mutex");
+            return;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            if (clp->debug)
+                sg_print_command_len(rep->cdb, rep->cdbsz_in);
+            /* FALL THROUGH */
+        default:
+            pr2serr("error finishing sg in command (%d)\n", res);
+            if (exit_status <= 0)
+                exit_status = res;
+            rep->in_stop = true;
+            rep->in_err = true;
+            return;
+        }
+    }   /* end of while loop */
+}
+
+static void
+sg_out_operation(struct opts_t * clp, Rq_elem * rep, bool bump_out_blk)
+{
+    int res;
+    int status;
+
+    while (1) {
+        res = sg_start_io(rep);
+        if (1 == res)
+            err_exit(ENOMEM, "sg starting out command");
+        else if (res < 0) {
+            pr2serr("%soutputting from sg failed, blk=%" PRId64 "\n",
+                    my_name, rep->blk);
+            rep->out_err = true;
+            return;
+        }
+        res = sg_finish_io(rep->wr, rep, &clp->inout_mutex);
+        switch (res) {
+        case SG_LIB_CAT_ABORTED_COMMAND:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            /* try again with same addr, count info */
+            /* now re-acquire out mutex for balance */
+            /* N.B. This re-write could now be out of write sequence */
+            break;
+        case SG_LIB_CAT_MEDIUM_HARD:
+            if (0 == rep->out_flags.coe) {
+                pr2serr("error finishing sg out command (medium)\n");
+                if (exit_status <= 0)
+                    exit_status = res;
+                rep->out_err = true;
+                return;
+            } else
+                pr2serr(">> ignored error for out blk=%" PRId64 " for %d "
+                        "bytes\n", rep->blk, rep->num_blks * rep->bs);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        case 0:
+            status = pthread_mutex_lock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "lock inout_mutex");
+            if (rep->dio_incomplete_count || rep->resid) {
+                clp->dio_incomplete_count += rep->dio_incomplete_count;
+                clp->sum_of_resids += rep->resid;
+            }
+            clp->out_rem_count -= rep->num_blks;
+            if (bump_out_blk)
+                clp->out_blk += rep->num_blks;
+            status = pthread_mutex_unlock(&clp->inout_mutex);
+            if (0 != status) err_exit(status, "unlock inout_mutex");
+            return;
+        case SG_LIB_CAT_ILLEGAL_REQ:
+            if (clp->debug)
+                sg_print_command_len(rep->cdb, rep->cdbsz_out);
+            /* FALL THROUGH */
+        default:
+            rep->out_err = true;
+            pr2serr("error finishing sg out command (%d)\n", res);
+            if (exit_status <= 0)
+                exit_status = res;
+            return;
+        }
+    }
+}
+
+static int
+sg_start_io(Rq_elem * rep)
+{
+    struct sg_io_hdr * hp = &rep->io_hdr;
+    bool fua = rep->wr ? rep->out_flags.fua : rep->in_flags.fua;
+    bool dpo = rep->wr ? rep->out_flags.dpo : rep->in_flags.dpo;
+    bool dio = rep->wr ? rep->out_flags.dio : rep->in_flags.dio;
+    bool mmap = rep->wr ? rep->out_flags.mmap : rep->in_flags.mmap;
+    bool no_dxfer = rep->wr ? false : rep->use_no_dxfer;
+    int cdbsz = rep->wr ? rep->cdbsz_out : rep->cdbsz_in;
+    int res;
+
+    if (sg_build_scsi_cdb(rep->cdb, cdbsz, rep->num_blks, rep->blk,
+                          rep->wr, fua, dpo)) {
+        pr2serr("%sbad cdb build, start_blk=%" PRId64 ", blocks=%d\n",
+                my_name, rep->blk, rep->num_blks);
+        return -1;
+    }
+    memset(hp, 0, sizeof(struct sg_io_hdr));
+    hp->interface_id = 'S';
+    hp->cmd_len = cdbsz;
+    hp->cmdp = rep->cdb;
+    hp->dxfer_direction = rep->wr ? SG_DXFER_TO_DEV : SG_DXFER_FROM_DEV;
+    hp->dxfer_len = rep->bs * rep->num_blks;
+    hp->dxferp = mmap ? NULL : rep->buffp;
+    hp->mx_sb_len = sizeof(rep->sb);
+    hp->sbp = rep->sb;
+    hp->timeout = DEF_TIMEOUT;
+    hp->usr_ptr = rep;
+    rep->pack_id = GET_NEXT_PACK_ID(1);
+    hp->pack_id = (int)rep->pack_id;
+    if (dio)
+        hp->flags |= SG_FLAG_DIRECT_IO;
+    if (mmap)
+        hp->flags |= SG_FLAG_MMAP_IO;
+    if (no_dxfer)
+        hp->flags |= SG_FLAG_NO_DXFER;
+    if (rep->debug > 8) {
+        pr2serr("%s: SCSI %s, blk=%" PRId64 " num_blks=%d\n", __func__,
+                rep->wr ? "WRITE" : "READ", rep->blk, rep->num_blks);
+        sg_print_command(hp->cmdp);
+    }
+
+    while (((res = write(rep->wr ? rep->outfd : rep->infd, hp,
+                         sizeof(struct sg_io_hdr))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno))) {
+#ifdef HAVE_C11_ATOMICS
+        if (EINTR == errno)
+            atomic_fetch_add(&num_eintr, 1);
+        else if (EAGAIN == errno)
+            atomic_fetch_add(&num_eagain, 1);
+        else
+            atomic_fetch_add(&num_ebusy, 1);
+#endif
+    }
+    if (res < 0) {
+        if (ENOMEM == errno)
+            return 1;
+        perror("starting io on sg device, error");
+        return -1;
+    }
+    return 0;
+}
+
+/* 0 -> successful, SG_LIB_CAT_UNIT_ATTENTION or SG_LIB_CAT_ABORTED_COMMAND
+   -> try again, SG_LIB_CAT_NOT_READY, SG_LIB_CAT_MEDIUM_HARD,
+   -1 other errors */
+static int
+sg_finish_io(bool wr, Rq_elem * rep, pthread_mutex_t * a_mutp)
+{
+    int res, status;
+    struct sg_io_hdr io_hdr;
+    struct sg_io_hdr * hp;
+#if 0
+    static int testing = 0;     /* thread dubious! */
+#endif
+
+    memset(&io_hdr, 0 , sizeof(struct sg_io_hdr));
+    /* FORCE_PACK_ID active set only read packet with matching pack_id */
+    io_hdr.interface_id = 'S';
+    io_hdr.dxfer_direction = wr ? SG_DXFER_TO_DEV : SG_DXFER_FROM_DEV;
+    io_hdr.pack_id = (int)rep->pack_id;
+
+    while (((res = read(wr ? rep->outfd : rep->infd, &io_hdr,
+                        sizeof(struct sg_io_hdr))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    if (res < 0) {
+        perror("finishing io on sg device, error");
+        return -1;
+    }
+    if (rep != (Rq_elem *)io_hdr.usr_ptr)
+        err_exit(0, "sg_finish_io: bad usr_ptr, request-response mismatch\n");
+    memcpy(&rep->io_hdr, &io_hdr, sizeof(struct sg_io_hdr));
+    hp = &rep->io_hdr;
+
+    res = sg_err_category3(hp);
+    switch (res) {
+        case SG_LIB_CAT_CLEAN:
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            sg_chk_n_print3((wr ? "writing continuing":
+                                       "reading continuing"), hp, false);
+            break;
+        case SG_LIB_CAT_ABORTED_COMMAND:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            if (rep->debug)
+                sg_chk_n_print3((wr ? "writing": "reading"), hp, false);
+            return res;
+        case SG_LIB_CAT_NOT_READY:
+        default:
+            rep->out_err = false;
+            if (rep->debug) {
+                char ebuff[EBUFF_SZ];
+
+                snprintf(ebuff, EBUFF_SZ, "%s blk=%" PRId64,
+                         wr ? "writing": "reading", rep->blk);
+                status = pthread_mutex_lock(a_mutp);
+                if (0 != status) err_exit(status, "lock inout_mutex");
+                sg_chk_n_print3(ebuff, hp, false);
+                status = pthread_mutex_unlock(a_mutp);
+                if (0 != status) err_exit(status, "unlock inout_mutex");
+            }
+            return res;
+    }
+#if 0
+    if (0 == (++testing % 100)) return -1;
+#endif
+    if ((wr ? rep->out_flags.dio : rep->in_flags.dio) &&
+        ((hp->info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+        rep->dio_incomplete_count = 1; /* count dios done as indirect IO */
+    else
+        rep->dio_incomplete_count = 0;
+    rep->resid = hp->resid;
+    if (rep->debug > 8)
+        pr2serr("%s: completed %s\n", __func__, wr ? "WRITE" : "READ");
+    return 0;
+}
+
+static int
+process_flags(const char * arg, struct flags_t * fp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no flag found\n");
+        return 1;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "append"))
+            fp->append = true;
+        else if (0 == strcmp(cp, "coe"))
+            fp->coe = true;
+        else if (0 == strcmp(cp, "dio"))
+            fp->dio = true;
+        else if (0 == strcmp(cp, "direct"))
+            fp->direct = true;
+        else if (0 == strcmp(cp, "dpo"))
+            fp->dpo = true;
+        else if (0 == strcmp(cp, "dsync"))
+            fp->dsync = true;
+        else if (0 == strcmp(cp, "excl"))
+            fp->excl = true;
+        else if (0 == strcmp(cp, "fua"))
+            fp->fua = true;
+        else if (0 == strcmp(cp, "mmap"))
+            fp->mmap = true;
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return 1;
+        }
+        cp = np;
+    } while (cp);
+    return 0;
+}
+
+/* Returns the number of times 'ch' is found in string 's' given the
+ * string's length. */
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool verbose_given = false;
+    bool version_given = false;
+    int64_t skip = 0;
+    int64_t seek = 0;
+    int ibs = 0;
+    int obs = 0;
+    int bpt_given = 0;
+    int cdbsz_given = 0;
+    char str[STR_SZ];
+    char * key;
+    char * buf;
+    int res, k, err, keylen;
+    int64_t in_num_sect = 0;
+    int64_t out_num_sect = 0;
+    int64_t seek_skip;
+    int in_sect_sz, out_sect_sz, status, n, flags;
+    void * vp;
+    struct opts_t * clp = &my_opts;
+    char ebuff[EBUFF_SZ];
+#if SG_LIB_ANDROID
+    struct sigaction actions;
+
+    memset(&actions, 0, sizeof(actions));
+    sigemptyset(&actions.sa_mask);
+    actions.sa_flags = 0;
+    actions.sa_handler = thread_exit_handler;
+    sigaction(SIGUSR1, &actions, NULL);
+#endif
+    memset(clp, 0, sizeof(*clp));
+    clp->num_threads = DEF_NUM_THREADS;
+    clp->bpt = DEF_BLOCKS_PER_TRANSFER;
+    clp->in_type = FT_OTHER;
+    clp->out_type = FT_OTHER;
+    clp->cdbsz_in = DEF_SCSI_CDBSZ;
+    clp->cdbsz_out = DEF_SCSI_CDBSZ;
+    infn[0] = '\0';
+    outfn[0] = '\0';
+
+    for (k = 1; k < argc; k++) {
+        if (argv[k]) {
+            strncpy(str, argv[k], STR_SZ);
+            str[STR_SZ - 1] = '\0';
+        }
+        else
+            continue;
+        for (key = str, buf = key; *buf && *buf != '=';)
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        keylen = strlen(key);
+        if (0 == strcmp(key,"bpt")) {
+            clp->bpt = sg_get_num(buf);
+            if ((clp->bpt < 0) || (clp->bpt > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'bpt='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            bpt_given = 1;
+        } else if (0 == strcmp(key,"bs")) {
+            clp->bs = sg_get_num(buf);
+            if ((clp->bs < 0) || (clp->bs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'bs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"cdbsz")) {
+            clp->cdbsz_in = sg_get_num(buf);
+            if ((clp->cdbsz_in < 6) || (clp->cdbsz_in > 32)) {
+                pr2serr("%s'cdbsz' expects 6, 10, 12, 16 or 32\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            clp->cdbsz_out = clp->cdbsz_in;
+            cdbsz_given = 1;
+        } else if (0 == strcmp(key,"coe")) {
+            clp->in_flags.coe = !! sg_get_num(buf);
+            clp->out_flags.coe = clp->in_flags.coe;
+        } else if (0 == strcmp(key,"count")) {
+            if (0 != strcmp("-1", buf)) {
+                dd_count = sg_get_llnum(buf);
+                if ((dd_count < 0) || (dd_count > MAX_COUNT_SKIP_SEEK)) {
+                    pr2serr("%sbad argument to 'count='\n", my_name);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }   /* treat 'count=-1' as calculate count (same as not given) */
+        } else if ((0 == strncmp(key,"deb", 3)) ||
+                   (0 == strncmp(key,"verb", 4)))
+            clp->debug = sg_get_num(buf);
+        else if (0 == strcmp(key,"dio")) {
+            clp->in_flags.dio = !! sg_get_num(buf);
+            clp->out_flags.dio = clp->in_flags.dio;
+        } else if (0 == strcmp(key,"fua")) {
+            n = sg_get_num(buf);
+            if (n & 1)
+                clp->out_flags.fua = true;
+            if (n & 2)
+                clp->in_flags.fua = true;
+        } else if (0 == strcmp(key,"ibs")) {
+            ibs = sg_get_num(buf);
+            if ((ibs < 0) || (ibs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'ibs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key,"if") == 0) {
+            if ('\0' != infn[0]) {
+                pr2serr("Second 'if=' argument??\n");
+                return SG_LIB_SYNTAX_ERROR;
+            } else {
+                memcpy(infn, buf, INOUTF_SZ);
+                infn[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (0 == strcmp(key, "iflag")) {
+            if (process_flags(buf, &clp->in_flags)) {
+                pr2serr("%sbad argument to 'iflag='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"obs")) {
+            obs = sg_get_num(buf);
+            if ((obs < 0) || (obs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'obs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key,"of") == 0) {
+            if ('\0' != outfn[0]) {
+                pr2serr("Second 'of=' argument??\n");
+                return SG_LIB_SYNTAX_ERROR;
+            } else {
+                memcpy(outfn, buf, INOUTF_SZ);
+                outfn[INOUTF_SZ - 1] = '\0';
+            }
+        } else if (0 == strcmp(key, "oflag")) {
+            if (process_flags(buf, &clp->out_flags)) {
+                pr2serr("%sbad argument to 'oflag='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"seek")) {
+            seek = sg_get_llnum(buf);
+            if ((seek < 0) || (seek > MAX_COUNT_SKIP_SEEK)) {
+                pr2serr("%sbad argument to 'seek='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"skip")) {
+            skip = sg_get_llnum(buf);
+            if ((skip < 0) || (skip > MAX_COUNT_SKIP_SEEK)) {
+                pr2serr("%sbad argument to 'skip='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"sync"))
+            do_sync = !! sg_get_num(buf);
+        else if (0 == strcmp(key,"thr"))
+            clp->num_threads = sg_get_num(buf);
+        else if (0 == strcmp(key,"time"))
+            do_time = !! sg_get_num(buf);
+        else if ((keylen > 1) && ('-' == key[0]) && ('-' != key[1])) {
+            res = 0;
+            n = num_chs_in_str(key + 1, keylen - 1, 'c');
+            clp->chkaddr += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'd');
+            clp->dry_run += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'h');
+            if (n > 0) {
+                usage();
+                return 0;
+            }
+            n = num_chs_in_str(key + 1, keylen - 1, 'p');
+            clp->progress += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'v');
+            if (n > 0)
+                verbose_given = true;
+            clp->debug += n;   /* -v  ---> --verbose */
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'V');
+            if (n > 0)
+                version_given = true;
+            res += n;
+
+            if (res < (keylen - 1)) {
+                pr2serr("Unrecognised short option in '%s', try '--help'\n",
+                        key);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strncmp(key, "--chkaddr", 9))
+            ++clp->chkaddr;
+        else if ((0 == strncmp(key, "--dry-run", 9)) ||
+                   (0 == strncmp(key, "--dry_run", 9)))
+            ++clp->dry_run;
+        else if ((0 == strncmp(key, "--help", 6)) ||
+                   (0 == strcmp(key, "-?"))) {
+            usage();
+            return 0;
+        } else if (0 == strncmp(key, "--prog", 6))
+            ++clp->progress;
+        else if (0 == strncmp(key, "--verb", 6)) {
+            verbose_given = true;
+            ++clp->debug;      /* --verbose */
+        } else if (0 == strncmp(key, "--vers", 6))
+            version_given = true;
+        else {
+            pr2serr("Unrecognized option '%s'\n", key);
+            pr2serr("For more information use '--help'\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        clp->debug = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        clp->debug = 2;
+    } else
+        pr2serr("keep verbose=%d\n", clp->debug);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("%s%s\n", my_name, version_str);
+        return 0;
+    }
+
+    if (clp->bs <= 0) {
+        clp->bs = DEF_BLOCK_SIZE;
+        pr2serr("Assume default 'bs' ((logical) block size) of %d bytes\n",
+                clp->bs);
+    }
+    if ((ibs && (ibs != clp->bs)) || (obs && (obs != clp->bs))) {
+        pr2serr("If 'ibs' or 'obs' given must be same as 'bs'\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((skip < 0) || (seek < 0)) {
+        pr2serr("skip and seek cannot be negative\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (clp->out_flags.append && (seek > 0)) {
+        pr2serr("Can't use both append and seek switches\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((clp->bpt < 1) || (clp->bpt > MAX_BPT_VALUE)) {
+        pr2serr("bpt must be > 0 and <= %d\n", MAX_BPT_VALUE);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (clp->in_flags.mmap && clp->out_flags.mmap) {
+        pr2serr("can only use mmap flag in iflag= or oflag=, not both\n");
+        return SG_LIB_SYNTAX_ERROR;
+    } else if (clp->in_flags.mmap || clp->out_flags.mmap)
+        clp->mmap_active = true;
+    /* defaulting transfer size to 128*2048 for CD/DVDs is too large
+       for the block layer in lk 2.6 and results in an EIO on the
+       SG_IO ioctl. So reduce it in that case. */
+    if ((clp->bs >= 2048) && (0 == bpt_given))
+        clp->bpt = DEF_BLOCKS_PER_2048TRANSFER;
+    if ((clp->num_threads < 1) || (clp->num_threads > MAX_NUM_THREADS)) {
+        pr2serr("too few or too many threads requested\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (clp->debug > 2)
+        pr2serr("%sif=%s skip=%" PRId64 " of=%s seek=%" PRId64 " count=%"
+                PRId64 "\n", my_name, infn, skip, outfn, seek, dd_count);
+
+    install_handler(SIGINT, interrupt_handler);
+    install_handler(SIGQUIT, interrupt_handler);
+    install_handler(SIGPIPE, interrupt_handler);
+    install_handler(SIGUSR1, siginfo_handler);
+
+    clp->infd = STDIN_FILENO;
+    clp->outfd = STDOUT_FILENO;
+    if (infn[0] && ('-' != infn[0])) {
+        clp->in_type = dd_filetype(infn);
+
+        if (FT_ERROR == clp->in_type) {
+            pr2serr("%sunable to access %s\n", my_name, infn);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_ST == clp->in_type) {
+            pr2serr("%sunable to use scsi tape device %s\n", my_name, infn);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_SG == clp->in_type) {
+            clp->infd = sg_in_open(infn, &clp->in_flags, clp->bs, clp->bpt);
+            if (clp->infd < 0)
+                return -clp->infd;
+        }
+        else {
+            flags = O_RDONLY;
+            if (clp->in_flags.direct)
+                flags |= O_DIRECT;
+            if (clp->in_flags.excl)
+                flags |= O_EXCL;
+            if (clp->in_flags.dsync)
+                flags |= O_SYNC;
+
+            if ((clp->infd = open(infn, flags)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ, "%scould not open %s for reading",
+                         my_name, infn);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+            else if (skip > 0) {
+                off64_t offset = skip;
+
+                offset *= clp->bs;       /* could exceed 32 bits here! */
+                if (lseek64(clp->infd, offset, SEEK_SET) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, "%scouldn't skip to required "
+                             "position on %s", my_name, infn);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+        }
+    }
+    if (outfn[0] && ('-' != outfn[0])) {
+        clp->out_type = dd_filetype(outfn);
+
+        if (FT_ST == clp->out_type) {
+            pr2serr("%sunable to use scsi tape device %s\n", my_name, outfn);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_SG == clp->out_type) {
+            clp->outfd = sg_out_open(outfn, &clp->out_flags, clp->bs,
+                                     clp->bpt);
+            if (clp->outfd < 0)
+                return -clp->outfd;
+        } else if (FT_DEV_NULL == clp->out_type)
+            clp->outfd = -1; /* don't bother opening */
+        else {
+            if (FT_RAW != clp->out_type) {
+                flags = O_WRONLY | O_CREAT;
+                if (clp->out_flags.direct)
+                    flags |= O_DIRECT;
+                if (clp->out_flags.excl)
+                    flags |= O_EXCL;
+                if (clp->out_flags.dsync)
+                    flags |= O_SYNC;
+                if (clp->out_flags.append)
+                    flags |= O_APPEND;
+
+                if ((clp->outfd = open(outfn, flags, 0666)) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, "%scould not open %s for "
+                             "writing", my_name, outfn);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+            else {      /* raw output file */
+                if ((clp->outfd = open(outfn, O_WRONLY)) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, "%scould not open %s for raw "
+                             "writing", my_name, outfn);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+            if (seek > 0) {
+                off64_t offset = seek;
+
+                offset *= clp->bs;       /* could exceed 32 bits here! */
+                if (lseek64(clp->outfd, offset, SEEK_SET) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, "%scouldn't seek to required "
+                             "position on %s", my_name, outfn);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+        }
+    }
+    if ((STDIN_FILENO == clp->infd) && (STDOUT_FILENO == clp->outfd)) {
+        pr2serr("Won't default both IFILE to stdin _and_ OFILE to stdout\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (dd_count < 0) {
+        in_num_sect = -1;
+        if (FT_SG == clp->in_type) {
+            res = scsi_read_capacity(clp->infd, &in_num_sect, &in_sect_sz);
+            if (2 == res) {
+                pr2serr("Unit attention, media changed(in), continuing\n");
+                res = scsi_read_capacity(clp->infd, &in_num_sect,
+                                         &in_sect_sz);
+            }
+            if (0 != res) {
+                if (res == SG_LIB_CAT_INVALID_OP)
+                    pr2serr("read capacity not supported on %s\n", infn);
+                else if (res == SG_LIB_CAT_NOT_READY)
+                    pr2serr("read capacity failed, %s not ready\n", infn);
+                else
+                    pr2serr("Unable to read capacity on %s\n", infn);
+                in_num_sect = -1;
+            }
+        } else if (FT_BLOCK == clp->in_type) {
+            if (0 != read_blkdev_capacity(clp->infd, &in_num_sect,
+                                          &in_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", infn);
+                in_num_sect = -1;
+            }
+            if (clp->bs != in_sect_sz) {
+                pr2serr("logical block size on %s confusion; bs=%d, from "
+                        "device=%d\n", infn, clp->bs, in_sect_sz);
+                in_num_sect = -1;
+            }
+        }
+        if (in_num_sect > skip)
+            in_num_sect -= skip;
+
+        out_num_sect = -1;
+        if (FT_SG == clp->out_type) {
+            res = scsi_read_capacity(clp->outfd, &out_num_sect, &out_sect_sz);
+            if (2 == res) {
+                pr2serr("Unit attention, media changed(out), continuing\n");
+                res = scsi_read_capacity(clp->outfd, &out_num_sect,
+                                         &out_sect_sz);
+            }
+            if (0 != res) {
+                if (res == SG_LIB_CAT_INVALID_OP)
+                    pr2serr("read capacity not supported on %s\n", outfn);
+                else if (res == SG_LIB_CAT_NOT_READY)
+                    pr2serr("read capacity failed, %s not ready\n", outfn);
+                else
+                    pr2serr("Unable to read capacity on %s\n", outfn);
+                out_num_sect = -1;
+            }
+        } else if (FT_BLOCK == clp->out_type) {
+            if (0 != read_blkdev_capacity(clp->outfd, &out_num_sect,
+                                          &out_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", outfn);
+                out_num_sect = -1;
+            }
+            if (clp->bs != out_sect_sz) {
+                pr2serr("logical block size on %s confusion: bs=%d, from "
+                        "device=%d\n", outfn, clp->bs, out_sect_sz);
+                out_num_sect = -1;
+            }
+        }
+        if (out_num_sect > seek)
+            out_num_sect -= seek;
+
+        if (in_num_sect > 0) {
+            if (out_num_sect > 0)
+                dd_count = (in_num_sect > out_num_sect) ? out_num_sect :
+                                                          in_num_sect;
+            else
+                dd_count = in_num_sect;
+        }
+        else
+            dd_count = out_num_sect;
+    }
+    if (clp->debug > 1)
+        pr2serr("Start of loop, count=%" PRId64 ", in_num_sect=%" PRId64
+                ", out_num_sect=%" PRId64 "\n", dd_count, in_num_sect,
+                out_num_sect);
+    if (dd_count < 0) {
+        pr2serr("Couldn't calculate count, please give one\n");
+        return SG_LIB_CAT_OTHER;
+    }
+    if (! cdbsz_given) {
+        if ((FT_SG == clp->in_type) && (MAX_SCSI_CDBSZ != clp->cdbsz_in) &&
+            (((dd_count + skip) > UINT_MAX) || (clp->bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'if')\n");
+            clp->cdbsz_in = MAX_SCSI_CDBSZ;
+        }
+        if ((FT_SG == clp->out_type) && (MAX_SCSI_CDBSZ != clp->cdbsz_out) &&
+            (((dd_count + seek) > UINT_MAX) || (clp->bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'of')\n");
+            clp->cdbsz_out = MAX_SCSI_CDBSZ;
+        }
+    }
+
+    clp->in_count = dd_count;
+    clp->in_rem_count = dd_count;
+    clp->skip = skip;
+    clp->in_blk = skip;
+    clp->out_count = dd_count;
+    clp->out_rem_count = dd_count;
+    clp->seek = seek;
+    status = pthread_mutex_init(&clp->inout_mutex, NULL);
+    if (0 != status) err_exit(status, "init inout_mutex");
+    status = pthread_mutex_lock(&clp->inout_mutex);
+    if (0 != status) err_exit(status, "lock inout_mutex");
+    clp->out_blk = seek;
+    status = pthread_mutex_unlock(&clp->inout_mutex);
+    if (0 != status) err_exit(status, "unlock inout_mutex");
+
+    status = pthread_cond_init(&clp->out_sync_cv, NULL);
+    if (0 != status) err_exit(status, "init out_sync_cv");
+
+    if (clp->dry_run > 0) {
+        pr2serr("Due to --dry-run option, bypass copy/read\n");
+        goto fini;
+    }
+    sigemptyset(&signal_set);
+    sigaddset(&signal_set, SIGINT);
+    status = pthread_sigmask(SIG_BLOCK, &signal_set, NULL);
+    if (0 != status) err_exit(status, "pthread_sigmask");
+    status = pthread_create(&sig_listen_thread_id, NULL,
+                            sig_listen_thread, (void *)clp);
+    if (0 != status) err_exit(status, "pthread_create, sig...");
+
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+    }
+
+/* vvvvvvvvvvv  Start worker threads  vvvvvvvvvvvvvvvvvvvvvvvv */
+    if ((clp->out_rem_count > 0) && (clp->num_threads > 0)) {
+        /* Run 1 work thread to shake down infant retryable stuff */
+        status = pthread_mutex_lock(&clp->inout_mutex);
+        if (0 != status) err_exit(status, "lock out_mutex");
+        seek_skip = clp->seek - clp->skip;
+        thr_arg_a[0].id = 0;
+        thr_arg_a[0].seek_skip = seek_skip;
+        status = pthread_create(&threads[0], NULL, read_write_thread,
+                                (void *)(thr_arg_a + 0));
+        if (0 != status) err_exit(status, "pthread_create");
+        if (clp->debug)
+            pr2serr("Starting worker thread k=0\n");
+
+        /* wait for any broadcast */
+        pthread_cleanup_push(cleanup_out, (void *)clp);
+        status = pthread_cond_wait(&clp->out_sync_cv, &clp->inout_mutex);
+        if (0 != status) err_exit(status, "cond out_sync_cv");
+        pthread_cleanup_pop(0);
+        status = pthread_mutex_unlock(&clp->inout_mutex);
+        if (0 != status) err_exit(status, "unlock out_mutex");
+
+        /* now start the rest of the threads */
+        for (k = 1; k < clp->num_threads; ++k) {
+
+            thr_arg_a[k].id = k;
+            thr_arg_a[k].seek_skip = seek_skip;
+            status = pthread_create(&threads[k], NULL, read_write_thread,
+                                    (void *)(thr_arg_a + k));
+            if (0 != status) err_exit(status, "pthread_create");
+            if (clp->debug > 2)
+                pr2serr("Starting worker thread k=%d\n", k);
+        }
+
+        /* now wait for worker threads to finish */
+        for (k = 0; k < clp->num_threads; ++k) {
+            status = pthread_join(threads[k], &vp);
+            if (0 != status) err_exit(status, "pthread_join");
+            if (clp->debug > 2)
+                pr2serr("Worker thread k=%d terminated\n", k);
+        }
+    }   /* started worker threads and here after they have all exited */
+
+    if (do_time && (start_tm.tv_sec || start_tm.tv_usec))
+        calc_duration_throughput(0);
+
+    if (do_sync) {
+        if (FT_SG == clp->out_type) {
+            pr2serr(">> Synchronizing cache on %s\n", outfn);
+            res = sg_ll_sync_cache_10(clp->outfd, 0, 0, 0, 0, 0, false, 0);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr("Unit attention(out), continuing\n");
+                res = sg_ll_sync_cache_10(clp->outfd, 0, 0, 0, 0, 0, false,
+                                          0);
+            }
+            if (0 != res)
+                pr2serr("Unable to synchronize cache\n");
+        }
+    }
+
+#if 0
+#if SG_LIB_ANDROID
+    /* Android doesn't have pthread_cancel() so use pthread_kill() instead.
+     * Also there is no need to link with -lpthread in Android */
+    status = pthread_kill(sig_listen_thread_id, SIGUSR1);
+    if (0 != status) err_exit(status, "pthread_kill");
+#else
+    status = pthread_cancel(sig_listen_thread_id);
+    if (0 != status) err_exit(status, "pthread_cancel");
+#endif
+#endif  /* 0, because always do pthread_kill() next */
+
+    shutting_down = true;
+    status = pthread_kill(sig_listen_thread_id, SIGINT);
+    if (0 != status) err_exit(status, "pthread_kill");
+    /* valgrind says the above _kill() leaks; web says it needs a following
+     * _join() to clear heap taken by associated _create() */
+
+fini:
+    if ((STDIN_FILENO != clp->infd) && (clp->infd >= 0))
+        close(clp->infd);
+    if ((STDOUT_FILENO != clp->outfd) && (FT_DEV_NULL != clp->out_type)) {
+        if (clp->outfd >= 0)
+            close(clp->outfd);
+    }
+    res = exit_status;
+    if ((0 != clp->out_count) && (0 == clp->dry_run)) {
+        pr2serr(">>>> Some error occurred, remaining blocks=%" PRId64 "\n",
+                clp->out_count);
+        if (0 == res)
+            res = SG_LIB_CAT_OTHER;
+    }
+    print_stats("");
+    if (clp->dio_incomplete_count) {
+        int fd;
+        char c;
+
+        pr2serr(">> Direct IO requested but incomplete %d times\n",
+                clp->dio_incomplete_count);
+        if ((fd = open(sg_allow_dio, O_RDONLY)) >= 0) {
+            if (1 == read(fd, &c, 1)) {
+                if ('0' == c)
+                    pr2serr(">>> %s set to '0' but should be set to '1' for "
+                            "direct IO\n", sg_allow_dio);
+            }
+            close(fd);
+        }
+    }
+    if (clp->sum_of_resids)
+        pr2serr(">> Non-zero sum of residual counts=%d\n",
+               clp->sum_of_resids);
+#ifdef HAVE_C11_ATOMICS
+    {
+        unsigned int ui;
+
+        if ((ui = atomic_load(&num_eagain)) > 0)
+            pr2serr(">> number of IO call yielding EAGAIN %u\n", ui);
+        if ((ui = atomic_load(&num_ebusy)) > 0)
+            pr2serr(">> number of IO call yielding EBUSY %u\n", ui);
+        if ((ui = atomic_load(&num_eintr)) > 0)
+            pr2serr(">> number of IO call yielding EINTR %u\n", ui);
+    }
+#endif
+    return (res >= 0) ? res : SG_LIB_CAT_OTHER;
+}
diff --git a/suse/sg3_utils.changes b/suse/sg3_utils.changes
new file mode 100644
index 0000000..4279a29
--- /dev/null
+++ b/suse/sg3_utils.changes
@@ -0,0 +1,393 @@
+-------------------------------------------------------------------
+Thu Jan 23 15:00:00 EST 2014 - dgilbert@interlog.com
+
+- import Suse build files into sg3_utils in the suse directory
+  * change suse spec file to be patch-less
+  * henceforth see ChangeLog in main directory
+
+-------------------------------------------------------------------
+Thu Jan 23 08:57:56 CET 2014 - hare@suse.de
+
+- Update to inofficial release 1.38r546
+  * sg_ses: error and warning message cleanup
+    - fix --data=- problem with large buffers
+    - new --data=@FN to read hex data from file FN
+    - add --maxlen= option
+  * sg_inq:
+    - add LU_CONG to standard inquiry response
+    - sync version descriptors dated 20131126
+    - fix overflow in encode_whitespaces
+  * sg_vpd: add LU_CONG to standard inquiry response output
+    - decode Third Party Copy (tpc) page
+  * sg_persist: add PROUT: Replace Lost Reservation (spc4r36)
+  * sg_readcap: for --16 show physical block size if
+  * sg_xcopy:
+    - environment variables: XCOPY_TO_SRC and
+      XCOPY_TO_DST indicate where xcopy command is sent
+    - change default to send xcopy to dst (was src)
+    - improve CL handling of short options (e.g. '-vv')
+  * sg_write_same: repeat if unit attention
+  * sg_rtpg: fix indexing bug with --extended option
+  * sg_lib_data: sync asc/ascq codes with T10 dated 20131110
+  * sg_cmds_extra: fix sa bug in sg_ll_3party_copy_out()
+- Update tarball to 1.38b7r537
+- Add sg3_utils-1.38r546.patch
+
+-------------------------------------------------------------------
+Mon Nov  4 01:59:38 UTC 2013 - jengelh@inai.de
+
+- Update to new upstream release 1.37
+* sg_compare_and_write: add --quiet option to suppress miscompare
+  report
+* sg_persist: fix core dump on -Q option
+* sg_unmap: fix core dump on -g option
+* sg_ses: add --nickname and --nickid options
+- Remove sg3_utils-Fixup-T10-Vendor-designator-display.patch
+  (merged upstream)
+
+-------------------------------------------------------------------
+Sun Aug 25 18:45:14 CEST 2013 - ohering@suse.de
+
+- Fixup T10 Vendor designator display (bnc#805059)
+  sg3_utils-Fixup-T10-Vendor-designator-display.patch
+- In rescan-scsi-bus.sh, check if the HBA driver exports issue_lip
+  in sysfs before using it (bnc#780946)
+  sg3_utils-check-if-hba-supports-issue-lip.patch
+
+-------------------------------------------------------------------
+Thu Jun 13 14:15:26 UTC 2013 - jengelh@inai.de
+
+- Implement shlib packaging guidelines; rename sg3_utils-devel
+  to libsgutils-devel (upstream recommendation)
+- More robust make install call; remove redundant %clean section;
+  simplify file lists
+
+-------------------------------------------------------------------
+Tue Jun 11 08:56:39 UTC 2013 - rmilasan@suse.com
+
+- Update to version 1.36
+  - sg_vpd: Protocol-specific port information VPD page
+    for SAS SSP, persistent connection (spl3r2), power
+    disable (spl3r3)
+    - block device characteristics: add FUAB bit
+  - sg_xcopy: handle more descriptor types; handle zero
+    maximum segment length; allow list IDs to be disabled;
+    improve skip/seek handling; allow xcopy on destination
+  - sg_reset: and --no-esc option to stop reset escalation
+    - clean up cli, add long option names
+  - sg_luns: add --test=ALUN option for decoding LUNs
+    - decoded luns output in decimal or hex (if -HH given)
+    - add '--linux' option to show Linux LUN after T10
+      representation, can map one to the other
+  - sg_inq: add --vendor option to show standard inquiry's
+    vendor specific fields in ASCII
+    - take resid into account with response output
+  - sg_sync: add --16 (for 16 byte command) and --timeout=
+  - sg_logs: add data compression page (ssc4)
+  - sg_sat_set_features: increase --lba from 1 to 4 bytes
+  - sg_write_same: add --ndob option (sbc3r35d)
+  - sg_map: mark as deprecated
+  - sginfo: mark as deprecated, especially -l (list)
+  - sg_lib: improve snprintf handling
+  - sg_lib_data: sync asc/ascq codes with T10 20130117
+  - sg_cmds (lib): if noisy given, give more UA info
+  - make code more C++ friendly
+
+-------------------------------------------------------------------
+Tue Mar 12 09:13:45 CET 2013 - hare@suse.de
+
+- Update to version 1.35
+  - sg_compare_and_write: new utility
+  - sg_inq+sg_vpd: block device characteristics VPD page:
+    add product_type, WABEREQ, WACEREQ and VBULS fields
+  - sg_inq: more --export option changes for udev
+  - sg_vpd: add more rdac vendor specific vpd pages
+  - sg_verify: add --ebytchk option for sbc3r34 changes
+  - sg_stpg: --offline option: fix 'Invalid state 0xe'
+  - sg_ses: Door Lock element changed to Door element and
+    abbreviation changed from 'dl' to 'do' (ses3r05)
+  - archive/rescan-scsi-bus.sh: upgrade to version 1.53hr
+    - move rescan-scsi-bus.sh to scripts directory
+  - sync to sbc3r34
+  - sg_lib: sg_ll_verify10+16 expand BYTCHK to 2 bit field
+  - sg_pt_win32, sg_scan(win32): changes for cygwin 1.7.17
+  - clean up man page summary lines
+  - sg_xcopy: new dd like utility for extended copy command
+  - sg_copy_results: new utility for receive copy results
+  - sg_verify: add 16 byte cdb, bytchk (data-out buffer)
+    and group number support
+  - sync to spc4r36 and sbc3r32
+  - sg_inq: add --export so sg_inq can replace udev's scsi_id
+    - decode old EMC Symmetrix abuse of VPD page 0x83
+  - sg_vpd: decode old EMC Symmetrix abuse of VPD page 0x83
+  - sg_ses: increase max dpage response size to 64 KB
+    - allow ident,locate on enclosure controller
+    - more sanity for additional element status descriptor
+  - sg_sanitize: add --ause, --fail and --test=
+  - sg_luns: add long extended flat space addressing format
+  - sg_logs: add ATA pass-through results lpage (SAT-2)
+  - sg_rtpg: add --extended option
+  - sg_senddiag: list rebuild assist diag page name
+  - sg_pt_linux: expand DID_ (host_byte) codes
+    - cope with a transport error plus sense data
+    - prefer major() over MAJOR() macro
+  - sg_lib: fix sg_get_command_name() service actions
+    - report sdat_ovfl bit (if set) in sense data
+    - decode extended_copy and receive_copy service actions
+    - decode read_buffer and write_buffer modes
+    - decode ATA PT fixed format sense (SAT-2)
+  - sg_cmds_extra: add sg_ll_report_tgt_prt_grp2()
+  - ./configure options:
+    - change --enable-no-linux-bsg to --disable-linuxbsg
+    - add --disable-scsistrings to reduce utility sizes
+
+-------------------------------------------------------------------
+Wed Jul  4 07:01:46 UTC 2012 - cfarrell@suse.com
+
+- license update: GPL-2.0+ and BSD-3-Clause
+  Show aggregation and make compatible with Fedora declaration
+
+-------------------------------------------------------------------
+Sun Apr 22 11:50:44 UTC 2012 - puzel@suse.com
+
+- Update to version 1.33
+  - sg_ses: major rework of indexes (again), now two level
+  - sg_write_buffer: new --specific option for mode specific
+    field; new mode 13 (spc4r32)
+  - sg_vpd: add hp3par volume info vendor VPD page
+    - fix 'scsi ports' [0x88] page problem
+    - add 'sinq' pseudo page for standard inquiry response
+    - add power consumption page
+  - sg_format: add --poll= option for request sense polling
+    - improve handling of disks > 2 TB and DIF (protection)
+  - sg_logs: LB provision lpage extra (sbc3r28)
+  - sg_modes: application tag mpage subcode 0xf0->0x2
+  - sg_write_same: no prot fields when wrprotect=0
+  - sg_get_lba_status: reflect change in sbc3r25 to Parameter
+    Data Length response field (offset reduced from 8 to 4)
+  - sg_inq, sg_vpd: sync with spc4r33
+  - win32: change DataBufferOffset type per MSDN; caused
+    problem with 64 bit machines (with buffered interface)
+  - sg_luns: tweak documentation for vendor specific reports
+  - add man pages for scsi_loging_level, scsi_mandat,
+    scsi_satl and scsi_temperature
+
+-------------------------------------------------------------------
+Mon Jan 16 19:59:42 UTC 2012 - tabraham@novell.com
+
+- Update to version 1.32
+  + sg_sanitize: new utility for command added in sb3r27
+  + sg_sat_identify: add '--ident' to output WWN
+  + sg_ses: major rework of descriptor output
+    + add --index, --descriptor, --join, --clear, --get, and --set
+      options
+  + sg_raw: exit status corrections
+  + sg_decode_sense: add --nospace and --hex options
+  + sg_logs: fix bug with large --maxlen
+    + zero response length when resid implies it is invalid
+    + add scope field to lb provisioning lpage (sb3r27)
+  + sg_inq: sync version descriptors with spc4r31
+  + sb_lib_data: sync asc/ascq codes with spc4r31
+  + sg_vpd: add LBPRZ field in LP provisioning VPD page
+  + sg_format: allow format of pdt 7 (some MO drives)
+  + sg_cmd_basic: sg_cmds_process_resp() handle status good
+    with a sense key other than no_sense (e.g. completed)
+  + add README.iscsi
+
+- Updated rescan-scsi-bus.sh to v1.56
+
+-------------------------------------------------------------------
+Thu Mar 10 08:47:43 UTC 2011 - coolo@novell.com
+
+- fix file list
+
+-------------------------------------------------------------------
+Fri Feb 18 16:41:32 CET 2011 - hare@suse.de
+
+- Update to version 1.31:
+  + sg_decode_sense: new utility to decode sense data
+  + sg_vpd: LB provisioning + Block limits pages (sbc3r26)
+  + sync asc/ascq and version descriptors with spc4r28
+  + sg_get_config, sg_rmsn, sg_verify: add --readonly option
+  + sg_lib: implement forwarded sense data descriptor
+    - decode user data segment referral sense data descriptor
+  + sg_lib, sg_turs, sg_format: more precision for progress
+    indication (two places after decimal point)
+  + sg_lib(win32): add runtime selection of SPT direct or
+    indirect interface
+    - sg_read_buffer+sg_write_buffer: set SPT direct
+ + add examples/forwarded_sense.txt + examples/ref_sense.txt
+
+- Changes from version 1.30:
+  + sg_referrals: new utility for REPORT REFERRALS
+  + sbc3r25 renames 'thin' provisioning' to 'logical block
+    provisioning': changes in sg_format, sg_inq, sg_logs,
+    sg_modes, sg_readcap, sg_vpd
+  + sg_inq: update version descriptor list to spc4r27
+    - extended inquiry vpd page add extended self test
+      completion minutes field
+  + sg_lib: sync asc/ascq list to spc4r27
+    - dStrHex(): trim excess trailing spaces
+  + sg_read_long: add --readonly option (open() is rw)
+  + sg_raw: add --readonly option (open() is rw)
+    - allow bidirectional commands
+  + sg_vpd: rdac vendor page [0xc8] parse corrections
+    - extended inquiry vpd page add extended self test
+    -completion minutes field
+  + sg_ses: expand --data (in) buffer to 2048 bytes
+  + sg_opcodes: add extended parameter data for TMFs (spc4r26)
+  + sg_dd: clean count calculation, document nocache flag
+    - treat bsg devices as implicit sg_io
+  + sg_write_same: if READ CAPACITY(16) fails try 10 byte variant
+    - anticipate approval of proposal to allow UNMAP and ANCHOR
+      bits to be set on WRITE SAME(10) with '--10' option
+  + sg3_utils man page: sections added for OS device names
+
+-------------------------------------------------------------------
+Fri Aug 13 11:42:50 CEST 2010 - dimstar@opensuse.org
+
+- Update to version 1.29:
+  + sg_rtpg: new logical block dependent state and bit (spc4r23)
+  + sg_start: add '--readonly' option for ATA disks
+  + sg_lib: update asc/ascq list to spc4r23
+  + sg_inq: update version descriptor list to spc4r23
+  + sg_vpd: block device characteristics page: fix form factor
+    - update Extended Inquiry VPD page to spc4r23
+    - update Block Limits VPD page to sbc3r22
+    - update Thin Provisioning VPD page to sbc3r22
+    - Automation device serial number and Data transfer device
+      element VPD pages (ssc4r01)
+    - add Referrals VPD page (sbc3r22)
+  + sg_logs: add thin provisioning and solid state media log pages
+    - addition of IBM LTO specific log pages
+  + sg_modes: new page names from ssc4r01
+  + sg_ses: sync with ses3r02 (SAS-2.1 connector types)
+  + sg_unmap: add '--anchor' option (sbc3r22)
+  + sg_write_same: add '--anchor' option (sbc3r22)
+  + sg_pt interface: add set_scsi_pt_flags() to permit passing
+    through SCSI_PT_FLAGS_QUEUE_AT_TAIL and AT_HEAD flags
+  + add examples/sg_queue_tst+bsg_queue_tst for SG_FLAG_Q_AT_TAIL
+  + add AM_MAINTAINER_MODE to configure.ac to lessen build issues
+  + add BSD_LICENSE file to this and lib directories, refer to
+    it from source and header files. Some source has GPL license
+- Changes from version 1.28:
+  + sg_unmap: new utility for thin provisioning
+    - add examples/sg_unmap_example.txt
+  + sg_get_lba_status: new utility for thin provisioning
+  + sg_read_block_limits: new utility for tape drives
+  + sg_logs: add cache memory statistics log (sub)page
+  + sg_vpd, sg_inq: extend Block limits VPD page (sbc3r19)
+  + sg_vpd: add Thin provisioning VPD page (sbc3r20) and
+            TapeAlert supported flags VPD page
+  + sg_inq: note VPD page support better in sg_vpd
+  + sg_persist: add transport specific transportID format
+    - allow transportIDs to be read from named file
+  + sg_opcodes: allow --opcode= option to take OP and SA
+    values (comma seperated)
+    - tweak print format, remove test code
+  + sg_requests: remove test code in progress calculation
+  + sg_reset: add target reset option
+  + sg_luns: reduce default maxlen to 8192 (for FreeBSD)
+  + sg_raw: extend max cdb length from 16 to 256 bytes
+    - align heap allocs to page boundaries
+  + sg_lib: sg_set_binary_mode() needs config.h included
+    - add progress indication sense data descriptor (0xa)
+    - change SG3_UTILS_* constants to SG_LIB_*
+    - decode service actions within persistent reserve in/out
+    - sync with spc4r21
+  + sg_cmds_extra: add sg_ll_unmap() and sg_ll_get_lba_status()
+  + sg_pt_linux: fix check condition but empty sense buffer;
+    - major() macro grief, if present include <linux/kdev_t.h> and
+      use MAJOR() instead
+  + scripts/sas_disk_blink: moved from this package to sdparm
+  + utils/hxascdmp: in Windows set binary mode on read files
+  + examples/sg_persist_tst.sh: add PRIN read full status command
+  + sg_raw,sg_write_buffer,sg_write_long,sg_write_same: in Windows
+    set binary mode on read files
+  + sg_pt_win32: default to non-direct variant of SPT interface
+    - use './configure --enable-win32-spt-direct' to override
+    - non-direct data length set to 16 KB, extended if required
+  + debian: incorporate patch from debian sid
+
+-------------------------------------------------------------------
+Mon Jun 28 06:38:35 UTC 2010 - jengelh@medozas.de
+
+- use %_smp_mflags
+
+-------------------------------------------------------------------
+Tue Jul 21 14:00:16 CEST 2009 - hare@suse.de
+
+- Clean up spec file and remove obsolete cruft
+
+-------------------------------------------------------------------
+Fri Apr 17 20:15:58 CEST 2009 - crrodriguez@suse.de
+
+- remove static libraries and "la" files
+
+-------------------------------------------------------------------
+Mon Jan 26 15:30:31 CET 2009 - hare@suse.de
+
+- Fixes to rescan-scsi-bus.sh:
+  * Implement '--forcerescan' to force a rescan of existing devices
+  * Handle LUN changes correctly
+  * Check variables before evaluation
+
+-------------------------------------------------------------------
+Wed Oct 29 11:05:47 CET 2008 - garloff@suse.de
+
+- rescan-scsi-bus.sh 1.29:
+  * Fix error in script (returning "" does not work)
+  * Support systems without /proc/scsi
+- Don't install INSTALL
+
+-------------------------------------------------------------------
+Tue Sep 30 14:11:15 CEST 2008 - hare@suse.de
+
+-  Add %insserv_prereq (bnc#423204)
+
+-------------------------------------------------------------------
+Fri Sep 12 20:29:08 CEST 2008 - garloff@suse.de
+
+- Update rescan-scsi-bus.sh script to 1.28:
+  * Merge fixes from Hannes
+  * Minor cleanups
+  * Sort hosts numerically
+
+-------------------------------------------------------------------
+Tue Aug 12 18:25:43 CEST 2008 - garloff@suse.de
+
+- Update to sg3_utils-1.27:
+  * Adapted to linux-2.6.26 (sg_map26)
+  * sg_dd uses flock (rw -- if that fails ro)
+  * sg_get_config: OSSC feature (mmc6r02)
+- Update to sg3_utils-1.26:
+  * Minor fixes and enhancements to
+    sg_sat_phy_event, sg_ses, sg_get_config, sg_verify, sg_vpd,
+    sg_inq, sg_modes, sg_start, sg_request, sg_luns, sg_dd,
+    sg_opcodes, sg_turs.
+  * sg_lib: asc/ascq update for spc4r15, osd2r03 service actions,
+    sense key specific unit attn queue overflow decoding, ...
+  * Restructuring: sg_lib -> sg_lib_data, sg_inq_data, (u)int64_t,
+    sg_io_linux -> lib/.
+  * Documentation enhancements.
+
+-------------------------------------------------------------------
+Wed Jul 16 09:55:33 CEST 2008 - hare@suse.de
+
+- Use correct length parameter for sg_inq (bnc#363438)
+
+-------------------------------------------------------------------
+Fri May 23 10:22:31 CEST 2008 - hare@suse.de
+
+- Use 'Provides' to clean update dependency
+
+-------------------------------------------------------------------
+Fri May  9 17:31:33 CEST 2008 - schwab@suse.de
+
+- Use autoreconf -i.
+
+-------------------------------------------------------------------
+Thu Apr 24 14:14:14 CEST 2008 - hare@suse.de
+
+- Split off from original scsi package.
+
diff --git a/suse/sg3_utils.spec b/suse/sg3_utils.spec
new file mode 100644
index 0000000..f1b1507
--- /dev/null
+++ b/suse/sg3_utils.spec
@@ -0,0 +1,125 @@
+#
+# spec file for package sg3_utils
+#
+# Copyright (c) 2013 SUSE LINUX Products GmbH, Nuernberg, Germany.
+#
+# All modifications and additions to the file contributed by third parties
+# remain the property of their copyright owners, unless otherwise agreed
+# upon. The license for this file, and modifications and additions to the
+# file, is the same license as for the pristine package itself (unless the
+# license for the pristine package is not an Open Source License, in which
+# case the license is the MIT License). An "Open Source License" is a
+# license that conforms to the Open Source Definition (Version 1.9)
+# published by the Open Source Initiative.
+
+# Please submit bugfixes or comments via http://bugs.opensuse.org/
+#
+#
+# No patches, this is the maintainer's version for Suse targets.
+# Patch lines would appear after the "Source:" line and look like:
+#   Patch1:         sg3_utils-1.38r546.patch
+# then under the "%setup -q" line there would be one or more lines:
+#   %patch1 -p1
+
+
+Name:           sg3_utils
+%define lname	libsgutils2-2
+Version:        1.41
+Release:        0
+Summary:        A collection of tools that send SCSI commands to devices
+License:        GPL-2.0+ and BSD-3-Clause
+Group:          Hardware/Other
+Url:            https://sg.danny.cz/sg/sg3_utils.html
+
+Source:         https://sg.danny.cz/sg/p/%name-%{version}.tar.xz
+BuildRoot:      %{_tmppath}/%{name}-%{version}-build
+BuildRequires:  xz
+Requires(pre):  %insserv_prereq
+Provides:       scsi
+Provides:       sg_utils
+Obsoletes:      scsi <= 1.7_2.38_1.25_0.19_1.02_0.93
+
+%description
+The sg3_utils package contains utilities that send SCSI commands to
+devices. As well as devices on transports traditionally associated with
+SCSI (e.g. Fibre Channel (FCP), Serial Attached SCSI (SAS) and the SCSI
+Parallel Interface(SPI)) many other devices use SCSI command sets.
+ATAPI cd/dvd drives and SATA disks that connect via a translation layer
+or a bridge device are examples of devices that use SCSI command sets.
+
+%package -n %lname
+Summary:        Library to hold functions common to the SCSI utilities
+License:        BSD-3-Clause
+Group:          System/Libraries
+
+%description -n %lname
+The sg3_utils package contains utilities that send SCSI commands to
+devices. As well as devices on transports traditionally associated with
+SCSI (e.g. Fibre Channel (FCP), Serial Attached SCSI (SAS) and the SCSI
+Parallel Interface(SPI)) many other devices use SCSI command sets.
+ATAPI cd/dvd drives and SATA disks that connect via a translation layer
+or a bridge device are examples of devices that use SCSI command sets.
+
+This subpackage contains the library of common sg_utils code, such as
+SCSI error processing.
+
+%package -n libsgutils-devel
+Summary:        A collection of tools that send SCSI commands to devices
+License:        BSD-3-Clause
+Group:          Development/Libraries/C and C++
+Requires:       %lname = %version
+# Added for 13.1
+Obsoletes:      %name-devel < %version-%release
+Provides:       %name-devel = %version-%release
+
+%description -n libsgutils-devel
+The sg3_utils package contains utilities that send SCSI commands to
+devices. As well as devices on transports traditionally associated with
+SCSI (e.g. Fibre Channel (FCP), Serial Attached SCSI (SAS) and the SCSI
+Parallel Interface(SPI)) many other devices use SCSI command sets.
+ATAPI cd/dvd drives and SATA disks that connect via a translation layer
+or a bridge device are examples of devices that use SCSI command sets.
+
+This subpackage contains libraries and header files for developing
+applications that want to make use of libsgutils.
+
+%prep
+%setup -q
+
+%build
+%configure --disable-static --with-pic
+make %{?_smp_mflags}
+
+%install
+make install DESTDIR="%buildroot"
+install -m 755 scripts/scsi_logging_level $RPM_BUILD_ROOT%{_bindir}
+install -m 755 scripts/rescan-scsi-bus.sh $RPM_BUILD_ROOT%{_bindir}
+%{__rm} -f %{buildroot}%{_libdir}/*.la
+
+%post   -p /sbin/ldconfig -n %lname
+
+%postun -p /sbin/ldconfig -n %lname
+
+%files
+%defattr(-,root,root)
+%doc README README.sg_start
+%doc ChangeLog CREDITS NEWS
+%_bindir/sg_*
+%_bindir/scsi_*
+%_bindir/sginfo
+%_bindir/sgp_dd
+%_bindir/sgm_dd
+%_bindir/scsi_logging_level
+%_bindir/rescan-scsi-bus.sh
+%_mandir/man8/*.8*
+
+%files -n %lname
+%defattr(-,root,root)
+%_libdir/libsgutils2.so.2*
+
+%files -n libsgutils-devel
+%defattr(-,root,root)
+%_libdir/libsgutils2.so
+%_includedir/scsi/
+
+%changelog
diff --git a/testing/Makefile b/testing/Makefile
new file mode 100644
index 0000000..3491491
--- /dev/null
+++ b/testing/Makefile
@@ -0,0 +1,158 @@
+SHELL = /bin/sh
+
+PREFIX=/usr/local
+INSTDIR=$(DESTDIR)/$(PREFIX)/bin
+MANDIR=$(DESTDIR)/$(PREFIX)/man
+
+EXECS = sg_sense_test sg_queue_tst bsg_queue_tst sg_chk_asc sg_tst_nvme \
+	sg_tst_ioctl sg_tst_bidi tst_sg_lib sgs_dd sg_tst_excl \
+	sg_tst_excl2 sg_tst_excl3 sg_tst_context sg_tst_async sgh_dd \
+	sg_mrq_dd sg_iovec_tst sg_take_snap sg_tst_json_builder
+	
+EXTRAS =
+
+BSG_EXTRAS =
+
+
+MAN_PGS =
+MAN_PREF = man8
+
+LARGE_FILE_FLAGS = -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64
+
+# For C++/clang testing
+## CC = gcc
+## CXX = g++
+## CC = clang
+## CXX = clang++
+
+LD = $(CXX)
+CXXLD = $(CXX)
+
+CPPFLAGS = -iquote ../include -iquote .. -D_REENTRANT $(LARGE_FILE_FLAGS) -DHAVE_CONFIG_H -DHAVE_NVME
+CXXFLAGS = -std=c++11 -pthread -ggdb -O2 -W -Wall -iquote ../include -D_REENTRANT $(LARGE_FILE_FLAGS)
+## CXXFLAGS = -std=c++14 -pthread -ggdb -O2 -W -Wall -iquote ../include -D_REENTRANT $(LARGE_FILE_FLAGS)
+## CXXFLAGS = -std=c++17 -pthread -ggdb -O2 -W -Wall -iquote ../include -D_REENTRANT $(LARGE_FILE_FLAGS)
+## CXXFLAGS = -std=c++20 -pthread -ggdb -O2 -W -Wall -iquote ../include -D_REENTRANT $(LARGE_FILE_FLAGS)
+## CXXFLAGS = -std=c++2a -pthread -ggdb -O2 -W -Wall -iquote ../include -D_REENTRANT $(LARGE_FILE_FLAGS)
+# CPPFLAGS = -iquote ../include -iquote .. -D_REENTRANT $(LARGE_FILE_FLAGS) -DHAVE_CONFIG_H -DHAVE_NVME -DDEBUG
+CFLAGS = -g -O2 -W -Wall
+# CFLAGS = -ggdb -O2 -W -Wall -DDEBUG
+# CFLAGS = -g -O2 -Wall -DSG_KERNEL_INCLUDES
+# CFLAGS = -g -O2 -Wall -pedantic
+# CFLAGS = -Wall -W -pedantic -std=c11 --analyze
+# CFLAGS = -Wall -W -pedantic -std=c++14 -fPIC
+# CFLAGS = -Wall -W -pedantic -std=c++20
+
+LDFLAGS =
+
+LIBFILESOLD = ../lib/sg_lib.o ../lib/sg_lib_data.o ../lib/sg_io_linux.o \
+		../lib/sg_json_builder.o ../lib/sg_pr2serr.o
+
+LIBFILESNEW = ../lib/sg_pt_linux_nvme.o ../lib/sg_lib.o ../lib/sg_lib_data.o \
+		../lib/sg_pt_linux.o ../lib/sg_io_linux.o \
+		../lib/sg_pt_common.o  ../lib/sg_cmds_basic.o \
+		../lib/sg_cmds_basic2.o ../lib/sg_lib_names.o \
+		../lib/sg_json_builder.o ../lib/sg_pr2serr.o
+
+all: $(EXECS)
+
+extras: $(EXTRAS)
+
+bsg: $(BSG_EXTRAS)
+
+
+depend dep:
+	for i in *.c; do $(CC) $(CPPFLAGS) $(INCLUDES) $(CFLAGS) -M $$i; \
+	done > .depend
+
+clean:
+	/bin/rm -f *.o $(EXECS) $(EXTRAS) $(BSG_EXTRAS) json_writer core .depend
+
+sg_sense_test: sg_sense_test.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_queue_tst: sg_queue_tst.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+bsg_queue_tst: bsg_queue_tst.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_tst_ioctl: sg_tst_ioctl.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+# building sg_chk_asc depends on a prior successful make in ../lib
+sg_chk_asc: sg_chk_asc.o ../lib/sg_lib.o ../lib/sg_lib_data.o
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_tst_nvme: sg_tst_nvme.o $(LIBFILESNEW)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+tst_sg_lib: tst_sg_lib.o $(LIBFILESNEW)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sgs_dd: sgs_dd.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_tst_bidi: sg_tst_bidi.o $(LIBFILESNEW)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_tst_excl: sg_tst_excl.o $(LIBFILESNEW)
+	$(CXXLD) -o $@ $(LDFLAGS) -pthread $^
+
+sg_tst_excl2: sg_tst_excl2.o $(LIBFILESNEW)
+	$(CXXLD) -o $@ $(LDFLAGS) -pthread $^
+
+sg_tst_excl3: sg_tst_excl3.o $(LIBFILESNEW)
+	$(CXXLD) -o $@ $(LDFLAGS) -pthread $^
+
+sg_tst_context: sg_tst_context.o $(LIBFILESNEW)
+	$(CXXLD) -o $@ $(LDFLAGS) -pthread $^
+
+sg_tst_async: sg_tst_async.o $(LIBFILESNEW)
+	$(CXXLD) -o $@ $(LDFLAGS) -pthread $^
+
+# Next three used to require '-latomic', may not anymore
+sgh_dd: sgh_dd.o $(LIBFILESNEW)
+	$(CXXLD) -o $@ $(LDFLAGS) -pthread $^
+
+sg_mrq_dd: sg_mrq_dd.o sg_scat_gath.o $(LIBFILESNEW)
+	$(CXXLD) -o $@ $(LDFLAGS) -pthread $^
+
+sg_iovec_tst: sg_iovec_tst.o sg_scat_gath.o $(LIBFILESNEW)
+	$(CXXLD) -o $@ $(LDFLAGS) -pthread $^
+
+sg_take_snap: sg_take_snap.o $(LIBFILESNEW)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_tst_json_builder: sg_tst_json_builder.o $(LIBFILESNEW)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+
+install: $(EXECS)
+	install -d $(INSTDIR)
+	for name in $^; \
+	 do install -s -o root -g root -m 755 $$name $(INSTDIR); \
+	done
+	install -d $(MANDIR)/$(MAN_PREF)
+	for mp in $(MAN_PGS); \
+	 do install -o root -g root -m 644 $$mp $(MANDIR)/$(MAN_PREF); \
+	 gzip -9f $(MANDIR)/$(MAN_PREF)/$$mp; \
+	done
+
+uninstall:
+	dists="$(EXECS)"; \
+	for name in $$dists; do \
+	 rm -f $(INSTDIR)/$$name; \
+	done
+	for mp in $(MAN_PGS); do \
+	 rm -f $(MANDIR)/$(MAN_PREF)/$$mp.gz; \
+	done
+
+# Linux uses GNU make and FreeBSD uses Berkely make. The following lines
+# only work in Linux. Possible solutions in FreeBSD:
+#    a) use 'gmake'; b) comment out the next 3 lines, starting with 'ifeq'
+#        c) build with 'make -f Makefile.freebsd'
+# In Linux one can install bmake (but that won't help here).
+ifeq (.depend,$(wildcard .depend))
+include .depend
+endif
diff --git a/testing/Makefile.cyg b/testing/Makefile.cyg
new file mode 100644
index 0000000..0dfbb07
--- /dev/null
+++ b/testing/Makefile.cyg
@@ -0,0 +1,110 @@
+SHELL = /bin/sh
+
+PREFIX=/usr/local
+INSTDIR=$(DESTDIR)/$(PREFIX)/bin
+MANDIR=$(DESTDIR)/$(PREFIX)/man
+
+# In Linux the default C compiler is GCC while in FreeBSD (since release 10 ?)
+# the default C compiler is clang. Swap the comment marks (lines starting
+# with '#') on the next 4 (non-blank) lines.
+CC = gcc
+# CC = clang
+
+LD = gcc
+# LD = clang
+
+EXECS = sg_sense_test sg_chk_asc sg_tst_nvme tst_sg_lib
+	
+EXTRAS =
+
+BSG_EXTRAS =
+
+
+MAN_PGS =
+MAN_PREF = man8
+
+LARGE_FILE_FLAGS = -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64
+
+# For C++/clang testing
+## CC = gcc
+## CC = g++
+## CC = clang
+## CC = clang++
+
+CPPFLAGS = -iquote ../include -iquote .. -D_REENTRANT $(LARGE_FILE_FLAGS) -DHAVE_CONFIG_H -DHAVE_NVME
+CFLAGS = -g -O2 -W -Wall
+# CFLAGS = -g -O2 -Wall -DSG_KERNEL_INCLUDES
+# CFLAGS = -g -O2 -Wall -pedantic
+# CFLAGS = -Wall -W -pedantic -std=c11 --analyze
+# CFLAGS = -Wall -W -pedantic -std=c++14 -fPIC
+
+LDFLAGS =
+
+LIBFILESOLD = ../lib/sg_lib.o ../lib/sg_lib_data.o
+LIBFILESNEW = ../lib/sg_lib.o ../lib/sg_lib_data.o \
+		../lib/sg_pt_win32.o ../lib/sg_pt_common.o  ../lib/sg_cmds_basic.o
+
+all: $(EXECS)
+
+extras: $(EXTRAS)
+
+bsg: $(BSG_EXTRAS)
+
+
+depend dep:
+	for i in *.c; do $(CC) $(INCLUDES) $(CFLAGS) -M $$i; \
+	done > .depend
+
+clean:
+	/bin/rm -f *.o $(EXECS) $(EXTRAS) $(BSG_EXTRAS) core .depend
+
+sg_iovec_tst: sg_iovec_tst.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_sense_test: sg_sense_test.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_queue_tst: sg_queue_tst.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+bsg_queue_tst: bsg_queue_tst.o $(LIBFILESOLD)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+# building sg_chk_asc depends on a prior successful make in ../lib
+sg_chk_asc: sg_chk_asc.o ../lib/sg_lib.o ../lib/sg_lib_data.o
+	$(LD) -o $@ $(LDFLAGS) $^
+
+sg_tst_nvme: sg_tst_nvme.o $(LIBFILESNEW)
+	$(LD) -o $@ $(LDFLAGS) $^
+
+tst_sg_lib: tst_sg_lib.o ../lib/sg_lib.o ../lib/sg_lib_data.o
+	$(LD) -o $@ $(LDFLAGS) $^
+
+install: $(EXECS)
+	install -d $(INSTDIR)
+	for name in $^; \
+	 do install -s -o root -g root -m 755 $$name $(INSTDIR); \
+	done
+	install -d $(MANDIR)/$(MAN_PREF)
+	for mp in $(MAN_PGS); \
+	 do install -o root -g root -m 644 $$mp $(MANDIR)/$(MAN_PREF); \
+	 gzip -9f $(MANDIR)/$(MAN_PREF)/$$mp; \
+	done
+
+uninstall:
+	dists="$(EXECS)"; \
+	for name in $$dists; do \
+	 rm -f $(INSTDIR)/$$name; \
+	done
+	for mp in $(MAN_PGS); do \
+	 rm -f $(MANDIR)/$(MAN_PREF)/$$mp.gz; \
+	done
+
+# Linux uses GNU make and FreeBSD uses Berkely make. The following lines
+# only work in Linux. Possible solutions in FreeBSD:
+#    a) use 'gmake'; b) comment out the next 3 lines, starting with 'ifeq'
+#        c) build with 'make -f Makefile.freebsd'
+# In Linux one can install bmake (but that won't help here).
+ifeq (.depend,$(wildcard .depend))
+include .depend
+endif
diff --git a/testing/Makefile.freebsd b/testing/Makefile.freebsd
new file mode 100644
index 0000000..5b11cd0
--- /dev/null
+++ b/testing/Makefile.freebsd
@@ -0,0 +1,96 @@
+SHELL = /bin/sh
+
+PREFIX=/usr/local
+INSTDIR=$(DESTDIR)/$(PREFIX)/bin
+MANDIR=$(DESTDIR)/$(PREFIX)/man
+
+# In Linux the default C compiler is GCC while in FreeBSD (since release 10 ?)
+# the default C compiler is clang. Swap the comment marks (lines starting
+# with '#') on the next 4 (non-blank) lines.
+# CC = gcc
+# CC = clang
+
+# LD = gcc
+# LD = clang
+
+EXECS = sg_sense_test sg_chk_asc sg_tst_nvme tst_sg_lib
+	
+EXTRAS =
+
+MAN_PGS =
+MAN_PREF = man8
+
+OS_FLAGS = -DSG_LIB_FREEBSD -DHAVE_NVME
+EXTRA_FLAGS = $(OS_FLAGS)
+
+# For C++/clang testing
+## CC = gcc
+## CC = g++
+## CC = clang
+## CC = clang++
+
+# CFLAGS = -O2 -Wall -W $(EXTRA_FLAGS) -I ../include
+CFLAGS = -g -O2 -Wall -W $(EXTRA_FLAGS) -I ../include
+# CFLAGS = -g -O2 -Wall -W -pedantic -std=c99 $(EXTRA_FLAGS) -I ../include
+
+CFLAGS_PTHREADS = -D_REENTRANT
+
+# there is no rule to make the following in the parent directory,
+# it is assumed they are already built.
+D_FILES = ../lib/sg_lib.o ../lib/sg_lib_data.o ../lib/sg_cmds_basic.o ../lib/sg_pt_common.o ../lib/sg_pt_freebsd.o
+
+LDFLAGS = -lcam
+
+all: $(EXECS)
+
+extras: $(EXTRAS)
+
+
+depend dep:
+	for i in *.c; do $(CC) $(INCLUDES) $(CFLAGS) -M $$i; \
+	done > .depend
+
+clean:
+	/bin/rm -f *.o $(EXECS) $(EXTRAS) core .depend
+
+sg_sense_test: sg_sense_test.o $(D_FILES)
+	$(CC) -o $@ $(LDFLAGS) $@.o $(D_FILES)
+
+# building sg_chk_asc depends on a prior successful make in ../lib
+sg_chk_asc: sg_chk_asc.o $(D_FILES)
+	$(CC) -o $@ $(LDFLAGS) $@.o $(D_FILES)
+
+sg_tst_nvme: sg_tst_nvme.o $(D_FILES)
+	$(CC) -o $@ $(LDFLAGS) $@.o $(D_FILES)
+
+tst_sg_lib: tst_sg_lib.o $(D_FILES)
+	$(CC) -o $@ $(LDFLAGS) $@.o $(D_FILES)
+
+install: $(EXECS)
+	install -d $(INSTDIR)
+	for name in $(EXECS) ; \
+	 do install -s -o root -g wheel -m 755 $$name $(INSTDIR); \
+	done
+	install -d $(MANDIR)/$(MAN_PREF)
+	for mp in $(MAN_PGS); \
+	 do install -o root -g wheel -m 644 $$mp $(MANDIR)/$(MAN_PREF); \
+	 gzip -9f $(MANDIR)/$(MAN_PREF)/$$mp; \
+	done
+
+uninstall:
+	dists="$(EXECS)"; \
+	for name in $$dists; do \
+	 rm -f $(INSTDIR)/$$name; \
+	done
+	for mp in $(MAN_PGS); do \
+	 rm -f $(MANDIR)/$(MAN_PREF)/$$mp.gz; \
+	done
+
+# Linux uses GNU make and FreeBSD uses Berkely make. The following lines
+# only work in Linux. Possible solutions in FreeBSD:
+#    a) use 'gmake'; b) comment out the next 3 lines, starting with 'ifeq'
+#        c) build with 'make -f Makefile.freebsd'
+# In Linux one can install bmake (but that won't help here).
+# ifeq (.depend,$(wildcard .depend))
+# include .depend
+# endif
diff --git a/testing/README b/testing/README
new file mode 100644
index 0000000..a144e77
--- /dev/null
+++ b/testing/README
@@ -0,0 +1,47 @@
+
+
+The utilities in this directory are _not_ built automatically. So:
+    cd <root_of_sg3_utils_src>
+    ./configure ; make ; make install
+will _not_ build and install them. The make command (or some variant
+of it) needs to be run in this directory as outlined below.
+
+Building files in this directory depends on several files being already
+built in the ../lib directory. So to build files here, the ./configure
+needs to be executed in the parent directory followed by changing
+directory to the lib directory and calling 'make' there.
+Another way is to do a top level 'make' after the ./configure which
+will make the libraries followed by all the utilities in the src/
+directory. To make them in FreeBSD use 'make -f Makefile.freebsd' .
+
+The utilities in this directory do not have manpages. They have
+relatively complete but terse help messages, typically seen by using
+the '--help' option one or more times. If called several times, the
+shorter form of the help option is more convenient, for example: '-hhh'.
+And of course there is the source code. Unfortunately where the code
+implements many different options, it can become a bit dense. There
+is also a large amount of error checking, as many of these utilities
+were used to test new features placed in the sg v4 driver in Linux.
+
+The sg_chk_asc utility decodes the SCSI additional sense code table
+found at https://www.t10.org/lists/asc-num.txt and checks it against
+the table found in sg_lib_data.c in the lib/ subdirectory. It is
+designed to keep the table in sg_lib_data.c in "sync" with the
+table at the t10.org web site.
+
+The tst_sg_lib utility exercises several functions found in sg_lib.c
+and related files in the 'lib' sibling directory. Use 'tst_sg_lib -h'
+to get more information.
+
+There are both C and C++ files in this directory, they have extensions
+'.c' and '.cpp' respectively. Now both are built with rules in Makefile
+(at least in Linux). A gcc/g++ compiler of 4.7.3 vintage or later
+(or a recent clang compiler) will be required. To make them in FreeBSD
+use 'make -f Makefile.freebsd'.
+
+The sgh_dd utility (C++) uses 'libatomic' which may not be installed
+on some systems. On Debian based systems 'apt install libatomic1' fixes
+this.
+
+Douglas Gilbert
+17th September 2019
diff --git a/testing/bsg_queue_tst.c b/testing/bsg_queue_tst.c
new file mode 100644
index 0000000..06a3522
--- /dev/null
+++ b/testing/bsg_queue_tst.c
@@ -0,0 +1,171 @@
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+/* If the following fails the Linux kernel is probably too old */
+#include <linux/bsg.h>
+
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_linux_inc.h"
+
+/* This program was used to test SCSI mid level queue ordering.
+   The default behaviour is to "queue at head" which is useful for
+   error processing but not for streaming READ and WRITE commands.
+
+*  Copyright (C) 2010-2021 D. Gilbert
+*  This program is free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2, or (at your option)
+*  any later version.
+
+   Invocation: bsg_queue_tst [-t] <bsg_device>
+        -t      queue at tail
+
+   Version 0.91 (20190111)
+
+*/
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+#define SDIAG_CMD_LEN 6
+#define SENSE_BUFFER_LEN 96
+
+#define EBUFF_SZ 256
+
+#ifndef BSG_FLAG_Q_AT_TAIL
+#define BSG_FLAG_Q_AT_TAIL 0x10
+#endif
+
+#ifndef BSG_FLAG_Q_AT_HEAD
+#define BSG_FLAG_Q_AT_HEAD 0x20
+#endif
+
+
+int main(int argc, char * argv[])
+{
+    int bsg_fd, k, ok;
+    uint8_t inq_cdb[INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    uint8_t sdiag_cdb[SDIAG_CMD_LEN] =
+                                {0x1d, 0x10 /* PF */, 0, 0, 0, 0};
+    uint8_t inqBuff[16][INQ_REPLY_LEN];
+    struct sg_io_v4 io_hdr[16];
+    struct sg_io_v4 rio_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t sense_buffer[16][SENSE_BUFFER_LEN] SG_C_CPP_ZERO_INIT;
+    int q_at_tail = 0;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-t", argv[k], 2))
+            ++q_at_tail;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'bsg_queue_tst [-t] <bsg_device>'\n"
+               "where:\n      -t   queue_at_tail (def: q_at_head)\n");
+        return 1;
+    }
+
+    /* An access mode of O_RDWR is required for write()/read() interface */
+    if ((bsg_fd = open(file_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "bsg_queue_tst: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+
+    for (k = 0; k < 16; ++k) {
+        /* Prepare INQUIRY command */
+        memset(&io_hdr[k], 0, sizeof(struct sg_io_v4));
+        io_hdr[k].guard = 'Q';
+        /* io_hdr[k].iovec_count = 0; */  /* memset takes care of this */
+        if (0 == (k % 3)) {
+            io_hdr[k].request_len = sizeof(sdiag_cdb);
+            io_hdr[k].request = (uint64_t)(long)sdiag_cdb;
+        } else {
+            io_hdr[k].request_len = sizeof(inq_cdb);
+            io_hdr[k].request = (uint64_t)(long)inq_cdb;
+            io_hdr[k].din_xfer_len = INQ_REPLY_LEN;
+            io_hdr[k].din_xferp = (uint64_t)(long)inqBuff[k];
+        }
+        io_hdr[k].response = (uint64_t)(long)sense_buffer[k];
+        io_hdr[k].max_response_len = SENSE_BUFFER_LEN;
+        io_hdr[k].timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        io_hdr[k].usr_ptr = k;
+        /* default is to queue at head (in SCSI mid level) */
+        if (q_at_tail)
+            io_hdr[k].flags |= BSG_FLAG_Q_AT_TAIL;
+        else
+            io_hdr[k].flags |= BSG_FLAG_Q_AT_HEAD;
+
+        if (write(bsg_fd, &io_hdr[k], sizeof(struct sg_io_v4)) < 0) {
+            perror("bsg_queue_tst: bsg write error");
+            close(bsg_fd);
+            return 1;
+        }
+    }
+    /* sleep(3); */
+    for (k = 0; k < 16; ++k) {
+        memset(&rio_hdr, 0, sizeof(struct sg_io_v4));
+        rio_hdr.guard = 'Q';
+        if (read(bsg_fd, &rio_hdr, sizeof(struct sg_io_v4)) < 0) {
+            perror("bsg_queue_tst: bsg read error");
+            close(bsg_fd);
+            return 1;
+        }
+        /* now for the error processing */
+        ok = 0;
+        if (0 == rio_hdr.device_status)
+            ok = 1;
+        else {
+            switch (sg_err_category_sense((uint8_t *)(long)
+                    rio_hdr.response, rio_hdr.response_len)) {
+            case SG_LIB_CAT_CLEAN:
+                ok = 1;
+                break;
+            case SG_LIB_CAT_RECOVERED:
+                printf("Recovered error, continuing\n");
+                ok = 1;
+                break;
+            default: /* won't bother decoding other categories */
+                fprintf(stderr, "command error:\n");
+                sg_print_sense(NULL, (uint8_t *)(long)rio_hdr.response,
+                               rio_hdr.response_len, 1);
+                break;
+            }
+        }
+
+        if (ok) { /* output result if it is available */
+            /* if (0 == rio_hdr.pack_id) */
+            if (0 == (rio_hdr.usr_ptr % 3))
+                printf("SEND DIAGNOSTIC %d duration=%u\n",
+                       (int)rio_hdr.usr_ptr, rio_hdr.duration);
+            else
+                printf("INQUIRY %d duration=%u\n", (int)rio_hdr.usr_ptr,
+                       rio_hdr.duration);
+        }
+    }
+
+    close(bsg_fd);
+    return 0;
+}
diff --git a/testing/sg_chk_asc.c b/testing/sg_chk_asc.c
new file mode 100644
index 0000000..1f5f7ed
--- /dev/null
+++ b/testing/sg_chk_asc.c
@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2006-2021 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <ctype.h>
+#include <errno.h>
+
+#include "sg_lib.h"
+
+/* A utility program for the Linux OS SCSI subsystem.
+ *
+ * This program takes a asc_ascq.txt file from www.t10.org and
+ * checks it against the additional sense codes held in the
+ * sg_lib.c file.
+ * The online version of the asc_ascq codes can be found at:
+ * https://www.t10.org/lists/asc-num.txt
+ */
+
+static const char * version_str = "1.09 20210226";
+
+
+#define MAX_LINE_LEN 1024
+
+
+static struct option long_options[] = {
+        {"help", 0, 0, 'h'},
+        {"verbose", 0, 0, 'v'},
+        {"version", 0, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void usage()
+{
+    fprintf(stderr, "Usage: "
+            "sg_chk_asc [--help] [--offset=POS] [--verbose] [--version]\n"
+            "                  <asc_ascq_file>\n"
+            "  where:\n"
+            "    --help|-h          print out usage message\n"
+            "    --offset=POS|-o POS    line position in file where "
+            "text starts\n"
+            "                           origin 0 (def: 24 (was 25))\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Checks asc/ascq codes in <asc_ascq_file> against the sg3_utils "
+            "library.\nThe additional sense codes (asc_ascq) can be found "
+            "at\nwww.t10.org/lists/asc-num.txt .\n"
+           );
+
+}
+
+int main(int argc, char * argv[])
+{
+    int k, j, res, c, num, len;
+    unsigned int asc, ascq;
+    FILE * fp;
+    int offset = 24;
+    int verbose = 0;
+    char file_name[256];
+    char line[MAX_LINE_LEN];
+    char b[MAX_LINE_LEN];
+    char bb[MAX_LINE_LEN];
+    char * cp;
+    int ret = 1;
+
+    memset(file_name, 0, sizeof file_name);
+    memset(line, 0, sizeof file_name);
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "ho:vV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'o':
+            offset = sg_get_num(optarg);
+            if (offset < 0) {
+                fprintf(stderr, "bad argument to --offset\n");
+                return 1;
+            }
+            break;
+        case 'v':
+            ++verbose;
+            break;
+        case 'V':
+            fprintf(stderr, "version: %s\n", version_str);
+            return 0;
+        default:
+            fprintf(stderr, "unrecognised switch code 0x%x ??\n", c);
+            usage();
+            return 1;
+        }
+    }
+    if (optind < argc) {
+        if ('\0' == file_name[0]) {
+            strncpy(file_name, argv[optind], sizeof(file_name) - 1);
+            file_name[sizeof(file_name) - 1] = '\0';
+            ++optind;
+        }
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                fprintf(stderr, "Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return 1;
+        }
+    }
+
+    if (0 == file_name[0]) {
+        fprintf(stderr, "missing file name!\n");
+        usage();
+        return 1;
+    }
+    fp = fopen(file_name, "r");
+    if (NULL == fp) {
+        fprintf(stderr, "open error: %s: %s\n", file_name,
+                safe_strerror(errno));
+        return 1;
+    }
+    for (k = 0; (cp = fgets(line, sizeof(line) - 1, fp)); ++k) {
+        len = strlen(line);
+        if (len < 1)
+            continue;
+        if (! isdigit(line[0]))
+            continue;
+        num = sscanf(line, "%xh/%xh", &asc, &ascq);
+        if (1 == num)
+            ascq = 999;
+        if (num < 1) {
+            if (verbose)
+                fprintf(stderr, "Badly formed line number %d (num=%d)\n",
+                        k + 1, num);
+            continue;
+        }
+        if (len < 26)
+            continue;
+#if 0
+strncpy(b , line, sizeof(b) - 1);
+b[sizeof(b) - 1] = '\0';
+num = strlen(b);
+if (0xd == b[num - 2]) {
+    b[num - 2] = '\0';
+    b[num - 1] = '\0';
+}
+printf("\"%s\",\n", b);
+#endif
+        strncpy(b , line + offset, sizeof(b) - 1);
+        b[sizeof(b) - 1] = '\0';
+        num = strlen(b);
+        if (0xd == b[num - 2])
+            b[num - 2] = '\0';
+        b[num - 1] = '\0';
+        num = strlen(b);
+        for (j = 0; j < num; ++j)
+            b[j] = toupper(b[j]);
+
+        bb[0] = '\0';
+        if (ascq < 999) {
+            cp = sg_get_asc_ascq_str(asc, ascq, sizeof(bb) - 1, bb);
+            if (NULL == cp) {
+                fprintf(stderr, "no entry for %x,%x : %s\n", asc, ascq, b);
+                continue;
+            }
+            num = strlen(cp);
+// fprintf(stderr, "file: asc=%x  acsq=%x  strlen=%d %s\n", asc, ascq, num,
+//         cp);
+//            if (num < 20)
+//                continue;
+            if ((num > 6) &&
+                ((0 == memcmp("ASC", cp, 3)) ||
+                 (0 == memcmp("vendor", cp, 6)))) {
+                fprintf(stderr, "%x,%x differ, ref: %s, sg_lib_data: "
+                        "<missing>\n", asc, ascq, b);
+                continue;
+            }
+            if (num > 20) {
+                cp += 18;
+                num -= 18;
+                for (j = 0; j < num; ++j)
+                    cp[j] = toupper(cp[j]);
+            }
+            if (0 != strcmp(b, cp))
+                fprintf(stderr, "%x,%x differ, ref: %s, sg_lib_data: "
+                        "%s\n", asc, ascq, b, cp);
+        }
+    }
+    if (NULL == cp) {
+        if (feof(fp)) {
+            if (verbose > 2)
+                fprintf(stderr, "EOF detected\n");
+        } else
+            fprintf(stderr, "fgets: %s\n", safe_strerror(errno));
+    } else
+        fprintf(stderr, "%s\n", line);
+
+    res = fclose(fp);
+    if (EOF == res) {
+        fprintf(stderr, "close error: %s\n", safe_strerror(errno));
+        return 1;
+    }
+    return ret;
+}
diff --git a/testing/sg_iovec_tst.cpp b/testing/sg_iovec_tst.cpp
new file mode 100644
index 0000000..7cc1893
--- /dev/null
+++ b/testing/sg_iovec_tst.cpp
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2003-2021 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Test code for D. Gilbert's extensions to the Linux OS SCSI generic ("sg")
+ * device driver.
+ * This C++ program will read a certain number of blocks of a given block
+ * size from a given sg device node using struct sg_iovec and write what is
+ * retrieved out to a normal file. The purpose is to test the sg_iovec
+ * mechanism within the sg_io_hdr and sg_io_v4 structures.
+ *
+ * struct sg_iovec and struct iovec [in include/uapi/uio.h] are basically
+ * the same thing: a pointer followed by a length (of type size_t). If
+ * applied to a disk then the pointer will hold a LBA and 'length' will
+ * be a number of logical blocks (which usually cannot exceed 2**32-1 .
+ *
+ */
+
+#include <unistd.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <errno.h>
+#include <poll.h>
+#include <limits.h>
+#include <time.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <linux/bsg.h>
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+
+// C++ local header
+#include "sg_scat_gath.h"
+
+static const char * version_str = "1.08 20210214";
+
+#define ME "sg_iovec_tst: "
+
+#define IOVEC_ELEMS 1024  /* match current UIO_MAXIOV in <linux/uio.h> */
+
+#define DEF_BLK_SZ 512
+#define SENSE_BUFF_LEN 32
+#define DEF_TIMEOUT 40000       /* 40,000 milliseconds */
+
+static struct sg_iovec iovec[IOVEC_ELEMS];
+
+static int verbose;
+
+static struct option long_options[] = {
+        {"async", no_argument, 0, 'a'},
+        {"bs", required_argument, 0, 'b'},
+        {"elem_size", required_argument, 0, 'e'},
+        {"elem-size", required_argument, 0, 'e'},
+        {"elemsz", required_argument, 0, 'e'},
+        {"fill", required_argument, 0, 'f'},
+        {"from_skip", no_argument, 0, 'F'},
+        {"from-skip", no_argument, 0, 'F'},
+        {"help", no_argument, 0, 'h'},
+        {"num", required_argument, 0, 'n'},
+        {"num_blks", required_argument, 0, 'n'},
+        {"num-blks", required_argument, 0, 'n'},
+        {"sgl", required_argument, 0, 'S'},
+        {"sgv4", no_argument, 0, '4'},
+        {"skip", required_argument, 0, 's'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage(void)
+{
+    printf("Usage: sg_iovec_tst [--async] [--bs=BS] [--elem_sz=ES] "
+           "[--fill=F_ELEMS]\n"
+           "                    [from_skip] [--help] --num=NUM [--sgl=SFN] "
+           "[--sgv4]\n"
+           "                    [--skip=SKIP] [--verbose] [--version] "
+           "SG_DEV OUT_F\n");
+    printf("where:\n"
+           "    --async|-a       async sg usage (def: use ioctl(SG_IO) )\n");
+    printf("    --bs=BS|-b BS    logical block size of SG_DEV (def: 512 "
+           "bytes)\n");
+    printf("    --elem_sz=ES|-e ES    iovec element size (def: BS bytes)\n");
+    printf("    --fill=F_ELEMS|-f F_ELEMS    append F_ELEMS*ES zero bytes "
+           "onto OUT_F\n"
+           "                                 after each iovec element (def: "
+           "0)\n");
+    printf("    --from_skip|-F    sgl output starts from SKIP (def: 0)\n");
+    printf("    --help|-h        this usage message\n");
+    printf("    --num=NUM|-n NUM    number of blocks to read from SG_DEV\n");
+    printf("    --sgl=SFN|-S SFN    Sgl FileName (SFN) that is written to, "
+           "with\n"
+           "                        addresses and lengths having ES as "
+           "their unit\n");
+    printf("    --sgv4|-4        use the sg v4 interface (def: v3 "
+           "interface)\n");
+    printf("    --skip=SKIP|-s SKIP    SKIP blocks before reading S_DEV "
+           "(def: 0)\n");
+    printf("    --verbose|-v     increase verbosity\n");
+    printf("    --version|-V     print version and exit\n\n");
+    printf("Reads from SG_DEV and writes that data to OUT_F in binary. Uses "
+           "iovec\n(a scatter gather list) in linear mode (i.e. it cuts up "
+           "a contiguous\nbuffer). Example:\n"
+           "     sg_iovec_tst -n 8k -e 4k /dev/sg3 out.bin\n");
+}
+
+/* Returns 0 if everything ok */
+static int
+sg_read(int sg_fd, uint8_t * buff, int num_blocks, int from_block, int bs,
+        int elem_size, int async)
+{
+    uint8_t rdCmd[10] = {READ_10, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t senseBuff[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_hdr io_hdr;
+    struct pollfd a_poll;
+    int dxfer_len = bs * num_blocks;
+    int k, pos, rem;
+
+    sg_put_unaligned_be32((uint32_t)from_block, rdCmd + 2);
+    sg_put_unaligned_be16((uint16_t)num_blocks, rdCmd + 7);
+
+    for (k = 0, pos = 0, rem = dxfer_len; k < IOVEC_ELEMS; ++k) {
+        iovec[k].iov_base = buff + pos;
+        iovec[k].iov_len = (rem > elem_size) ? elem_size : rem;
+        if (rem <= elem_size)
+            break;
+        pos += elem_size;
+        rem -= elem_size;
+    }
+    if (k >= IOVEC_ELEMS) {
+        fprintf(stderr, "Can't fit dxfer_len=%d bytes in %d iovec elements "
+                "(would need %d)\n", dxfer_len, IOVEC_ELEMS,
+                dxfer_len / elem_size);
+        fprintf(stderr, "Try expanding elem_size which is currently %d "
+                "bytes\n", elem_size);
+        return -1;
+    }
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(rdCmd);
+    io_hdr.cmdp = rdCmd;
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = dxfer_len;
+    io_hdr.iovec_count = k + 1;
+    io_hdr.dxferp = iovec;
+    io_hdr.mx_sb_len = SENSE_BUFF_LEN;
+    io_hdr.sbp = senseBuff;
+    io_hdr.timeout = DEF_TIMEOUT;
+    io_hdr.pack_id = from_block;
+    if (verbose) {
+        char b[128];
+
+        fprintf(stderr, "cdb: %s\n", sg_get_command_str(rdCmd, 10, true,
+                sizeof(b), b));
+    }
+
+    if (async) {
+        int res = write(sg_fd, &io_hdr, sizeof(io_hdr));
+
+        if (res < 0) {
+            perror("write(<sg_device>), error");
+            return -1;
+        } else if (res < (int)sizeof(io_hdr)) {
+            fprintf(stderr, "write(<sg_device>) returned %d, expected %d\n",
+                    res, (int)sizeof(io_hdr));
+            return -1;
+        }
+        a_poll.fd = sg_fd;
+        a_poll.events = POLLIN;
+        a_poll.revents = 0;
+        res = poll(&a_poll, 1, 2000 /* millisecs */ );
+        if (res < 0) {
+            perror("poll error on <sg_device>");
+            return -1;
+        }
+        if (0 == (POLLIN & a_poll.revents)) {
+            fprintf(stderr, "strange, poll() completed without data to "
+                    "read\n");
+            return -1;
+        }
+        res = read(sg_fd, &io_hdr, sizeof(io_hdr));
+        if (res < 0) {
+            perror("read(<sg_device>), error");
+            return -1;
+        } else if (res < (int)sizeof(io_hdr)) {
+            fprintf(stderr, "read(<sg_device>) returned %d, expected %d\n",
+                    res, (int)sizeof(io_hdr));
+            return -1;
+        }
+    } else if (ioctl(sg_fd, SG_IO, &io_hdr)) {
+        perror("reading (SG_IO) on sg device, error");
+        return -1;
+    }
+    switch (sg_err_category3(&io_hdr)) {
+    case SG_LIB_CAT_CLEAN:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        fprintf(stderr, "Recovered error while reading block=%d, num=%d\n",
+               from_block, num_blocks);
+        break;
+    case SG_LIB_CAT_UNIT_ATTENTION:
+        fprintf(stderr, "Unit attention\n");
+        return -1;
+    default:
+        sg_chk_n_print3("reading", &io_hdr, 1);
+        return -1;
+    }
+    return 0;
+}
+
+/* Returns 0 if everything ok */
+static int
+sg_read_v4(int sg_fd, uint8_t * buff, int num_blocks, int from_block, int bs,
+           int elem_size, int async)
+{
+    uint8_t rdCmd[10] = {READ_10, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t senseBuff[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    struct sg_io_v4 io_hdr;
+    struct pollfd a_poll;
+    int dxfer_len = bs * num_blocks;
+    int k, pos, rem, res;
+
+    sg_put_unaligned_be32((uint32_t)from_block, rdCmd + 2);
+    sg_put_unaligned_be16((uint16_t)num_blocks, rdCmd + 7);
+
+    for (k = 0, pos = 0, rem = dxfer_len; k < IOVEC_ELEMS; ++k) {
+        iovec[k].iov_base = buff + pos;
+        iovec[k].iov_len = (rem > elem_size) ? elem_size : rem;
+        if (rem <= elem_size)
+            break;
+        pos += elem_size;
+        rem -= elem_size;
+    }
+    if (k >= IOVEC_ELEMS) {
+        fprintf(stderr, "Can't fit dxfer_len=%d bytes in %d iovec elements "
+                "(would need %d)\n", dxfer_len, IOVEC_ELEMS,
+                dxfer_len / elem_size);
+        fprintf(stderr, "Try expanding elem_size which is currently %d "
+                "bytes\n", elem_size);
+        return -1;
+    }
+    memset(&io_hdr, 0, sizeof(struct sg_io_v4));
+    io_hdr.guard = 'Q';
+    io_hdr.request_len = sizeof(rdCmd);
+    io_hdr.request = (uint64_t)(uintptr_t)rdCmd;
+    io_hdr.din_xfer_len = dxfer_len;
+    io_hdr.din_xferp = (uint64_t)(uintptr_t)iovec;
+    io_hdr.din_iovec_count = k + 1;
+    io_hdr.max_response_len = SG_DXFER_FROM_DEV;
+    io_hdr.response = (uint64_t)(uintptr_t)senseBuff;
+    io_hdr.timeout = DEF_TIMEOUT;
+    io_hdr.request_extra = from_block;   /* pack_id */
+    if (verbose) {
+        char b[128];
+
+        fprintf(stderr, "cdb: %s\n", sg_get_command_str(rdCmd, 10, true,
+                sizeof(b), b));
+    }
+    if (async) {
+        res = ioctl(sg_fd, SG_IOSUBMIT, &io_hdr);
+        if (res < 0) {
+            perror("ioctl(SG_IOSUBMIT <sg_device>), error");
+            return -1;
+        }
+        a_poll.fd = sg_fd;
+        a_poll.events = POLLIN;
+        a_poll.revents = 0;
+        res = poll(&a_poll, 1, 2000 /* millisecs */ );
+        if (res < 0) {
+            perror("poll error on <sg_device>");
+            return -1;
+        }
+        if (0 == (POLLIN & a_poll.revents)) {
+            fprintf(stderr, "strange, poll() completed without data to "
+                    "read\n");
+            return -1;
+        }
+        res = ioctl(sg_fd, SG_IORECEIVE, &io_hdr);
+        if (res < 0) {
+            perror("ioctl(SG_IORECEIVE <sg_device>), error");
+            return -1;
+        }
+    } else if (ioctl(sg_fd, SG_IO, &io_hdr)) {
+        perror("ioctl(SG_IO) on sg device, error");
+        return -1;
+    }
+
+    res = sg_err_category_new(io_hdr.device_status, io_hdr.transport_status,
+                              io_hdr.driver_status,
+                              (const uint8_t *)(unsigned long)io_hdr.response,
+                              io_hdr.response_len);
+    switch (res) {
+    case SG_LIB_CAT_CLEAN:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        fprintf(stderr, "Recovered error while reading block=%d, num=%d\n",
+               from_block, num_blocks);
+        break;
+    case SG_LIB_CAT_UNIT_ATTENTION:
+        fprintf(stderr, "Unit attention\n");
+        return -1;
+    default:
+        sg_linux_sense_print("reading", io_hdr.device_status,
+                         io_hdr.transport_status, io_hdr.driver_status,
+                         senseBuff, io_hdr.response_len, true);
+        return -1;
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool do_sgv4 = false;
+    bool do_async = false;
+    bool do_help = false;
+    bool from_skip = false;
+    bool blk_size_given = false;
+    bool elem_size_given = false;
+    int sg_fd, fd, c, res, res2, err, dxfer_len;
+    unsigned int k;
+    int blk_size = DEF_BLK_SZ;
+    int elem_size = blk_size;
+    int num_blks = 0;
+    int f_elems = 0;
+    int64_t start_blk = 0;
+    char * sg_dev_name = 0;
+    char * out_file_name = 0;
+    char * sgl_fn = 0;
+    uint8_t * buffp;
+    uint8_t * fillp = NULL;
+    FILE * fp = NULL;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "4ab:e:f:Fhn:s:S:vV",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case '4':
+            do_sgv4 = true;
+            break;
+        case 'a':
+            do_async = true;
+            break;
+        case 'b':
+            blk_size = sg_get_num(optarg);
+            if (blk_size < 1) {
+                printf("Couldn't decode positive number after '--bs=' "
+                       "option\n");
+                sg_dev_name = 0;
+            } else
+                blk_size_given = true;
+            break;
+        case 'e':
+            elem_size = sg_get_num(optarg);
+            if (elem_size < 1) {
+                printf("Couldn't decode positive number after '--elem_size=' "
+                       "option\n");
+                sg_dev_name = 0;
+            } else
+                elem_size_given = true;
+            break;
+        case 'f':
+            f_elems = sg_get_num(optarg);
+            if (f_elems < 0) {
+                printf("Couldn't decode number after '--fill=' option\n");
+                sg_dev_name = 0;
+            }
+            break;
+        case 'F':
+            from_skip = true;
+            break;
+        case 'h':
+            do_help = true;
+            break;
+        case 'n':
+            num_blks = sg_get_num(optarg);
+            if (num_blks < 1) {
+                printf("Couldn't decode positive number after '--num=' "
+                       "option\n");
+                sg_dev_name = 0;
+            }
+            break;
+        case 's':
+            start_blk = sg_get_llnum(optarg);
+            if ((start_blk < 0) || (start_blk > INT_MAX)) {
+                printf("Couldn't decode number after '--skip=' option\n");
+                sg_dev_name = 0;
+            }
+            break;
+        case 'S':
+            if (sgl_fn) {
+                printf("Looks like --sgl=SFN has been given twice\n");
+                sg_dev_name = 0;
+            } else
+                sgl_fn = optarg;
+            break;
+        case 'v':
+            ++verbose;
+            break;
+        case 'V':
+            printf("Version: %s\n", version_str);
+            return 0;
+        default:
+            fprintf(stderr, "unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        if (NULL == sg_dev_name) {
+            sg_dev_name = argv[optind];
+            ++optind;
+        }
+        if (optind < argc) {
+            if (sg_dev_name) {
+                out_file_name = argv[optind];
+                ++optind;
+            }
+            if (optind < argc) {
+                for (; optind < argc; ++optind)
+                    fprintf(stderr, "Unexpected extra argument: %s\n",
+                            argv[optind]);
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+    }
+    if (do_help) {
+        usage();
+        return 0;
+    }
+    if (NULL == sg_dev_name) {
+        printf(">>> need sg node name (e.g. /dev/sg3)\n\n");
+        usage();
+        return 1;
+    }
+    if (NULL == out_file_name) {
+        printf(">>> need out filename (to place what is fetched by READ\n\n");
+        usage();
+        return 1;
+    }
+    if (0 == num_blks) {
+        printf(">>> need number of blocks to READ\n\n");
+        usage();
+        return 1;
+    }
+
+    if ((! elem_size_given) && blk_size_given)
+        elem_size = blk_size;
+
+    if (do_async)
+        sg_fd = open(sg_dev_name, O_RDWR);
+    else
+        sg_fd = open(sg_dev_name, O_RDONLY);
+    if (sg_fd < 0) {
+        perror(ME "sg device node open error");
+        return 1;
+    }
+    /* Don't worry, being very careful not to write to a none-sg file ... */
+    res = ioctl(sg_fd, SG_GET_VERSION_NUM, &k);
+    if ((res < 0) || (k < 30000)) {
+        printf(ME "not a sg device, or driver prior to 3.x\n");
+        return 1;
+    }
+    fd = open(out_file_name, O_WRONLY | O_CREAT, 0666);
+    if (fd < 0) {
+        perror(ME "output file open error");
+        return 1;
+    }
+    if (f_elems > 0) {
+        fillp = (uint8_t *)calloc(f_elems, elem_size);
+        if (NULL == fillp) {
+            fprintf(stderr, "fill calloc for %d bytes failed\n",
+                    f_elems * elem_size);
+            goto fini;
+        }
+    }
+    if (sgl_fn) {
+        time_t t = time(NULL);
+        struct tm *tm = localtime(&t);
+        char s[128];
+
+        fp = fopen(sgl_fn, "w");
+        if (NULL == fp) {
+            err = errno;
+            fprintf(stderr, "Unable to open %s, error: %s\n", sgl_fn,
+                    strerror(err));
+            res = sg_convert_errno(err);
+            goto fini;
+        }
+        strftime(s, sizeof(s), "%c", tm);
+        fprintf(fp, "# Scatter gather list generated by sg_iovec_tst  "
+                "%s\n#\n", s);
+    }
+
+    dxfer_len = num_blks * blk_size;
+    buffp = (uint8_t *)calloc(num_blks, blk_size);
+    if (buffp) {
+        int dx_len;
+        int64_t curr_blk = from_skip ? start_blk : 0;
+
+        if (do_sgv4) {
+            if (sg_read(sg_fd, buffp, num_blks, (int)start_blk, blk_size,
+                        elem_size, do_async))
+                goto free_buff;
+        } else {
+            if (sg_read_v4(sg_fd, buffp, num_blks, (int)start_blk, blk_size,
+                           elem_size, do_async))
+                goto free_buff;
+        }
+        if (f_elems > 0) {
+            int fill_len = f_elems * elem_size;
+
+            for (dx_len = 0; dx_len < dxfer_len; dx_len += elem_size) {
+                if (write(fd, buffp + dx_len, elem_size) < 0) {
+                    perror(ME "partial dxfer output write failed");
+                    break;
+                }
+                if (sgl_fn) {
+                    fprintf(fp, "%" PRId64 ",1\n", curr_blk);
+                    curr_blk += f_elems + 1;
+                }
+                if (write(fd, fillp, fill_len) < 0) {
+                    perror(ME "partial fill output write failed");
+                    break;
+                }
+            }
+        } else if (write(fd, buffp, dxfer_len) < 0)
+            perror(ME "full output write failed");
+        else if (sgl_fn) {
+            for (dx_len = 0; dx_len < dxfer_len; dx_len += elem_size)
+                fprintf(fp, "%" PRId64 ",1\n", curr_blk++);
+        }
+free_buff:
+        free(buffp);
+    } else
+        fprintf(stderr, "user space calloc for %d bytes failed\n",
+                dxfer_len);
+    res = close(fd);
+    if (res < 0) {
+        perror(ME "output file close error");
+        close(sg_fd);
+        return 1;
+    }
+fini:
+    res2 = close(sg_fd);
+    if (res2 < 0) {
+        err = errno;
+        perror(ME "sg device close error");
+        if (0 == res)
+            res = sg_convert_errno(err);
+    }
+    if (fp)
+        fclose(fp);
+    return res;
+}
diff --git a/testing/sg_json_builder_test.c b/testing/sg_json_builder_test.c
new file mode 100644
index 0000000..a43b9f7
--- /dev/null
+++ b/testing/sg_json_builder_test.c
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * Simple streaming JSON writer
+ *
+ * This takes care of the annoying bits of JSON syntax like the commas
+ * after elements
+ *
+ * Authors:	Stephen Hemminger <stephen@networkplumber.org>
+ *
+ * Borrowed from Linux kernel [5.17.0]: tools/bpf/bpftool/json_writer.[hc]
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <malloc.h>
+#include <inttypes.h>
+#include <stdint.h>
+
+#include "../lib/sg_json_builder.h"
+
+int main(int argc, char **argv)
+{
+	json_writer_t *wr = jsonw_new(stdout);
+
+	jsonw_start_object(wr);
+	jsonw_pretty(wr, true);
+	jsonw_name(wr, "Vyatta");
+	jsonw_start_object(wr);
+	jsonw_string_field(wr, "url", "http://vyatta.com");
+	jsonw_uint_field(wr, "downloads", 2000000ul);
+	jsonw_float_field(wr, "stock", 8.16);
+
+	jsonw_name(wr, "ARGV");
+	jsonw_start_array(wr);
+	while (--argc)
+		jsonw_string(wr, *++argv);
+	jsonw_end_array(wr);
+
+	jsonw_name(wr, "empty");
+	jsonw_start_array(wr);
+	jsonw_end_array(wr);
+
+	jsonw_name(wr, "NIL");
+	jsonw_start_object(wr);
+	jsonw_end_object(wr);
+
+	jsonw_null_field(wr, "my_null");
+
+	jsonw_name(wr, "special chars");
+	jsonw_start_array(wr);
+	jsonw_string_field(wr, "slash", "/");
+	jsonw_string_field(wr, "newline", "\n");
+	jsonw_string_field(wr, "tab", "\t");
+	jsonw_string_field(wr, "ff", "\f");
+	jsonw_string_field(wr, "quote", "\"");
+	jsonw_string_field(wr, "tick", "\'");
+	jsonw_string_field(wr, "backslash", "\\");
+	jsonw_end_array(wr);
+
+jsonw_name(wr, "ARGV");
+jsonw_start_array(wr);
+jsonw_string(wr, "boo: appended or new entry?");
+jsonw_end_array(wr);
+
+	jsonw_end_object(wr);
+
+	jsonw_end_object(wr);
+	jsonw_destroy(&wr);
+	return 0;
+}
+
diff --git a/testing/sg_mrq_dd.cpp b/testing/sg_mrq_dd.cpp
new file mode 100644
index 0000000..01b38e7
--- /dev/null
+++ b/testing/sg_mrq_dd.cpp
@@ -0,0 +1,4664 @@
+/*
+ * A utility program for copying files. Specialised for "files" that
+ * represent devices that understand the SCSI command set.
+ *
+ * Copyright (C) 2018-2022 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is a specialisation of the Unix "dd" command in which
+ * one or both of the given files is a scsi generic device.
+ * A logical block size ('bs') is assumed to be 512 if not given. This
+ * program complains if 'ibs' or 'obs' are given with some other value
+ * than 'bs'. If 'if' is not given or 'if=-' then stdin is assumed. If
+ * 'of' is not given or 'of=-' then stdout assumed.
+ *
+ * A non-standard argument "bpt" (blocks per transfer) is added to control
+ * the maximum number of blocks in each transfer. The default value is 128.
+ * For example if "bs=512" and "bpt=32" then a maximum of 32 blocks (16 KiB
+ * in this case) are transferred to or from the sg device in a single SCSI
+ * command.
+ *
+ * This version is designed for the linux kernel 4 and 5 series.
+ *
+ * sg_mrq_dd uses C++ threads and MRQ (multiple requests (in one invocation))
+ * facilities in the sg version 4 driver to do "dd" type copies and verifies.
+ *
+ */
+
+static const char * version_str = "1.44 20221020";
+
+#define _XOPEN_SOURCE 600
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <time.h>       /* for nanosleep() */
+#include <poll.h>
+#include <limits.h>
+// #include <pthread.h>
+#include <signal.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#ifndef major
+#include <sys/types.h>
+#endif
+#include <sys/time.h>
+#include <linux/major.h>        /* for MEM_MAJOR, SCSI_GENERIC_MAJOR, etc */
+#include <linux/fs.h>           /* for BLKSSZGET and friends */
+#include <sys/mman.h>           /* for mmap() system call */
+
+#include <vector>
+#include <array>
+#include <atomic>       // C++ header replacing <stdatomic.h>
+#include <random>
+#include <thread>       // needed for std::this_thread::yield()
+#include <mutex>
+#include <condition_variable>   // for infant_cv: copy/verify first segment
+                                // single threaded
+#include <chrono>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifdef HAVE_GETRANDOM
+#include <sys/random.h>         /* for getrandom() system call */
+#endif
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+// C++ local header
+#include "sg_scat_gath.h"
+
+// C headers associated with sg3_utils library
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+using namespace std;
+
+// #ifdef __GNUC__
+// #ifndef  __clang__
+// #pragma GCC diagnostic ignored "-Wclobbered"
+// #endif
+// #endif
+
+
+#ifndef SGV4_FLAG_POLLED
+#define SGV4_FLAG_POLLED 0x800
+#endif
+
+#define MAX_SGL_NUM_VAL (INT32_MAX - 1)  /* should reduce for testing */
+// #define MAX_SGL_NUM_VAL 7  /* should reduce for testing */
+#if MAX_SGL_NUM_VAL > INT32_MAX
+#error "MAX_SGL_NUM_VAL cannot exceed 2^31 - 1"
+#endif
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BLOCKS_PER_TRANSFER 128
+#define DEF_BLOCKS_PER_2048TRANSFER 32
+#define DEF_SDT_ICT_MS 300
+#define DEF_SDT_CRT_SEC 3
+#define DEF_SCSI_CDB_SZ 10
+#define MAX_SCSI_CDB_SZ 16      /* could be 32 */
+#define PACK_ID_TID_MULTIPLIER (0x1000000)      /* 16,777,216 */
+#define MAX_SLICES 16           /* number of IFILE,OFILE pairs */
+#define MAX_BPT_VALUE (1 << 24)         /* used for maximum bs as well */
+#define MAX_COUNT_SKIP_SEEK (1LL << 48) /* coverity wants upper bound */
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define READ_CAP_REPLY_LEN 8
+#define RCAP16_REPLY_LEN 32
+
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs == 60 seconds */
+
+#define SGP_READ10 0x28
+#define SGP_PRE_FETCH10 0x34
+#define SGP_PRE_FETCH16 0x90
+#define SGP_VERIFY10 0x2f
+#define SGP_WRITE10 0x2a
+#define DEF_NUM_THREADS 4
+#define MAX_NUM_THREADS 1024 /* was SG_MAX_QUEUE with v3 driver */
+#define DEF_MRQ_NUM 16
+
+#define FT_UNKNOWN 0            /* yet to be checked */
+#define FT_OTHER 1              /* filetype other than one of the following */
+#define FT_SG 2                 /* filetype is sg char device */
+#define FT_DEV_NULL 4           /* either /dev/null, /dev/zero, or "." */
+#define FT_ST 8                 /* filetype is st char device (tape) */
+#define FT_BLOCK 16             /* filetype is a block device */
+#define FT_FIFO 32              /* fifo (named or unnamed pipe (stdout)) */
+#define FT_CHAR 64              /* fifo (named or unnamed pipe (stdout)) */
+#define FT_RANDOM_0_FF 128      /* iflag=00, iflag=ff and iflag=random
+                                   override if=IFILE */
+#define FT_ERROR 256            /* couldn't "stat" file */
+
+#define DEV_NULL_MINOR_NUM 3
+#define DEV_ZERO_MINOR_NUM 5
+
+#define EBUFF_SZ 768
+
+#define PROC_SCSI_SG_VERSION "/proc/scsi/sg/version"
+#define SYS_SCSI_SG_VERSION "/sys/module/sg/version"
+
+
+struct flags_t {
+    bool append;
+    bool coe;
+    bool dio;
+    bool direct;
+    bool dpo;
+    bool dsync;
+    bool excl;
+    bool ff;
+    bool fua;
+    bool masync;        /* more async sg v4 driver fd flag */
+    bool mout_if;       /* META_OUT_IF flag at mrq level */
+    bool nocreat;
+    bool no_dur;
+    bool no_thresh;
+    bool no_waitq;      /* dummy, no longer supported, just warn */
+    bool order_wr;
+    bool polled;        /* was previously 'hipri' */
+    bool qhead;
+    bool qtail;
+    bool random;
+    bool serial;
+    bool same_fds;
+    bool wq_excl;
+    bool zero;
+    int cdl;            /* command duration limits, 0 --> no cdl */
+    int mmap;
+};
+
+typedef pair<int64_t, int> get_next_res_t;      /* LBA, num */
+typedef array<uint8_t, MAX_SCSI_CDB_SZ> cdb_arr_t;
+
+struct cp_ver_pair_t {
+    cp_ver_pair_t() {}
+
+    get_next_res_t get_next(int desired_num_blks);
+
+    enum class my_state {empty,
+                         init,
+                         underway,
+                         ignore,
+                         finished} state = {my_state::empty};
+
+    int my_index = 0;
+    int in_fd = -1;
+    int in_type = FT_UNKNOWN;
+    int out_fd = -1;
+    int out_type = FT_UNKNOWN;
+
+    int64_t dd_count = 0;
+    atomic<int64_t> next_count_pos {};
+    atomic<int64_t> in_rem_count {};
+    atomic<int64_t> out_rem_count {};
+    atomic<int> in_partial {};
+    atomic<int> out_partial {};
+    atomic<int> sum_of_resids {};
+};
+
+typedef array<cp_ver_pair_t, MAX_SLICES> cp_ver_arr_t;
+
+/* There is one instance of this structure and it is at file scope so it is
+ * initialized to zero. The design of this copy multi-threaded copy algorithm
+ * attempts to have no locks on the fast path. Contention in gcoll.get_next()
+ * is resolved by the loser repeating its operation. Statistics and error
+ * information is held in each thread until it shuts down and contention
+ * can occur at that point. */
+struct global_collection        /* one instance visible to all threads */
+{
+    cp_ver_arr_t cp_ver_arr;
+
+    /* get_next() is the pivotal function for multi-threaded safety. It can
+     * be safely called from all threads with the desired number of blocks
+     * (typically mrq*bpt) and this function returns a pair. The first pair
+     * value is the starting count value/index [0..dd_count) and the second
+     * pair value is the number of blocks to copy. If desired_num_blks is
+     * negative this flags an error has occurred. If the second value in the
+     * returned pair is 0 then the calling thread should shutdown; a
+     * negative value indicates an error has occurred (e.g. in another
+     * thread) and the calling thread should shutdown. */
+
+    int in0fd;
+    int64_t dd_count;
+    int in_type;                /* expect all IFILEs to have same type */
+    int cdbsz_in;
+    int help;
+    struct flags_t in_flags;
+    atomic<int> in_partial;           /*  | */
+    off_t in_st_size;                 /* Only for FT_OTHER (regular) file */
+    int mrq_num;                      /* if user gives 0, set this to 1 */
+    int out0fd;
+    int out_type;
+    int cdbsz_out;
+    struct flags_t out_flags;
+    atomic<int> out_partial;          /*  | */
+    off_t out_st_size;                /* Only for FT_OTHER (regular) file */
+    condition_variable infant_cv;     /* after thread:0 does first segment */
+    mutex infant_mut;
+    int bs;
+    int bpt;
+    int cmd_timeout;            /* in milliseconds */
+    int elem_sz;
+    int outregfd;
+    int outreg_type;
+    off_t outreg_st_size;
+    atomic<int> dio_incomplete_count;
+    atomic<int> sum_of_resids;
+    atomic<int> reason_res;
+    atomic<int> most_recent_pack_id;
+    uint32_t sdt_ict; /* stall detection; initial check time (milliseconds) */
+    uint32_t sdt_crt; /* check repetition time (seconds), after first stall */
+    int dry_run;
+    int verbose;
+    bool mrq_eq_0;              /* true when user gives mrq=0 */
+    bool processed;
+    bool cdbsz_given;
+    bool cdl_given;
+    bool count_given;
+    bool ese;
+    bool flexible;
+    bool mrq_polled;
+    bool ofile_given;
+    bool unit_nanosec;          /* default duration unit is millisecond */
+    bool verify;                /* don't copy, verify like Unix: cmp */
+    bool prefetch;              /* for verify: do PF(b),RD(a),V(b)_a_data */
+    vector<string> inf_v;
+    vector<string> outf_v;
+    const char * infp;
+    const char * outfp;
+    class scat_gath_list i_sgl;
+    class scat_gath_list o_sgl;
+};
+
+typedef struct request_element
+{       /* one instance per worker thread */
+    struct global_collection *clp;
+    bool has_share;
+    bool both_sg;
+    bool same_sg;
+    bool only_in_sg;
+    bool only_out_sg;
+    bool stop_after_write;
+    bool stop_now;
+    int id;
+    int bs;
+    int infd;
+    int outfd;
+    int outregfd;
+    uint8_t * buffp;
+    uint8_t * alloc_bp;
+    struct sg_io_v4 io_hdr4[2];
+    uint8_t cmd[MAX_SCSI_CDB_SZ];
+    uint8_t sb[SENSE_BUFF_LEN];
+    int dio_incomplete_count;
+    int mmap_active;
+    int rd_p_id;
+    int rep_count;
+    int rq_id;
+    int mmap_len;
+    int mrq_id;
+    int mrq_index;
+    int mrq_pack_id_off;
+    uint32_t a_mrq_din_blks;
+    uint32_t a_mrq_dout_blks;
+    int64_t in_follow_on;
+    int64_t out_follow_on;
+    int64_t in_local_count;
+    int64_t out_local_count;
+    int64_t in_rem_count;
+    int64_t out_rem_count;
+    int in_local_partial;
+    int out_local_partial;
+    int in_resid_bytes;
+    long seed;
+#ifdef HAVE_SRAND48_R   /* gcc extension. N.B. non-reentrant version slower */
+    struct drand48_data drand;/* opaque, used by srand48_r and mrand48_r */
+#endif
+} Rq_elem;
+
+/* Additional parameters for sg_start_io() and sg_finish_io() */
+struct sg_io_extra {
+    bool prefetch;
+    bool dout_is_split;
+    int hpv4_ind;
+    int blk_offset;
+    int blks;
+};
+
+#define MONO_MRQ_ID_INIT 0x10000
+
+
+
+/* Use this class to wrap C++11 <random> features to produce uniform random
+ * unsigned ints in the range [lo, hi] (inclusive) given a_seed */
+class Rand_uint {
+public:
+    Rand_uint(unsigned int lo, unsigned int hi, unsigned int a_seed)
+        : uid(lo, hi), dre(a_seed) { }
+    /* uid ctor takes inclusive range when integral type */
+
+    unsigned int get() { return uid(dre); }
+
+private:
+    uniform_int_distribution<unsigned int> uid;
+    default_random_engine dre;
+};
+
+static atomic<int> num_ebusy(0);
+static atomic<int> num_start_eagain(0);
+static atomic<int> num_fin_eagain(0);
+static atomic<int> num_miscompare(0);
+static atomic<int> num_fallthru_sigusr2(0);
+static atomic<bool> vb_first_time(true);
+
+static sigset_t signal_set;
+static sigset_t orig_signal_set;
+
+static const char * sg_allow_dio = "/sys/module/sg/parameters/allow_dio";
+
+static int do_both_sg_segment(Rq_elem * rep, scat_gath_iter & i_sg_it,
+                              scat_gath_iter & o_sg_it, int seg_blks,
+                              vector<cdb_arr_t> & a_cdb,
+                              vector<struct sg_io_v4> & a_v4);
+static int do_both_sg_segment_mrq0(Rq_elem * rep, scat_gath_iter & i_sg_it,
+                                   scat_gath_iter & o_sg_it, int seg_blks);
+static int do_normal_sg_segment(Rq_elem * rep, scat_gath_iter & i_sg_it,
+                                scat_gath_iter & o_sg_it, int seg_blks,
+                                vector<cdb_arr_t> & a_cdb,
+                                vector<struct sg_io_v4> & a_v4);
+static int do_normal_normal_segment(Rq_elem * rep, scat_gath_iter & i_sg_it,
+                                    scat_gath_iter & o_sg_it, int seg_blks);
+
+#define STRERR_BUFF_LEN 128
+
+static mutex strerr_mut;
+
+static bool have_sg_version = false;
+static int sg_version = 0;
+static bool sg_version_ge_40045 = false;
+static atomic<bool> shutting_down{false};
+static bool do_sync = false;
+static int do_time = 1;
+static struct global_collection gcoll;
+static struct timeval start_tm;
+static int num_threads = DEF_NUM_THREADS;
+static bool after1 = false;
+static int listen_t_tid;
+
+static const char * my_name = "sg_mrq_dd: ";
+
+// static const char * mrq_blk_s = "mrq: ordinary blocking";
+static const char * mrq_svb_s = "mrq: shared variable blocking (svb)";
+static const char * mrq_ob_s = "mrq: ordered blocking";
+static const char * mrq_vb_s = "mrq: variable blocking";
+
+
+#ifdef __GNUC__
+static int pr2serr_lk(const char * fmt, ...)
+        __attribute__ ((format (printf, 1, 2)));
+#else
+static int pr2serr_lk(const char * fmt, ...);
+#endif
+
+
+static int
+pr2serr_lk(const char * fmt, ...)
+{
+    int n;
+    va_list args;
+    lock_guard<mutex> lk(strerr_mut);
+
+    va_start(args, fmt);
+    n = vfprintf(stderr, fmt, args);
+    va_end(args);
+    return n;
+}
+
+static void
+usage(int pg_num)
+{
+    if (pg_num > 4)
+        goto page5;
+    if (pg_num > 3)
+        goto page4;
+    else if (pg_num > 2)
+        goto page3;
+    else if (pg_num > 1)
+        goto page2;
+
+    pr2serr("Usage: sg_mrq_dd  [bs=BS] [conv=CONV] [count=COUNT] [ibs=BS] "
+            "[if=IFILE*]\n"
+            "                  [iflag=FLAGS] [obs=BS] [of=OFILE*] "
+            "[oflag=FLAGS]\n"
+            "                  [seek=SEEK] [skip=SKIP] [--help] [--verify] "
+            "[--version]\n\n");
+    pr2serr("                  [bpt=BPT] [cdbsz=6|10|12|16] [cdl=CDL] "
+            "[dio=0|1]\n"
+            "                  [elemsz_kb=EKB] [ese=0|1] [fua=0|1|2|3] "
+            "[polled=NRQS]\n"
+            "                  [mrq=NRQS] [ofreg=OFREG] [sdt=SDT] "
+            "[sync=0|1]\n"
+            "                  [thr=THR] [time=0|1|2[,TO]] [verbose=VERB] "
+            "[--dry-run]\n"
+            "                  [--pre-fetch] [--verbose] [--version]\n\n"
+            "  where: operands have the form name=value and are pecular to "
+            "'dd'\n"
+            "         style commands, and options start with one or "
+            "two hyphens;\n"
+            "         the main operands and options (shown in first group "
+            "above) are:\n"
+            "    bs          must be device logical block size (default "
+            "512)\n"
+            "    conv        comma separated list from: [nocreat,noerror,"
+            "notrunc,\n"
+            "                null,sync]\n"
+            "    count       number of blocks to copy (def: device size)\n"
+            "    if          file(s) or device(s) to read from (def: "
+            "stdin)\n"
+            "    iflag       comma separated list from: [00,coe,dio,"
+            "direct,dpo,\n"
+            "                dsync,excl,ff,fua,masync,mmap,mout_if,nodur,"
+            "null,\n"
+            "                order,qhead,qtail,random,same_fds,serial,"
+            "wq_excl]\n"
+            "    of          file(s) or device(s) to write to (def: "
+            "/dev/null)\n"
+            "                'of=.' also outputs to /dev/null\n"
+            "    oflag       comma separated list from: [append,nocreat,\n"
+            "                <<list from iflag>>]\n"
+            "    seek        block position to start writing to OFILE\n"
+            "    skip        block position to start reading from IFILE\n"
+            "    --help|-h      output this usage message then exit\n"
+            "    --verify|-x    do a verify (compare) operation [def: do a "
+            "copy]\n"
+            "    --version|-V   output version string then exit\n\n"
+            "Copy IFILE to OFILE, similar to dd command. A comma separated "
+            "list of files\n may be given for IFILE*, ditto for OFILE*. "
+            "This utility is specialized for\nSCSI devices and uses the "
+            "'multiple requests' (mrq) in a single invocation\nfacility in "
+            "version 4 of the sg driver unless mrq=0. Usually one or both\n"
+            "IFILE and OFILE will be sg devices. With the --verify option "
+            "it does a\nverify/compare operation instead of a copy. This "
+            "utility is Linux specific.\nUse '-hh', '-hhh', '-hhhh' or "
+            "'-hhhhh' for more information.\n"
+           );
+    return;
+page2:
+    pr2serr("Syntax:  sg_mrq_dd [operands] [options]\n\n"
+            "         the lesser used operands and option are:\n\n"
+            "    bpt         is blocks_per_transfer (default is 128)\n"
+            "    cdbsz       size of SCSI READ, WRITE or VERIFY cdb_s "
+            "(default is 10)\n"
+            "    cdl         command duration limits value 0 to 7 (def: "
+            "0 (no cdl))\n"
+            "    dio         is direct IO, 1->attempt, 0->indirect IO (def)\n"
+            "    elemsz_kb=EKB    scatter gather list element size in "
+            "kibibytes;\n"
+            "                     must be power of two, >= page_size "
+            "(typically 4)\n"
+            "    ese=0|1     exit on secondary error when 1, else continue\n"
+            "    fua         force unit access: 0->don't(def), 1->OFILE, "
+            "2->IFILE,\n"
+            "                3->OFILE+IFILE\n"
+            "    ibs         IFILE logical block size, cannot differ from "
+            "obs or bs\n"
+            "    hipri       same as polled=NRQS; name 'hipri' is deprecated\n"
+            "    mrq         NRQS is number of cmds placed in each sg "
+            "ioctl\n"
+            "                (def: 16). Does not set mrq hipri flag.\n"
+            "                if mrq=0 does one-by-one, blocking "
+            "ioctl(SG_IO)s\n"
+            "    obs         OFILE logical block size, cannot differ from "
+            "ibs or bs\n"
+            "    ofreg       OFREG is regular file or pipe to send what is "
+            "read from\n"
+            "    polled      similar to mrq=NRQS operand but also sets "
+            "polled flag\n"
+            "                IFILE in the first half of each shared element\n"
+            "    sdt         stall detection times: CRT[,ICT]. CRT: check "
+            "repetition\n"
+            "                time (after first) in seconds; ICT: initial "
+            "check time\n"
+            "                in milliseconds. Default: 3,300 . Use CRT=0 "
+            "to disable\n"
+            "    sync        0->no sync(def), 1->SYNCHRONIZE CACHE on OFILE "
+            "after copy\n"
+            "    thr         is number of threads, must be > 0, default 4, "
+            "max 1024\n"
+            "    time        0->no timing; 1/2->millisec/nanosec precision "
+            "(def: 1);\n"
+            "                TO is command timeout in seconds (def: 60)\n"
+            "    verbose     increase verbosity (def: VERB=0)\n"
+            "    --dry-run|-d     prepare but bypass copy/read\n"
+            "    --prefetch|-p    with verify: do pre-fetch first\n"
+            "    --verbose|-v     increase verbosity of utility\n\n"
+            "Use '-hhh', '-hhhh' or '-hhhhh' for more information about "
+            "flags.\n"
+           );
+    return;
+page3:
+    pr2serr("Syntax:  sg_mrq_dd [operands] [options]\n\n"
+            "  where: 'iflag=<arg>' and 'oflag=<arg>' arguments are listed "
+            "below:\n\n"
+            "    00          use all zeros instead of if=IFILE (only in "
+            "iflag)\n"
+            "    00,ff       generates blocks that contain own (32 bit be) "
+            "blk addr\n"
+            "    append      append output to OFILE (assumes OFILE is "
+            "regular file)\n"
+            "    coe         continue of error (reading, fills with zeros)\n"
+            "    dio         sets the SG_FLAG_DIRECT_IO in sg requests\n"
+            "    direct      sets the O_DIRECT flag on open()\n"
+            "    dpo         sets the DPO (disable page out) in SCSI READs "
+            "and WRITEs\n"
+            "    dsync       sets the O_SYNC flag on open()\n"
+            "    excl        sets the O_EXCL flag on open()\n"
+            "    ff          use all 0xff bytes instead of if=IFILE (only in "
+            "iflag)\n"
+            "    fua         sets the FUA (force unit access) in SCSI READs "
+            "and WRITEs\n"
+            "    hipri       same as 'polled'; name 'hipri' is deprecated\n"
+            "    masync      set 'more async' flag on this sg device\n"
+            "    mmap        setup mmap IO on IFILE or OFILE\n"
+            "    mmap,mmap    when used twice, doesn't call munmap()\n"
+            "    mout_if     set META_OUT_IF flag on control object\n"
+            "    nocreat     will fail rather than create OFILE\n"
+            "    nodur       turns off command duration calculations\n"
+            "    no_thresh   skip checking per fd max data xfer size\n"
+            "    order       require write ordering on sg->sg copy; only "
+            "for oflag\n"
+            "    polled      set POLLED flag and use blk_poll() for "
+            "completions\n"
+            "    qhead       queue new request at head of block queue\n"
+            "    qtail       queue new request at tail of block queue (def: "
+            "q at head)\n"
+            "    random      use random data instead of if=IFILE (only in "
+            "iflag)\n"
+            "    same_fds    each thread of a IOFILE pair uses same fds\n"
+            "    serial      serialize sg command execution (def: overlap)\n"
+            "    wq_excl     set SG_CTL_FLAGM_EXCL_WAITQ on this sg fd\n"
+            "\n"
+            "Copies IFILE to OFILE (and to OFILE2 if given). If IFILE and "
+            "OFILE are sg\ndevices 'shared' mode is selected. "
+            "When sharing, the data stays in a\nsingle "
+            "in-kernel buffer which is copied (or mmap-ed) to the user "
+            "space\nif the 'ofreg=OFREG' is given. Use '-hhhh' or '-hhhhh' "
+            "for more information.\n"
+           );
+    return;
+page4:
+    pr2serr("pack_id:\n"
+            "These are ascending integers, starting at 1, associated with "
+            "each issued\nSCSI command. When both IFILE and OFILE are sg "
+            "devices, then the READ in\neach read-write pair is issued an "
+            "even pack_id and its WRITE pair is\ngiven the pack_id one "
+            "higher (i.e. an odd number). This enables a\n'dmesg -w' "
+            "user to see that progress is being "
+            "made.\n\n");
+    pr2serr("Debugging:\n"
+            "Apart from using one or more '--verbose' options which gets a "
+            "bit noisy\n'dmesg -w' can give a good overview "
+            "of what is happening.\nThat does a sg driver object tree "
+            "traversal that does minimal locking\nto make sure that each "
+            "traversal is 'safe'. So it is important to note\nthe whole "
+            "tree is not locked. This means for fast devices the overall\n"
+            "tree state may change while the traversal is occurring. For "
+            "example,\nit has been observed that both the read- and write- "
+            "sides of a request\nshare show they are in 'active' state "
+            "which should not be possible.\nIt occurs because the read-side "
+            "probably jumped out of active state and\nthe write-side "
+            "request entered it while some other nodes were being "
+            "printed.\n\n");
+    pr2serr("Busy state:\n"
+            "Busy state (abbreviated to 'bsy' in the dmesg "
+            "output)\nis entered during request setup and completion. It "
+            "is intended to be\na temporary state. It should not block "
+            "but does sometimes (e.g. in\nblock_get_request()). Even so "
+            "that blockage should be short and if not\nthere is a "
+            "problem.\n\n");
+    pr2serr("--verify :\n"
+            "For comparing IFILE with OFILE. Does repeated sequences of: "
+            "READ(ifile)\nand uses data returned to send to VERIFY(ofile, "
+            "BYTCHK=1). So the OFILE\ndevice/disk is doing the actual "
+            "comparison. Stops on first miscompare\nunless oflag=coe is "
+            "given\n\n");
+    pr2serr("--prefetch :\n"
+            "Used with --verify option. Prepends a PRE-FETCH(ofile, IMMED) "
+            "to verify\nsequence. This should speed the trailing VERIFY by "
+            "making sure that\nthe data it needs for the comparison is "
+            "already in its cache.\n");
+    return;
+page5:
+    pr2serr("       IFILE and/or OFILE lists\n\n"
+            "For dd, its if= operand takes a single file (or device), ditto "
+            "for the of=\noperand. This utility extends that to "
+            "allowing a comma separated list\nof files. Ideally if multiple "
+            "IFILEs are given, the same number of OFILEs\nshould be given. "
+            "Simple expansions occur to make the list lengths equal\n"
+            "(e.g. if 5 IFILEs are given but no OFILEs, then OFILEs is "
+            "expanded to 5\n'/dev/null' files). IFILE,OFILE pairs with "
+            "the same list position are\ncalled a 'slice'. Each slice is "
+            "processed (i.e. copy or verify) in one or\nmore threads. The "
+            "number of threads must be >= the number of slices. Best\nif "
+            "the number of threads is an integer multiple of the number of "
+            "slices.\nThe file type of multiple IFILEs must be the same, "
+            "ditto for OFILEs.\nSupport for slices is for testing rather "
+            "than a general mechanism.\n");
+}
+
+static void
+lk_print_command_len(const char *prefix, uint8_t * cmdp, int len, bool lock)
+{
+    if (lock) {
+        lock_guard<mutex> lk(strerr_mut);
+
+        if (prefix && *prefix)
+            fputs(prefix, stderr);
+        sg_print_command_len(cmdp, len);
+    } else {
+        if (prefix && *prefix)
+            fputs(prefix, stderr);
+        sg_print_command_len(cmdp, len);
+    }
+}
+
+static void
+lk_chk_n_print4(const char * leadin, const struct sg_io_v4 * h4p,
+                bool raw_sinfo)
+{
+    lock_guard<mutex> lk(strerr_mut);
+
+    if (h4p->usr_ptr) {
+        const cdb_arr_t * cdbp = (const cdb_arr_t *)h4p->usr_ptr;
+
+        pr2serr("Failed cdb: ");
+        sg_print_command(cdbp->data());
+    } else
+        pr2serr("cdb: <null>\n");
+    sg_linux_sense_print(leadin, h4p->device_status, h4p->transport_status,
+                         h4p->driver_status, (const uint8_t *)h4p->response,
+                         h4p->response_len, raw_sinfo);
+}
+
+static void
+hex2stderr_lk(const uint8_t * b_str, int len, int no_ascii)
+{
+    lock_guard<mutex> lk(strerr_mut);
+
+    hex2stderr(b_str, len, no_ascii);
+}
+
+static int
+system_wrapper(const char * cmd)
+{
+    int res;
+
+    res = system(cmd);
+    if (WIFSIGNALED(res) &&
+        (WTERMSIG(res) == SIGINT || WTERMSIG(res) == SIGQUIT))
+        raise(WTERMSIG(res));
+    return WEXITSTATUS(res);
+}
+
+/* Flags decoded into abbreviations for those that are set, separated by
+ * '|' . */
+static char *
+sg_flags_str(int flags, int b_len, char * b)
+{
+    int n = 0;
+
+    if ((b_len < 1) || (! b))
+        return b;
+    b[0] = '\0';
+    if (SG_FLAG_DIRECT_IO & flags) {            /* 0x1 */
+        n += sg_scnpr(b + n, b_len - n, "DIO|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_FLAG_MMAP_IO & flags) {              /* 0x4 */
+        n += sg_scnpr(b + n, b_len - n, "MMAP|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_YIELD_TAG & flags) {          /* 0x8 */
+        n += sg_scnpr(b + n, b_len - n, "YTAG|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_FLAG_Q_AT_TAIL & flags) {            /* 0x10 */
+        n += sg_scnpr(b + n, b_len - n, "QTAI|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_FLAG_Q_AT_HEAD & flags) {            /* 0x20 */
+        n += sg_scnpr(b + n, b_len - n, "QHEA|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_DOUT_OFFSET & flags) {        /* 0x40 */
+        n += sg_scnpr(b + n, b_len - n, "DOFF|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_EVENTFD & flags) {           /* 0x80 */
+        n += sg_scnpr(b + n, b_len - n, "EVFD|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_COMPLETE_B4 & flags) {        /* 0x100 */
+        n += sg_scnpr(b + n, b_len - n, "CPL_B4|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_SIGNAL & flags) {       /* 0x200 */
+        n += sg_scnpr(b + n, b_len - n, "SIGNAL|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_IMMED & flags) {              /* 0x400 */
+        n += sg_scnpr(b + n, b_len - n, "IMM|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_POLLED & flags) {             /* 0x800 */
+        n += sg_scnpr(b + n, b_len - n, "POLLED|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_STOP_IF & flags) {            /* 0x1000 */
+        n += sg_scnpr(b + n, b_len - n, "STOPIF|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_DEV_SCOPE & flags) {          /* 0x2000 */
+        n += sg_scnpr(b + n, b_len - n, "DEV_SC|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_SHARE & flags) {              /* 0x4000 */
+        n += sg_scnpr(b + n, b_len - n, "SHARE|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_DO_ON_OTHER & flags) {        /* 0x8000 */
+        n += sg_scnpr(b + n, b_len - n, "DO_OTH|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_NO_DXFER & flags) {          /* 0x10000 */
+        n += sg_scnpr(b + n, b_len - n, "NOXFER|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_KEEP_SHARE & flags) {        /* 0x20000 */
+        n += sg_scnpr(b + n, b_len - n, "KEEP_SH|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_MULTIPLE_REQS & flags) {     /* 0x40000 */
+        n += sg_scnpr(b + n, b_len - n, "MRQS|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_ORDERED_WR & flags) {        /* 0x80000 */
+        n += sg_scnpr(b + n, b_len - n, "OWR|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_REC_ORDER & flags) {         /* 0x100000 */
+        n += sg_scnpr(b + n, b_len - n, "REC_O|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_META_OUT_IF & flags) {       /* 0x200000 */
+        n += sg_scnpr(b + n, b_len - n, "MOUT_IF|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (0 == n)
+        n += sg_scnpr(b + n, b_len - n, "<none>");
+fini:
+    if (n < b_len) {    /* trim trailing '\' */
+        if ('|' == b[n - 1])
+            b[n - 1] = '\0';
+    } else if ('|' == b[b_len - 1])
+        b[b_len - 1] = '\0';
+    return b;
+}
+
+/* Info field decoded into abbreviations for those bits that are set,
+ * separated by '|' . */
+static char *
+sg_info_str(int info, int b_len, char * b)
+{
+    int n = 0;
+
+    if ((b_len < 1) || (! b))
+        return b;
+    b[0] = '\0';
+    if (SG_INFO_CHECK & info) {               /* 0x1 */
+        n += sg_scnpr(b + n, b_len - n, "CHK|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_DIRECT_IO & info) {           /* 0x2 */
+        n += sg_scnpr(b + n, b_len - n, "DIO|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_MIXED_IO & info) {            /* 0x4 */
+        n += sg_scnpr(b + n, b_len - n, "MIO|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_DEVICE_DETACHING & info) {    /* 0x8 */
+        n += sg_scnpr(b + n, b_len - n, "DETA|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_ABORTED & info) {             /* 0x10 */
+        n += sg_scnpr(b + n, b_len - n, "ABRT|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_MRQ_FINI & info) {            /* 0x20 */
+        n += sg_scnpr(b + n, b_len - n, "MRQF|");
+        if (n >= b_len)
+            goto fini;
+    }
+fini:
+    if (n < b_len) {    /* trim trailing '\' */
+        if ('|' == b[n - 1])
+            b[n - 1] = '\0';
+    } else if ('|' == b[b_len - 1])
+        b[b_len - 1] = '\0';
+    return b;
+}
+
+static void
+v4hdr_out_lk(const char * leadin, const sg_io_v4 * h4p, int id, bool chk_info)
+{
+    lock_guard<mutex> lk(strerr_mut);
+    char b[80];
+
+    if (leadin)
+        pr2serr("%s [id=%d]:\n", leadin, id);
+    if (('Q' != h4p->guard) || (0 != h4p->protocol) ||
+        (0 != h4p->subprotocol))
+        pr2serr("  <<<sg_io_v4 _NOT_ properly set>>>\n");
+    pr2serr("  pointers: cdb=%s  sense=%s  din=%p  dout=%p\n",
+            (h4p->request ? "y" : "NULL"), (h4p->response ? "y" : "NULL"),
+            (void *)h4p->din_xferp, (void *)h4p->dout_xferp);
+    pr2serr("  lengths: cdb=%u  sense=%u  din=%u  dout=%u\n",
+            h4p->request_len, h4p->max_response_len, h4p->din_xfer_len,
+             h4p->dout_xfer_len);
+    pr2serr("  flags=0x%x  request_extra{pack_id}=%d\n",
+            h4p->flags, h4p->request_extra);
+    pr2serr("  flags set: %s\n", sg_flags_str(h4p->flags, sizeof(b), b));
+    pr2serr(" %s OUT:\n", leadin);
+    pr2serr("  response_len=%d driver/transport/device_status="
+            "0x%x/0x%x/0x%x\n", h4p->response_len, h4p->driver_status,
+            h4p->transport_status, h4p->device_status);
+    pr2serr("  info=0x%x  din_resid=%u  dout_resid=%u  spare_out=%u  "
+            "dur=%u\n",
+            h4p->info, h4p->din_resid, h4p->dout_resid, h4p->spare_out,
+            h4p->duration);
+    if (chk_info && (SG_INFO_CHECK & h4p->info))
+        pr2serr("  >>>> info: %s\n", sg_info_str(h4p->info, sizeof(b), b));
+}
+
+static void
+fetch_sg_version(void)
+{
+    FILE * fp;
+    char b[96];
+
+    have_sg_version = false;
+    sg_version = 0;
+    fp = fopen(PROC_SCSI_SG_VERSION, "r");
+    if (fp && fgets(b, sizeof(b) - 1, fp)) {
+        if (1 == sscanf(b, "%d", &sg_version))
+            have_sg_version = !!sg_version;
+    } else {
+        int j, k, l;
+
+        if (fp)
+            fclose(fp);
+        fp = fopen(SYS_SCSI_SG_VERSION, "r");
+        if (fp && fgets(b, sizeof(b) - 1, fp)) {
+            if (3 == sscanf(b, "%d.%d.%d", &j, &k, &l)) {
+                sg_version = (j * 10000) + (k * 100) + l;
+                have_sg_version = !!sg_version;
+            }
+        }
+        if (NULL == fp)
+                pr2serr("The sg driver may not be loaded\n");
+    }
+    if (fp)
+        fclose(fp);
+}
+
+static void
+calc_duration_throughput(int contin)
+{
+    struct timeval end_tm, res_tm;
+    double a, b;
+
+    gettimeofday(&end_tm, NULL);
+    res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+    res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+    if (res_tm.tv_usec < 0) {
+        --res_tm.tv_sec;
+        res_tm.tv_usec += 1000000;
+    }
+    a = res_tm.tv_sec;
+    a += (0.000001 * res_tm.tv_usec);
+
+    b = 0.0;
+    for (auto && cvp : gcoll.cp_ver_arr) {
+        if (cvp.state == cp_ver_pair_t::my_state::empty)
+            break;
+        b += (double)(cvp.dd_count - cvp.out_rem_count.load());
+    }
+    b *= (double)gcoll.bs;
+    pr2serr("time to %s data %s %d.%06d secs",
+            (gcoll.verify ? "verify" : "copy"), (contin ? "so far" : "was"),
+            (int)res_tm.tv_sec, (int)res_tm.tv_usec);
+    if ((a > 0.00001) && (b > 511))
+        pr2serr(", %.2f MB/sec\n", b / (a * 1000000.0));
+    else
+        pr2serr("\n");
+}
+
+static void
+print_stats(const char * str)
+{
+    bool show_slice = ((gcoll.cp_ver_arr.size() > 1) &&
+                       (gcoll.cp_ver_arr[1].state !=
+                        cp_ver_pair_t::my_state::empty));
+    int k = 0;
+    int64_t infull, outfull;
+
+    for (auto && cvp : gcoll.cp_ver_arr) {
+        ++k;
+        if (cvp.state == cp_ver_pair_t::my_state::empty)
+            break;
+        if (cvp.state == cp_ver_pair_t::my_state::ignore) {
+            pr2serr(">>> IGNORING slice: %d\n", k);
+            continue;
+        }
+        if (show_slice)
+            pr2serr(">>> slice: %d\n", k);
+        if (0 != cvp.out_rem_count.load())
+            pr2serr("  remaining block count=%" PRId64 "\n",
+                    cvp.out_rem_count.load());
+        infull = cvp.dd_count - cvp.in_rem_count.load();
+        pr2serr("%s%" PRId64 "+%d records in\n", str,
+                infull, cvp.in_partial.load());
+
+        if (cvp.out_type == FT_DEV_NULL)
+            pr2serr("%s0+0 records out\n", str);
+        else {
+            outfull = cvp.dd_count - cvp.out_rem_count.load();
+            pr2serr("%s%" PRId64 "+%d records %s\n", str,
+                    outfull, cvp.out_partial.load(),
+                    (gcoll.verify ? "verified" : "out"));
+        }
+    }
+}
+
+static void
+interrupt_handler(int sig)
+{
+    struct sigaction sigact;
+
+    sigact.sa_handler = SIG_DFL;
+    sigemptyset(&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigaction(sig, &sigact, NULL);
+    pr2serr("Interrupted by signal,");
+    if (do_time > 0)
+        calc_duration_throughput(0);
+    print_stats("");
+    kill(getpid(), sig);
+}
+
+static void
+siginfo_handler(int sig)
+{
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    pr2serr("Progress report, continuing ...\n");
+    if (do_time > 0)
+        calc_duration_throughput(1);
+    print_stats("  ");
+}
+
+/* Usually this signal (SIGUSR2) will be caught by the timed wait in the
+ * sig_listen_thread thread but some might slip through while the timed
+ * wait is being re-armed or after that thread is finished. This handler
+ * acts as a backstop. */
+static void
+siginfo2_handler(int sig)
+{
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    ++num_fallthru_sigusr2;
+}
+
+static void
+install_handler(int sig_num, void (*sig_handler) (int sig))
+{
+    struct sigaction sigact;
+    sigaction (sig_num, NULL, &sigact);
+    if (sigact.sa_handler != SIG_IGN)
+    {
+        sigact.sa_handler = sig_handler;
+        sigemptyset (&sigact.sa_mask);
+        sigact.sa_flags = 0;
+        sigaction (sig_num, &sigact, NULL);
+    }
+}
+
+/* Make safe_strerror() thread safe */
+static char *
+tsafe_strerror(int code, char * ebp)
+{
+    lock_guard<mutex> lk(strerr_mut);
+    char * cp;
+
+    cp = safe_strerror(code);
+    strncpy(ebp, cp, STRERR_BUFF_LEN);
+    ebp[STRERR_BUFF_LEN - 1] = '\0';
+    return ebp;
+}
+
+
+static int
+dd_filetype(const char * filename, off_t & st_size)
+{
+    struct stat st;
+    size_t len = strlen(filename);
+
+    if ((1 == len) && ('.' == filename[0]))
+        return FT_DEV_NULL;
+    if (stat(filename, &st) < 0)
+        return FT_ERROR;
+    if (S_ISCHR(st.st_mode)) {
+        if ((MEM_MAJOR == major(st.st_rdev)) &&
+            ((DEV_NULL_MINOR_NUM == minor(st.st_rdev)) ||
+             (DEV_ZERO_MINOR_NUM == minor(st.st_rdev))))
+            return FT_DEV_NULL; /* treat /dev/null + /dev/zero the same */
+        if (SCSI_GENERIC_MAJOR == major(st.st_rdev))
+            return FT_SG;
+        if (SCSI_TAPE_MAJOR == major(st.st_rdev))
+            return FT_ST;
+        return FT_CHAR;
+    } else if (S_ISBLK(st.st_mode))
+        return FT_BLOCK;
+    else if (S_ISFIFO(st.st_mode))
+        return FT_FIFO;
+    st_size = st.st_size;
+    return FT_OTHER;
+}
+
+/* Returns reserved_buffer_size/mmap_size if success, else 0 for failure */
+static int
+sg_prepare_resbuf(int fd, struct global_collection *clp, bool is_in,
+                  uint8_t **mmpp)
+{
+    static bool done = false;
+    bool no_dur = is_in ? clp->in_flags.no_dur : clp->out_flags.no_dur;
+    bool masync = is_in ? clp->in_flags.masync : clp->out_flags.masync;
+    bool wq_excl = is_in ? clp->in_flags.wq_excl : clp->out_flags.wq_excl;
+    bool skip_thresh = is_in ? clp->in_flags.no_thresh :
+                               clp->out_flags.no_thresh;
+    int elem_sz = clp->elem_sz;
+    int res, t, num, err;
+    uint8_t *mmp;
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    res = ioctl(fd, SG_GET_VERSION_NUM, &t);
+    if ((res < 0) || (t < 40000)) {
+        if (ioctl(fd, SG_GET_RESERVED_SIZE, &num) < 0) {
+            perror("SG_GET_RESERVED_SIZE ioctl failed");
+            return 0;
+        }
+        if (! done) {
+            done = true;
+            pr2serr_lk("%ssg driver prior to 4.0.00, reduced functionality\n",
+                       my_name);
+        }
+        goto bypass;
+    }
+    if (elem_sz >= 4096) {
+        seip->sei_rd_mask |= SG_SEIM_SGAT_ELEM_SZ;
+        res = ioctl(fd, SG_SET_GET_EXTENDED, seip);
+        if (res < 0)
+            pr2serr_lk("sg_mrq_dd: %s: SG_SET_GET_EXTENDED(SGAT_ELEM_SZ) rd "
+                       "error: %s\n", __func__, strerror(errno));
+        if (elem_sz != (int)seip->sgat_elem_sz) {
+            seip->sei_wr_mask |= SG_SEIM_SGAT_ELEM_SZ;
+            seip->sgat_elem_sz = elem_sz;
+            res = ioctl(fd, SG_SET_GET_EXTENDED, seip);
+            if (res < 0)
+                pr2serr_lk("sg_mrq_dd: %s: SG_SET_GET_EXTENDED(SGAT_ELEM_SZ) "
+                           "wr error: %s\n", __func__, strerror(errno));
+        }
+    }
+    if (no_dur || masync || skip_thresh) {
+        seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+        if (no_dur) {
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_NO_DURATION;
+            seip->ctl_flags |= SG_CTL_FLAGM_NO_DURATION;
+        }
+        if (masync) {
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_MORE_ASYNC;
+            seip->ctl_flags |= SG_CTL_FLAGM_MORE_ASYNC;
+        }
+        if (wq_excl) {
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_EXCL_WAITQ;
+            seip->ctl_flags |= SG_CTL_FLAGM_EXCL_WAITQ;
+        }
+        if (skip_thresh) {
+            seip->tot_fd_thresh = 0;
+            sei.sei_wr_mask |= SG_SEIM_TOT_FD_THRESH;
+        }
+        res = ioctl(fd, SG_SET_GET_EXTENDED, seip);
+        if (res < 0)
+            pr2serr_lk("sg_mrq_dd: %s: SG_SET_GET_EXTENDED(NO_DURATION) "
+                       "error: %s\n", __func__, strerror(errno));
+    }
+bypass:
+    num = clp->bs * clp->bpt;
+    res = ioctl(fd, SG_SET_RESERVED_SIZE, &num);
+    if (res < 0) {
+        perror("sg_mrq_dd: SG_SET_RESERVED_SIZE error");
+        return 0;
+    } else {
+        int nn;
+
+        res = ioctl(fd, SG_GET_RESERVED_SIZE, &nn);
+        if (res < 0) {
+            perror("sg_mrq_dd: SG_GET_RESERVED_SIZE error");
+            return 0;
+        }
+        if (nn < num) {
+            pr2serr_lk("%s: SG_GET_RESERVED_SIZE shows size truncated, "
+                       "wanted %d got %d\n", __func__, num, nn);
+            return 0;
+        }
+        if (mmpp) {
+            mmp = (uint8_t *)mmap(NULL, num, PROT_READ | PROT_WRITE,
+                                  MAP_SHARED, fd, 0);
+            if (MAP_FAILED == mmp) {
+                err = errno;
+                pr2serr_lk("sg_mrq_dd: %s: sz=%d, fd=%d, mmap() failed: %s\n",
+                           __func__, num, fd, strerror(err));
+                return 0;
+            }
+            *mmpp = mmp;
+        }
+    }
+    t = 1;
+    res = ioctl(fd, SG_SET_FORCE_PACK_ID, &t);
+    if (res < 0)
+        perror("sg_mrq_dd: SG_SET_FORCE_PACK_ID error");
+    if (clp->unit_nanosec) {
+        seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+        seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+        seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+        if (ioctl(fd, SG_SET_GET_EXTENDED, seip) < 0) {
+            res = -1;
+            pr2serr_lk("ioctl(EXTENDED(TIME_IN_NS)) failed, errno=%d %s\n",
+                       errno, strerror(errno));
+        }
+    }
+    if (clp->verbose) {
+        t = 1;
+        /* more info in the kernel log */
+        res = ioctl(fd, SG_SET_DEBUG, &t);
+        if (res < 0)
+            perror("sg_mrq_dd: SG_SET_DEBUG error");
+    }
+    return (res < 0) ? 0 : num;
+}
+
+static int
+sg_in_open(struct global_collection *clp, const string & inf, uint8_t **mmpp,
+           int * mmap_lenp)
+{
+    int fd, err, n;
+    int flags = O_RDWR;
+    char ebuff[EBUFF_SZ];
+    const char * fnp = inf.c_str();
+
+    if (clp->in_flags.direct)
+        flags |= O_DIRECT;
+    if (clp->in_flags.excl)
+        flags |= O_EXCL;
+    if (clp->in_flags.dsync)
+        flags |= O_SYNC;
+
+    if ((fd = open(fnp, flags)) < 0) {
+        err = errno;
+        snprintf(ebuff, EBUFF_SZ, "%s: could not open %s for sg reading",
+                 __func__, fnp);
+        perror(ebuff);
+        return -sg_convert_errno(err);
+    }
+    n = sg_prepare_resbuf(fd, clp, true, mmpp);
+    if (n <= 0) {
+        close(fd);
+        return -SG_LIB_FILE_ERROR;
+    }
+    if (mmap_lenp)
+        *mmap_lenp = n;
+    return fd;
+}
+
+static int
+sg_out_open(struct global_collection *clp, const string & outf,
+            uint8_t **mmpp, int * mmap_lenp)
+{
+    int fd, err, n;
+    int flags = O_RDWR;
+    char ebuff[EBUFF_SZ];
+    const char * fnp = outf.c_str();
+
+    if (clp->out_flags.direct)
+        flags |= O_DIRECT;
+    if (clp->out_flags.excl)
+        flags |= O_EXCL;
+    if (clp->out_flags.dsync)
+        flags |= O_SYNC;
+
+    if ((fd = open(fnp, flags)) < 0) {
+        err = errno;
+        snprintf(ebuff,  EBUFF_SZ, "%s: could not open %s for sg %s",
+                 __func__, fnp, (clp->verify ? "verifying" : "writing"));
+        perror(ebuff);
+        return -sg_convert_errno(err);
+    }
+    n = sg_prepare_resbuf(fd, clp, false, mmpp);
+    if (n <= 0) {
+        close(fd);
+        return -SG_LIB_FILE_ERROR;
+    }
+    if (mmap_lenp)
+        *mmap_lenp = n;
+    return fd;
+}
+
+static int
+reg_file_open(struct global_collection *clp, const string & fn_s,
+              bool for_wr)
+{
+    int fd, flags;
+    char ebuff[EBUFF_SZ];
+
+    if (for_wr) {
+        flags = O_WRONLY;
+        if (! clp->out_flags.nocreat)
+            flags |= O_CREAT;
+        if (clp->out_flags.append)
+            flags |= O_APPEND;
+    } else
+        flags = O_RDONLY;
+    if (clp->in_flags.direct)
+        flags |= O_DIRECT;
+    if (clp->in_flags.excl)
+        flags |= O_EXCL;
+    if (clp->in_flags.dsync)
+        flags |= O_SYNC;
+
+    if (for_wr)
+        fd = open(fn_s.c_str(), flags, 0666);
+    else
+        fd = open(fn_s.c_str(), flags);
+    if (fd < 0) {
+        int err = errno;
+        snprintf(ebuff, EBUFF_SZ, "%scould not open %s for %sing ",
+                 my_name, fn_s.c_str(), (for_wr ? "writ" : "read"));
+        perror(ebuff);
+        return -err;
+    }
+    return fd;
+}
+
+get_next_res_t
+cp_ver_pair_t::get_next(int desired_num_blks)
+{
+    int64_t expected, desired;
+
+    if (desired_num_blks <= 0) {
+        if (desired_num_blks < 0) {
+            if (next_count_pos.load() >= 0)     /* flag error detection */
+                next_count_pos.store(desired_num_blks);
+        }
+        return make_pair(next_count_pos.load(), 0);
+    }
+
+    expected = next_count_pos.load();
+    do {        /* allowed to race with other threads */
+        if (expected < 0)
+            return make_pair(0, (int)expected);
+        else if (expected >= dd_count)
+            return make_pair(expected, 0);      /* clean finish */
+        desired = expected + desired_num_blks;
+        if (desired > dd_count)
+            desired = dd_count;
+    } while (! next_count_pos.compare_exchange_strong(expected, desired));
+    return make_pair(expected, desired - expected);
+}
+
+/* Return of 0 -> success, see sg_ll_read_capacity*() otherwise */
+static int
+scsi_read_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+    int res;
+    uint8_t rcBuff[RCAP16_REPLY_LEN] = {};
+
+    res = sg_ll_readcap_10(sg_fd, 0, 0, rcBuff, READ_CAP_REPLY_LEN, false, 0);
+    if (0 != res)
+        goto bad;
+
+    if ((0xff == rcBuff[0]) && (0xff == rcBuff[1]) && (0xff == rcBuff[2]) &&
+        (0xff == rcBuff[3])) {
+
+        res = sg_ll_readcap_16(sg_fd, 0, 0, rcBuff, RCAP16_REPLY_LEN, false,
+                               0);
+        if (0 != res)
+            goto bad;
+        *num_sect = sg_get_unaligned_be64(rcBuff + 0) + 1;
+        *sect_sz = sg_get_unaligned_be32(rcBuff + 8);
+    } else {
+        /* take care not to sign extend values > 0x7fffffff */
+        *num_sect = (int64_t)sg_get_unaligned_be32(rcBuff + 0) + 1;
+        *sect_sz = sg_get_unaligned_be32(rcBuff + 4);
+    }
+    return 0;
+bad:
+    *num_sect = 0;
+    *sect_sz = 0;
+    return res;
+}
+
+/* Return of 0 -> success, -1 -> failure. BLKGETSIZE64, BLKGETSIZE and */
+/* BLKSSZGET macros problematic (from <linux/fs.h> or <sys/mount.h>). */
+static int
+read_blkdev_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+#ifdef BLKSSZGET
+    if ((ioctl(sg_fd, BLKSSZGET, sect_sz) < 0) && (*sect_sz > 0)) {
+        perror("BLKSSZGET ioctl error");
+        return -1;
+    } else {
+ #ifdef BLKGETSIZE64
+        uint64_t ull;
+
+        if (ioctl(sg_fd, BLKGETSIZE64, &ull) < 0) {
+
+            perror("BLKGETSIZE64 ioctl error");
+            return -1;
+        }
+        *num_sect = ((int64_t)ull / (int64_t)*sect_sz);
+ #else
+        unsigned long ul;
+
+        if (ioctl(sg_fd, BLKGETSIZE, &ul) < 0) {
+            perror("BLKGETSIZE ioctl error");
+            return -1;
+        }
+        *num_sect = (int64_t)ul;
+ #endif
+    }
+    return 0;
+#else
+    *num_sect = 0;
+    *sect_sz = 0;
+    return -1;
+#endif
+}
+
+static void
+flag_all_stop(struct global_collection * clp)
+{
+    for (auto && elem : clp->cp_ver_arr) {
+        if (elem.state == cp_ver_pair_t::my_state::empty)
+            break;
+        elem.next_count_pos.store(-1);
+    }
+}
+
+/* Has an infinite loop doing a timed wait for any signals in signal_set.
+ * After each timeout (300 ms) checks if the most_recent_pack_id atomic
+ * integer has changed. If not after another two timeouts announces a stall
+ * has been detected. If shutting down atomic is true breaks out of loop and
+ * shuts down this thread. Other than that, this thread is normally cancelled
+ * by the main thread, after other threads have exited. */
+static void
+sig_listen_thread(struct global_collection * clp)
+{
+    bool stall_reported = false;
+    int prev_pack_id = 0;
+    int sig_number, pack_id;
+    uint32_t ict_ms = (clp->sdt_ict ? clp->sdt_ict : DEF_SDT_ICT_MS);
+    struct timespec ts;
+    struct timespec * tsp = &ts;
+
+    tsp->tv_sec = ict_ms / 1000;
+    tsp->tv_nsec = (ict_ms % 1000) * 1000 * 1000;   /* DEF_SDT_ICT_MS */
+    listen_t_tid = gettid();    // to facilitate sending SIGUSR2 to exit
+    while (1) {
+        sig_number = sigtimedwait(&signal_set, NULL, tsp);
+        if (sig_number < 0) {
+            int err = errno;
+
+            /* EAGAIN implies a timeout */
+            if ((EAGAIN == err) && (clp->sdt_crt > 0)) {
+                pack_id = clp->most_recent_pack_id.load();
+                if ((pack_id > 0) && (pack_id == prev_pack_id)) {
+                    if (! stall_reported) {
+                        stall_reported = true;
+                        tsp->tv_sec = clp->sdt_crt;
+                        tsp->tv_nsec = 0;
+                        pr2serr_lk("%s: first stall at pack_id=%d detected\n",
+                                   __func__, pack_id);
+                    } else
+                        pr2serr_lk("%s: subsequent stall at pack_id=%d\n",
+                                   __func__, pack_id);
+                    // following command assumes linux bash or similar shell
+                    system_wrapper("cat /proc/scsi/sg/debug >> /dev/stderr\n");
+                    // system_wrapper("/usr/bin/dmesg\n");
+                } else
+                    prev_pack_id = pack_id;
+            } else if (EAGAIN != err)
+                pr2serr_lk("%s: sigtimedwait() errno=%d\n", __func__, err);
+        }
+        if (SIGINT == sig_number) {
+            pr2serr_lk("%sinterrupted by SIGINT\n", my_name);
+            flag_all_stop(clp);
+            shutting_down.store(true);
+            sigprocmask(SIG_SETMASK, &orig_signal_set, NULL);
+            raise(SIGINT);
+            break;
+        }
+        if (SIGUSR2 == sig_number) {
+            if (clp->verbose > 2)
+                pr2serr_lk("%s: SIGUSR2 received\n", __func__);
+            break;
+        } if (shutting_down)
+            break;
+    }           /* end of while loop */
+    if (clp->verbose > 3)
+        pr2serr_lk("%s: exiting\n", __func__);
+}
+
+static bool
+sg_share_prepare(int write_side_fd, int read_side_fd, int id, bool vb_b)
+{
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    seip->sei_wr_mask |= SG_SEIM_SHARE_FD;
+    seip->sei_rd_mask |= SG_SEIM_SHARE_FD;
+    seip->share_fd = read_side_fd;
+    if (ioctl(write_side_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr_lk("tid=%d: ioctl(EXTENDED(shared_fd=%d), failed "
+                   "errno=%d %s\n", id, read_side_fd, errno,
+                   strerror(errno));
+        return false;
+    }
+    if (vb_b)
+        pr2serr_lk("%s: tid=%d: ioctl(EXTENDED(shared_fd)) ok, "
+                   "read_side_fd=%d, write_side_fd=%d\n", __func__, id,
+                   read_side_fd, write_side_fd);
+    return true;
+}
+
+static void
+sg_take_snap(int sg_fd, int id, bool vb_b)
+{
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+    seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS;
+    seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_SNAP_DEV;
+    seip->ctl_flags &= ~SG_CTL_FLAGM_SNAP_DEV;   /* 0 --> append */
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr_lk("tid=%d: ioctl(EXTENDED(SNAP_DEV), failed errno=%d %s\n",
+                   id,  errno, strerror(errno));
+        return;
+    }
+    if (vb_b)
+        pr2serr_lk("tid=%d: ioctl(SNAP_DEV) ok\n", id);
+}
+
+// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+/* Each thread's "main" function */
+static void
+read_write_thread(struct global_collection * clp, int thr_idx, int slice_idx,
+                  bool singleton)
+{
+    Rq_elem rel {};
+    Rq_elem * rep = &rel;
+    int n, sz, fd, vb, err, seg_blks;
+    int res = 0;
+    int num_sg = 0;
+    bool own_infd = false;
+    bool in_is_sg, in_mmap, out_is_sg, out_mmap;
+    bool own_outfd = false;
+    bool only_one_sg = false;
+    struct cp_ver_pair_t & cvp = clp->cp_ver_arr[slice_idx];
+    class scat_gath_iter i_sg_it(clp->i_sgl);
+    class scat_gath_iter o_sg_it(clp->o_sgl);
+    const string & inf = clp->inf_v[slice_idx];
+    const string & outf = clp->outf_v[slice_idx];
+    vector<cdb_arr_t> a_cdb;
+    vector<struct sg_io_v4> a_v4;
+
+    vb = clp->verbose;
+    sz = clp->mrq_num * clp->bpt * clp->bs;
+    in_is_sg = (FT_SG == clp->in_type);
+    in_mmap = (in_is_sg && (clp->in_flags.mmap > 0));
+    out_is_sg = (FT_SG == clp->out_type);
+    out_mmap = (out_is_sg && (clp->out_flags.mmap > 0));
+    rep->clp = clp;
+    rep->id = thr_idx;
+    rep->bs = clp->bs;
+
+    if (in_is_sg && out_is_sg)
+        rep->both_sg = true;
+    else if (in_is_sg || out_is_sg) {
+        only_one_sg = true;
+        if (in_is_sg)
+            rep->only_in_sg = true;
+        else
+            rep->only_out_sg = true;
+    }
+
+    if (vb > 2) {
+        pr2serr_lk("%d <-- Starting worker thread, slice=%d\n", thr_idx,
+                   slice_idx);
+        if (vb > 3)
+            pr2serr_lk("   %s ---> %s\n", inf.c_str(), outf.c_str());
+    }
+    if (! (rep->both_sg || in_mmap)) {
+        rep->buffp = sg_memalign(sz, 0 /* page align */, &rep->alloc_bp,
+                                 false);
+        if (NULL == rep->buffp) {
+            pr2serr_lk("Failed to allocate %d bytes, exiting\n", sz);
+            return;
+        }
+    }
+    rep->infd = clp->in0fd;
+    rep->outfd = clp->out0fd;
+    rep->outregfd = clp->outregfd;
+    rep->rep_count = 0;
+    rep->in_follow_on = -1;
+    rep->out_follow_on = -1;
+    if (cvp.state == cp_ver_pair_t::my_state::init)
+        cvp.state = cp_ver_pair_t::my_state::underway;
+    if (FT_OTHER == cvp.in_type) {
+        fd = reg_file_open(clp, inf, false);
+        if (fd < 0) {
+            pr2serr_lk("[%d]: unable to open IFILE of slice=%d\n", thr_idx,
+                       slice_idx);
+            return;
+        }
+        rep->infd = fd;
+    }
+    if (FT_OTHER == cvp.out_type) {
+        fd = reg_file_open(clp, outf, true);
+        if (fd < 0) {
+            pr2serr_lk("[%d]: unable to open OFILE of slice=%d\n", thr_idx,
+                       slice_idx);
+            return;
+        }
+        rep->outfd = fd;
+    }
+
+    if (rep->infd == rep->outfd) {
+        if (in_is_sg)
+            rep->same_sg = true;
+    }
+    if (clp->in_flags.random) {
+#ifdef HAVE_GETRANDOM
+        ssize_t ssz = getrandom(&rep->seed, sizeof(rep->seed), GRND_NONBLOCK);
+
+        if (ssz < (ssize_t)sizeof(rep->seed)) {
+            pr2serr_lk("[%d] %s: getrandom() failed, ret=%d\n", thr_idx,
+                       __func__, (int)ssz);
+            rep->seed = (long)time(NULL);
+        }
+#else
+        rep->seed = (long)time(NULL);    /* use seconds since epoch as proxy */
+#endif
+        if (vb > 1)
+            pr2serr_lk("[%d] %s: seed=%ld\n", thr_idx, __func__, rep->seed);
+#ifdef HAVE_SRAND48_R
+        srand48_r(rep->seed, &rep->drand);
+#else
+        srand48(rep->seed);
+#endif
+    }
+
+    if (in_is_sg && inf.size()) {
+        if ((clp->in_flags.same_fds || (0 == thr_idx)) &&
+            (cvp.in_fd >= 0))
+            fd = cvp.in_fd;
+        else {
+            fd = sg_in_open(clp, inf, (in_mmap ? &rep->buffp : NULL),
+                            (in_mmap ? &rep->mmap_len : NULL));
+            if (fd < 0)
+                goto fini;
+            own_infd = true;
+            if (cvp.in_fd < 0)
+                cvp.in_fd = fd;
+        }
+        rep->infd = fd;
+        rep->mmap_active = in_mmap ? clp->in_flags.mmap : 0;
+        if (in_mmap && (vb > 4))
+            pr2serr_lk("[%d] %s: mmap buffp=%p\n", thr_idx, __func__,
+                       rep->buffp);
+        ++num_sg;
+        if (vb > 2)
+            pr2serr_lk("[%d]: opened local sg IFILE\n", thr_idx);
+    }
+    if (out_is_sg && outf.size()) {
+        if ((clp->out_flags.same_fds || (0 == thr_idx)) &&
+            (cvp.out_fd >= 0))
+            fd = cvp.out_fd;
+        else {
+            fd = sg_out_open(clp, outf, (out_mmap ? &rep->buffp : NULL),
+                             (out_mmap ? &rep->mmap_len : NULL));
+            if (fd < 0)
+                goto fini;
+            own_outfd = true;
+            if (cvp.out_fd < 0)
+                cvp.out_fd = fd;
+        }
+        rep->outfd = fd;
+        if (! rep->mmap_active)
+            rep->mmap_active = out_mmap ? clp->out_flags.mmap : 0;
+        if (out_mmap && (vb > 4))
+            pr2serr_lk("[%d]: mmap buffp=%p\n", thr_idx, rep->buffp);
+        ++num_sg;
+        if (vb > 2)
+            pr2serr_lk("[%d]: opened local sg OFILE\n", thr_idx);
+    }
+    if (vb > 2) {
+        if (in_is_sg && (! own_infd))
+            pr2serr_lk("[%d]: using global sg IFILE, fd=%d\n", thr_idx,
+                       rep->infd);
+        if (out_is_sg && (! own_outfd))
+            pr2serr_lk("[%d]: using global sg OFILE, fd=%d\n", thr_idx,
+                       rep->outfd);
+    }
+    if (rep->both_sg)
+        rep->has_share = sg_share_prepare(rep->outfd, rep->infd, thr_idx,
+                                          vb > 9);
+    if (vb > 9)
+        pr2serr_lk("[%d]: has_share=%s\n", thr_idx,
+                   (rep->has_share ? "true" : "false"));
+    // share_and_ofreg = (rep->has_share && (rep->outregfd >= 0));
+
+    /* vvvvvvvvvvvvvv  Main segment copy loop  vvvvvvvvvvvvvvvvvvvvvvv */
+    while (! shutting_down) {
+        get_next_res_t gnr = cvp.get_next(clp->mrq_num * clp->bpt);
+
+        seg_blks = gnr.second;
+        if (seg_blks <= 0) {
+            if (seg_blks < 0)
+                res = -seg_blks;
+            else
+                cvp.state = cp_ver_pair_t::my_state::finished;
+            break;
+        }
+        if (! i_sg_it.set_by_blk_idx(gnr.first)) {
+            lock_guard<mutex> lk(strerr_mut);
+
+            pr2serr_lk("[%d]: input set_by_blk_idx() failed\n", thr_idx);
+            i_sg_it.dbg_print("input after set_by_blk_idx", false, vb > 5);
+            res = 2;
+            break;
+        }
+        if (! o_sg_it.set_by_blk_idx(gnr.first)) {
+            pr2serr_lk("[%d]: output set_by_blk_idx() failed\n", thr_idx);
+            res = 3;
+            break;
+        }
+        if (rep->both_sg) {
+            uint32_t nn = (2 * clp->mrq_num) + 4;
+
+            if (a_cdb.capacity() < nn)
+                a_cdb.reserve(nn);
+            if (a_v4.capacity() < nn)
+                a_v4.reserve(nn);
+            if (clp->mrq_eq_0)
+                res = do_both_sg_segment_mrq0(rep, i_sg_it, o_sg_it,
+                                              seg_blks);
+            else
+                res = do_both_sg_segment(rep, i_sg_it, o_sg_it, seg_blks,
+                                         a_cdb, a_v4);
+            if (res < 0)
+                break;
+        } else if (only_one_sg) {
+            uint32_t nn = clp->mrq_num + 4;
+
+            if (a_cdb.capacity() < nn)
+                a_cdb.reserve(nn);
+            if (a_v4.capacity() < nn)
+                a_v4.reserve(nn);
+            res = do_normal_sg_segment(rep, i_sg_it, o_sg_it, seg_blks, a_cdb,
+                                       a_v4);
+            if (res < 0)
+                break;
+        } else {
+            res = do_normal_normal_segment(rep, i_sg_it, o_sg_it, seg_blks);
+            if (res < 0)
+                break;
+        }
+        if (singleton) {
+            {
+                lock_guard<mutex> lk(clp->infant_mut);
+
+                clp->processed = true;
+            }   /* this unlocks lk */
+            clp->infant_cv.notify_one();
+            singleton = false;
+        }
+        if (rep->stop_after_write || rep->stop_now) {
+            shutting_down = true;
+            break;
+        }
+    }   /* ^^^^^^^^^^ end of main while loop which copies segments ^^^^^^ */
+
+    if (shutting_down) {
+        if (vb > 3)
+            pr2serr_lk("%s: t=%d: shutting down\n", __func__, rep->id);
+        goto fini;
+    }
+    if (singleton) {
+        {
+            lock_guard<mutex> lk(clp->infant_mut);
+
+            clp->processed = true;
+        }   /* this unlocks lk */
+        clp->infant_cv.notify_one();
+    }
+    if (res < 0) {
+        if (seg_blks >= 0)
+            cvp.get_next(-1);  /* flag error to main */
+        pr2serr_lk("%s: t=%d: aborting, res=%d\n", __func__, rep->id, res);
+    }
+
+fini:
+
+    if ((1 == rep->mmap_active) && (rep->mmap_len > 0)) {
+        if (munmap(rep->buffp, rep->mmap_len) < 0) {
+            err = errno;
+            char bb[64];
+
+            pr2serr_lk("thread=%d: munmap() failed: %s\n", rep->id,
+                       tsafe_strerror(err, bb));
+        }
+        if (vb > 4)
+            pr2serr_lk("thread=%d: munmap(%p, %d)\n", rep->id, rep->buffp,
+                       rep->mmap_len);
+        rep->mmap_active = 0;
+    }
+
+    if (own_infd && (rep->infd >= 0)) {
+        if (vb && in_is_sg) {
+            if (ioctl(rep->infd, SG_GET_NUM_WAITING, &n) >= 0) {
+                if (n > 0)
+                    pr2serr_lk("%s: tid=%d: num_waiting=%d prior close(in)\n",
+                               __func__, rep->id, n);
+            } else {
+                err = errno;
+                pr2serr_lk("%s: [%d] ioctl(SG_GET_NUM_WAITING) errno=%d: "
+                           "%s\n", __func__, rep->id, err, strerror(err));
+            }
+        }
+        close(rep->infd);
+    }
+    if (own_outfd && (rep->outfd >= 0)) {
+        if (vb && out_is_sg) {
+            if (ioctl(rep->outfd, SG_GET_NUM_WAITING, &n) >= 0) {
+                if (n > 0)
+                    pr2serr_lk("%s: tid=%d: num_waiting=%d prior "
+                               "close(out)\n", __func__, rep->id, n);
+            } else {
+                err = errno;
+                pr2serr_lk("%s: [%d] ioctl(SG_GET_NUM_WAITING) errno=%d: "
+                           "%s\n", __func__, rep->id, err, strerror(err));
+            }
+        }
+        close(rep->outfd);
+    }
+    /* pass stats back to read-side */
+    if (vb > 3)
+        pr2serr_lk("%s: [%d] leaving: in/out local count=%" PRId64 "/%"
+                   PRId64 "\n", __func__, rep->id, rep->in_local_count,
+                   rep->out_local_count);
+    cvp.in_rem_count -= rep->in_local_count;
+    cvp.out_rem_count -= rep->out_local_count;
+    cvp.in_partial += rep->in_local_partial;
+    cvp.out_partial += rep->out_local_partial;
+    cvp.sum_of_resids += rep->in_resid_bytes;
+    if (rep->alloc_bp)
+        free(rep->alloc_bp);
+}
+
+/* N.B. Returns 'blocks' is successful, lesser positive number if there was
+ * a short read, or an error code which is negative. */
+static int
+normal_in_rd(Rq_elem * rep, int64_t lba, int blocks, int d_boff)
+{
+    struct global_collection * clp = rep->clp;
+    int res, err;
+    int id = rep->id;
+    uint8_t * bp;
+    char strerr_buff[STRERR_BUFF_LEN];
+
+    if (clp->verbose > 4)
+        pr2serr_lk("[%d] %s: lba=%" PRIu64 ", blocks=%d, d_boff=%d\n", id,
+                   __func__, lba, blocks, d_boff);
+    if (FT_RANDOM_0_FF == clp->in_type) {
+        int k, j;
+        const int jbump = sizeof(uint32_t);
+        long rn;
+        uint8_t * bp;
+
+        if (clp->in_flags.zero && clp->in_flags.ff && (rep->bs >= 4)) {
+            uint32_t pos = (uint32_t)lba;
+            uint32_t off;
+
+            for (k = 0, off = 0; k < blocks; ++k, off += rep->bs, ++pos) {
+                for (j = 0; j < (rep->bs - 3); j += 4)
+                    sg_put_unaligned_be32(pos, rep->buffp + off + j);
+            }
+        } else if (clp->in_flags.zero)
+            memset(rep->buffp + d_boff, 0, blocks * rep->bs);
+        else if (clp->in_flags.ff)
+            memset(rep->buffp + d_boff, 0xff, blocks * rep->bs);
+        else {
+            bp = rep->buffp + d_boff;
+            for (k = 0; k < blocks; ++k, bp += rep->bs) {
+                for (j = 0; j < rep->bs; j += jbump) {
+                    /* mrand48 takes uniformly from [-2^31, 2^31) */
+#ifdef HAVE_SRAND48_R
+                    mrand48_r(&rep->drand, &rn);
+#else
+                    rn = mrand48();
+#endif
+                    *((uint32_t *)(bp + j)) = (uint32_t)rn;
+                }
+            }
+        }
+        return blocks;
+    }
+
+    if (clp->in_type != FT_FIFO) {
+        int64_t pos = lba * rep->bs;
+
+        if (rep->in_follow_on != pos) {
+            if (lseek64(rep->infd, pos, SEEK_SET) < 0) {
+                err = errno;
+                pr2serr_lk("[%d] %s: >> lseek64(%" PRId64 "): %s\n", id,
+                           __func__, pos, safe_strerror(err));
+                return -err;
+            }
+            rep->in_follow_on = pos;
+        }
+    }
+    bp = rep->buffp + d_boff;
+    while (((res = read(rep->infd, bp, blocks * rep->bs)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno)))
+        std::this_thread::yield();/* another thread may be able to progress */
+    if (res < 0) {
+        err = errno;
+        if (clp->in_flags.coe) {
+            memset(bp, 0, blocks * rep->bs);
+            pr2serr_lk("[%d] %s : >> substituted zeros for in blk=%" PRId64
+                      " for %d bytes, %s\n", id, __func__, lba,
+                       blocks * rep->bs,
+                       tsafe_strerror(err, strerr_buff));
+            res = blocks * rep->bs;
+        } else {
+            pr2serr_lk("[%d] %s: error in normal read, %s\n", id, __func__,
+                       tsafe_strerror(err, strerr_buff));
+            return -err;
+        }
+    }
+    rep->in_follow_on += res;
+    if (res < blocks * rep->bs) {
+        blocks = res / rep->bs;
+        if ((res % rep->bs) > 0) {
+            rep->in_local_partial++;
+            rep->in_resid_bytes = res % rep->bs;
+        }
+    }
+    return blocks;
+}
+
+/* N.B. Returns 'blocks' is successful, lesser positive number if there was
+ * a short write, or an error code which is negative. */
+static int
+normal_out_wr(Rq_elem * rep, int64_t lba, int blocks, int d_boff)
+{
+    int res, err;
+    int id = rep->id;
+    struct global_collection * clp = rep->clp;
+    uint8_t * bp = rep->buffp + d_boff;
+    char strerr_buff[STRERR_BUFF_LEN];
+
+    if (clp->verbose > 4)
+        pr2serr_lk("[%d] %s: lba=%" PRIu64 ", blocks=%d, d_boff=%d\n", id,
+                    __func__, lba, blocks, d_boff);
+
+    if (clp->in_type != FT_FIFO) {
+        int64_t pos = lba * rep->bs;
+
+        if (rep->out_follow_on != pos) {
+            if (lseek64(rep->outfd, pos, SEEK_SET) < 0) {
+                err = errno;
+                pr2serr_lk("[%d] %s: >> lseek64(%" PRId64 "): %s\n", id,
+                           __func__, pos, safe_strerror(err));
+                return -err;
+            }
+            rep->out_follow_on = pos;
+        }
+    }
+    while (((res = write(rep->outfd, bp, blocks * rep->bs))
+            < 0) && ((EINTR == errno) || (EAGAIN == errno)))
+        std::this_thread::yield();/* another thread may be able to progress */
+    if (res < 0) {
+        err = errno;
+        if (clp->out_flags.coe) {
+            pr2serr_lk("[%d] %s: >> ignored error for out lba=%" PRId64
+                       " for %d bytes, %s\n", id, __func__, lba,
+                       blocks * rep->bs, tsafe_strerror(err, strerr_buff));
+            res = blocks * rep->bs;
+        }
+        else {
+            pr2serr_lk("[%d] %s: error normal write, %s\n", id, __func__,
+                       tsafe_strerror(err, strerr_buff));
+            return -err;
+        }
+    }
+    rep->out_follow_on += res;
+    if (res < blocks * rep->bs) {
+        blocks = res / rep->bs;
+        if ((res % rep->bs) > 0) {
+            blocks++;
+            rep->out_local_partial++;
+        }
+    }
+    return blocks;
+}
+
+static int
+extra_out_wr(Rq_elem * rep, int num_bytes, int d_boff)
+{
+    int res, err;
+    int id = rep->id;
+    struct global_collection * clp = rep->clp;
+    uint8_t * bp = rep->buffp + d_boff;
+    char strerr_buff[STRERR_BUFF_LEN];
+
+    if (clp->verbose > 4)
+        pr2serr_lk("[%d] %s: num_bytes=%d, d_boff=%d\n", id, __func__,
+                   num_bytes, d_boff);
+
+    while (((res = write(clp->out0fd, bp, num_bytes))
+            < 0) && ((EINTR == errno) || (EAGAIN == errno)))
+        std::this_thread::yield();/* another thread may be able to progress */
+    if (res < 0) {
+        err = errno;
+        pr2serr_lk("[%d] %s: error normal write, %s\n", id, __func__,
+                   tsafe_strerror(err, strerr_buff));
+        return -err;
+    }
+    if (res > 0)
+        rep->out_local_partial++;
+    return res;
+}
+
+static int
+sg_build_scsi_cdb(uint8_t * cdbp, int cdb_sz, unsigned int blocks,
+                  int64_t start_block, bool ver_true, bool write_true,
+                  bool fua, bool dpo, int cdl)
+{
+    bool normal_rw = true;
+    int rd_opcode[] = {0x8, 0x28, 0xa8, 0x88};
+    int ve_opcode[] = {0xff /* no VER(6) */, 0x2f, 0xaf, 0x8f};
+    int wr_opcode[] = {0xa, 0x2a, 0xaa, 0x8a};
+    int sz_ind;
+
+    memset(cdbp, 0, cdb_sz);
+    if (ver_true) {     /* only support VERIFY(10) */
+        if (cdb_sz < 10) {
+            pr2serr_lk("%s only support VERIFY(10)\n", my_name);
+            return 1;
+        }
+        cdb_sz = 10;
+        fua = false;
+        cdbp[1] |= 0x2; /* BYTCHK=1 --> sending dout for comparison */
+        cdbp[0] = ve_opcode[1];
+        normal_rw = false;
+    }
+    if (dpo)
+        cdbp[1] |= 0x10;
+    if (fua)
+        cdbp[1] |= 0x8;
+    switch (cdb_sz) {
+    case 6:
+        sz_ind = 0;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                         rd_opcode[sz_ind]);
+        sg_put_unaligned_be24(0x1fffff & start_block, cdbp + 1);
+        cdbp[4] = (256 == blocks) ? 0 : (uint8_t)blocks;
+        if (blocks > 256) {
+            pr2serr_lk("%sfor 6 byte commands, maximum number of blocks is "
+                       "256\n", my_name);
+            return 1;
+        }
+        if ((start_block + blocks - 1) & (~0x1fffff)) {
+            pr2serr_lk("%sfor 6 byte commands, can't address blocks beyond "
+                       "%d\n", my_name, 0x1fffff);
+            return 1;
+        }
+        if (dpo || fua) {
+            pr2serr_lk("%sfor 6 byte commands, neither dpo nor fua bits "
+                       "supported\n", my_name);
+            return 1;
+        }
+        break;
+    case 10:
+        if (! ver_true) {
+            sz_ind = 1;
+            cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                             rd_opcode[sz_ind]);
+        }
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be16((uint16_t)blocks, cdbp + 7);
+        if (blocks & (~0xffff)) {
+            pr2serr_lk("%sfor 10 byte commands, maximum number of blocks is "
+                       "%d\n", my_name, 0xffff);
+            return 1;
+        }
+        break;
+    case 12:
+        sz_ind = 2;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                         rd_opcode[sz_ind]);
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 6);
+        break;
+    case 16:
+        sz_ind = 3;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                         rd_opcode[sz_ind]);
+        sg_put_unaligned_be64((uint64_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 10);
+        if (normal_rw && (cdl > 0)) {
+            if (cdl & 0x4)
+                cdbp[1] |= 0x1;
+            if (cdl & 0x3)
+                cdbp[14] |= ((cdl & 0x3) << 6);
+        }
+        break;
+    default:
+        pr2serr_lk("%sexpected cdb size of 6, 10, 12, or 16 but got %d\n",
+                   my_name, cdb_sz);
+        return 1;
+    }
+    return 0;
+}
+
+static int
+process_mrq_response(Rq_elem * rep, const struct sg_io_v4 * ctl_v4p,
+                     const struct sg_io_v4 * a_v4p, int num_mrq,
+                     uint32_t & good_inblks, uint32_t & good_outblks,
+                     bool & last_err_on_in)
+{
+    struct global_collection * clp = rep->clp;
+    bool ok, all_good;
+    bool sb_in_co = !!(ctl_v4p->response);
+    int id = rep->id;
+    int resid = ctl_v4p->din_resid;
+    int sres = ctl_v4p->spare_out;
+    int n_subm = num_mrq - ctl_v4p->dout_resid;
+    int n_cmpl = ctl_v4p->info;
+    int n_good = 0;
+    int hole_count = 0;
+    int cat = 0;
+    int vb = clp->verbose;
+    int k, j, f1, slen;
+    char b[160];
+
+    good_inblks = 0;
+    good_outblks = 0;
+    if (vb > 2)
+        pr2serr_lk("[thread_id=%d] %s: num_mrq=%d, n_subm=%d, n_cmpl=%d\n",
+                   id, __func__, num_mrq, n_subm, n_cmpl);
+    if (n_subm < 0) {
+        pr2serr_lk("[%d] co.dout_resid(%d) > num_mrq(%d)\n", id,
+                   ctl_v4p->dout_resid, num_mrq);
+        return -1;
+    }
+    if (n_cmpl != (num_mrq - resid))
+        pr2serr_lk("[%d] co.info(%d) != (num_mrq(%d) - co.din_resid(%d))\n"
+                   "will use co.info\n", id, n_cmpl, num_mrq, resid);
+    if (n_cmpl > n_subm) {
+        pr2serr_lk("[%d] n_cmpl(%d) > n_subm(%d), use n_subm for both\n",
+                   id, n_cmpl, n_subm);
+        n_cmpl = n_subm;
+    }
+    if (sres) {
+        pr2serr_lk("[%d] secondary error: %s [%d], info=0x%x\n", id,
+                   strerror(sres), sres, ctl_v4p->info);
+        if (E2BIG == sres) {
+            sg_take_snap(rep->infd, id, true);
+            sg_take_snap(rep->outfd, id, true);
+        }
+    }
+    /* Check if those submitted have finished or not. N.B. If there has been
+     * an error then there may be "holes" (i.e. info=0x0) in the array due
+     * to completions being out-of-order. */
+    for (k = 0, j = 0; ((k < num_mrq) && (j < n_subm));
+         ++k, j += f1, ++a_v4p) {
+        slen = a_v4p->response_len;
+        if (! (SG_INFO_MRQ_FINI & a_v4p->info))
+            ++hole_count;
+        ok = true;
+        f1 = !!(a_v4p->info);   /* want to skip n_subm count if info is 0x0 */
+        if (SG_INFO_CHECK & a_v4p->info) {
+            if ((0 == k) && (SGV4_FLAG_META_OUT_IF & ctl_v4p->flags) &&
+                (UINT32_MAX == a_v4p->info)) {
+                hole_count = 0;
+                n_good = num_mrq;
+                good_inblks = rep->a_mrq_din_blks;
+                good_outblks = rep->a_mrq_dout_blks;
+                break;
+            }
+            ok = false;
+            pr2serr_lk("[%d] a_v4[%d]: SG_INFO_CHECK set [%s]\n", id, k,
+                       sg_info_str(a_v4p->info, sizeof(b), b));
+        }
+        if (sg_scsi_status_is_bad(a_v4p->device_status) ||
+            a_v4p->transport_status || a_v4p->driver_status) {
+            ok = false;
+            last_err_on_in = ! (a_v4p->flags & SGV4_FLAG_DO_ON_OTHER);
+            if (SAM_STAT_CHECK_CONDITION != a_v4p->device_status) {
+                pr2serr_lk("[%d] a_v4[%d]:\n", id, k);
+                if (vb)
+                    lk_chk_n_print4("  >>", a_v4p, vb > 4);
+            }
+        }
+        if (slen > 0) {
+            struct sg_scsi_sense_hdr ssh;
+            const uint8_t *sbp = (const uint8_t *)
+                        (sb_in_co ? ctl_v4p->response : a_v4p->response);
+
+            if (sg_scsi_normalize_sense(sbp, slen, &ssh) &&
+                (ssh.response_code >= 0x70)) {
+                if (ssh.response_code & 0x1) {
+                    ok = true;
+                    last_err_on_in = false;
+                } else
+                    cat = sg_err_category_sense(sbp, slen);
+                if (SPC_SK_MISCOMPARE == ssh.sense_key)
+                    ++num_miscompare;
+
+                pr2serr_lk("[%d] a_v4[%d]:\n", id, k);
+                if (vb)
+                    lk_chk_n_print4("  >>", a_v4p, vb > 4);
+            }
+        } else if (! ok)
+            cat = SG_LIB_CAT_OTHER;
+        if (ok && f1) {
+            ++n_good;
+            if (a_v4p->dout_xfer_len >= (uint32_t)rep->bs)
+                good_outblks += (a_v4p->dout_xfer_len - a_v4p->dout_resid) /
+                                rep->bs;
+            if (a_v4p->din_xfer_len >= (uint32_t)rep->bs)
+                good_inblks += (a_v4p->din_xfer_len - a_v4p->din_resid) /
+                               rep->bs;
+        }
+        if (! ok) {
+            if ((a_v4p->dout_xfer_len > 0) || (! clp->in_flags.coe))
+                rep->stop_after_write = true;
+        }
+    }   /* end of request array scan loop */
+    if ((n_subm == num_mrq) || (vb < 3))
+        goto fini;
+    pr2serr_lk("[%d] checking response array _beyond_ number of "
+               "submissions [%d] to num_mrq:\n", id, k);
+    for (all_good = true; k < num_mrq; ++k, ++a_v4p) {
+        if (SG_INFO_MRQ_FINI & a_v4p->info) {
+            pr2serr_lk("[%d] a_v4[%d]: unexpected SG_INFO_MRQ_FINI set [%s]\n",
+                       id, k, sg_info_str(a_v4p->info, sizeof(b), b));
+            all_good = false;
+        }
+        if (a_v4p->device_status || a_v4p->transport_status ||
+            a_v4p->driver_status) {
+            pr2serr_lk("[%d] a_v4[%d]:\n", id, k);
+            lk_chk_n_print4("    ", a_v4p, vb > 4);
+            all_good = false;
+        }
+    }
+    if (all_good)
+        pr2serr_lk("    ... all good\n");
+fini:
+    if (cat > 0)
+        clp->reason_res.store(cat);
+    return n_good;
+}
+
+/* Returns number of blocks successfully processed or a negative error
+ * number. */
+static int
+sg_half_segment_mrq0(Rq_elem * rep, scat_gath_iter & sg_it, bool is_wr,
+                     int seg_blks, uint8_t *dp)
+{
+    int k, res, fd, pack_id_base, id, rflags;
+    int num, kk, lin_blks, cdbsz, err;
+    uint32_t q_blks = 0;
+    struct global_collection * clp = rep->clp;
+    cdb_arr_t t_cdb {};
+    struct sg_io_v4 t_v4 {};
+    struct sg_io_v4 * t_v4p = &t_v4;
+    struct flags_t * flagsp = is_wr ? &clp->out_flags : &clp->in_flags;
+    int vb = clp->verbose;
+
+    id = rep->id;
+    pack_id_base = id * PACK_ID_TID_MULTIPLIER;
+    rflags = 0;
+    fd = is_wr ? rep->outfd : rep->infd;
+    if (flagsp->mmap && (rep->outregfd >= 0))
+        rflags |= SGV4_FLAG_MMAP_IO;
+    if (flagsp->dio)
+        rflags |= SGV4_FLAG_DIRECT_IO;
+    if (flagsp->qhead)
+        rflags |= SGV4_FLAG_Q_AT_HEAD;
+    if (flagsp->qtail)
+        rflags |= SGV4_FLAG_Q_AT_TAIL;
+    if (flagsp->polled)
+        rflags |= SGV4_FLAG_POLLED;
+
+    for (k = 0, num = 0; seg_blks > 0; ++k, seg_blks -= num) {
+        kk = min<int>(seg_blks, clp->bpt);
+        lin_blks = sg_it.linear_for_n_blks(kk);
+        num = lin_blks;
+        if (num <= 0) {
+            res = 0;
+            pr2serr_lk("[%d] %s: unexpected num=%d\n", id, __func__, num);
+            break;
+        }
+
+        /* First build the command/request for the read-side */
+        cdbsz = is_wr ? clp->cdbsz_out : clp->cdbsz_in;
+        res = sg_build_scsi_cdb(t_cdb.data(), cdbsz, num, sg_it.current_lba(),
+                                false, is_wr, flagsp->fua, flagsp->dpo,
+                                flagsp->cdl);
+        if (res) {
+            pr2serr_lk("[%d] %s: sg_build_scsi_cdb() failed\n", id, __func__);
+            break;
+        } else if (vb > 3)
+            lk_print_command_len("cdb: ", t_cdb.data(), cdbsz, true);
+
+        t_v4p->guard = 'Q';
+        t_v4p->request = (uint64_t)t_cdb.data();
+        t_v4p->usr_ptr = t_v4p->request;
+        t_v4p->response = (uint64_t)rep->sb;
+        t_v4p->max_response_len = sizeof(rep->sb);
+        t_v4p->flags = rflags;
+        t_v4p->request_len = cdbsz;
+        if (is_wr) {
+            t_v4p->dout_xfer_len = num * rep->bs;
+            t_v4p->dout_xferp = (uint64_t)(dp + (q_blks * rep->bs));
+            t_v4p->din_xfer_len = 0;
+        } else {
+            t_v4p->din_xfer_len = num * rep->bs;
+            t_v4p->din_xferp = (uint64_t)(dp + (q_blks * rep->bs));
+            t_v4p->dout_xfer_len = 0;
+        }
+        t_v4p->timeout = clp->cmd_timeout;
+        t_v4p->request_extra = pack_id_base + ++rep->mrq_pack_id_off;
+        clp->most_recent_pack_id.store(t_v4p->request_extra);
+mrq0_again:
+        res = ioctl(fd, SG_IO, t_v4p);
+        err = errno;
+        if (vb > 5)
+            v4hdr_out_lk("sg_half_segment_mrq0: >> after ioctl(SG_IO)",
+                         t_v4p, id, false);
+        if (res < 0) {
+            if (E2BIG == err)
+                sg_take_snap(fd, id, true);
+            else if (EBUSY == err) {
+                ++num_ebusy;
+                std::this_thread::yield();/* so other threads can progress */
+                goto mrq0_again;
+            }
+            pr2serr_lk("[%d] %s: ioctl(SG_IO)-->%d, errno=%d: %s\n", id,
+                       __func__, res, err, strerror(err));
+            return -err;
+        }
+        if (t_v4p->device_status || t_v4p->transport_status ||
+            t_v4p->driver_status) {
+            rep->stop_now = true;
+            pr2serr_lk("[%d] t_v4[%d]:\n", id, k);
+            lk_chk_n_print4("    ", t_v4p, vb > 4);
+            return q_blks;
+        }
+        q_blks += num;
+        sg_it.add_blks(num);
+    }
+    return q_blks;
+}
+
+/* Returns number of blocks successfully processed or a negative error
+ * number. */
+static int
+sg_half_segment(Rq_elem * rep, scat_gath_iter & sg_it, bool is_wr,
+                int seg_blks, uint8_t *dp, vector<cdb_arr_t> & a_cdb,
+                vector<struct sg_io_v4> & a_v4)
+{
+    int num_mrq, k, res, fd, mrq_pack_id_base, id, b_len, rflags;
+    int num, kk, lin_blks, cdbsz, num_good, err;
+    int o_seg_blks = seg_blks;
+    uint32_t in_fin_blks, out_fin_blks;
+    uint32_t mrq_q_blks = 0;
+    uint32_t in_mrq_q_blks = 0;
+    uint32_t out_mrq_q_blks = 0;
+    const int max_cdb_sz = MAX_SCSI_CDB_SZ;
+    struct sg_io_v4 * a_v4p;
+    struct sg_io_v4 ctl_v4 {};     /* MRQ control object */
+    struct global_collection * clp = rep->clp;
+    const char * iosub_str = "SG_IOSUBMIT(variable blocking)";
+    char b[80];
+    cdb_arr_t t_cdb {};
+    struct sg_io_v4 t_v4 {};
+    struct sg_io_v4 * t_v4p = &t_v4;
+    struct flags_t * flagsp = is_wr ? &clp->out_flags : &clp->in_flags;
+    bool serial = flagsp->serial;
+    bool err_on_in = false;
+    int vb = clp->verbose;
+
+    id = rep->id;
+    b_len = sizeof(b);
+    if (serial)
+        iosub_str = "SG_IO(ordered blocking)";
+
+    a_cdb.clear();
+    a_v4.clear();
+    rep->a_mrq_din_blks = 0;
+    rep->a_mrq_dout_blks = 0;
+    mrq_pack_id_base = id * PACK_ID_TID_MULTIPLIER;
+
+    rflags = 0;
+    if (flagsp->mmap && (rep->outregfd >= 0))
+        rflags |= SGV4_FLAG_MMAP_IO;
+    if (flagsp->dio)
+        rflags |= SGV4_FLAG_DIRECT_IO;
+    if (flagsp->qhead)
+        rflags |= SGV4_FLAG_Q_AT_HEAD;
+    if (flagsp->qtail)
+        rflags |= SGV4_FLAG_Q_AT_TAIL;
+    if (flagsp->polled)
+        rflags |= SGV4_FLAG_POLLED;
+
+    for (k = 0, num = 0; seg_blks > 0; ++k, seg_blks -= num) {
+        kk = min<int>(seg_blks, clp->bpt);
+        lin_blks = sg_it.linear_for_n_blks(kk);
+        num = lin_blks;
+        if (num <= 0) {
+            res = 0;
+            pr2serr_lk("[%d] %s: unexpected num=%d\n", id, __func__, num);
+            break;
+        }
+
+        /* First build the command/request for the read-side */
+        cdbsz = is_wr ? clp->cdbsz_out : clp->cdbsz_in;
+        res = sg_build_scsi_cdb(t_cdb.data(), cdbsz, num, sg_it.current_lba(),
+                                false, is_wr, flagsp->fua, flagsp->dpo,
+                                flagsp->cdl);
+        if (res) {
+            pr2serr_lk("[%d] %s: sg_build_scsi_cdb() failed\n", id, __func__);
+            break;
+        } else if (vb > 3)
+            lk_print_command_len("cdb: ", t_cdb.data(), cdbsz, true);
+        a_cdb.push_back(t_cdb);
+
+        t_v4p->guard = 'Q';
+        t_v4p->flags = rflags;
+        t_v4p->request_len = cdbsz;
+        t_v4p->response = (uint64_t)rep->sb;
+        t_v4p->max_response_len = sizeof(rep->sb);
+        t_v4p->flags = rflags;
+        t_v4p->usr_ptr = (uint64_t)&a_cdb[a_cdb.size() - 1];
+        if (is_wr) {
+            rep->a_mrq_dout_blks += num;
+            t_v4p->dout_xfer_len = num * rep->bs;
+            t_v4p->dout_xferp = (uint64_t)(dp + (mrq_q_blks * rep->bs));
+            t_v4p->din_xfer_len = 0;
+        } else {
+            rep->a_mrq_din_blks += num;
+            t_v4p->din_xfer_len = num * rep->bs;
+            t_v4p->din_xferp = (uint64_t)(dp + (mrq_q_blks * rep->bs));
+            t_v4p->dout_xfer_len = 0;
+        }
+        t_v4p->timeout = clp->cmd_timeout;
+        mrq_q_blks += num;
+        t_v4p->request_extra = mrq_pack_id_base + ++rep->mrq_pack_id_off;
+        clp->most_recent_pack_id.store(t_v4p->request_extra);
+        a_v4.push_back(t_v4);
+
+        sg_it.add_blks(num);
+    }
+
+    if (rep->only_in_sg)
+        fd = rep->infd;
+    else if (rep->only_out_sg)
+        fd = rep->outfd;
+    else {
+        pr2serr_lk("[%d] %s: why am I here? No sg devices\n", id, __func__);
+        return -EINVAL;
+    }
+    num_mrq = a_v4.size();
+    a_v4p = a_v4.data();
+    res = 0;
+    ctl_v4.guard = 'Q';
+    ctl_v4.request_len = a_cdb.size() * max_cdb_sz;
+    ctl_v4.request = (uint64_t)a_cdb.data();
+    ctl_v4.max_response_len = sizeof(rep->sb);
+    ctl_v4.response = (uint64_t)rep->sb;
+    ctl_v4.flags = SGV4_FLAG_MULTIPLE_REQS;
+    if (! flagsp->coe)
+        ctl_v4.flags |= SGV4_FLAG_STOP_IF;
+    if (clp->mrq_polled)
+        ctl_v4.flags |= SGV4_FLAG_POLLED;
+    if (clp->in_flags.mout_if || clp->out_flags.mout_if) {
+        ctl_v4.flags |= SGV4_FLAG_META_OUT_IF;
+        if (num_mrq > 0)
+            a_v4[0].info = UINT32_MAX;
+    }
+    ctl_v4.dout_xferp = (uint64_t)a_v4.data();        /* request array */
+    ctl_v4.dout_xfer_len = a_v4.size() * sizeof(struct sg_io_v4);
+    ctl_v4.din_xferp = (uint64_t)a_v4.data();         /* response array */
+    ctl_v4.din_xfer_len = a_v4.size() * sizeof(struct sg_io_v4);
+    if (false /* allow_mrq_abort */) {
+        ctl_v4.request_extra = mrq_pack_id_base + ++rep->mrq_pack_id_off;
+        clp->most_recent_pack_id.store(ctl_v4.request_extra);
+    }
+
+    if (vb && vb_first_time.load()) {
+        pr2serr_lk("First controlling object output by ioctl(%s), flags: "
+                   "%s\n", iosub_str, sg_flags_str(ctl_v4.flags, b_len, b));
+        vb_first_time.store(false);
+    } else if (vb > 4) {
+        pr2serr_lk("[%d] %s: >> Control object _before_ ioctl(%s):\n", id,
+                   __func__, iosub_str);
+    }
+    if (vb > 4) {
+        if (vb > 5)
+            hex2stderr_lk((const uint8_t *)&ctl_v4, sizeof(ctl_v4), 1);
+        v4hdr_out_lk(">> Control object before", &ctl_v4, id, false);
+    }
+
+try_again:
+    if (!after1 && (vb > 1)) {
+        after1 = true;
+        pr2serr_lk("%s: %s\n", __func__, serial ? mrq_ob_s : mrq_vb_s);
+    }
+    if (serial)
+        res = ioctl(fd, SG_IO, &ctl_v4);
+    else
+        res = ioctl(fd, SG_IOSUBMIT, &ctl_v4);  /* overlapping commands */
+    if (res < 0) {
+        err = errno;
+        if (E2BIG == err)
+            sg_take_snap(fd, id, true);
+        else if (EBUSY == err) {
+            ++num_ebusy;
+            std::this_thread::yield();/* allow another thread to progress */
+            goto try_again;
+        }
+        pr2serr_lk("[%d] %s: ioctl(%s, %s)-->%d, errno=%d: %s\n", id,
+                   __func__, iosub_str, sg_flags_str(ctl_v4.flags, b_len, b),
+                   res, err, strerror(err));
+        return -err;
+    }
+    if (vb > 4) {
+        pr2serr_lk("%s: >> Control object after ioctl(%s) seg_blks=%d:\n",
+                   __func__, iosub_str, o_seg_blks);
+        if (vb > 5)
+            hex2stderr_lk((const uint8_t *)&ctl_v4, sizeof(ctl_v4), 1);
+        v4hdr_out_lk(">> Control object after", &ctl_v4, id, false);
+        if (vb > 5) {
+            for (k = 0; k < num_mrq; ++k) {
+                if ((vb > 6) || a_v4p[k].info) {
+                    snprintf(b, b_len, "a_v4[%d/%d]", k, num_mrq);
+                    v4hdr_out_lk(b, (a_v4p + k), id, true);
+                }
+            }
+        }
+    }
+    num_good = process_mrq_response(rep, &ctl_v4, a_v4p, num_mrq, in_fin_blks,
+                                    out_fin_blks, err_on_in);
+    if (is_wr)
+        out_mrq_q_blks = mrq_q_blks;
+    else
+        in_mrq_q_blks = mrq_q_blks;
+    if (vb > 2)
+        pr2serr_lk("%s: >>> seg_blks=%d, num_good=%d, in_q/fin blks=%u/%u;  "
+                   "out_q/fin blks=%u/%u\n", __func__, o_seg_blks, num_good,
+                   in_mrq_q_blks, in_fin_blks, out_mrq_q_blks, out_fin_blks);
+
+    if (clp->ese) {
+        int sres = ctl_v4.spare_out;
+
+        if (sres != 0) {
+            clp->reason_res.store(sg_convert_errno(sres));
+            pr2serr_lk("Exit due to secondary error [%d]\n", sres);
+            return -sres;
+        }
+    }
+    if (num_good < 0)
+        return -ENODATA;
+    else {
+        if (num_good < num_mrq) {
+            int resid_blks = in_mrq_q_blks - in_fin_blks;
+
+            if (resid_blks > 0) {
+                rep->in_rem_count += resid_blks;
+                rep->stop_after_write = ! (err_on_in && clp->in_flags.coe);
+            }
+
+            resid_blks = out_mrq_q_blks - out_fin_blks;
+            if (resid_blks > 0) {
+                rep->out_rem_count += resid_blks;
+                rep->stop_after_write = ! (! err_on_in && clp->out_flags.coe);
+            }
+        }
+    }
+    return is_wr ? out_fin_blks : in_fin_blks;
+}
+
+/* Returns number of blocks successfully processed or a negative error
+ * number. */
+static int
+do_normal_normal_segment(Rq_elem * rep, scat_gath_iter & i_sg_it,
+                         scat_gath_iter & o_sg_it, int seg_blks)
+{
+    int k, kk, res, id, num, d_off;
+    int o_seg_blks = seg_blks;
+    uint32_t in_fin_blks = 0;
+    uint32_t out_fin_blks = 0;
+    struct global_collection * clp = rep->clp;
+
+    id = rep->id;
+    d_off = 0;
+    for (k = 0; seg_blks > 0; ++k, seg_blks -= num, d_off += num) {
+        kk = min<int>(seg_blks, clp->bpt);
+        num = i_sg_it.linear_for_n_blks(kk);
+        res = normal_in_rd(rep, i_sg_it.current_lba(), num,
+                           d_off * rep->bs);
+        if (res < 0) {
+            pr2serr_lk("[%d] %s: normal in failed d_off=%d, err=%d\n",
+                       id, __func__, d_off, -res);
+            break;
+        }
+        i_sg_it.add_blks(res);
+        if (res < num) {
+            d_off += res;
+            rep->stop_after_write = true;
+            break;
+        }
+    }
+    seg_blks = d_off;
+    in_fin_blks = seg_blks;
+
+    if (FT_DEV_NULL == clp->out_type)
+        goto fini;
+    d_off = 0;
+    for (k = 0; seg_blks > 0; ++k, seg_blks -= num, d_off += num) {
+        kk = min<int>(seg_blks, clp->bpt);
+        num = o_sg_it.linear_for_n_blks(kk);
+        res = normal_out_wr(rep, o_sg_it.current_lba(), num,
+                            d_off * rep->bs);
+        if (res < num) {
+            if (res < 0) {
+                pr2serr_lk("[%d] %s: normal out failed d_off=%d, err=%d\n",
+                           id, __func__, d_off, -res);
+                break;
+            }
+        }
+        o_sg_it.add_blks(res);
+        if (res < num) {
+            d_off += res;
+            rep->stop_after_write = true;
+            break;
+        }
+    }
+    if (rep->in_resid_bytes > 0) {
+        res = extra_out_wr(rep, rep->in_resid_bytes, d_off * rep->bs);
+        if (res < 0)
+            pr2serr_lk("[%d] %s: extr out failed d_off=%d, err=%d\n", id,
+                       __func__, d_off, -res);
+        rep->in_resid_bytes = 0;
+    }
+    seg_blks = d_off;
+    out_fin_blks = seg_blks;
+
+fini:
+    rep->in_local_count += in_fin_blks;
+    rep->out_local_count += out_fin_blks;
+
+    if ((in_fin_blks + out_fin_blks) < (uint32_t)o_seg_blks) {
+        int resid_blks = o_seg_blks - in_fin_blks;
+
+        if (resid_blks > 0)
+            rep->in_rem_count += resid_blks;
+        resid_blks = o_seg_blks - out_fin_blks;
+        if (resid_blks > 0)
+            rep->out_rem_count += resid_blks;
+    }
+    return res < 0 ? res : (min<int>(in_fin_blks, out_fin_blks));
+}
+
+/* Returns number of blocks successfully processed or a negative error
+ * number. */
+static int
+do_normal_sg_segment(Rq_elem * rep, scat_gath_iter & i_sg_it,
+                     scat_gath_iter & o_sg_it, int seg_blks,
+                     vector<cdb_arr_t> & a_cdb,
+                     vector<struct sg_io_v4> & a_v4)
+{
+    bool in_is_normal = ! rep->only_in_sg;
+    int k, kk, res, id, num, d_off;
+    int o_seg_blks = seg_blks;
+    uint32_t in_fin_blks = 0;
+    uint32_t out_fin_blks = 0;
+    struct global_collection * clp = rep->clp;
+
+    id = rep->id;
+    a_cdb.clear();
+    a_v4.clear();
+
+    if (in_is_normal) {   /* in: normal --> out : sg */
+        d_off = 0;
+        for (k = 0; seg_blks > 0; ++k, seg_blks -= num, d_off += num) {
+            kk = min<int>(seg_blks, clp->bpt);
+            num = i_sg_it.linear_for_n_blks(kk);
+            res = normal_in_rd(rep, i_sg_it.current_lba(), num,
+                               d_off * rep->bs);
+            if (res < 0) {
+                pr2serr_lk("[%d] %s: normal in failed d_off=%d, err=%d\n",
+                           id, __func__, d_off, -res);
+                break;
+            }
+            i_sg_it.add_blks(res);
+            if (res < num) {
+                d_off += res;
+                rep->stop_after_write = true;
+                break;
+            }
+        }
+        seg_blks = d_off;
+        in_fin_blks = seg_blks;
+
+        if (rep->in_resid_bytes > 0) {
+            ++seg_blks;
+            rep->in_resid_bytes = 0;
+        }
+        if (clp->mrq_eq_0)
+            res = sg_half_segment_mrq0(rep, o_sg_it, true /* is_wr */,
+                                      seg_blks, rep->buffp);
+        else
+            res = sg_half_segment(rep, o_sg_it, true /* is_wr */, seg_blks,
+                                  rep->buffp, a_cdb, a_v4);
+        if (res < seg_blks) {
+            if (res < 0) {
+                pr2serr_lk("[%d] %s: sg out failed d_off=%d, err=%d\n",
+                           id, __func__, d_off, -res);
+                goto fini;
+            }
+            rep->stop_after_write = true;
+        }
+        seg_blks = res;
+        out_fin_blks = seg_blks;
+
+    } else {      /* in: sg --> out: normal */
+        if (clp->mrq_eq_0)
+            res = sg_half_segment_mrq0(rep, i_sg_it, false, seg_blks,
+                                       rep->buffp);
+        else
+            res = sg_half_segment(rep, i_sg_it, false, seg_blks, rep->buffp,
+                                  a_cdb, a_v4);
+        if (res < seg_blks) {
+            if (res < 0) {
+                pr2serr_lk("[%d] %s: sg in failed, err=%d\n", id, __func__,
+                           -res);
+                goto fini;
+            }
+            rep->stop_after_write = true;
+        }
+        seg_blks = res;
+        in_fin_blks = seg_blks;
+
+        if (FT_DEV_NULL == clp->out_type) {
+            out_fin_blks = seg_blks;/* so finish logic doesn't suspect ... */
+            goto bypass;
+        }
+        d_off = 0;
+        for (k = 0; seg_blks > 0; ++k, seg_blks -= num, d_off += num) {
+            kk = min<int>(seg_blks, clp->bpt);
+            num = o_sg_it.linear_for_n_blks(kk);
+            res = normal_out_wr(rep, o_sg_it.current_lba(), num,
+                                d_off * rep->bs);
+            if (res < num) {
+                if (res < 0) {
+                    pr2serr_lk("[%d] %s: normal out failed d_off=%d, err=%d\n",
+                               id, __func__, d_off, -res);
+                    break;
+                }
+            }
+            o_sg_it.add_blks(res);
+            if (res < num) {
+                d_off += res;
+                rep->stop_after_write = true;
+                break;
+            }
+        }
+        seg_blks = d_off;
+        out_fin_blks = seg_blks;
+    }
+bypass:
+    rep->in_local_count += in_fin_blks;
+    rep->out_local_count += out_fin_blks;
+
+    if ((in_fin_blks + out_fin_blks) < (uint32_t)o_seg_blks) {
+        int resid_blks = o_seg_blks - in_fin_blks;
+
+        if (resid_blks > 0)
+            rep->in_rem_count += resid_blks;
+        resid_blks = o_seg_blks - out_fin_blks;
+        if (resid_blks > 0)
+            rep->out_rem_count += resid_blks;
+    }
+fini:
+    return res < 0 ? res : (min<int>(in_fin_blks, out_fin_blks));
+}
+
+/* This function sets up a multiple request (mrq) transaction and sends it
+ * to the pass-through. Returns number of blocks processed (==seg_blks for
+ * all good) or a negative error number. */
+static int
+do_both_sg_segment_mrq0(Rq_elem * rep, scat_gath_iter & i_sg_it,
+                        scat_gath_iter & o_sg_it, int seg_blks)
+{
+    int k, kk, res, pack_id_base, id, iflags, oflags;
+    int num, i_lin_blks, o_lin_blks, cdbsz, err;
+    uint32_t in_fin_blks = 0;
+    uint32_t out_fin_blks = 0;
+    struct global_collection * clp = rep->clp;
+    int vb = clp->verbose;
+    cdb_arr_t t_cdb {};
+    struct sg_io_v4 t_v4 {};
+    struct sg_io_v4 * t_v4p = &t_v4;
+    struct flags_t * iflagsp = &clp->in_flags;
+    struct flags_t * oflagsp = &clp->out_flags;
+    const char * const a_ioctl_s = "do_both_sg_segment_mrq0: after "
+                                   "ioctl(SG_IO)";
+
+    id = rep->id;
+    pack_id_base = id * PACK_ID_TID_MULTIPLIER;
+
+    iflags = SGV4_FLAG_SHARE;
+    if (iflagsp->mmap && (rep->outregfd >= 0))
+        iflags |= SGV4_FLAG_MMAP_IO;
+    else
+        iflags |= SGV4_FLAG_NO_DXFER;
+    if (iflagsp->dio)
+        iflags |= SGV4_FLAG_DIRECT_IO;
+    if (iflagsp->qhead)
+        iflags |= SGV4_FLAG_Q_AT_HEAD;
+    if (iflagsp->qtail)
+        iflags |= SGV4_FLAG_Q_AT_TAIL;
+    if (iflagsp->polled)
+        iflags |= SGV4_FLAG_POLLED;
+
+    oflags = SGV4_FLAG_SHARE | SGV4_FLAG_NO_DXFER;
+    if (oflagsp->dio)
+        oflags |= SGV4_FLAG_DIRECT_IO;
+    if (oflagsp->qhead)
+        oflags |= SGV4_FLAG_Q_AT_HEAD;
+    if (oflagsp->qtail)
+        oflags |= SGV4_FLAG_Q_AT_TAIL;
+    if (oflagsp->polled)
+        oflags |= SGV4_FLAG_POLLED;
+
+    for (k = 0; seg_blks > 0; ++k, seg_blks -= num) {
+        kk = min<int>(seg_blks, clp->bpt);
+        i_lin_blks = i_sg_it.linear_for_n_blks(kk);
+        o_lin_blks = o_sg_it.linear_for_n_blks(kk);
+        num = min<int>(i_lin_blks, o_lin_blks);
+        if (num <= 0) {
+            res = 0;
+            pr2serr_lk("[%d] %s: min(i_lin_blks=%d o_lin_blks=%d) < 1\n", id,
+                       __func__, i_lin_blks, o_lin_blks);
+            break;
+        }
+
+        /* First build the command/request for the read-side*/
+        cdbsz = clp->cdbsz_in;
+        res = sg_build_scsi_cdb(t_cdb.data(), cdbsz, num,
+                                i_sg_it.current_lba(), false, false,
+                                iflagsp->fua, iflagsp->dpo, iflagsp->cdl);
+        if (res) {
+            pr2serr_lk("%s: t=%d: input sg_build_scsi_cdb() failed\n",
+                       __func__, id);
+            break;
+        } else if (vb > 3)
+            lk_print_command_len("input cdb: ", t_cdb.data(), cdbsz, true);
+
+        t_v4p->guard = 'Q';
+        t_v4p->request = (uint64_t)t_cdb.data();
+        t_v4p->usr_ptr = t_v4p->request;
+        t_v4p->response = (uint64_t)rep->sb;
+        t_v4p->max_response_len = sizeof(rep->sb);
+        t_v4p->flags = iflags;
+        t_v4p->request_len = cdbsz;
+        t_v4p->din_xfer_len = num * rep->bs;
+        t_v4p->dout_xfer_len = 0;
+        t_v4p->timeout = clp->cmd_timeout;
+        t_v4p->request_extra = pack_id_base + ++rep->mrq_pack_id_off;
+        clp->most_recent_pack_id.store(t_v4p->request_extra);
+mrq0_again:
+        res = ioctl(rep->infd, SG_IO, t_v4p);
+        err = errno;
+        if (vb > 5)
+            v4hdr_out_lk(a_ioctl_s, t_v4p, id, false);
+        if (res < 0) {
+            if (E2BIG == err)
+                sg_take_snap(rep->infd, id, true);
+            else if (EBUSY == err) {
+                ++num_ebusy;
+                std::this_thread::yield();/* so other threads can progress */
+                goto mrq0_again;
+            }
+            pr2serr_lk("[%d] %s: ioctl(SG_IO, read-side)-->%d, errno=%d: "
+                       "%s\n", id, __func__, res, err, strerror(err));
+            return -err;
+        }
+        if (t_v4p->device_status || t_v4p->transport_status ||
+            t_v4p->driver_status) {
+            rep->stop_now = true;
+            pr2serr_lk("[%d] t_v4[%d]:\n", id, k);
+            lk_chk_n_print4("    ", t_v4p, vb > 4);
+            return min<int>(in_fin_blks, out_fin_blks);
+        }
+        rep->in_local_count += num;
+        in_fin_blks += num;
+
+        /* Now build the command/request for write-side (WRITE or VERIFY) */
+        cdbsz = clp->cdbsz_out;
+        res = sg_build_scsi_cdb(t_cdb.data(), cdbsz, num,
+                                o_sg_it.current_lba(), clp->verify, true,
+                                oflagsp->fua, oflagsp->dpo, oflagsp->cdl);
+        if (res) {
+            pr2serr_lk("%s: t=%d: output sg_build_scsi_cdb() failed\n",
+                       __func__, id);
+            break;
+        } else if (vb > 3)
+            lk_print_command_len("output cdb: ", t_cdb.data(), cdbsz, true);
+
+        t_v4p->guard = 'Q';
+        t_v4p->request = (uint64_t)t_cdb.data();
+        t_v4p->usr_ptr = t_v4p->request;
+        t_v4p->response = (uint64_t)rep->sb;
+        t_v4p->max_response_len = sizeof(rep->sb);
+        t_v4p->flags = oflags;
+        t_v4p->request_len = cdbsz;
+        t_v4p->din_xfer_len = 0;
+        t_v4p->dout_xfer_len = num * rep->bs;
+        t_v4p->timeout = clp->cmd_timeout;
+        t_v4p->request_extra = pack_id_base + ++rep->mrq_pack_id_off;
+        clp->most_recent_pack_id.store(t_v4p->request_extra);
+mrq0_again2:
+        res = ioctl(rep->outfd, SG_IO, t_v4p);
+        err = errno;
+        if (vb > 5)
+            v4hdr_out_lk(a_ioctl_s, t_v4p, id, false);
+        if (res < 0) {
+            if (E2BIG == err)
+                sg_take_snap(rep->outfd, id, true);
+            else if (EBUSY == err) {
+                ++num_ebusy;
+                std::this_thread::yield();/* so other threads can progress */
+                goto mrq0_again2;
+            }
+            pr2serr_lk("[%d] %s: ioctl(SG_IO, write-side)-->%d, errno=%d: "
+                       "%s\n", id, __func__, res, err, strerror(err));
+            return -err;
+        }
+        if (t_v4p->device_status || t_v4p->transport_status ||
+            t_v4p->driver_status) {
+            rep->stop_now = true;
+            pr2serr_lk("[%d] t_v4[%d]:\n", id, k);
+            lk_chk_n_print4("    ", t_v4p, vb > 4);
+            return min<int>(in_fin_blks, out_fin_blks);
+        }
+        rep->out_local_count += num;
+        out_fin_blks += num;
+
+        i_sg_it.add_blks(num);
+        o_sg_it.add_blks(num);
+    }
+    return min<int>(in_fin_blks, out_fin_blks);
+}
+
+/* This function sets up a multiple request (mrq) transaction and sends it
+ * to the pass-through. Returns number of blocks processed (==seg_blks for
+ * all good) or a negative error number. */
+static int
+do_both_sg_segment(Rq_elem * rep, scat_gath_iter & i_sg_it,
+                   scat_gath_iter & o_sg_it, int seg_blks,
+                   vector<cdb_arr_t> & a_cdb,
+                   vector<struct sg_io_v4> & a_v4)
+{
+    bool err_on_in = false;
+    int num_mrq, k, res, fd, mrq_pack_id_base, id, b_len, iflags, oflags;
+    int num, kk, i_lin_blks, o_lin_blks, cdbsz, num_good, err;
+    int o_seg_blks = seg_blks;
+    uint32_t in_fin_blks = 0;
+    uint32_t out_fin_blks = 0;;
+    uint32_t in_mrq_q_blks = 0;
+    uint32_t out_mrq_q_blks = 0;
+    const int max_cdb_sz = MAX_SCSI_CDB_SZ;
+    struct sg_io_v4 * a_v4p;
+    struct sg_io_v4 ctl_v4 {};     /* MRQ control object */
+    struct global_collection * clp = rep->clp;
+    const char * iosub_str = "SG_IOSUBMIT(svb)";
+    char b[80];
+    cdb_arr_t t_cdb {};
+    struct sg_io_v4 t_v4 {};
+    struct sg_io_v4 * t_v4p = &t_v4;
+    struct flags_t * iflagsp = &clp->in_flags;
+    struct flags_t * oflagsp = &clp->out_flags;
+    int vb = clp->verbose;
+
+    id = rep->id;
+    b_len = sizeof(b);
+
+    a_cdb.clear();
+    a_v4.clear();
+    rep->a_mrq_din_blks = 0;
+    rep->a_mrq_dout_blks = 0;
+    mrq_pack_id_base = id * PACK_ID_TID_MULTIPLIER;
+
+    iflags = SGV4_FLAG_SHARE;
+    if (iflagsp->mmap && (rep->outregfd >= 0))
+        iflags |= SGV4_FLAG_MMAP_IO;
+    else
+        iflags |= SGV4_FLAG_NO_DXFER;
+    if (iflagsp->dio)
+        iflags |= SGV4_FLAG_DIRECT_IO;
+    if (iflagsp->qhead)
+        iflags |= SGV4_FLAG_Q_AT_HEAD;
+    if (iflagsp->qtail)
+        iflags |= SGV4_FLAG_Q_AT_TAIL;
+    if (iflagsp->polled)
+        iflags |= SGV4_FLAG_POLLED;
+
+    oflags = SGV4_FLAG_SHARE | SGV4_FLAG_NO_DXFER;
+    if (oflagsp->dio)
+        oflags |= SGV4_FLAG_DIRECT_IO;
+    if (oflagsp->qhead)
+        oflags |= SGV4_FLAG_Q_AT_HEAD;
+    if (oflagsp->qtail)
+        oflags |= SGV4_FLAG_Q_AT_TAIL;
+    if (oflagsp->polled)
+        oflags |= SGV4_FLAG_POLLED;
+    oflags |= SGV4_FLAG_DO_ON_OTHER;
+
+    for (k = 0; seg_blks > 0; ++k, seg_blks -= num) {
+        kk = min<int>(seg_blks, clp->bpt);
+        i_lin_blks = i_sg_it.linear_for_n_blks(kk);
+        o_lin_blks = o_sg_it.linear_for_n_blks(kk);
+        num = min<int>(i_lin_blks, o_lin_blks);
+        if (num <= 0) {
+            res = 0;
+            pr2serr_lk("[%d] %s: min(i_lin_blks=%d o_lin_blks=%d) < 1\n", id,
+                       __func__, i_lin_blks, o_lin_blks);
+            break;
+        }
+
+        /* First build the command/request for the read-side*/
+        cdbsz = clp->cdbsz_in;
+        res = sg_build_scsi_cdb(t_cdb.data(), cdbsz, num,
+                                i_sg_it.current_lba(), false, false,
+                                iflagsp->fua, iflagsp->dpo, iflagsp->cdl);
+        if (res) {
+            pr2serr_lk("%s: t=%d: input sg_build_scsi_cdb() failed\n",
+                       __func__, id);
+            break;
+        } else if (vb > 3)
+            lk_print_command_len("input cdb: ", t_cdb.data(), cdbsz, true);
+        a_cdb.push_back(t_cdb);
+
+        t_v4p->guard = 'Q';
+        t_v4p->flags = iflags;
+        t_v4p->request_len = cdbsz;
+        t_v4p->response = (uint64_t)rep->sb;
+        t_v4p->max_response_len = sizeof(rep->sb);
+        t_v4p->usr_ptr = (uint64_t)&a_cdb[a_cdb.size() - 1];
+        t_v4p->din_xfer_len = num * rep->bs;
+        rep->a_mrq_din_blks += num;
+        t_v4p->dout_xfer_len = 0;
+        t_v4p->timeout = clp->cmd_timeout;
+        in_mrq_q_blks += num;
+        t_v4p->request_extra = mrq_pack_id_base + ++rep->mrq_pack_id_off;
+        clp->most_recent_pack_id.store(t_v4p->request_extra);
+        a_v4.push_back(t_v4);
+
+        /* Now build the command/request for write-side (WRITE or VERIFY) */
+        cdbsz = clp->cdbsz_out;
+        res = sg_build_scsi_cdb(t_cdb.data(), cdbsz, num,
+                                o_sg_it.current_lba(), clp->verify, true,
+                                oflagsp->fua, oflagsp->dpo, oflagsp->cdl);
+        if (res) {
+            pr2serr_lk("%s: t=%d: output sg_build_scsi_cdb() failed\n",
+                       __func__, id);
+            break;
+        } else if (vb > 3)
+            lk_print_command_len("output cdb: ", t_cdb.data(), cdbsz, true);
+        a_cdb.push_back(t_cdb);
+        t_v4p->guard = 'Q';
+        t_v4p->flags = oflags;
+        t_v4p->request_len = cdbsz;
+        t_v4p->response = (uint64_t)rep->sb;
+        t_v4p->max_response_len = sizeof(rep->sb);
+        t_v4p->usr_ptr = (uint64_t)&a_cdb[a_cdb.size() - 1];
+        t_v4p->din_xfer_len = 0;
+        t_v4p->dout_xfer_len = num * rep->bs;
+        rep->a_mrq_dout_blks += num;
+        t_v4p->timeout = clp->cmd_timeout;
+        out_mrq_q_blks += num;
+        t_v4p->request_extra = mrq_pack_id_base + ++rep->mrq_pack_id_off;
+        clp->most_recent_pack_id.store(t_v4p->request_extra);
+        a_v4.push_back(t_v4);
+
+        i_sg_it.add_blks(num);
+        o_sg_it.add_blks(num);
+    }
+
+    if (vb > 6) {
+        pr2serr_lk("%s: t=%d: a_v4 array contents:\n", __func__, id);
+        hex2stderr_lk((const uint8_t *)a_v4.data(),
+                      a_v4.size() * sizeof(struct sg_io_v4), 1);
+    }
+    if (rep->both_sg || rep->same_sg)
+        fd = rep->infd;         /* assume share to rep->outfd */
+    else {
+        pr2serr_lk("[%d] %s: why am I here? Want 2 sg devices\n", id,
+                   __func__);
+        res = -1;
+        goto fini;
+    }
+    num_mrq = a_v4.size();
+    a_v4p = a_v4.data();
+    res = 0;
+    ctl_v4.guard = 'Q';
+    ctl_v4.request_len = a_cdb.size() * max_cdb_sz;
+    ctl_v4.request = (uint64_t)a_cdb.data();
+    ctl_v4.max_response_len = sizeof(rep->sb);
+    ctl_v4.response = (uint64_t)rep->sb;
+    ctl_v4.flags = SGV4_FLAG_MULTIPLE_REQS | SGV4_FLAG_SHARE;
+    if (! (iflagsp->coe || oflagsp->coe))
+        ctl_v4.flags |= SGV4_FLAG_STOP_IF;
+    if ((! clp->verify) && clp->out_flags.order_wr)
+        ctl_v4.flags |= SGV4_FLAG_ORDERED_WR;
+    if (clp->mrq_polled)
+        ctl_v4.flags |= SGV4_FLAG_POLLED;
+    if (clp->in_flags.mout_if || clp->out_flags.mout_if) {
+        ctl_v4.flags |= SGV4_FLAG_META_OUT_IF;
+        if (num_mrq > 0)
+            a_v4[0].info = UINT32_MAX;
+    }
+    ctl_v4.dout_xferp = (uint64_t)a_v4.data();        /* request array */
+    ctl_v4.dout_xfer_len = a_v4.size() * sizeof(struct sg_io_v4);
+    ctl_v4.din_xferp = (uint64_t)a_v4.data();         /* response array */
+    ctl_v4.din_xfer_len = a_v4.size() * sizeof(struct sg_io_v4);
+    if (false /* allow_mrq_abort */) {
+        ctl_v4.request_extra = mrq_pack_id_base + ++rep->mrq_pack_id_off;
+        clp->most_recent_pack_id.store(ctl_v4.request_extra);
+    }
+
+    if (vb && vb_first_time.load()) {
+        pr2serr_lk("First controlling object output by ioctl(%s), flags: "
+                   "%s\n", iosub_str, sg_flags_str(ctl_v4.flags, b_len, b));
+        vb_first_time.store(false);
+    } else if (vb > 4)
+        pr2serr_lk("%s: >> Control object _before_ ioctl(%s):\n", __func__,
+                   iosub_str);
+    if (vb > 4) {
+        if (vb > 5)
+            hex2stderr_lk((const uint8_t *)&ctl_v4, sizeof(ctl_v4), 1);
+        v4hdr_out_lk(">> Control object before", &ctl_v4, id, false);
+    }
+
+try_again:
+    if (!after1 && (vb > 1)) {
+        after1 = true;
+        pr2serr_lk("%s: %s\n", __func__, mrq_svb_s);
+    }
+    res = ioctl(fd, SG_IOSUBMIT, &ctl_v4);
+    if (res < 0) {
+        err = errno;
+        if (E2BIG == err)
+                sg_take_snap(fd, id, true);
+        else if (EBUSY == err) {
+            ++num_ebusy;
+            std::this_thread::yield();/* allow another thread to progress */
+            goto try_again;
+        }
+        pr2serr_lk("%s: ioctl(%s, %s)-->%d, errno=%d: %s\n", __func__,
+                   iosub_str, sg_flags_str(ctl_v4.flags, b_len, b), res, err,
+                   strerror(err));
+        res = -err;
+        goto fini;
+    }
+    if (vb > 4) {
+        pr2serr_lk("%s: >> Control object after ioctl(%s) seg_blks=%d:\n",
+                   __func__, iosub_str, o_seg_blks);
+        if (vb > 5)
+            hex2stderr_lk((const uint8_t *)&ctl_v4, sizeof(ctl_v4), 1);
+        v4hdr_out_lk(">> Control object after", &ctl_v4, id, false);
+        if (vb > 5) {
+            for (k = 0; k < num_mrq; ++k) {
+                if ((vb > 6) || a_v4p[k].info) {
+                    snprintf(b, b_len, "a_v4[%d/%d]", k, num_mrq);
+                    v4hdr_out_lk(b, (a_v4p + k), id, true);
+                }
+            }
+        }
+    }
+    num_good = process_mrq_response(rep, &ctl_v4, a_v4p, num_mrq, in_fin_blks,
+                                    out_fin_blks, err_on_in);
+    if (vb > 2)
+        pr2serr_lk("%s: >>> seg_blks=%d, num_good=%d, in_q/fin blks=%u/%u;  "
+                   "out_q/fin blks=%u/%u\n", __func__, o_seg_blks, num_good,
+                   in_mrq_q_blks, in_fin_blks, out_mrq_q_blks, out_fin_blks);
+
+    if (clp->ese) {
+        int sres = ctl_v4.spare_out;
+
+        if (sres != 0) {
+            clp->reason_res.store(sg_convert_errno(sres));
+            pr2serr_lk("Exit due to secondary error [%d]\n", sres);
+            return -sres;
+        }
+    }
+    if (num_good < 0)
+        res = -ENODATA;
+    else {
+        rep->in_local_count += in_fin_blks;
+        rep->out_local_count += out_fin_blks;
+
+        if (num_good < num_mrq) {       /* reduced number completed */
+            int resid_blks = in_mrq_q_blks - in_fin_blks;
+
+            if (resid_blks > 0) {
+                rep->in_rem_count += resid_blks;
+                rep->stop_after_write = ! (err_on_in && clp->in_flags.coe);
+            }
+
+            resid_blks = out_mrq_q_blks - out_fin_blks;
+            if (resid_blks > 0) {
+                rep->out_rem_count += resid_blks;
+                rep->stop_after_write = ! ((! err_on_in) &&
+                                           clp->out_flags.coe);
+            }
+        }
+    }
+fini:
+    return res < 0 ? res : (min<int>(in_fin_blks, out_fin_blks));
+}
+
+#if 0
+/* Returns number found and (partially) processed. 'num' is the number of
+ * completions to wait for when > 0. When 'num' is zero check all inflight
+ * request on 'fd' and return quickly if none completed (i.e. don't wait)
+ * If error return negative errno and if no request inflight or waiting
+ * then return -9999 . */
+static int
+sg_blk_poll(int fd, int num)
+{
+    int res;
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    seip->sei_rd_mask |= SG_SEIM_BLK_POLL;
+    seip->sei_wr_mask |= SG_SEIM_BLK_POLL;
+    seip->num = (num < 0) ? 0 : num;
+    res = ioctl(fd, SG_SET_GET_EXTENDED, seip);
+    if (res < 0) {
+        pr2serr_lk("%s: SG_SET_GET_EXTENDED(BLK_POLL) error: %s\n",
+                   __func__, strerror(errno));
+        return res;
+    }
+    return (seip->num == -1) ? -9999 : seip->num;
+}
+#endif
+
+/* Returns the number of times 'ch' is found in string 's' given the
+ * string's length. */
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+/* Returns the number of times either 'ch1' or 'ch2' is found in
+ * string 's' given the string's length. */
+int
+num_either_ch_in_str(const char * s, int slen, int ch1, int ch2)
+{
+    int k;
+    int res = 0;
+
+    while (--slen >= 0) {
+        k = s[slen];
+        if ((ch1 == k) || (ch2 == k))
+            ++res;
+    }
+    return res;
+}
+
+/* Allocates and then populates a scatter gether list (array) and returns
+ * it via *sgl_pp. Return of 0 is okay, else error number (in which case
+ * NULL is written to *sgl_pp) . */
+static int
+skip_seek(struct global_collection *clp, const char * key, const char * buf,
+          bool is_skip, bool ignore_verbose)
+{
+    bool def_hex = false;
+    int len;
+    int vb = clp->verbose;  /* needs to appear before skip/seek= on cl */
+    int64_t ll;
+    const char * cp;
+    class scat_gath_list & either_list = is_skip ? clp->i_sgl : clp->o_sgl;
+
+    if (ignore_verbose)
+        vb = 0;
+    len = (int)strlen(buf);
+    if ((('-' == buf[0]) && (1 == len)) || ((len > 1) && ('@' == buf[0])) ||
+        ((len > 2) && ('H' == toupper(buf[0])) && ('@' == buf[1]))) {
+        if ('H' == toupper(buf[0])) {
+            cp = buf + 2;
+            def_hex = true;
+        } else if ('-' == buf[0])
+            cp = buf;
+        else
+            cp = buf + 1;
+        if (! either_list.load_from_file(cp, def_hex, clp->flexible, true)) {
+            pr2serr("bad argument to '%s=' [err=%d]\n", key,
+                    either_list.m_errno);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    } else if (num_either_ch_in_str(buf, len, ',', ' ') > 0) {
+        if (! either_list.load_from_cli(buf, vb > 0)) {
+            pr2serr("bad command line argument to '%s='\n", key);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    } else {    /* single number on command line (e.g. skip=1234) */
+        ll = sg_get_llnum(buf);
+        if ((ll < 0) || (ll > MAX_COUNT_SKIP_SEEK)) {
+            pr2serr("bad argument to '%s='\n", key);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        either_list.append_1or(0, ll);
+        if (vb > 1)
+            pr2serr("%s: singleton, half a degenerate sgl element\n", key);
+    }
+
+    either_list.sum_scan(key, vb > 3 /* bool show_sgl */, vb > 1);
+    return 0;
+}
+
+static bool
+process_flags(const char * arg, struct flags_t * fp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no flag found\n");
+        return false;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "00"))
+            fp->zero = true;
+        else if (0 == strcmp(cp, "append"))
+            fp->append = true;
+        else if (0 == strcmp(cp, "coe"))
+            fp->coe = true;
+        else if (0 == strcmp(cp, "dio"))
+            fp->dio = true;
+        else if (0 == strcmp(cp, "direct"))
+            fp->direct = true;
+        else if (0 == strcmp(cp, "dpo"))
+            fp->dpo = true;
+        else if (0 == strcmp(cp, "dsync"))
+            fp->dsync = true;
+        else if (0 == strcmp(cp, "excl"))
+            fp->excl = true;
+        else if (0 == strcmp(cp, "ff"))
+            fp->ff = true;
+        else if (0 == strcmp(cp, "fua"))
+            fp->fua = true;
+        else if (0 == strcmp(cp, "hipri"))
+            fp->polled = true;
+        else if (0 == strcmp(cp, "masync"))
+            fp->masync = true;
+        else if (0 == strcmp(cp, "mmap"))
+            ++fp->mmap;         /* mmap > 1 stops munmap() being called */
+        else if (0 == strcmp(cp, "nocreat"))
+            fp->nocreat = true;
+        else if (0 == strcmp(cp, "nodur"))
+            fp->no_dur = true;
+        else if (0 == strcmp(cp, "no_dur"))
+            fp->no_dur = true;
+        else if (0 == strcmp(cp, "no-dur"))
+            fp->no_dur = true;
+        else if (0 == strcmp(cp, "nothresh"))
+            fp->no_thresh = true;
+        else if (0 == strcmp(cp, "no_thresh"))
+            fp->no_thresh = true;
+        else if (0 == strcmp(cp, "no-thresh"))
+            fp->no_thresh = true;
+        else if (0 == strcmp(cp, "noxfer"))
+            ;           /* accept but ignore */
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else if (0 == strcmp(cp, "ordered"))
+            fp->order_wr = true;
+        else if (0 == strcmp(cp, "order"))
+            fp->order_wr = true;
+        else if (0 == strcmp(cp, "polled"))
+            fp->polled = true;
+        else if (0 == strcmp(cp, "qhead"))
+            fp->qhead = true;
+        else if (0 == strcmp(cp, "qtail"))
+            fp->qtail = true;
+        else if (0 == strcmp(cp, "random"))
+            fp->random = true;
+        else if ((0 == strcmp(cp, "mout_if")) || (0 == strcmp(cp, "mout-if")))
+            fp->mout_if = true;
+        else if ((0 == strcmp(cp, "same_fds")) ||
+                 (0 == strcmp(cp, "same-fds")))
+            fp->same_fds = true;
+        else if (0 == strcmp(cp, "serial"))
+            fp->serial = true;
+        else if (0 == strcmp(cp, "swait"))
+            ;           /* accept but ignore */
+        else if (0 == strcmp(cp, "wq_excl"))
+            fp->wq_excl = true;
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return false;
+        }
+        cp = np;
+    } while (cp);
+    return true;
+}
+
+/* Process arguments given to 'conv=" option. Returns 0 on success,
+ * 1 on error. */
+static int
+process_conv(const char * arg, struct flags_t * ifp, struct flags_t * ofp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no conversions found\n");
+        return 1;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "nocreat"))
+            ofp->nocreat = true;
+        else if (0 == strcmp(cp, "noerror"))
+            ifp->coe = true;         /* will still fail on write error */
+        else if (0 == strcmp(cp, "notrunc"))
+            ;         /* this is the default action of sg_dd so ignore */
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else if (0 == strcmp(cp, "sync"))
+            ;   /* dd(susv4): pad errored block(s) with zeros but sg_dd does
+                 * that by default. Typical dd use: 'conv=noerror,sync' */
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return 1;
+        }
+        cp = np;
+    } while (cp);
+    return 0;
+}
+
+#define STR_SZ 1024
+#define INOUTF_SZ 512
+
+static int
+parse_cmdline_sanity(int argc, char * argv[], struct global_collection * clp,
+                     char * outregf)
+{
+    bool contra = false;
+    bool verbose_given = false;
+    bool version_given = false;
+    bool verify_given = false;
+    bool bpt_given = false;
+    int ibs = 0;
+    int obs = 0;
+    int ret = 0;
+    int k, keylen, n, res;
+    char str[STR_SZ];
+    char * key;
+    char * buf;
+    char * skip_buf = NULL;
+    char * seek_buf = NULL;
+    const char * cp;
+    const char * ccp;
+
+    for (k = 1; k < argc; k++) {
+        if (argv[k]) {
+            strncpy(str, argv[k], STR_SZ);
+            str[STR_SZ - 1] = '\0';
+        } else
+            continue;
+
+        for (key = str, buf = key; *buf && *buf != '=';)
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        keylen = strlen(key);
+        if (0 == strcmp(key, "bpt")) {
+            clp->bpt = sg_get_num(buf);
+            if ((clp->bpt < 0) || (clp->bpt > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'bpt='\n", my_name);
+                goto syn_err;
+            }
+            bpt_given = true;
+        } else if (0 == strcmp(key, "bs")) {
+            clp->bs = sg_get_num(buf);
+            if ((clp->bs < 0) || (clp->bs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'bs='\n", my_name);
+                goto syn_err;
+            }
+        } else if (0 == strcmp(key, "cdbsz")) {
+            ccp = strchr(buf, ',');
+            n = sg_get_num(buf);
+            if ((n < 0) || (n > 32)) {
+                pr2serr("%s: bad argument to 'cdbsz=', expect 6, 10, 12 or "
+                        "16\n", my_name);
+                goto syn_err;
+            }
+            clp->cdbsz_in = n;
+            if (ccp) {
+                n = sg_get_num(ccp + 1);
+                if ((n < 0) || (n > 32)) {
+                    pr2serr("%s: bad second argument to 'cdbsz=', expect 6, "
+                            "10, 12 or 16\n", my_name);
+                    goto syn_err;
+                }
+            }
+            clp->cdbsz_out = n;
+            clp->cdbsz_given = true;
+        } else if (0 == strcmp(key, "cdl")) {
+            ccp = strchr(buf, ',');
+            n = sg_get_num(buf);
+            if ((n < 0) || (n > 7)) {
+                pr2serr("%s: bad argument to 'cdl=', expect 0 to 7\n",
+                         my_name);
+                goto syn_err;
+            }
+            clp->in_flags.cdl = n;
+            if (ccp) {
+                n = sg_get_num(ccp + 1);
+                if ((n < 0) || (n > 7)) {
+                    pr2serr("%s: bad second argument to 'cdl=', expect 0 "
+                            "to 7\n", my_name);
+                    goto syn_err;
+                }
+            }
+            clp->out_flags.cdl = n;
+            clp->cdl_given = true;
+        } else if (0 == strcmp(key, "coe")) {
+            /* not documented, for compat with sgh_dd */
+            clp->in_flags.coe = !! sg_get_num(buf);
+            clp->out_flags.coe = clp->in_flags.coe;
+        } else if (0 == strcmp(key, "conv")) {
+            if (process_conv(buf, &clp->in_flags, &clp->out_flags)) {
+                pr2serr("%s: bad argument to 'conv='\n", my_name);
+                goto syn_err;
+            }
+        } else if (0 == strcmp(key, "count")) {
+            if (clp->count_given) {
+                pr2serr("second 'count=' argument detected, only one "
+                        "please\n");
+                contra = true;
+                goto syn_err;
+            }
+            if (0 != strcmp("-1", buf)) {
+                clp->dd_count = sg_get_llnum(buf);
+                if ((clp->dd_count < 0) ||
+                    (clp->dd_count > MAX_COUNT_SKIP_SEEK)) {
+                    pr2serr("%sbad argument to 'count='\n", my_name);
+                    goto syn_err;
+                }
+            }   /* treat 'count=-1' as calculate count (same as not given) */
+            clp->count_given = true;
+        } else if (0 == strcmp(key, "dio")) {
+            clp->in_flags.dio = !! sg_get_num(buf);
+            clp->out_flags.dio = clp->in_flags.dio;
+        } else if (0 == strcmp(key, "elemsz_kb")) {
+            n = sg_get_num(buf);
+            if ((n < 1) || (n > (MAX_BPT_VALUE / 1024))) {
+                pr2serr("elemsz_kb=EKB wants an integer > 0\n");
+                goto syn_err;
+            }
+            if (n & (n - 1)) {
+                pr2serr("elemsz_kb=EKB wants EKB to be power of 2\n");
+                goto syn_err;
+            }
+            clp->elem_sz = n * 1024;
+        } else if (0 == strcmp(key, "ese")) {
+            n = sg_get_num(buf);
+            if (n < 0) {
+                pr2serr("ese= wants 0 (default) or 1\n");
+                goto syn_err;
+            }
+            clp->ese = !!n;
+        } else if (0 == strcmp(key, "fua")) {
+            n = sg_get_num(buf);
+            if (n & 1)
+                clp->out_flags.fua = true;
+            if (n & 2)
+                clp->in_flags.fua = true;
+        } else if (0 == strcmp(key, "ibs")) {
+            ibs = sg_get_num(buf);
+            if ((ibs < 0) || (ibs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'ibs='\n", my_name);
+                goto syn_err;
+            }
+        } else if (0 == strcmp(key, "if")) {
+            if (clp->inf_v.size() > 0) {
+                pr2serr("Second 'if=' argument??\n");
+                goto syn_err;
+            } else {
+                cp = buf;
+                while ((ccp = strchr(cp, ','))) {
+                    clp->inf_v.push_back(string(cp , ccp - cp));
+                    cp = ccp + 1;
+                }
+                clp->inf_v.push_back(string(cp , strlen(cp)));
+            }
+        } else if (0 == strcmp(key, "iflag")) {
+            if (! process_flags(buf, &clp->in_flags)) {
+                pr2serr("%sbad argument to 'iflag='\n", my_name);
+                goto syn_err;
+            }
+        } else if ((0 == strcmp(key, "hipri")) ||
+                   (0 == strcmp(key, "mrq")) ||
+                   (0 == strcmp(key, "polled"))) {
+            if (isdigit(buf[0]))
+                cp = buf;
+            else {
+                pr2serr("%sonly mrq=NRQS or polled=NRQS which is a number "
+                        "allowed here\n", my_name);
+                goto syn_err;
+            }
+            clp->mrq_num = sg_get_num(cp);
+            if (clp->mrq_num < 0) {
+                pr2serr("%sbad argument to 'mrq='\n", my_name);
+                goto syn_err;
+            }
+            if (0 == clp->mrq_num) {
+                clp->mrq_eq_0 = true;
+                clp->mrq_num = 1;
+                pr2serr("note: send single, non-mrq commands\n");
+            }
+            if ('m' != key[0])
+                clp->mrq_polled = true;
+        } else if ((0 == strcmp(key, "no_waitq")) ||
+                   (0 == strcmp(key, "no-waitq"))) {
+            n = sg_get_num(buf);
+            if (-1 == n) {
+                pr2serr("%sbad argument to 'no_waitq=', expect 0 or 1\n",
+                        my_name);
+                goto syn_err;
+            }
+            clp->in_flags.no_waitq = true;
+            clp->out_flags.no_waitq = true;
+        } else if (0 == strcmp(key, "obs")) {
+            obs = sg_get_num(buf);
+            if ((obs < 0) || (obs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'obs='\n", my_name);
+                goto syn_err;
+            }
+        } else if (strcmp(key, "ofreg") == 0) {
+            if ('\0' != outregf[0]) {
+                pr2serr("Second OFREG argument??\n");
+                contra = true;
+                goto syn_err;
+            } else {
+                memcpy(outregf, buf, INOUTF_SZ);
+                outregf[INOUTF_SZ - 1] = '\0';  /* noisy compiler */
+            }
+        } else if (strcmp(key, "of") == 0) {
+            if (clp->outf_v.size() > 0) {
+                pr2serr("Second 'of=' argument??\n");
+                goto syn_err;
+            } else {
+                cp = buf;
+                while ((ccp = strchr(cp, ','))) {
+                    clp->outf_v.push_back(string(cp , ccp - cp));
+                    cp = ccp + 1;
+                }
+                clp->outf_v.push_back(string(cp , strlen(cp)));
+            }
+        } else if (0 == strcmp(key, "oflag")) {
+            if (! process_flags(buf, &clp->out_flags)) {
+                pr2serr("%sbad argument to 'oflag='\n", my_name);
+                goto syn_err;
+            }
+        } else if (0 == strcmp(key, "sdt")) {
+            ccp = strchr(buf, ',');
+            n = sg_get_num(buf);
+            if (n < 0) {
+                pr2serr("%sbad argument to 'sdt=CRT[,ICT]'\n", my_name);
+                goto syn_err;
+            }
+            clp->sdt_crt = n;
+            if (ccp) {
+                n = sg_get_num(ccp + 1);
+                if (n < 0) {
+                    pr2serr("%sbad 2nd argument to 'sdt=CRT,ICT'\n",
+                            my_name);
+                    goto syn_err;
+                }
+                clp->sdt_ict = n;
+            }
+        } else if (0 == strcmp(key, "seek")) {
+            n = strlen(buf);
+            if (n < 1) {
+                pr2serr("%sneed argument to 'seek='\n", my_name);
+                goto syn_err;
+            }
+            seek_buf = (char *)calloc(n + 16, 1);
+            if (NULL == seek_buf)
+                goto syn_err;
+            memcpy(seek_buf, buf, n + 1);
+        } else if (0 == strcmp(key, "skip")) {
+            n = strlen(buf);
+            if (n < 1) {
+                pr2serr("%sneed argument to 'skip='\n", my_name);
+                goto syn_err;
+            }
+            skip_buf = (char *)calloc(n + 16, 1);
+            if (NULL == skip_buf)
+                goto syn_err;
+            memcpy(skip_buf, buf, n + 1);
+        } else if (0 == strcmp(key, "sync"))
+            do_sync = !! sg_get_num(buf);
+        else if (0 == strcmp(key, "thr")) {
+            num_threads = sg_get_num(buf);
+            if ((num_threads < 0) || (num_threads > MAX_BPT_VALUE)) {
+                pr2serr("%sneed argument to 'skip='\n", my_name);
+                goto syn_err;
+            }
+        } else if (0 == strcmp(key, "time")) {
+            ccp = strchr(buf, ',');
+            do_time = sg_get_num(buf);
+            if (do_time < 0) {
+                pr2serr("%sbad argument to 'time=0|1|2'\n", my_name);
+                goto syn_err;
+            }
+            if (ccp) {
+                n = sg_get_num(ccp + 1);
+                if ((n < 0) || (n > (MAX_BPT_VALUE / 1000))) {
+                    pr2serr("%sbad argument to 'time=0|1|2,TO'\n", my_name);
+                    goto syn_err;
+                }
+                clp->cmd_timeout = n ? (n * 1000) : DEF_TIMEOUT;
+            }
+        } else if (0 == strncmp(key, "verb", 4))
+            clp->verbose = sg_get_num(buf);
+        else if ((keylen > 1) && ('-' == key[0]) && ('-' != key[1])) {
+            res = 0;
+            n = num_chs_in_str(key + 1, keylen - 1, 'd');
+            clp->dry_run += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'h');
+            clp->help += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'p');
+            if (n > 0)
+                clp->prefetch = true;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'v');
+            if (n > 0)
+                verbose_given = true;
+            clp->verbose += n;   /* -v  ---> --verbose */
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'V');
+            if (n > 0)
+                version_given = true;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'x');
+            if (n > 0)
+                verify_given = true;
+            res += n;
+
+            if (res < (keylen - 1)) {
+                pr2serr("Unrecognised short option in '%s', try '--help'\n",
+                        key);
+                goto syn_err;
+            }
+        } else if ((0 == strncmp(key, "--dry-run", 9)) ||
+                   (0 == strncmp(key, "--dry_run", 9)))
+            ++clp->dry_run;
+        else if ((0 == strncmp(key, "--help", 6)) ||
+                   (0 == strcmp(key, "-?")))
+            ++clp->help;
+        else if ((0 == strncmp(key, "--prefetch", 10)) ||
+                 (0 == strncmp(key, "--pre-fetch", 11)))
+            clp->prefetch = true;
+        else if (0 == strncmp(key, "--verb", 6)) {
+            verbose_given = true;
+            ++clp->verbose;      /* --verbose */
+        } else if (0 == strncmp(key, "--veri", 6))
+            verify_given = true;
+        else if (0 == strncmp(key, "--vers", 6))
+            version_given = true;
+        else {
+            pr2serr("Unrecognized option '%s'\n", key);
+            pr2serr("For more information use '--help'\n");
+            goto syn_err;
+        }
+    }   /* end of parsing for loop */
+
+    if (skip_buf) {
+        res = skip_seek(clp, "skip", skip_buf, true /* skip */, false);
+        free(skip_buf);
+        skip_buf = NULL;
+        if (res) {
+            pr2serr("%sbad argument to 'seek='\n", my_name);
+            goto syn_err;
+        }
+    }
+    if (seek_buf) {
+        res = skip_seek(clp, "seek", seek_buf, false /* skip */, false);
+        free(seek_buf);
+        seek_buf = NULL;
+        if (res) {
+            pr2serr("%sbad argument to 'seek='\n", my_name);
+            goto syn_err;
+        }
+    }
+    /* heap usage should be all freed up now */
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        clp->verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        clp->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", clp->verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("%s%s\n", my_name, version_str);
+        ret = SG_LIB_OK_FALSE;
+        goto oth_err;
+    }
+    if (clp->help > 0) {
+        usage(clp->help);
+        ret = SG_LIB_OK_FALSE;
+        goto oth_err;
+    }
+    if (clp->bs <= 0) {
+        clp->bs = DEF_BLOCK_SIZE;
+        pr2serr("Assume default 'bs' ((logical) block size) of %d bytes\n",
+                clp->bs);
+    }
+    if (verify_given) {
+        pr2serr("Doing verify/cmp rather than copy\n");
+        clp->verify = true;
+    }
+    if ((ibs && (ibs != clp->bs)) || (obs && (obs != clp->bs))) {
+        pr2serr("If 'ibs' or 'obs' given must be same as 'bs'\n");
+        usage(0);
+        goto syn_err;
+    }
+    if (clp->out_flags.append) {
+        if ((clp->o_sgl.lowest_lba > 0) ||
+            (clp->o_sgl.linearity != SGL_LINEAR)) {
+            pr2serr("Can't use both append and seek switches\n");
+            goto syn_err;
+        }
+        if (verify_given) {
+            pr2serr("Can't use both append and verify switches\n");
+            goto syn_err;
+        }
+    }
+    if (clp->bpt < 1) {
+        pr2serr("bpt must be greater than 0\n");
+        goto syn_err;
+    }
+    if (clp->in_flags.mmap && clp->out_flags.mmap) {
+        pr2serr("mmap flag on both IFILE and OFILE doesn't work\n");
+        goto syn_err;
+    }
+    /* defaulting transfer size to 128*2048 for CD/DVDs is too large
+     * for the block layer in lk 2.6 and results in an EIO on the
+     * SG_IO ioctl. So reduce it in that case. */
+    if ((clp->bs >= 2048) && (! bpt_given))
+        clp->bpt = DEF_BLOCKS_PER_2048TRANSFER;
+    if (clp->in_flags.order_wr && (! clp->out_flags.order_wr))
+        pr2serr("Warning iflag=order is ignored, use with oflag=\n");
+    if ((num_threads < 1) || (num_threads > MAX_NUM_THREADS)) {
+        pr2serr("too few or too many threads requested\n");
+        usage(1);
+        goto syn_err;
+    }
+    clp->unit_nanosec = (do_time > 1) || !!getenv("SG3_UTILS_LINUX_NANO");
+    return 0;
+
+syn_err:
+    if (seek_buf)
+        free(seek_buf);
+    if (skip_buf)
+        free(skip_buf);
+    return contra ? SG_LIB_CONTRADICT : SG_LIB_SYNTAX_ERROR;
+oth_err:
+    if (seek_buf)
+        free(seek_buf);
+    if (skip_buf)
+        free(skip_buf);
+    return ret;
+}
+
+static int
+calc_count(struct global_collection * clp, const char * inf,
+           int64_t & in_num_sect, const char * outf, int64_t & out_num_sect)
+{
+    int in_sect_sz, out_sect_sz, res;
+
+    if (clp->dd_count < 0) {
+        in_num_sect = -1;
+        out_num_sect = -1;
+    }
+    if (FT_SG == clp->in_type) {
+        res = scsi_read_capacity(clp->in0fd, &in_num_sect, &in_sect_sz);
+        if (2 == res) {
+            pr2serr("Unit attention, media changed(in), continuing\n");
+            res = scsi_read_capacity(clp->in0fd, &in_num_sect,
+                                     &in_sect_sz);
+        }
+        if (0 != res) {
+            if (res == SG_LIB_CAT_INVALID_OP)
+                pr2serr("read capacity not supported on %s\n", inf);
+            else if (res == SG_LIB_CAT_NOT_READY)
+                pr2serr("read capacity failed, %s not ready\n", inf);
+            else
+                pr2serr("Unable to read capacity on %s\n", inf);
+            return SG_LIB_FILE_ERROR;
+        } else if (clp->bs != in_sect_sz) {
+            pr2serr(">> warning: logical block size on %s confusion: "
+                    "bs=%d, device claims=%d\n", clp->infp, clp->bs,
+                    in_sect_sz);
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+    if (FT_SG == clp->out_type) {
+        res = scsi_read_capacity(clp->out0fd, &out_num_sect, &out_sect_sz);
+        if (2 == res) {
+            pr2serr("Unit attention, media changed(out), continuing\n");
+            res = scsi_read_capacity(clp->out0fd, &out_num_sect,
+                                     &out_sect_sz);
+        }
+        if (0 != res) {
+            if (res == SG_LIB_CAT_INVALID_OP)
+                pr2serr("read capacity not supported on %s\n", outf);
+            else if (res == SG_LIB_CAT_NOT_READY)
+                pr2serr("read capacity failed, %s not ready\n", outf);
+            else
+                pr2serr("Unable to read capacity on %s\n", outf);
+            out_num_sect = -1;
+            return SG_LIB_FILE_ERROR;
+        } else if (clp->bs != out_sect_sz) {
+            pr2serr(">> warning: logical block size on %s confusion: "
+                    "bs=%d, device claims=%d\n", clp->outfp, clp->bs,
+                    out_sect_sz);
+            return SG_LIB_FILE_ERROR;
+        }
+    }
+
+    if (clp->dd_count < 0) {
+        if (FT_SG == clp->in_type)
+            ;
+        else if (FT_BLOCK == clp->in_type) {
+            if (0 != read_blkdev_capacity(clp->in0fd, &in_num_sect,
+                                          &in_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", inf);
+                in_num_sect = -1;
+            }
+            if (clp->bs != in_sect_sz) {
+                pr2serr("logical block size on %s confusion; bs=%d, from "
+                        "device=%d\n", inf, clp->bs, in_sect_sz);
+                in_num_sect = -1;
+            }
+        }
+
+        if (FT_SG == clp->out_type)
+            ;
+        else if (FT_BLOCK == clp->out_type) {
+            if (0 != read_blkdev_capacity(clp->out0fd, &out_num_sect,
+                                          &out_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", outf);
+                out_num_sect = -1;
+            }
+            if (clp->bs != out_sect_sz) {
+                pr2serr("logical block size on %s confusion: bs=%d, from "
+                        "device=%d\n", outf, clp->bs, out_sect_sz);
+                out_num_sect = -1;
+            }
+        }
+    }
+    return 0;
+}
+
+static int
+do_count_work(struct global_collection * clp, const char * inf,
+              int64_t & in_num_sect, const char * outf,
+              int64_t & out_num_sect)
+{
+    int res;
+    class scat_gath_list * isglp = &clp->i_sgl;
+    class scat_gath_list * osglp = &clp->o_sgl;
+
+    res = calc_count(clp, inf, in_num_sect, outf, out_num_sect);
+    if (res)
+        return res;
+
+    if ((-1 == in_num_sect) && (FT_OTHER == clp->in_type)) {
+        in_num_sect = clp->in_st_size / clp->bs;
+        if (clp->in_st_size % clp->bs) {
+            ++in_num_sect;
+            pr2serr("Warning: the file size of %s is not a multiple of BS "
+                    "[%d]\n", inf, clp->bs);
+        }
+    }
+    if ((in_num_sect > 0) && (isglp->high_lba_p1 > in_num_sect)) {
+        pr2serr("%shighest LBA [0x%" PRIx64 "] exceeds input length: %"
+                PRIx64 " blocks\n", my_name, isglp->high_lba_p1 - 1,
+                in_num_sect);
+        return SG_LIB_CAT_OTHER;
+    }
+    if ((out_num_sect > 0) && (osglp->high_lba_p1 > out_num_sect)) {
+        pr2serr("%shighest LBA [0x%" PRIx64 "] exceeds output length: %"
+                PRIx64 " blocks\n", my_name, osglp->high_lba_p1 - 1,
+                out_num_sect);
+        return SG_LIB_CAT_OTHER;
+    }
+
+    if (isglp->sum_hard || osglp->sum_hard) {
+        int64_t ccount;
+
+        if (isglp->sum_hard && osglp->sum_hard) {
+            if (isglp->sum != osglp->sum) {
+                pr2serr("%stwo hard sgl_s, sum of blocks differ: in=%" PRId64
+                        ", out=%" PRId64 "\n", my_name , isglp->sum,
+                        osglp->sum);
+                return SG_LIB_CAT_OTHER;
+            }
+            ccount = isglp->sum;
+        } else if (isglp->sum_hard) {
+            if (osglp->sum > isglp->sum) {
+                pr2serr("%soutput sgl already too many blocks [%" PRId64
+                        "]\n", my_name, osglp->sum);
+                return SG_LIB_CAT_OTHER;
+            }
+            if (osglp->linearity != SGL_NON_MONOTONIC)
+                osglp->append_1or(isglp->sum - osglp->sum);
+            else {
+                pr2serr("%soutput sgl non-montonic: can't extend\n",
+                        my_name);
+                return SG_LIB_CAT_OTHER;
+            }
+            ccount = isglp->sum;
+        } else {        /* only osglp hard */
+            if (isglp->sum > osglp->sum) {
+                pr2serr("%sinput sgl already too many blocks [%" PRId64
+                        "]\n", my_name, isglp->sum);
+                return SG_LIB_CAT_OTHER;
+            }
+            if (isglp->linearity != SGL_NON_MONOTONIC)
+                isglp->append_1or(osglp->sum - isglp->sum);
+            else {
+                pr2serr("%sinput sgl non-monotonic: can't extend\n",
+                        my_name);
+                return SG_LIB_CAT_OTHER;
+            }
+            ccount = osglp->sum;
+        }
+        if (SG_COUNT_INDEFINITE == clp->dd_count)
+            clp->dd_count = ccount;
+        else if (ccount != clp->dd_count) {
+            pr2serr("%scount=COUNT disagrees with scatter gather list "
+                    "length [%" PRId64 "]\n", my_name, ccount);
+            return SG_LIB_CAT_OTHER;
+        }
+    } else if (clp->dd_count != 0) { /* and both input and output are soft */
+        int64_t iposs = INT64_MAX;
+        int64_t oposs = INT64_MAX;
+
+        if (clp->dd_count > 0) {
+            if (isglp->sum > clp->dd_count) {
+                pr2serr("%sskip sgl sum [%" PRId64 "] exceeds COUNT\n",
+                        my_name, isglp->sum);
+                return SG_LIB_CAT_OTHER;
+            }
+            if (osglp->sum > clp->dd_count) {
+                pr2serr("%sseek sgl sum [%" PRId64 "] exceeds COUNT\n",
+                        my_name, osglp->sum);
+                return SG_LIB_CAT_OTHER;
+            }
+            goto fini;
+        }
+
+        /* clp->dd_count == SG_COUNT_INDEFINITE */
+        if (in_num_sect > 0)
+            iposs = in_num_sect + isglp->sum - isglp->high_lba_p1;
+        if (out_num_sect > 0)
+            oposs = out_num_sect + osglp->sum - osglp->high_lba_p1;
+        clp->dd_count = iposs < oposs ? iposs : oposs;
+        if (INT64_MAX == clp->dd_count) {
+            pr2serr("%scan't deduce count=COUNT, please supply one\n",
+                    my_name);
+            return SG_LIB_CAT_OTHER;
+        }
+        if (isglp->sum > clp->dd_count) {
+            pr2serr("%sdeduced COUNT [%" PRId64 "] exceeds skip sgl sum\n",
+                    my_name, clp->dd_count);
+            return SG_LIB_CAT_OTHER;
+        }
+        if (osglp->sum > clp->dd_count) {
+            pr2serr("%sdeduced COUNT [%" PRId64 "] exceeds seek sgl sum\n",
+                    my_name, clp->dd_count);
+            return SG_LIB_CAT_OTHER;
+        }
+    }
+    if (clp->dd_count == 0)
+        return 0;
+fini:
+    if (clp->dd_count > isglp->sum)
+        isglp->append_1or(clp->dd_count - isglp->sum);
+    if (clp->dd_count > osglp->sum)
+        osglp->append_1or(clp->dd_count - osglp->sum);
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool fail_after_cli = false;
+    bool ifile_given = true;
+    // char inf[INOUTF_SZ];
+    // char outf[INOUTF_SZ];
+    char outregf[INOUTF_SZ];
+    int res, k, err;
+    size_t num_ifiles, num_ofiles, num_slices, inf0_sz;
+    int64_t in_num_sect = -1;
+    int64_t out_num_sect = -1;
+    const char * ccp = NULL;
+    const char * cc2p;
+    struct global_collection * clp = &gcoll;
+    thread sig_listen_thr;
+    vector<thread> work_thr_v;
+    vector<thread> listen_thr_v;
+    char ebuff[EBUFF_SZ];
+#if 0   /* SG_LIB_ANDROID */
+    struct sigaction actions;
+
+    memset(&actions, 0, sizeof(actions));
+    sigemptyset(&actions.sa_mask);
+    actions.sa_flags = 0;
+    actions.sa_handler = thread_exit_handler;
+    sigaction(SIGUSR1, &actions, NULL);
+    sigaction(SIGUSR2, &actions, NULL);
+#endif
+    /* memset(clp, 0, sizeof(*clp)); */
+    clp->dd_count = SG_COUNT_INDEFINITE;
+    clp->bpt = DEF_BLOCKS_PER_TRANSFER;
+    clp->cmd_timeout = DEF_TIMEOUT;
+    clp->sdt_ict = DEF_SDT_ICT_MS;
+    clp->sdt_crt = DEF_SDT_CRT_SEC;
+    clp->in_type = FT_FIFO;
+    /* change dd's default: if of=OFILE not given, assume /dev/null */
+    clp->out_type = FT_DEV_NULL;
+    clp->cdbsz_in = DEF_SCSI_CDB_SZ;
+    clp->cdbsz_out = DEF_SCSI_CDB_SZ;
+    clp->mrq_num = DEF_MRQ_NUM;
+    // inf[0] = '\0';
+    // outf[0] = '\0';
+    outregf[0] = '\0';
+    fetch_sg_version();
+    if (sg_version >= 40045)
+        sg_version_ge_40045 = true;
+    else {
+        pr2serr(">>> %srequires an sg driver version of 4.0.45 or later\n\n",
+                my_name);
+        fail_after_cli = true;
+    }
+
+    res = parse_cmdline_sanity(argc, argv, clp, outregf);
+    if (SG_LIB_OK_FALSE == res)
+        return 0;
+    if (res)
+        return res;
+    if (fail_after_cli) {
+        pr2serr("%scommand line parsing was okay but sg driver is too old\n",
+                my_name);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    install_handler(SIGINT, interrupt_handler);
+    install_handler(SIGQUIT, interrupt_handler);
+    install_handler(SIGPIPE, interrupt_handler);
+    install_handler(SIGUSR1, siginfo_handler);
+    install_handler(SIGUSR2, siginfo2_handler);
+
+    num_ifiles = clp->inf_v.size();
+    num_ofiles = clp->outf_v.size();
+    if (num_ifiles > MAX_SLICES) {
+        pr2serr("%sonly support %d slices but given %zd IFILEs\n", my_name,
+                MAX_SLICES, num_ifiles);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (num_ofiles > MAX_SLICES) {
+        pr2serr("%sonly support %d slices but given %zd OFILEs\n", my_name,
+                MAX_SLICES, num_ifiles);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (0 == num_ofiles) {
+        if (0 == num_ifiles) {
+            pr2serr("%sexpect either if= or of= to be given\n", my_name);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        for (k = 0; k < (int)num_ifiles; ++k)
+            clp->outf_v.push_back("."); /* same as /dev/null */
+    }
+    if (0 == num_ifiles) {
+        ifile_given = false;
+        for (k = 0; k < (int)num_ofiles; ++k)
+            clp->inf_v.push_back("");
+    }
+    if ((num_ifiles > 1) && (num_ofiles > 1) && (num_ifiles != num_ofiles)) {
+        pr2serr("%snumber of IFILEs [%zd] and number of OFILEs [%zd] > 1 "
+                "and unequal\n", my_name, num_ifiles, num_ofiles);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((num_ifiles > 1) && (1 == num_ofiles)) {
+        /* if many IFILEs and one OFILE, replicate OFILE till same size */
+        for (k = 1; k < (int)num_ifiles; ++k)
+            clp->outf_v.push_back(clp->outf_v[0]);
+        num_ofiles = clp->outf_v.size();
+    } else if ((num_ofiles > 1) && (1 == num_ifiles)) {
+        /* if many OFILEs and one IFILE, replicate IFILE till same size */
+        for (k = 1; k < (int)num_ofiles; ++k)
+            clp->inf_v.push_back(clp->inf_v[0]);
+        num_ifiles = clp->inf_v.size();
+    }
+    num_slices = (num_ifiles > num_ofiles) ? num_ifiles : num_ofiles;
+    if ((int)num_slices > num_threads) {
+        pr2serr("%sNumber of slices [%zd] exceeds number of threads [%d].\n",
+                my_name, num_slices, num_threads);
+        pr2serr("Number of threads needs to be increased.\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    k = 0;
+    for (auto && cvp : clp->cp_ver_arr) {
+        if (k >= (int)num_slices)
+            break;
+        cvp.my_index = k++;
+        cvp.state = cp_ver_pair_t::my_state::init;
+    }
+    clp->in0fd = STDIN_FILENO;
+    clp->out0fd = STDOUT_FILENO;
+    if (clp->in_flags.ff && clp->in_flags.zero) {
+        ccp = "<addr_as_data>";
+        cc2p = "addr_as_data";
+    } else if (clp->in_flags.ff) {
+        ccp = "<0xff bytes>";
+        cc2p = "ff";
+    } else if (clp->in_flags.random) {
+        ccp = "<random>";
+        cc2p = "random";
+    } else if (clp->in_flags.zero) {
+        ccp = "<zero bytes>";
+        cc2p = "00";
+    }
+    inf0_sz = clp->inf_v.size() ? clp->inf_v[0].size() : 0;
+    if (ccp) {
+        if (ifile_given) {
+            pr2serr("%siflag=%s and if=%s contradict\n", my_name, cc2p,
+                    clp->inf_v[0].c_str());
+            return SG_LIB_CONTRADICT;
+        }
+        for (auto && cvp : clp->cp_ver_arr) {
+            if (cvp.state == cp_ver_pair_t::my_state::empty)
+                break;
+            cvp.in_type = FT_RANDOM_0_FF;
+        }
+        clp->in_type = FT_RANDOM_0_FF;
+        clp->infp = ccp;
+        clp->in0fd = -1;
+    } else if (inf0_sz && ('-' != clp->inf_v[0].c_str()[0])) {
+        const string & inf_s = clp->inf_v[0];
+        const char * infp = inf_s.c_str();
+
+        clp->in_type = dd_filetype(infp, clp->in_st_size);
+        if (FT_ERROR == clp->in_type) {
+            pr2serr("%sunable to access %s\n", my_name, infp);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_ST == clp->in_type) {
+            pr2serr("%sunable to use scsi tape device %s\n", my_name, infp);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_CHAR == clp->in_type) {
+            pr2serr("%sunable to use unknown char device %s\n", my_name, infp);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_SG == clp->in_type) {
+            clp->in0fd = sg_in_open(clp, inf_s, NULL, NULL);
+            if (clp->in0fd < 0)
+                return -clp->in0fd;
+        } else {
+            clp->in0fd = reg_file_open(clp, infp, false /* read */);
+            if (clp->in0fd < 0)
+                return sg_convert_errno(-clp->in0fd);
+        }
+        clp->infp = infp;
+    }
+    if (clp->cdl_given && (! clp->cdbsz_given)) {
+        bool changed = false;
+
+        if ((clp->cdbsz_in < 16) && (clp->in_flags.cdl > 0)) {
+            clp->cdbsz_in = 16;
+            changed = true;
+        }
+        if ((clp->cdbsz_out < 16) && (! clp->verify) &&
+            (clp->out_flags.cdl > 0)) {
+            clp->cdbsz_out = 16;
+            changed = true;
+        }
+        if (changed)
+            pr2serr(">> increasing cdbsz to 16 due to cdl > 0\n");
+    }
+    if ((clp->verbose > 0) &&
+        (clp->in_flags.no_waitq || clp->out_flags.no_waitq))
+        pr2serr("no_waitq=<n> operand is now ignored\n");
+    if (clp->outf_v.size()) {
+        const string & outf_s = clp->outf_v[0].c_str();
+        const char * outfp = outf_s.c_str();
+
+        clp->ofile_given = true;
+        if ('-' == outfp[0])
+            clp->out_type = FT_FIFO;
+        else
+            clp->out_type = dd_filetype(outfp, clp->out_st_size);
+
+        if ((FT_SG != clp->out_type) && clp->verify) {
+            pr2serr("%s --verify only supported by sg OFILEs\n", my_name);
+            return SG_LIB_FILE_ERROR;
+        }
+        if (FT_FIFO == clp->out_type)
+            ;
+        else if (FT_ST == clp->out_type) {
+            pr2serr("%sunable to use scsi tape device %s\n", my_name, outfp);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_CHAR == clp->out_type) {
+            pr2serr("%sunable to use unknown char device %s\n", my_name,
+                    outfp);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_SG == clp->out_type) {
+            clp->out0fd = sg_out_open(clp, outf_s, NULL, NULL);
+            if (clp->out0fd < 0)
+                return -clp->out0fd;
+        } else if (FT_DEV_NULL == clp->out_type)
+            clp->out0fd = -1; /* don't bother opening */
+        else {
+            clp->out0fd = reg_file_open(clp, outfp, true /* write */);
+            if (clp->out0fd < 0)
+                return sg_convert_errno(-clp->out0fd);
+        }
+        clp->outfp = outfp;
+    }
+    if (clp->verify && (clp->out_type == FT_DEV_NULL)) {
+        pr2serr("Can't do verify when OFILE not given\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if ((FT_SG == clp->in_type) && (FT_SG == clp->out_type)) {
+        if (clp->in_flags.serial || clp->out_flags.serial)
+            pr2serr("serial flag ignored when both IFILE and OFILE are sg "
+                    "devices\n");
+        if (clp->in_flags.order_wr && (num_threads > 1))
+            pr2serr("Warning: write ordering only guaranteed for single "
+                    "thread\n");
+    } else if (clp->in_flags.order_wr)
+        pr2serr("Warning: oflag=order only active on sg->sg copies\n");
+
+    if (outregf[0]) {
+        int ftyp = dd_filetype(outregf, clp->outreg_st_size);
+
+        clp->outreg_type = ftyp;
+        if (! ((FT_OTHER == ftyp) || (FT_ERROR == ftyp) ||
+               (FT_DEV_NULL == ftyp))) {
+            pr2serr("File: %s can only be regular file or pipe (or "
+                    "/dev/null)\n", outregf);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if ((clp->outregfd = open(outregf, O_WRONLY | O_CREAT, 0666)) < 0) {
+            err = errno;
+            snprintf(ebuff, EBUFF_SZ, "could not open %s for writing",
+                     outregf);
+            perror(ebuff);
+            return sg_convert_errno(err);
+        }
+        if (clp->verbose > 1)
+            pr2serr("ofreg=%s opened okay, fd=%d\n", outregf, clp->outregfd);
+        if (FT_ERROR == ftyp)
+            clp->outreg_type = FT_OTHER;        /* regular file created */
+    } else
+        clp->outregfd = -1;
+
+    if ((STDIN_FILENO == clp->in0fd) && (STDOUT_FILENO == clp->out0fd)) {
+        pr2serr("Won't default both IFILE to stdin _and_ OFILE to "
+                "/dev/null\n");
+        pr2serr("For more information use '--help'\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((clp->in_type == FT_FIFO) && (! clp->i_sgl.is_pipe_suitable())) {
+        pr2serr("The skip= argument is not suitable for a pipe\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((clp->out_type == FT_FIFO) && (! clp->o_sgl.is_pipe_suitable())) {
+        pr2serr("The seek= argument is not suitable for a pipe\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    res = do_count_work(clp, clp->inf_v[0].c_str(), in_num_sect,
+                        clp->outf_v[0].c_str(), out_num_sect);
+    if (res)
+        return res;
+
+    if (clp->verbose > 2)
+        pr2serr("Start of loop, count=%" PRId64 ", in_num_sect=%" PRId64
+                ", out_num_sect=%" PRId64 "\n", clp->dd_count, in_num_sect,
+                out_num_sect);
+    if (clp->dd_count < 0) {
+        pr2serr("Couldn't calculate count, please give one\n");
+        return SG_LIB_CAT_OTHER;
+    }
+    if (! clp->cdbsz_given) {
+        if ((FT_SG == clp->in_type) && (MAX_SCSI_CDB_SZ != clp->cdbsz_in) &&
+            ((clp->i_sgl.high_lba_p1 > UINT_MAX) || (clp->bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'if')\n");
+            clp->cdbsz_in = MAX_SCSI_CDB_SZ;
+        }
+        if ((FT_SG == clp->out_type) && (MAX_SCSI_CDB_SZ != clp->cdbsz_out) &&
+            ((clp->o_sgl.high_lba_p1 > UINT_MAX) || (clp->bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'of')\n");
+            clp->cdbsz_out = MAX_SCSI_CDB_SZ;
+        }
+    }
+
+    for (auto && cvp : clp->cp_ver_arr) {
+        cvp.in_type = clp->in_type;
+        cvp.out_type = clp->out_type;
+        cvp.dd_count = clp->dd_count;
+        cvp.in_rem_count = clp->dd_count;
+        cvp.out_rem_count = clp->dd_count;
+    }
+
+    if (clp->dry_run > 0) {
+        pr2serr("Due to --dry-run option, bypass copy/read\n");
+        goto fini;
+    }
+    if (! clp->ofile_given)
+        pr2serr("of=OFILE not given so only read from IFILE, to output to "
+                "stdout use 'of=-'\n");
+
+    sigemptyset(&signal_set);
+    sigaddset(&signal_set, SIGINT);
+    sigaddset(&signal_set, SIGUSR2);
+
+    res = sigprocmask(SIG_BLOCK, &signal_set, &orig_signal_set);
+    if (res < 0) {
+        pr2serr("sigprocmask failed: %s\n", safe_strerror(errno));
+        goto fini;
+    }
+
+    listen_thr_v.emplace_back(sig_listen_thread, clp);
+
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+    }
+
+/* vvvvvvvvvvv  Start worker threads  vvvvvvvvvvvvvvvvvvvvvvvv */
+    if (num_threads > 0) {
+        auto & cvp = clp->cp_ver_arr[0];
+
+        cvp.in_fd = clp->in0fd;
+        cvp.out_fd = clp->out0fd;
+
+        /* launch "infant" thread to catch early mortality, if any */
+        work_thr_v.emplace_back(read_write_thread, clp, 0, 0, true);
+        {
+            unique_lock<mutex> lk(clp->infant_mut);
+            clp->infant_cv.wait(lk, []{ return gcoll.processed; });
+        }
+        if (clp->cp_ver_arr[0].next_count_pos.load() < 0) {
+            /* infant thread error-ed out, join with it */
+            for (auto & t : work_thr_v) {
+                if (t.joinable())
+                    t.join();
+            }
+            goto jump;
+        }
+
+        /* now start the rest of the threads */
+        for (k = 1; k < num_threads; ++k)
+            work_thr_v.emplace_back(read_write_thread, clp, k,
+                                  k % (int)num_slices, false);
+
+        /* now wait for worker threads to finish */
+        for (auto & t : work_thr_v) {
+            if (t.joinable())
+                t.join();
+        }
+    }   /* worker threads hereafter have all exited */
+jump:
+    if (do_time && (start_tm.tv_sec || start_tm.tv_usec))
+        calc_duration_throughput(0);
+
+    if (do_sync) {
+        if (FT_SG == clp->out_type) {
+            pr2serr_lk(">> Synchronizing cache on %s\n",
+                       (clp->outf_v.size() ? clp->outf_v[0].c_str() : "" ));
+            res = sg_ll_sync_cache_10(clp->out0fd, 0, 0, 0, 0, 0, false, 0);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr_lk("Unit attention(out), continuing\n");
+                res = sg_ll_sync_cache_10(clp->out0fd, 0, 0, 0, 0, 0, false,
+                                          0);
+            }
+            if (0 != res)
+                pr2serr_lk("Unable to synchronize cache\n");
+        }
+    }
+
+    shutting_down = true;
+    for (auto & t : listen_thr_v) {
+        if (t.joinable()) {
+            t.detach();
+            if (listen_t_tid > 0)
+                kill(listen_t_tid, SIGUSR2);
+            // t.~thread();        /* kill listening thread; doesn't work */
+        }
+        std::this_thread::yield();      // not enough it seems
+        {   /* allow time for SIGUSR2 signal to get through */
+            struct timespec tspec = {0, 1000000}; /* 1 msec */
+            struct timespec rem;
+
+            while ((nanosleep(&tspec, &rem) < 0) && (EINTR == errno))
+                tspec = rem;
+        }
+    }
+
+fini:
+    if ((STDIN_FILENO != clp->in0fd) && (clp->in0fd >= 0))
+        close(clp->in0fd);
+    if ((STDOUT_FILENO != clp->out0fd) && (FT_DEV_NULL != clp->out_type) &&
+        (clp->out0fd >= 0))
+        close(clp->out0fd);
+    if ((clp->outregfd >= 0) && (STDOUT_FILENO != clp->outregfd) &&
+        (FT_DEV_NULL != clp->outreg_type))
+        close(clp->outregfd);
+    print_stats("");
+    if (clp->dio_incomplete_count.load()) {
+        int fd;
+        char c;
+
+        pr2serr(">> Direct IO requested but incomplete %d times\n",
+                clp->dio_incomplete_count.load());
+        if ((fd = open(sg_allow_dio, O_RDONLY)) >= 0) {
+            if (1 == read(fd, &c, 1)) {
+                if ('0' == c)
+                    pr2serr(">>> %s set to '0' but should be set to '1' for "
+                            "direct IO\n", sg_allow_dio);
+            }
+            close(fd);
+        }
+    }
+
+    k = 0;
+    for (auto && cvp : gcoll.cp_ver_arr) {
+        if (cvp.state == cp_ver_pair_t::my_state::empty)
+            break;
+        ++k;
+        if (cvp.sum_of_resids.load())
+            pr2serr(">> slice: %d, Non-zero sum of residual counts=%d\n",
+                    k, cvp.sum_of_resids.load());
+    }
+    if (clp->verbose && (num_start_eagain > 0))
+        pr2serr("Number of start EAGAINs: %d\n", num_start_eagain.load());
+    if (clp->verbose && (num_fin_eagain > 0))
+        pr2serr("Number of finish EAGAINs: %d\n", num_fin_eagain.load());
+    if (clp->verbose && (num_ebusy > 0))
+        pr2serr("Number of EBUSYs: %d\n", num_ebusy.load());
+    if (clp->verbose && (num_miscompare > 0))
+        pr2serr("Number of miscompare%s: %d\n",
+                (num_miscompare > 1) ? "s" : "", num_miscompare.load());
+    if (clp->verify && (SG_LIB_CAT_MISCOMPARE == res))
+        pr2serr("Verify/compare failed due to miscompare\n");
+    if (0 == res)
+        res = clp->reason_res.load();
+    sigprocmask(SIG_SETMASK, &orig_signal_set, NULL);
+    if (clp->verbose) {
+        int num_sigusr2 = num_fallthru_sigusr2.load();
+        if (num_sigusr2 > 0)
+            pr2serr("Number of fall-through SIGUSR2 signals caught: %d\n",
+                    num_sigusr2);
+    }
+    return (res >= 0) ? res : SG_LIB_CAT_OTHER;
+}
diff --git a/testing/sg_queue_tst.c b/testing/sg_queue_tst.c
new file mode 100644
index 0000000..9f83fd3
--- /dev/null
+++ b/testing/sg_queue_tst.c
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2010-2019 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ *
+ * This program was used to test SCSI mid level queue ordering.
+ * The default behaviour is to "queue at head" which is useful for
+ * error processing but not for streaming READ and WRITE commands.
+ *
+ * Invocation: sg_queue_tst [-l=Q_LEN] [-t] <sg_device>
+ *      -t      queue at tail
+ *
+ * Version 0.96 (20190128)
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdarg.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_linux_inc.h"
+
+
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+#define SDIAG_CMD_LEN 6
+#define SENSE_BUFFER_LEN 96
+
+#define EBUFF_SZ 256
+
+#ifndef SG_FLAG_Q_AT_TAIL
+#define SG_FLAG_Q_AT_TAIL 0x10
+#endif
+
+#ifndef SG_FLAG_Q_AT_HEAD
+#define SG_FLAG_Q_AT_HEAD 0x20
+#endif
+
+#define DEF_Q_LEN 16    /* max in sg v3 and earlier */
+#define MAX_Q_LEN 256
+
+static void
+set_nanosecs(int sg_fd)
+{
+    struct sg_extended_info sei;
+    struct sg_extended_info * seip;
+
+    seip = &sei;
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+    seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS; /* this or previous optional */
+    seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+    seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        fprintf(stderr, "ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    }
+}
+
+
+int main(int argc, char * argv[])
+{
+    bool q_at_tail = false;
+    bool dur_in_nanosecs = false;
+    int sg_fd, k, ok;
+    uint8_t inq_cdb[INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    uint8_t sdiag_cdb[SDIAG_CMD_LEN] =
+                                {0x1d, 0x10 /* PF */, 0, 0, 0, 0};
+    uint8_t inqBuff[MAX_Q_LEN][INQ_REPLY_LEN];
+    sg_io_hdr_t io_hdr[MAX_Q_LEN];
+    sg_io_hdr_t rio_hdr;
+    char * file_name = 0;
+    char ebuff[EBUFF_SZ];
+    uint8_t sense_buffer[MAX_Q_LEN][SENSE_BUFFER_LEN] SG_C_CPP_ZERO_INIT;
+    int q_len = DEF_Q_LEN;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-n", argv[k], 2))
+            dur_in_nanosecs = true;
+        else if (0 == memcmp("-t", argv[k], 2))
+            q_at_tail = true;
+        else if (0 == memcmp("-l=", argv[k], 3)) {
+            q_len = atoi(argv[k] + 3);
+            if ((q_len > 511) || (q_len < 1)) {
+                printf("Expect -l= to take a number (q length) between 1 "
+                       "and 511\n");
+                file_name = 0;
+                break;
+            }
+
+        } else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("Usage: 'sg_queue_tst [-l=Q_LEN] [-n] [-t] <sg_device>'\n"
+               "where:\n"
+               "      -l=Q_LEN    queue length, between 1 and 511 "
+               "(def: 16)\n"
+               "      -n    duration in nanosecs (def: milliseconds)\n"
+               "      -t    queue_at_tail (def: q_at_head)\n");
+        return 1;
+    }
+
+    /* An access mode of O_RDWR is required for write()/read() interface */
+    if ((sg_fd = open(file_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "sg_queue_tst: error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    if (dur_in_nanosecs)
+        set_nanosecs(sg_fd);
+
+    for (k = 0; k < q_len; ++k) {
+        /* Prepare INQUIRY command */
+        memset(&io_hdr[k], 0, sizeof(sg_io_hdr_t));
+        io_hdr[k].interface_id = 'S';
+        /* io_hdr[k].iovec_count = 0; */  /* memset takes care of this */
+        io_hdr[k].mx_sb_len = (uint8_t)sizeof(sense_buffer);
+        if (0 == (k % 3)) {
+            io_hdr[k].cmd_len = sizeof(sdiag_cdb);
+            io_hdr[k].cmdp = sdiag_cdb;
+            io_hdr[k].dxfer_direction = SG_DXFER_NONE;
+        } else {
+            io_hdr[k].cmd_len = sizeof(inq_cdb);
+            io_hdr[k].cmdp = inq_cdb;
+            io_hdr[k].dxfer_direction = SG_DXFER_FROM_DEV;
+            io_hdr[k].dxfer_len = INQ_REPLY_LEN;
+            io_hdr[k].dxferp = inqBuff[k];
+        }
+        io_hdr[k].sbp = sense_buffer[k];
+        io_hdr[k].mx_sb_len = SENSE_BUFFER_LEN;
+        io_hdr[k].timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        io_hdr[k].pack_id = k;
+        /* default is to queue at head (in SCSI mid level) */
+        if (q_at_tail)
+            io_hdr[k].flags |= SG_FLAG_Q_AT_TAIL;
+        else
+            io_hdr[k].flags |= SG_FLAG_Q_AT_HEAD;
+        /* io_hdr[k].usr_ptr = NULL; */
+
+        if (write(sg_fd, &io_hdr[k], sizeof(sg_io_hdr_t)) < 0) {
+            perror("sg_queue_tst: sg write error");
+            close(sg_fd);
+            return 1;
+        }
+    }
+    /* sleep(3); */
+    for (k = 0; k < q_len; ++k) {
+        memset(&rio_hdr, 0, sizeof(sg_io_hdr_t));
+        rio_hdr.interface_id = 'S';
+        if (read(sg_fd, &rio_hdr, sizeof(sg_io_hdr_t)) < 0) {
+            perror("sg_queue_tst: sg read error");
+            close(sg_fd);
+            return 1;
+        }
+        /* now for the error processing */
+        ok = 0;
+        switch (sg_err_category3(&rio_hdr)) {
+        case SG_LIB_CAT_CLEAN:
+            ok = 1;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            printf("Recovered error, continuing\n");
+            ok = 1;
+            break;
+        default: /* won't bother decoding other categories */
+            sg_chk_n_print3("command error", &rio_hdr, 1);
+            break;
+        }
+
+        if (ok) { /* output result if it is available */
+            /* if (0 == rio_hdr.pack_id) */
+            if (0 == (rio_hdr.pack_id % 3))
+                printf("SEND DIAGNOSTIC %d duration=%u %s\n", rio_hdr.pack_id,
+                       rio_hdr.duration, (dur_in_nanosecs ? "ns" : "ms"));
+            else
+                printf("INQUIRY %d duration=%u %s\n", rio_hdr.pack_id,
+                       rio_hdr.duration, (dur_in_nanosecs ? "ns" : "ms"));
+        }
+    }
+
+    close(sg_fd);
+    return 0;
+}
diff --git a/testing/sg_scat_gath.cpp b/testing/sg_scat_gath.cpp
new file mode 100644
index 0000000..5675271
--- /dev/null
+++ b/testing/sg_scat_gath.cpp
@@ -0,0 +1,1049 @@
+/*
+ * Copyright (c) 2014-2020 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Version 1.02 [20201124]
+ */
+
+// C headers
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <limits.h>
+#include <ctype.h>
+#include <errno.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+// C++ headers
+#include <array>
+
+#include "sg_scat_gath.h"
+#include "sg_lib.h"
+#include "sg_pr2serr.h"
+
+using namespace std;
+
+#define MAX_SGL_NUM_VAL (INT32_MAX - 1)  /* should reduce for testing */
+// #define MAX_SGL_NUM_VAL 7  /* should reduce for testing */
+#if MAX_SGL_NUM_VAL > INT32_MAX
+#error "MAX_SGL_NUM_VAL cannot exceed 2^31 - 1"
+#endif
+
+bool
+scat_gath_list::empty() const
+{
+    return sgl.empty();
+}
+
+bool
+scat_gath_list::empty_or_00() const
+{
+    if (sgl.empty())
+        return true;
+    return ((sgl.size() == 1) && (sgl[0].lba == 0) && (sgl[0].num == 0));
+}
+
+int
+scat_gath_list::num_elems() const
+{
+    return sgl.size();
+}
+
+
+/* Read numbers (up to 64 bits in size) from command line (comma (or
+ * (single) space **) separated list). Assumed decimal unless prefixed
+ * by '0x', '0X' or contains trailing 'h' or 'H' (which indicate hex).
+ * Returns 0 if ok, or 1 if error. Assumed to be LBA (64 bit) and
+ * number_of_block (32 bit) pairs. ** Space on command line needs to
+ * be escaped, otherwise it is an operand/option separator. */
+bool
+scat_gath_list::load_from_cli(const char * cl_p, bool b_vb)
+{
+    bool split, full_pair;
+    int in_len, k, j;
+    const int max_nbs = MAX_SGL_NUM_VAL;
+    int64_t ll, large_num;
+    uint64_t prev_lba;
+    char * cp;
+    char * c2p;
+    const char * lcp;
+    class scat_gath_elem sge;
+
+    if (NULL == cl_p) {
+        pr2serr("%s: bad arguments\n", __func__);
+        goto err_out;
+    }
+    lcp = cl_p;
+    in_len = strlen(cl_p);
+    if ('-' == cl_p[0]) {        /* read from stdin */
+        pr2serr("%s: logic error: no stdin here\n", __func__);
+        goto err_out;
+    } else {        /* list of numbers (default decimal) on command line */
+        k = strspn(cl_p, "0123456789aAbBcCdDeEfFhHxXiIkKmMgGtTpP, ");
+        if (in_len != k) {
+            if (b_vb)
+                pr2serr("%s: error at pos %d\n", __func__, k + 1);
+            goto err_out;
+        }
+        j = 0;
+        full_pair = true;
+        for (k = 0, split = false; ; ++k) {
+            if (split) {
+                /* splitting given elem with large number_of_blocks into
+                 * multiple elems within array being built */
+                ++j;
+                sge.lba = prev_lba + (uint64_t)max_nbs;
+                if (large_num > max_nbs) {
+                    sge.num = (uint32_t)max_nbs;
+                    prev_lba = sge.lba;
+                    large_num -= max_nbs;
+                    sgl.push_back(sge);
+                } else {
+                    sge.num = (uint32_t)large_num;
+                    split = false;
+                    if (b_vb)
+                        pr2serr("%s: split large sg elem into %d element%s\n",
+                                __func__, j, (j == 1 ? "" : "s"));
+                    sgl.push_back(sge);
+                    goto check_for_next;
+                }
+                continue;
+            }
+            full_pair = false;
+            ll = sg_get_llnum(lcp);
+            if (-1 != ll) {
+                sge.lba = (uint64_t)ll;
+                cp = (char *)strchr(lcp, ',');
+                c2p = (char *)strchr(lcp, ' ');
+                if (NULL == cp) {
+                    cp = c2p;
+                    if (NULL == cp)
+                        break;
+                }
+                if (c2p && (c2p < cp))
+                    cp = c2p;
+                lcp = cp + 1;
+            } else {
+                if (b_vb)
+                    pr2serr("%s: error at pos %d\n", __func__,
+                            (int)(lcp - cl_p + 1));
+                goto err_out;
+            }
+            ll = sg_get_llnum(lcp);
+            if (ll >= 0) {
+                full_pair = true;
+                if (ll > max_nbs) {
+                    sge.num = (uint32_t)max_nbs;
+                    prev_lba = sge.lba;
+                    large_num = ll - max_nbs;
+                    split = true;
+                    j = 1;
+                    continue;
+                }
+                sge.num = (uint32_t)ll;
+            } else {    /* bad or negative number as number_of_blocks */
+                if (b_vb)
+                    pr2serr("%s: bad number at pos %d\n", __func__,
+                            (int)(lcp - cl_p + 1));
+                goto err_out;
+            }
+            sgl.push_back(sge);
+check_for_next:
+            cp = (char *)strchr(lcp, ',');
+            c2p = (char *)strchr(lcp, ' ');
+            if (NULL == cp) {
+                cp = c2p;
+                if (NULL == cp)
+                    break;
+            }
+            if (c2p && (c2p < cp))
+                cp = c2p;
+            lcp = cp + 1;
+        }       /* end of for loop over items in operand */
+        /* other than first pair, expect even number of items */
+        if ((k > 0) && (! full_pair)) {
+            if (b_vb)
+                pr2serr("%s:  expected even number of items: "
+                        "LBA0,NUM0,LBA1,NUM1...\n", __func__);
+            goto err_out;
+        }
+    }
+    return true;
+err_out:
+    if (0 == m_errno)
+        m_errno = SG_LIB_SYNTAX_ERROR;
+    return false;
+}
+
+bool
+scat_gath_list::file2sgl_helper(FILE * fp, const char * fnp, bool def_hex,
+                                bool flexible, bool b_vb)
+{
+    bool bit0;
+    bool pre_addr1 = true;
+    bool pre_hex_seen = false;
+    int in_len, k, j, m, ind;
+    const int max_nbs = MAX_SGL_NUM_VAL;
+    int off = 0;
+    int64_t ll;
+    uint64_t ull, prev_lba;
+    char * lcp;
+    class scat_gath_elem sge;
+    char line[1024];
+
+    for (j = 0 ; ; ++j) {
+        if (NULL == fgets(line, sizeof(line), fp))
+            break;
+        // could improve with carry_over logic if sizeof(line) too small
+        in_len = strlen(line);
+        if (in_len > 0) {
+            if ('\n' == line[in_len - 1]) {
+                --in_len;
+                line[in_len] = '\0';
+            } else {
+                m_errno = SG_LIB_SYNTAX_ERROR;
+                if (b_vb)
+                    pr2serr("%s: %s: line too long, max %d bytes\n",
+                            __func__, fnp, (int)(sizeof(line) - 1));
+                goto err_out;
+            }
+        }
+        if (in_len < 1)
+            continue;
+        lcp = line;
+        m = strspn(lcp, " \t");
+        if (m == in_len)
+            continue;
+        lcp += m;
+        in_len -= m;
+        if ('#' == *lcp)
+            continue;
+        if (pre_addr1 || pre_hex_seen) {
+            /* Accept lines with leading 'HEX' and ignore as long as there
+             * is one _before_ any LBA,NUM lines in the file. This allows
+             * HEX marked sgls to be concaternated together. */
+            if (('H' == toupper(lcp[0])) && ('E' == toupper(lcp[1])) &&
+                ('X' == toupper(lcp[2]))) {
+                pre_hex_seen = true;
+                if (def_hex)
+                    continue; /* bypass 'HEX' marker line if expecting hex */
+                else {
+                    if (flexible) {
+                        def_hex = true; /* okay, switch to hex parse */
+                        continue;
+                    } else {
+                        pr2serr("%s: %s: 'hex' string detected on line %d, "
+                                "expecting decimal\n", __func__, fnp, j + 1);
+                        m_errno = EINVAL;
+                        goto err_out;
+                    }
+                }
+            }
+        }
+        k = strspn(lcp, "0123456789aAbBcCdDeEfFhHxXbBdDiIkKmMgGtTpP, \t");
+        if ((k < in_len) && ('#' != lcp[k])) {
+            m_errno = EINVAL;
+            if (b_vb)
+                pr2serr("%s: %s: syntax error at line %d, pos %d\n",
+                        __func__, fnp, j + 1, m + k + 1);
+            goto err_out;
+        }
+        for (k = 0; k < 256; ++k) {
+            /* limit parseable items on one line to 256 */
+            if (def_hex) {      /* don't accept negatives or multipliers */
+                if (1 == sscanf(lcp, "%" SCNx64, &ull))
+                    ll = (int64_t)ull;
+                else
+                    ll = -1;    /* use (2**64 - 1) as error flag */
+            } else
+                ll = sg_get_llnum(lcp);
+            if (-1 != ll) {
+                ind = ((off + k) >> 1);
+                bit0 = !! (0x1 & (off + k));
+                if (ind >= SG_SGL_MAX_ELEMENTS) {
+                    m_errno = EINVAL;
+                    if (b_vb)
+                        pr2serr("%s: %s: array length exceeded\n", __func__,
+                                fnp);
+                    goto err_out;
+                }
+                if (bit0) {     /* bit0 set when decoding a NUM */
+                    if (ll < 0) {
+                        m_errno = EINVAL;
+                        if (b_vb)
+                            pr2serr("%s: %s: bad number in line %d, at pos "
+                                    "%d\n", __func__, fnp, j + 1,
+                                    (int)(lcp - line + 1));
+                        goto err_out;
+                    }
+                    if (ll > max_nbs) {
+                        int h = 1;
+
+                        /* split up this elem into multiple, smaller elems */
+                        do {
+                            sge.num = (uint32_t)max_nbs;
+                            prev_lba = sge.lba;
+                            sgl.push_back(sge);
+                            sge.lba = prev_lba + (uint64_t)max_nbs;
+                            ++h;
+                            off += 2;
+                            ll -= max_nbs;
+                        } while (ll > max_nbs);
+                        if (b_vb)
+                            pr2serr("%s: split large sg elem into %d "
+                                    "elements\n", __func__, h);
+                    }
+                    sge.num = (uint32_t)ll;
+                    sgl.push_back(sge);
+                } else {        /* bit0 clear when decoding a LBA */
+                    if (pre_addr1)
+                        pre_addr1 = false;
+                    sge.lba = (uint64_t)ll;
+                }
+            } else {    /* failed to decode number on line */
+                if ('#' == *lcp) { /* numbers before #, rest of line comment */
+                    --k;
+                    break;      /* goes to next line */
+                }
+                m_errno = EINVAL;
+                if (b_vb)
+                    pr2serr("%s: %s: error in line %d, at pos %d\n",
+                            __func__, fnp, j + 1, (int)(lcp - line + 1));
+                goto err_out;
+            }
+            lcp = strpbrk(lcp, " ,\t#");
+            if ((NULL == lcp) || ('#' == *lcp))
+                break;
+            lcp += strspn(lcp, " ,\t");
+            if ('\0' == *lcp)
+                break;
+        }       /* <<< end of for(k < 256) loop */
+        off += (k + 1);
+    }   /* <<< end of for loop, one iteration per line */
+    /* allow one items, but not higher odd number of items */
+    if ((off > 1) && (0x1 & off)) {
+        m_errno = EINVAL;
+        if (b_vb)
+            pr2serr("%s: %s: expect even number of items: "
+                    "LBA0,NUM0,LBA1,NUM1...\n", __func__, fnp);
+        goto err_out;
+    }
+    clearerr(fp);    /* even EOF on first pass needs this before rescan */
+    return true;
+err_out:
+    clearerr(fp);
+    return false;
+}
+
+/* Read numbers from filename (or stdin), line by line (comma (or (single)
+ * space) separated list); places starting_LBA,number_of_block pairs in an
+ * array of scat_gath_elem elements pointed to by the returned value. If
+ * this fails NULL is returned and an error number is written to errp (if it
+ * is non-NULL). Assumed decimal (and may have suffix multipliers) when
+ * def_hex==false; if a number is prefixed by '0x', '0X' or contains trailing
+ * 'h' or 'H' that denotes a hex number. When def_hex==true all numbers are
+ * assumed to be hex (ignored '0x' prefixes and 'h' suffixes) and multipliers
+ * are not permitted. Heap allocates an array just big enough to hold all
+ * elements if the file is countable. Pipes and stdin are not considered
+ * countable. In the non-countable case an array of MAX_FIXED_SGL_ELEMS
+ * elements is pre-allocated; if it is exceeded sg_convert_errno(EDOM) is
+ * placed in *errp (if it is non-NULL). One of the first actions is to write
+ * 0 to *errp (if it is non-NULL) so the caller does not need to zero it
+ * before calling. */
+bool
+scat_gath_list::load_from_file(const char * file_name, bool def_hex,
+                               bool flexible, bool b_vb)
+{
+    bool have_stdin;
+    bool have_err = false;
+    FILE * fp;
+    const char * fnp;
+
+    have_stdin = ((1 == strlen(file_name)) && ('-' == file_name[0]));
+    if (have_stdin) {
+        fp = stdin;
+        fnp = "<stdin>";
+    } else {
+        fnp = file_name;
+        fp = fopen(fnp, "r");
+        if (NULL == fp) {
+            m_errno = errno;
+            if (b_vb)
+                pr2serr("%s: opening %s: %s\n", __func__, fnp,
+                        safe_strerror(m_errno));
+            return false;
+        }
+    }
+    if (! file2sgl_helper(fp, fnp, def_hex, flexible, b_vb))
+        have_err = true;
+    if (! have_stdin)
+        fclose(fp);
+    return have_err ? false : true;
+}
+
+const char *
+scat_gath_list::linearity_as_str() const
+{
+    switch (linearity) {
+    case SGL_LINEAR:
+        return "linear";
+    case SGL_MONOTONIC:
+        return "monotonic";
+    case SGL_MONO_OVERLAP:
+        return "monotonic, overlapping";
+    case SGL_NON_MONOTONIC:
+        return "non-monotonic";
+    default:
+        return "unknown";
+    }
+}
+
+void
+scat_gath_list::set_weaker_linearity(enum sgl_linearity_e lin)
+{
+    int i_lin = (int)lin;
+
+    if (i_lin > (int)linearity)
+        linearity = lin;
+}
+
+/* id_str may be NULL (if so replace by "unknown"), present to enhance verbose
+ * output. */
+void
+scat_gath_list::dbg_print(bool skip_meta, const char * id_str, bool to_stdout,
+                          bool show_sgl) const
+{
+    int num = sgl.size();
+    const char * caller = id_str ? id_str : "unknown";
+    FILE * fp = to_stdout ? stdout : stderr;
+
+    if (! skip_meta) {
+        fprintf(fp, "%s: elems=%d, sgl %spresent, linearity=%s\n",
+                caller, num, (sgl.empty() ? "not " : ""),
+                linearity_as_str());
+        fprintf(fp, "  sum=%" PRId64 ", sum_hard=%s lowest=0x%" PRIx64
+                ", high_lba_p1=", sum, (sum_hard ? "true" : "false"),
+                lowest_lba);
+        fprintf(fp, "0x%" PRIx64 "\n", high_lba_p1);
+    }
+    fprintf(fp, "  >> %s scatter gather list (%d element%s):\n", caller, num,
+            (num == 1 ? "" : "s"));
+    if (show_sgl) {
+        int k;
+
+        for (k = 0; k < num; ++k) {
+            const class scat_gath_elem & sge = sgl[k];
+
+            fprintf(fp, "    lba: 0x%" PRIx64 ", number: 0x%" PRIx32,
+                    sge.lba, sge.num);
+            if (sge.lba > 0)
+                fprintf(fp, " [next lba: 0x%" PRIx64 "]", sge.lba + sge.num);
+            fprintf(fp, "\n");
+        }
+    }
+}
+
+/* Assumes sgl array (vector) is setup. The other fields in this object are
+ * set by analyzing sgl in a single pass. The fields that are set are:
+ * fragmented, lowest_lba, high_lba_p1, monotonic, overlapping, sum and
+ * sum_hard. Degenerate elements (i.e. those with 0 blocks) are ignored apart
+ * from when one is last which makes sum_hard false and its LBA becomes
+ * high_lba_p1 if it is the highest in the list. An empty sgl is equivalent
+ * to a 1 element list with [0, 0], so sum_hard==false, monit==true,
+ * fragmented==false and overlapping==false . id_str may be NULL, present
+ * to enhance verbose output. */
+void
+scat_gath_list::sum_scan(const char * id_str, bool show_sgl, bool b_vb)
+{
+    bool degen = false;
+    bool first = true;
+    bool regular = true;        /* no overlapping segments detected */
+    int k;
+    int elems = sgl.size();
+    uint32_t prev_num, t_num;
+    uint64_t prev_lba, t_lba, low, high, end;
+
+    sum = 0;
+    for (k = 0, low = 0, high = 0; k < elems; ++k) {
+        const class scat_gath_elem & sge = sgl[k];
+
+        degen = false;
+        t_num = sge.num;
+        if (0 == t_num) {
+            degen = true;
+            if (! first)
+                continue;       /* ignore degen element that not first */
+        }
+        if (first) {
+            low = sge.lba;
+            sum = t_num;
+            high = sge.lba + sge.num;
+            first = false;
+        } else {
+            t_lba = sge.lba;
+            if ((prev_lba + prev_num) != t_lba)
+                set_weaker_linearity(SGL_MONOTONIC);
+            sum += t_num;
+            end = t_lba + t_num;
+            if (end > high)
+                high = end;     /* high is one plus highest LBA */
+            if (prev_lba < t_lba)
+                ;
+            else if (prev_lba == t_lba) {
+                if (prev_num > 0) {
+                    set_weaker_linearity(SGL_MONO_OVERLAP);
+                    break;
+                }
+            } else {
+                low = t_lba;
+                set_weaker_linearity(SGL_NON_MONOTONIC);
+                break;
+            }
+            if (regular) {
+                if ((prev_lba + prev_num) > t_lba)
+                    regular = false;
+            }
+        }
+        prev_lba = sge.lba;
+        prev_num = sge.num;
+    }           /* end of for loop while still elements and monot true */
+
+    if (k < elems) {    /* only here if above breaks are taken */
+        prev_lba = t_lba;
+        ++k;
+        for ( ; k < elems; ++k) {
+            const class scat_gath_elem & sge = sgl[k];
+
+            degen = false;
+            t_lba = sge.lba;
+            t_num = sge.num;
+            if (0 == t_num) {
+                degen = true;
+                continue;
+            }
+            sum += t_num;
+            end = t_lba + t_num;
+            if (end > high)
+                high = end;
+            if (prev_lba > t_lba) {
+                if (t_lba < low)
+                    low = t_lba;
+            }
+            prev_lba = t_lba;
+        }
+    } else
+        if (! regular)
+            set_weaker_linearity(SGL_MONO_OVERLAP);
+
+    lowest_lba = low;
+    if (degen && (elems > 0)) { /* last element always impacts high_lba_p1 */
+        t_lba = sgl[elems - 1].lba;
+        high_lba_p1 = (t_lba > high) ? t_lba : high;
+    } else
+        high_lba_p1 = high;
+    sum_hard = (elems > 0) ? ! degen : false;
+    if (b_vb)
+        dbg_print(false, id_str, false, show_sgl);
+}
+
+/* Usually will append (or add to start if empty) sge unless 'extra_blks'
+ * exceeds MAX_SGL_NUM_VAL. In that case multiple sge_s are added with
+ * sge.num = MAX_SGL_NUM_VAL or less (for final sge) until extra_blks is
+ * exhausted. Returns new size of scatter gather list. */
+int
+scat_gath_list::append_1or(int64_t extra_blks, int64_t start_lba)
+{
+    int o_num = sgl.size();
+    const int max_nbs = MAX_SGL_NUM_VAL;
+    int64_t cnt = 0;
+    class scat_gath_elem sge;
+
+    if ((extra_blks <= 0) && (start_lba < 0))
+        return o_num;       /* nothing to do */
+    if ((o_num > 0) && (! sum_hard)) {
+        sge = sgl[o_num - 1];   /* assume sge.num==0 */
+        if (sge.lba == (uint64_t)start_lba) {
+            if (extra_blks <= max_nbs)
+                sge.num = extra_blks;
+            else
+                sge.num = max_nbs;
+            sgl[o_num - 1] = sge;
+            cnt = sge.num;
+            sum += cnt;
+            sum_hard = true;
+            if (cnt <= extra_blks) {
+                high_lba_p1 = sge.lba + cnt;
+                return o_num;
+            }
+        }
+    } else if (0 == o_num) {
+        lowest_lba = start_lba;
+        if (0 == extra_blks) {
+            sge.lba = start_lba;
+            sge.num = 0;
+            sgl.push_back(sge);
+            high_lba_p1 = sge.lba;
+            return sgl.size();
+        }
+    }
+    for ( ; cnt < extra_blks; cnt += max_nbs) {
+        sge.lba = start_lba + cnt;
+        if ((extra_blks - cnt) <= max_nbs)
+            sge.num = extra_blks - cnt;
+        else
+            sge.num = max_nbs;
+        sgl.push_back(sge);
+        sum += sge.num;
+    }           /* always loops at least once */
+    sum_hard = true;
+    high_lba_p1 = sge.lba + sge.num;
+    return sgl.size();
+}
+
+int
+scat_gath_list::append_1or(int64_t extra_blks)
+{
+    int o_num = sgl.size();
+
+    if (o_num < 1)
+        return append_1or(extra_blks, 0);
+
+    class scat_gath_elem sge = sgl[o_num - 1];
+
+    return append_1or(extra_blks, sge.lba + sge.num);
+}
+
+bool
+sgls_eq_off(const scat_gath_list & left, int l_e_ind, int l_blk_off,
+            const scat_gath_list & right, int r_e_ind, int r_blk_off,
+            bool allow_partial)
+{
+    int lelems = left.sgl.size();
+    int relems = right.sgl.size();
+
+    while ((l_e_ind < lelems) && (r_e_ind < relems)) {
+        if ((left.sgl[l_e_ind].lba + l_blk_off) !=
+            (right.sgl[r_e_ind].lba + r_blk_off))
+            return false;
+
+        int lrem = left.sgl[l_e_ind].num - l_blk_off;
+        int rrem = right.sgl[r_e_ind].num - r_blk_off;
+
+        if (lrem == rrem) {
+            ++l_e_ind;
+            l_blk_off = 0;
+            ++r_e_ind;
+            r_blk_off = 0;
+        } else if (lrem < rrem) {
+            ++l_e_ind;
+            l_blk_off = 0;
+            r_blk_off += lrem;
+        } else {
+            ++r_e_ind;
+            r_blk_off = 0;
+            l_blk_off += rrem;
+        }
+    }
+    if ((l_e_ind >= lelems) && (r_e_ind >= relems))
+        return true;
+    return allow_partial;
+}
+
+/* If bad arguments returns -1, otherwise returns the lowest LBA in *sglp .
+ * If no elements considered returns 0. If ignore_degen is true than
+ * ignores all elements with sge.num zero unless always_last is also
+ * true in which case the last element is always considered. */
+int64_t
+scat_gath_list::get_lowest_lba(bool ignore_degen, bool always_last) const
+{
+    int k;
+    const int num_elems = sgl.size();
+    bool some = (num_elems > 0);
+    int64_t res = INT64_MAX;
+
+    for (k = 0; k < num_elems; ++k) {
+        if ((0 == sgl[k].num) && ignore_degen)
+            continue;
+        if ((int64_t)sgl[k].lba < res)
+            res = sgl[k].lba;
+    }
+    if (always_last && some) {
+        if ((int64_t)sgl[k - 1].lba < res)
+            res = sgl[k - 1].lba;
+    }
+    return (INT64_MAX == res) ? 0 : res;
+}
+
+/* Returns >= 0 if sgl can be simplified to a single LBA. So an empty sgl
+ * will return 0; a one element sgl will return its LBA. A multiple element
+ * sgl only returns the first element's LBA (that is not degenerate) if the
+ * sgl is monotonic and not fragmented. In the extreme case takes last
+ * element's LBA if all prior elements are degenerate. Else returns -1 .
+ * Assumes sgl_sum_scan() has been called. */
+int64_t
+scat_gath_list::get_low_lba_from_linear() const
+{
+    const int num_elems = sgl.size();
+    int k;
+
+    if (num_elems <= 1)
+        return (1 == num_elems) ? sgl[0].lba : 0;
+    else {
+        if (linearity == SGL_LINEAR) {
+            for (k = 0; k < (num_elems - 1); ++k) {
+                if (sgl[k].num > 0)
+                    return sgl[k].lba;
+            }
+            /* take last element's LBA if all earlier are degenerate */
+            return sgl[k].lba;
+        } else
+            return -1;
+    }
+}
+
+bool
+scat_gath_list::is_pipe_suitable() const
+{
+    return (lowest_lba == 0) && (linearity == SGL_LINEAR);
+}
+
+scat_gath_iter::scat_gath_iter(const scat_gath_list & parent)
+    : sglist(parent), it_el_ind(0), it_blk_off(0), blk_idx(0)
+{
+    int elems = sglist.num_elems();
+
+    if (elems > 0)
+        extend_last = (0 == sglist.sgl[elems - 1].num);
+}
+
+bool
+scat_gath_iter::set_by_blk_idx(int64_t _blk_idx)
+{
+    bool first;
+    int k;
+    const int elems = sglist.sgl.size();
+    const int last_ind = elems - 1;
+    int64_t bc = _blk_idx;
+
+    if (bc < 0)
+        return false;
+
+    if (bc == blk_idx)
+        return true;
+    else if (bc > blk_idx) {
+        k = it_el_ind;
+        bc -= blk_idx;
+    } else
+        k = 0;
+    for (first = true; k < elems; ++k, first = false) {
+        uint32_t num = ((k == last_ind) && extend_last) ? MAX_SGL_NUM_VAL :
+                                                          sglist.sgl[k].num;
+        if (first) {
+            if ((int64_t)(num - it_blk_off) < bc)
+                bc -= (num - it_blk_off);
+            else {
+                it_blk_off = bc + it_blk_off;
+                break;
+            }
+        } else {
+            if ((int64_t)num < bc)
+                bc -= num;
+            else {
+                it_blk_off = (uint32_t)bc;
+                break;
+            }
+        }
+    }
+    it_el_ind = k;
+    blk_idx = _blk_idx;
+
+    if (k < elems)
+        return true;
+    else if ((k == elems) && (0 == it_blk_off))
+        return true;    /* EOL */
+    else
+        return false;
+}
+
+/* Given a blk_count, the iterator (*iter_p) is moved toward the EOL.
+ * Returns true unless blk_count takes iterator two or more past the last
+ * element. So if blk_count takes the iterator to the EOL, this function
+ * returns true. Takes into account iterator's extend_last flag. */
+bool
+scat_gath_iter::add_blks(uint64_t blk_count)
+{
+    bool first;
+    int k;
+    const int elems = sglist.sgl.size();
+    const int last_ind = elems - 1;
+    uint64_t bc = blk_count;
+
+    if (0 == bc)
+        return true;
+    for (first = true, k = it_el_ind; k < elems; ++k) {
+        uint32_t num = ((k == last_ind) && extend_last) ? MAX_SGL_NUM_VAL :
+                                                          sglist.sgl[k].num;
+        if (first) {
+            first = false;
+            if ((uint64_t)(num - it_blk_off) <= bc)
+                bc -= (num - it_blk_off);
+            else {
+                it_blk_off = bc + it_blk_off;
+                break;
+            }
+        } else {
+            if ((uint64_t)num <= bc)
+                bc -= num;
+            else {
+                it_blk_off = (uint32_t)bc;
+                break;
+            }
+        }
+    }
+    it_el_ind = k;
+    blk_idx += blk_count;
+
+    if (k < elems)
+        return true;
+    else if ((k == elems) && (0 == it_blk_off))
+        return true;    /* EOL */
+    else
+        return false;
+}
+
+/* Move the iterator from its current position (which may be to EOL) towards
+ * the start of the sgl (i.e. backwards) for blk_count blocks. Returns true
+ * if iterator is valid after the move, else returns false. N.B. if false is
+ * returned, then the iterator is invalid and may need to set it to a valid
+ * value. */
+bool
+scat_gath_iter::sub_blks(uint64_t blk_count)
+{
+    bool first;
+    int k = it_el_ind;
+    uint64_t bc = 0;
+    const uint64_t orig_blk_count = blk_count;
+
+    if (0 == blk_count)
+        return true;
+    for (first = true; k >= 0; --k) {
+        if (first) {
+            if (blk_count > (uint64_t)it_blk_off)
+                blk_count -= it_blk_off;
+            else {
+                it_blk_off -= blk_count;
+                break;
+            }
+            first = false;
+        } else {
+            uint32_t off = sglist.sgl[k].num;
+
+            bc = blk_count;
+            if (bc > (uint64_t)off)
+                blk_count -= off;
+            else {
+                bc = off - bc;
+                break;
+            }
+        }
+    }
+    if (k < 0) {
+        blk_idx = 0;
+        it_blk_off = 0;
+        return false;           /* bad situation */
+    }
+    if ((int64_t)orig_blk_count <= blk_idx)
+        blk_idx -= orig_blk_count;
+    else
+        blk_idx = 0;
+    it_el_ind = k;
+    if (! first)
+        it_blk_off = (uint32_t)bc;
+    return true;
+}
+
+/* Returns LBA referred to by iterator if valid or returns SG_LBA_INVALID
+ * (-1) if at end or invalid. */
+int64_t
+scat_gath_iter::current_lba() const
+{
+    const int elems = sglist.sgl.size();
+    int64_t res = SG_LBA_INVALID; /* for at end or invalid (-1) */
+
+    if (it_el_ind < elems) {
+        class scat_gath_elem sge = sglist.sgl[it_el_ind];
+
+        if ((uint32_t)it_blk_off < sge.num)
+            return sge.lba + it_blk_off;
+        else if (((uint32_t)it_blk_off == sge.num) &&
+                 ((it_el_ind + 1) < elems)) {
+            class scat_gath_iter iter(*this);
+
+            ++iter.it_el_ind;
+            iter.it_blk_off = 0;
+            /* worst case recursion will stop at end of sgl */
+            return iter.current_lba();
+        }
+    }
+    return res;
+}
+
+int64_t
+scat_gath_iter::current_lba_rem_num(int & rem_num) const
+{
+    const int elems = sglist.sgl.size();
+    int64_t res = SG_LBA_INVALID; /* for at end or invalid (-1) */
+
+    if (it_el_ind < elems) {
+        class scat_gath_elem sge = sglist.sgl[it_el_ind];
+
+        if ((uint32_t)it_blk_off < sge.num) {
+            rem_num = sge.num - it_blk_off;
+            return sge.lba + it_blk_off;
+        } else if (((uint32_t)it_blk_off == sge.num) &&
+                 ((it_el_ind + 1) < elems)) {
+            class scat_gath_iter iter(*this);
+
+            ++iter.it_el_ind;
+            iter.it_blk_off = 0;
+            /* worst case recursion will stop at end of sgl */
+            return iter.current_lba_rem_num(rem_num);
+        }
+    }
+    rem_num = -1;
+    return res;
+}
+
+class scat_gath_elem
+scat_gath_iter::current_elem() const
+{
+    const int elems = sglist.sgl.size();
+    class scat_gath_elem sge;
+
+    sge.make_bad();
+    if (it_el_ind < elems)
+        return sglist.sgl[it_el_ind];
+    return sge;
+}
+
+/* Returns true of no sgl or sgl is at the end [elems, 0], otherwise it
+ * returns false. */
+bool
+scat_gath_iter::at_end() const
+{
+    const int elems = sglist.sgl.size();
+
+    return ((0 == elems) || ((it_el_ind == elems) && (0 == it_blk_off)));
+}
+
+/* Returns true if associated iterator is monotonic (increasing) and not
+ * fragmented. Empty sgl and single element degenerate considered linear.
+ * Assumes sgl_sum_scan() has been called on sgl. */
+bool
+scat_gath_iter::is_sgl_linear() const
+{
+    return sglist.linearity == SGL_LINEAR;
+}
+
+/* Should return 1 or more unless max_n<=0 or at_end() */
+int
+scat_gath_iter::linear_for_n_blks(int max_n) const
+{
+    int k, rem;
+    const int elems = sglist.sgl.size();
+    uint64_t prev_lba;
+    class scat_gath_elem sge;
+
+    if (at_end() || (max_n <= 0))
+        return 0;
+    sge = sglist.sgl[it_el_ind];
+    rem = (int)sge.num - it_blk_off;
+    if (rem <= 0) {
+        sge = sglist.sgl[it_el_ind + 1];
+        rem = (int)sge.num;
+    }
+    if (max_n <= rem)
+        return max_n;
+    prev_lba = sge.lba + sge.num;
+    for (k = it_el_ind + 1; k < elems; ++k) {
+        sge = sglist.sgl[k];
+        if (sge.lba != prev_lba)
+            return rem;
+        rem += sge.num;
+        if (max_n <= rem)
+            return max_n;
+        prev_lba = sge.lba + sge.num;
+    }
+    return rem;
+}
+
+/* id_str may be NULL (if so replace by "unknown"), present to enhance verbose
+ * output. */
+void
+scat_gath_iter::dbg_print(const char * id_str, bool to_stdout,
+                          int verbose) const
+{
+    const char * caller = id_str ? id_str : "unknown";
+    FILE * fp = to_stdout ? stdout : stderr;
+
+    fprintf(fp, "%s: it_el_ind=%d, it_blk_off=%d, blk_idx=%" PRId64 "\n",
+            caller, it_el_ind, it_blk_off, blk_idx);
+    fprintf(fp, "  extend_last=%d\n", extend_last);
+    if (verbose)
+        sglist.dbg_print(false, " iterator's", to_stdout, verbose > 1);
+}
+
+/* Calculates difference between iterators, logically: res <-- lhs - rhs
+ * Checks that lhsp and rhsp have same underlying sgl, if not returns
+ * INT_MIN. Assumes iterators close enough for result to lie in range
+ * from (-INT_MAX) to INT_MAX (inclusive). */
+int
+diff_between_iters(const class scat_gath_iter & left,
+                   const class scat_gath_iter & right)
+{
+    int res, k, r_e_ind, l_e_ind;
+
+    if (&left.sglist != &right.sglist) {
+        pr2serr("%s: bad args\n", __func__);
+        return INT_MIN;
+    }
+    r_e_ind = right.it_el_ind;
+    l_e_ind = left.it_el_ind;
+    if (l_e_ind < r_e_ind) { /* so difference will be negative */
+        res = diff_between_iters(right, left);        /* cheat */
+        if (INT_MIN == res)
+            return res;
+        return -res;
+    } else if (l_e_ind == r_e_ind)
+        return (int)left.it_blk_off - (int)right.it_blk_off;
+    /* (l_e_ind > r_e_ind) so (lhs > rhs) */
+    res = (int)right.sglist.sgl[r_e_ind].num - right.it_blk_off;
+    for (k = 1; (r_e_ind + k) < l_e_ind; ++k) {
+        // pr2serr("%s: k=%d, res=%d, num=%d\n", __func__, k, res,
+        //         (int)right.sglist.sgl[r_e_ind + k].num);
+        res += (int)right.sglist.sgl[r_e_ind + k].num;
+    }
+    res += left.it_blk_off;
+    // pr2serr("%s: at exit res=%d\n", __func__, res);
+    return res;
+}
+
+/* Compares from the current iterator positions of left and left until
+ * the shorter list is exhausted. Returns false on the first inequality.
+ * If no inequality and both remaining lists are same length then returns
+ * true. If no inequality but remaining lists differ in length then returns
+ * allow_partial. */
+bool
+sgls_eq_from_iters(const class scat_gath_iter & left,
+                   const class scat_gath_iter & right,
+                   bool allow_partial)
+{
+    return sgls_eq_off(left.sglist, left.it_el_ind, left.it_blk_off,
+                       right.sglist, right.it_el_ind, right.it_blk_off,
+                       allow_partial);
+}
diff --git a/testing/sg_scat_gath.h b/testing/sg_scat_gath.h
new file mode 100644
index 0000000..d316a7b
--- /dev/null
+++ b/testing/sg_scat_gath.h
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2014-2020 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+// C standard headers
+#include <stdio.h>
+#include <stdint.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+// C++ standard headers
+#include <vector>
+
+// This file is a C++ header file
+
+
+#define SG_SGL_MAX_ELEMENTS 16384
+
+#define SG_COUNT_INDEFINITE (-1)
+#define SG_LBA_INVALID SG_COUNT_INDEFINITE
+
+// Sizing matches largest SCSI READ and WRITE commands plus those of Unix
+// read(2)s and write(2)s. User can give larger than 31 bit 'num's but they
+// are split into several consecutive elements.
+class scat_gath_elem {
+public:
+    uint64_t lba;       // of start block
+    uint32_t num;       // number of blocks from and including start block
+
+    void make_bad() { lba = UINT64_MAX; num = UINT32_MAX; }
+    bool is_bad() const { return (lba == UINT64_MAX && num == UINT32_MAX); }
+};
+
+// Consider "linearity" as a scatter gather list property. Elements of this
+// of from the strongest form to the weakest.
+enum sgl_linearity_e {
+    SGL_LINEAR = 0,     // empty list and 0,0 considered linear
+    SGL_MONOTONIC,      // since not linear, implies holes
+    SGL_MONO_OVERLAP,   // monotonic but same LBA in two or more elements
+    SGL_NON_MONOTONIC   // weakest
+};
+
+
+// Holds one scatter gather list and its associated metadata
+class scat_gath_list {
+public:
+    scat_gath_list() : linearity(SGL_LINEAR), sum_hard(false), m_errno(0),
+        high_lba_p1(0), lowest_lba(0), sum(0) { }
+
+    scat_gath_list(const scat_gath_list &) = default;
+    scat_gath_list & operator=(const scat_gath_list &) = default;
+    ~scat_gath_list() = default;
+
+    bool empty() const;
+    bool empty_or_00() const;
+    int num_elems() const;
+    int64_t get_lowest_lba(bool ignore_degen, bool always_last) const;
+    int64_t get_low_lba_from_linear() const;
+    bool is_pipe_suitable() const;
+
+    friend bool sgls_eq_off(const scat_gath_list &left, int l_e_ind,
+                            int l_blk_off,
+                            const scat_gath_list &right, int r_e_ind,
+                            int r_blk_off, bool allow_partial);
+
+    bool load_from_cli(const char * cl_p, bool b_vb);
+    bool load_from_file(const char * file_name, bool def_hex, bool flexible,
+                        bool b_vb);
+    int append_1or(int64_t extra_blks, int64_t start_lba);
+    int append_1or(int64_t extra_blks);
+
+    void dbg_print(bool skip_meta, const char * id_str, bool to_stdout,
+                   bool show_sgl) const;
+
+    // calculates and sets following bool-s and int64_t-s
+    void sum_scan(const char * id_str, bool show_sgl, bool b_verbose);
+
+    void set_weaker_linearity(enum sgl_linearity_e lin);
+    enum sgl_linearity_e linearity;
+    const char * linearity_as_str() const;
+
+    bool sum_hard;      // 'num' in last element of 'sgl' is > 0
+    int m_errno;        // OS failure errno
+    int64_t high_lba_p1;  // highest LBA plus 1, next write from and above
+    int64_t lowest_lba; // initialized to 0
+    int64_t sum;        // of all 'num' elements in 'sgl'
+
+    friend int diff_between_iters(const class scat_gath_iter & left,
+                                  const class scat_gath_iter & right);
+
+private:
+    friend class scat_gath_iter;
+
+    bool file2sgl_helper(FILE * fp, const char * fnp, bool def_hex,
+                         bool flexible, bool b_vb);
+
+    std::vector<scat_gath_elem> sgl;  // an array on heap [0..num_elems())
+};
+
+
+class scat_gath_iter {
+public:
+    explicit scat_gath_iter(const scat_gath_list & my_scat_gath_list);
+    scat_gath_iter(const scat_gath_iter & src) = default;
+    scat_gath_iter&  operator=(const scat_gath_iter&) = delete;
+    ~scat_gath_iter() = default;
+
+    int64_t current_lba() const;
+    int64_t current_lba_rem_num(int & rem_num) const;
+    class scat_gath_elem current_elem() const;
+    bool at_end() const;
+    bool is_sgl_linear() const; // the whole list
+    // Should return 1 or more unless max_n<=0 or at_end()
+    int linear_for_n_blks(int max_n) const;
+
+    bool set_by_blk_idx(int64_t _blk_idx);
+    // add/sub blocks return true if they reach EOL/start, else false
+    bool add_blks(uint64_t blk_count);
+    bool sub_blks(uint64_t blk_count);
+
+    void dbg_print(const char * id_str, bool to_stdout, int verbose) const;
+
+    friend int diff_between_iters(const class scat_gath_iter & left,
+                                  const class scat_gath_iter & right);
+
+    friend bool sgls_eq_from_iters(const class scat_gath_iter & left,
+                                   const class scat_gath_iter & right,
+                                   bool allow_partial);
+
+private:
+    const scat_gath_list &sglist;
+
+    // dual representation: either it_el_ind,it_blk_off or blk_idx
+    int it_el_ind;      // refers to sge==sglist[it_el_ind]
+    int it_blk_off;     // refers to LBA==(sge.lba + it_blk_off)
+    int64_t blk_idx;    // in range: [0 .. sglist.sum)
+    bool extend_last;
+};
diff --git a/testing/sg_sense_test.c b/testing/sg_sense_test.c
new file mode 100644
index 0000000..ce66cf3
--- /dev/null
+++ b/testing/sg_sense_test.c
@@ -0,0 +1,204 @@
+/*
+ *  Copyright (C) 2004-2018 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+/* This is a simple program that tests the sense data descriptor format
+ * printout function in sg_lib.c . */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <getopt.h>
+
+#include "sg_lib.h"
+
+
+#define EBUFF_SZ 256
+
+#define ME "sg_sense_test: "
+
+static const char * version_str = "2.04 20181207";
+
+static struct option long_options[] = {
+        {"help", no_argument, 0, 'h'},
+        {"leadin",  required_argument, 0, 'l'},
+        {"stdout",  no_argument, 0, 's'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},   /* sentinel */
+};
+
+
+static void
+usage()
+{
+    fprintf(stderr,
+            "Usage: %s [--help] [--leadin=STR] [--stdout] [--verbose] "
+            "[--version]\n"
+            "  where: --help|-h          print out usage message\n"
+            "         --leadin=STR|-l STR    every line output by --sense "
+            "should\n"
+            "                                be prefixed by STR\n"
+            "         --stdout|-s        send output to stdout (def: "
+            "stderr)\n"
+            "         --verbose|-v       increase verbosity\n"
+            "         --version|-V       print version string and exit\n\n"
+            "Test sense data handling of sg_lib. Overlaps somewhat with "
+            "tst_sg_lib\n", ME
+           );
+
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool to_stdout = false;
+    int c, k, prev_len;
+    int verbose = 0;
+    const char * leadin = NULL;
+    FILE * outfp = stderr;
+    uint8_t err1[] = {0x72, 0x5, 0x24, 0x0, 0, 0, 0, 32,
+                      0x2, 0x6, 0, 0, 0xc8, 0x0, 0x3, 0,
+                      0, 0xa, 0x80, 0, 1, 2, 3, 4,
+                      0xaa, 0xbb, 0xcc, 0xdd,
+                      1, 0xa, 0, 0, 1, 2, 3, 4,
+                      0xaa, 0xbb, 0xee, 0xff};
+    uint8_t err2[] = {0x72, SPC_SK_MEDIUM_ERROR, 0x11, 0xb, 0x80, 0, 0,
+                      32,
+                      0x2, 0x6, 0, 0, 0xc8, 0x0, 0x3, 0,
+                      0, 0xa, 0x80, 0, 1, 2, 3, 4,
+                      0xaa, 0xbb, 0xcc, 0xdd,
+                      1, 0xa, 0, 0, 1, 2, 3, 4,
+                      0xaa, 0xbb, 0xee, 0xff};
+                     /* Set SDAT_OVFL */
+    uint8_t err3[] = {0x72, SPC_SK_NO_SENSE, 0x4, 0x4, 0, 0, 0, 8,
+                      0x2, 0x6, 0, 0, 0xc8, 0x12, 0x34, 0};
+    uint8_t err4[] = {0x73, SPC_SK_COPY_ABORTED, 0x8, 0x4, 0, 0, 0, 22,
+                      0x2, 0x6, 0, 0, 0xc8, 0x0, 0x3, 0,
+                      0x3, 0x2, 0, 0x55,
+                      0x5, 0x2, 0, 0x20,
+                      0x85, 0x4, 0, 0x20, 0x33, 0x44};
+                     /* Set Filemark, EOM, ILI and SDAT_OVFL */
+    uint8_t err5[] = {0xf1, 0, (0xf0 | SPC_SK_ILLEGAL_REQUEST), 0x11,
+                      0x22, 0x33, 0x44, 0xa,
+                      0x0, 0x0, 0, 0, 0x4, 0x1, 0, 0xcf, 0, 5,};
+    uint8_t err6[] = {0x72, SPC_SK_NO_SENSE, 0x4, 0x1, 0, 0, 0, 14,
+                      0x9, 0xc, 1, 0, 0x11, 0x22, 0x66, 0x33,
+                      0x77, 0x44, 0x88, 0x55, 0x1, 0x2};
+    uint8_t err7[] = {0xf1, 0, 0xe5, 0x11, 0x22, 0x33, 0x44, 0xa,
+                      0x0, 0x0, 0x0, 0x0, 0x24, 0x1, 0xbb,
+                      0xc9, 0x0, 0x2};
+                     /* Vendor specific, with "valid" bit set */
+    uint8_t err8[] = {0xff, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc,
+                      0xd, 0xe, 0xf, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99,
+                      0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0};
+    char b[2048];
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "hl:svV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'l':
+            leadin = optarg;
+            break;
+        case 's':
+            to_stdout = true;
+            break;
+        case 'v':
+            ++verbose;
+            break;
+        case 'V':
+            fprintf(stderr, "version: %s\n", version_str);
+            return 0;
+        default:
+            fprintf(stderr, "unrecognised switch code 0x%x ??\n", c);
+            usage();
+            return 1;
+        }
+    }
+    if (optind < argc) {
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                fprintf(stderr, "Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return 1;
+        }
+    }
+    if (to_stdout) {
+        outfp = stdout;
+        sg_set_warnings_strm(outfp);
+    }
+
+    fprintf(outfp, "err1 test:\n");
+    sg_print_sense(leadin, err1, sizeof(err1), verbose /* raw_info */);
+    fprintf(outfp, "\n");
+    fprintf(outfp, "err2 test:\n");
+    sg_print_sense(leadin, err2, sizeof(err2), verbose);
+    fprintf(outfp, "\n");
+    fprintf(outfp, "err3 test:\n");
+    sg_print_sense(leadin, err3, sizeof(err3), verbose);
+    fprintf(outfp, "\n");
+    fprintf(outfp, "err4 test:\n");
+    sg_print_sense(leadin, err4, sizeof(err4), verbose);
+    fprintf(outfp, "\n");
+    fprintf(outfp, "err5 test: Set Filemark, EOM, ILI and SDAT_OVFL\n");
+    sg_print_sense(leadin, err5, sizeof(err5), verbose);
+    fprintf(outfp, "\n");
+    fprintf(outfp, "err6 test:\n");
+    sg_print_sense(leadin, err6, sizeof(err6), verbose);
+    fprintf(outfp, "\n");
+    fprintf(outfp, "err7 test:\n");
+    sg_print_sense(leadin, err7, sizeof(err7), verbose);
+    fprintf(outfp, "\n");
+    fprintf(outfp, "err8 test (vendor specific):\n");
+    sg_print_sense(leadin, err8, sizeof(err8), verbose);
+    fprintf(outfp, "\n");
+
+    if (verbose > 1) {
+        fprintf(outfp, "\n\nTry different output string sizes with "
+               "sg_get_sense_str(err2):\n");
+        for (k = 1, prev_len = -1; k < 512; ++k) {
+            /* snprintf(leadin, sizeof(leadin), "blen=%d", k); */
+            sg_get_sense_str(NULL, err2, sizeof(err2), 0, k, b);
+            fprintf(outfp, "%s\n", b);
+            if (prev_len == (int)strlen(b))
+                break;
+            else
+                prev_len = strlen(b);
+        }
+    }
+
+    if (verbose > 2) {
+        fprintf(outfp, "\n\nTry different output string sizes with "
+               "sg_get_sense_str(err4):\n");
+        for (k = 1, prev_len = -1; k < 512; ++k) {
+            /* snprintf(leadin, sizeof(leadin), "blen=%d", k); */
+            sg_get_sense_str(NULL, err4, sizeof(err4), 0, k, b);
+            fprintf(outfp, "%s\n", b);
+            if (prev_len == (int)strlen(b))
+                break;
+            else
+                prev_len = strlen(b);
+        }
+    }
+    return 0;
+}
diff --git a/testing/sg_take_snap.c b/testing/sg_take_snap.c
new file mode 100644
index 0000000..c714a40
--- /dev/null
+++ b/testing/sg_take_snap.c
@@ -0,0 +1,225 @@
+/* A utility program originally written for the Linux OS SCSI subsystem.
+ *  Copyright (C) 2021 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is experimental. It allows the SG_CTL_FLAGM_SNAP_DEV
+ * variant of ioctl(SG_SET_GET_EXTENDED) to be called. This assumes
+ * a Linux sg driver whose version number > 4.00.30 .
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <getopt.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_pr2serr.h"
+
+
+#define ME "sg_take_snap: "
+
+static const char * version_str = "1.01 20210403";
+
+#define SG_TAKE_MAX_DEVS 16
+
+static const char *dev_arr[SG_TAKE_MAX_DEVS];
+static int next_vacant_dev_idx = 0;
+
+static struct option long_options[] = {
+        {"clear", no_argument, 0, 'c'},
+        {"help", no_argument, 0, 'h'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},
+};
+
+static void
+usage(void)
+{
+    pr2serr("Usage: sg_take_snap [--clear] [--help] [--verbose] [--version] "
+            "DEVICE*\n"
+            "  where:\n"
+            "    --clear|-c      set 'clear_first' flag; otherwise appends\n"
+            "    --help|-h       print usage information then exit\n"
+            "    --verbose|-v    increase the level of verbosity\n"
+            "    --version|-V    print version number then exit\n\n"
+            "Use ioctl(SG_SET_GET_EXTENDED(SG_CTL_FLAGM_SNAP_DEV)) to take "
+            "snap .\nThe output is placed in /sys/kernel/debug/scsi_generic/"
+            "snapped and needs\nroot permissions to read. Requires a Linux "
+            "sg driver version > 4.00.30 .\nOne or more DEVICEs can be "
+            "given. Note: sending the ioctl to do this\ncreates some "
+            "'noise' in the output\n"
+           );
+}
+
+
+int main(int argc, char * argv[])
+{
+    bool clear_first = false;
+    int c, k, sg_fd, res;
+    int ret = 0;
+    int verbose = 0;
+    const char * device_name = NULL;
+    struct sg_extended_info sei;
+    struct sg_extended_info * seip;
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "chvV", long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            clear_first = true;
+            break;
+        case 'h':
+            usage();
+            return 0;
+        case 'v':
+            ++verbose;
+            break;
+        case 'V':
+            pr2serr(ME "version: %s\n", version_str);
+            return 0;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+    if (optind < argc) {
+        for (; optind < argc; ++optind) {
+            if (next_vacant_dev_idx < SG_TAKE_MAX_DEVS) {
+                dev_arr[next_vacant_dev_idx] = argv[optind];
+                ++next_vacant_dev_idx;
+            } else if (next_vacant_dev_idx == SG_TAKE_MAX_DEVS) {
+                pr2serr("Maximum of %d DEVICEs on command line\n",
+                        next_vacant_dev_idx);
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            } else {
+                pr2serr("something is wrong ...\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        }
+    }
+    if (NULL == dev_arr[0]) {
+        pr2serr("Need at least one DEVICE name. Use '--help' to see "
+                "usage.\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    for (k = 0; k < next_vacant_dev_idx; ++k) {
+        device_name = dev_arr[k];
+        sg_fd = open(device_name, O_RDWR | O_NONBLOCK);
+        if (sg_fd < 0) {
+            int err = errno;
+
+            ret = sg_convert_errno(err);
+            pr2serr(ME "open error: %s: ", device_name);
+            perror("");
+            sg_fd = -1;
+            goto fini;
+        }
+        if (0 == k) {
+            int t;
+
+            res = ioctl(sg_fd, SG_GET_VERSION_NUM, &t);
+            if ((res < 0) || (t < 30000)) {
+                pr2serr("sg driver prior to 3.0.00\n");
+                ret = SG_LIB_FILE_ERROR;
+                goto fini;
+            }
+            if (verbose) {
+                pr2serr("sg driver version: %d.%02d.%02d\n",
+                        t / 10000, (t % 10000) / 100, t % 100);
+            }
+            if (t < 40000) {
+                pr2serr("Warning: sg driver prior to 4.0.00\n");
+                ret = SG_LIB_FILE_ERROR;
+                goto fini;
+            } else if (t < 40045) {
+                pr2serr("Warning: sg driver prior to 4.0.45\n");
+                ret = SG_LIB_FILE_ERROR;
+                goto fini;
+            }
+        }
+
+        seip = &sei;
+        memset(seip, 0, sizeof(*seip));
+        seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+        seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS;
+        seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_SNAP_DEV;
+        if (clear_first)    /* ... else 0 (due to memset) --> append */
+            seip->ctl_flags |= SG_CTL_FLAGM_SNAP_DEV;
+        if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+            pr2serr("ioctl(SG_SET_GET_EXTENDED(SG_CTL_FLAGM_SNAP_DEV)), %s "
+                    "failed errno=%d %s\n", device_name, errno,
+                    strerror(errno));
+            ret = SG_LIB_FILE_ERROR;
+            goto fini;
+        }
+        if (verbose)
+            pr2serr("ioctl(%s, SG_SET_GET_EXTENDED(SG_CTL_FLAGM_SNAP_DEV)) "
+                    "ok\n", device_name);
+        res = close(sg_fd);
+        sg_fd = -1;
+        if (res < 0) {
+            pr2serr("close errno=%d on %s\n", errno, device_name);
+            ret = res;
+            goto fini;
+        }
+    }
+
+fini:
+    if (sg_fd >= 0) {
+        res = close(sg_fd);
+        if (res < 0) {
+            res = sg_convert_errno(errno);
+            perror(ME "close error");
+            if (0 == ret)
+                ret = res;
+        }
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/testing/sg_tst_async.cpp b/testing/sg_tst_async.cpp
new file mode 100644
index 0000000..8904e37
--- /dev/null
+++ b/testing/sg_tst_async.cpp
@@ -0,0 +1,2227 @@
+/*
+ * Copyright (c) 2014-2022 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <iostream>
+#include <vector>
+#include <map>
+#include <list>
+#include <system_error>
+#include <thread>
+#include <mutex>
+#include <chrono>
+#include <atomic>
+#include <random>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <poll.h>
+#include <errno.h>
+#include <ctype.h>
+#include <time.h>
+#include <limits.h>
+#include <getopt.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/resource.h>       /* getrusage */
+
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pt.h"
+#include "sg_cmds.h"
+
+static const char * version_str = "1.42 20220425";
+static const char * util_name = "sg_tst_async";
+
+/* This is a test program for checking the async usage of the Linux sg
+ * driver. Each thread opens 1 file descriptor to the next sg device (1
+ * or more can be given on the command line) and then starts up to
+ * num_per_thread commands or more while checking with the poll command (or
+ * ioctl(SG_GET_NUM_WAITING) ) for the completion of those commands. Each
+ * command has a unique "pack_id" which is a sequence starting at 1.
+ * Either TEST UNIT UNIT, READ(16) or WRITE(16) commands are issued.
+ *
+ * This is C++ code with some things from C++11 (e.g. threads) and was
+ * only just able to compile (when some things were reverted) with gcc/g++
+ * version 4.7.3 found in Ubuntu 13.04 . C++11 "feature complete" support
+ * was not available until g++ version 4.8.1 . It should build okay on
+ * recent distributions.
+ *
+ * The build uses various object files from the <sg3_utils>/lib directory
+ * which is assumed to be a sibling of this examples directory. Those
+ * object files in the lib directory can be built with:
+ *   cd <sg3_utils_package_root> ; ./configure ; cd lib; make
+ *   cd ../testing
+ *   make sg_tst_async
+ *
+ * Currently this utility is Linux only and uses the sg driver. The bsg
+ * driver is known to be broken (it doesn't match responses to the
+ * correct file descriptor that requested them). Around Linux kernel 4.15
+ * the async capability of the bsg driver was removed. So this test code
+ * no longer appiles to the bsg driver.
+ *
+ * BEWARE: >>> This utility will modify a logical block (default LBA 1000)
+ * on the given device _when_ the '-W' option is given.
+ *
+ */
+
+using namespace std;
+using namespace std::chrono;
+
+#define DEF_NUM_PER_THREAD 1000
+#define DEF_NUM_THREADS 4
+#define DEF_WAIT_MS 10          /* 0: yield or no wait */
+#define DEF_NANOSEC_WAIT 25000  /* 25 microsecs */
+#define DEF_TIMEOUT_MS 20000    /* 20 seconds */
+#define DEF_LB_SZ 512
+#define DEF_BLOCKING 0
+#define DEF_DIRECT false        /* true: direct_io */
+#define DEF_MMAP_IO false       /* true: mmap-ed IO with sg */
+#define DEF_NO_XFER 0
+#define DEF_LBA 1000U
+
+#define MAX_Q_PER_FD 16383      /* sg driver per file descriptor limit */
+#define MAX_CONSEC_NOMEMS 4     /* was 16 */
+#define URANDOM_DEV "/dev/urandom"
+
+#ifndef SG_FLAG_Q_AT_TAIL
+#define SG_FLAG_Q_AT_TAIL 0x10
+#endif
+#ifndef SG_FLAG_Q_AT_HEAD
+#define SG_FLAG_Q_AT_HEAD 0x20
+#endif
+
+
+#define DEF_PT_TIMEOUT 60       /* 60 seconds */
+
+#define EBUFF_SZ 256
+
+static mutex console_mutex;
+static mutex rand_lba_mutex;
+static atomic<int> async_starts(0);
+static atomic<int> sync_starts(0);
+static atomic<int> async_finishes(0);
+static atomic<int> start_ebusy_count(0);
+static atomic<int> start_e2big_count(0);
+static atomic<int> start_eagain_count(0);
+static atomic<int> fin_eagain_count(0);
+static atomic<int> fin_ebusy_count(0);
+static atomic<int> start_edom_count(0);
+static atomic<int> enomem_count(0);
+static atomic<int> uniq_pack_id(1);
+// static atomic<int> generic_errs(0);
+
+static int page_size = 4096;   /* rough guess, will ask sysconf() */
+
+enum command2execute {SCSI_TUR, SCSI_READ16, SCSI_WRITE16};
+/* Linux Block layer queue disciplines: */
+enum blkLQDiscipline {BLQ_DEFAULT, BLQ_AT_HEAD, BLQ_AT_TAIL};
+/* Queue disciplines of this utility. When both completions and
+ * queuing a new command are both possible: */
+enum myQDiscipline {MYQD_LOW,   /* favour completions over new cmds */
+                    MYQD_MEDIUM,
+                    MYQD_HIGH}; /* favour new cmds over completions */
+
+struct opts_t {
+    vector<const char *> dev_names;
+    vector<int> blk_szs;
+    bool block;
+    bool cmd_time;
+    bool direct;
+    bool excl;
+    bool generic_sync;
+    bool masync;
+    bool mmap_io;
+    bool no_xfer;
+    bool pack_id_force;
+    bool sg_vn_ge_40000;
+    bool sg_vn_ge_40030;
+    bool submit;
+    bool verbose_given;
+    bool v3;
+    bool v3_given;
+    bool v4;
+    bool v4_given;
+    bool version_given;
+    int maxq_per_thread;
+    int num_per_thread;
+    uint64_t lba;
+    unsigned int hi_lba;        /* last one, inclusive range */
+    vector<unsigned int> hi_lbas; /* only used when hi_lba=-1 */
+    int lb_sz;
+    int num_lbs;
+    int ovn;            /* override number for submission */
+    int stats;
+    int verbose;
+    int wait_ms;
+    command2execute c2e;
+    blkLQDiscipline blqd;       /* --qat= 0|1 -> at_head|at_tail */
+    myQDiscipline myqd;         /* --qfav= value (def: 2 --> MYQD_HIGH) */
+};
+
+static struct opts_t a_opts;    /* Expect zero fill on simple types */
+
+static int pr_rusage(int id);
+
+#if 0
+class Rand_uint {
+public:
+    Rand_uint(unsigned int lo, unsigned int hi) : p{lo, hi} {}
+    unsigned int operator()() const { return r(); }
+private:
+    uniform_int_distribution<unsigned int>::param_type p;
+    auto r = bind(uniform_int_distribution<unsigned int>{p},
+                  default_random_engine());
+    /* compiler thinks auto should be a static, bs again? */
+};
+#endif
+
+#if 0
+class Rand_uint {
+public:
+    Rand_uint(unsigned int lo, unsigned int hi, unsigned int my_seed)
+        : r(bind(uniform_int_distribution<unsigned int>{lo, hi},
+                 default_random_engine())) { r.seed(myseed); }
+    unsigned int operator()() const { return r(); }
+private:
+    function<unsigned int()> r;
+};
+#endif
+
+/* Use this class to wrap C++11 <random> features to produce uniform random
+ * unsigned ints in the range [lo, hi] (inclusive) given a_seed */
+class Rand_uint {
+public:
+    Rand_uint(unsigned int lo, unsigned int hi, unsigned int a_seed)
+        : uid(lo, hi), dre(a_seed) { }
+    /* uid ctor takes inclusive range when integral type */
+
+    unsigned int get() { return uid(dre); }
+
+private:
+    uniform_int_distribution<unsigned int> uid;
+    default_random_engine dre;
+};
+
+static struct option long_options[] = {
+        {"v3", no_argument, 0, '3'},
+        {"v4", no_argument, 0, '4'},
+        {"more-async", no_argument, 0, 'a'},
+        {"more_async", no_argument, 0, 'a'},
+        {"masync", no_argument, 0, 'a'},
+        {"cmd-time", no_argument, 0, 'c'},
+        {"cmd_time", no_argument, 0, 'c'},
+        {"direct", no_argument, 0, 'd'},
+        {"excl", no_argument, 0, 'e'},
+        {"force", no_argument, 0, 'f'},
+        {"generic-sync", no_argument, 0, 'g'},
+        {"generic_sync", no_argument, 0, 'g'},
+        {"help", no_argument, 0, 'h'},
+        {"lba", required_argument, 0, 'l'},
+        {"lbsz", required_argument, 0, 'L'},
+        {"maxqpt", required_argument, 0, 'M'},
+        {"mmap-io", no_argument, 0, 'm'},
+        {"mmap_io", no_argument, 0, 'm'},
+        {"numpt", required_argument, 0, 'n'},
+        {"num-pt", required_argument, 0, 'n'},
+        {"num_pt", required_argument, 0, 'n'},
+        {"noxfer", no_argument, 0, 'N'},
+        {"override", required_argument, 0, 'O'},
+        {"pack-id", no_argument, 0, 'p'},
+        {"pack_id", no_argument, 0, 'p'},
+        {"qat", required_argument, 0, 'q'},
+        {"qfav", required_argument, 0, 'Q'},
+        {"read", no_argument, 0, 'R'},
+        {"stats", no_argument, 0, 'S'},
+        {"submit", no_argument, 0, 'u'},
+        {"szlb", required_argument, 0, 's'},
+        {"tnum", required_argument, 0, 't'},
+        {"tur", no_argument, 0, 'T'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {"wait", required_argument, 0, 'w'},
+        {"write", no_argument, 0, 'W'},
+        {0, 0, 0, 0},
+};
+
+
+static void
+usage(void)
+{
+    printf("Usage: %s [--cmd-time] [--direct] [--excl] [--force]\n"
+           "                    [--generic-sync] [--help] [--lba=LBA+] "
+           "[--lbsz=LBSZ]\n"
+           "                    [--masync] [--maxqpt=QPT] [--mmap-io] "
+           "[--no-waitq]\n"
+           "                    [--noxfer] [--numpt=NPT] [--override=OVN] "
+           "[--pack-id]\n"
+           "                    [--qat=AT] [-qfav=FAV] [--read] [--stats] "
+           "[--submit]\n"
+           "                    [--szlb=LB[,NLBS]] [--tnum=NT] [--tur] "
+           "[--v3] [--v4]\n"
+           "                    [--verbose] [--version] [--wait=MS] "
+           "[--write]\n"
+           "                    <sg_disk_device>*\n",
+           util_name);
+    printf("  where\n");
+    printf("    --cmd-time|-c    calculate per command average time (ns)\n");
+    printf("    --direct|-d     do direct_io (def: indirect)\n");
+    printf("    --excl|-e       do wait_exclusive calls\n");
+    printf("    --force|-f      force: any sg device (def: only scsi_debug "
+           "owned)\n");
+    printf("                    WARNING: <lba> written to if '-W' given\n");
+    printf("    --generic-sync|-g    use generic synchronous SG_IO ioctl "
+           "instead\n");
+    printf("                       of Linux sg driver assuming /dev/sg* "
+           "(def)\n");
+    printf("    --help|-h       print this usage message then exit\n");
+    printf("    --lba=LBA|-l LBA    logical block to access (def: %u)\n",
+           DEF_LBA);
+    printf("    --lba=LBA,HI_LBA|-l LBA,HI_LBA    logical block range "
+           "(inclusive)\n"
+           "                          if hi_lba=-1 assume last block on "
+           "device\n");
+    printf("    --lbsz=LBSZ|-L LBSZ    logical block size in bytes (def: "
+           "512)\n"
+           "                           should be power of 2 (0 --> 512)\n");
+    printf("    --masync|-a     set 'more async' flag on devices\n");
+    printf("    --maxqpt=QPT|-M QPT    maximum commands queued per thread "
+           "(def:%d)\n", MAX_Q_PER_FD);
+    printf("    --mmap-io|-m    mmap-ed IO (1 cmd outstanding per thread)\n");
+    printf("    --noxfer|-N          no data xfer (def: xfer on READ and "
+           "WRITE)\n");
+    printf("    --numpt=NPT|-n NPT    number of commands per thread "
+           "(def: %d)\n", DEF_NUM_PER_THREAD);
+    printf("    --override OVN|-O OVN    override FAV=2 when OVN queue "
+           "depth\n"
+           "                             reached (def: 0 -> no override)\n");
+    printf("    --pack-id|-p    set FORCE_PACK_ID, pack-id input to "
+           "read/finish\n");
+    printf("    --qat=AT|-q AT       AT=0: q_at_head; AT=1: q_at_tail (def: "
+           "(drv): head)\n");
+    printf("    --qfav=FAV|-Q FAV    FAV=0: favour completions (smaller q),\n"
+           "                         FAV=1: medium,\n"
+           "                         FAV=2: favour submissions (larger q, "
+           "default)\n");
+    printf("    --read|-R       do READs (def: TUR)\n");
+    printf("    --stats|-S      show more statistics on completion\n");
+    printf("    --submit|-u     use SG_IOSUBMIT+SG_IORECEIVE instead of "
+           "write+read\n");
+    printf("    --szlb=LB[,NLBS]|    LB is logical block size (def: 512)\n");
+    printf("         -s LB[,NLBS]    NLBS is number of logical blocks (def: "
+           "1)\n");
+    printf("    --tnum=NT|-t NT    number of threads (def: %d)\n",
+           DEF_NUM_THREADS);
+    printf("    --tur|-T        do TEST UNIT READYs (default is TURs)\n");
+    printf("    --v3|-3         use sg v3 interface (def: v3 if driver < "
+           "3.9)\n");
+    printf("    --v4|-4         use sg v4 interface (def if v4 driver). Sets "
+           "--submit\n");
+    printf("    --verbose|-v    increase verbosity\n");
+    printf("    --version|-V    print version number then exit\n");
+    printf("    --wait=MS|-w MS    >0: poll(<wait_ms>); =0: poll(0); (def: "
+           "%d)\n", DEF_WAIT_MS);
+    printf("    --write|-W      do WRITEs (def: TUR)\n\n");
+    printf("Multiple threads send READ(16), WRITE(16) or TEST UNIT READY "
+           "(TUR) SCSI\ncommands. There can be 1 or more <sg_disk_device>s "
+           "and each thread takes\nthe next in a round robin fashion. "
+           "Each thread queues up to NT commands.\nOne block is transferred "
+           "by each READ and WRITE; zeros are written. If a\nlogical block "
+           "range is given, a uniform distribution generates a pseudo\n"
+           "random sequence of LBAs. Set environment variable\n"
+           "SG3_UTILS_LINUX_NANO to get command timings in nanoseconds\n");
+}
+
+#ifdef __GNUC__
+static int pr2serr_lk(const char * fmt, ...)
+        __attribute__ ((format (printf, 1, 2)));
+static void pr_errno_lk(int e_no, const char * fmt, ...)
+        __attribute__ ((format (printf, 2, 3)));
+#else
+static int pr2serr_lk(const char * fmt, ...);
+static void pr_errno_lk(int e_no, const char * fmt, ...);
+#endif
+
+
+static int
+pr2serr_lk(const char * fmt, ...)
+{
+    int n;
+    va_list args;
+    lock_guard<mutex> lg(console_mutex);
+
+    va_start(args, fmt);
+    n = vfprintf(stderr, fmt, args);
+    va_end(args);
+    return n;
+}
+
+static void
+pr_errno_lk(int e_no, const char * fmt, ...)
+{
+    char b[160];
+    va_list args;
+    lock_guard<mutex> lg(console_mutex);
+
+    va_start(args, fmt);
+    vsnprintf(b, sizeof(b), fmt, args);
+    fprintf(stderr, "%s: %s\n", b, strerror(e_no));
+    va_end(args);
+}
+
+static unsigned int
+get_urandom_uint(void)
+{
+    unsigned int res = 0;
+    lock_guard<mutex> lg(rand_lba_mutex);
+
+    int fd = open(URANDOM_DEV, O_RDONLY);
+    if (fd >= 0) {
+        uint8_t b[sizeof(unsigned int)];
+        int n = read(fd, b, sizeof(unsigned int));
+
+        if (sizeof(unsigned int) == n)
+            memcpy(&res, b, sizeof(unsigned int));
+        close(fd);
+    }
+    return res;
+}
+
+#define TUR_CMD_LEN 6
+#define READ16_CMD_LEN 16
+#define READ16_REPLY_LEN 4096
+#define WRITE16_REPLY_LEN 4096
+#define WRITE16_CMD_LEN 16
+
+/* Returns 0 if command injected okay, return -1 for error and 2 for
+ * not done due to queue data size limit struck. */
+static int
+start_sg3_cmd(int sg_fd, command2execute cmd2exe, int pack_id, uint64_t lba,
+              uint8_t * lbp, int xfer_bytes, int flags, bool submit,
+              unsigned int & enomem, unsigned int & eagains,
+              unsigned int & ebusy, unsigned int & e2big, unsigned int & edom)
+{
+    struct sg_io_hdr pt;
+    struct sg_io_v4 p4t;
+    uint8_t turCmdBlk[TUR_CMD_LEN] = {0, 0, 0, 0, 0, 0};
+    uint8_t r16CmdBlk[READ16_CMD_LEN] =
+                {0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    uint8_t w16CmdBlk[WRITE16_CMD_LEN] =
+                {0x8a, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    const char * np = NULL;
+    struct sg_io_hdr * ptp;
+
+    if (submit) {       /* nest a v3 interface inside a store for v4 */
+        memset(&p4t, 0, sizeof(p4t));
+        ptp = (struct sg_io_hdr *)&p4t; /* p4t is larger than pt */
+    } else {
+        ptp = &pt;
+        memset(ptp, 0, sizeof(*ptp));
+    }
+    switch (cmd2exe) {
+    case SCSI_TUR:
+        np = "TEST UNIT READY";
+        ptp->cmdp = turCmdBlk;
+        ptp->cmd_len = sizeof(turCmdBlk);
+        ptp->dxfer_direction = SG_DXFER_NONE;
+        break;
+    case SCSI_READ16:
+        np = "READ(16)";
+        if (lba > 0xffffffff)
+            sg_put_unaligned_be32(lba >> 32, &r16CmdBlk[2]);
+        sg_put_unaligned_be32(lba & 0xffffffff, &r16CmdBlk[6]);
+        ptp->cmdp = r16CmdBlk;
+        ptp->cmd_len = sizeof(r16CmdBlk);
+        ptp->dxfer_direction = SG_DXFER_FROM_DEV;
+        ptp->dxferp = lbp;
+        ptp->dxfer_len = xfer_bytes;
+        break;
+    case SCSI_WRITE16:
+        np = "WRITE(16)";
+        if (lba > 0xffffffff)
+            sg_put_unaligned_be32(lba >> 32, &w16CmdBlk[2]);
+        sg_put_unaligned_be32(lba & 0xffffffff, &w16CmdBlk[6]);
+        ptp->cmdp = w16CmdBlk;
+        ptp->cmd_len = sizeof(w16CmdBlk);
+        ptp->dxfer_direction = SG_DXFER_TO_DEV;
+        ptp->dxferp = lbp;
+        ptp->dxfer_len = xfer_bytes;
+        break;
+    }
+    ptp->interface_id = 'S';
+    ptp->mx_sb_len = sizeof(sense_buffer);
+    ptp->sbp = sense_buffer;      /* ignored .... */
+    ptp->timeout = DEF_TIMEOUT_MS;
+    ptp->pack_id = pack_id;
+    ptp->flags = flags;
+
+    for (int k = 0;
+         (submit ? ioctl(sg_fd, SG_IOSUBMIT_V3, ptp) :
+                   write(sg_fd, ptp, sizeof(*ptp)) < 0);
+         ++k) {
+        if ((ENOMEM == errno) && (k < MAX_CONSEC_NOMEMS)) {
+            ++enomem;
+            this_thread::yield();
+            continue;
+        } else if (EAGAIN == errno) {
+            ++eagains;
+            this_thread::yield();
+            continue;
+        } else if (EBUSY == errno) {
+            ++ebusy;
+            this_thread::yield();
+            continue;
+        } else if (E2BIG == errno) {
+            ++e2big;
+            return 2;
+        } else if (EDOM == errno)
+            ++edom;
+        else if (ENOMEM == errno)
+            pr_rusage(-1);
+        pr_errno_lk(errno, "%s: %s, pack_id=%d", __func__, np, pack_id);
+        return -1;
+    }
+    return 0;
+}
+
+static int
+finish_sg3_cmd(int sg_fd, command2execute cmd2exe, int & pack_id,
+               bool receive, int wait_ms, unsigned int & enomem,
+               unsigned int & eagains, unsigned int & ebusys,
+               unsigned int & nanosecs)
+{
+    bool ok;
+    int res, k;
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    const char * np = NULL;
+    struct sg_io_hdr pt;
+    struct sg_io_hdr * ptp;
+    struct sg_io_v4 p4t;
+
+    if (receive) {      /* nest a v3 interface inside a store for v4 */
+        memset(&p4t, 0, sizeof(p4t));
+        ptp = (struct sg_io_hdr *)&p4t; /* p4t is larger than pt */
+    } else {
+        ptp = &pt;
+        memset(ptp, 0, sizeof(*ptp));
+    }
+    switch (cmd2exe) {
+    case SCSI_TUR:
+        np = "TEST UNIT READY";
+        ptp->dxfer_direction = SG_DXFER_NONE;
+        break;
+    case SCSI_READ16:
+        np = "READ(16)";
+        ptp->dxfer_direction = SG_DXFER_FROM_DEV;
+        break;
+    case SCSI_WRITE16:
+        np = "WRITE(16)";
+        ptp->dxfer_direction = SG_DXFER_TO_DEV;
+        break;
+    }
+    ptp->interface_id = 'S';
+    ptp->mx_sb_len = sizeof(sense_buffer);
+    ptp->sbp = sense_buffer;
+    ptp->timeout = DEF_TIMEOUT_MS;
+    /* if SG_SET_FORCE_PACK_ID, then need to set ptp->dxfer_direction */
+    ptp->pack_id = pack_id;
+
+    k = 0;
+    while ((((res = receive ? ioctl(sg_fd, SG_IORECEIVE_V3, ptp) :
+                              read(sg_fd, ptp, sizeof(*ptp)))) < 0) &&
+           ((EAGAIN == errno) || (EBUSY == errno) || (ENOMEM == errno))) {
+        if (ENOMEM == errno)
+            ++enomem;
+        else if (EAGAIN == errno)
+            ++eagains;
+        else
+            ++ebusys;
+        ++k;
+        if (k > 10000) {
+            pr2serr_lk("%s: sg_fd=%d: after %d EAGAINs, unable to find "
+                       "pack_id=%d\n", __func__, sg_fd, k, pack_id);
+            return -1;      /* crash out */
+        }
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (res < 0) {
+        if (ENOMEM == errno)
+            pr_rusage(-1);
+        pr_errno_lk(errno, "%s: %s", __func__, np);
+        return -1;
+    }
+    /* now for the error processing */
+    pack_id = ptp->pack_id;
+    ok = false;
+    switch (sg_err_category3(ptp)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = true;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        pr2serr_lk("%s: Recovered error on %s, continuing\n", __func__, np);
+        ok = true;
+        break;
+    default: /* won't bother decoding other categories */
+        {
+            lock_guard<mutex> lg(console_mutex);
+            sg_chk_n_print3(np, ptp, 1);
+        }
+        break;
+    }
+    if (ok)
+        nanosecs = ptp->duration;
+    return ok ? 0 : -1;
+}
+
+/* Returns 0 if command injected okay, return -1 for error and 2 for
+ * not done due to queue data size limit struck. */
+static int
+start_sg4_cmd(int sg_fd, command2execute cmd2exe, int pack_id, uint64_t lba,
+              uint8_t * lbp, int xfer_bytes, int flags, bool submit,
+              unsigned int & enomem, unsigned int & eagains,
+              unsigned int & ebusy, unsigned int & e2big, unsigned int & edom)
+{
+    struct sg_io_v4 p4t;
+    uint8_t turCmdBlk[TUR_CMD_LEN] = {0, 0, 0, 0, 0, 0};
+    uint8_t r16CmdBlk[READ16_CMD_LEN] =
+                {0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    uint8_t w16CmdBlk[WRITE16_CMD_LEN] =
+                {0x8a, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    const char * np = NULL;
+    struct sg_io_v4 * ptp;
+
+    if (! submit) {
+        pr2serr_lk("%s: logic error, submit must be true, isn't\n", __func__);
+        return -1;
+    }
+    ptp = &p4t;
+    memset(ptp, 0, sizeof(*ptp));
+    switch (cmd2exe) {
+    case SCSI_TUR:
+        np = "TEST UNIT READY";
+        ptp->request = (uint64_t)turCmdBlk;
+        ptp->request_len = sizeof(turCmdBlk);
+        break;
+    case SCSI_READ16:
+        np = "READ(16)";
+        if (lba > 0xffffffff)
+            sg_put_unaligned_be32(lba >> 32, &r16CmdBlk[2]);
+        sg_put_unaligned_be32(lba & 0xffffffff, &r16CmdBlk[6]);
+        ptp->request = (uint64_t)r16CmdBlk;
+        ptp->request_len = sizeof(r16CmdBlk);
+        ptp->din_xferp = (uint64_t)lbp;
+        ptp->din_xfer_len = xfer_bytes;
+        break;
+    case SCSI_WRITE16:
+        np = "WRITE(16)";
+        if (lba > 0xffffffff)
+            sg_put_unaligned_be32(lba >> 32, &w16CmdBlk[2]);
+        sg_put_unaligned_be32(lba & 0xffffffff, &w16CmdBlk[6]);
+        ptp->request = (uint64_t)w16CmdBlk;
+        ptp->request_len = sizeof(w16CmdBlk);
+        ptp->dout_xferp = (uint64_t)lbp;
+        ptp->dout_xfer_len = xfer_bytes;
+        break;
+    }
+    ptp->guard = 'Q';
+    ptp->max_response_len = sizeof(sense_buffer);
+    ptp->response = (uint64_t)sense_buffer;      /* ignored .... */
+    ptp->timeout = DEF_TIMEOUT_MS;
+    ptp->request_extra = pack_id;
+    ptp->flags = flags;
+
+    for (int k = 0; ioctl(sg_fd, SG_IOSUBMIT, ptp) < 0; ++k) {
+        if ((ENOMEM == errno) && (k < MAX_CONSEC_NOMEMS)) {
+            ++enomem;
+            this_thread::yield();
+            continue;
+        } else if (EAGAIN == errno) {
+            ++eagains;
+            this_thread::yield();
+            continue;
+        } else if (EBUSY == errno) {
+            ++ebusy;
+            this_thread::yield();
+            continue;
+        } else if (E2BIG == errno) {
+            ++e2big;
+            return 2;
+        } else if (EDOM == errno)
+            ++edom;
+        else if (ENOMEM == errno)
+            pr_rusage(-1);
+        pr_errno_lk(errno, "%s: %s, pack_id=%d", __func__, np, pack_id);
+        return -1;
+    }
+    return 0;
+}
+
+static int
+finish_sg4_cmd(int sg_fd, command2execute cmd2exe, int & pack_id,
+               bool receive, int wait_ms, unsigned int & enomem,
+               unsigned int & eagains, unsigned int & ebusys,
+               unsigned int & nanosecs)
+{
+    bool ok;
+    int res, k;
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    const char * np = NULL;
+    struct sg_io_v4 * ptp;
+    struct sg_io_v4 p4t;
+
+    if (! receive) {
+        pr2serr_lk("%s: logic error, receive must be true, isn't\n",
+                   __func__);
+        return -1;
+    }
+    ptp = &p4t;
+    memset(ptp, 0, sizeof(*ptp));
+    switch (cmd2exe) {
+    case SCSI_TUR:
+        np = "TEST UNIT READY";
+        break;
+    case SCSI_READ16:
+        np = "READ(16)";
+        break;
+    case SCSI_WRITE16:
+        np = "WRITE(16)";
+        break;
+    }
+    ptp->guard = 'Q';
+    ptp->max_response_len = sizeof(sense_buffer);
+    ptp->response = (uint64_t)sense_buffer;
+    ptp->timeout = DEF_TIMEOUT_MS;
+    /* if SG_SET_FORCE_PACK_ID, then need to set ptp->dxfer_direction */
+    ptp->request_extra = pack_id;
+
+    k = 0;
+    while ((((res = ioctl(sg_fd, SG_IORECEIVE, ptp))) < 0) &&
+           ((EAGAIN == errno) || (EBUSY == errno))) {
+        if (EAGAIN == errno)
+            ++eagains;
+        else
+            ++ebusys;
+        ++k;
+        if (k > 10000) {
+            pr2serr_lk("%s: sg_fd=%d: after %d EAGAINs, unable to find "
+                       "pack_id=%d\n", __func__, sg_fd, k, pack_id);
+            return -1;      /* crash out */
+        }
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (res < 0) {
+        if (ENOMEM == errno) {
+            ++enomem;
+            pr_rusage(-1);
+        }
+        pr_errno_lk(errno, "%s: %s", __func__, np);
+        return -1;
+    }
+    /* now for the error processing */
+    pack_id = ptp->request_extra;
+    ok = false;
+    res = sg_err_category_new(ptp->device_status, ptp->transport_status,
+                              ptp->driver_status,
+                              (const uint8_t *)ptp->response,
+                              ptp->response_len);
+    switch (res) {
+    case SG_LIB_CAT_CLEAN:
+        ok = true;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        pr2serr_lk("%s: Recovered error on %s, continuing\n", __func__, np);
+        ok = true;
+        break;
+    default: /* won't bother decoding other categories */
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            sg_linux_sense_print(np, ptp->device_status,
+                                 ptp->transport_status,
+                                 ptp->driver_status,
+                                 (const uint8_t *)ptp->response,
+                                 ptp->response_len, true);
+        }
+        break;
+    }
+    if (ok)
+        nanosecs = ptp->duration;
+    return ok ? 0 : -1;
+}
+
+static int
+num_submitted(int sg_fd)
+{
+    uint32_t num_subm_wait = 0;
+    struct sg_extended_info sei;
+    struct sg_extended_info *seip = &sei;
+    const char * err = NULL;
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_SUBMITTED;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0)
+        err = "ioctl(SG_SET_GET_EXTENDED) failed\n";
+    else
+        num_subm_wait = seip->read_value;
+    if (err)
+        pr2serr_lk("%s: %s, errno=%d\n", __func__, err, errno);
+    return err ? -1 : (int)num_subm_wait;
+}
+
+static int
+pr_rusage(int id)
+{
+    int res;
+    struct rusage ru;
+
+    res = getrusage(RUSAGE_SELF /* RUSAGE_THREAD */, &ru);
+    if (res < 0) {
+        pr2serr_lk("%d->id: %s: getrusage() failed, errno=%d\n", id,
+                   __func__, errno);
+        return res;
+    }
+    pr2serr_lk("%d->id: maxrss=%ldKB  nvcsw=%ld nivcsw=%ld  majflt=%ld\n", id,
+               ru.ru_maxrss, ru.ru_nvcsw, ru.ru_nivcsw, ru.ru_majflt);
+    return 0;
+}
+
+static void
+work_sync_thread(int id, const char * dev_name, unsigned int /* hi_lba */,
+                 struct opts_t * op)
+{
+    bool is_rw = (SCSI_TUR != op->c2e);
+    int k, sg_fd, err, rs, n, sense_cat, ret;
+    int vb = op->verbose;
+    int num_errs = 0;
+    int thr_sync_starts = 0;
+    struct sg_pt_base * ptp = NULL;
+    uint8_t cdb[6];
+    uint8_t sense_b[32] SG_C_CPP_ZERO_INIT;
+    char b[120];
+
+    if (is_rw) {
+        pr2serr_lk("id=%d: only support TUR here for now\n", id);
+        goto err_out;
+    }
+    if (op->verbose)
+        pr2serr_lk("id=%d: using libsgutils generic sync passthrough\n", id);
+
+    if ((sg_fd = sg_cmds_open_device(dev_name, false /* ro */, vb)) < 0) {
+        pr2serr_lk("id=%d: error opening file: %s: %s\n", id, dev_name,
+                   safe_strerror(-sg_fd));
+        if (ENOMEM == -sg_fd)
+            pr_rusage(id);
+        goto err_out;
+    }
+    if (vb > 2)
+        pr2serr_lk(">>>> id=%d: open(%s) --> fd=%d\n", id, dev_name, sg_fd);
+
+    ptp = construct_scsi_pt_obj_with_fd(sg_fd, vb);
+    err = 0;
+    if ((NULL == ptp) || ((err = get_scsi_pt_os_err(ptp)))) {
+        ret = sg_convert_errno(err ? err : ENOMEM);
+        sg_exit2str(ret, true, sizeof(b), b);
+        pr2serr_lk("id=%d: construct_scsi_pt_obj_with_fd: %s\n", id, b);
+        goto err_out;
+    }
+    for (k = 0; k < op->num_per_thread; ++k) {
+        /* Might get Unit Attention on first invocation */
+        memset(cdb, 0, sizeof(cdb));    /* TUR's cdb is 6 zeros */
+        set_scsi_pt_cdb(ptp, cdb, sizeof(cdb));
+        set_scsi_pt_sense(ptp, sense_b, sizeof(sense_b));
+        set_scsi_pt_packet_id(ptp, uniq_pack_id.fetch_add(1));
+        ++thr_sync_starts;
+        rs = do_scsi_pt(ptp, -1, DEF_PT_TIMEOUT, vb);
+        n = sg_cmds_process_resp(ptp, "Test unit ready", rs,
+                                 (0 == k), vb, &sense_cat);
+        if (-1 == n) {
+            ret = sg_convert_errno(get_scsi_pt_os_err(ptp));
+            sg_exit2str(ret, true, sizeof(b), b);
+            pr2serr_lk("id=%d: do_scsi_pt: %s\n", id, b);
+            goto err_out;
+        } else if (-2 == n) {
+            switch (sense_cat) {
+            case SG_LIB_CAT_RECOVERED:
+            case SG_LIB_CAT_NO_SENSE:
+                break;
+            case SG_LIB_CAT_NOT_READY:
+                ++num_errs;
+                if (1 ==  op->num_per_thread) {
+                    pr2serr_lk("id=%d: device not ready\n", id);
+                }
+                break;
+            case SG_LIB_CAT_UNIT_ATTENTION:
+                ++num_errs;
+                if (vb)
+                    pr2serr_lk("Ignoring Unit attention (sense key)\n");
+                break;
+            default:
+                ++num_errs;
+                if (1 == op->num_per_thread) {
+                    sg_get_category_sense_str(sense_cat, sizeof(b), b, vb);
+                    pr2serr_lk("%s\n", b);
+                    goto err_out;
+                }
+                break;
+            }
+        }
+        clear_scsi_pt_obj(ptp);
+    }
+err_out:
+    if (ptp)
+        destruct_scsi_pt_obj(ptp);
+    if (num_errs > 0)
+        pr2serr_lk("id=%d: number of errors: %d\n", id, num_errs);
+    sync_starts += thr_sync_starts;
+}
+
+static void
+work_thread(int id, struct opts_t * op)
+{
+    bool is_rw = (SCSI_TUR != op->c2e);
+    bool need_finish, repeat;
+    bool once = false;
+    bool once1000 = false;
+    bool once_2000 = false;
+    bool once_4000 = false;
+    bool once5000 = false;
+    bool once_6000 = false;
+    bool once_7000 = false;
+    bool once10_000 = false;
+    bool once20_000 = false;
+    int open_flags = O_RDWR;
+    int thr_async_starts = 0;
+    int thr_async_finishes = 0;
+    int vb = op->verbose;
+    int k, n, res, sg_fd, num_outstanding, do_inc, npt, pack_id, sg_flags;
+    int num_waiting_read, sz, encore_pack_id, ask, j, m, o;
+    int prev_pack_id, blk_sz;
+    unsigned int thr_enomem_count = 0;
+    unsigned int thr_start_eagain_count = 0;
+    unsigned int thr_start_ebusy_count = 0;
+    unsigned int thr_start_e2big_count = 0;
+    unsigned int thr_fin_eagain_count = 0;
+    unsigned int thr_fin_ebusy_count = 0;
+    unsigned int thr_start_edom_count = 0;
+    int needed_sz = op->lb_sz * op->num_lbs;
+    unsigned int nanosecs;
+    unsigned int hi_lba;
+    uint64_t lba;
+    uint64_t sum_nanosecs = 0;
+    uint8_t * lbp;
+    uint8_t * free_lbp = NULL;
+    uint8_t * wrkMmap = NULL;
+    const char * dev_name;
+    const char * err = NULL;
+    Rand_uint * ruip = NULL;
+    char ebuff[EBUFF_SZ];
+    struct pollfd  pfd[1];
+    list<pair<uint8_t *, uint8_t *> > free_lst;   /* of aligned lb buffers */
+    map<int, pair<uint8_t *, uint8_t *> > pi2buff;/* pack_id -> lb buffer */
+    map<int, uint64_t> pi_2_lba;            /* pack_id -> LBA */
+    pair<uint8_t *, uint8_t *> encore_lbps;
+
+    /* device name and hi_lba may depend on id */
+    n = op->dev_names.size();
+    dev_name = op->dev_names[id % n];
+    if (op->blk_szs.size() >= (unsigned)n)
+        blk_sz = op->blk_szs[id % n];
+    else
+        blk_sz = DEF_LB_SZ;
+    if ((UINT_MAX == op->hi_lba) && (n == (int)op->hi_lbas.size()))
+        hi_lba = op->hi_lbas[id % n];
+    else
+        hi_lba = op->hi_lba;
+
+    if (vb) {
+        if ((vb > 1) && hi_lba)
+            pr2serr_lk("Enter work_t_id=%d using %s\n"
+                       "    LBA range: 0x%x to 0x%x (inclusive)\n",
+                       id, dev_name, (unsigned int)op->lba, hi_lba);
+        else
+            pr2serr_lk("Enter work_t_id=%d using %s\n", id, dev_name);
+    }
+    if (op->generic_sync) {
+        work_sync_thread(id, dev_name, hi_lba, op);
+        return;
+    }
+    if (! op->block)
+        open_flags |= O_NONBLOCK;
+
+    sg_fd = open(dev_name, open_flags);
+    if (sg_fd < 0) {
+        pr_errno_lk(errno, "%s: id=%d, error opening file: %s", __func__, id,
+                    dev_name);
+        if (ENOMEM == -sg_fd)
+            pr_rusage(id);
+        return;
+    }
+    if (vb > 2)
+        pr2serr_lk(">>>> id=%d: open(%s) --> fd=%d\n", id, dev_name, sg_fd);
+    if (op->pack_id_force) {
+        k = 1;
+        if (ioctl(sg_fd, SG_SET_FORCE_PACK_ID, &k) < 0)
+            pr2serr_lk("ioctl(SG_SET_FORCE_PACK_ID) failed, errno=%d %s\n",
+                       errno, strerror(errno));
+    }
+    if (op->sg_vn_ge_40000) {
+        if (ioctl(sg_fd, SG_GET_RESERVED_SIZE, &k) >= 0) {
+            if (needed_sz > k)
+                ioctl(sg_fd, SG_SET_RESERVED_SIZE, &needed_sz);
+        }
+        if (op->sg_vn_ge_40030 && (op->cmd_time || op->masync)) {
+            struct sg_extended_info sei;
+            struct sg_extended_info * seip;
+
+            seip = &sei;
+            memset(seip, 0, sizeof(*seip));
+            seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+            seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS;
+            if (op->cmd_time) {
+                seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+                seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+                seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+            }
+            if (op->masync) {
+                seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_MORE_ASYNC;
+                seip->ctl_flags |= SG_CTL_FLAGM_MORE_ASYNC;
+            }
+            if (op->excl) {
+                seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_EXCL_WAITQ;
+                seip->ctl_flags |= SG_CTL_FLAGM_EXCL_WAITQ;
+            }
+            if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+                pr2serr_lk("ioctl(EXTENDED(TIME_IN_NS)) failed, errno=%d %s\n",
+                           errno, strerror(errno));
+            }
+            if (op->cmd_time &&
+                (! (SG_CTL_FLAGM_TIME_IN_NS & seip->ctl_flags))) {
+                memset(seip, 0, sizeof(*seip));
+                seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS;
+                seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+                seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+                seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+                if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0)
+                    pr2serr_lk("ioctl(EXTENDED(TIME_IN_NS)) failed, "
+                               "errno=%d %s\n", errno, strerror(errno));
+                else if (vb > 1)
+                    pr2serr_lk("t_id: %d: set TIME_IN_NS flag\n", id);
+            }
+        }
+    }
+    if (is_rw && op->mmap_io) {
+
+        if (ioctl(sg_fd, SG_GET_RESERVED_SIZE, &sz) < 0) {
+            pr2serr_lk("t_id=%d: ioctl(SG_GET_RESERVED_SIZE) errno=%d\n",
+                       id, errno);
+            return;
+        }
+        if (sz < needed_sz) {
+            sz = needed_sz;
+            if (ioctl(sg_fd, SG_SET_RESERVED_SIZE, &sz) < 0) {
+                pr2serr_lk("t_id=%d: ioctl(SG_SET_RESERVED_SIZE) errno=%d\n",
+                           id, errno);
+                return;
+            }
+            if (ioctl(sg_fd, SG_GET_RESERVED_SIZE, &sz) < 0) {
+                pr2serr_lk("t_id=%d: ioctl(SG_GET_RESERVED_SIZE) errno=%d\n",
+                           id, errno);
+                return;
+            }
+            if (sz < needed_sz) {
+                pr2serr_lk("t_id=%d: unable to grow reserve buffer to %d "
+                           "bytes\n", id, needed_sz);
+                return;
+            }
+        }
+        wrkMmap = (uint8_t *)mmap(NULL, needed_sz, PROT_READ | PROT_WRITE,
+                                  MAP_SHARED, sg_fd, 0);
+        if (MAP_FAILED == wrkMmap) {
+            int ern = errno;
+
+            pr2serr_lk("t_id=%d: mmap() failed, errno=%d\n", id, ern);
+            return;
+        }
+    }
+    pfd[0].fd = sg_fd;
+    pfd[0].events = POLLIN;
+    if (is_rw && hi_lba) {
+        unsigned int seed = get_urandom_uint();
+
+        if (vb > 1)
+            pr2serr_lk("  id=%d, /dev/urandom seed=0x%x\n", id, seed);
+        ruip = new Rand_uint((unsigned int)op->lba, hi_lba, seed);
+    }
+
+    sg_flags = 0;
+    if (BLQ_AT_TAIL == op->blqd)
+        sg_flags |= SG_FLAG_Q_AT_TAIL;
+    else if (BLQ_AT_HEAD == op->blqd)
+        sg_flags |= SG_FLAG_Q_AT_HEAD;
+    if (op->direct)
+        sg_flags |= SG_FLAG_DIRECT_IO;
+    if (op->mmap_io)
+        sg_flags |= SG_FLAG_MMAP_IO;
+    if (op->no_xfer)
+        sg_flags |= SG_FLAG_NO_DXFER;
+    if (vb > 1)
+        pr2serr_lk("  id=%d, sg_flags=0x%x, %s cmds\n", id, sg_flags,
+                   ((SCSI_TUR == op->c2e) ? "TUR":
+                    ((SCSI_READ16 == op->c2e) ? "READ" : "WRITE")));
+
+    npt = op->num_per_thread;
+    need_finish = false;
+    lba = 0;
+    pack_id = 0;
+    prev_pack_id = 0;
+    encore_pack_id = 0;
+    do_inc = 0;
+    /* main loop, continues until num_per_thread exhausted and there are
+     * no more outstanding responses */
+    for (k = 0, m = 0, o=0, num_outstanding = 0; (k < npt) || num_outstanding;
+         k = do_inc ? k + 1 : k, ++o) {
+        int num_to_read = 0;
+
+        if (do_inc)
+            m = 0;
+        else {
+            ++m;
+            if (m > 100) {
+                if (vb)
+                    pr2serr_lk("%d->id: no main loop inc =%d times\n", id, m);
+                m = 0;
+            }
+        }
+        if (vb && (! once1000) && (num_outstanding >= 1000)) {
+            int num_waiting;
+            int num_subm = (op->sg_vn_ge_40030) ? num_submitted(sg_fd) :
+                                                  pi2buff.size();
+
+            once1000 = true;
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0) {
+                err = "ioctl(SG_GET_NUM_WAITING) failed";
+                break;
+            }
+            pr2serr_lk("%d->id: once 1000: k=%d, submitted=%d waiting=%d; "
+                       "pi2buff.sz=%u\n", id, k, num_subm, num_waiting,
+                       (uint32_t)pi2buff.size());
+            pr_rusage(id);
+        }
+        if (vb && ! once5000 && num_outstanding >= 5000) {
+            int num_waiting;
+            int num_subm = (op->sg_vn_ge_40030) ? num_submitted(sg_fd) :
+                                                  pi2buff.size();
+
+            once5000 = true;
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0) {
+                err = "ioctl(SG_GET_NUM_WAITING) failed";
+                break;
+            }
+            pr2serr_lk("%d->id: once 5000: k=%d, submitted=%d waiting=%d\n",
+                       id, k, num_subm, num_waiting);
+            pr_rusage(id);
+        }
+        if (vb && ! once_7000 && num_outstanding >= 7000) {
+            int num_waiting;
+            int num_subm = (op->sg_vn_ge_40030) ? num_submitted(sg_fd) :
+                                                  pi2buff.size();
+
+            once_7000 = true;
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0) {
+                err = "ioctl(SG_GET_NUM_WAITING) failed";
+                break;
+            }
+            pr2serr_lk("%d->id: once 7000: k=%d, submitted=%d waiting=%d\n",
+                       id, k, num_subm, num_waiting);
+            pr_rusage(id);
+        }
+        if (vb && ! once10_000 && num_outstanding >= 10000) {
+            int num_waiting;
+            int num_subm = (op->sg_vn_ge_40030) ? num_submitted(sg_fd) :
+                                                  pi2buff.size();
+
+            once10_000 = true;
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0) {
+                err = "ioctl(SG_GET_NUM_WAITING) failed";
+                break;
+            }
+            pr2serr_lk("%d->id: once 10^4: k=%d, submitted=%d waiting=%d\n",
+                       id, k, num_subm, num_waiting);
+            pr_rusage(id);
+        }
+        if (vb && ! once20_000 && num_outstanding >= 20000) {
+            int num_waiting;
+            int num_subm = (op->sg_vn_ge_40030) ? num_submitted(sg_fd) :
+                                                  pi2buff.size();
+
+            once20_000 = true;
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0) {
+                err = "ioctl(SG_GET_NUM_WAITING) failed";
+                break;
+            }
+            pr2serr_lk("%d->id: once 20000: k=%d, submitted=%d waiting=%d\n",
+                       id, k, num_subm, num_waiting);
+            pr_rusage(id);
+        }
+        do_inc = 0;
+        if ((num_outstanding < op->maxq_per_thread) && (k < npt)) {
+            do_inc = 1;
+            if (need_finish) {
+                pack_id = encore_pack_id;
+                need_finish = false;
+                repeat = true;
+            } else {
+                prev_pack_id = pack_id;
+                pack_id = uniq_pack_id.fetch_add(1);
+                repeat = false;
+            }
+            if (is_rw) {    /* get new lb buffer or one from free list */
+                if (free_lst.empty()) {
+                    lbp = sg_memalign(op->lb_sz * op->num_lbs, 0, &free_lbp,
+                                      false);
+                    if (NULL == lbp) {
+                        err = "out of memory";
+                        break;
+                    }
+                } else if (! repeat) {
+                    lbp = free_lst.back().first;
+                    free_lbp = free_lst.back().second;
+                    free_lst.pop_back();
+                } else {
+                    lbp = encore_lbps.first;
+                    free_lbp = encore_lbps.second;
+                    if (vb && !once && free_lst.size() > 1000) {
+                        once = true;
+                        pr2serr_lk("%d->id: free_lst.size() over 1000\n", id);
+                    }
+                    if (vb && !once_2000 && free_lst.size() > 2000) {
+                        once_2000 = true;
+                        pr2serr_lk("%d->id: free_lst.size() over 2000\n", id);
+                    }
+                    if (vb && !once_6000 && free_lst.size() > 6000) {
+                        once_2000 = true;
+                        pr2serr_lk("%d->id: free_lst.size() over 6000\n", id);
+                    }
+                }
+            } else
+                lbp = NULL;
+            if (is_rw) {
+                if (ruip) {
+                    if (! repeat) {
+                        lba = ruip->get();  /* fetch a random LBA */
+                        if (vb > 3)
+                            pr2serr_lk("  id=%d: start IO at lba=0x%" PRIx64
+                                       "\n", id, lba);
+                    }
+                } else
+                    lba = op->lba;
+            } else
+                lba = 0;
+            if (vb > 4)
+                pr2serr_lk("t_id=%d: starting pack_id=%d\n", id, pack_id);
+            res = (op->v4) ?
+                start_sg4_cmd(sg_fd, op->c2e, pack_id, lba, lbp,
+                              blk_sz * op->num_lbs, sg_flags, op->submit,
+                              thr_enomem_count, thr_start_eagain_count,
+                              thr_start_ebusy_count, thr_start_e2big_count,
+                              thr_start_edom_count)  :
+                start_sg3_cmd(sg_fd, op->c2e, pack_id, lba, lbp,
+                              blk_sz * op->num_lbs, sg_flags, op->submit,
+                              thr_enomem_count, thr_start_eagain_count,
+                              thr_start_ebusy_count, thr_start_e2big_count,
+                              thr_start_edom_count);
+            if (res) {
+                if (res > 1) { /* here if E2BIG, start not done, try finish */
+                    do_inc = 0;
+                    need_finish = true;
+                    encore_pack_id = pack_id;
+                    pack_id = prev_pack_id;
+                    encore_lbps = make_pair(lbp, free_lbp);
+                    if (vb > 2)
+                        pr2serr_lk("t_id=%d: E2BIG hit, prev_pack_id=%d, "
+                                   "encore_pack_id=%d\n", id, prev_pack_id,
+                                   encore_pack_id);
+                } else {
+                    err = "start_sg3_cmd()";
+                    break;
+                }
+            } else {    /* no error */
+                ++thr_async_starts;
+                ++num_outstanding;
+                pi2buff[pack_id] = make_pair(lbp, free_lbp);
+                if (ruip)
+                    pi_2_lba[pack_id] = lba;
+            }
+            if (vb && !once && (pi2buff.size() > 1000)) {
+                once = true;
+                pr2serr_lk("%d->id: pi2buff.size() over 1000 (b)\n", id);
+            }
+            if (vb && !once_2000 && free_lst.size() > 2000) {
+                once_2000 = true;
+                pr2serr_lk("%d->id: free_lst.size() over 2000 (b)\n", id);
+            }
+            if (vb && !once_6000 && free_lst.size() > 6000) {
+                once_2000 = true;
+                pr2serr_lk("%d->id: free_lst.size() over 6000 (b)\n", id);
+            }
+        }
+        if (need_finish) {
+            num_waiting_read = 0;
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting_read) < 0) {
+                err = "ioctl(SG_GET_NUM_WAITING) failed";
+                break;
+            } else if (vb > 3)
+                pr2serr_lk("t_id=%d: num_waiting_read=%d\n", id,
+                           num_waiting_read);
+            if (num_waiting_read > 0)
+                num_to_read = num_waiting_read;
+            else {
+                struct timespec tspec = {0, 100000 /* 100 usecs */};
+
+                nanosleep(&tspec, NULL);
+                if (vb > 3)
+                    pr2serr_lk("t_id=%d: E2BIG, 100 usecs sleep\n", id);
+                // err = "strange, E2BIG but nothing to read";
+                // break;
+            }
+        } else if ((num_outstanding >= op->maxq_per_thread) || (k >= npt)) {
+            /* full queue or finished injecting */
+            num_waiting_read = 0;
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting_read) < 0) {
+                err = "ioctl(SG_GET_NUM_WAITING) failed";
+                break;
+            }
+            if (1 == num_waiting_read)
+                num_to_read = num_waiting_read;
+            else if (num_waiting_read > 0) {
+                if (k >= npt)
+                    num_to_read = num_waiting_read;
+                else {
+                    switch (op->myqd) {
+                    case MYQD_LOW:
+                        num_to_read = num_waiting_read;
+                        break;
+                    case MYQD_MEDIUM:
+                        num_to_read = num_waiting_read / 2;
+                        break;
+                    case MYQD_HIGH:
+                    default:
+                        if (op->ovn > 0) {
+                            if (op->sg_vn_ge_40030) {
+                                int num_subm = num_submitted(sg_fd);
+
+                                if (num_subm > op->ovn) {
+                                    num_to_read = num_waiting_read > 0 ?
+                                                    num_waiting_read : 1;
+                                    break;
+                                }
+                            } else {
+                                if (num_waiting_read > (op->ovn / 2)) {
+                                    num_to_read = num_waiting_read / 2;
+                                    break;
+                                }
+                            }
+                        }
+                        num_to_read = 1;
+                        break;
+                    }
+                }
+            } else {    /* nothing waiting to be read */
+                if (op->sg_vn_ge_40030) {
+                    int val = num_submitted(sg_fd);
+
+                    if (0 == val) {
+                        err = "nothing submitted now ??";
+                        break;
+                    } else if (val < 0) {
+                        err = "num_submitted failed";
+                        break;
+                    }
+                }
+                n = (op->wait_ms > 0) ? op->wait_ms : 0;
+                if (n > 0) {
+                    for (j = 0; (j < 1000000) &&
+                         (0 == (res = poll(pfd, 1, n)));
+                         ++j)
+                        ;
+                    if (j >= 1000000) {
+                        err = "poll() looped 1 million times";
+                        break;
+                    }
+                    if (res < 0) {
+                        err = "poll(wait_ms) failed";
+                        break;
+                    }
+                } else {
+                    struct timespec ts;
+
+                    ts.tv_sec = 0;
+                    ts.tv_nsec = DEF_NANOSEC_WAIT;
+                    if (nanosleep(&ts, NULL) < 0) {
+                        err = "nanosleep() failed";
+                        break;
+                    }
+                }
+            }
+        } else {        /* not full, not finished injecting */
+            if (MYQD_HIGH == op->myqd) {
+                num_to_read = 0;
+                if (op->ovn) {
+                    if (op->sg_vn_ge_40030) {
+                        int num_subm = num_submitted(sg_fd);
+
+                        if (num_subm > op->ovn)
+                            num_to_read = num_waiting_read > 0 ?
+                                            num_waiting_read : 1;
+                    } else {
+                        num_waiting_read = 0;
+                        if (ioctl(sg_fd, SG_GET_NUM_WAITING,
+                                  &num_waiting_read) < 0) {
+                            err = "ioctl(SG_GET_NUM_WAITING) failed";
+                            break;
+                        }
+                        if (num_waiting_read > (op->ovn / 2))
+                            num_to_read = num_waiting_read / 2;
+                    }
+                }
+            } else {
+                num_waiting_read = 0;
+                if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting_read) < 0) {
+                    err = "ioctl(SG_GET_NUM_WAITING) failed";
+                    break;
+                }
+                if (num_waiting_read > 0)
+                    num_to_read = num_waiting_read /
+                                  ((MYQD_LOW == op->myqd) ? 1 : 2);
+                else
+                    num_to_read = 0;
+            }
+        }
+
+        if (vb && !once_4000 && (num_to_read > 4000)) {
+            once_4000 = true;
+            pr2serr_lk("%d->id: num_to_read=%d\n", id, num_to_read);
+        }
+        while (num_to_read > 0) {
+            --num_to_read;
+            if (op->pack_id_force) {
+                j = pi2buff.size();
+                if (j > 0)
+                    pack_id = pi2buff.begin()->first;
+                else
+                    pack_id = -1;
+            } else
+                pack_id = -1;
+            ask = pack_id;
+            res = (op->v4) ?
+                    finish_sg4_cmd(sg_fd, op->c2e, pack_id, op->submit,
+                                   op->wait_ms, thr_enomem_count,
+                                   thr_fin_eagain_count, thr_fin_ebusy_count,
+                                   nanosecs)           :
+                    finish_sg3_cmd(sg_fd, op->c2e, pack_id, op->submit,
+                                   op->wait_ms, thr_enomem_count,
+                                   thr_fin_eagain_count, thr_fin_ebusy_count,
+                                   nanosecs);
+            if (res) {
+                err = "finish_sg3_cmd()";
+                if (ruip && (pack_id > 0)) {
+                    auto q = pi_2_lba.find(pack_id);
+
+                    if (q != pi_2_lba.end()) {
+                        snprintf(ebuff, sizeof(ebuff), "%s: lba=0x%" PRIx64 ,
+                                 err, q->second);
+                        err = ebuff;
+                    }
+                }
+                break;
+            }
+            if (op->cmd_time && op->sg_vn_ge_40030)
+                sum_nanosecs += nanosecs;
+            ++thr_async_finishes;
+            --num_outstanding;
+            if (vb > 4)
+                pr2serr_lk("t_id=%d: finishing pack_id ask=%d, got=%d, "
+                           "outstanding=%d\n", id, ask, pack_id,
+                           num_outstanding);
+            auto p = pi2buff.find(pack_id);
+
+            if (p == pi2buff.end()) {
+                snprintf(ebuff, sizeof(ebuff), "pack_id=%d from "
+                         "finish_sg3_cmd() not found\n", pack_id);
+                if (! err)
+                    err = ebuff;
+            } else {
+                lbp = p->second.first;
+                free_lbp = p->second.second;
+                pi2buff.erase(p);
+                if (lbp)
+                    free_lst.push_front(make_pair(lbp, free_lbp));
+            }
+            if (ruip && (pack_id > 0)) {
+                auto q = pi_2_lba.find(pack_id);
+
+                if (q != pi_2_lba.end()) {
+                    if (vb > 3)
+                        pr2serr_lk("    id=%d: finish IO at lba=0x%" PRIx64
+                                   "\n", id, q->second);
+                    pi_2_lba.erase(q);
+                }
+            }
+            if (err)
+                break;
+        }       /* end of while loop counting down num_to_read */
+        if (err)
+            break;
+    }           /* end of for loop over npt (number per thread) */
+    if (vb)
+        pr2serr_lk("%d->id: leaving main thread loop; k=%d, o=%d\n", id, k,
+                   o);
+    close(sg_fd);       // sg driver will handle any commands "in flight"
+    if (ruip)
+        delete ruip;
+
+    if (err || (k < npt)) {
+        if (k < npt)
+            pr2serr_lk("t_id=%d FAILed at iteration %d%s%s\n", id, k,
+                       (err ? ", Reason: " : ""), (err ? err : ""));
+        else
+            pr2serr_lk("t_id=%d FAILed on last%s%s\n", id,
+                       (err ? ", Reason: " : ""), (err ? err : ""));
+    }
+    n = pi2buff.size();
+    if (n > 0)
+        pr2serr_lk("t_id=%d Still %d elements in pi2buff map on "
+                   "exit\n", id, n);
+    for (k = 0; ! free_lst.empty(); ++k) {
+        lbp = free_lst.back().first;
+        free_lbp = free_lst.back().second;
+        free_lst.back().second = NULL;
+        free_lst.pop_back();
+        if (vb > 6)
+            pr2serr_lk("t_id=%d freeing %p (free_ %p)\n", id, lbp, free_lbp);
+        if (free_lbp) {
+            free(free_lbp);
+            free_lbp = NULL;
+        }
+    }
+    if ((vb > 2) && (k > 0))
+        pr2serr_lk("%d->id: Maximum number of READ/WRITEs queued: %d\n",
+                   id, k);
+    async_starts += thr_async_starts;
+    async_finishes += thr_async_finishes;
+    start_eagain_count += thr_start_eagain_count;
+    start_ebusy_count += thr_start_ebusy_count;
+    start_e2big_count += thr_start_e2big_count;
+    fin_eagain_count += thr_fin_eagain_count;
+    fin_ebusy_count += thr_fin_ebusy_count;
+    enomem_count += thr_enomem_count;
+    start_edom_count += thr_start_edom_count;
+    if (op->cmd_time && op->sg_vn_ge_40030 && (npt > 0)) {
+        pr2serr_lk("t_id=%d average nanosecs per cmd: %" PRId64
+                   "\n", id, sum_nanosecs / npt);
+    }
+}
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+
+/* Send INQUIRY and fetches response. If okay puts PRODUCT ID field
+ * in b (up to m_blen bytes). Does not use O_EXCL flag. Returns 0 on success,
+ * else -1 . */
+static int
+do_inquiry_prod_id(const char * dev_name, int block, int & sg_ver_num,
+                   char * b, int b_mlen)
+{
+    int sg_fd, ok, ret;
+    struct sg_io_hdr pt;
+    uint8_t inqCmdBlk [INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    uint8_t inqBuff[INQ_REPLY_LEN];
+    uint8_t sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    int open_flags = O_RDWR;    /* O_EXCL | O_RDONLY fails with EPERM */
+
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    sg_fd = open(dev_name, open_flags);
+    if (sg_fd < 0) {
+        pr_errno_lk(errno, "%s: error opening file: %s", __func__, dev_name);
+        return -1;
+    }
+    if (ioctl(sg_fd, SG_GET_VERSION_NUM, &sg_ver_num) < 0)
+        sg_ver_num = 0;
+    /* Prepare INQUIRY command */
+    memset(&pt, 0, sizeof(pt));
+    pt.interface_id = 'S';
+    pt.cmd_len = sizeof(inqCmdBlk);
+    /* pt.iovec_count = 0; */  /* memset takes care of this */
+    pt.mx_sb_len = sizeof(sense_buffer);
+    pt.dxfer_direction = SG_DXFER_FROM_DEV;
+    pt.dxfer_len = INQ_REPLY_LEN;
+    pt.dxferp = inqBuff;
+    pt.cmdp = inqCmdBlk;
+    pt.sbp = sense_buffer;
+    pt.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* pt.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* pt.pack_id = 0; */
+    /* pt.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &pt) < 0) {
+        pr_errno_lk(errno, "%s: Inquiry SG_IO ioctl error", __func__);
+        close(sg_fd);
+        return -1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&pt)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        pr2serr_lk("Recovered error on INQUIRY, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        {
+            lock_guard<mutex> lg(console_mutex);
+            sg_chk_n_print3("INQUIRY command error", &pt, 1);
+        }
+        break;
+    }
+    if (ok) {
+        /* Good, so fetch Product ID from response, copy to 'b' */
+        if (b_mlen > 0) {
+            if (b_mlen > 16) {
+                memcpy(b, inqBuff + 16, 16);
+                b[16] = '\0';
+            } else {
+                memcpy(b, inqBuff + 16, b_mlen - 1);
+                b[b_mlen - 1] = '\0';
+            }
+        }
+        ret = 0;
+    } else
+        ret = -1;
+
+    close(sg_fd);
+    return ret;
+}
+
+/* Only allow ranges up to 2**32-1 upper limit, so READ CAPACITY(10)
+ * sufficient. Return of 0 -> success, -1 -> failure, 2 -> try again */
+static int
+do_read_capacity(const char * dev_name, int block, unsigned int * last_lba,
+                 unsigned int * blk_sz)
+{
+    int res, sg_fd;
+    uint8_t rcCmdBlk [10] = {0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t rcBuff[64];
+    uint8_t sense_b[64] SG_C_CPP_ZERO_INIT;
+    sg_io_hdr_t io_hdr SG_C_CPP_ZERO_INIT;
+    int open_flags = O_RDWR;    /* O_EXCL | O_RDONLY fails with EPERM */
+
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    sg_fd = open(dev_name, open_flags);
+    if (sg_fd < 0) {
+        pr_errno_lk(errno, "%s: error opening file: %s", __func__, dev_name);
+        return -1;
+    }
+    /* Prepare READ CAPACITY(10) command */
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(rcCmdBlk);
+    io_hdr.mx_sb_len = sizeof(sense_b);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = sizeof(rcBuff);
+    io_hdr.dxferp = rcBuff;
+    io_hdr.cmdp = rcCmdBlk;
+    io_hdr.sbp = sense_b;
+    io_hdr.timeout = 20000;     /* 20000 millisecs == 20 seconds */;
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        pr_errno_lk(errno, "%s (SG_IO) error", __func__);
+        close(sg_fd);
+        return -1;
+    }
+    res = sg_err_category3(&io_hdr);
+    if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+        lock_guard<mutex> lg(console_mutex);
+        sg_chk_n_print3("read capacity", &io_hdr, 1);
+        close(sg_fd);
+        return 2; /* probably have another go ... */
+    } else if (SG_LIB_CAT_CLEAN != res) {
+        lock_guard<mutex> lg(console_mutex);
+        sg_chk_n_print3("read capacity", &io_hdr, 1);
+        close(sg_fd);
+        return -1;
+    }
+    *last_lba = sg_get_unaligned_be32(&rcBuff[0]);
+    *blk_sz = sg_get_unaligned_be32(&rcBuff[4]);
+    close(sg_fd);
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool maxq_per_thread_given = false;
+    int n;
+    int force = 0;
+    int64_t ll;
+    int num_threads = DEF_NUM_THREADS;
+    struct timespec start_tm, end_tm;
+    struct opts_t * op;
+    const char * cp;
+
+    op = &a_opts;
+#if 0
+    memset(op, 0, sizeof(*op));         // C++ doesn't like this
+#endif
+    op->direct = DEF_DIRECT;
+    op->lba = DEF_LBA;
+    op->hi_lba = 0;
+    op->lb_sz = DEF_LB_SZ;
+    op->maxq_per_thread = MAX_Q_PER_FD;
+    op->mmap_io = DEF_MMAP_IO;
+    op->num_per_thread = DEF_NUM_PER_THREAD;
+    op->num_lbs = 1;
+    op->no_xfer = !! DEF_NO_XFER;
+    op->verbose = 0;
+    op->wait_ms = DEF_WAIT_MS;
+    op->c2e = SCSI_TUR;
+    op->blqd = BLQ_DEFAULT;
+    op->block = !! DEF_BLOCKING;
+    op->myqd = MYQD_HIGH;
+    page_size = sysconf(_SC_PAGESIZE);
+
+    while (1) {
+        int option_index = 0;
+        int c;
+
+        c = getopt_long(argc, argv,
+                        "34acdefghl:L:mM:n:NO:pq:Q:Rs:St:TuvVw:W",
+                        long_options, &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case '3':
+            op->v3 = true;
+            op->v3_given = true;
+            op->v4 = false;     /* if '-4 -3' take latter */
+            op->v4_given = false;
+            break;
+        case '4':
+            op->v4 = true;
+            op->v4_given = true;
+            op->v3 = false;
+            op->v3_given = false;
+            break;
+        case 'a':
+            op->masync = true;
+            break;
+        case 'c':
+            op->cmd_time = true;
+            break;
+        case 'd':
+            op->direct = true;
+            break;
+        case 'e':
+            op->excl = true;
+            break;
+        case 'f':
+            force = true;
+            break;
+        case 'g':
+            op->generic_sync = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'l':
+            if (isdigit(*optarg)) {
+                ll = sg_get_llnum(optarg);
+                if (-1 == ll) {
+                    pr2serr_lk("could not decode lba\n");
+                    return 1;
+                } else
+                    op->lba = (uint64_t)ll;
+                cp = strchr(optarg, ',');
+                if (cp) {
+                    if (0 == strcmp("-1", cp + 1))
+                        op->hi_lba = UINT_MAX;
+                    else {
+                        ll = sg_get_llnum(cp + 1);
+                        if ((-1 == ll) || (ll > UINT_MAX)) {
+                            pr2serr_lk("could not decode hi_lba, or > "
+                                       "UINT_MAX\n");
+                            return 1;
+                        } else
+                            op->hi_lba = (unsigned int)ll;
+                    }
+                }
+            } else {
+                pr2serr_lk("--lba= expects a number\n");
+                return 1;
+            }
+            break;
+        case 'L':
+            op->lb_sz = sg_get_num(optarg);
+            if (op->lb_sz < 0) {
+                pr2serr_lk("--lbsz= expects power of 2\n");
+                return 1;
+            }
+            if (0 == op->lb_sz)
+                op->lb_sz = DEF_LB_SZ;
+            break;
+        case 'm':
+            op->mmap_io = true;
+            break;
+        case 'M':
+            if (isdigit(*optarg)) {
+                n = atoi(optarg);
+                if ((n < 1) || (n > MAX_Q_PER_FD)) {
+                    pr2serr_lk("-M expects a value from 1 to %d\n",
+                               MAX_Q_PER_FD);
+                    return 1;
+                }
+                maxq_per_thread_given = true;
+                op->maxq_per_thread = n;
+            } else {
+                pr2serr_lk("--maxqpt= expects a number\n");
+                return 1;
+            }
+            break;
+        case 'n':
+            if (isdigit(*optarg))
+                op->num_per_thread = sg_get_num(optarg);
+            else {
+                pr2serr_lk("--numpt= expects a number\n");
+                return 1;
+            }
+            break;
+        case 'N':
+            op->no_xfer = true;
+            break;
+        case 'O':
+            if (isdigit(*optarg))
+                op->ovn = sg_get_num(optarg);
+            else {
+                pr2serr_lk("--override= expects a number\n");
+                return 1;
+            }
+            if (op->ovn < 0) {
+                pr2serr_lk("--override= bad number\n");
+                return 1;
+            }
+            break;
+        case 'p':
+            op->pack_id_force = true;
+            break;
+        case 'q':
+            if (isdigit(*optarg)) {
+                n = atoi(optarg);
+                if (0 == n)
+                    op->blqd = BLQ_AT_HEAD;
+                else if (1 == n)
+                    op->blqd = BLQ_AT_TAIL;
+            } else {
+                pr2serr_lk("--qat= expects a number: 0 or 1\n");
+                return 1;
+            }
+            break;
+        case 'Q':
+            if (isdigit(*optarg)) {
+                n = atoi(optarg);
+                if (0 == n)
+                    op->myqd = MYQD_LOW;
+                else if (1 == n)
+                    op->myqd = MYQD_MEDIUM;
+                else if (2 == n)
+                    op->myqd = MYQD_HIGH;
+            } else {
+                pr2serr_lk("--qfav= expects a number: 0, 1 or 2\n");
+                return 1;
+            }
+            break;
+        case 'R':
+            op->c2e = SCSI_READ16;
+            break;
+        case 's':
+            if (isdigit(*optarg)) {
+                op->lb_sz = atoi(optarg);
+                if (op->lb_sz < 256) {
+                    cerr << "Strange lb_sz, using 256" << endl;
+                    op->lb_sz = 256;
+                }
+            } else {
+                pr2serr_lk("--szlb= expects a number\n");
+                return 1;
+            }
+            if ((cp = strchr(optarg, ','))) {
+                n = sg_get_num(cp + 1);
+                if (n < 1) {
+                    pr2serr_lk("could not decode 2nd part of "
+                               "--szlb=LBS,NLBS\n");
+                    return 1;
+                }
+                op->num_lbs = n;
+            }
+            break;
+        case 'S':
+            ++op->stats;
+            break;
+        case 't':
+            if (isdigit(*optarg))
+                num_threads = atoi(optarg);
+            else {
+                pr2serr_lk("--tnum= expects a number\n");
+                return 1;
+            }
+            break;
+        case 'T':
+            op->c2e = SCSI_TUR;
+            break;
+        case 'u':
+            op->submit = true;
+            break;
+        case 'v':
+            op->verbose_given = true;
+            ++op->verbose;
+            break;
+        case 'V':
+            op->version_given = true;
+            break;
+        case 'w':
+            if ((isdigit(*optarg) || ('-' == *optarg))) {
+                if ('-' == *optarg)
+                    op->wait_ms = - atoi(optarg + 1);
+                else
+                    op->wait_ms = atoi(optarg);
+            } else {
+                pr2serr_lk("--wait= expects a number\n");
+                return 1;
+            }
+            break;
+        case 'W':
+            op->c2e = SCSI_WRITE16;
+            break;
+        default:
+            pr2serr_lk("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return 1;
+        }
+    }
+    if (optind < argc) {
+        for (; optind < argc; ++optind)
+            op->dev_names.push_back(argv[optind]);
+    }
+#ifdef DEBUG
+    pr2serr_lk("In DEBUG mode, ");
+    if (op->verbose_given && op->version_given) {
+        pr2serr_lk("but override: '-vV' given, zero verbose and continue\n");
+        op->verbose_given = false;
+        op->version_given = false;
+        op->verbose = 0;
+    } else if (! op->verbose_given) {
+        pr2serr_lk("set '-vv'\n");
+        op->verbose = 2;
+    } else
+        pr2serr_lk("keep verbose=%d\n", op->verbose);
+#else
+    if (op->verbose_given && op->version_given)
+        pr2serr_lk("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (op->version_given) {
+        pr2serr_lk("version: %s\n", version_str);
+        return 0;
+    }
+    if (op->mmap_io) {
+        if (maxq_per_thread_given && (op->maxq_per_thread > 1)) {
+            pr2serr_lk("With mmap_io selected, QPT cannot exceed 1\n");
+            return 1;
+        } else if (op->direct) {
+            pr2serr_lk("direct IO and mmap-ed IO cannot both be selected\n");
+            return 1;
+        } else if (op->generic_sync) {
+            pr2serr_lk("--generic-sync and and mmap-ed IO are compatible\n");
+            return 1;
+        } else
+            op->maxq_per_thread = 1;
+    }
+    if (! op->cmd_time && getenv("SG3_UTILS_LINUX_NANO")) {
+        op->cmd_time = true;
+        if (op->verbose)
+            fprintf(stderr, "setting nanosecond timing due to environment "
+                    "variable: SG3_UTILS_LINUX_NANO\n");
+    }
+    if (0 == op->dev_names.size()) {
+        fprintf(stderr, "No sg_disk_device-s given\n\n");
+        usage();
+        return 1;
+    }
+    if (op->hi_lba && (op->lba > op->hi_lba)) {
+        cerr << "lba,hi_lba range is illegal" << endl;
+        return 1;
+    }
+    if (op->v4) {
+        if (! op->submit) {
+            op->submit = true;
+            if (op->verbose > 1)
+                cerr << "when --v4 is given, --submit will be set" << endl;
+        }
+    }
+
+    try {
+        int k, sg_ver_num;
+        unsigned int last_lba;
+        unsigned int blk_sz;
+        struct stat a_stat;
+
+        for (k = 0; k < (int)op->dev_names.size(); ++k) {
+            int res;
+            const char * dev_name;
+            char b[128];
+
+            dev_name = op->dev_names[k];
+            if (stat(dev_name, &a_stat) < 0) {
+                snprintf(b, sizeof(b), "could not stat() %s", dev_name);
+                perror(b);
+                return 1;
+            }
+            if (! S_ISCHR(a_stat.st_mode)) {
+                pr2serr_lk("%s should be a sg device which is a char "
+                           "device. %s\n", dev_name, dev_name);
+                pr2serr_lk("is not a char device and damage could be done "
+                           "if it is a BLOCK\ndevice, exiting ...\n");
+                return 1;
+            }
+            res = do_inquiry_prod_id(dev_name, op->block, sg_ver_num,
+                                     b, sizeof(b));
+            if (! force) {
+                if (res) {
+                    pr2serr_lk("INQUIRY failed on %s\n", dev_name);
+                    return 1;
+                }
+                // For safety, since <lba> written to, only permit scsi_debug
+                // devices. Bypass this with '-f' option.
+                if (0 != memcmp("scsi_debug", b, 10)) {
+                    pr2serr_lk("Since this utility may write to LBAs, "
+                               "only devices with the\n"
+                               "product ID 'scsi_debug' accepted. Use '-f' "
+                               "to override.\n");
+                    return 2;
+                }
+            }
+            if (sg_ver_num < 30000) {
+                pr2serr_lk("%s either not sg device or too old\n", dev_name);
+                return 2;
+            } else if (sg_ver_num >= 40030) {
+                op->sg_vn_ge_40030 = true;
+                op->sg_vn_ge_40000 = true;
+                if (! (op->v3_given || op->v4_given)) {
+                    op->v4 = true;
+                    op->v3 = false;
+                    op->submit = true;
+                }
+            } else if (sg_ver_num >= 40000) {
+                op->sg_vn_ge_40030 = false;
+                op->sg_vn_ge_40000 = true;
+                if (! (op->v3_given || op->v4_given)) {
+                    op->v4 = true;
+                    op->v3 = false;
+                    op->submit = true;
+                }
+            } else {
+                if (! (op->v3_given || op->v4_given)) {
+                    op->v4 = false;
+                    op->v3 = true;
+                    op->submit = false;
+                }
+            }
+
+            if ((SCSI_WRITE16 == op->c2e) || (SCSI_READ16 == op->c2e)) {
+                res = do_read_capacity(dev_name, op->block, &last_lba,
+                                       &blk_sz);
+                if (2 == res)
+                    res = do_read_capacity(dev_name, op->block, &last_lba,
+                                           &blk_sz);
+                if (res) {
+                    pr2serr_lk("READ CAPACITY(10) failed on %s\n", dev_name);
+                    return 1;
+                }
+                if (blk_sz != (unsigned int)op->lb_sz) {
+                    pr2serr_lk(">>> Logical block size (%d) of %s\n"
+                               "    differs from command line option (or "
+                               "default)\n", blk_sz, dev_name);
+                   pr2serr_lk("... continue anyway\n");
+                }
+                op->blk_szs.push_back(blk_sz);
+                if (UINT_MAX == op->hi_lba)
+                    op->hi_lbas.push_back(last_lba);
+            }
+        }
+
+        start_tm.tv_sec = 0;
+        start_tm.tv_nsec = 0;
+        if (clock_gettime(CLOCK_MONOTONIC, &start_tm) < 0)
+            perror("clock_gettime failed");
+
+        vector<thread *> vt;
+
+        /* start multi-threaded section */
+        for (k = 0; k < num_threads; ++k) {
+            thread * tp = new thread {work_thread, k, op};
+            vt.push_back(tp);
+        }
+
+        // g++ 4.7.3 didn't like range-for loop here
+        for (k = 0; k < (int)vt.size(); ++k)
+            vt[k]->join();
+        /* end multi-threaded section, just this main thread left */
+
+        for (k = 0; k < (int)vt.size(); ++k)
+            delete vt[k];
+
+        n = uniq_pack_id.load() - 1;
+        if (((n > 0) || op->generic_sync) &&
+            (0 == clock_gettime(CLOCK_MONOTONIC, &end_tm))) {
+            struct timespec res_tm;
+            double a, b;
+
+            if (op->generic_sync)
+                n = op->num_per_thread * num_threads;
+            res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+            res_tm.tv_nsec = end_tm.tv_nsec - start_tm.tv_nsec;
+            if (res_tm.tv_nsec < 0) {
+                --res_tm.tv_sec;
+                res_tm.tv_nsec += 1000000000;
+            }
+            a = res_tm.tv_sec;
+            a += (0.000001 * (res_tm.tv_nsec / 1000));
+            b = (double)n;
+            if (a > 0.000001) {
+                printf("Time to complete %d commands was %d.%06d seconds\n",
+                       n, (int)res_tm.tv_sec, (int)(res_tm.tv_nsec / 1000));
+                printf("Implies %.0f IOPS\n", (b / a));
+            }
+        }
+
+        if (op->verbose || op->stats) {
+            cout << "Number of sync_starts: " << sync_starts.load() << endl;
+            cout << "Number of async_starts: " << async_starts.load() << endl;
+            cout << "Number of async_finishes: " << async_finishes.load() <<
+                    endl;
+            cout << "Last pack_id: " << n << endl;
+        }
+        n = start_ebusy_count.load();
+        if (op->verbose || op->stats || (n > 0))
+            cout << "Number of start EBUSYs: " << n << endl;
+        n = fin_ebusy_count.load();
+        if (op->verbose || op->stats || (n > 0))
+            cout << "Number of finish EBUSYs: " << n << endl;
+        n = start_eagain_count.load();
+        if (op->verbose || op->stats || (n > 0))
+            cout << "Number of start EAGAINs: " << n << endl;
+        n = fin_eagain_count.load();
+        if (op->verbose || op->stats || (n > 0))
+            cout << "Number of finish EAGAINs: " << n << endl;
+        n = start_e2big_count.load();
+        if (op->verbose || op->stats || (n > 0))
+            cout << "Number of E2BIGs: " << n << endl;
+        n = start_edom_count.load();
+        if (op->verbose || op->stats || (n > 0))
+            cout << "Number of EDOMs: " << n << endl;
+        n = enomem_count.load();
+        if (op->verbose || op->stats || (n > 0))
+            cout << "Number of ENOMEMs: " << n << endl;
+    }
+    catch(system_error& e)  {
+        cerr << "got a system_error exception: " << e.what() << '\n';
+        auto ec = e.code();
+        cerr << "category: " << ec.category().name() << '\n';
+        cerr << "value: " << ec.value() << '\n';
+        cerr << "message: " << ec.message() << '\n';
+        cerr << "\nNote: if g++ may need '-pthread' or similar in "
+                "compile/link line" << '\n';
+    }
+    catch(...) {
+        cerr << "got another exception: " << '\n';
+    }
+    return 0;
+}
diff --git a/testing/sg_tst_bidi.c b/testing/sg_tst_bidi.c
new file mode 100644
index 0000000..0b81e14
--- /dev/null
+++ b/testing/sg_tst_bidi.c
@@ -0,0 +1,602 @@
+/*
+ *  Copyright (C) 2019 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Invocation: See usage() function below.
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_linux_inc.h"
+#include "sg_pr2serr.h"
+#include "sg_unaligned.h"
+
+/* This program tests bidirectional (bidi) SCSI command support in version 4.0
+ * and later of the Linux sg driver. The SBC-3 command XDWRITEREAD(10) that
+ is implemented by the scsi_debug driver is used.  */
+
+
+static const char * version_str = "Version: 1.06  20191021";
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_OP 0x12
+#define INQ_CMD_LEN 6
+#define SENSE_BUFFER_LEN 96
+#define XDWRITEREAD_10_OP 0x53
+#define XDWRITEREAD_10_LEN 10
+
+#define EBUFF_SZ 256
+
+#ifndef SG_FLAG_Q_AT_TAIL
+#define SG_FLAG_Q_AT_TAIL 0x10
+#endif
+
+#ifndef SG_FLAG_Q_AT_HEAD
+#define SG_FLAG_Q_AT_HEAD 0x20
+#endif
+
+#define DEF_Q_LEN 16    /* max in sg v3 and earlier */
+#define MAX_Q_LEN 256
+
+#define DEF_RESERVE_BUFF_SZ (256 * 1024)
+
+
+static bool q_at_tail = false;
+static int q_len = DEF_Q_LEN;
+static int sleep_secs = 0;
+static int reserve_buff_sz = DEF_RESERVE_BUFF_SZ;
+static int verbose = 0;
+
+
+static void
+usage(void)
+{
+    printf("Usage: sg_tst_bidi [-b=LB_SZ] [-d=DIO_BLKS] [-D] [-h] -l=LBA [-N] "
+           "[-q=Q_LEN]\n"
+           "                   [-Q] [-r=SZ] [-R=RC] [-s=SEC] [-t] [-v] [-V] "
+           "[-w]\n"
+           "                   <sg_or_bsg_device>\n"
+           " where:\n"
+           "      -b=LB_SZ    logical block size (def: 512 bytes)\n"
+           "      -d=DIO_BLKS    data in and out length (unit: logical "
+           "blocks; def: 1)\n"
+           "      -D    do direct IO (def: indirect which is also "
+           "fallback)\n"
+           "      -h    help: print usage message then exit\n"
+           "      -l=LBA    logical block address (LDA) of first modded "
+           "block\n"
+           "      -N    durations in nanoseconds (def: milliseconds)\n"
+           "      -q=Q_LEN    queue length, between 1 and 511 (def: 16). "
+           " Calls\n"
+           "                  ioctl(SG_IO) when -q=1 else SG_IOSUBMIT "
+           "(async)\n"
+           "      -Q    quiet, suppress usual output\n"
+           "      -r=SZ     reserve buffer size in KB (def: 256 --> 256 "
+           "KB)\n"
+           "      -R=RC     repetition count (def: 0)\n"
+           "      -s=SEC    sleep between writes and reads (def: 0)\n"
+           "      -t    queue_at_tail (def: q_at_head)\n"
+           "      -v    increase verbosity of output\n"
+           "      -V    print version string then exit\n"
+           "      -w    sets DISABLE WRITE bit on cdb to 0 (def: 1)\n\n"
+           "Warning: this test utility writes to location LBA and Q_LEN "
+           "following\nblocks using the XDWRITEREAD(10) SBC-3 command. "
+           "When -q=1 does\nioctl(SG_IO) and that is only case when a "
+           "bsg device can be given.\n");
+}
+
+static int
+ext_ioctl(int sg_fd, bool nanosecs)
+{
+    struct sg_extended_info sei;
+    struct sg_extended_info * seip;
+
+    seip = &sei;
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_RESERVED_SIZE;
+    seip->reserved_sz = reserve_buff_sz;
+    seip->sei_rd_mask |= SG_SEIM_RESERVED_SIZE;
+    seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+    seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS; /* this or previous optional */
+    seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+    if (nanosecs)
+        seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+    else
+        seip->ctl_flags &= ~SG_CTL_FLAGM_TIME_IN_NS;
+
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool done;
+    bool direct_io = false;
+    bool lba_given = false;
+    bool nanosecs = false;
+    bool quiet = false;
+    bool disable_write = true;
+    int k, j, ok, ver_num, pack_id, num_waiting, din_len;
+    int dout_len, cat;
+    int ret = 0;
+    int rep_count = 0;
+    int sg_fd = -1;
+    int lb_sz = 512;
+    int dio_blks = 1;
+    int dirio_count = 0;
+    int64_t lba = 0;
+    uint8_t inq_cdb[INQ_CMD_LEN] = {INQ_CMD_OP, 0, 0, 0, INQ_REPLY_LEN, 0};
+    uint8_t xdwrrd10_cdb[XDWRITEREAD_10_LEN] =
+                        {XDWRITEREAD_10_OP, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
+    uint8_t inqBuff[MAX_Q_LEN][INQ_REPLY_LEN];
+    struct sg_io_v4 io_v4[MAX_Q_LEN];
+    struct sg_io_v4 rio_v4;
+    struct sg_io_v4 * io_v4p;
+    char * file_name = 0;
+    uint8_t * dinp;
+    uint8_t * free_dinp = NULL;
+    uint8_t * doutp;
+    uint8_t * free_doutp = NULL;
+    char ebuff[EBUFF_SZ];
+    uint8_t sense_buffer[MAX_Q_LEN][SENSE_BUFFER_LEN] SG_C_CPP_ZERO_INIT;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-b=", argv[k], 3)) {
+            lb_sz = atoi(argv[k] + 3);
+            if (lb_sz < 512 || (0 != (lb_sz % 512))) {
+                printf("Expect -b=LB_SZ be 512 or higher and a power of 2\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-d=", argv[k], 3)) {
+            dio_blks = atoi(argv[k] + 3);
+            if ((dio_blks < 1) || (dio_blks > 0xffff)) {
+                fprintf(stderr, "Expect -d=DIO_BLKS to be 1 or greater and "
+                        "less than 65536\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-D", argv[k], 2))
+            direct_io = true;
+        else if (0 == memcmp("-h", argv[k], 2)) {
+            file_name = 0;
+            break;
+        } else if (0 == memcmp("-l=", argv[k], 3)) {
+            if (lba_given) {
+                pr2serr("can only give -l=LBA option once\n");
+                file_name = 0;
+                break;
+            }
+            lba = sg_get_llnum(argv[k] + 3);
+            if ((lba < 0) || (lba > 0xffffffff)) {
+                pr2serr("Expect -l= argument (LBA) to be non-negative and "
+                        "fit in 32 bits\n");
+                return -1;
+            }
+            lba_given = true;
+        } else if (0 == memcmp("-N", argv[k], 2))
+            nanosecs = true;
+        else if (0 == memcmp("-q=", argv[k], 3)) {
+            q_len = atoi(argv[k] + 3);
+            if ((q_len > 511) || (q_len < 1)) {
+                printf("Expect -q= to take a number (q length) between 1 "
+                       "and 511\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-Q", argv[k], 2))
+            quiet = true;
+        else if (0 == memcmp("-r=", argv[k], 3)) {
+            reserve_buff_sz = atoi(argv[k] + 3);
+            if (reserve_buff_sz < 0) {
+                printf("Expect -r= to take a number 0 or higher\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-R=", argv[k], 3)) {
+            rep_count = atoi(argv[k] + 3);
+            if (rep_count < 0) {
+                printf("Expect -R= to take a number 0 or higher\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-s=", argv[k], 3)) {
+            sleep_secs = atoi(argv[k] + 3);
+            if (sleep_secs < 0) {
+                printf("Expect -s= to take a number 0 or higher\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-t", argv[k], 2))
+            q_at_tail = true;
+        else if (0 == memcmp("-vvvvv", argv[k], 5))
+            verbose += 5;
+        else if (0 == memcmp("-vvvv", argv[k], 5))
+            verbose += 4;
+        else if (0 == memcmp("-vvv", argv[k], 4))
+            verbose += 3;
+        else if (0 == memcmp("-vv", argv[k], 3))
+            verbose += 2;
+        else if (0 == memcmp("-v", argv[k], 2))
+            verbose += 1;
+        else if (0 == memcmp("-V", argv[k], 2)) {
+            printf("%s\n", version_str);
+            return 0;
+        } else if (0 == memcmp("-w", argv[k], 2))
+            disable_write = false;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if (0 == file_name) {
+        printf("No filename (sg device) given\n\n");
+        usage();
+        return 1;
+    }
+    if (! lba_given) {
+        pr2serr("Needs the -l=LBA 'option' to be given, hex numbers "
+                "prefixed by '0x';\nor with a trailing 'h'\n");
+        ret = 1;
+        goto out;
+    }
+    din_len = lb_sz * dio_blks;
+    dout_len = lb_sz * dio_blks;
+    dinp = sg_memalign(din_len * q_len, 0, &free_dinp, false);
+    if (NULL == dinp) {
+        fprintf(stderr, "Unable to allocate %d byte for din buffer\n",
+                din_len * q_len);
+        ret = 1;
+        goto out;
+    }
+    doutp = sg_memalign(dout_len * q_len, 0, &free_doutp, false);
+    if (NULL == doutp) {
+        fprintf(stderr, "Unable to allocate %d byte for dout buffer\n",
+                dout_len * q_len);
+        ret = 1;
+        goto out;
+    }
+
+    /* An access mode of O_RDWR is required for write()/read() interface */
+    if ((sg_fd = open(file_name, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "error opening file: %s", file_name);
+        perror(ebuff);
+        return 1;
+    }
+    if (verbose)
+        fprintf(stderr, "opened given file: %s successfully, fd=%d\n",
+                file_name, sg_fd);
+
+    if (ioctl(sg_fd, SG_GET_VERSION_NUM, &ver_num) < 0) {
+        pr2serr("ioctl(SG_GET_VERSION_NUM) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        goto out;
+    }
+    if (! quiet)
+        printf("Linux sg driver version: %d\n", ver_num);
+    if ((q_len > 1) && ext_ioctl(sg_fd, nanosecs))
+        goto out;
+
+
+    if (1 == q_len) {   /* do sync ioct(SG_IO) */
+        io_v4p = &io_v4[k];
+rep_sg_io:
+        memset(io_v4p, 0, sizeof(*io_v4p));
+        io_v4p->guard = 'Q';
+        if (direct_io)
+            io_v4p->flags |= SG_FLAG_DIRECT_IO;
+        if (disable_write)
+            xdwrrd10_cdb[2] |= 0x4;
+        sg_put_unaligned_be16(dio_blks, xdwrrd10_cdb + 7);
+        sg_put_unaligned_be32(lba, xdwrrd10_cdb + 2);
+        if (verbose > 2) {
+            pr2serr("    %s cdb: ", "XDWRITE(10)");
+            for (j = 0; j < XDWRITEREAD_10_LEN; ++j)
+                pr2serr("%02x ", xdwrrd10_cdb[j]);
+            pr2serr("\n");
+        }
+        io_v4p->request_len = XDWRITEREAD_10_LEN;
+        io_v4p->request = (uint64_t)(uintptr_t)xdwrrd10_cdb;
+        io_v4p->din_xfer_len = din_len;
+        io_v4p->din_xferp = (uint64_t)(uintptr_t)(dinp + (k * din_len));
+        io_v4p->dout_xfer_len = dout_len;
+        io_v4p->dout_xferp = (uint64_t)(uintptr_t)(doutp + (k * dout_len));
+        io_v4p->response = (uint64_t)(uintptr_t)sense_buffer[k];
+        io_v4p->max_response_len = SENSE_BUFFER_LEN;
+        io_v4p->timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        io_v4p->request_extra = 99;  /* so pack_id doesn't start at 0 */
+        /* default is to queue at head (in SCSI mid level) */
+        if (q_at_tail)
+            io_v4p->flags |= SG_FLAG_Q_AT_TAIL;
+        else
+            io_v4p->flags |= SG_FLAG_Q_AT_HEAD;
+        /* io_v4p->usr_ptr = NULL; */
+
+        if (ioctl(sg_fd, SG_IO, io_v4p) < 0) {
+            pr2serr("sg ioctl(SG_IO) errno=%d [%s]\n", errno,
+                    strerror(errno));
+            close(sg_fd);
+            return 1;
+        }
+        /* now for the error processing */
+        ok = 0;
+        rio_v4 = *io_v4p;
+        cat = sg_err_category_new(rio_v4.device_status,
+                                  rio_v4.transport_status,
+                                  rio_v4.driver_status,
+                          (const uint8_t *)(unsigned long)rio_v4.response,
+                                  rio_v4.response_len);
+        switch (cat) {
+        case SG_LIB_CAT_CLEAN:
+            ok = 1;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            printf("Recovered error, continuing\n");
+            ok = 1;
+            break;
+        default: /* won't bother decoding other categories */
+            sg_linux_sense_print(NULL, rio_v4.device_status,
+                                 rio_v4.transport_status,
+                                 rio_v4.driver_status,
+                         (const uint8_t *)(unsigned long)rio_v4.response,
+                                 rio_v4.response_len, true);
+            break;
+        }
+        if ((rio_v4.info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO)
+            ++dirio_count;
+        if (verbose > 3) {
+            pr2serr(">> din_resid=%d, dout_resid=%d, info=0x%x\n",
+                    rio_v4.din_resid, rio_v4.dout_resid, rio_v4.info);
+            if (rio_v4.response_len > 0) {
+                pr2serr("sense buffer: ");
+                hex2stderr(sense_buffer[k], rio_v4.response_len, -1);
+            }
+        }
+        if ((! quiet) && ok)  /* output result if it is available */
+            printf("XDWRITEREAD(10) using ioctl(SG_IO) duration=%u\n",
+                   rio_v4.duration);
+        if (rep_count-- > 0)
+            goto rep_sg_io;
+        goto out;
+    }
+
+rep_async:
+    if (! quiet)
+        printf("start write() calls\n");
+    for (k = 0; k < q_len; ++k) {
+        io_v4p = &io_v4[k];
+        memset(io_v4p, 0, sizeof(*io_v4p));
+        io_v4p->guard = 'Q';
+        if (direct_io)
+            io_v4p->flags |= SG_FLAG_DIRECT_IO;
+        /* io_v4p->iovec_count = 0; */  /* memset takes care of this */
+        if (0 != (k % 64)) {
+            if (disable_write)
+                xdwrrd10_cdb[2] |= 0x4;
+            sg_put_unaligned_be16(dio_blks, xdwrrd10_cdb + 7);
+            sg_put_unaligned_be32(lba, xdwrrd10_cdb + 2);
+            if (verbose > 2) {
+                pr2serr("    %s cdb: ", "XDWRITE(10)");
+                for (j = 0; j < XDWRITEREAD_10_LEN; ++j)
+                    pr2serr("%02x ", xdwrrd10_cdb[j]);
+                pr2serr("\n");
+            }
+            io_v4p->request_len = XDWRITEREAD_10_LEN;
+            io_v4p->request = (uint64_t)(uintptr_t)xdwrrd10_cdb;
+            io_v4p->din_xfer_len = din_len;
+            io_v4p->din_xferp = (uint64_t)(uintptr_t)(dinp + (k * din_len));
+            io_v4p->dout_xfer_len = dout_len;
+            io_v4p->dout_xferp = (uint64_t)(uintptr_t)(doutp + (k * dout_len));
+        } else {
+            if (verbose > 2) {
+                pr2serr("    %s cdb: ", "INQUIRY");
+                for (j = 0; j < INQ_CMD_LEN; ++j)
+                    pr2serr("%02x ", inq_cdb[j]);
+                pr2serr("\n");
+            }
+            io_v4p->request_len = sizeof(inq_cdb);
+            io_v4p->request = (uint64_t)(uintptr_t)inq_cdb;
+            io_v4p->din_xfer_len = INQ_REPLY_LEN;
+            io_v4p->din_xferp = (uint64_t)(uintptr_t)inqBuff[k];
+        }
+        io_v4p->response = (uint64_t)(uintptr_t)sense_buffer[k];
+        io_v4p->max_response_len = SENSE_BUFFER_LEN;
+        io_v4p->timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        io_v4p->request_extra = k + 3;  /* so pack_id doesn't start at 0 */
+        /* default is to queue at head (in SCSI mid level) */
+        if (q_at_tail)
+            io_v4p->flags |= SG_FLAG_Q_AT_TAIL;
+        else
+            io_v4p->flags |= SG_FLAG_Q_AT_HEAD;
+        /* io_v4p->usr_ptr = NULL; */
+
+        if (ioctl(sg_fd, SG_IOSUBMIT, io_v4p) < 0) {
+            pr2serr("sg ioctl(SG_IOSUBMIT) errno=%d [%s]\n", errno,
+                    strerror(errno));
+            close(sg_fd);
+            return 1;
+        }
+    }
+
+#if 0
+    {
+        struct sg_scsi_id ssi;
+
+        memset(&ssi, 0, sizeof(ssi));
+        if (ioctl(sg_fd, SG_GET_SCSI_ID, &ssi) < 0)
+            pr2serr("ioctl(SG_GET_SCSI_ID) failed, errno=%d %s\n",
+                    errno, strerror(errno));
+        else {
+            printf("host_no: %d\n", ssi.host_no);
+            printf("  channel: %d\n", ssi.channel);
+            printf("  scsi_id: %d\n", ssi.scsi_id);
+            printf("  lun: %d\n", ssi.lun);
+            printf("  pdt: %d\n", ssi.scsi_type);
+            printf("  h_cmd_per_lun: %d\n", ssi.h_cmd_per_lun);
+            printf("  d_queue_depth: %d\n", ssi.d_queue_depth);
+        }
+    }
+#endif
+    if (ioctl(sg_fd, SG_GET_PACK_ID, &pack_id) < 0)
+        pr2serr("ioctl(SG_GET_PACK_ID) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else if (! quiet)
+        printf("first available pack_id: %d\n", pack_id);
+    if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0)
+        pr2serr("ioctl(SG_GET_NUM_WAITING) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else if (! quiet)
+        printf("num_waiting: %d\n", num_waiting);
+
+    if (sleep_secs > 0)
+        sleep(sleep_secs);
+
+    if (ioctl(sg_fd, SG_GET_PACK_ID, &pack_id) < 0)
+        pr2serr("ioctl(SG_GET_PACK_ID) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else if (! quiet)
+        printf("first available pack_id: %d\n", pack_id);
+    if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0)
+        pr2serr("ioctl(SG_GET_NUM_WAITING) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else if (! quiet)
+        printf("num_waiting: %d\n", num_waiting);
+
+    if (! quiet)
+        printf("\nstart read() calls\n");
+    for (k = 0, done = false; k < q_len; ++k) {
+        if ((! done) && (k == q_len / 2)) {
+            done = true;
+            if (! quiet)
+                printf("\n>>> half way through read\n");
+            if (ioctl(sg_fd, SG_GET_PACK_ID, &pack_id) < 0)
+                pr2serr("ioctl(SG_GET_PACK_ID) failed, errno=%d %s\n",
+                        errno, strerror(errno));
+            else if (! quiet)
+                printf("first available pack_id: %d\n", pack_id);
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0)
+                pr2serr("ioctl(SG_GET_NUM_WAITING) failed, errno=%d %s\n",
+                        errno, strerror(errno));
+            else if (! quiet)
+                printf("num_waiting: %d\n", num_waiting);
+        }
+        memset(&rio_v4, 0, sizeof(struct sg_io_v4));
+        rio_v4.guard = 'Q';
+        if (ioctl(sg_fd, SG_IORECEIVE, &rio_v4) < 0) {
+            perror("sg ioctl(SG_IORECEIVE) error");
+            close(sg_fd);
+            return 1;
+        }
+        /* now for the error processing */
+        ok = 0;
+        cat = sg_err_category_new(rio_v4.device_status,
+                                  rio_v4.transport_status,
+                                  rio_v4.driver_status,
+                          (const uint8_t *)(unsigned long)rio_v4.response,
+                                  rio_v4.response_len);
+        switch (cat) {
+        case SG_LIB_CAT_CLEAN:
+            ok = 1;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            printf("Recovered error, continuing\n");
+            ok = 1;
+            break;
+        default: /* won't bother decoding other categories */
+            sg_linux_sense_print(NULL, rio_v4.device_status,
+                                 rio_v4.transport_status,
+                                 rio_v4.driver_status,
+                         (const uint8_t *)(unsigned long)rio_v4.response,
+                                 rio_v4.response_len, true);
+            break;
+        }
+        if ((rio_v4.info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO)
+            ++dirio_count;
+        if (verbose > 3) {
+            pr2serr(">> din_resid=%d, dout_resid=%d, info=0x%x\n",
+                    rio_v4.din_resid, rio_v4.dout_resid, rio_v4.info);
+            if (rio_v4.response_len > 0) {
+                pr2serr("sense buffer: ");
+                hex2stderr(sense_buffer[k], rio_v4.response_len, -1);
+            }
+        }
+        if ((! quiet) && ok) { /* output result if it is available */
+            if (0 != ((rio_v4.request_extra - 3) % 64))
+                printf("XDWRITEREAD(10) %d duration=%u\n",
+                       rio_v4.request_extra, rio_v4.duration);
+            else
+                printf("INQUIRY %d duration=%u\n",
+                       rio_v4.request_extra,
+                       rio_v4.duration);
+        }
+    }
+    if (direct_io && (dirio_count < q_len)) {
+        pr2serr("Direct IO requested %d times, done %d times\nMaybe need "
+                "'echo 1 > /sys/module/sg/parameters/allow_dio'\n", q_len, dirio_count);
+    }
+    if (rep_count-- > 0)
+        goto rep_async;
+    ret = 0;
+
+out:
+    if (sg_fd >= 0)
+        close(sg_fd);
+    if (free_dinp)
+        free(free_dinp);
+    if (free_doutp)
+        free(free_doutp);
+    return ret;
+}
diff --git a/testing/sg_tst_context.cpp b/testing/sg_tst_context.cpp
new file mode 100644
index 0000000..f7cff2f
--- /dev/null
+++ b/testing/sg_tst_context.cpp
@@ -0,0 +1,502 @@
+/*
+ * Copyright (c) 2013-2022 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <iostream>
+#include <vector>
+#include <system_error>
+#include <thread>
+#include <mutex>
+#include <chrono>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_lib.h"
+#include "sg_pt.h"
+
+static const char * version_str = "1.06 20220425";
+static const char * util_name = "sg_tst_context";
+
+/* This is a test program for checking that file handles keep their
+ * context properly when sent (synchronous) SCSI pass-through commands.
+ * A disk device is assumed and even-numbered threads send TEST UNIT
+ * READY commands while odd-numbered threads send alternating START STOP
+ * UNIT commands (i.e. start then stop then start, etc). The point is to
+ * check the results to make sure that they don't get the other command's
+ * response. For example a START STOP UNIT command should not see a "not
+ * ready" sense key.
+ *
+ * This is C++ code with some things from C++11 (e.g. threads) and was
+ * only just able to compile (when some things were reverted) with gcc/g++
+ * version 4.7.3 found in Ubuntu 13.04 . C++11 "feature complete" support
+ * was not available until g++ version 4.8.1 and that is found in Fedora
+ * 19 and Ubuntu 13.10 .
+ *
+ * The build uses various object files from the <sg3_utils>/lib directory
+ * which is assumed to be a sibling of this examples directory. Those
+ * object files in the lib directory can be built with:
+ *   cd <sg3_utils> ; ./configure ; cd lib; make
+ * Then:
+ *   cd ../testing
+ *   make sg_tst_context
+ *
+ */
+
+using namespace std;
+using namespace std::chrono;
+
+#define DEF_NUM_PER_THREAD 200
+#define DEF_NUM_THREADS 2
+
+#define EBUFF_SZ 256
+
+
+static mutex count_mutex;
+static mutex console_mutex;
+static unsigned int even_notreadys;
+static unsigned int odd_notreadys;
+static unsigned int ebusy_count;
+static int verbose;
+
+
+static void
+usage(void)
+{
+    printf("Usage: %s [-e] [-h] [-n <n_per_thr>] [-N] [-R] [-s]\n"
+           "                      [-t <num_thrs>] [-v] [-V] <disk_device>\n",
+           util_name);
+    printf("  where\n");
+    printf("    -e                use O_EXCL on open (def: don't)\n");
+    printf("    -h                print this usage message then exit\n");
+    printf("    -n <n_per_thr>    number of loops per thread "
+           "(def: %d)\n", DEF_NUM_PER_THREAD);
+    printf("    -N                use O_NONBLOCK on open (def: don't)\n");
+    printf("    -R                make sure device in ready (started) "
+           "state after\n"
+           "                      test (do extra iteration if "
+           "necessary)\n");
+    printf("    -s                share an open file handle (def: one "
+           "per thread)\n");
+    printf("    -t <num_thrs>     number of threads (def: %d)\n",
+           DEF_NUM_THREADS);
+    printf("    -v                increase verbosity\n");
+    printf("    -V                print version number then exit\n\n");
+    printf("Test if file handles keep context through to their responses. "
+           "Sends\nTEST UNIT READY commands on even threads (origin 0) and "
+           "START STOP\nUNIT commands on odd threads. Expect NOT READY "
+           "sense keys only\nfrom the even threads (i.e from TUR)\n");
+}
+
+static int
+pt_err(int res)
+{
+    if (res < 0)
+        fprintf(stderr, "  pass through OS error: %s\n", safe_strerror(-res));
+    else if (SCSI_PT_DO_BAD_PARAMS == res)
+        fprintf(stderr, "  bad pass through setup\n");
+    else if (SCSI_PT_DO_TIMEOUT == res)
+        fprintf(stderr, "  pass through timeout\n");
+    else
+        fprintf(stderr, "  do_scsi_pt error=%d\n", res);
+    return (res < 0) ? res : -EPERM /* -1 */;
+}
+
+static int
+pt_cat_no_good(int cat, struct sg_pt_base * ptp, const unsigned char * sbp)
+{
+    int slen;
+    char b[256];
+    const int bl = (int)sizeof(b);
+    const char * cp = NULL;
+
+    b[0] = '\0';
+    switch (cat) {
+    case SCSI_PT_RESULT_STATUS: /* other than GOOD and CHECK CONDITION */
+        sg_get_scsi_status_str(get_scsi_pt_status_response(ptp), bl, b);
+        cp = "  scsi status: %s\n";
+        break;
+    case SCSI_PT_RESULT_SENSE:
+        slen = get_scsi_pt_sense_len(ptp);
+        sg_get_sense_str("", sbp, slen, 1, bl, b);
+        cp = "%s\n";
+        break;
+    case SCSI_PT_RESULT_TRANSPORT_ERR:
+        get_scsi_pt_transport_err_str(ptp, bl, b);
+        cp = "  transport: %s\n";
+        break;
+    case SCSI_PT_RESULT_OS_ERR:
+        get_scsi_pt_os_err_str(ptp, bl, b);
+        cp = "  os: %s\n";
+        break;
+    default:
+        cp = "  unknown pt result category (%d)\n";
+        break;
+    }
+    if (cp) {
+        lock_guard<mutex> lg(console_mutex);
+
+        fprintf(stderr, cp, b);
+    }
+    return -EIO /* -5 */;
+}
+
+#define TUR_CMD_LEN 6
+#define SSU_CMD_LEN 6
+#define NOT_READY SG_LIB_CAT_NOT_READY
+
+/* Returns 0 for good, 1024 for a sense key of NOT_READY, or a negative
+ * errno */
+static int
+do_tur(struct sg_pt_base * ptp, int id)
+{
+    int slen, res, cat;
+    unsigned char turCmdBlk [TUR_CMD_LEN] = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+
+    clear_scsi_pt_obj(ptp);
+    set_scsi_pt_cdb(ptp, turCmdBlk, sizeof(turCmdBlk));
+    set_scsi_pt_sense(ptp, sense_buffer, sizeof(sense_buffer));
+    res = do_scsi_pt(ptp, -1, 20 /* secs timeout */, verbose);
+    if (res) {
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            fprintf(stderr, "TEST UNIT READY do_scsi_pt() submission error, "
+                    "id=%d\n", id);
+        }
+        res = pt_err(res);
+        goto err;
+    }
+    cat = get_scsi_pt_result_category(ptp);
+    if (SCSI_PT_RESULT_GOOD != cat) {
+        slen = get_scsi_pt_sense_len(ptp);
+        if ((SCSI_PT_RESULT_SENSE == cat) &&
+            (NOT_READY == sg_err_category_sense(sense_buffer, slen))) {
+            res = 1024;
+            goto err;
+        }
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            fprintf(stderr, "TEST UNIT READY do_scsi_pt() category problem, "
+                    "id=%d\n", id);
+        }
+        res = pt_cat_no_good(cat, ptp, sense_buffer);
+        goto err;
+    }
+    res = 0;
+err:
+    return res;
+}
+
+/* Returns 0 for good, 1024 for a sense key of NOT_READY, or a negative
+ * errno */
+static int
+do_ssu(struct sg_pt_base * ptp, int id, bool start)
+{
+    int slen, res, cat;
+    unsigned char ssuCmdBlk [SSU_CMD_LEN] = {0x1b, 0x0, 0x0, 0x0, 0x0, 0x0};
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+
+    if (start)
+        ssuCmdBlk[4] |= 0x1;
+    clear_scsi_pt_obj(ptp);
+    set_scsi_pt_cdb(ptp, ssuCmdBlk, sizeof(ssuCmdBlk));
+    set_scsi_pt_sense(ptp, sense_buffer, sizeof(sense_buffer));
+    res = do_scsi_pt(ptp, -1, 40 /* secs timeout */, verbose);
+    if (res) {
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            fprintf(stderr, "START STOP UNIT do_scsi_pt() submission error, "
+                    "id=%d\n", id);
+        }
+        res = pt_err(res);
+        goto err;
+    }
+    cat = get_scsi_pt_result_category(ptp);
+    if (SCSI_PT_RESULT_GOOD != cat) {
+        slen = get_scsi_pt_sense_len(ptp);
+        if ((SCSI_PT_RESULT_SENSE == cat) &&
+            (NOT_READY == sg_err_category_sense(sense_buffer, slen))) {
+            res = 1024;
+            goto err;
+        }
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            fprintf(stderr, "START STOP UNIT do_scsi_pt() category problem, "
+                    "id=%d\n", id);
+        }
+        res = pt_cat_no_good(cat, ptp, sense_buffer);
+        goto err;
+    }
+    res = 0;
+err:
+    return res;
+}
+
+static void
+work_thread(const char * dev_name, int id, int num, bool share,
+            int pt_fd, int nonblock, int oexcl, bool ready_after)
+{
+    bool started = true;
+    int k;
+    int res = 0;
+    unsigned int thr_even_notreadys = 0;
+    unsigned int thr_odd_notreadys = 0;
+    struct sg_pt_base * ptp = NULL;
+
+    {
+        lock_guard<mutex> lg(console_mutex);
+
+        cerr << "Enter work_thread id=" << id << " num=" << num << " share="
+             << share << endl;
+    }
+    if (! share) {      /* ignore passed ptp, make this thread's own */
+        int oflags = O_RDWR;
+        unsigned int thr_ebusy_count = 0;
+
+        if (nonblock)
+            oflags |= O_NONBLOCK;
+        if (oexcl)
+            oflags |= O_EXCL;
+        while (((pt_fd = scsi_pt_open_flags(dev_name, oflags, verbose)) < 0)
+               && (-EBUSY == pt_fd)) {
+            ++thr_ebusy_count;
+            this_thread::yield();       // give other threads a chance
+        }
+        if (pt_fd < 0) {
+            char ebuff[EBUFF_SZ];
+
+            snprintf(ebuff, EBUFF_SZ, "work_thread id=%d: error opening: %s",
+                     id, dev_name);
+            perror(ebuff);
+            return;
+        }
+        if (thr_ebusy_count) {
+            lock_guard<mutex> lg(count_mutex);
+
+            ebusy_count += thr_ebusy_count;
+        }
+    }
+    /* The instance of 'struct sg_pt_base' is local to this thread but the
+     * pt_fd it contains may be shared, depending on the 'share' boolean. */
+    ptp = construct_scsi_pt_obj_with_fd(pt_fd, verbose);
+    if (NULL == ptp) {
+        fprintf(stderr, "work_thread id=%d: "
+                "construct_scsi_pt_obj_with_fd() failed, memory?\n", id);
+        return;
+    }
+    for (k = 0; k < num; ++k) {
+        if (0 == (id % 2)) {
+            /* Even thread ids do TEST UNIT READYs */
+            res = do_tur(ptp, id);
+            if (1024 == res) {
+                ++thr_even_notreadys;
+                res = 0;
+            }
+        } else {
+            /* Odd thread ids do START STOP UNITs, alternating between
+             * starts and stops */
+            started = (0 == (k % 2));
+            res = do_ssu(ptp, id, started);
+            if (1024 == res) {
+                ++thr_odd_notreadys;
+                res = 0;
+            }
+        }
+        if (res)
+            break;
+        if (ready_after && (! started))
+            do_ssu(ptp, id, true);
+    }
+    if (ptp)
+        destruct_scsi_pt_obj(ptp);
+    if ((! share) && (pt_fd >= 0))
+        close(pt_fd);
+
+    {
+        lock_guard<mutex> lg(count_mutex);
+
+        even_notreadys += thr_even_notreadys;
+        odd_notreadys += thr_odd_notreadys;
+    }
+
+    {
+        lock_guard<mutex> lg(console_mutex);
+
+        if (k < num)
+            cerr << "thread id=" << id << " FAILed at iteration: " << k
+                 << "  [negated errno: " << res << " <"
+                 <<  safe_strerror(-res) << ">]" << endl;
+        else
+            cerr << "thread id=" << id << " normal exit" << '\n';
+    }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    int k;
+    int pt_fd = -1;
+    int oexcl = 0;
+    int nonblock = 0;
+    int num_per_thread = DEF_NUM_PER_THREAD;
+    bool ready_after = false;
+    bool share = false;
+    int num_threads = DEF_NUM_THREADS;
+    char * dev_name = NULL;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-e", argv[k], 2))
+            ++oexcl;
+        else if (0 == memcmp("-h", argv[k], 2)) {
+            usage();
+            return 0;
+        } else if (0 == memcmp("-n", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k])) {
+                num_per_thread = sg_get_num(argv[k]);
+                if (num_per_thread<= 0) {
+                    fprintf(stderr, "want positive integer for number "
+                            "per thread\n");
+                    return 1;
+                }
+            } else
+                break;
+        } else if (0 == memcmp("-N", argv[k], 2))
+            ++nonblock;
+        else if (0 == memcmp("-R", argv[k], 2))
+            ready_after = true;
+        else if (0 == memcmp("-s", argv[k], 2))
+            share = true;
+        else if (0 == memcmp("-t", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                num_threads = atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-v", argv[k], 2))
+            ++verbose;
+        else if (0 == memcmp("-V", argv[k], 2)) {
+            printf("%s version: %s\n", util_name, version_str);
+            return 0;
+        } else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            dev_name = NULL;
+            break;
+        }
+        else if (! dev_name)
+            dev_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            dev_name = 0;
+            break;
+        }
+    }
+    if (0 == dev_name) {
+        usage();
+        return 1;
+    }
+    try {
+        if (share) {
+            int oflags = O_RDWR;
+
+            if (nonblock)
+                oflags |= O_NONBLOCK;
+            if (oexcl)
+                oflags |= O_EXCL;
+            while (((pt_fd = scsi_pt_open_flags(dev_name, oflags, verbose))
+                    < 0) && (-EBUSY == pt_fd)) {
+                ++ebusy_count;
+                sleep(0);                   // process yield ??
+            }
+            if (pt_fd < 0) {
+                char ebuff[EBUFF_SZ];
+
+                snprintf(ebuff, EBUFF_SZ, "main: error opening: %s",
+                         dev_name);
+                perror(ebuff);
+                return 1;
+            }
+            /* Tried calling construct_scsi_pt_obj_with_fd() here but that
+             * doesn't work since 'struct sg_pt_base' objects aren't
+             * thread-safe without user space intervention (e.g. mutexes). */
+        }
+
+        vector<thread *> vt;
+
+        for (k = 0; k < num_threads; ++k) {
+            thread * tp = new thread {work_thread, dev_name, k,
+                                      num_per_thread, share, pt_fd, nonblock,
+                                      oexcl, ready_after};
+            vt.push_back(tp);
+        }
+
+        for (k = 0; k < (int)vt.size(); ++k)
+            vt[k]->join();
+
+        for (k = 0; k < (int)vt.size(); ++k)
+            delete vt[k];
+
+        if (share)
+            scsi_pt_close_device(pt_fd);
+
+        cout << "Expected not_readys on TEST UNIT READY: " << even_notreadys
+             << endl;
+        cout << "UNEXPECTED not_readys on START STOP UNIT: "
+             << odd_notreadys << endl;
+        if (ebusy_count)
+            cout << "Number of EBUSYs (on open): " << ebusy_count << endl;
+
+    }
+    catch(system_error& e)  {
+        cerr << "got a system_error exception: " << e.what() << '\n';
+        auto ec = e.code();
+        cerr << "category: " << ec.category().name() << '\n';
+        cerr << "value: " << ec.value() << '\n';
+        cerr << "message: " << ec.message() << '\n';
+        cerr << "\nNote: if g++ may need '-pthread' or similar in "
+                "compile/link line" << '\n';
+    }
+    catch(...) {
+        cerr << "got another exception: " << '\n';
+    }
+    if (pt_fd >= 0)
+        close(pt_fd);
+    return 0;
+}
diff --git a/testing/sg_tst_excl.cpp b/testing/sg_tst_excl.cpp
new file mode 100644
index 0000000..0354de6
--- /dev/null
+++ b/testing/sg_tst_excl.cpp
@@ -0,0 +1,984 @@
+/*
+ * Copyright (c) 2013-2022 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <iostream>
+#include <vector>
+#include <system_error>
+#include <thread>
+#include <mutex>
+#include <chrono>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+
+static const char * version_str = "1.14 20220425";
+static const char * util_name = "sg_tst_excl";
+
+/* This is a test program for checking O_EXCL on open() works. It uses
+ * multiple threads and can be run as multiple processes and attempts
+ * to "break" O_EXCL. The strategy is to open a device O_EXCL|O_NONBLOCK
+ * and do a double increment on a LB then close it. Prior to the first
+ * increment, the value is checked for even or odd. Assuming the count
+ * starts as an even (typically 0) then it should remain even. Odd instances
+ * are counted and reported at the end of the program, after all threads
+ * have completed.
+ *
+ * This is C++ code with some things from C++11 (e.g. threads) and was
+ * only just able to compile (when some things were reverted) with gcc/g++
+ * version 4.7.3 found in Ubuntu 13.04 . C++11 "feature complete" support
+ * was not available until g++ version 4.8.1 and that is only currently
+ * found in Fedora 19 .
+ *
+ * The build uses various object files from the <sg3_utils>/lib directory
+ * which is assumed to be a sibling of this examples directory. Those
+ * object files in the lib directory can be built with:
+ *   cd <sg3_utils> ; ./configure ; cd lib; make
+ * Then:
+ *   cd ../testing
+ *   make sg_tst_excl
+ *
+ * Currently this utility is Linux only and assumes the SG_IO v3 interface
+ * which is supported by sg and block devices (but not bsg devices which
+ * require the SG_IO v4 interface). This restriction is relaxed in the
+ * sg_tst_excl2 variant of this utility.
+ *
+ * BEWARE: this utility modifies a logical block (default LBA 1000) on the
+ * given device.
+ *
+ */
+
+using namespace std;
+using namespace std::chrono;
+
+#define DEF_NUM_PER_THREAD 200
+#define DEF_NUM_THREADS 4
+#define DEF_WAIT_MS 0          /* 0: yield; -1: don't wait; -2: sleep(0) */
+
+
+#define DEF_LBA 1000U
+
+#define EBUFF_SZ 256
+
+static mutex odd_count_mutex;
+static mutex console_mutex;
+static unsigned int odd_count;
+static unsigned int ebusy_count;
+static unsigned int eagain_count;
+static int sg_ifc_ver = 3;
+
+
+static void
+usage(void)
+{
+    printf("Usage: %s [-b] [-f] [-h] [-i <sg_ver>] [-l <lba>] "
+           "[-n <n_per_thr>]\n"
+           "                   [-t <num_thrs>] [-V] [-w <wait_ms>] "
+           "[-x] [-xx]\n"
+           "                   <sg_disk_device>\n", util_name);
+    printf("  where\n");
+    printf("    -b                block on open (def: O_NONBLOCK)\n");
+    printf("    -f                force: any SCSI disk (def: only "
+           "scsi_debug)\n");
+    printf("                      WARNING: <lba> written to\n");
+    printf("    -h                print this usage message then exit\n");
+    printf("    -i <sg_ver>       sg driver interface version (default: "
+           "3)\n");
+    printf("    -l <lba>          logical block to increment (def: %u)\n",
+           DEF_LBA);
+    printf("    -n <n_per_thr>    number of loops per thread "
+           "(def: %d)\n", DEF_NUM_PER_THREAD);
+    printf("    -t <num_thrs>     number of threads (def: %d)\n",
+           DEF_NUM_THREADS);
+    printf("    -V                print version number then exit\n");
+    printf("    -w <wait_ms>      >0: sleep_for(<wait_ms>); =0: "
+           "yield(); -1: no\n"
+           "                      wait; -2: sleep(0)  (def: %d)\n",
+           DEF_WAIT_MS);
+    printf("    -x                don't use O_EXCL on first thread "
+           "(def: use\n"
+           "                      O_EXCL on all threads)\n"
+           "    -xx               don't use O_EXCL on any thread\n\n");
+    printf("Test O_EXCL open flag with Linux sg driver. Each open/close "
+           "cycle with the\nO_EXCL flag does a double increment on "
+           "lba (using its first 4 bytes).\nEach increment uses a READ_16, "
+           "READ_16, increment, WRITE_16 cycle. The two\nREAD_16s are "
+           "launched asynchronously. Note that '-xx' will run test\n"
+           "without any O_EXCL flags.\n");
+}
+
+
+#define READ16_REPLY_LEN 512
+#define READ16_CMD_LEN 16
+#define WRITE16_REPLY_LEN 512
+#define WRITE16_CMD_LEN 16
+
+/* Opens dev_name and spins if busy (i.e. gets EBUSY), sleeping for
+ * wait_ms milliseconds if wait_ms is positive.
+ * Reads lba (twice) and treats the first 4 bytes as an int (SCSI endian),
+ * increments it and writes it back. Repeats so that happens twice. Then
+ * closes dev_name. If an error occurs returns -1 else returns 0 if
+ * first int read from lba is even otherwise returns 1. */
+static int
+do_rd_inc_wr_twice_v3(const char * dev_name, unsigned int lba, int block,
+                      int excl, int wait_ms, int id, unsigned int & ebusy,
+                      unsigned int & eagains)
+{
+    bool odd = false;
+    int k, sg_fd;
+    struct sg_io_hdr pt, pt2;
+    unsigned char r16CmdBlk [READ16_CMD_LEN] =
+                {0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    unsigned char w16CmdBlk [WRITE16_CMD_LEN] =
+                {0x8a, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    unsigned char lb[READ16_REPLY_LEN];
+    int open_flags = O_RDWR;
+
+    sg_put_unaligned_be64(lba, r16CmdBlk + 2);
+    sg_put_unaligned_be64(lba, w16CmdBlk + 2);
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    if (excl)
+        open_flags |= O_EXCL;
+
+    while (((sg_fd = open(dev_name, open_flags)) < 0) &&
+           (EBUSY == errno)) {
+        ++ebusy;
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (sg_fd < 0) {
+        char ebuff[EBUFF_SZ];
+
+        snprintf(ebuff, EBUFF_SZ, "%s: error opening file: %s", __func__,
+                 dev_name);
+        perror(ebuff);
+        return -1;
+    }
+
+    for (k = 0; k < 2; ++k) {
+        bool ok = false;
+        int res;
+        unsigned int u = 0;
+
+        /* Prepare READ_16 command */
+        memset(&pt, 0, sizeof(pt));
+        pt.interface_id = 'S';
+        pt.cmd_len = sizeof(r16CmdBlk);
+        pt.mx_sb_len = sizeof(sense_buffer);
+        pt.dxfer_direction = SG_DXFER_FROM_DEV;
+        pt.dxfer_len = READ16_REPLY_LEN;
+        pt.dxferp = lb;
+        pt.cmdp = r16CmdBlk;
+        pt.sbp = sense_buffer;
+        pt.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        pt.pack_id = id;
+
+        // queue up two READ_16s to same LBA
+        if (write(sg_fd, &pt, sizeof(pt)) < 0) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                perror(" write(sg, READ_16)");
+            }
+            close(sg_fd);
+            return -1;
+        }
+        pt2 = pt;
+        if (write(sg_fd, &pt2, sizeof(pt2)) < 0) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                perror(" write(sg, READ_16) 2");
+            }
+            close(sg_fd);
+            return -1;
+        }
+
+        while (((res = read(sg_fd, &pt, sizeof(pt))) < 0) &&
+               (EAGAIN == errno)) {
+            ++eagains;
+            if (wait_ms > 0)
+                this_thread::sleep_for(milliseconds{wait_ms});
+            else if (0 == wait_ms)
+                this_thread::yield();
+            else if (-2 == wait_ms)
+                sleep(0);                   // process yield ??
+        }
+        if (res < 0) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                perror(" read(sg, READ_16)");
+            }
+            close(sg_fd);
+            return -1;
+        }
+        /* now for the error processing */
+        switch (sg_err_category3(&pt)) {
+        case SG_LIB_CAT_CLEAN:
+            ok = true;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "Recovered error on READ_16, continuing\n");
+            }
+            ok = true;
+            break;
+        default: /* won't bother decoding other categories */
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                sg_chk_n_print3("READ_16 command error", &pt, 1);
+            }
+            break;
+        }
+        if (ok) {
+            while (((res = read(sg_fd, &pt2, sizeof(pt2))) < 0) &&
+                   (EAGAIN == errno)) {
+                ++eagains;
+                if (wait_ms > 0)
+                    this_thread::sleep_for(milliseconds{wait_ms});
+                else if (0 == wait_ms)
+                    this_thread::yield();
+                else if (-2 == wait_ms)
+                    sleep(0);                   // process yield ??
+            }
+            if (res < 0) {
+                {
+                    lock_guard<mutex> lg(console_mutex);
+
+                    perror(" read(sg, READ_16) 2");
+                }
+                close(sg_fd);
+                return -1;
+            }
+            pt = pt2;
+            /* now for the error processing */
+            ok = false;
+            switch (sg_err_category3(&pt)) {
+            case SG_LIB_CAT_CLEAN:
+                ok = true;
+                break;
+            case SG_LIB_CAT_RECOVERED:
+                {
+                    lock_guard<mutex> lg(console_mutex);
+
+                    fprintf(stderr, "%s: Recovered error on READ_16, "
+                            "continuing 2\n", __func__);
+                }
+                ok = true;
+                break;
+            default: /* won't bother decoding other categories */
+                {
+                    lock_guard<mutex> lg(console_mutex);
+
+                    sg_chk_n_print3("READ_16 command error 2", &pt, 1);
+                }
+                break;
+            }
+        }
+        if (! ok) {
+            close(sg_fd);
+            return -1;
+        }
+
+        u = sg_get_unaligned_be32(lb);
+        // Assuming u starts test as even (probably 0), expect it to stay even
+        if (0 == k)
+            odd = (1 == (u % 2));
+        ++u;
+        sg_put_unaligned_be32(u, lb);
+
+        if (wait_ms > 0)       /* allow daylight for bad things ... */
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+
+        /* Prepare WRITE_16 command */
+        memset(&pt, 0, sizeof(pt));
+        pt.interface_id = 'S';
+        pt.cmd_len = sizeof(w16CmdBlk);
+        pt.mx_sb_len = sizeof(sense_buffer);
+        pt.dxfer_direction = SG_DXFER_TO_DEV;
+        pt.dxfer_len = WRITE16_REPLY_LEN;
+        pt.dxferp = lb;
+        pt.cmdp = w16CmdBlk;
+        pt.sbp = sense_buffer;
+        pt.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        pt.pack_id = id;
+
+        if (ioctl(sg_fd, SG_IO, &pt) < 0) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                perror(" WRITE_16 SG_IO ioctl error");
+            }
+            close(sg_fd);
+            return -1;
+        }
+        /* now for the error processing */
+        ok = false;
+        switch (sg_err_category3(&pt)) {
+        case SG_LIB_CAT_CLEAN:
+            ok = true;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "%s: Recovered error on WRITE_16, "
+                        "continuing\n", __func__);
+            }
+            ok = true;
+            break;
+        default: /* won't bother decoding other categories */
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                sg_chk_n_print3("WRITE_16 command error", &pt, 1);
+            }
+            break;
+        }
+        if (! ok) {
+            close(sg_fd);
+            return -1;
+        }
+    }
+    close(sg_fd);
+    return (int)odd;
+}
+
+/* Opens dev_name and spins if busy (i.e. gets EBUSY), sleeping for
+ * wait_ms milliseconds if wait_ms is positive.
+ * Reads lba (twice) and treats the first 4 bytes as an int (SCSI endian),
+ * increments it and writes it back. Repeats so that happens twice. Then
+ * closes dev_name. If an error occurs returns -1 else returns 0 if
+ * first int read from lba is even otherwise returns 1. */
+static int
+do_rd_inc_wr_twice_v4(const char * dev_name, unsigned int lba, int block,
+                      int excl, int wait_ms, int id, unsigned int & ebusy,
+                      unsigned int & eagains)
+{
+    bool odd = false;
+    int k, sg_fd;
+    struct sg_io_v4 pt, pt2;
+    unsigned char r16CmdBlk [READ16_CMD_LEN] =
+                {0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    unsigned char w16CmdBlk [WRITE16_CMD_LEN] =
+                {0x8a, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    unsigned char lb[READ16_REPLY_LEN];
+    int open_flags = O_RDWR;
+
+    sg_put_unaligned_be64(lba, r16CmdBlk + 2);
+    sg_put_unaligned_be64(lba, w16CmdBlk + 2);
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    if (excl)
+        open_flags |= O_EXCL;
+
+    while (((sg_fd = open(dev_name, open_flags)) < 0) &&
+           (EBUSY == errno)) {
+        ++ebusy;
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (sg_fd < 0) {
+        char ebuff[EBUFF_SZ];
+
+        snprintf(ebuff, EBUFF_SZ, "%s: error opening file: %s", __func__,
+                 dev_name);
+        perror(ebuff);
+        return -1;
+    }
+
+    for (k = 0; k < 2; ++k) {
+        bool ok = false;
+        int res;
+        unsigned int u = 0;
+
+        /* Prepare READ_16 command */
+        memset(&pt, 0, sizeof(pt));
+        pt.guard = 'Q';
+        pt.request_len = sizeof(r16CmdBlk);
+        pt.max_response_len = sizeof(sense_buffer);
+        // pt.dxfer_direction = SG_DXFER_FROM_DEV;
+        pt.din_xfer_len = READ16_REPLY_LEN;
+        pt.din_xferp = (uint64_t)(sg_uintptr_t)lb;
+        pt.request = (uint64_t)(sg_uintptr_t)r16CmdBlk;
+        pt.response = (uint64_t)(sg_uintptr_t)sense_buffer;
+        pt.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        pt.request_extra = id;  /* pack_id field */
+
+        // queue up two READ_16s to same LBA
+        if (ioctl(sg_fd, SG_IOSUBMIT, &pt) < 0) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                perror(" write(sg, READ_16)");
+            }
+            close(sg_fd);
+            return -1;
+        }
+        pt2 = pt;
+        if (ioctl(sg_fd, SG_IOSUBMIT, &pt2) < 0) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                perror(" write(sg, READ_16) 2");
+            }
+            close(sg_fd);
+            return -1;
+        }
+
+        while (((res = ioctl(sg_fd, SG_IORECEIVE, &pt)) < 0) &&
+               (EAGAIN == errno)) {
+            ++eagains;
+            if (wait_ms > 0)
+                this_thread::sleep_for(milliseconds{wait_ms});
+            else if (0 == wait_ms)
+                this_thread::yield();
+            else if (-2 == wait_ms)
+                sleep(0);                   // process yield ??
+        }
+        if (res < 0) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                perror(" read(sg, READ_16)");
+            }
+            close(sg_fd);
+            return -1;
+        }
+        /* now for the error processing */
+        switch (sg_err_category_new(pt.device_status, pt.transport_status,
+                pt.driver_status, sense_buffer, pt.response_len)) {
+        case SG_LIB_CAT_CLEAN:
+            ok = true;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "Recovered error on READ_16, continuing\n");
+            }
+            ok = true;
+            break;
+        default: /* won't bother decoding other categories */
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                sg_linux_sense_print("READ_16 command error",
+                                     pt.device_status, pt.transport_status,
+                                     pt.driver_status, sense_buffer,
+                                     pt.response_len, true);
+                // sg_chk_n_print3("READ_16 command error", &pt, 1);
+            }
+            break;
+        }
+        if (ok) {
+            while (((res = ioctl(sg_fd, SG_IORECEIVE, &pt2)) < 0) &&
+                   (EAGAIN == errno)) {
+                ++eagains;
+                if (wait_ms > 0)
+                    this_thread::sleep_for(milliseconds{wait_ms});
+                else if (0 == wait_ms)
+                    this_thread::yield();
+                else if (-2 == wait_ms)
+                    sleep(0);                   // process yield ??
+            }
+            if (res < 0) {
+                {
+                    lock_guard<mutex> lg(console_mutex);
+
+                    perror(" read(sg, READ_16) 2");
+                }
+                close(sg_fd);
+                return -1;
+            }
+            pt = pt2;
+            /* now for the error processing */
+            ok = false;
+            switch (sg_err_category_new(pt.device_status, pt.transport_status,
+                    pt.driver_status, sense_buffer, pt.response_len)) {
+            case SG_LIB_CAT_CLEAN:
+                ok = true;
+                break;
+            case SG_LIB_CAT_RECOVERED:
+                {
+                    lock_guard<mutex> lg(console_mutex);
+
+                    fprintf(stderr, "%s: Recovered error on READ_16, "
+                            "continuing 2\n", __func__);
+                }
+                ok = true;
+                break;
+            default: /* won't bother decoding other categories */
+                {
+                    lock_guard<mutex> lg(console_mutex);
+
+                    sg_linux_sense_print("READ_16 command error 2",
+                                         pt.device_status,
+                                         pt.transport_status,
+                                         pt.driver_status, sense_buffer,
+                                         pt.response_len, true);
+                    // sg_chk_n_print3("READ_16 command error 2", &pt, 1);
+                }
+                break;
+            }
+        }
+        if (! ok) {
+            close(sg_fd);
+            return -1;
+        }
+
+        u = sg_get_unaligned_be32(lb);
+        // Assuming u starts test as even (probably 0), expect it to stay even
+        if (0 == k)
+            odd = (1 == (u % 2));
+        ++u;
+        sg_put_unaligned_be32(u, lb);
+
+        if (wait_ms > 0)       /* allow daylight for bad things ... */
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+
+        /* Prepare WRITE_16 command */
+        memset(&pt, 0, sizeof(pt));
+        pt.guard = 'Q';
+        pt.request_len = sizeof(w16CmdBlk);
+        pt.max_response_len = sizeof(sense_buffer);
+        // pt.dxfer_direction = SG_DXFER_TO_DEV;
+        pt.dout_xfer_len = WRITE16_REPLY_LEN;
+        pt.dout_xferp = (uint64_t)(sg_uintptr_t)lb;
+        pt.request = (uint64_t)(sg_uintptr_t)w16CmdBlk;
+        pt.response = (uint64_t)(sg_uintptr_t)sense_buffer;
+        pt.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        pt.request_extra = id;  /* pack_id field */
+
+        if (ioctl(sg_fd, SG_IO, &pt) < 0) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                perror(" WRITE_16 SG_IO ioctl error");
+            }
+            close(sg_fd);
+            return -1;
+        }
+        /* now for the error processing */
+        ok = false;
+        switch (sg_err_category_new(pt.device_status, pt.transport_status,
+                pt.driver_status, sense_buffer, pt.response_len)) {
+        case SG_LIB_CAT_CLEAN:
+            ok = true;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "%s: Recovered error on WRITE_16, "
+                        "continuing\n", __func__);
+            }
+            ok = true;
+            break;
+        default: /* won't bother decoding other categories */
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                sg_linux_sense_print("WRITE_16 command error",
+                                     pt.device_status, pt.transport_status,
+                                     pt.driver_status, sense_buffer,
+                                     pt.response_len, true);
+            }
+            break;
+        }
+        if (! ok) {
+            close(sg_fd);
+            return -1;
+        }
+    }
+    close(sg_fd);
+    return odd;
+}
+
+
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+
+/* Send INQUIRY and fetches response. If okay puts PRODUCT ID field
+ * in b (up to m_blen bytes). Does not use O_EXCL flag. Returns 0 on success,
+ * else -1 . */
+static int
+do_inquiry_prod_id(const char * dev_name, int block, int wait_ms,
+                   unsigned int & ebusys, char * b, int b_mlen)
+{
+    int sg_fd, ok, ret;
+    struct sg_io_hdr pt;
+    unsigned char inqCmdBlk [INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    unsigned char inqBuff[INQ_REPLY_LEN];
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    int open_flags = O_RDWR;    /* O_EXCL | O_RDONLY fails with EPERM */
+
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    while (((sg_fd = open(dev_name, open_flags)) < 0) &&
+           (EBUSY == errno)) {
+        ++ebusys;
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (sg_fd < 0) {
+        char ebuff[EBUFF_SZ];
+
+        snprintf(ebuff, EBUFF_SZ,
+                 "do_inquiry_prod_id: error opening file: %s", dev_name);
+        perror(ebuff);
+        return -1;
+    }
+    /* Prepare INQUIRY command */
+    memset(&pt, 0, sizeof(pt));
+    pt.interface_id = 'S';
+    pt.cmd_len = sizeof(inqCmdBlk);
+    /* pt.iovec_count = 0; */  /* memset takes care of this */
+    pt.mx_sb_len = sizeof(sense_buffer);
+    pt.dxfer_direction = SG_DXFER_FROM_DEV;
+    pt.dxfer_len = INQ_REPLY_LEN;
+    pt.dxferp = inqBuff;
+    pt.cmdp = inqCmdBlk;
+    pt.sbp = sense_buffer;
+    pt.timeout = 20000;     /* 20000 millisecs == 20 seconds */
+    /* pt.flags = 0; */     /* take defaults: indirect IO, etc */
+    /* pt.pack_id = 0; */
+    /* pt.usr_ptr = NULL; */
+
+    if (ioctl(sg_fd, SG_IO, &pt) < 0) {
+        perror("do_inquiry_prod_id: Inquiry SG_IO ioctl error");
+        close(sg_fd);
+        return -1;
+    }
+
+    /* now for the error processing */
+    ok = 0;
+    switch (sg_err_category3(&pt)) {
+    case SG_LIB_CAT_CLEAN:
+        ok = 1;
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        fprintf(stderr, "Recovered error on INQUIRY, continuing\n");
+        ok = 1;
+        break;
+    default: /* won't bother decoding other categories */
+        sg_chk_n_print3("INQUIRY command error", &pt, 1);
+        break;
+    }
+    if (ok) {
+        /* Good, so fetch Product ID from response, copy to 'b' */
+        if (b_mlen > 0) {
+            if (b_mlen > 16) {
+                memcpy(b, inqBuff + 16, 16);
+                b[16] = '\0';
+            } else {
+                memcpy(b, inqBuff + 16, b_mlen - 1);
+                b[b_mlen - 1] = '\0';
+            }
+        }
+        ret = 0;
+    } else
+        ret = -1;
+    close(sg_fd);
+    return ret;
+}
+
+static void
+work_thread(const char * dev_name, unsigned int lba, int id, int block,
+            int excl, int num, int wait_ms)
+{
+    unsigned int thr_odd_count = 0;
+    unsigned int thr_ebusy_count = 0;
+    unsigned int thr_eagain_count = 0;
+    int k, res;
+
+    {
+        lock_guard<mutex> lg(console_mutex);
+
+        cerr << "Enter work_thread id=" << id << " excl=" << excl << " block="
+             << block << endl;
+    }
+    for (k = 0; k < num; ++k) {
+        if (sg_ifc_ver == 3)
+            res = do_rd_inc_wr_twice_v3(dev_name, lba, block, excl, wait_ms,
+                                        k, thr_ebusy_count, thr_eagain_count);
+        else if (sg_ifc_ver == 4)
+            res = do_rd_inc_wr_twice_v4(dev_name, lba, block, excl, wait_ms,
+                                        k, thr_ebusy_count, thr_eagain_count);
+        else {
+            lock_guard<mutex> lg(console_mutex);
+
+            cerr << "sg_ifc_ver=" << sg_ifc_ver << " not supported" << endl;
+            res = -1;
+        }
+        if (res < 0)
+            break;
+        if (res)
+            ++thr_odd_count;
+    }
+    {
+        lock_guard<mutex> lg(console_mutex);
+
+        if (k < num)
+            cerr << "thread id=" << id << " FAILed at iteration: " << k <<
+                    '\n';
+        else
+            cerr << "thread id=" << id << " normal exit" << '\n';
+    }
+    {
+        lock_guard<mutex> lg(odd_count_mutex);
+
+        odd_count += thr_odd_count;
+        ebusy_count += thr_ebusy_count;
+        eagain_count += thr_eagain_count;
+    }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    int k;
+    int block = 0;
+    int force = 0;
+    unsigned int lba = DEF_LBA;
+    int num_per_thread = DEF_NUM_PER_THREAD;
+    int num_threads = DEF_NUM_THREADS;
+    int wait_ms = DEF_WAIT_MS;
+    int no_o_excl = 0;
+    char * dev_name = NULL;
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-b", argv[k], 2))
+            ++block;
+        else if (0 == memcmp("-f", argv[k], 2))
+            ++force;
+        else if (0 == memcmp("-h", argv[k], 2)) {
+            usage();
+            return 0;
+        } else if (0 == memcmp("-i", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                sg_ifc_ver = atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-l", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                lba = (unsigned int)atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-n", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                num_per_thread = atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-t", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                num_threads = atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-V", argv[k], 2)) {
+            printf("%s version: %s\n", util_name, version_str);
+            return 0;
+        } else if (0 == memcmp("-w", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && (isdigit(*argv[k]) || ('-' == *argv[k]))) {
+                if ('-' == *argv[k])
+                    wait_ms = - atoi(argv[k] + 1);
+                else
+                    wait_ms = atoi(argv[k]);
+            } else
+                break;
+        } else if (0 == memcmp("-xxx", argv[k], 4))
+            no_o_excl += 3;
+        else if (0 == memcmp("-xx", argv[k], 3))
+            no_o_excl += 2;
+        else if (0 == memcmp("-x", argv[k], 2))
+            ++no_o_excl;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            dev_name = NULL;
+            break;
+        }
+        else if (! dev_name)
+            dev_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            dev_name = 0;
+            break;
+        }
+    }
+    if (0 == dev_name) {
+        usage();
+        return 1;
+    }
+    try {
+        struct stat a_stat;
+
+        if (stat(dev_name, &a_stat) < 0) {
+            perror("stat() on dev_name failed");
+            return 1;
+        }
+        if (! S_ISCHR(a_stat.st_mode)) {
+            fprintf(stderr, "%s should be a sg device which is a char "
+                    "device. %s\n", dev_name, dev_name);
+            fprintf(stderr, "is not a char device and damage could be done "
+                    "if it is a BLOCK\ndevice, exiting ...\n");
+            return 1;
+        }
+        if (! force) {
+            char b[64];
+            int res = do_inquiry_prod_id(dev_name, block, wait_ms,
+                                         ebusy_count, b, sizeof(b));
+
+            if (res) {
+                fprintf(stderr, "INQUIRY failed on %s\n", dev_name);
+                return 1;
+            }
+            // For safety, since <lba> written to, only permit scsi_debug
+            // devices. Bypass this with '-f' option.
+            if (0 != memcmp("scsi_debug", b, 10)) {
+                fprintf(stderr, "Since this utility writes to LBA %u, only "
+                        "devices with scsi_debug\nproduct ID accepted.\n",
+                        lba);
+                return 2;
+            }
+        }
+
+        vector<thread *> vt;
+
+        for (k = 0; k < num_threads; ++k) {
+            int excl = 1;
+
+            if (no_o_excl > 1)
+                excl = 0;
+            else if ((0 == k) && (1 == no_o_excl))
+                excl = 0;
+
+            thread * tp = new thread {work_thread, dev_name, lba, k, block,
+                                      excl, num_per_thread, wait_ms};
+            vt.push_back(tp);
+        }
+
+        // g++ 4.7.3 didn't like range-for loop here
+        for (k = 0; k < (int)vt.size(); ++k)
+            vt[k]->join();
+
+        for (k = 0; k < (int)vt.size(); ++k)
+            delete vt[k];
+
+        if (no_o_excl)
+            cout << "Odd count: " << odd_count << endl;
+        else
+            cout << "Expecting odd count of 0, got " << odd_count << endl;
+        cout << "Number of EBUSYs: " << ebusy_count << endl;
+        cout << "Number of EAGAINs: " << eagain_count << endl;
+
+    }
+    catch(system_error& e)  {
+        cerr << "got a system_error exception: " << e.what() << '\n';
+        auto ec = e.code();
+        cerr << "category: " << ec.category().name() << '\n';
+        cerr << "value: " << ec.value() << '\n';
+        cerr << "message: " << ec.message() << '\n';
+        cerr << "\nNote: if g++ may need '-pthread' or similar in "
+                "compile/link line" << '\n';
+    }
+    catch(...) {
+        cerr << "got another exception: " << '\n';
+    }
+    return 0;
+}
diff --git a/testing/sg_tst_excl2.cpp b/testing/sg_tst_excl2.cpp
new file mode 100644
index 0000000..d198d71
--- /dev/null
+++ b/testing/sg_tst_excl2.cpp
@@ -0,0 +1,556 @@
+/*
+ * Copyright (c) 2013-2022 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <iostream>
+#include <vector>
+#include <system_error>
+#include <thread>
+#include <mutex>
+#include <chrono>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+
+static const char * version_str = "1.11 20220425";
+static const char * util_name = "sg_tst_excl2";
+
+/* This is a test program for checking O_EXCL on open() works. It uses
+ * multiple threads and can be run as multiple processes and attempts
+ * to "break" O_EXCL. The strategy is to open a device O_EXCL|O_NONBLOCK
+ * and do a double increment on a LB then close it. Prior to the first
+ * increment, the value is checked for even or odd. Assuming the count
+ * starts as an even (typically 0) then it should remain even. Odd instances
+ * are counted and reported at the end of the program, after all threads
+ * have completed.
+ *
+ * This is C++ code with some things from C++11 (e.g. threads) and was
+ * only just able to compile (when some things were reverted) with gcc/g++
+ * version 4.7.3 found in Ubuntu 13.04 . C++11 "feature complete" support
+ * was not available until g++ version 4.8.1 and that is only currently
+ * found in Fedora 19 .
+ *
+ * The build uses various object files from the <sg3_utils>/lib directory
+ * which is assumed to be a sibling of this examples directory. Those
+ * object files in the lib directory can be built with:
+ *   cd <sg3_utils> ; ./configure ; cd lib; make
+ * Then:
+ *   cd ../testing
+ *   make sg_tst_excl2
+ *
+ * BEWARE: this utility modifies a logical block (default LBA 1000) on the
+ * given device.
+ *
+ */
+
+using namespace std;
+using namespace std::chrono;
+
+#define DEF_NUM_PER_THREAD 200
+#define DEF_NUM_THREADS 4
+#define DEF_WAIT_MS 0          /* 0: yield; -1: don't wait; -2: sleep(0) */
+
+#define DEF_LBA 1000
+
+#define EBUFF_SZ 256
+
+
+static mutex odd_count_mutex;
+static mutex console_mutex;
+static unsigned int odd_count;
+static unsigned int ebusy_count;
+
+
+static void
+usage(void)
+{
+    printf("Usage: %s [-b] [-f] [-h] [-l <lba>] [-n <n_per_thr>] "
+           "[-t <num_thrs>]\n"
+           "                    [-V] [-w <wait_ms>] [-x] "
+           "<disk_device>\n", util_name);
+    printf("  where\n");
+    printf("    -b                block on open (def: O_NONBLOCK)\n");
+    printf("    -f                force: any SCSI disk (def: only "
+           "scsi_debug)\n");
+    printf("                      WARNING: <lba> written to\n");
+    printf("    -h                print this usage message then exit\n");
+    printf("    -l <lba>          logical block to increment (def: %u)\n",
+           DEF_LBA);
+    printf("    -n <n_per_thr>    number of loops per thread "
+           "(def: %d)\n", DEF_NUM_PER_THREAD);
+    printf("    -t <num_thrs>     number of threads (def: %d)\n",
+           DEF_NUM_THREADS);
+    printf("    -V                print version number then exit\n");
+    printf("    -w <wait_ms>      >0: sleep_for(<wait_ms>); =0: "
+           "yield(); -1: no\n"
+           "                      wait; -2: sleep(0)  (def: %d)\n",
+           DEF_WAIT_MS);
+    printf("    -x                don't use O_EXCL on first thread "
+           "(def: use\n"
+           "                      O_EXCL on all threads)\n\n");
+    printf("Test O_EXCL open flag with pass-through drivers. Each "
+           "open/close cycle with\nthe O_EXCL flag does a double increment "
+           "on lba (using its first 4 bytes).\n");
+}
+
+/* Assumed a lock (mutex) held when pt_err() is called */
+static int
+pt_err(int res)
+{
+    if (res < 0)
+        fprintf(stderr, "  pass through os error: %s\n", safe_strerror(-res));
+    else if (SCSI_PT_DO_BAD_PARAMS == res)
+        fprintf(stderr, "  bad pass through setup\n");
+    else if (SCSI_PT_DO_TIMEOUT == res)
+        fprintf(stderr, "  pass through timeout\n");
+    else
+        fprintf(stderr, "  do_scsi_pt error=%d\n", res);
+    return -1;
+}
+
+/* Assumed a lock (mutex) held when pt_cat_no_good() is called */
+static int
+pt_cat_no_good(int cat, struct sg_pt_base * ptp, const unsigned char * sbp)
+{
+    int slen;
+    char b[256];
+    const int bl = (int)sizeof(b);
+
+    switch (cat) {
+    case SCSI_PT_RESULT_STATUS: /* other than GOOD and CHECK CONDITION */
+        sg_get_scsi_status_str(get_scsi_pt_status_response(ptp), bl, b);
+        fprintf(stderr, "  scsi status: %s\n", b);
+        break;
+    case SCSI_PT_RESULT_SENSE:
+        slen = get_scsi_pt_sense_len(ptp);
+        sg_get_sense_str("", sbp, slen, 1, bl, b);
+        fprintf(stderr, "%s", b);
+        break;
+    case SCSI_PT_RESULT_TRANSPORT_ERR:
+        get_scsi_pt_transport_err_str(ptp, bl, b);
+        fprintf(stderr, "  transport: %s", b);
+        break;
+    case SCSI_PT_RESULT_OS_ERR:
+        get_scsi_pt_os_err_str(ptp, bl, b);
+        fprintf(stderr, "  os: %s", b);
+        break;
+    default:
+        fprintf(stderr, "  unknown pt result category (%d)\n", cat);
+        break;
+    }
+    return -1;
+}
+
+#define READ16_REPLY_LEN 512
+#define READ16_CMD_LEN 16
+#define WRITE16_REPLY_LEN 512
+#define WRITE16_CMD_LEN 16
+
+/* Opens dev_name and spins if busy (i.e. gets EBUSY), sleeping for
+ * wait_ms milliseconds if wait_ms is positive. Reads lba and treats the
+ * first 4 bytes as an int (SCSI endian), increments it and writes it back.
+ * Repeats so that happens twice. Then closes dev_name. If an error occurs
+ * returns -1 else returns 0 if first int read is even otherwise returns 1. */
+static int
+do_rd_inc_wr_twice(const char * dev_name, unsigned int lba, int block,
+                   int excl, int wait_ms, unsigned int & ebusys)
+{
+    int k, sg_fd, res, cat;
+    int odd = 0;
+    unsigned int u = 0;
+    struct sg_pt_base * ptp = NULL;
+    unsigned char r16CmdBlk [READ16_CMD_LEN] =
+                {0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    unsigned char w16CmdBlk [WRITE16_CMD_LEN] =
+                {0x8a, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    unsigned char lb[READ16_REPLY_LEN];
+    char ebuff[EBUFF_SZ];
+    int open_flags = O_RDWR;
+
+    sg_put_unaligned_be64(lba, r16CmdBlk + 2);
+    sg_put_unaligned_be64(lba, w16CmdBlk + 2);
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    if (excl)
+        open_flags |= O_EXCL;
+
+    while (((sg_fd = scsi_pt_open_flags(dev_name, open_flags, 0)) < 0) &&
+           (-EBUSY == sg_fd)) {
+        ++ebusys;
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();       // thread yield
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (sg_fd < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "do_rd_inc_wr_twice: error opening file: %s", dev_name);
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            perror(ebuff);
+        }
+        return -1;
+    }
+
+    ptp = construct_scsi_pt_obj();
+    for (k = 0; k < 2; ++k) {
+        /* Prepare READ_16 command */
+        clear_scsi_pt_obj(ptp);
+        set_scsi_pt_cdb(ptp, r16CmdBlk, sizeof(r16CmdBlk));
+        set_scsi_pt_sense(ptp, sense_buffer, sizeof(sense_buffer));
+        set_scsi_pt_data_in(ptp, lb, READ16_REPLY_LEN);
+        res = do_scsi_pt(ptp, sg_fd, 20 /* secs timeout */, 1);
+        if (res) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "READ_16 do_scsi_pt() submission error\n");
+                res = pt_err(res);
+            }
+            goto err;
+        }
+        cat = get_scsi_pt_result_category(ptp);
+        if (SCSI_PT_RESULT_GOOD != cat) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "READ_16 do_scsi_pt() category problem\n");
+                res = pt_cat_no_good(cat, ptp, sense_buffer);
+            }
+            goto err;
+        }
+
+        u = sg_get_unaligned_be32(lb);
+        // Assuming u starts test as even (probably 0), expect it to stay even
+        if (0 == k)
+            odd = (1 == (u % 2));
+        ++u;
+        sg_put_unaligned_be32(u, lb);
+
+        if (wait_ms > 0)       /* allow daylight for bad things ... */
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();       // thread yield
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+
+        /* Prepare WRITE_16 command */
+        clear_scsi_pt_obj(ptp);
+        set_scsi_pt_cdb(ptp, w16CmdBlk, sizeof(w16CmdBlk));
+        set_scsi_pt_sense(ptp, sense_buffer, sizeof(sense_buffer));
+        set_scsi_pt_data_out(ptp, lb, WRITE16_REPLY_LEN);
+        res = do_scsi_pt(ptp, sg_fd, 20 /* secs timeout */, 1);
+        if (res) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "WRITE_16 do_scsi_pt() submission error\n");
+                res = pt_err(res);
+            }
+            goto err;
+        }
+        cat = get_scsi_pt_result_category(ptp);
+        if (SCSI_PT_RESULT_GOOD != cat) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "WRITE_16 do_scsi_pt() category problem\n");
+                res = pt_cat_no_good(cat, ptp, sense_buffer);
+            }
+            goto err;
+        }
+    }
+err:
+    if (ptp)
+        destruct_scsi_pt_obj(ptp);
+    scsi_pt_close_device(sg_fd);
+    return odd;
+}
+
+
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+
+/* Send INQUIRY and fetches response. If okay puts PRODUCT ID field
+ * in b (up to m_blen bytes). Does not use O_EXCL flag. Returns 0 on success,
+ * else -1 . */
+static int
+do_inquiry_prod_id(const char * dev_name, int block, int wait_ms,
+                   unsigned int & ebusys, char * b, int b_mlen)
+{
+    int sg_fd, res, cat;
+    struct sg_pt_base * ptp = NULL;
+    unsigned char inqCmdBlk [INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    unsigned char inqBuff[INQ_REPLY_LEN];
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    char ebuff[EBUFF_SZ];
+    int open_flags = O_RDWR;    /* since O_EXCL | O_RDONLY gives EPERM */
+
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    while (((sg_fd = scsi_pt_open_flags(dev_name, open_flags, 0)) < 0) &&
+           (-EBUSY == sg_fd)) {
+        ++ebusys;
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();       // thread yield
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (sg_fd < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "do_inquiry_prod_id: error opening file: %s", dev_name);
+        perror(ebuff);
+        return -1;
+    }
+    /* Prepare INQUIRY command */
+    ptp = construct_scsi_pt_obj();
+    clear_scsi_pt_obj(ptp);
+    set_scsi_pt_cdb(ptp, inqCmdBlk, sizeof(inqCmdBlk));
+    set_scsi_pt_sense(ptp, sense_buffer, sizeof(sense_buffer));
+    set_scsi_pt_data_in(ptp, inqBuff, INQ_REPLY_LEN);
+    res = do_scsi_pt(ptp, sg_fd, 20 /* secs timeout */, 1);
+    if (res) {
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            fprintf(stderr, "INQUIRY do_scsi_pt() submission error\n");
+            res = pt_err(res);
+        }
+        goto err;
+    }
+    cat = get_scsi_pt_result_category(ptp);
+    if (SCSI_PT_RESULT_GOOD != cat) {
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            fprintf(stderr, "INQUIRY do_scsi_pt() category problem\n");
+            res = pt_cat_no_good(cat, ptp, sense_buffer);
+        }
+        goto err;
+    }
+
+    /* Good, so fetch Product ID from response, copy to 'b' */
+    if (b_mlen > 0) {
+        if (b_mlen > 16) {
+            memcpy(b, inqBuff + 16, 16);
+            b[16] = '\0';
+        } else {
+            memcpy(b, inqBuff + 16, b_mlen - 1);
+            b[b_mlen - 1] = '\0';
+        }
+    }
+err:
+    if (ptp)
+        destruct_scsi_pt_obj(ptp);
+    close(sg_fd);
+    return 0;
+}
+
+static void
+work_thread(const char * dev_name, unsigned int lba, int id, int block,
+            int excl, int num, int wait_ms)
+{
+    unsigned int thr_odd_count = 0;
+    unsigned int thr_ebusy_count = 0;
+    int k, res;
+
+    {
+        lock_guard<mutex> lg(console_mutex);
+
+        cerr << "Enter work_thread id=" << id << " excl=" << excl << " block="
+             << block << endl;
+    }
+    for (k = 0; k < num; ++k) {
+        res = do_rd_inc_wr_twice(dev_name, lba, block, excl, wait_ms,
+                                 thr_ebusy_count);
+        if (res < 0)
+            break;
+        if (res)
+            ++thr_odd_count;
+    }
+    {
+        lock_guard<mutex> lg(console_mutex);
+
+        if (k < num)
+            cerr << "thread id=" << id << " FAILed at iteration: " << k <<
+                    '\n';
+        else
+            cerr << "thread id=" << id << " normal exit" << '\n';
+    }
+
+    {
+        lock_guard<mutex> lg(odd_count_mutex);
+
+        odd_count += thr_odd_count;
+        ebusy_count += thr_ebusy_count;
+    }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    int k, res;
+    int block = 0;
+    int force = 0;
+    unsigned int lba = DEF_LBA;
+    int num_per_thread = DEF_NUM_PER_THREAD;
+    int num_threads = DEF_NUM_THREADS;
+    int wait_ms = DEF_WAIT_MS;
+    int exclude_o_excl = 0;
+    char * dev_name = NULL;
+    char b[64];
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-b", argv[k], 2))
+            ++block;
+        else if (0 == memcmp("-f", argv[k], 2))
+            ++force;
+        else if (0 == memcmp("-h", argv[k], 2)) {
+            usage();
+            return 0;
+        } else if (0 == memcmp("-l", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                lba = (unsigned int)atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-n", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                num_per_thread = atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-t", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                num_threads = atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-V", argv[k], 2)) {
+            printf("%s version: %s\n", util_name, version_str);
+            return 0;
+        } else if (0 == memcmp("-w", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && (isdigit(*argv[k]) || ('-' == *argv[k]))) {
+                if ('-' == *argv[k])
+                    wait_ms = - atoi(argv[k] + 1);
+                else
+                    wait_ms = atoi(argv[k]);
+            } else
+                break;
+        } else if (0 == memcmp("-x", argv[k], 2))
+            ++exclude_o_excl;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            dev_name = NULL;
+            break;
+        }
+        else if (! dev_name)
+            dev_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            dev_name = 0;
+            break;
+        }
+    }
+    if (0 == dev_name) {
+        usage();
+        return 1;
+    }
+    try {
+
+        if (! force) {
+            res = do_inquiry_prod_id(dev_name, block, wait_ms, ebusy_count,
+                                     b, sizeof(b));
+            if (res) {
+                fprintf(stderr, "INQUIRY failed on %s\n", dev_name);
+                return 1;
+            }
+            // For safety, since <lba> written to, only permit scsi_debug
+            // devices. Bypass this with '-f' option.
+            if (0 != memcmp("scsi_debug", b, 10)) {
+                fprintf(stderr, "Since this utility writes to LBA %d, only "
+                        "devices with scsi_debug\nproduct ID accepted.\n",
+                        lba);
+                return 2;
+            }
+        }
+
+        vector<thread *> vt;
+
+        for (k = 0; k < num_threads; ++k) {
+            int excl = ((0 == k) && exclude_o_excl) ? 0 : 1;
+
+            thread * tp = new thread {work_thread, dev_name, lba, k, block,
+                                      excl, num_per_thread, wait_ms};
+            vt.push_back(tp);
+        }
+
+        for (k = 0; k < (int)vt.size(); ++k)
+            vt[k]->join();
+
+        for (k = 0; k < (int)vt.size(); ++k)
+            delete vt[k];
+
+        cout << "Expecting odd count of 0, got " << odd_count << endl;
+        cout << "Number of EBUSYs: " << ebusy_count << endl;
+
+    }
+    catch(system_error& e)  {
+        cerr << "got a system_error exception: " << e.what() << '\n';
+        auto ec = e.code();
+        cerr << "category: " << ec.category().name() << '\n';
+        cerr << "value: " << ec.value() << '\n';
+        cerr << "message: " << ec.message() << '\n';
+        cerr << "\nNote: if g++ may need '-pthread' or similar in "
+                "compile/link line" << '\n';
+    }
+    catch(...) {
+        cerr << "got another exception: " << '\n';
+    }
+    return 0;
+}
diff --git a/testing/sg_tst_excl3.cpp b/testing/sg_tst_excl3.cpp
new file mode 100644
index 0000000..b1cbf13
--- /dev/null
+++ b/testing/sg_tst_excl3.cpp
@@ -0,0 +1,561 @@
+/*
+ * Copyright (c) 2013-2022 Douglas Gilbert.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <iostream>
+#include <vector>
+#include <system_error>
+#include <thread>
+#include <mutex>
+#include <chrono>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_unaligned.h"
+
+static const char * version_str = "1.11 20220425";
+static const char * util_name = "sg_tst_excl3";
+
+/* This is a test program for checking O_EXCL on open() works. It uses
+ * multiple threads and can be run as multiple processes and attempts
+ * to "break" O_EXCL. The strategy is to open a device O_EXCL|O_NONBLOCK
+ * and do a double increment on a LB then close it from a single thread.
+ * the remaining threads open that device O_NONBLOCK and do a read and
+ * note if the number is odd. Assuming the count starts as an even
+ * (typically 0) then it should remain even. Odd instances
+ * are counted and reported at the end of the program, after all threads
+ * have completed.
+ *
+ * This is C++ code with some things from C++11 (e.g. threads) and was
+ * only just able to compile (when some things were reverted) with gcc/g++
+ * version 4.7.3 found in Ubuntu 13.04 . C++11 "feature complete" support
+ * was not available until g++ version 4.8.1 and that is found in Fedora
+ * 19 and Ubuntu 13.10 .
+ *
+ * The build uses various object files from the <sg3_utils>/lib directory
+ * which is assumed to be a sibling of this examples directory. Those
+ * object files in the lib directory can be built with:
+ *   cd <sg3_utils> ; ./configure ; cd lib; make
+ * Then:
+ *   cd ../testing
+ *   make sg_tst_excl3
+ *
+ * BEWARE: this utility modifies a logical block (default LBA 1000) on the
+ * given device.
+ *
+ */
+
+using namespace std;
+using namespace std::chrono;
+
+#define DEF_NUM_PER_THREAD 200
+#define DEF_NUM_THREADS 4
+#define DEF_WAIT_MS 0          /* 0: yield; -1: don't wait; -2: sleep(0) */
+
+#define DEF_LBA 1000
+
+#define EBUFF_SZ 256
+
+
+static mutex odd_count_mutex;
+static mutex console_mutex;
+static unsigned int odd_count;
+static unsigned int ebusy_count;
+
+
+static void
+usage(void)
+{
+    printf("Usage: %s [-b] [-f] [-h] [-l <lba>] [-n <n_per_thr>]\n"
+           "                    [-R] [-t <num_thrs>] [-V] [-w <wait_ms>] "
+           "[-x]\n"
+           "                    <disk_device>\n", util_name);
+    printf("  where\n");
+    printf("    -b                block on open (def: O_NONBLOCK)\n");
+    printf("    -f                force: any SCSI disk (def: only "
+           "scsi_debug)\n");
+    printf("                      WARNING: <lba> written to\n");
+    printf("    -h                print this usage message then exit\n");
+    printf("    -l <lba>          logical block to increment (def: %u)\n",
+           DEF_LBA);
+    printf("    -n <n_per_thr>    number of loops per thread "
+           "(def: %d)\n", DEF_NUM_PER_THREAD);
+    printf("    -R                all readers; so first thread (id=0) "
+           "just reads\n");
+    printf("    -t <num_thrs>     number of threads (def: %d)\n",
+           DEF_NUM_THREADS);
+    printf("    -V                print version number then exit\n");
+    printf("    -w <wait_ms>      >0: sleep_for(<wait_ms>); =0: "
+           "yield(); -1: no\n"
+           "                      wait; -2: sleep(0)  (def: %d)\n",
+           DEF_WAIT_MS);
+    printf("    -x                don't use O_EXCL on first thread "
+           "(def: use\n"
+           "                      O_EXCL on first thread)\n\n");
+    printf("Test O_EXCL open flag with pass-through drivers. First thread "
+           "(id=0) does\nopen/close cycle with the O_EXCL flag then does a "
+           "double increment on\nlba (using its first 4 bytes). Remaining "
+           "theads read (without\nO_EXCL flag on open) and check the "
+           "value is even.\n");
+}
+
+/* Assumed a lock (mutex) held when pt_err() is called */
+static int
+pt_err(int res)
+{
+    if (res < 0)
+        fprintf(stderr, "  pass through os error: %s\n", safe_strerror(-res));
+    else if (SCSI_PT_DO_BAD_PARAMS == res)
+        fprintf(stderr, "  bad pass through setup\n");
+    else if (SCSI_PT_DO_TIMEOUT == res)
+        fprintf(stderr, "  pass through timeout\n");
+    else
+        fprintf(stderr, "  do_scsi_pt error=%d\n", res);
+    return -1;
+}
+
+/* Assumed a lock (mutex) held when pt_cat_no_good() is called */
+static int
+pt_cat_no_good(int cat, struct sg_pt_base * ptp, const unsigned char * sbp)
+{
+    int slen;
+    char b[256];
+    const int bl = (int)sizeof(b);
+
+    switch (cat) {
+    case SCSI_PT_RESULT_STATUS: /* other than GOOD and CHECK CONDITION */
+        sg_get_scsi_status_str(get_scsi_pt_status_response(ptp), bl, b);
+        fprintf(stderr, "  scsi status: %s\n", b);
+        break;
+    case SCSI_PT_RESULT_SENSE:
+        slen = get_scsi_pt_sense_len(ptp);
+        sg_get_sense_str("", sbp, slen, 1, bl, b);
+        fprintf(stderr, "%s", b);
+        break;
+    case SCSI_PT_RESULT_TRANSPORT_ERR:
+        get_scsi_pt_transport_err_str(ptp, bl, b);
+        fprintf(stderr, "  transport: %s", b);
+        break;
+    case SCSI_PT_RESULT_OS_ERR:
+        get_scsi_pt_os_err_str(ptp, bl, b);
+        fprintf(stderr, "  os: %s", b);
+        break;
+    default:
+        fprintf(stderr, "  unknown pt result category (%d)\n", cat);
+        break;
+    }
+    return -1;
+}
+
+#define READ16_REPLY_LEN 512
+#define READ16_CMD_LEN 16
+#define WRITE16_REPLY_LEN 512
+#define WRITE16_CMD_LEN 16
+
+/* Opens dev_name and spins if busy (i.e. gets EBUSY), sleeping for
+ * wait_ms milliseconds if wait_ms is positive. Reads lba and treats the
+ * first 4 bytes as an int (SCSI endian), increments it and writes it back.
+ * Repeats so that happens twice. Then closes dev_name. If an error occurs
+ * returns -1 else returns 0 if first int read is even otherwise returns 1. */
+static int
+do_rd_inc_wr_twice(const char * dev_name, int read_only, unsigned int lba,
+                   int block, int excl, int wait_ms, unsigned int & ebusys)
+{
+    int k, sg_fd, res, cat;
+    int odd = 0;
+    unsigned int u = 0;
+    struct sg_pt_base * ptp = NULL;
+    unsigned char r16CmdBlk [READ16_CMD_LEN] =
+                {0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    unsigned char w16CmdBlk [WRITE16_CMD_LEN] =
+                {0x8a, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0};
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    unsigned char lb[READ16_REPLY_LEN];
+    char ebuff[EBUFF_SZ];
+    int open_flags = O_RDWR;
+
+    sg_put_unaligned_be64(lba, r16CmdBlk + 2);
+    sg_put_unaligned_be64(lba, w16CmdBlk + 2);
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    if (excl)
+        open_flags |= O_EXCL;
+
+    while (((sg_fd = scsi_pt_open_flags(dev_name, open_flags, 0)) < 0) &&
+           (-EBUSY == sg_fd)) {
+        ++ebusys;
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();       // thread yield
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (sg_fd < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "do_rd_inc_wr_twice: error opening file: %s", dev_name);
+        {
+            lock_guard<mutex> lg(console_mutex);
+
+            perror(ebuff);
+        }
+        return -1;
+    }
+
+    ptp = construct_scsi_pt_obj();
+    for (k = 0; k < 2; ++k) {
+        /* Prepare READ_16 command */
+        clear_scsi_pt_obj(ptp);
+        set_scsi_pt_cdb(ptp, r16CmdBlk, sizeof(r16CmdBlk));
+        set_scsi_pt_sense(ptp, sense_buffer, sizeof(sense_buffer));
+        set_scsi_pt_data_in(ptp, lb, READ16_REPLY_LEN);
+        res = do_scsi_pt(ptp, sg_fd, 20 /* secs timeout */, 1);
+        if (res) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "READ_16 do_scsi_pt() submission error\n");
+                res = pt_err(res);
+            }
+            goto err;
+        }
+        cat = get_scsi_pt_result_category(ptp);
+        if (SCSI_PT_RESULT_GOOD != cat) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "READ_16 do_scsi_pt() category problem\n");
+                res = pt_cat_no_good(cat, ptp, sense_buffer);
+            }
+            goto err;
+        }
+
+        u = sg_get_unaligned_be32(lb);
+        // Assuming u starts test as even (probably 0), expect it to stay even
+        if (0 == k)
+            odd = (1 == (u % 2));
+
+        if (wait_ms > 0)       /* allow daylight for bad things ... */
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();       // thread yield
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+
+        if (read_only)
+            break;
+        ++u;
+        sg_put_unaligned_be32(u, lb);
+
+        /* Prepare WRITE_16 command */
+        clear_scsi_pt_obj(ptp);
+        set_scsi_pt_cdb(ptp, w16CmdBlk, sizeof(w16CmdBlk));
+        set_scsi_pt_sense(ptp, sense_buffer, sizeof(sense_buffer));
+        set_scsi_pt_data_out(ptp, lb, WRITE16_REPLY_LEN);
+        res = do_scsi_pt(ptp, sg_fd, 20 /* secs timeout */, 1);
+        if (res) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "WRITE_16 do_scsi_pt() submission error\n");
+                res = pt_err(res);
+            }
+            goto err;
+        }
+        cat = get_scsi_pt_result_category(ptp);
+        if (SCSI_PT_RESULT_GOOD != cat) {
+            {
+                lock_guard<mutex> lg(console_mutex);
+
+                fprintf(stderr, "WRITE_16 do_scsi_pt() category problem\n");
+                res = pt_cat_no_good(cat, ptp, sense_buffer);
+            }
+            goto err;
+        }
+    }
+err:
+    if (ptp)
+        destruct_scsi_pt_obj(ptp);
+    scsi_pt_close_device(sg_fd);
+    return odd;
+}
+
+
+#define INQ_REPLY_LEN 96
+#define INQ_CMD_LEN 6
+
+/* Send INQUIRY and fetches response. If okay puts PRODUCT ID field
+ * in b (up to m_blen bytes). Does not use O_EXCL flag. Returns 0 on success,
+ * else -1 . */
+static int
+do_inquiry_prod_id(const char * dev_name, int block, int wait_ms,
+                   unsigned int & ebusys, char * b, int b_mlen)
+{
+    int sg_fd, res, cat;
+    struct sg_pt_base * ptp = NULL;
+    unsigned char inqCmdBlk [INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    unsigned char inqBuff[INQ_REPLY_LEN];
+    unsigned char sense_buffer[64] SG_C_CPP_ZERO_INIT;
+    char ebuff[EBUFF_SZ];
+    int open_flags = O_RDWR;    /* since O_EXCL | O_RDONLY gives EPERM */
+
+    if (! block)
+        open_flags |= O_NONBLOCK;
+    while (((sg_fd = scsi_pt_open_flags(dev_name, open_flags, 0)) < 0) &&
+           (-EBUSY == sg_fd)) {
+        ++ebusys;
+        if (wait_ms > 0)
+            this_thread::sleep_for(milliseconds{wait_ms});
+        else if (0 == wait_ms)
+            this_thread::yield();       // thread yield
+        else if (-2 == wait_ms)
+            sleep(0);                   // process yield ??
+    }
+    if (sg_fd < 0) {
+        snprintf(ebuff, EBUFF_SZ,
+                 "do_inquiry_prod_id: error opening file: %s", dev_name);
+        perror(ebuff);
+        return -1;
+    }
+    /* Prepare INQUIRY command */
+    ptp = construct_scsi_pt_obj();
+    clear_scsi_pt_obj(ptp);
+    set_scsi_pt_cdb(ptp, inqCmdBlk, sizeof(inqCmdBlk));
+    set_scsi_pt_sense(ptp, sense_buffer, sizeof(sense_buffer));
+    set_scsi_pt_data_in(ptp, inqBuff, INQ_REPLY_LEN);
+    res = do_scsi_pt(ptp, sg_fd, 20 /* secs timeout */, 1);
+    if (res) {
+        fprintf(stderr, "INQUIRY do_scsi_pt() submission error\n");
+        res = pt_err(res);
+        goto err;
+    }
+    cat = get_scsi_pt_result_category(ptp);
+    if (SCSI_PT_RESULT_GOOD != cat) {
+        fprintf(stderr, "INQUIRY do_scsi_pt() category problem\n");
+        res = pt_cat_no_good(cat, ptp, sense_buffer);
+        goto err;
+    }
+
+    /* Good, so fetch Product ID from response, copy to 'b' */
+    if (b_mlen > 0) {
+        if (b_mlen > 16) {
+            memcpy(b, inqBuff + 16, 16);
+            b[16] = '\0';
+        } else {
+            memcpy(b, inqBuff + 16, b_mlen - 1);
+            b[b_mlen - 1] = '\0';
+        }
+    }
+err:
+    if (ptp)
+        destruct_scsi_pt_obj(ptp);
+    close(sg_fd);
+    return res;
+}
+
+static void
+work_thread(const char * dev_name, unsigned int lba, int id, int block,
+            int excl, bool all_readers, int num, int wait_ms)
+{
+    unsigned int thr_odd_count = 0;
+    unsigned int thr_ebusy_count = 0;
+    int k, res;
+    int reader = ((id > 0) || (all_readers));
+
+    {
+        lock_guard<mutex> lg(console_mutex);
+
+        cerr << "Enter work_thread id=" << id << " excl=" << excl << " block="
+             << block << " reader=" << reader << endl;
+    }
+    for (k = 0; k < num; ++k) {
+        res = do_rd_inc_wr_twice(dev_name, reader, lba, block, excl,
+                                 wait_ms, thr_ebusy_count);
+        if (res < 0)
+            break;
+        if (res)
+            ++thr_odd_count;
+    }
+    {
+        lock_guard<mutex> lg(console_mutex);
+
+        if (k < num)
+            cerr << "thread id=" << id << " FAILed at iteration: " << k
+                 << '\n';
+        else
+            cerr << "thread id=" << id << " normal exit" << '\n';
+    }
+
+    {
+        lock_guard<mutex> lg(odd_count_mutex);
+
+        odd_count += thr_odd_count;
+        ebusy_count += thr_ebusy_count;
+    }
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    int k, res;
+    int block = 0;
+    int force = 0;
+    unsigned int lba = DEF_LBA;
+    int num_per_thread = DEF_NUM_PER_THREAD;
+    bool all_readers = false;
+    int num_threads = DEF_NUM_THREADS;
+    int wait_ms = DEF_WAIT_MS;
+    int exclude_o_excl = 0;
+    char * dev_name = NULL;
+    char b[64];
+
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-b", argv[k], 2))
+            ++block;
+        else if (0 == memcmp("-f", argv[k], 2))
+            ++force;
+        else if (0 == memcmp("-h", argv[k], 2)) {
+            usage();
+            return 0;
+        } else if (0 == memcmp("-l", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                lba = (unsigned int)atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-n", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                num_per_thread = atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-t", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && isdigit(*argv[k]))
+                num_threads = atoi(argv[k]);
+            else
+                break;
+        } else if (0 == memcmp("-R", argv[k], 2))
+            all_readers = true;
+        else if (0 == memcmp("-V", argv[k], 2)) {
+            printf("%s version: %s\n", util_name, version_str);
+            return 0;
+        } else if (0 == memcmp("-w", argv[k], 2)) {
+            ++k;
+            if ((k < argc) && (isdigit(*argv[k]) || ('-' == *argv[k]))) {
+                if ('-' == *argv[k])
+                    wait_ms = - atoi(argv[k] + 1);
+                else
+                    wait_ms = atoi(argv[k]);
+            } else
+                break;
+        } else if (0 == memcmp("-x", argv[k], 2))
+            ++exclude_o_excl;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            dev_name = NULL;
+            break;
+        }
+        else if (! dev_name)
+            dev_name = argv[k];
+        else {
+            printf("too many arguments\n");
+            dev_name = 0;
+            break;
+        }
+    }
+    if (0 == dev_name) {
+        usage();
+        return 1;
+    }
+    try {
+
+        if (! force) {
+            res = do_inquiry_prod_id(dev_name, block, wait_ms, ebusy_count,
+                                     b, sizeof(b));
+            if (res) {
+                fprintf(stderr, "INQUIRY failed on %s\n", dev_name);
+                return 1;
+            }
+            // For safety, since <lba> written to, only permit scsi_debug
+            // devices. Bypass this with '-f' option.
+            if (0 != memcmp("scsi_debug", b, 10)) {
+                fprintf(stderr, "Since this utility writes to LBA %d, only "
+                        "devices with scsi_debug\nproduct ID accepted.\n",
+                        lba);
+                return 2;
+            }
+        }
+
+        vector<thread *> vt;
+
+        for (k = 0; k < num_threads; ++k) {
+            int excl = ((0 == k) && (! exclude_o_excl)) ? 1 : 0;
+
+            thread * tp = new thread {work_thread, dev_name, lba, k, block,
+                                      excl, all_readers, num_per_thread,
+                                      wait_ms};
+            vt.push_back(tp);
+        }
+
+        for (k = 0; k < (int)vt.size(); ++k)
+            vt[k]->join();
+
+        for (k = 0; k < (int)vt.size(); ++k)
+            delete vt[k];
+
+        cout << "Expecting odd count of 0, got " << odd_count << endl;
+        cout << "Number of EBUSYs: " << ebusy_count << endl;
+
+    }
+    catch(system_error& e)  {
+        cerr << "got a system_error exception: " << e.what() << '\n';
+        auto ec = e.code();
+        cerr << "category: " << ec.category().name() << '\n';
+        cerr << "value: " << ec.value() << '\n';
+        cerr << "message: " << ec.message() << '\n';
+        cerr << "\nNote: if g++ may need '-pthread' or similar in "
+                "compile/link line" << '\n';
+    }
+    catch(...) {
+        cerr << "got another exception: " << '\n';
+    }
+    return 0;
+}
diff --git a/testing/sg_tst_ioctl.c b/testing/sg_tst_ioctl.c
new file mode 100644
index 0000000..ade56a1
--- /dev/null
+++ b/testing/sg_tst_ioctl.c
@@ -0,0 +1,1351 @@
+/*
+ *  Copyright (C) 2018-2022 D. Gilbert
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Invocation: See usage() function below.
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <string.h>
+#include <errno.h>
+#include <time.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/utsname.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#include <sys/socket.h> /* For passing fd_s via Unix sockets */
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_io_linux.h"
+#include "sg_linux_inc.h"
+#include "sg_pr2serr.h"
+
+/* This program tests ioctl() calls added and modified in version 4.0 and
+ * later of the Linux sg driver.  */
+
+
+static const char * version_str = "Version: 1.21  20220202";
+
+#define INQ_REPLY_LEN 128
+#define INQ_CMD_LEN 6
+#define SDIAG_CMD_LEN 6
+#define SENSE_BUFFER_LEN 96
+
+#define EBUFF_SZ 512
+
+#ifndef SG_FLAG_Q_AT_TAIL
+#define SG_FLAG_Q_AT_TAIL 0x10
+#endif
+
+#ifndef SG_FLAG_Q_AT_HEAD
+#define SG_FLAG_Q_AT_HEAD 0x20
+#endif
+
+#define DEF_Q_LEN 16    /* max in sg v3 and earlier */
+#define MAX_Q_LEN 512
+
+#define DEF_RESERVE_BUFF_SZ (256 * 1024)
+
+static bool create_time = false;
+static bool is_parent = false;
+static bool do_fork = false;
+static bool ioctl_only = false;
+static bool more_async = false;
+static bool no_duration = false;
+static bool q_at_tail = false;
+static bool write_only = false;
+static bool mrq_immed = false;  /* if set, also sets mrq_iosubmit */
+static bool mrq_half_immed = false;
+static bool mrq_iosubmit = false;
+static bool show_size_value = false;
+static bool do_v3_only = false;
+
+static int childs_pid = 0;
+static int iterator_test = -1;
+static int object_walk_test = -1;
+static int sg_drv_ver_num = 0;
+static int q_len = DEF_Q_LEN;
+static int sleep_secs = 0;
+static int reserve_buff_sz = DEF_RESERVE_BUFF_SZ;
+static int num_mrqs = 0;
+static int num_sgnw = 0;
+static int dname_current = 0;
+static int dname_last = 0;
+static int dname_pos = 0;
+static int verbose = 0;
+
+static const char * relative_cp = "";
+static char * file_name = NULL;
+
+
+static void
+usage(void)
+{
+    printf("Usage: sg_tst_ioctl [-3] [-c] [-f] [-h] [-I=0|1] [-J=0|1] "
+           "[-l=Q_LEN]\n"
+           "                    [-m=MRQS[,I|S]] [-M] [-n] [-o] [-r=SZ] "
+           "[-s=SEC]\n"
+           "                    [-S] [-t] [-T=NUM] [-v] [-V] [-w]\n"
+           "                    <sg_device>[-<num>] [<sg_device2>]\n"
+           " where:\n"
+           "      -3      use sg v3 interface (def: sg v4 if available)\n"
+           "      -c      timestamp when sg driver created <sg_device>\n"
+           "      -f      fork and test share between processes\n"
+           "      -h      help: print usage message then exit\n"
+           "      -I=0|1    iterator test of mid-level; 0: unlocked, 1: "
+           "locked\n"
+           "                does test -T=NUM times, outputs duration\n"
+           "      -J=0|1    object walk up then 2 lookups; 0: no logging; "
+           "1: log\n"
+           "                up-scan once per 1000 iterations\n"
+           "      -l=Q_LEN    queue length, between 1 and 511 (def: 16)\n"
+           "      -m=MRQS[,I|S]    test multi-req, MRQS number to do; if "
+           "the letter\n"
+           "                     'I' is appended after a comma, then do "
+           "IMMED mrq;\n"
+           "                     'i' IMMED on submission, non-IMMED on "
+           "receive;\n"
+           "                     'S' is appended, then use "
+           "ioctl(SG_IOSUBMIT)\n"
+           "      -M      set 'more async' flag\n"
+           "      -n      do not calculate per command duration (def: do)\n"
+           "      -o      ioctls only, then exit\n"
+           "      -r=SZ     reserve buffer size in KB (def: 256 --> 256 "
+           "KB)\n"
+           "      -s=SEC    sleep between writes and reads (def: 0)\n"
+           "      -S        size of interface structures plus ioctl "
+           "values\n"
+           "      -t    queue_at_tail (def: q_at_head)\n"
+           "      -T=NUM    time overhead of NUM invocations of\n"
+           "                ioctl(SG_GET_NUM_WAITING); then exit\n"
+           "      -v    increase verbosity of output\n"
+           "      -V    print version string then exit\n"
+           "      -w    write (submit) only then exit\n\n");
+    printf("There are various groups of options for different tests. The "
+           "get_num_waiting\ngroup needs '-T=NUM' given. When '-I=0|1' is "
+           "also given then an object tree\niterator test is done NUM "
+           "times. If instead '-J=0|1' is given then an\nobject tree "
+           "traversal (up/down) is done 10,000 times (and NUM is\n"
+           "ignored).\n"
+          );
+}
+
+static void
+timespec_add(const struct timespec *lhs_p, const struct timespec *rhs_p,
+              struct timespec *res_p)
+{
+    if ((lhs_p->tv_nsec + rhs_p->tv_nsec) > 1000000000L) {
+        res_p->tv_sec = lhs_p->tv_sec + rhs_p->tv_sec + 1;
+        res_p->tv_nsec = lhs_p->tv_nsec + rhs_p->tv_nsec - 1000000000L;
+    } else {
+        res_p->tv_sec = lhs_p->tv_sec + rhs_p->tv_sec;
+        res_p->tv_nsec = lhs_p->tv_nsec + rhs_p->tv_nsec;
+    }
+}
+
+static void
+timespec_diff(const struct timespec *lhs_p, const struct timespec *rhs_p,
+              struct timespec *res_p)
+{
+    if ((lhs_p->tv_nsec - rhs_p->tv_nsec) < 0) {
+        res_p->tv_sec = lhs_p->tv_sec - rhs_p->tv_sec - 1;
+        res_p->tv_nsec = lhs_p->tv_nsec - rhs_p->tv_nsec + 1000000000L;
+    } else {
+        res_p->tv_sec = lhs_p->tv_sec - rhs_p->tv_sec;
+        res_p->tv_nsec = lhs_p->tv_nsec - rhs_p->tv_nsec;
+    }
+}
+
+/* Returns 0 on success. */
+int timespec2str(char *buf, unsigned int len, struct timespec *ts)
+{
+    int ret;
+    struct tm t;
+
+    tzset();
+    if (localtime_r(&(ts->tv_sec), &t) == NULL)
+        return 1;
+
+    ret = strftime(buf, len, "%F %T", &t);
+    if (ret == 0)
+        return 2;
+    len -= ret - 1;
+
+    ret = snprintf(&buf[strlen(buf)], len, ".%09ld", ts->tv_nsec);
+    if (ret >= (int)len)
+        return 3;
+    return 0;
+}
+
+/* This function taken from Keith Parkard's blog dated 20121005 */
+static ssize_t
+sock_fd_write(int sock, const void *buf, ssize_t buflen, int fd)
+{
+    ssize_t     size;
+    struct msghdr   msg;
+    struct iovec    iov;
+    union {
+        struct cmsghdr  cmsghdr;
+        char        control[CMSG_SPACE(sizeof (int))];
+    } cmsgu;
+    struct cmsghdr  *cmsg;
+
+    iov.iov_base = (void *)buf; /* OS shouldn't write back in this */
+    iov.iov_len = buflen;
+
+    msg.msg_name = NULL;
+    msg.msg_namelen = 0;
+    msg.msg_iov = &iov;
+    msg.msg_iovlen = 1;
+
+    if (fd != -1) {
+        msg.msg_control = cmsgu.control;
+        msg.msg_controllen = sizeof(cmsgu.control);
+
+        cmsg = CMSG_FIRSTHDR(&msg);
+        cmsg->cmsg_len = CMSG_LEN(sizeof (int));
+        cmsg->cmsg_level = SOL_SOCKET;
+        cmsg->cmsg_type = SCM_RIGHTS;
+
+        printf ("passing fd %d\n", fd);
+        *((int *) CMSG_DATA(cmsg)) = fd;
+    } else {
+        msg.msg_control = NULL;
+        msg.msg_controllen = 0;
+        printf ("not passing fd\n");
+    }
+
+    size = sendmsg(sock, &msg, 0);
+
+    if (size < 0)
+        perror ("sendmsg");
+    return size;
+}
+
+/* This function taken from Keith Parkard's blog dated 2101205 */
+static ssize_t
+sock_fd_read(int sock, void *buf, ssize_t bufsize, int *fd)
+{
+    ssize_t     size;
+
+    if (fd) {
+        struct msghdr   msg;
+        struct iovec    iov;
+        union {
+            struct cmsghdr  cmsghdr;
+            char        control[CMSG_SPACE(sizeof (int))];
+        } cmsgu;
+        struct cmsghdr  *cmsg;
+
+        iov.iov_base = buf;
+        iov.iov_len = bufsize;
+
+        msg.msg_name = NULL;
+        msg.msg_namelen = 0;
+        msg.msg_iov = &iov;
+        msg.msg_iovlen = 1;
+        msg.msg_control = cmsgu.control;
+        msg.msg_controllen = sizeof(cmsgu.control);
+        size = recvmsg (sock, &msg, 0);
+        if (size < 0) {
+            perror ("recvmsg");
+            exit(1);
+        }
+        cmsg = CMSG_FIRSTHDR(&msg);
+        if (cmsg && cmsg->cmsg_len == CMSG_LEN(sizeof(int))) {
+            if (cmsg->cmsg_level != SOL_SOCKET) {
+                fprintf (stderr, "invalid cmsg_level %d\n",
+                     cmsg->cmsg_level);
+                exit(1);
+            }
+            if (cmsg->cmsg_type != SCM_RIGHTS) {
+                fprintf (stderr, "invalid cmsg_type %d\n",
+                     cmsg->cmsg_type);
+                exit(1);
+            }
+
+            *fd = *((int *) CMSG_DATA(cmsg));
+            printf ("received fd %d\n", *fd);
+        } else
+            *fd = -1;
+    } else {
+        size = read (sock, buf, bufsize);
+        if (size < 0) {
+            perror("read");
+            exit(1);
+        }
+    }
+    return size;
+}
+
+static void
+set_more_async(int fd, bool more_asy, bool no_dur)
+{
+    if (sg_drv_ver_num > 40030) {
+        struct sg_extended_info sei;
+        struct sg_extended_info * seip;
+
+        seip = &sei;
+        memset(seip, 0, sizeof(*seip));
+        seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+        seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS;
+        if (more_asy) {
+           seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_MORE_ASYNC;
+           seip->ctl_flags = SG_CTL_FLAGM_MORE_ASYNC;
+        }
+        if (no_dur) {
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_NO_DURATION;
+            seip->ctl_flags = SG_CTL_FLAGM_NO_DURATION;
+        }
+        if (ioctl(fd, SG_SET_GET_EXTENDED, seip) < 0) {
+            pr2serr("ioctl(SG_SET_GET_EXTENDED, MORE_ASYNC(|NO_DUR)) failed, "
+                    "errno=%d %s\n", errno, strerror(errno));
+            return;
+        }
+    } else
+        pr2serr("sg driver too old for ioctl(SG_SET_GET_EXTENDED)\n");
+}
+
+static void
+pr_create_dev_time(int sg_fd, const char * dev_name)
+{
+    uint32_t u;
+    uint64_t l;
+    struct sg_extended_info sei;
+    struct sg_extended_info * seip;
+    struct timespec time_up, realtime, boottime, createtime, tmp;
+    char b[64];
+
+    seip = &sei;
+        memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_DEV_TS_LOWER;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("%s: ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n",
+                __func__, errno, strerror(errno));
+        return;
+    }
+    u = seip->read_value;
+    seip->read_value = SG_SEIRV_DEV_TS_UPPER;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("%s: ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n",
+                __func__, errno, strerror(errno));
+        return;
+    }
+    l = seip->read_value;
+    l <<= 32;
+    l |= u;
+    time_up.tv_sec = l / 1000000000UL;
+    time_up.tv_nsec = l % 1000000000UL;
+    /* printf("create time nanoseconds=%" PRIu64 "\n", l); */
+    if (clock_gettime(CLOCK_REALTIME, &realtime) < 0) {
+        pr2serr("%s: clock_gettime(CLOCK_REALTIME) failed, errno=%d %s\n",
+                __func__, errno, strerror(errno));
+        return;
+    }
+    if (clock_gettime(CLOCK_BOOTTIME, &boottime) < 0) {
+        pr2serr("%s: clock_gettime(CLOCK_REALTIME) failed, errno=%d %s\n",
+                __func__, errno, strerror(errno));
+        return;
+    }
+    timespec_diff(&realtime, &boottime, &tmp);
+    timespec_add(&tmp, &time_up, &createtime);
+#if 0
+    printf("real time: %ld,%ld\n", realtime.tv_sec, realtime.tv_nsec);
+    printf("boot time: %ld,%ld\n", boottime.tv_sec, boottime.tv_nsec);
+    printf("time up: %ld,%ld\n", time_up.tv_sec, time_up.tv_nsec);
+    printf("create time: %ld,%ld\n", createtime.tv_sec, createtime.tv_nsec);
+#endif
+    timespec2str(b, sizeof(b), &createtime);
+    printf("Create time of %s was %s\n", dev_name, b);
+}
+
+static int
+tst_extended_ioctl(const char * fnp, int sg_fd, const char * fn2p, int sg_fd2,
+                   int sock, const char * cp)
+{
+    uint32_t cflags;
+    struct sg_extended_info sei;
+    struct sg_extended_info * seip;
+
+    seip = &sei;
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_RESERVED_SIZE;
+    seip->reserved_sz = reserve_buff_sz;
+    seip->sgat_elem_sz = 64 * 1024;
+    seip->sei_rd_mask |= SG_SEIM_RESERVED_SIZE;
+    seip->sei_rd_mask |= SG_SEIM_TOT_FD_THRESH;
+    seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+    seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS; /* this or previous optional */
+    seip->sei_rd_mask |= SG_SEIM_MINOR_INDEX;
+    seip->sei_wr_mask |= SG_SEIM_SGAT_ELEM_SZ;
+    seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_OTHER_OPENS;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_ORPHANS;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_Q_TAIL;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_IS_SHARE;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_IS_READ_SIDE;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_UNSHARE;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_READ_SIDE_FINI;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_READ_SIDE_ERR;
+    seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_NO_DURATION;
+    seip->ctl_flags_rd_mask |= SG_CTL_FLAGM_NO_DURATION;
+    seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+    seip->ctl_flags |= SG_CTL_FLAGM_NO_DURATION;
+
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+#if 1
+    printf("%sSG_SET_GET_EXTENDED ioctl ok\n", cp);
+    if (SG_SEIM_RESERVED_SIZE & seip->sei_rd_mask)
+        printf("  %sreserved size: %u\n", cp, seip->reserved_sz);
+    if (SG_SEIM_MINOR_INDEX & seip->sei_rd_mask)
+        printf("  %sminor index: %u\n", cp, seip->minor_index);
+    if (SG_SEIM_TOT_FD_THRESH & seip->sei_rd_mask)
+        printf("  %stot_fd_thresh: %u\n", cp, seip->tot_fd_thresh);
+    if ((SG_SEIM_CTL_FLAGS & seip->sei_rd_mask) ||
+         (SG_SEIM_CTL_FLAGS & seip->sei_wr_mask)) {
+        cflags = seip->ctl_flags;
+        if (SG_CTL_FLAGM_TIME_IN_NS & seip->ctl_flags_rd_mask)
+            printf("  %sTIME_IN_NS: %s\n", cp,
+                   (SG_CTL_FLAGM_TIME_IN_NS & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_OTHER_OPENS & seip->ctl_flags_rd_mask)
+            printf("  %sOTHER_OPENS: %s\n", cp,
+                   (SG_CTL_FLAGM_OTHER_OPENS & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_ORPHANS & seip->ctl_flags_rd_mask)
+            printf("  %sORPHANS: %s\n", cp,
+                   (SG_CTL_FLAGM_ORPHANS & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_Q_TAIL & seip->ctl_flags_rd_mask)
+            printf("  %sQ_TAIL: %s\n", cp,
+                   (SG_CTL_FLAGM_Q_TAIL & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_IS_SHARE & seip->ctl_flags_rd_mask)
+            printf("  %sIS_SHARE: %s\n", cp,
+                   (SG_CTL_FLAGM_IS_SHARE & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_IS_READ_SIDE & seip->ctl_flags_rd_mask)
+            printf("  %sIS_READ_SIDE: %s\n", cp,
+                   (SG_CTL_FLAGM_IS_READ_SIDE & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_UNSHARE & seip->ctl_flags_rd_mask)
+            printf("  %sUNSHARE: %s\n", cp,
+                   (SG_CTL_FLAGM_UNSHARE & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_READ_SIDE_FINI & seip->ctl_flags_rd_mask)
+            printf("  %sREAD_SIDE_FINI: %s\n", cp,
+                   (SG_CTL_FLAGM_READ_SIDE_FINI & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_READ_SIDE_ERR & seip->ctl_flags_rd_mask)
+            printf("  %sREAD_SIDE_ERR: %s\n", cp,
+                   (SG_CTL_FLAGM_READ_SIDE_ERR & cflags) ? "true" : "false");
+        if (SG_CTL_FLAGM_NO_DURATION & seip->ctl_flags_rd_mask)
+            printf("  %sNO_DURATION: %s\n", cp,
+                   (SG_CTL_FLAGM_NO_DURATION & cflags) ? "true" : "false");
+    }
+    if (SG_SEIM_MINOR_INDEX & seip->sei_rd_mask)
+        printf("  %sminor_index: %u\n", cp, seip->minor_index);
+    printf("\n");
+#endif
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_INT_MASK;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+    printf("  %sread_value[SG_SEIRV_INT_MASK]= %u\n", cp, seip->read_value);
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_BOOL_MASK;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+    printf("  %sread_value[SG_SEIRV_BOOL_MASK]= %u\n", cp, seip->read_value);
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_VERS_NUM;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+    printf("  %sread_value[SG_SEIRV_VERS_NUM]= %u\n", cp, seip->read_value);
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_INACT_RQS;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+    printf("  %sread_value[SG_SEIRV_INACT_RQS]= %u\n", cp, seip->read_value);
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_DEV_INACT_RQS;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+    printf("  %sread_value[SG_SEIRV_DEV_INACT_RQS]= %u\n", cp,
+           seip->read_value);
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_SUBMITTED;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+    printf("  %sread_value[SG_SEIRV_SUBMITTED]= %u\n", cp, seip->read_value);
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_READ_VAL;
+    seip->sei_rd_mask |= SG_SEIM_READ_VAL;
+    seip->read_value = SG_SEIRV_DEV_SUBMITTED;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("ioctl(SG_SET_GET_EXTENDED) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        return 1;
+    }
+    printf("  %sread_value[SG_SEIRV_DEV_SUBMITTED]= %u\n", cp,
+           seip->read_value);
+
+    memset(seip, 0, sizeof(*seip));
+    seip->sei_wr_mask |= SG_SEIM_SHARE_FD;
+    seip->sei_rd_mask |= SG_SEIM_SHARE_FD;
+#if 1
+    seip->share_fd = sg_fd2;
+#else
+    seip->share_fd = sg_fd;
+#endif
+    if (do_fork && is_parent)
+        goto bypass_share;
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr("%sioctl(SG_SET_GET_EXTENDED) shared_fd=%d, failed errno=%d "
+                "%s\n", cp, sg_fd2, errno, strerror(errno));
+    }
+    printf("  %sshare successful, read back previous shared_fd= %d\n", cp,
+           (int)seip->share_fd);
+bypass_share:
+
+    if (ioctl(sg_fd, SG_GET_TRANSFORM, NULL) < 0)
+        pr2serr("ioctl(SG_GET_TRANSFORM) fail expected, errno=%d %s\n",
+                errno, strerror(errno));
+    else
+        printf("%sSG_GET_TRANSFORM okay (does nothing)\n", cp);
+    if (ioctl(sg_fd, SG_SET_TRANSFORM, NULL) < 0)
+        pr2serr("ioctl(SG_SET_TRANSFORM) fail expected, errno=%d %s\n",
+                errno, strerror(errno));
+    else
+        printf("%sSG_SET_TRANSFORM okay (does nothing)\n", cp);
+    printf("\n");
+
+    /* test sending a sg file descriptor between 2 processes using UNIX
+     * sockets */
+    if (do_fork && is_parent && fnp && (sock >= 0)) { /* master/READ side */
+        int res;
+        int fd_ma = open(fnp, O_RDWR);
+
+        if (fd_ma < 0) {
+            pr2serr("%s: opening %s failed: %s\n", __func__, fnp,
+                    strerror(errno));
+            return 1;
+        }
+        res = sock_fd_write(sock, "boo", 4, fd_ma);
+        if (res < 0)
+            pr2serr("%s: sock_fd_write() failed\n", __func__);
+        else
+            printf("%s: sock_fd_write() returned: %d\n", __func__, res);
+    } else if (do_fork && !is_parent && fn2p && (sock >= 0)) {
+        int res, fd_ma;
+        /* int fd_sl = open(fn2p, O_RDWR); not needed */
+        uint8_t b[32];
+
+        fd_ma = -1;
+        res = sock_fd_read(sock, b, sizeof(b), &fd_ma);
+        if (res < 0)
+            pr2serr("%s: sock_fd_read() failed\n", __func__);
+        else
+            printf("%s: sock_fd_read() returned: %d, fd_ma=%d\n", __func__,
+                   res, fd_ma);
+        /* yes it works! */
+    }
+    return 0;
+}
+
+static int
+do_mrqs(int sg_fd, int sg_fd2, int mrqs)
+{
+    bool both = (sg_fd2 >= 0);
+    int k, j, arr_v4_sz, good;
+    int res = 0;
+    struct sg_io_v4 * arr_v4;
+    struct sg_io_v4 * h4p;
+    struct sg_io_v4 * mrq_h4p;
+    struct sg_io_v4 mrq_h4;
+    uint8_t sense_buffer[SENSE_BUFFER_LEN] SG_C_CPP_ZERO_INIT;
+    uint8_t inq_cdb[INQ_CMD_LEN] =      /* Device Id VPD page */
+                                {0x12, 0x1, 0x83, 0, INQ_REPLY_LEN, 0};
+    uint8_t sdiag_cdb[SDIAG_CMD_LEN] =
+                                {0x1d, 0x10 /* PF */, 0, 0, 0, 0};
+    uint8_t inqBuff[INQ_REPLY_LEN];
+
+    if (both) {
+        struct sg_extended_info sei;
+        struct sg_extended_info * seip;
+
+        seip = &sei;
+        memset(seip, 0, sizeof(*seip));
+        seip->sei_wr_mask |= SG_SEIM_SHARE_FD;
+        seip->sei_rd_mask |= SG_SEIM_SHARE_FD;
+        seip->share_fd = sg_fd;         /* master */
+        if (ioctl(sg_fd2, SG_SET_GET_EXTENDED, seip) < 0) {
+            res = errno;
+            pr2serr("ioctl(sg_fd2, SG_SET_GET_EXTENDED) shared_fd, "
+                    "failed errno=%d %s\n", res, strerror(res));
+            return res;
+        }
+    }
+    memset(inqBuff, 0, sizeof(inqBuff));
+    mrq_h4p = &mrq_h4;
+    memset(mrq_h4p, 0, sizeof(*mrq_h4p));
+    mrq_h4p->guard = 'Q';
+    mrq_h4p->flags = SGV4_FLAG_MULTIPLE_REQS;
+    if (mrq_immed)
+        mrq_h4p->flags |= SGV4_FLAG_IMMED;
+    arr_v4 = (struct sg_io_v4 *)calloc(mrqs, sizeof(struct sg_io_v4));
+    if (NULL == arr_v4) {
+        res = ENOMEM;
+        goto fini;
+    }
+    arr_v4_sz = mrqs * sizeof(struct sg_io_v4);
+
+    for (k = 0; k < mrqs; ++k) {
+        h4p = arr_v4 + k;
+
+        h4p->guard = 'Q';
+        /* ->protocol and ->subprotocol are already zero */
+        /* io_hdr[k].iovec_count = 0; */  /* memset takes care of this */
+        if (0 == (k % 2)) {
+            h4p->request_len = sizeof(sdiag_cdb);
+            h4p->request = (uint64_t)(uintptr_t)sdiag_cdb;
+            /* all din and dout fields are zero */
+        } else {
+            h4p->request_len = sizeof(inq_cdb);
+            h4p->request = (uint64_t)(uintptr_t)inq_cdb;
+            h4p->din_xfer_len = INQ_REPLY_LEN;
+            h4p->din_xferp = (uint64_t)(uintptr_t)inqBuff;
+            if (both)
+                h4p->flags |= SGV4_FLAG_DO_ON_OTHER;
+        }
+        h4p->response = (uint64_t)(uintptr_t)sense_buffer;
+        h4p->max_response_len = sizeof(sense_buffer);
+        h4p->timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        h4p->request_extra = k + 3;      /* so pack_id doesn't start at 0 */
+        /* default is to queue at head (in SCSI mid level) */
+        if (q_at_tail)
+            h4p->flags |= SG_FLAG_Q_AT_TAIL;
+        else
+            h4p->flags |= SG_FLAG_Q_AT_HEAD;
+    }
+    mrq_h4p->dout_xferp = (uint64_t)(uintptr_t)arr_v4;
+    mrq_h4p->dout_xfer_len = arr_v4_sz;
+    mrq_h4p->din_xferp = mrq_h4p->dout_xferp;
+    mrq_h4p->din_xfer_len = mrq_h4p->dout_xfer_len;
+    if (ioctl(sg_fd, (mrq_iosubmit ? SG_IOSUBMIT : SG_IO), mrq_h4p) < 0) {
+        res = errno;
+        pr2serr("ioctl(SG_IO%s, mrq) failed, errno=%d %s\n",
+                (mrq_iosubmit ? "SUBMIT" : ""), res, strerror(res));
+        goto fini;
+    }
+    if ((mrq_h4p->dout_resid > 0) || ((int)mrq_h4p->info < mrqs))
+        pr2serr("ioctl(SG_IO%s, mrq) dout_resid=%d, info=%d\n\n",
+                (mrq_iosubmit ? "SUBMIT" : ""), mrq_h4p->dout_resid,
+                mrq_h4p->info);
+
+    good = 0;
+    j = 0;
+    if (mrq_immed) {
+receive_more:
+        if (mrq_half_immed)
+            mrq_h4p->flags = SGV4_FLAG_MULTIPLE_REQS; // zap SGV4_FLAG_IMMED
+        if (ioctl(sg_fd, SG_IORECEIVE, mrq_h4p) < 0) {
+            res = errno;
+            pr2serr("ioctl(SG_IORECEIVE, mrq) failed, errno=%d %s\n",
+                    res, strerror(res));
+            goto fini;
+        }
+        if ((mrq_h4p->din_resid > 0) || ((int)mrq_h4p->info < mrqs))
+            pr2serr("ioctl(SG_IORECEIVE, mrq) din_resid=%d, info=%d\n",
+                    mrq_h4p->din_resid, mrq_h4p->info);
+    }
+
+    for (k = 0; k < (int)mrq_h4p->info; ++k, ++j) {
+        h4p = arr_v4 + k;
+        if (! (h4p->driver_status || h4p->transport_status ||
+               h4p->device_status)) {
+            if (h4p->info & SG_INFO_MRQ_FINI)
+                ++good;
+        }
+        if ((! (h4p->info & SG_INFO_MRQ_FINI)) && (verbose > 1))
+            pr2serr("%s: k=%d: SG_INFO_MRQ_FINI not set on response\n",
+                    __func__, k);
+    }
+    if (mrq_immed && (j < mrqs))
+        goto receive_more;
+
+    if (good > 0) {
+        printf("Final INQUIRY response:\n");
+        hex2stdout(inqBuff, INQ_REPLY_LEN, 0);
+    }
+    printf("Good responses: %d, bad responses: %d\n", good, mrqs - good);
+    if (mrq_h4p->driver_status != 0)
+        printf("Master mrq object: driver_status=%d\n",
+               mrq_h4p->driver_status);
+    h4p = arr_v4 + mrqs - 1;
+    if (h4p->driver_status != 0)
+        printf("Last mrq object: driver_status=%d\n", h4p->driver_status);
+
+fini:
+    if (arr_v4)
+        free(arr_v4);
+    return res;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool done, is_first;
+    bool nw_given = false;
+    bool has_dname_range = false;
+    int k, ok, pack_id, num_waiting;
+    int res = 0;
+    int sum_nw = 0;
+    int sg_fd = -1;
+    int sg_fd2 = -1;
+    int sock = -1;
+    uint8_t inq_cdb[INQ_CMD_LEN] =
+                                {0x12, 0, 0, 0, INQ_REPLY_LEN, 0};
+    uint8_t sdiag_cdb[SDIAG_CMD_LEN] =
+                                {0x1d, 0x10 /* PF */, 0, 0, 0, 0};
+    uint8_t inqBuff[MAX_Q_LEN][INQ_REPLY_LEN];
+    sg_io_hdr_t io_hdr[MAX_Q_LEN];
+    sg_io_hdr_t rio_hdr;
+    char ebuff[EBUFF_SZ];
+    char dname[256];
+    uint8_t sense_buffer[MAX_Q_LEN][SENSE_BUFFER_LEN] SG_C_CPP_ZERO_INIT;
+    const char * second_fname = NULL;
+    const char * cp;
+    char * chp;
+    struct sg_scsi_id ssi;
+
+
+    if (sizeof(struct sg_extended_info) != 96)
+        pr2serr("Warning <<<< sizeof(struct sg_extended_info)=%zu not 96\n",
+                sizeof(struct sg_extended_info));
+    for (k = 1; k < argc; ++k) {
+        if (0 == memcmp("-3", argv[k], 2))
+            do_v3_only = true;
+        else if (0 == memcmp("-c", argv[k], 2))
+            create_time = true;
+        else if (0 == memcmp("-f", argv[k], 2))
+            do_fork = true;
+        else if (0 == memcmp("-h", argv[k], 2)) {
+            file_name = 0;
+            break;
+        } else if (0 == memcmp("-I=", argv[k], 3)) {
+            iterator_test = atoi(argv[k] + 3);
+            if ((iterator_test > 1) || (iterator_test < -1)) {
+                printf("Expect -I= to take a number, either 0 or 1\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-J=", argv[k], 3)) {
+            object_walk_test = atoi(argv[k] + 3);
+            if ((object_walk_test > 1) || (object_walk_test < -1)) {
+                printf("Expect -J= to take a number, either 0 or 1\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-l=", argv[k], 3)) {
+            q_len = atoi(argv[k] + 3);
+            if ((q_len > 511) || (q_len < 1)) {
+                printf("Expect -l= to take a number (q length) between 1 "
+                       "and 511\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-m=", argv[k], 3)) {
+            num_mrqs = sg_get_num(argv[k] + 3);
+            if (num_mrqs < 1) {
+                printf("Expect -m= to take a number greater than 0\n");
+                file_name = 0;
+                break;
+            }
+            if ((cp = strchr(argv[k] + 3, ','))) {
+                mrq_iosubmit = true;
+                if (cp[1] == 'I')
+                    mrq_immed = true;
+                else if (cp[1] == 'i') {
+                    mrq_immed = true;
+                    mrq_half_immed = true;
+                } else if (toupper(cp[1]) == 'S')
+                    ;
+                else {
+                    printf("-m= option expects 'A' or 'a' as a suffix, "
+                           "after comma\n");
+                    file_name = 0;
+                    break;
+                }
+            }
+        } else if (0 == memcmp("-M", argv[k], 2))
+            more_async = true;
+        else if (0 == memcmp("-n", argv[k], 2))
+            no_duration = true;
+        else if (0 == memcmp("-o", argv[k], 2))
+            ioctl_only = true;
+        else if (0 == memcmp("-r=", argv[k], 3)) {
+            reserve_buff_sz = atoi(argv[k] + 3);
+            if (reserve_buff_sz < 0) {
+                printf("Expect -r= to take a number 0 or higher\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-s=", argv[k], 3)) {
+            sleep_secs = atoi(argv[k] + 3);
+            if (sleep_secs < 0) {
+                printf("Expect -s= to take a number 0 or higher\n");
+                file_name = 0;
+                break;
+            }
+        } else if (0 == memcmp("-S", argv[k], 2))
+            show_size_value = true;
+        else if (0 == memcmp("-t", argv[k], 2))
+            q_at_tail = true;
+        else if (0 == memcmp("-T=", argv[k], 3)) {
+            num_sgnw = sg_get_num(argv[k] + 3);
+            if (num_sgnw < 0) {
+                printf("Expect -T= to take a number >= 0\n");
+                file_name = 0;
+                break;
+            }
+            nw_given = true;
+        } else if (0 == memcmp("-vvvvvvv", argv[k], 8))
+            verbose += 7;
+        else if (0 == memcmp("-vvvvvv", argv[k], 7))
+            verbose += 6;
+        else if (0 == memcmp("-vvvvv", argv[k], 6))
+            verbose += 5;
+        else if (0 == memcmp("-vvvv", argv[k], 5))
+            verbose += 4;
+        else if (0 == memcmp("-vvv", argv[k], 4))
+            verbose += 3;
+        else if (0 == memcmp("-vv", argv[k], 3))
+            verbose += 2;
+        else if (0 == memcmp("-v", argv[k], 2))
+            verbose += 1;
+        else if (0 == memcmp("-V", argv[k], 2)) {
+            printf("%s\n", version_str);
+            return 0;
+        } else if (0 == memcmp("-w", argv[k], 2))
+            write_only = true;
+        else if (*argv[k] == '-') {
+            printf("Unrecognized switch: %s\n", argv[k]);
+            file_name = 0;
+            break;
+        }
+        else if (0 == file_name)
+            file_name = argv[k];
+        else if (NULL == second_fname)
+            second_fname = argv[k];
+        else {
+            printf("too many arguments\n");
+            file_name = 0;
+            break;
+        }
+    }
+    if ((iterator_test >= 0) || (object_walk_test >= 0))
+        nw_given = false;
+
+    if (show_size_value) {
+        struct utsname unam;
+
+        printf("Size in bytes:\n");
+        printf("\t%zu\tsizeof(struct sg_header) Version 2 interface "
+               "structure\n", sizeof(struct sg_header));
+        printf("\t%zu\tsizeof(struct sg_io_hdr) Version 3 interface "
+               "structure\n", sizeof(struct sg_io_hdr));
+        printf("\t%zu\tsizeof(struct sg_io_v4) Version 4 interface "
+               "structure\n", sizeof(struct sg_io_v4));
+        printf("\t%zu\tsizeof(struct sg_iovec) scatter gather element\n",
+               sizeof(struct sg_iovec));
+        printf("\t%zu\tsizeof(struct sg_scsi_id) topological device id\n",
+               sizeof(struct sg_scsi_id));
+        printf("\t%zu\tsizeof(struct sg_req_info) request information\n",
+               sizeof(struct sg_req_info));
+        printf("\t%zu\tsizeof(struct sg_extended_info) for "
+               "SG_SET_GET_EXTENDED\n",
+               sizeof(struct sg_extended_info));
+        printf("\nioctl values (i.e. second argument to ioctl()):\n");
+        printf("\t0x%lx\t\tvalue of SG_GET_NUM_WAITING ioctl\n",
+               (unsigned long)SG_GET_NUM_WAITING);
+        printf("\t0x%lx\t\tvalue of SG_IO ioctl\n",
+               (unsigned long)SG_IO);
+        printf("\t0x%lx\tvalue of SG_IOABORT ioctl\n",
+               (unsigned long)SG_IOABORT);
+        printf("\t0x%lx\tvalue of SG_IORECEIVE ioctl\n",
+               (unsigned long)SG_IORECEIVE);
+        printf("\t0x%lx\tvalue of SG_IORECEIVE_V3 ioctl\n",
+               (unsigned long)SG_IORECEIVE_V3);
+        printf("\t0x%lx\tvalue of SG_IOSUBMIT ioctl\n",
+               (unsigned long)SG_IOSUBMIT);
+        printf("\t0x%lx\tvalue of SG_IOSUBMIT_V3 ioctl\n",
+               (unsigned long)SG_IOSUBMIT_V3);
+        printf("\t0x%lx\tvalue of SG_SET_GET_EXTENDED ioctl\n",
+               (unsigned long)SG_SET_GET_EXTENDED);
+        printf("\n\t0x%x\t\tbase value of most SG_* ioctls\n",
+               SG_IOCTL_MAGIC_NUM);
+        printf("\nsizeof(void *) [a pointer] on this machine: %u bytes\n",
+               (unsigned)sizeof(void *));
+        if (0 == uname(&unam))
+            printf("Machine name: %s\n", unam.machine);
+
+        return 0;
+    }
+    if (0 == file_name) {
+        printf("No filename (sg device) given\n\n");
+        usage();
+        return 1;
+    }
+    memset(dname, 0, sizeof(dname));
+    if (strlen(file_name) > 255) {
+        fprintf(stderr, "file_name too long\n");
+        goto out;
+    }
+    strncpy(dname, file_name, sizeof(dname) - 1);
+    if ((chp = strchr(dname, '-'))) {
+        if (1 != sscanf(chp + 1, "%d", &dname_last)) {
+            fprintf(stderr, "can't code number after '-' in file_name\n");
+            goto out;
+        }
+        *chp = '\0';
+        --chp;
+        while (isdigit(*chp))
+            --chp;
+        ++chp;
+        if (1 != sscanf(chp, "%d", &dname_current)) {
+            fprintf(stderr, "can't code number before '-' in file_name\n");
+            goto out;
+        }
+        *chp = '\0';
+        has_dname_range = true;
+        dname_pos = strlen(dname);
+    }
+    is_first = true;
+
+dname_range_loop:
+    if (has_dname_range)
+        sprintf(dname + dname_pos, "%d", dname_current);
+
+    /* An access mode of O_RDWR is required for write()/read() interface */
+    if ((sg_fd = open(dname, O_RDWR)) < 0) {
+        snprintf(ebuff, EBUFF_SZ, "error opening file: %s", dname);
+        perror(ebuff);
+        return 1;
+    }
+    if (verbose)
+        fprintf(stderr, "opened given file: %s successfully, fd=%d\n",
+                dname, sg_fd);
+
+    if (ioctl(sg_fd, SG_GET_VERSION_NUM, &sg_drv_ver_num) < 0) {
+        pr2serr("ioctl(SG_GET_VERSION_NUM) failed, errno=%d %s\n", errno,
+                strerror(errno));
+        goto out;
+    }
+    if (is_first)
+        printf("Linux sg driver version: %d\n", sg_drv_ver_num);
+
+    if (create_time && (sg_drv_ver_num > 40030)) {
+        pr_create_dev_time(sg_fd, dname);
+        goto out;
+    }
+
+    if (nw_given || (iterator_test >= 0) || (object_walk_test >= 0)) {
+        /* -T=NUM and/or -I=0|1 or -j=0|1 */
+        /* time ioctl(SG_GET_NUM_WAITING) or do iterator_test */
+        int nw;
+        struct timespec start_tm, fin_tm, res_tm;
+
+        if (is_first) {
+            int rang = has_dname_range ? (1 + dname_last - dname_current) : 1;
+
+            is_first = false;
+            if (nw_given)
+                printf("Timing %d x %d calls to ioctl(SG_GET_NUM_WAITING)\n",
+                       rang, num_sgnw);
+            else if (iterator_test >= 0) {
+                k = num_sgnw + 1000;
+                printf("Timing %d calls to ioctl(SG_SET_DEBUG, %d)\n",
+                       rang, ((0 == iterator_test) ? -k : k));
+            } else
+                printf("Timing %d calls to ioctl(SG_SET_DEBUG, %d)\n",
+                       rang, (object_walk_test == 0) ? 999 : -999);
+            if (0 != clock_gettime(CLOCK_MONOTONIC, &start_tm)) {
+                    res = errno;
+                    perror("start clock_gettime() failed:");
+                    goto out;
+            }
+        }
+        if (nw_given) {
+            for (k = 0; k < num_sgnw; ++k, sum_nw += nw) {
+                if (ioctl(sg_fd, SG_GET_NUM_WAITING, &nw) < 0) {
+                    res = errno;
+                    fprintf(stderr, "%d: ioctl(SG_GET_NUM_WAITING) failed "
+                            "errno=%d\n", k, res);
+                    goto out;
+                }
+            }
+        } else if (iterator_test >= 0) {
+            int fd, pid;
+
+            k = num_sgnw + 1000;
+            if (0 == iterator_test)
+                k = -k;
+            if (second_fname) {
+                if ((sg_fd2 = open(second_fname, O_RDWR)) < 0) {
+                    snprintf(ebuff, EBUFF_SZ, "%s: error opening file: %s",
+                             __func__, second_fname);
+                    perror(ebuff);
+                    return 1;
+                }
+                printf("About to fork due to second filename\n");
+                pid = fork();
+                if (pid < 0) {
+                    perror("fork() failed");
+                    goto out;
+                } else if (0 == pid) {
+                    relative_cp = "child: ";
+                    is_parent = false;
+                    fd = sg_fd2;
+                } else {
+                    relative_cp = "parent: ";
+                    is_parent = true;
+                    childs_pid = pid;
+                    fd = sg_fd;
+                }
+            } else {
+                fd = sg_fd;
+                relative_cp = "";
+            }
+            if (ioctl(fd, SG_SET_DEBUG, &k) < 0) {
+                res = errno;
+                fprintf(stderr, "%s%d: ioctl(SG_SET_DEBUG) failed errno=%d\n",
+                        relative_cp, k, res);
+                goto out;
+            } else if (verbose)
+                fprintf(stderr, "%siterator_test good ioctl(SG_SET_DEBUG, "
+                        "%d)\n", relative_cp, k);
+            sum_nw += num_sgnw;
+        } else if (object_walk_test >= 0) {
+            const char * ccp = "object_walk_test";
+
+            relative_cp = "";
+            k = (object_walk_test == 0) ? 999 : -999;
+            if (ioctl(sg_fd, SG_SET_DEBUG, &k) < 0) {
+                res = errno;
+                fprintf(stderr, "%s: ioctl(SG_SET_DEBUG, %d) failed "
+                        "errno=%d\n", ccp, k, res);
+            } else if (verbose)
+                fprintf(stderr, "%s: good call to ioctl(SG_SET_DEBUG, %d)\n",
+                        ccp, k);
+            sum_nw += 10000;    /* (1_up-scan + 2_lookups) * 10,000 times */
+        }
+
+        if (has_dname_range) {
+            ++dname_current;
+            if (dname_current <= dname_last) {
+                if (sg_fd >= 0)
+                    close(sg_fd);
+                goto dname_range_loop;
+            }
+        }
+        if (0 != clock_gettime(CLOCK_MONOTONIC, &fin_tm)) {
+            res = errno;
+            perror("finish clock_gettime() failed:");
+            goto out;
+        }
+        res_tm.tv_sec = fin_tm.tv_sec - start_tm.tv_sec;
+        res_tm.tv_nsec = fin_tm.tv_nsec - start_tm.tv_nsec;
+        if (res_tm.tv_nsec < 0) {
+            --res_tm.tv_sec;
+            res_tm.tv_nsec += 1000000000;
+        }
+        if (verbose) {
+            if (nw_given && (verbose > 1))
+                printf("sum of num_waiting_s=%d\n", sum_nw);
+            printf("%selapsed time (nanosecond precision): %d.%09d secs\n",
+                   relative_cp, (int)res_tm.tv_sec, (int)res_tm.tv_nsec);
+        } else
+            printf("%selapsed time: %d.%06d secs\n", relative_cp,
+                   (int)res_tm.tv_sec, (int)(res_tm.tv_nsec / 1000));
+        if (num_sgnw >= 100) {
+            double m = (double)res_tm.tv_sec +
+                       ((double)res_tm.tv_nsec / 1000000000.0);
+            double num = num_sgnw;
+
+            if (m > 0.000001)
+                printf("%sCalls per second: %.2f\n", relative_cp, num / m);
+        }
+        res = 0;
+        goto out;
+    }
+    if ((more_async || no_duration) && !do_v3_only)
+        set_more_async(sg_fd, more_async, no_duration);
+
+    if (second_fname) {
+        if ((sg_fd2 = open(second_fname, O_RDWR)) < 0) {
+            snprintf(ebuff, EBUFF_SZ,
+                     "%s: error opening file: %s", __func__, second_fname);
+            perror(ebuff);
+            return 1;
+        }
+        if (verbose)
+            fprintf(stderr, "opened second file: %s successfully, fd=%d\n",
+                    second_fname, sg_fd2);
+        if (more_async && !do_v3_only)
+            set_more_async(sg_fd2, more_async, no_duration);
+    }
+
+    if ((num_mrqs > 0) && !do_v3_only) {
+        res = do_mrqs(sg_fd, sg_fd2, num_mrqs);
+        goto out;
+    }
+
+    if (do_fork) {
+        int pid;
+        int sv[2];
+
+        if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sv) < 0) {
+            perror("socketpair");
+            exit(1);
+        }
+        printf("socketpair: sv[0]=%d, sv[1]=%d sg_fd=%d\n", sv[0], sv[1],
+               sg_fd);
+        pid = fork();
+        if (pid < 0) {
+            perror("fork() failed");
+            goto out;
+        } else if (0 == pid) {
+            relative_cp = "child ";
+            is_parent = false;
+            close(sv[0]);
+            sock = sv[1];
+        } else {
+            relative_cp = "parent ";
+            is_parent = true;
+            childs_pid = pid;
+            close(sv[1]);
+            sock = sv[0];
+        }
+    }
+
+    cp = do_fork ? relative_cp : "";
+    if (! do_v3_only && (sg_drv_ver_num > 40030)) {
+        if (tst_extended_ioctl(dname, sg_fd, second_fname, sg_fd2, sock,
+                               cp))
+            goto out;
+    }
+    if (ioctl_only)
+        goto out;
+
+    if (do_fork && !is_parent)
+        return 0;
+
+    printf("start write() calls [submits]\n");
+    for (k = 0; k < q_len; ++k) {
+        /* Prepare INQUIRY command */
+        memset(&io_hdr[k], 0, sizeof(sg_io_hdr_t));
+        io_hdr[k].interface_id = 'S';
+        /* io_hdr[k].iovec_count = 0; */  /* memset takes care of this */
+        io_hdr[k].mx_sb_len = (uint8_t)sizeof(sense_buffer);
+        if (0 == (k % 3)) {
+            io_hdr[k].cmd_len = sizeof(sdiag_cdb);
+            io_hdr[k].cmdp = sdiag_cdb;
+            io_hdr[k].dxfer_direction = SG_DXFER_NONE;
+        } else {
+            io_hdr[k].cmd_len = sizeof(inq_cdb);
+            io_hdr[k].cmdp = inq_cdb;
+            io_hdr[k].dxfer_direction = SG_DXFER_FROM_DEV;
+            io_hdr[k].dxfer_len = INQ_REPLY_LEN;
+            io_hdr[k].dxferp = inqBuff[k];
+        }
+        io_hdr[k].sbp = sense_buffer[k];
+        io_hdr[k].mx_sb_len = SENSE_BUFFER_LEN;
+        io_hdr[k].timeout = 20000;     /* 20000 millisecs == 20 seconds */
+        io_hdr[k].pack_id = k + 3;      /* so pack_id doesn't start at 0 */
+        /* default is to queue at head (in SCSI mid level) */
+        if (q_at_tail)
+            io_hdr[k].flags |= SG_FLAG_Q_AT_TAIL;
+        else
+            io_hdr[k].flags |= SG_FLAG_Q_AT_HEAD;
+        /* io_hdr[k].usr_ptr = NULL; */
+
+        if (write(sg_fd, &io_hdr[k], sizeof(sg_io_hdr_t)) < 0) {
+            pr2serr("%ssg write errno=%d [%s]\n", cp, errno, strerror(errno));
+            close(sg_fd);
+            return 1;
+        }
+    }
+
+    memset(&ssi, 0, sizeof(ssi));
+    if (ioctl(sg_fd, SG_GET_SCSI_ID, &ssi) < 0)
+        pr2serr("ioctl(SG_GET_SCSI_ID) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else {
+        printf("host_no: %d\n", ssi.host_no);
+        printf("  channel: %d\n", ssi.channel);
+        printf("  scsi_id: %d\n", ssi.scsi_id);
+        printf("  lun: %d\n", ssi.lun);
+        printf("  pdt: %d\n", ssi.scsi_type);
+        printf("  h_cmd_per_lun: %d\n", ssi.h_cmd_per_lun);
+        printf("  d_queue_depth: %d\n", ssi.d_queue_depth);
+        printf("  SCSI 8 byte LUN: ");
+        hex2stdout(ssi.scsi_lun, 8, -1);
+    }
+    if (ioctl(sg_fd, SG_GET_PACK_ID, &pack_id) < 0)
+        pr2serr("ioctl(SG_GET_PACK_ID) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else
+        printf("first available pack_id: %d\n", pack_id);
+    if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0)
+        pr2serr("ioctl(SG_GET_NUM_WAITING) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else
+        printf("num_waiting: %d\n", num_waiting);
+
+    sleep(sleep_secs);
+
+    if (write_only)
+        goto out;
+
+    if (do_fork)
+        printf("\n\nFollowing starting with get_pack_id are all CHILD\n");
+    if (ioctl(sg_fd, SG_GET_PACK_ID, &pack_id) < 0)
+        pr2serr("ioctl(SG_GET_PACK_ID) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else
+        printf("first available pack_id: %d\n", pack_id);
+    if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0)
+        pr2serr("ioctl(SG_GET_NUM_WAITING) failed, errno=%d %s\n",
+                errno, strerror(errno));
+    else
+        printf("num_waiting: %d\n", num_waiting);
+
+    printf("\nstart read() calls [io receive]\n");
+    for (k = 0, done = false; k < q_len; ++k) {
+        if ((! done) && (k == q_len / 2)) {
+            done = true;
+            printf("\n>>> half way through read\n");
+            if (ioctl(sg_fd, SG_GET_PACK_ID, &pack_id) < 0)
+                pr2serr("ioctl(SG_GET_PACK_ID) failed, errno=%d %s\n",
+                        errno, strerror(errno));
+            else
+                printf("first available pack_id: %d\n", pack_id);
+            if (ioctl(sg_fd, SG_GET_NUM_WAITING, &num_waiting) < 0)
+                pr2serr("ioctl(SG_GET_NUM_WAITING) failed, errno=%d %s\n",
+                        errno, strerror(errno));
+            else
+                printf("num_waiting: %d\n", num_waiting);
+        }
+        memset(&rio_hdr, 0, sizeof(sg_io_hdr_t));
+        rio_hdr.interface_id = 'S';
+        if (read(sg_fd, &rio_hdr, sizeof(sg_io_hdr_t)) < 0) {
+            perror("sg read error");
+            close(sg_fd);
+            return 1;
+        }
+        /* now for the error processing */
+        ok = 0;
+        switch (sg_err_category3(&rio_hdr)) {
+        case SG_LIB_CAT_CLEAN:
+            ok = 1;
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            printf("Recovered error, continuing\n");
+            ok = 1;
+            break;
+        default: /* won't bother decoding other categories */
+            sg_chk_n_print3("command error", &rio_hdr, 1);
+            break;
+        }
+
+        if (ok) { /* output result if it is available */
+            if (0 == (rio_hdr.pack_id % 3))
+                printf("SEND DIAGNOSTIC %d duration=%u\n", rio_hdr.pack_id,
+                       rio_hdr.duration);
+            else
+                printf("INQUIRY %d duration=%u\n", rio_hdr.pack_id,
+                       rio_hdr.duration);
+        }
+    }
+
+out:
+    if (sg_fd >= 0)
+        close(sg_fd);
+    if (sg_fd2 >= 0)
+        close(sg_fd2);
+    return res;
+}
diff --git a/testing/sg_tst_json_builder.c b/testing/sg_tst_json_builder.c
new file mode 100644
index 0000000..0637e30
--- /dev/null
+++ b/testing/sg_tst_json_builder.c
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * Simple streaming JSON writer
+ *
+ * This takes care of the annoying bits of JSON syntax like the commas
+ * after elements
+ *
+ * Authors:     Stephen Hemminger <stephen@networkplumber.org>
+ *
+ * Borrowed from Linux kernel [5.17.0]: tools/bpf/bpftool/json_writer.[hc]
+ */
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <malloc.h>
+#include <inttypes.h>
+#include <stdint.h>
+
+#include "../lib/sg_json_builder.h"
+#include "sg_pr2serr.h"
+
+#define MY_NAME "sg_tst_json_builder"
+
+
+static  json_serialize_opts out_settings = {
+    json_serialize_mode_multiline,
+    0,
+    4
+};
+
+int
+main(int argc, char * argv[])
+{
+    size_t len;
+    sgj_state jstate;
+    sgj_state * jstp = &jstate;
+    json_value * jv1p;
+    json_value * jv2p;
+    json_value * jv3p = json_object_new(0);
+    json_value * jvp = NULL;
+    json_value * jv4p;
+    json_value * jv5p;
+    json_value * ja1p = json_array_new(0);
+    json_value * ja2p;
+    json_value * jsp = json_string_new("hello world 1");
+    json_value * js2p = json_string_new("hello world 2");
+    json_value * js3p = json_string_new("hello world 3");
+    json_value * js10 = json_string_new("good-bye world");
+    json_value * js11 = json_string_new("good-bye world 2");
+    json_value * js12 = json_string_new("duplicate name 1");
+    char b[8192];
+
+    sgj_init_state(jstp, NULL);
+    jvp = sgj_start_r(MY_NAME, "0.02 20220503", argc, argv, jstp);
+    jv1p = json_object_push(jvp, "contents", jsp);
+
+    if (jvp == jv1p)
+        printf("jvp == jv1p\n");
+    else
+        printf("jvp != jv1p\n");
+
+#if 1
+    json_array_push(ja1p, js2p);
+    jv2p = json_object_push(jvp, "extra", js3p);
+    if (jv2p)
+        printf("jv2p->type=%d\n", jv2p->type);
+    else
+        printf("jv2p is NULL\n");
+    ja2p = json_array_push(ja1p, json_string_new(
+                "test double quote, etc: \" world \\ 99\t\ttwo tabs"));
+    if (ja2p)
+        printf("ja2p->type=%d\n", ja2p->type);
+    else
+        printf("ja2p is NULL\n");
+    // json_object_push(ja2p, "boo", json_string_new("hello world 88"));
+    json_object_push(jvp, "a_array", ja1p);
+    jv4p = json_object_push(jvp, "a_object", jv3p);
+    if (jv4p)
+        printf("jv4p->type=%d\n", jv4p->type);
+    else
+        printf("jv4p is NULL\n");
+    json_object_push(jv4p, "test", js10);
+    json_object_push(jv4p, "test2", js11);
+    json_object_push(jv4p, "test", js12);
+    // ja3p = json_array_push(ja2p, json_string_new("good-bye"));
+    // jv4p = json_object_push(jvp, "a_array", ja2p);
+    // jv5p = json_object_merge(jvp, ja1p);
+#endif
+    jv5p = jvp;
+
+    len = json_measure_ex(jv5p, out_settings);
+    printf("jvp length: %zu bytes\n", len);
+    if (len < sizeof(b)) {
+        json_serialize_ex(b, jv5p, out_settings);
+        printf("json serialized:\n");
+        printf("%s\n", b);
+    } else
+        printf("since json output length [%zu] > 8192, skip outputting\n",
+               len);
+
+    json_builder_free(jvp);
+    return 0;
+}
+
+
+#if 0
+int main(int argc, char **argv)
+{
+        json_writer_t *wr = jsonw_new(stdout);
+
+        jsonw_start_object(wr);
+        jsonw_pretty(wr, true);
+        jsonw_name(wr, "Vyatta");
+        jsonw_start_object(wr);
+        jsonw_string_field(wr, "url", "http://vyatta.com");
+        jsonw_uint_field(wr, "downloads", 2000000ul);
+        jsonw_float_field(wr, "stock", 8.16);
+
+        jsonw_name(wr, "ARGV");
+        jsonw_start_array(wr);
+        while (--argc)
+                jsonw_string(wr, *++argv);
+        jsonw_end_array(wr);
+
+        jsonw_name(wr, "empty");
+        jsonw_start_array(wr);
+        jsonw_end_array(wr);
+
+        jsonw_name(wr, "NIL");
+        jsonw_start_object(wr);
+        jsonw_end_object(wr);
+
+        jsonw_null_field(wr, "my_null");
+
+        jsonw_name(wr, "special chars");
+        jsonw_start_array(wr);
+        jsonw_string_field(wr, "slash", "/");
+        jsonw_string_field(wr, "newline", "\n");
+        jsonw_string_field(wr, "tab", "\t");
+        jsonw_string_field(wr, "ff", "\f");
+        jsonw_string_field(wr, "quote", "\"");
+        jsonw_string_field(wr, "tick", "\'");
+        jsonw_string_field(wr, "backslash", "\\");
+        jsonw_end_array(wr);
+
+jsonw_name(wr, "ARGV");
+jsonw_start_array(wr);
+jsonw_string(wr, "boo: appended or new entry?");
+jsonw_end_array(wr);
+
+        jsonw_end_object(wr);
+
+        jsonw_end_object(wr);
+        jsonw_destroy(&wr);
+        return 0;
+}
+
+#endif
diff --git a/testing/sg_tst_nvme.c b/testing/sg_tst_nvme.c
new file mode 100644
index 0000000..4cc696a
--- /dev/null
+++ b/testing/sg_tst_nvme.c
@@ -0,0 +1,957 @@
+/*
+ * Copyright (c) 2018-2021 Douglas Gilbert
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * This program issues a NVMe Identify command (controller or namespace)
+ * or a Device self-test command via the "SCSI" pass-through interface of
+ * this package's sg_utils library. That interface is primarily shown in
+ * the ../include/sg_pt.h header file.
+ *
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <errno.h>
+#include <limits.h>
+#include <getopt.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "sg_lib.h"
+#include "sg_pt.h"
+#include "sg_pt_nvme.h"
+#include "sg_cmds_basic.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+static const char * version_str = "1.07 20210225";
+
+
+#define ME "sg_tst_nvme: "
+
+#define SENSE_BUFF_LEN 32       /* Arbitrary, only need 16 bytes for NVME
+                                 * (and SCSI at least 18) currently */
+#define SENSE_BUFF_NVME_LEN 16  /* 4 DWords, little endian, as byte string */
+
+#define INQUIRY_CMD     0x12    /* SCSI command to get VPD page 0x83 */
+#define INQUIRY_CMDLEN  6
+#define INQUIRY_MAX_RESP_LEN 252
+
+#define VPD_DEVICE_ID  0x83
+
+#define NVME_NSID_ALL  0xffffffff
+
+#define DEF_TIMEOUT_SECS 60
+
+
+static struct option long_options[] = {
+    {"ctl", no_argument, 0, 'c'},
+    {"dev-id", no_argument, 0, 'd'},
+    {"dev_id", no_argument, 0, 'd'},
+    {"help", no_argument, 0, 'h'},
+    {"long", no_argument, 0, 'l'},
+    {"maxlen", required_argument, 0, 'm'},
+    {"nsid", required_argument, 0, 'n'},
+    {"self-test", required_argument, 0, 's'},
+    {"self_test", required_argument, 0, 's'},
+    {"to-ms", required_argument, 0, 't'},
+    {"to_ms", required_argument, 0, 't'},
+    {"verbose", no_argument, 0, 'v'},
+    {"version", no_argument, 0, 'V'},
+    {0, 0, 0, 0},
+};
+
+/* Assume index is less than 16 */
+static const char * sg_ansi_version_arr[16] =
+{
+    "no conformance claimed",
+    "SCSI-1",           /* obsolete, ANSI X3.131-1986 */
+    "SCSI-2",           /* obsolete, ANSI X3.131-1994 */
+    "SPC",              /* withdrawn, ANSI INCITS 301-1997 */
+    "SPC-2",            /* ANSI INCITS 351-2001, ISO/IEC 14776-452 */
+    "SPC-3",            /* ANSI INCITS 408-2005, ISO/IEC 14776-453 */
+    "SPC-4",            /* ANSI INCITS 513-2015 */
+    "SPC-5",
+    "ecma=1, [8h]",
+    "ecma=1, [9h]",
+    "ecma=1, [Ah]",
+    "ecma=1, [Bh]",
+    "reserved [Ch]",
+    "reserved [Dh]",
+    "reserved [Eh]",
+    "reserved [Fh]",
+};
+
+#define MAX_DEV_NAMES 8
+
+static const char * dev_name_arr[MAX_DEV_NAMES] = {
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+};
+
+static int next_dev_name_pos = 0;
+
+
+static void
+usage()
+{
+    pr2serr("Usage: sg_tst_nvme [--ctl] [--dev-id] [--help] [--long] "
+            "[--maxlen=LEN]\n"
+            "                   [--nsid=ID] [--self-test=ST] [--to-ms=TO] "
+            "[--verbose]\n"
+            "                   [--version] DEVICE [DEVICE ...]\n"
+            "  where:\n"
+            "    --ctl|-c             only do Identify controller command\n"
+            "    --dev-id|-d          do SCSI INQUIRY for device "
+            " identification\n"
+            "                         VPD page (0x83) via own SNTL\n"
+            "    --help|-h            print out usage message\n"
+            "    --long|-l            add more detail to decoded output\n"
+            "    --maxlen=LEN| -m LEN    allocation length for SCSI devices\n"
+            "    --nsid=ID| -n ID     do Identify namespace with nsid set to "
+            "ID; if ID\n"
+            "                         is 0 then try to get nsid from "
+            "DEVICE.\n"
+            "                         Can also be used with self-test (def: "
+            "0)\n"
+            "    --self-test=ST|-s ST    do (or abort) device self-test, ST "
+            "can be:\n"
+            "                              0:  do nothing\n"
+            "                              1:  do short (background) "
+            "self-test\n"
+            "                              2:  do long self-test\n"
+            "                              15: abort self-test in "
+            "progress\n"
+            "                         if nsid is 0 then test controller "
+            "only\n"
+            "                         if nsid is 0xffffffff (-1) then test "
+            "controller\n"
+            "                         and all namespaces\n"
+            "    --to-ms=TO|-t TO     command timeout in milliseconds (def: "
+            "60,000)\n"
+            "    --verbose|-v         increase verbosity\n"
+            "    --version|-V         print version string then exit\n\n"
+            "Performs a NVME Identify or Device self-test Admin command on "
+            "each DEVICE.\nCan also simulate a SCSI device identification VPD "
+            "page [0x83] via\na local SNTL. --nsid= accepts '-1' for "
+            "0xffffffff which means all.\n"
+         );
+}
+
+static void
+show_nvme_id_ctl(const uint8_t *dinp, const char *dev_name, int do_long,
+                 uint32_t * max_nsid_p)
+{
+    bool got_fguid;
+    uint8_t ver_min, ver_ter, mtds;
+    uint16_t ver_maj, oacs, oncs;
+    uint32_t k, ver, max_nsid, npss, j, n, m;
+    uint64_t sz1, sz2;
+    const uint8_t * up;
+
+    max_nsid = sg_get_unaligned_le32(dinp + 516); /* NN */
+    if (max_nsid_p)
+        *max_nsid_p = max_nsid;
+    printf("Identify controller for %s:\n", dev_name);
+    printf("  Model number: %.40s\n", (const char *)(dinp + 24));
+    printf("  Serial number: %.20s\n", (const char *)(dinp + 4));
+    printf("  Firmware revision: %.8s\n", (const char *)(dinp + 64));
+    ver = sg_get_unaligned_le32(dinp + 80);
+    ver_maj = (ver >> 16);
+    ver_min = (ver >> 8) & 0xff;
+    ver_ter = (ver & 0xff);
+    printf("  Version: %u.%u", ver_maj, ver_min);
+    if ((ver_maj > 1) || ((1 == ver_maj) && (ver_min > 2)) ||
+        ((1 == ver_maj) && (2 == ver_min) && (ver_ter > 0)))
+        printf(".%u\n", ver_ter);
+    else
+        printf("\n");
+    oacs = sg_get_unaligned_le16(dinp + 256);
+    if (0x1ff & oacs) {
+        printf("  Optional admin command support:\n");
+        if (0x100 & oacs)
+            printf("    Doorbell buffer config\n");
+        if (0x80 & oacs)
+            printf("    Virtualization management\n");
+        if (0x40 & oacs)
+            printf("    NVMe-MI send and NVMe-MI receive\n");
+        if (0x20 & oacs)
+            printf("    Directive send and directive receive\n");
+        if (0x10 & oacs)
+            printf("    Device self-test\n");
+        if (0x8 & oacs)
+            printf("    Namespace management and attachment\n");
+        if (0x4 & oacs)
+            printf("    Firmware download and commit\n");
+        if (0x2 & oacs)
+            printf("    Format NVM\n");
+        if (0x1 & oacs)
+            printf("    Security send and receive\n");
+    } else
+        printf("  No optional admin command support\n");
+    oncs = sg_get_unaligned_le16(dinp + 256);
+    if (0x7f & oncs) {
+        printf("  Optional NVM command support:\n");
+        if (0x40 & oncs)
+            printf("    Timestamp feature\n");
+        if (0x20 & oncs)
+            printf("    Reservations\n");
+        if (0x10 & oncs)
+            printf("    Save and Select fields non-zero\n");
+        if (0x8 & oncs)
+            printf("    Write zeroes\n");
+        if (0x4 & oncs)
+            printf("    Dataset management\n");
+        if (0x2 & oncs)
+            printf("    Write uncorrectable\n");
+        if (0x1 & oncs)
+            printf("    Compare\n");
+    } else
+        printf("  No optional NVM command support\n");
+    printf("  PCI vendor ID VID/SSVID: 0x%x/0x%x\n",
+           sg_get_unaligned_le16(dinp + 0),
+           sg_get_unaligned_le16(dinp + 2));
+    printf("  IEEE OUI Identifier: 0x%x\n",
+           sg_get_unaligned_le24(dinp + 73));
+    got_fguid = ! sg_all_zeros(dinp + 112, 16);
+    if (got_fguid) {
+        printf("  FGUID: 0x%02x", dinp[112]);
+        for (k = 1; k < 16; ++k)
+            printf("%02x", dinp[112 + k]);
+        printf("\n");
+    } else if (do_long)
+        printf("  FGUID: 0x0\n");
+    printf("  Controller ID: 0x%x\n", sg_get_unaligned_le16(dinp + 78));
+    if (do_long) {
+        printf("  Management endpoint capabilities, over a PCIe port: %d\n",
+               !! (0x2 & dinp[255]));
+        printf("  Management endpoint capabilities, over a SMBus/I2C port: "
+               "%d\n", !! (0x1 & dinp[255]));
+    }
+    printf("  Number of namespaces: %u\n", max_nsid);
+    sz1 = sg_get_unaligned_le64(dinp + 280);  /* lower 64 bits */
+    sz2 = sg_get_unaligned_le64(dinp + 288);  /* upper 64 bits */
+    if (sz2)
+        printf("  Total NVM capacity: huge ...\n");
+    else if (sz1)
+        printf("  Total NVM capacity: %" PRIu64 " bytes\n", sz1);
+    mtds = dinp[77];
+    printf("  Maximum data transfer size: ");
+    if (mtds)
+        printf("%u pages\n", 1U << mtds);
+    else
+        printf("<unlimited>\n");
+
+    if (do_long) {
+        const char * const non_op = "does not process I/O";
+        const char * const operat = "processes I/O";
+        const char * cp;
+
+        printf("  Total NVM capacity: 0 bytes\n");
+        npss = dinp[263] + 1;
+        up = dinp + 2048;
+        for (k = 0; k < npss; ++k, up += 32) {
+            n = sg_get_unaligned_le16(up + 0);
+            n *= (0x1 & up[3]) ? 1 : 100;    /* unit: 100 microWatts */
+            j = n / 10;                      /* unit: 1 milliWatts */
+            m = j % 1000;
+            j /= 1000;
+            cp = (0x2 & up[3]) ? non_op : operat;
+            printf("  Power state %u: Max power: ", k);
+            if (0 == j) {
+                m = n % 10;
+                n /= 10;
+                printf("%u.%u milliWatts, %s\n", n, m, cp);
+            } else
+                printf("%u.%03u Watts, %s\n", j, m, cp);
+            n = sg_get_unaligned_le32(up + 4);
+            if (0 == n)
+                printf("    [ENLAT], ");
+            else
+                printf("    ENLAT=%u, ", n);
+            n = sg_get_unaligned_le32(up + 8);
+            if (0 == n)
+                printf("[EXLAT], ");
+            else
+                printf("EXLAT=%u, ", n);
+            n = 0x1f & up[12];
+            printf("RRT=%u, ", n);
+            n = 0x1f & up[13];
+            printf("RRL=%u, ", n);
+            n = 0x1f & up[14];
+            printf("RWT=%u, ", n);
+            n = 0x1f & up[15];
+            printf("RWL=%u\n", n);
+        }
+    }
+}
+
+static const char * rperf[] = {"Best", "Better", "Good", "Degraded"};
+
+static void
+show_nvme_id_ns(const uint8_t * dinp, uint32_t nsid, const char *dev_name,
+                int do_long)
+{
+    bool got_eui_128 = false;
+    uint32_t u, k, off, num_lbaf, flbas, flba_info, md_size, lb_size;
+    uint64_t ns_sz, eui_64;
+
+    printf("Identify namespace %u for %s:\n", nsid, dev_name);
+    num_lbaf = dinp[25] + 1;  /* spec says this is "0's based value" */
+    flbas = dinp[26] & 0xf;   /* index of active LBA format (for this ns) */
+    ns_sz = sg_get_unaligned_le64(dinp + 0);
+    eui_64 = sg_get_unaligned_be64(dinp + 120);  /* N.B. EUI is big endian */
+    if (! sg_all_zeros(dinp + 104, 16))
+        got_eui_128 = true;
+    printf("    Namespace size/capacity: %" PRIu64 "/%" PRIu64
+           " blocks\n", ns_sz, sg_get_unaligned_le64(dinp + 8));
+    printf("    Namespace utilization: %" PRIu64 " blocks\n",
+           sg_get_unaligned_le64(dinp + 16));
+    if (got_eui_128) {          /* N.B. big endian */
+        printf("    NGUID: 0x%02x", dinp[104]);
+        for (k = 1; k < 16; ++k)
+            printf("%02x", dinp[104 + k]);
+        printf("\n");
+    } else if (do_long)
+        printf("    NGUID: 0x0\n");
+    if (eui_64)
+        printf("    EUI-64: 0x%" PRIx64 "\n", eui_64); /* N.B. big endian */
+    printf("    Number of LBA formats: %u\n", num_lbaf);
+    printf("    Index LBA size: %u\n", flbas);
+    for (k = 0, off = 128; k < num_lbaf; ++k, off += 4) {
+        printf("    LBA format %u support:", k);
+        if (k == flbas)
+            printf(" <-- active\n");
+        else
+            printf("\n");
+        flba_info = sg_get_unaligned_le32(dinp + off);
+        md_size = flba_info & 0xffff;
+        lb_size = flba_info >> 16 & 0xff;
+        if (lb_size > 31) {
+            pr2serr("%s: logical block size exponent of %u implies a LB "
+                    "size larger than 4 billion bytes, ignore\n", __func__,
+                    lb_size);
+            continue;
+        }
+        lb_size = 1U << lb_size;
+        ns_sz *= lb_size;
+        ns_sz /= 500*1000*1000;
+        if (ns_sz & 0x1)
+            ns_sz = (ns_sz / 2) + 1;
+        else
+            ns_sz = ns_sz / 2;
+        u = (flba_info >> 24) & 0x3;
+        printf("      Logical block size: %u bytes\n", lb_size);
+        printf("      Approximate namespace size: %" PRIu64 " GB\n", ns_sz);
+        printf("      Metadata size: %u bytes\n", md_size);
+        printf("      Relative performance: %s [0x%x]\n", rperf[u], u);
+    }
+}
+
+/* Invokes a NVMe Admin command via sg_utils library pass-through that will
+ * potentially fetch data from the device (din). Returns 0 -> success,
+ * various SG_LIB_* positive values or negated errno values.
+ * SG_LIB_NVME_STATUS is returned if the NVMe status is non-zero. */
+static int
+nvme_din_admin_cmd(struct sg_pt_base * ptvp, const uint8_t *cmdp,
+                   uint32_t cmd_len, const char *cmd_str, uint8_t *dip,
+                   int di_len, int timeout_ms, uint16_t *sct_scp, int vb)
+{
+    int res, k;
+    uint16_t sct_sc = 0;
+    uint32_t result, clen;
+    uint8_t sense_b[SENSE_BUFF_NVME_LEN] SG_C_CPP_ZERO_INIT;
+    uint8_t ucmd[128];
+    char b[32];
+
+    snprintf(b, sizeof(b), "%s", cmd_str);
+    clen = (cmd_len > sizeof(ucmd)) ? sizeof(ucmd) : cmd_len;
+    memcpy(ucmd, cmdp, clen);
+    if (vb > 1) {
+       pr2serr("    %s cdb:\n", b);
+       hex2stderr(ucmd, clen, -1);
+    }
+    set_scsi_pt_cdb(ptvp, ucmd, clen);
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    if (dip && (di_len > 0))
+        set_scsi_pt_data_in(ptvp, dip, di_len);
+    res = do_scsi_pt(ptvp, -1, -timeout_ms, vb);
+    if (res) {
+        if (res < 0) {
+            res = sg_convert_errno(-res);
+            goto err_out;
+        } else {
+            if (SCSI_PT_DO_BAD_PARAMS == res)
+                pr2serr("%s: bad parameters to do_scsi_pt()\n", __func__);
+            else if (SCSI_PT_DO_TIMEOUT == res)
+                pr2serr("%s: timeout in do_scsi_pt()\n", __func__);
+            else if (SCSI_PT_DO_NVME_STATUS == res) {
+                sct_sc = get_scsi_pt_status_response(ptvp);
+                res = SG_LIB_NVME_STATUS;
+                goto nvme_status_err;
+            } else
+                pr2serr("%s: unknown error (%d) from do_scsi_pt()\n",
+                        __func__, res);
+        }
+        res = SG_LIB_FILE_ERROR;
+        goto err_out;
+    }
+
+    if ((vb > 2) && dip && di_len) {
+        k = get_scsi_pt_resid(ptvp);
+        pr2serr("    Data in buffer [%d bytes]:\n", di_len - k);
+        if (di_len > k)
+            hex2stderr(dip, di_len - k, -1);
+        if (vb > 3)
+            pr2serr("    do_scsi_pt(nvme): res=%d resid=%d\n", res, k);
+    }
+    sct_sc = get_scsi_pt_status_response(ptvp);
+    result = get_pt_result(ptvp);
+    k = get_scsi_pt_sense_len(ptvp);
+    if (vb) {
+        pr2serr("Status: 0x%x [SCT<<8 + SC], Result: 0x%x, Completion Q:\n",
+                sct_sc, result);
+        if (k > 0)
+            hex2stderr(sense_b, k, -1);
+    }
+nvme_status_err:
+    if (sct_scp)
+        *sct_scp = sct_sc;
+err_out:
+    return res;
+}
+
+static void
+std_inq_decode(const char * prefix, uint8_t * b, int len, int vb)
+{
+    int pqual, n;
+
+    if (len < 4)
+        return;
+    pqual = (b[0] & 0xe0) >> 5;
+    if (0 == pqual)
+        printf("%s:\n", prefix);
+    else if (1 == pqual)
+        printf("%s: [qualifier indicates no connected LU]\n", prefix);
+    else if (3 == pqual)
+        printf("%s: [qualifier indicates not capable of supporting LU]\n",
+               prefix);
+    else
+        printf("%s: [reserved or vendor specific qualifier [%d]]\n",
+               prefix, pqual);
+    printf("      PQual=%d  Device_type=%d  RMB=%d  LU_CONG=%d  "
+           "version=0x%02x ", pqual, b[0] & 0x1f, !!(b[1] & 0x80),
+           !!(b[1] & 0x40), (unsigned int)b[2]);
+    printf(" [%s]\n", sg_ansi_version_arr[b[2] & 0xf]);
+    printf("      [AERC=%d]  [TrmTsk=%d]  NormACA=%d  HiSUP=%d "
+           " Resp_data_format=%d\n",
+           !!(b[3] & 0x80), !!(b[3] & 0x40), !!(b[3] & 0x20),
+           !!(b[3] & 0x10), b[3] & 0x0f);
+    if (len < 5)
+        return;
+    n = b[4] + 5;
+    if (vb)
+        pr2serr(">> requested %d bytes, %d bytes available\n", len, n);
+    printf("      SCCS=%d  ACC=%d  TPGS=%d  3PC=%d  Protect=%d ",
+           !!(b[5] & 0x80), !!(b[5] & 0x40), ((b[5] & 0x30) >> 4),
+           !!(b[5] & 0x08), !!(b[5] & 0x01));
+    printf("     [BQue=%d]\n      EncServ=%d  ", !!(b[6] & 0x80),
+           !!(b[6] & 0x40));
+    if (b[6] & 0x10)
+        printf("MultiP=1 (VS=%d)  ", !!(b[6] & 0x20));
+    else
+        printf("MultiP=0  ");
+    printf("[MChngr=%d]  [ACKREQQ=%d]  Addr16=%d\n      [RelAdr=%d]  ",
+           !!(b[6] & 0x08), !!(b[6] & 0x04), !!(b[6] & 0x01),
+           !!(b[7] & 0x80));
+    printf("WBus16=%d  Sync=%d  [Linked=%d]  [TranDis=%d]  ",
+           !!(b[7] & 0x20), !!(b[7] & 0x10), !!(b[7] & 0x08),
+           !!(b[7] & 0x04));
+    printf("CmdQue=%d\n", !!(b[7] & 0x02));
+    if (len < 36)
+        return;
+    printf("      Vendor_identification: %.8s\n", b + 8);
+    printf("      Product_identification: %.16s\n", b + 16);
+    printf("      Product_revision_level: %.4s\n", b + 32);
+}
+
+/* Invokes a SCSI INQUIRY command and yields the response. Returns 0 when
+ * successful, various SG_LIB_CAT_* positive values or -1 -> other errors.
+ * The CMDDT field is obsolete in the INQUIRY cdb (since spc3r16 in 2003) so
+ * an argument to set it has been removed (use the REPORT SUPPORTED OPERATION
+ * CODES command instead). Adds the ability to set the command abort timeout
+ * and the ability to report the residual count. If timeout_secs is zero
+ * the default command abort timeout (60 seconds) is used.
+ * If residp is non-NULL then the residual value is written where residp
+ * points. A residual value of 0 implies mx_resp_len bytes have be written
+ * where resp points. If the residual value equals mx_resp_len then no
+ * bytes have been written. */
+static int
+sg_scsi_inquiry(struct sg_pt_base * ptvp, bool evpd, int pg_op, void * resp,
+                int mx_resp_len, int timeout_secs, int * residp,
+                bool noisy, int vb)
+{
+    int res, ret, k, sense_cat, resid;
+    uint8_t inq_cdb[INQUIRY_CMDLEN] = {INQUIRY_CMD, 0, 0, 0, 0, 0};
+    uint8_t sense_b[SENSE_BUFF_LEN] SG_C_CPP_ZERO_INIT;
+    uint8_t * up;
+
+    if (evpd)
+        inq_cdb[1] |= 1;
+    inq_cdb[2] = (uint8_t)pg_op;
+    sg_put_unaligned_be16((uint16_t)mx_resp_len, inq_cdb + 3);
+    if (vb > 1) {
+        pr2serr("    INQUIRY cdb: ");
+        for (k = 0; k < INQUIRY_CMDLEN; ++k)
+            pr2serr("%02x ", inq_cdb[k]);
+        pr2serr("\n");
+    }
+    if (resp && (mx_resp_len > 0)) {
+        up = (uint8_t *)resp;
+        up[0] = 0x7f;   /* defensive prefill */
+        if (mx_resp_len > 4)
+            up[4] = 0;
+    }
+    if (timeout_secs == 0)
+        timeout_secs = DEF_TIMEOUT_SECS;
+    set_scsi_pt_cdb(ptvp, inq_cdb, sizeof(inq_cdb));
+    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
+    set_scsi_pt_data_in(ptvp, (uint8_t *)resp, mx_resp_len);
+    res = do_scsi_pt(ptvp, -1, timeout_secs, vb);
+    ret = sg_cmds_process_resp(ptvp, "inquiry", res, noisy, vb, &sense_cat);
+    resid = get_scsi_pt_resid(ptvp);
+    if (residp)
+        *residp = resid;
+    if (-1 == ret)
+        ;
+    else if (-2 == ret) {
+        switch (sense_cat) {
+        case SG_LIB_CAT_RECOVERED:
+        case SG_LIB_CAT_NO_SENSE:
+            ret = 0;
+            break;
+        default:
+            ret = sense_cat;
+            break;
+        }
+    } else if (ret < 4) {
+        if (vb)
+            pr2serr("%s: got too few bytes (%d)\n", __func__, ret);
+        ret = SG_LIB_CAT_MALFORMED;
+    } else
+        ret = 0;
+
+    if (resid > 0) {
+        if (resid > mx_resp_len) {
+            pr2serr("INQUIRY resid (%d) should never exceed requested "
+                    "len=%d\n", resid, mx_resp_len);
+            return ret ? ret : SG_LIB_CAT_MALFORMED;
+        }
+        /* zero unfilled section of response buffer */
+        memset((uint8_t *)resp + (mx_resp_len - resid), 0, resid);
+    }
+    return ret;
+}
+
+int
+main(int argc, char * argv[])
+{
+    bool do_all = false;
+    bool do_dev_id_vpd = false;
+    bool do_id_ctl = false;
+    bool do_id_ns = false;
+    bool do_self_test = false;
+    bool flagged = false;
+    bool is_nvme = false;
+    int res, c, n, resid, off, len, ln, k, q, num;
+    int curr_dev_name_pos = 0;
+    int do_long = 0;
+    int maxlen = INQUIRY_MAX_RESP_LEN;
+    int self_test = 0;
+    int sg_fd = -1;
+    int ret = 0;
+    int timeout_ms = DEF_TIMEOUT_SECS * 1000;
+    int vb = 0;
+    uint32_t nsid = 0;
+    uint32_t dn_nsid, al_size;
+    uint32_t pg_sz = sg_get_page_size();
+    int64_t ll;
+    uint8_t * al_buff = NULL;
+    uint8_t * free_al_buff = NULL;
+    uint8_t * bp;
+    const char * device_name = NULL;
+    const char * cp;
+    struct sg_pt_base * ptvp = NULL;
+    char cmd_name[32];
+    char b[2048];
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "cdhlm:n:s:t:vV", long_options,
+                       &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'c':
+            strcpy(cmd_name, "Identify(ctl)");
+            do_id_ctl = true;
+            break;
+        case 'd':
+            strcpy(cmd_name, "INQUIRY(vpd=0x83)");
+            do_dev_id_vpd = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'l':
+            ++do_long;
+            break;
+        case 'm':
+            maxlen = sg_get_num(optarg);
+            if (maxlen < 0) {
+                pr2serr("bad argument to '--maxlen='\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'n':
+            if ((2 == strlen(optarg)) && (0 == memcmp("-1", optarg, 2))) {
+                nsid = NVME_NSID_ALL;      /* treat '-1' as (2**32 - 1) */
+                break;
+            }
+            ll = sg_get_llnum(optarg);
+            if ((ll < 0) || (ll > UINT32_MAX)) {
+                pr2serr("bad argument to '--nsid', accept 0 to 0xffffffff\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            strcpy(cmd_name, "Identify(ns)");
+            nsid = (uint32_t)ll;
+            do_id_ns = true;
+            break;
+        case 's':
+            self_test = sg_get_num(optarg);
+            if (self_test < 0) {
+                pr2serr("bad argument to '--self-test=', expect 0 or "
+                        "higher\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            strcpy(cmd_name, "Device self-test");
+            do_self_test = true;
+            break;
+        case 't':
+            timeout_ms = sg_get_num(optarg);
+            if (timeout_ms < 0) {
+                pr2serr("bad argument to '--to-ms=', expect 0 or higher\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+        case 'v':
+            ++vb;
+            break;
+        case 'V':
+            pr2serr(ME "version: %s\n", version_str);
+            return 0;
+        default:
+            pr2serr("unrecognised option code 0x%x ??\n", c);
+            usage();
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (optind < argc) {
+        for (; optind < argc; ++optind) {
+            if (next_dev_name_pos >= MAX_DEV_NAMES) {
+                pr2serr("Only accepts %d DEVICE names\n", MAX_DEV_NAMES);
+                usage();
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            dev_name_arr[next_dev_name_pos++] = argv[optind];
+       }
+    }
+
+    if (next_dev_name_pos < 1) {
+        pr2serr("Need at least one DEVICE, can have up to %d\n\n",
+                MAX_DEV_NAMES);
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    }
+
+    if (do_self_test && do_id_ns)
+        do_id_ns = false;       /* self-test with DW10 set to nsid */
+    n = (int)do_id_ctl + (int)do_id_ns + (int)do_dev_id_vpd +
+        (int)do_self_test;
+    if (n > 1) {
+        pr2serr("can only have one of --ctl, --dev-id, --nsid= and "
+                "--self-test=\n\n");
+        usage();
+        return SG_LIB_SYNTAX_ERROR;
+    } else if (0 == n) {
+        do_id_ns = true;
+        strcpy(cmd_name, "Identify(ns)");
+    }
+
+    al_size = ((uint32_t)maxlen > pg_sz) ? (uint32_t)maxlen : pg_sz;
+    al_buff = sg_memalign(al_size, pg_sz, &free_al_buff, vb > 3);
+    if (NULL == al_buff) {
+        pr2serr("out of memory allocating page sized buffer (of %u bytes)\n",
+                al_size);
+        return SG_LIB_OS_BASE_ERR + ENOMEM;
+    }
+    device_name = dev_name_arr[curr_dev_name_pos++];
+    sg_fd = sg_cmds_open_device(device_name, false /* rw */, vb);
+    if (sg_fd < 0) {
+        pr2serr(ME "open error: %s: %s\n", device_name, safe_strerror(-sg_fd));
+        ret = SG_LIB_FILE_ERROR;
+        flagged = true;
+        goto fini;
+    }
+    n = check_pt_file_handle(sg_fd, device_name, vb);
+    if (n < 0) {
+        pr2serr("check_pt_file_handle error: %s: %s\n", device_name,
+                safe_strerror(-n));
+        flagged = true;
+        goto fini;
+    }
+    cp = NULL;
+    switch (n) {
+    case 0:
+        cp = "Unidentified device (SATA disk ?)";
+        break;
+    case 1:
+        cp = "SCSI char device (e.g. in Linux: sg or bsg device)";
+        break;
+    case 2:
+        cp = "SCSI block device (e.g. in FreeBSD: /dev/da0)";
+        break;
+    case 3:
+        cp = "NVMe char device (e.g. in Linux: /dev/nvme0)";
+        break;
+    case 4:
+        cp = "NVMe block device (e.g. in FreeBSD: /dev/nvme0ns1)";
+        break;
+    default:
+        pr2serr("Strange value from check_pt_file_handle() --> %d\n", n);
+        break;
+    }
+    if (cp && (vb || (do_long > 0)))
+        pr2serr("%s\n", cp);
+
+    ptvp = construct_scsi_pt_obj_with_fd(sg_fd, vb);
+    if (NULL == ptvp) {
+        pr2serr("%s: out of memory\n", b);
+        ret = sg_convert_errno(ENOMEM);
+        goto fini;
+    }
+    k = get_scsi_pt_os_err(ptvp);
+    if (k) {
+        pr2serr("OS error from construct_scsi_pt_obj_with_fd(): %s\n",
+                safe_strerror(k));
+        ret = sg_convert_errno(k);
+        goto fini;
+    }
+
+    /* Loop over all given DEVICEs */
+    for (q = 0; q < MAX_DEV_NAMES; ++q) {
+        is_nvme = pt_device_is_nvme(ptvp);
+        if ((curr_dev_name_pos > 1) && vb)
+            pr2serr("Device %d [%s] seems to be %s\n", q + 1, device_name,
+                    is_nvme ? "NVMe" : "SCSI or ATA");
+        resid = 0;
+        if (do_dev_id_vpd || (! is_nvme)) {
+            if (do_dev_id_vpd)
+                ret = sg_scsi_inquiry(ptvp, true /* evpd */, VPD_DEVICE_ID,
+                                      al_buff, maxlen, timeout_ms / 1000,
+                                      &resid, true, vb);
+            else    /* do a standard INQUIRY */
+                ret = sg_scsi_inquiry(ptvp, false /* evpd */, 0, al_buff,
+                                      maxlen, timeout_ms / 1000, &resid, true,
+                                      vb);
+            if (ret) {
+                pr2serr("SCSI INQUIRY(%s) failed\n",
+                        do_dev_id_vpd ? "dev_id" : "standard");
+                goto fini;
+            }
+            len = maxlen - resid;
+            if (len < 4) {
+                pr2serr("Something wrong with data-in, len=%d (resid=%d)\n",
+                        len, resid);
+                goto fini;
+            }
+            if (do_dev_id_vpd) {
+                printf("    Device %d [%s] identification VPD:\n", q + 1,
+                       device_name);
+                for (off = -1, bp = al_buff + 4, ln = len - 4;
+                     0 == sg_vpd_dev_id_iter(bp, ln, &off, -1, -1, -1); ) {
+                    n = sg_get_designation_descriptor_str("    ", bp + off,
+                                                bp[off + 3] + 4, do_long,
+                                                do_long > 1, sizeof(b), b);
+                    if (n > 0)
+                        printf("%s", b);
+                }
+            } else {
+                snprintf(b, sizeof(b), "    Device %d [%s] Standard INQUIRY:",
+                         q + 1, device_name);
+                std_inq_decode(b, al_buff, len, vb);
+            }
+            clear_scsi_pt_obj(ptvp);
+        } else { /* NVME Identify or Device self-test */
+            bool this_ctl = false;
+            uint16_t sct_sc = 0;
+            uint32_t max_nsid;
+            struct sg_nvme_passthru_cmd n_cmd;
+
+            if ((! do_self_test) && (NVME_NSID_ALL == nsid))
+                do_all = true;
+            num = 1;        /* preliminary, may alter */
+            for (k = 0; k < num; ++k) {
+                bp = (uint8_t *)&n_cmd;
+                memset(bp, 0, sizeof(n_cmd));
+                if (do_self_test) {
+                    n_cmd.opcode = 0x14;   /* Device self-test */
+                    n_cmd.nsid = nsid;
+                    n_cmd.cdw10 = self_test;
+                    if (0 == k) {
+                        if (0 == nsid)
+                            printf("Starting Device self-test for controller "
+                                   "only\n");
+                        else if (do_all)
+                            printf("Starting Device self-test for controller "
+                                   "and all namespaces\n");
+                        else
+                            printf("Starting Device self-test for controller "
+                                   "and namespace %u\n", nsid);
+                    }
+                } else {    /* one or more variants of Identify */
+                    n_cmd.opcode = 0x6;   /* Identify */
+                    dn_nsid = get_pt_nvme_nsid(ptvp);
+                    if ((0 == k) && (do_id_ctl || (0 == nsid) || do_all)) {
+                        n_cmd.cdw10 = 0x1;      /* Controller */
+                        this_ctl = true;
+                    } else {
+                        n_cmd.cdw10 = 0x0;      /* Namespace */
+                        if (do_all)
+                            n_cmd.nsid = k;
+                        else if (nsid > 0)
+                            n_cmd.nsid = nsid;
+                        else if (dn_nsid > 0)
+                            n_cmd.nsid = dn_nsid;
+                        else
+                            break;
+                        this_ctl = false;
+                    }
+                    sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)al_buff,
+                                          bp + SG_NVME_PT_ADDR);
+                    sg_put_unaligned_le32(pg_sz, bp + SG_NVME_PT_DATA_LEN);
+                }
+                ret = nvme_din_admin_cmd(ptvp, (const uint8_t *)&n_cmd,
+                                         sizeof(n_cmd), cmd_name, al_buff,
+                                         pg_sz, timeout_ms, &sct_sc, vb);
+                if (sct_sc || (SG_LIB_NVME_STATUS == ret)) {
+                    sg_get_nvme_cmd_status_str(sct_sc, sizeof(b), b);
+                    pr2serr("%s: %s\n", cmd_name, b);
+                        flagged = true;
+                    goto fini;
+                }
+                if (ret)
+                    goto fini;
+                if (0x6 == n_cmd.opcode) {
+                    if (this_ctl) {
+                        show_nvme_id_ctl(al_buff, device_name, do_long,
+                                         &max_nsid);
+                        num = max_nsid + 1;
+                    } else
+                        show_nvme_id_ns(al_buff, n_cmd.nsid, device_name,
+                                        do_long);
+                }
+
+                clear_scsi_pt_obj(ptvp);
+                if (do_self_test)
+                    break;
+                if (do_id_ctl)
+                    break;
+            }       /* end of for loop */
+        }
+        ret = 0;
+
+        if (sg_fd >= 0) {
+            res = sg_cmds_close_device(sg_fd);
+            if (res < 0) {
+                pr2serr("close error: %s\n", safe_strerror(-res));
+                ret = sg_convert_errno(-res);
+                break;
+            }
+            sg_fd = -1;
+        }
+        if (ret)
+            break;
+        if (curr_dev_name_pos < next_dev_name_pos)
+            device_name = dev_name_arr[curr_dev_name_pos++];
+        else
+            break;
+        if (NULL == device_name) {
+            pr2serr("Unexpected NULL device name at pos=%d\n",
+                    curr_dev_name_pos - 1);
+            ret = sg_convert_errno(EINVAL);
+            flagged = true;
+            break;
+        }
+        sg_fd = sg_cmds_open_device(device_name, false /* rw */, vb);
+        if (sg_fd < 0) {
+            pr2serr(ME "open error: %s: %s\n", device_name,
+                    safe_strerror(-sg_fd));
+            ret = sg_convert_errno(-sg_fd);
+            flagged = true;
+            break;
+        }
+        k = set_pt_file_handle(ptvp, sg_fd, vb);
+        if (k) {
+            ret = sg_convert_errno(k);
+            pr2serr("set_pt_file_handle() failed: %s\n", safe_strerror(k));
+            flagged = true;
+            break;
+        }
+        printf("\n");
+    }   /* end of "q" outer for loop */
+fini:
+    if (ptvp) {
+        destruct_scsi_pt_obj(ptvp);
+        ptvp = NULL;
+    }
+    if (free_al_buff)
+        free(free_al_buff);
+    if (sg_fd >= 0) {
+        res = sg_cmds_close_device(sg_fd);
+        if (res < 0) {
+            pr2serr("close error: %s\n", safe_strerror(-res));
+            if (0 == ret)
+                return SG_LIB_FILE_ERROR;
+        }
+    }
+    if (ret && (0 == vb) && (! flagged)) {
+        if (! sg_if_can2stderr("", ret))
+            pr2serr("Some error occurred [%d]\n", ret);
+    }
+    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+}
diff --git a/testing/sgh_dd.cpp b/testing/sgh_dd.cpp
new file mode 100644
index 0000000..b0704ff
--- /dev/null
+++ b/testing/sgh_dd.cpp
@@ -0,0 +1,5090 @@
+/*
+ * A utility program for copying files. Specialised for "files" that
+ * represent devices that understand the SCSI command set.
+ *
+ * Copyright (C) 2018-2022 D. Gilbert
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is a specialisation of the Unix "dd" command in which
+ * one or both of the given files is a scsi generic device.
+ * A logical block size ('bs') is assumed to be 512 if not given. This
+ * program complains if 'ibs' or 'obs' are given with some other value
+ * than 'bs'. If 'if' is not given or 'if=-' then stdin is assumed. If
+ * 'of' is not given or 'of=-' then stdout assumed.
+ *
+ * A non-standard argument "bpt" (blocks per transfer) is added to control
+ * the maximum number of blocks in each transfer. The default value is 128.
+ * For example if "bs=512" and "bpt=32" then a maximum of 32 blocks (16 KiB
+ * in this case) are transferred to or from the sg device in a single SCSI
+ * command.
+ *
+ * This version is designed for the linux kernel 2.4, 2.6, 3, 4 and 5 series.
+ *
+ * sgp_dd is a Posix threads specialization of the sg_dd utility. Both
+ * sgp_dd and sg_dd only perform special tasks when one or both of the given
+ * devices belong to the Linux sg driver.
+ *
+ * sgh_dd further extends sgp_dd to use the experimental kernel buffer
+ * sharing feature added in 3.9.02 .
+ * N.B. This utility was previously called sgs_dd but there was already an
+ * archived version of a dd variant called sgs_dd so this utility name was
+ * renamed [20181221]
+ */
+
+static const char * version_str = "2.22 20221020";
+
+#define _XOPEN_SOURCE 600
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1
+#endif
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <poll.h>
+#include <limits.h>
+#include <pthread.h>
+#include <signal.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#ifndef major
+#include <sys/types.h>
+#endif
+#include <sys/time.h>
+#include <linux/major.h>        /* for MEM_MAJOR, SCSI_GENERIC_MAJOR, etc */
+#include <linux/fs.h>           /* for BLKSSZGET and friends */
+#include <sys/mman.h>           /* for mmap() system call */
+
+#include <vector>
+#include <array>
+#include <atomic>       // C++ header replacing <stdatomic.h> also link
+                        // needed '-l atomic' . Not anymore??
+#include <random>
+#include <thread>       // needed for std::this_thread::yield()
+#include <mutex>
+#include <chrono>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifdef HAVE_GETRANDOM
+#include <sys/random.h>         /* for getrandom() system call */
+#endif
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_cmds_basic.h"
+#include "sg_io_linux.h"
+#include "sg_unaligned.h"
+#include "sg_pr2serr.h"
+
+
+using namespace std;
+
+#ifdef __GNUC__
+#ifndef  __clang__
+#pragma GCC diagnostic ignored "-Wclobbered"
+#endif
+#endif
+
+/* comment out following line to stop ioctl(SG_CTL_FLAGM_SNAP_DEV) */
+#define SGH_DD_SNAP_DEV 1
+
+#ifndef SGV4_FLAG_POLLED
+#define SGV4_FLAG_POLLED 0x800
+#endif
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BLOCKS_PER_TRANSFER 128
+#define DEF_BLOCKS_PER_2048TRANSFER 32
+#define DEF_SDT_ICT_MS 300
+#define DEF_SDT_CRT_SEC 3
+#define DEF_SCSI_CDBSZ 10
+#define MAX_SCSI_CDBSZ 16
+#define MAX_BPT_VALUE (1 << 24)         /* used for maximum bs as well */
+#define MAX_COUNT_SKIP_SEEK (1LL << 48) /* coverity wants upper bound */
+
+#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
+#define READ_CAP_REPLY_LEN 8
+#define RCAP16_REPLY_LEN 32
+
+#define DEF_TIMEOUT 60000       /* 60,000 millisecs == 60 seconds */
+
+#define SGP_READ10 0x28
+#define SGP_PRE_FETCH10 0x34
+#define SGP_PRE_FETCH16 0x90
+#define SGP_VERIFY10 0x2f
+#define SGP_WRITE10 0x2a
+#define DEF_NUM_THREADS 4
+#define MAX_NUM_THREADS 1024 /* was SG_MAX_QUEUE with v3 driver */
+#define DEF_NUM_MRQS 0
+
+#define FT_OTHER 1              /* filetype other than one of the following */
+#define FT_SG 2                 /* filetype is sg char device */
+#define FT_DEV_NULL 4           /* either /dev/null, /dev/zero or "." */
+#define FT_ST 8                 /* filetype is st char device (tape) */
+#define FT_CHAR 16              /* filetype is st char device (tape) */
+#define FT_BLOCK 32             /* filetype is a block device */
+#define FT_FIFO 64              /* fifo (named or unnamed pipe (stdout)) */
+#define FT_RANDOM_0_FF 128      /* iflag=00, iflag=ff and iflag=random
+                                   override if=IFILE */
+#define FT_ERROR 256            /* couldn't "stat" file */
+
+#define DEV_NULL_MINOR_NUM 3
+#define DEV_ZERO_MINOR_NUM 5
+
+#define EBUFF_SZ 768
+
+#define PROC_SCSI_SG_VERSION "/proc/scsi/sg/version"
+#define SYS_SCSI_SG_VERSION "/sys/module/sg/version"
+
+struct flags_t {
+    bool append;
+    bool coe;
+    bool defres;        /* without this res_sz==bs*bpt */
+    bool dio;
+    bool direct;
+    bool dpo;
+    bool dsync;
+    bool excl;
+    bool ff;
+    bool fua;
+    bool polled;	/* formerly called 'hipri' */
+    bool masync;        /* more async sg v4 driver flag */
+    bool mrq_immed;     /* mrq submit non-blocking */
+    bool mrq_svb;       /* mrq shared_variable_block, for sg->sg copy */
+    bool no_dur;
+    bool nocreat;
+    bool noshare;
+    bool no_thresh;
+    bool no_unshare;    /* leave it for driver close/release */
+    bool no_waitq;      /* dummy, no longer supported, just warn */
+    bool noxfer;
+    bool qhead;
+    bool qtail;
+    bool random;
+    bool mout_if;       /* META_OUT_IF flag at mrq level */
+    bool same_fds;
+    bool swait;         /* now ignore; kept for backward compatibility */
+    bool v3;
+    bool v4;
+    bool v4_given;
+    bool wq_excl;
+    bool zero;
+    int mmap;
+};
+
+struct global_collection
+{       /* one instance visible to all threads */
+    int infd;
+    int64_t skip;
+    int in_type;
+    int cdbsz_in;
+    int help;
+    int elem_sz;
+    struct flags_t in_flags;
+    // int64_t in_blk;                /* -\ next block address to read */
+    // int64_t in_count;              /*  | blocks remaining for next read */
+    atomic<int64_t> in_rem_count;     /*  | count of remaining in blocks */
+    atomic<int> in_partial;           /*  | */
+    atomic<bool> in_stop;             /*  | */
+    off_t in_st_size;                 /* Only for FT_OTHER (regular) file */
+    pthread_mutex_t in_mutex;         /* -/ */
+    int nmrqs;                        /* Number of multi-reqs for sg v4 */
+    int outfd;
+    int64_t seek;
+    int out_type;
+    int out2fd;
+    int out2_type;
+    int cdbsz_out;
+    int aen;                          /* abort every nth command */
+    int m_aen;                        /* abort mrq every nth command */
+    struct flags_t out_flags;
+    atomic<int64_t> out_blk;          /* -\ next block address to write */
+    atomic<int64_t> out_count;        /*  | blocks remaining for next write */
+    atomic<int64_t> out_rem_count;    /*  | count of remaining out blocks */
+    atomic<int> out_partial;          /*  | */
+    atomic<bool> out_stop;            /*  | */
+    off_t out_st_size;                /* Only for FT_OTHER (regular) file */
+    pthread_mutex_t out_mutex;        /*  | */
+    pthread_cond_t out_sync_cv;       /*  | hold writes until "in order" */
+    pthread_mutex_t out2_mutex;
+    int bs;
+    int bpt;
+    int cmd_timeout;            /* in milliseconds */
+    int outregfd;
+    int outreg_type;
+    int ofsplit;
+    atomic<int> dio_incomplete_count;
+    atomic<int> sum_of_resids;
+    uint32_t sdt_ict; /* stall detection; initial check time (milliseconds) */
+    uint32_t sdt_crt; /* check repetition time (seconds), after first stall */
+    int fail_mask;
+    int verbose;
+    int dry_run;
+    int chkaddr;
+    bool aen_given;
+    bool cdbsz_given;
+    bool is_mrq_i;
+    bool is_mrq_o;
+    bool m_aen_given;
+    bool ofile_given;
+    bool ofile2_given;
+    bool unit_nanosec;          /* default duration unit is millisecond */
+    bool mrq_cmds;              /* mrq=<NRQS>,C  given */
+    bool mrq_async;             /* mrq_immed flag given */
+    bool noshare;               /* don't use request sharing */
+    bool unbalanced_mrq;        /* so _not_ sg->sg request sharing sync mrq */
+    bool verify;                /* don't copy, verify like Unix: cmp */
+    bool prefetch;              /* for verify: do PF(b),RD(a),V(b)_a_data */
+    bool unshare;               /* let close() do file unshare operation */
+    const char * infp;
+    const char * outfp;
+    const char * out2fp;
+};
+
+typedef struct mrq_abort_info
+{
+    int from_tid;
+    int fd;
+    int mrq_id;
+    int debug;
+} Mrq_abort_info;
+
+typedef struct request_element
+{       /* one instance per worker thread */
+    struct global_collection *clp;
+    bool wr;
+    bool has_share;
+    bool both_sg;
+    bool same_sg;
+    bool only_in_sg;
+    bool only_out_sg;
+    // bool mrq_abort_thread_active;
+    int id;
+    int bs;
+    int infd;
+    int outfd;
+    int out2fd;
+    int outregfd;
+    int64_t iblk;
+    int64_t oblk;
+    int num_blks;
+    uint8_t * buffp;
+    uint8_t * alloc_bp;
+    struct sg_io_hdr io_hdr;
+    struct sg_io_v4 io_hdr4[2];
+    uint8_t cmd[MAX_SCSI_CDBSZ];
+    uint8_t sb[SENSE_BUFF_LEN];
+    int dio_incomplete_count;
+    int mmap_active;
+    int resid;
+    int rd_p_id;
+    int rep_count;
+    int rq_id;
+    int mmap_len;
+    int mrq_id;
+    int mrq_index;
+    uint32_t in_mrq_q_blks;
+    uint32_t out_mrq_q_blks;
+    long seed;
+#ifdef HAVE_SRAND48_R   /* gcc extension. N.B. non-reentrant version slower */
+    struct drand48_data drand;/* opaque, used by srand48_r and mrand48_r */
+#endif
+    pthread_t mrq_abort_thread_id;
+    Mrq_abort_info mai;
+} Rq_elem;
+
+typedef struct thread_info
+{
+    int id;
+    struct global_collection * gcp;
+    pthread_t a_pthr;
+} Thread_info;
+
+/* Additional parameters for sg_start_io() and sg_finish_io() */
+struct sg_io_extra {
+    bool is_wr2;
+    bool prefetch;
+    bool dout_is_split;
+    int hpv4_ind;
+    int blk_offset;
+    int blks;
+};
+
+#define MONO_MRQ_ID_INIT 0x10000
+
+// typedef vector< pair<int, struct sg_io_v4> > mrq_arr_t;
+typedef array<uint8_t, 32> big_cdb;     /* allow up to a 32 byte cdb */
+typedef pair< vector<struct sg_io_v4>, vector<big_cdb> >   mrq_arr_t;
+
+
+/* Use this class to wrap C++11 <random> features to produce uniform random
+ * unsigned ints in the range [lo, hi] (inclusive) given a_seed */
+class Rand_uint {
+public:
+    Rand_uint(unsigned int lo, unsigned int hi, unsigned int a_seed)
+        : uid(lo, hi), dre(a_seed) { }
+    /* uid ctor takes inclusive range when integral type */
+
+    unsigned int get() { return uid(dre); }
+
+private:
+    uniform_int_distribution<unsigned int> uid;
+    default_random_engine dre;
+};
+
+static atomic<int> mono_pack_id(1);
+static atomic<int> mono_mrq_id(MONO_MRQ_ID_INIT);
+static atomic<long int> pos_index(0);
+
+static atomic<int> num_ebusy(0);
+static atomic<int> num_start_eagain(0);
+static atomic<int> num_fin_eagain(0);
+static atomic<int> num_abort_req(0);
+static atomic<int> num_abort_req_success(0);
+static atomic<int> num_mrq_abort_req(0);
+static atomic<int> num_mrq_abort_req_success(0);
+static atomic<int> num_miscompare(0);
+static atomic<long> num_waiting_calls(0);
+static atomic<bool> vb_first_time(true);
+static atomic<bool> shutting_down(false);
+
+static sigset_t signal_set;
+static sigset_t orig_signal_set;
+static pthread_t sig_listen_thread_id;
+
+static const char * sg_allow_dio = "/sys/module/sg/parameters/allow_dio";
+
+static void sg_in_rd_cmd(struct global_collection * clp, Rq_elem * rep,
+                         mrq_arr_t & def_arr);
+static void sg_out_wr_cmd(Rq_elem * rep, mrq_arr_t & def_arr, bool is_wr2,
+                          bool prefetch);
+static bool normal_in_rd(Rq_elem * rep, int blocks);
+static void normal_out_wr(Rq_elem * rep, int blocks);
+static int sg_start_io(Rq_elem * rep, mrq_arr_t & def_arr, int & pack_id,
+                       struct sg_io_extra *xtrp);
+static int sg_finish_io(bool wr, Rq_elem * rep, int pack_id,
+                        struct sg_io_extra *xtrp);
+static int sg_in_open(struct global_collection *clp, const char *inf,
+                      uint8_t **mmpp, int *mmap_len);
+static int sg_out_open(struct global_collection *clp, const char *outf,
+                       uint8_t **mmpp, int *mmap_len);
+static int sgh_do_deferred_mrq(Rq_elem * rep, mrq_arr_t & def_arr);
+
+#define STRERR_BUFF_LEN 128
+
+static pthread_mutex_t strerr_mut = PTHREAD_MUTEX_INITIALIZER;
+
+static bool have_sg_version = false;
+static int sg_version = 0;
+static bool sg_version_lt_4 = false;
+static bool sg_version_ge_40045 = false;
+static bool do_sync = false;
+static int do_time = 1;
+static struct global_collection gcoll;
+static struct timeval start_tm;
+static int64_t dd_count = -1;
+static int num_threads = DEF_NUM_THREADS;
+static int exit_status = 0;
+static bool after1 = false;
+
+static const char * my_name = "sgh_dd: ";
+
+static const char * mrq_blk_s = "mrq: ordinary blocking";
+static const char * mrq_vb_s = "mrq: variable blocking";
+static const char * mrq_svb_s = "mrq: shared variable blocking (svb)";
+static const char * mrq_s_nb_s = "mrq: submit of full non-blocking";
+
+
+#ifdef __GNUC__
+static int pr2serr_lk(const char * fmt, ...)
+        __attribute__ ((format (printf, 1, 2)));
+#if 0
+static void pr_errno_lk(int e_no, const char * fmt, ...)
+        __attribute__ ((format (printf, 2, 3)));
+#endif
+#else
+static int pr2serr_lk(const char * fmt, ...);
+#if 0
+static void pr_errno_lk(int e_no, const char * fmt, ...);
+#endif
+#endif
+
+
+static int
+pr2serr_lk(const char * fmt, ...)
+{
+    int n;
+    va_list args;
+
+    pthread_mutex_lock(&strerr_mut);
+    va_start(args, fmt);
+    n = vfprintf(stderr, fmt, args);
+    va_end(args);
+    pthread_mutex_unlock(&strerr_mut);
+    return n;
+}
+
+static void
+usage(int pg_num)
+{
+    if (pg_num > 3)
+        goto page4;
+    else if (pg_num > 2)
+        goto page3;
+    else if (pg_num > 1)
+        goto page2;
+
+    pr2serr("Usage: sgh_dd  [bs=BS] [conv=CONVS] [count=COUNT] [ibs=BS] "
+            "[if=IFILE]\n"
+            "               [iflag=FLAGS] [obs=BS] [of=OFILE] [oflag=FLAGS] "
+            "[seek=SEEK]\n"
+            "               [skip=SKIP] [--help] [--version]\n\n");
+    pr2serr("               [ae=AEN[,MAEN]] [bpt=BPT] [cdbsz=6|10|12|16] "
+            "[coe=0|1]\n"
+            "               [dio=0|1] [elemsz_kb=EKB] [fail_mask=FM] "
+            "[fua=0|1|2|3]\n"
+            "               [mrq=[I|O,]NRQS[,C]] [noshare=0|1] "
+            "[of2=OFILE2]\n"
+            "               [ofreg=OFREG] [ofsplit=OSP] [sdt=SDT] "
+            "[sync=0|1]\n"
+            "               [thr=THR] [time=0|1|2[,TO]] [unshare=1|0] "
+            "[verbose=VERB]\n"
+            "               [--dry-run] [--prefetch] [-v|-vv|-vvv] "
+            "[--verbose]\n"
+            "               [--verify] [--version]\n\n"
+            "  where the main options (shown in first group above) are:\n"
+            "    bs          must be device logical block size (default "
+            "512)\n"
+            "    conv        comma separated list from: [nocreat,noerror,"
+            "notrunc,\n"
+            "                null,sync]\n"
+            "    count       number of blocks to copy (def: device size)\n"
+            "    if          file or device to read from (def: stdin)\n"
+            "    iflag       comma separated list from: [00,coe,defres,dio,"
+            "direct,dpo,\n"
+            "                dsync,excl,ff,fua,masync,mmap,mout_if,"
+            "mrq_immed,mrq_svb,\n"
+            "                nocreat,nodur,noxfer,null,polled,qhead,"
+            "qtail,\n"
+            "                random,same_fds,v3,v4,wq_excl]\n"
+            "    of          file or device to write to (def: /dev/null "
+            "N.B. different\n"
+            "                from dd it defaults to stdout). If 'of=.' "
+            "uses /dev/null\n"
+            "    of2         second file or device to write to (def: "
+            "/dev/null)\n"
+            "    oflag       comma separated list from: [append,<<list from "
+            "iflag>>]\n"
+            "    seek        block position to start writing to OFILE\n"
+            "    skip        block position to start reading from IFILE\n"
+            "    --help|-h      output this usage message then exit\n"
+            "    --verify|-x    do a verify (compare) operation [def: do a "
+            "copy]\n"
+            "    --version|-V   output version string then exit\n\n"
+            "Copy IFILE to OFILE, similar to dd command. This utility is "
+            "specialized for\nSCSI devices and uses multiple POSIX threads. "
+            "It expects one or both IFILE\nand OFILE to be sg devices. With "
+            "--verify option does a verify/compare\noperation instead of a "
+            "copy. This utility is Linux specific and uses the\nv4 sg "
+            "driver 'share' capability if available. Use '-hh', '-hhh' or "
+            "'-hhhh'\nfor more information.\n"
+           );
+    return;
+page2:
+    pr2serr("Syntax:  sgh_dd [operands] [options]\n\n"
+            "  where: operands have the form name=value and are pecular to "
+            "'dd'\n"
+            "         style commands, and options start with one or "
+            "two hyphens;\n"
+            "         the lesser used operands and option are:\n\n"
+            "    ae          AEN: abort every n commands (def: 0 --> don't "
+            "abort any)\n"
+            "                MAEN: abort every n mrq commands (def: 0 --> "
+            "don't)\n"
+            "                [requires commands with > 1 ms duration]\n"
+            "    bpt         is blocks_per_transfer (default is 128)\n"
+            "    cdbsz       size of SCSI READ, WRITE or VERIFY cdb_s "
+            "(default is 10)\n"
+            "    coe         continue on error, 0->exit (def), "
+            "1->zero + continue\n"
+            "    dio         is direct IO, 1->attempt, 0->indirect IO (def)\n"
+            "    elemsz_kb    scatter gather list element size in kilobytes "
+            "(def: 32[KB])\n"
+            "    fail_mask    1: misuse KEEP_SHARE flag; 0: nothing (def)\n"
+            "    fua         force unit access: 0->don't(def), 1->OFILE, "
+            "2->IFILE,\n"
+            "                3->OFILE+IFILE\n"
+            "    mrq         number of cmds placed in each sg call "
+            "(def: 0);\n"
+            "                may have trailing ',C', to send bulk cdb_s; "
+            "if preceded\n"
+            "                by 'I' then mrq only on IFILE, likewise 'O' "
+            "for OFILE\n"
+            "    noshare     0->use request sharing(def), 1->don't\n"
+            "    ofreg       OFREG is regular file or pipe to send what is "
+            "read from\n"
+            "                IFILE in the first half of each shared element\n"
+            "    ofsplit     split ofile write in two at block OSP (def: 0 "
+            "(no split))\n"
+            "    sdt         stall detection times: CRT[,ICT]. CRT: check "
+            "repetition\n"
+            "                time (after first) in seconds; ICT: initial "
+            "check time\n"
+            "                in milliseconds. Default: 3,300 . Use CRT=0 "
+            "to disable\n"
+            "    sync        0->no sync(def), 1->SYNCHRONIZE CACHE on OFILE "
+            "after copy\n"
+            "    thr         is number of threads, must be > 0, default 4, "
+            "max 1024\n"
+            "    time        0->no timing, 1->calc throughput(def), "
+            "2->nanosec\n"
+            "                precision; TO is command timeout in seconds "
+            "(def: 60)\n"
+            "    unshare     0->don't explicitly unshare after share; 1->let "
+            "close do\n"
+            "                file unshare (default)\n"
+            "    verbose     increase verbosity\n"
+            "    --chkaddr|-c    exits if read block does not contain "
+            "32 bit block\n"
+            "                    address, used once only checks first "
+            "address in block\n"
+            "    --dry-run|-d    prepare but bypass copy/read\n"
+            "    --prefetch|-p    with verify: do pre-fetch first\n"
+            "    --verbose|-v   increase verbosity of utility\n\n"
+            "Use '-hhh' or '-hhhh' for more information about flags.\n"
+           );
+    return;
+page3:
+    pr2serr("Syntax:  sgh_dd [operands] [options]\n\n"
+            "  where: 'iflag=<arg>' and 'oflag=<arg>' arguments are listed "
+            "below:\n\n"
+            "    00          use all zeros instead of if=IFILE (only in "
+            "iflags)\n"
+            "    00,ff       generates blocks that contain own (32 bit be) "
+            "blk address\n"
+            "    append      append output to OFILE (assumes OFILE is "
+            "regular file)\n"
+            "    coe         continue of error (reading, fills with zeros)\n"
+            "    defres      keep default reserve buffer size (else its "
+            "bs*bpt)\n"
+            "    dio         sets the SG_FLAG_DIRECT_IO in sg requests\n"
+            "    direct      sets the O_DIRECT flag on open()\n"
+            "    dpo         sets the DPO (disable page out) in SCSI READs "
+            "and WRITEs\n"
+            "    dsync       sets the O_SYNC flag on open()\n"
+            "    excl        sets the O_EXCL flag on open()\n"
+            "    ff          use all 0xff bytes instead of if=IFILE (only in "
+            "iflags)\n"
+            "    fua         sets the FUA (force unit access) in SCSI READs "
+            "and WRITEs\n"
+            "    hipri       same as 'polled'; 'hipri' name is deprecated\n"
+            "    masync      set 'more async' flag on this sg device\n"
+            "    mmap        setup mmap IO on IFILE or OFILE; OFILE only "
+            "with noshare\n"
+            "    mmap,mmap    when used twice, doesn't call munmap()\n"
+            "    mout_if     set META_OUT_IF flag on each request\n"
+            "    mrq_immed    if mrq active, do submit non-blocking (def: "
+            "ordered\n"
+            "                 blocking)\n"
+            "    mrq_svb     if mrq and sg->sg copy, do shared_variable_"
+            "blocking\n"
+            "    nocreat     will fail rather than create OFILE\n"
+            "    nodur       turns off command duration calculations\n"
+            "    noxfer      no transfer to/from the user space\n"
+            "    no_thresh   skip checking per fd max data xfer\n"
+            "    null        does nothing, placeholder\n"
+            "    polled      set POLLED flag on command, uses blk_poll() to "
+            "complete\n"
+            "    qhead       queue new request at head of block queue\n"
+            "    qtail       queue new request at tail of block queue (def: "
+            "q at head)\n"
+            "    random      use random data instead of if=IFILE (only in "
+            "iflags)\n"
+            "    same_fds    each thread uses the same IFILE and OFILE(2) "
+            "file\n"
+            "                descriptors (def: each threads has own file "
+            "descriptors)\n"
+            "    swait       this option is now ignored\n"
+            "    v3          use v3 sg interface (def: v3 unless sg driver "
+            "is v4)\n"
+            "    v4          use v4 sg interface (def: v3 unless sg driver "
+            "is v4)\n"
+            "    wq_excl     set SG_CTL_FLAGM_EXCL_WAITQ on this sg fd\n"
+            "\n"
+            "Copies IFILE to OFILE (and to OFILE2 if given). If IFILE and "
+            "OFILE are sg\ndevices 'shared' mode is selected unless "
+            "'noshare' is given to 'iflag=' or\n'oflag='. of2=OFILE2 uses "
+            "'oflag=FLAGS'. When sharing, the data stays in a\nsingle "
+            "in-kernel buffer which is copied (or mmap-ed) to the user "
+            "space\nif the 'ofreg=OFREG' is given. Use '-hhhh' for more "
+            "information.\n"
+           );
+    return;
+page4:
+    pr2serr("pack_id:\n"
+            "These are ascending integers, starting at 1, associated with "
+            "each issued\nSCSI command. When both IFILE and OFILE are sg "
+            "devices, then the READ in\neach read-write pair is issued an "
+            "even pack_id and its WRITE pair is\ngiven the pack_id one "
+            "higher (i.e. an odd number). This enables a\n'dmesg -w' "
+            "user to see that progress is being "
+            "made.\n\n");
+    pr2serr("Debugging:\n"
+            "Apart from using one or more '--verbose' options which gets a "
+            "bit noisy\n'dmesg -w' can give a good overview "
+            "of what is happening.\nThat does a sg driver object tree "
+            "traversal that does minimal locking\nto make sure that each "
+            "traversal is 'safe'. So it is important to note\nthe whole "
+            "tree is not locked. This means for fast devices the overall\n"
+            "tree state may change while the traversal is occurring. For "
+            "example,\nit has been observed that both the read- and write- "
+            "sides of a request\nshare show they are in 'active' state "
+            "which should not be possible.\nIt occurs because the read-"
+            "side probably jumped out of active state and\nthe write-side "
+            "request entered it while some other nodes were being "
+            "printed.\n\n");
+    pr2serr("Busy state:\n"
+            "Busy state (abbreviated to 'bsy' in the dmesg "
+            "output)\nis entered during request setup and completion. It "
+            "is intended to be\na temporary state. It should not block "
+            "but does sometimes (e.g. in\nblock_get_request()). Even so "
+            "that blockage should be short and if not\nthere is a "
+            "problem.\n\n");
+    pr2serr("--verify :\n"
+            "For comparing IFILE with OFILE. Does repeated sequences of: "
+            "READ(ifile)\nand uses data returned to send to VERIFY(ofile, "
+            "BYTCHK=1). So the OFILE\ndevice/disk is doing the actual "
+            "comparison. Stops on first miscompare.\n\n");
+    pr2serr("--prefetch :\n"
+            "Used with --verify option. Prepends a PRE-FETCH(ofile, IMMED) "
+            "to verify\nsequence. This should speed the trailing VERIFY by "
+            "making sure that\nthe data it needs for the comparison is "
+            "already in its cache.\n");
+    return;
+}
+
+static void
+lk_print_command_len(const char *prefix, uint8_t * cmdp, int len, bool lock)
+{
+    if (lock)
+        pthread_mutex_lock(&strerr_mut);
+    if (prefix && *prefix)
+        fputs(prefix, stderr);
+    sg_print_command_len(cmdp, len);
+    if (lock)
+        pthread_mutex_unlock(&strerr_mut);
+}
+
+static void
+lk_chk_n_print3(const char * leadin, struct sg_io_hdr * hp, bool raw_sinfo)
+{
+    pthread_mutex_lock(&strerr_mut);
+    sg_chk_n_print3(leadin, hp, raw_sinfo);
+    pthread_mutex_unlock(&strerr_mut);
+}
+
+static void
+lk_chk_n_print4(const char * leadin, const struct sg_io_v4 * h4p,
+                bool raw_sinfo)
+{
+    pthread_mutex_lock(&strerr_mut);
+    sg_linux_sense_print(leadin, h4p->device_status, h4p->transport_status,
+                         h4p->driver_status, (const uint8_t *)h4p->response,
+                         h4p->response_len, raw_sinfo);
+    pthread_mutex_unlock(&strerr_mut);
+}
+
+static void
+hex2stderr_lk(const uint8_t * b_str, int len, int no_ascii)
+{
+    pthread_mutex_lock(&strerr_mut);
+    hex2stderr(b_str, len, no_ascii);
+    pthread_mutex_unlock(&strerr_mut);
+}
+
+/* Flags decoded into abbreviations for those that are set, separated by
+ * '|' . */
+static char *
+sg_flags_str(int flags, int b_len, char * b)
+{
+    int n = 0;
+
+    if ((b_len < 1) || (! b))
+        return b;
+    b[0] = '\0';
+    if (SG_FLAG_DIRECT_IO & flags) {            /* 0x1 */
+        n += sg_scnpr(b + n, b_len - n, "DIO|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_FLAG_MMAP_IO & flags) {              /* 0x4 */
+        n += sg_scnpr(b + n, b_len - n, "MMAP|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_YIELD_TAG & flags) {          /* 0x8 */
+        n += sg_scnpr(b + n, b_len - n, "YTAG|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_FLAG_Q_AT_TAIL & flags) {            /* 0x10 */
+        n += sg_scnpr(b + n, b_len - n, "QTAI|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_FLAG_Q_AT_HEAD & flags) {            /* 0x20 */
+        n += sg_scnpr(b + n, b_len - n, "QHEA|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_DOUT_OFFSET & flags) {        /* 0x40 */
+        n += sg_scnpr(b + n, b_len - n, "DOFF|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_EVENTFD & flags) {           /* 0x80 */
+        n += sg_scnpr(b + n, b_len - n, "EVFD|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_COMPLETE_B4 & flags) {        /* 0x100 */
+        n += sg_scnpr(b + n, b_len - n, "CPL_B4|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_SIGNAL & flags) {       /* 0x200 */
+        n += sg_scnpr(b + n, b_len - n, "SIGNAL|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_IMMED & flags) {              /* 0x400 */
+        n += sg_scnpr(b + n, b_len - n, "IMM|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_POLLED & flags) {             /* 0x800 */
+        n += sg_scnpr(b + n, b_len - n, "POLLED|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_STOP_IF & flags) {            /* 0x1000 */
+        n += sg_scnpr(b + n, b_len - n, "STOPIF|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_DEV_SCOPE & flags) {          /* 0x2000 */
+        n += sg_scnpr(b + n, b_len - n, "DEV_SC|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_SHARE & flags) {              /* 0x4000 */
+        n += sg_scnpr(b + n, b_len - n, "SHARE|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_DO_ON_OTHER & flags) {        /* 0x8000 */
+        n += sg_scnpr(b + n, b_len - n, "DO_OTH|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_NO_DXFER & flags) {          /* 0x10000 */
+        n += sg_scnpr(b + n, b_len - n, "NOXFER|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_KEEP_SHARE & flags) {        /* 0x20000 */
+        n += sg_scnpr(b + n, b_len - n, "KEEP_SH|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_MULTIPLE_REQS & flags) {     /* 0x40000 */
+        n += sg_scnpr(b + n, b_len - n, "MRQS|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_ORDERED_WR & flags) {        /* 0x80000 */
+        n += sg_scnpr(b + n, b_len - n, "OWR|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_REC_ORDER & flags) {         /* 0x100000 */
+        n += sg_scnpr(b + n, b_len - n, "REC_O|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SGV4_FLAG_META_OUT_IF & flags) {       /* 0x200000 */
+        n += sg_scnpr(b + n, b_len - n, "MOUT_IF|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (0 == n)
+        n += sg_scnpr(b + n, b_len - n, "<none>");
+fini:
+    if (n < b_len) {    /* trim trailing '\' */
+        if ('|' == b[n - 1])
+            b[n - 1] = '\0';
+    } else if ('|' == b[b_len - 1])
+        b[b_len - 1] = '\0';
+    return b;
+}
+
+/* Info field decoded into abbreviations for those bits that are set,
+ * separated by '|' . */
+static char *
+sg_info_str(int info, int b_len, char * b)
+{
+    int n = 0;
+
+    if ((b_len < 1) || (! b))
+        return b;
+    b[0] = '\0';
+    if (SG_INFO_CHECK & info) {               /* 0x1 */
+        n += sg_scnpr(b + n, b_len - n, "CHK|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_DIRECT_IO & info) {           /* 0x2 */
+        n += sg_scnpr(b + n, b_len - n, "DIO|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_MIXED_IO & info) {            /* 0x4 */
+        n += sg_scnpr(b + n, b_len - n, "MIO|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_DEVICE_DETACHING & info) {    /* 0x8 */
+        n += sg_scnpr(b + n, b_len - n, "DETA|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_ABORTED & info) {             /* 0x10 */
+        n += sg_scnpr(b + n, b_len - n, "ABRT|");
+        if (n >= b_len)
+            goto fini;
+    }
+    if (SG_INFO_MRQ_FINI & info) {            /* 0x20 */
+        n += sg_scnpr(b + n, b_len - n, "MRQF|");
+        if (n >= b_len)
+            goto fini;
+    }
+fini:
+    if (n < b_len) {    /* trim trailing '\' */
+        if ('|' == b[n - 1])
+            b[n - 1] = '\0';
+    } else if ('|' == b[b_len - 1])
+        b[b_len - 1] = '\0';
+    return b;
+}
+
+static void
+v4hdr_out_lk(const char * leadin, const sg_io_v4 * h4p, int id)
+{
+    char b[80];
+
+    pthread_mutex_lock(&strerr_mut);
+    if (leadin)
+        pr2serr("%s [id=%d]:\n", leadin, id);
+    if (('Q' != h4p->guard) || (0 != h4p->protocol) ||
+        (0 != h4p->subprotocol))
+        pr2serr("  <<<sg_io_v4 _NOT_ properly set>>>\n");
+    pr2serr("  pointers: cdb=%s  sense=%s  din=%p  dout=%p\n",
+            (h4p->request ? "y" : "NULL"), (h4p->response ? "y" : "NULL"),
+            (void *)h4p->din_xferp, (void *)h4p->dout_xferp);
+    pr2serr("  lengths: cdb=%u  sense=%u  din=%u  dout=%u\n",
+            h4p->request_len, h4p->max_response_len, h4p->din_xfer_len,
+             h4p->dout_xfer_len);
+    pr2serr("  flags=0x%x  request_extra{pack_id}=%d\n",
+            h4p->flags, h4p->request_extra);
+    pr2serr("  flags set: %s\n", sg_flags_str(h4p->flags, sizeof(b), b));
+    pr2serr(" %s OUT fields:\n", leadin);
+    pr2serr("  response_len=%d driver/transport/device_status="
+            "0x%x/0x%x/0x%x\n", h4p->response_len, h4p->driver_status,
+            h4p->transport_status, h4p->device_status);
+    pr2serr("  info=0x%x  din_resid=%u  dout_resid=%u  spare_out=%u  "
+            "dur=%u\n",
+            h4p->info, h4p->din_resid, h4p->dout_resid, h4p->spare_out,
+            h4p->duration);
+    pthread_mutex_unlock(&strerr_mut);
+}
+
+static void
+fetch_sg_version(void)
+{
+    FILE * fp;
+    char b[96];
+
+    have_sg_version = false;
+    sg_version = 0;
+    fp = fopen(PROC_SCSI_SG_VERSION, "r");
+    if (fp && fgets(b, sizeof(b) - 1, fp)) {
+        if (1 == sscanf(b, "%d", &sg_version))
+            have_sg_version = !!sg_version;
+    } else {
+        int j, k, l;
+
+        if (fp)
+            fclose(fp);
+        fp = fopen(SYS_SCSI_SG_VERSION, "r");
+        if (fp && fgets(b, sizeof(b) - 1, fp)) {
+            if (3 == sscanf(b, "%d.%d.%d", &j, &k, &l)) {
+                sg_version = (j * 10000) + (k * 100) + l;
+                have_sg_version = !!sg_version;
+            }
+        }
+    }
+    if (fp)
+        fclose(fp);
+}
+
+static void
+calc_duration_throughput(int contin)
+{
+    struct timeval end_tm, res_tm;
+    double a, b;
+
+    gettimeofday(&end_tm, NULL);
+    res_tm.tv_sec = end_tm.tv_sec - start_tm.tv_sec;
+    res_tm.tv_usec = end_tm.tv_usec - start_tm.tv_usec;
+    if (res_tm.tv_usec < 0) {
+        --res_tm.tv_sec;
+        res_tm.tv_usec += 1000000;
+    }
+    a = res_tm.tv_sec;
+    a += (0.000001 * res_tm.tv_usec);
+    b = (double)gcoll.bs * (dd_count - gcoll.out_rem_count.load());
+    pr2serr("time to %s data %s %d.%06d secs",
+            (gcoll.verify ? "verify" : "copy"), (contin ? "so far" : "was"),
+            (int)res_tm.tv_sec, (int)res_tm.tv_usec);
+    if ((a > 0.00001) && (b > 511))
+        pr2serr(", %.2f MB/sec\n", b / (a * 1000000.0));
+    else
+        pr2serr("\n");
+}
+
+static void
+print_stats(const char * str)
+{
+    int64_t infull;
+
+    if (0 != gcoll.out_rem_count.load())
+        pr2serr("  remaining block count=%" PRId64 "\n",
+                gcoll.out_rem_count.load());
+    infull = dd_count - gcoll.in_rem_count.load();
+    pr2serr("%s%" PRId64 "+%d records in\n", str,
+            infull - gcoll.in_partial.load(), gcoll.in_partial.load());
+
+    if (gcoll.out_type == FT_DEV_NULL)
+        pr2serr("%s0+0 records out\n", str);
+    else {
+        int64_t outfull = dd_count - gcoll.out_rem_count.load();
+
+        pr2serr("%s%" PRId64 "+%d records %s\n", str,
+                outfull - gcoll.out_partial.load(), gcoll.out_partial.load(),
+                (gcoll.verify ? "verified" : "out"));
+    }
+}
+
+static void
+interrupt_handler(int sig)
+{
+    struct sigaction sigact;
+
+    sigact.sa_handler = SIG_DFL;
+    sigemptyset(&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigaction(sig, &sigact, NULL);
+    pr2serr("Interrupted by signal,");
+    if (do_time > 0)
+        calc_duration_throughput(0);
+    print_stats("");
+    kill(getpid (), sig);
+}
+
+static void
+siginfo_handler(int sig)
+{
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    pr2serr("Progress report, continuing ...\n");
+    if (do_time > 0)
+        calc_duration_throughput(1);
+    print_stats("  ");
+}
+
+static void
+siginfo2_handler(int sig)
+{
+    struct global_collection * clp = &gcoll;
+
+    if (sig) { ; }      /* unused, dummy to suppress warning */
+    pr2serr("Progress report, continuing ...\n");
+    if (do_time > 0)
+        calc_duration_throughput(1);
+    print_stats("  ");
+    pr2serr("Send broadcast on out_sync_cv condition variable\n");
+    pthread_cond_broadcast(&clp->out_sync_cv);
+}
+
+static void
+install_handler(int sig_num, void (*sig_handler) (int sig))
+{
+    struct sigaction sigact;
+    sigaction (sig_num, NULL, &sigact);
+    if (sigact.sa_handler != SIG_IGN)
+    {
+        sigact.sa_handler = sig_handler;
+        sigemptyset (&sigact.sa_mask);
+        sigact.sa_flags = 0;
+        sigaction (sig_num, &sigact, NULL);
+    }
+}
+
+#ifdef SG_LIB_ANDROID
+static void
+thread_exit_handler(int sig)
+{
+    pthread_exit(0);
+}
+#endif
+
+/* Make safe_strerror() thread safe */
+static char *
+tsafe_strerror(int code, char * ebp)
+{
+    char * cp;
+
+    pthread_mutex_lock(&strerr_mut);
+    cp = safe_strerror(code);
+    strncpy(ebp, cp, STRERR_BUFF_LEN);
+    pthread_mutex_unlock(&strerr_mut);
+
+    ebp[strlen(ebp)] = '\0';
+    return ebp;
+}
+
+
+/* Following macro from D.R. Butenhof's POSIX threads book:
+ * ISBN 0-201-63392-2 . [Highly recommended book.] Changed __FILE__
+ * to __func__ */
+#define err_exit(code,text) do { \
+    char strerr_buff[STRERR_BUFF_LEN + 1]; \
+    pr2serr("%s at \"%s\":%d: %s\n", \
+        text, __func__, __LINE__, tsafe_strerror(code, strerr_buff)); \
+    exit(1); \
+    } while (0)
+
+
+static int
+dd_filetype(const char * filename, off_t & st_size)
+{
+    struct stat st;
+    size_t len = strlen(filename);
+
+    if ((1 == len) && ('.' == filename[0]))
+        return FT_DEV_NULL;
+    if (stat(filename, &st) < 0)
+        return FT_ERROR;
+    if (S_ISCHR(st.st_mode)) {
+        if ((MEM_MAJOR == major(st.st_rdev)) &&
+            ((DEV_NULL_MINOR_NUM == minor(st.st_rdev)) ||
+             (DEV_ZERO_MINOR_NUM == minor(st.st_rdev))))
+            return FT_DEV_NULL; /* treat /dev/null + /dev/zero the same */
+        if (SCSI_GENERIC_MAJOR == major(st.st_rdev))
+            return FT_SG;
+        if (SCSI_TAPE_MAJOR == major(st.st_rdev))
+            return FT_ST;
+        return FT_CHAR;
+    } else if (S_ISBLK(st.st_mode))
+        return FT_BLOCK;
+    else if (S_ISFIFO(st.st_mode))
+        return FT_FIFO;
+    st_size = st.st_size;
+    return FT_OTHER;
+}
+
+static inline void
+stop_both(struct global_collection * clp)
+{
+    clp->in_stop = true;
+    clp->out_stop = true;
+}
+
+/* Return of 0 -> success, see sg_ll_read_capacity*() otherwise */
+static int
+scsi_read_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+    int res;
+    uint8_t rcBuff[RCAP16_REPLY_LEN] = {};
+
+    res = sg_ll_readcap_10(sg_fd, 0, 0, rcBuff, READ_CAP_REPLY_LEN, false, 0);
+    if (0 != res)
+        return res;
+
+    if ((0xff == rcBuff[0]) && (0xff == rcBuff[1]) && (0xff == rcBuff[2]) &&
+        (0xff == rcBuff[3])) {
+
+        res = sg_ll_readcap_16(sg_fd, 0, 0, rcBuff, RCAP16_REPLY_LEN, false,
+                               0);
+        if (0 != res)
+            return res;
+        *num_sect = sg_get_unaligned_be64(rcBuff + 0) + 1;
+        *sect_sz = sg_get_unaligned_be32(rcBuff + 8);
+    } else {
+        /* take care not to sign extend values > 0x7fffffff */
+        *num_sect = (int64_t)sg_get_unaligned_be32(rcBuff + 0) + 1;
+        *sect_sz = sg_get_unaligned_be32(rcBuff + 4);
+    }
+    return 0;
+}
+
+/* Return of 0 -> success, -1 -> failure. BLKGETSIZE64, BLKGETSIZE and */
+/* BLKSSZGET macros problematic (from <linux/fs.h> or <sys/mount.h>). */
+static int
+read_blkdev_capacity(int sg_fd, int64_t * num_sect, int * sect_sz)
+{
+#ifdef BLKSSZGET
+    if ((ioctl(sg_fd, BLKSSZGET, sect_sz) < 0) && (*sect_sz > 0)) {
+        perror("BLKSSZGET ioctl error");
+        return -1;
+    } else {
+ #ifdef BLKGETSIZE64
+        uint64_t ull;
+
+        if (ioctl(sg_fd, BLKGETSIZE64, &ull) < 0) {
+
+            perror("BLKGETSIZE64 ioctl error");
+            return -1;
+        }
+        *num_sect = ((int64_t)ull / (int64_t)*sect_sz);
+ #else
+        unsigned long ul;
+
+        if (ioctl(sg_fd, BLKGETSIZE, &ul) < 0) {
+            perror("BLKGETSIZE ioctl error");
+            return -1;
+        }
+        *num_sect = (int64_t)ul;
+ #endif
+    }
+    return 0;
+#else
+    *num_sect = 0;
+    *sect_sz = 0;
+    return -1;
+#endif
+}
+
+static int
+system_wrapper(const char * cmd)
+{
+    int res;
+
+    res = system(cmd);
+    if (WIFSIGNALED(res) &&
+        (WTERMSIG(res) == SIGINT || WTERMSIG(res) == SIGQUIT))
+        raise(WTERMSIG(res));
+    return WEXITSTATUS(res);
+}
+
+/* Has an infinite loop doing a timed wait for any signals in sig_set. After
+ * each timeout (300 ms) checks if the most_recent_pack_id atomic integer
+ * has changed. If not after another two timeouts announces a stall has
+ * been detected. If shutting down atomic is true breaks out of loop and
+ * shuts down this thread. Other than that, this thread is normally cancelled
+ * by the main thread, after other threads have exited. */
+static void *
+sig_listen_thread(void * v_clp)
+{
+    bool stall_reported = false;
+    int prev_pack_id = 0;
+    struct timespec ts;
+    struct timespec * tsp = &ts;
+    struct global_collection * clp = (struct global_collection *)v_clp;
+    uint32_t ict_ms = (clp->sdt_ict ? clp->sdt_ict : DEF_SDT_ICT_MS);
+
+    tsp->tv_sec = ict_ms / 1000;
+    tsp->tv_nsec = (ict_ms % 1000) * 1000 * 1000;   /* DEF_SDT_ICT_MS */
+    while (1) {
+        int sig_number = sigtimedwait(&signal_set, NULL, tsp);
+
+        if (sig_number < 0) {
+            int err = errno;
+
+            /* EAGAIN implies a timeout */
+            if ((EAGAIN == err) && (clp->sdt_crt > 0)) {
+                int pack_id = mono_pack_id.load();
+
+                if ((pack_id > 0) && (pack_id == prev_pack_id)) {
+                    if (! stall_reported) {
+                        stall_reported = true;
+                        tsp->tv_sec = clp->sdt_crt;
+                        tsp->tv_nsec = 0;
+                        pr2serr_lk("%s: first stall at pack_id=%d detected\n",
+                                   __func__, pack_id);
+                    } else
+                        pr2serr_lk("%s: subsequent stall at pack_id=%d\n",
+                               __func__, pack_id);
+                    // following command assumes linux bash or similar shell
+                    system_wrapper("cat /proc/scsi/sg/debug >> /dev/stderr\n");
+                    // system_wrapper("/usr/bin/dmesg\n");
+                } else
+                    prev_pack_id = pack_id;
+            } else if (EAGAIN != err)
+                pr2serr_lk("%s: sigtimedwait() errno=%d\n", __func__, err);
+        }
+        if (SIGINT == sig_number) {
+            pr2serr_lk("%sinterrupted by SIGINT\n", my_name);
+            stop_both(clp);
+            pthread_cond_broadcast(&clp->out_sync_cv);
+            sigprocmask(SIG_SETMASK, &orig_signal_set, NULL);
+            raise(SIGINT);
+            break;
+        }
+        if (SIGUSR2 == sig_number) {
+            if (clp->verbose > 2)
+                pr2serr_lk("%s: interrupted by SIGUSR2\n", __func__);
+            break;
+        }
+        if (shutting_down)
+            break;
+    }           /* end of while loop */
+    if (clp->verbose > 3)
+        pr2serr_lk("%s: exiting\n", __func__);
+
+    return NULL;
+}
+
+static void *
+mrq_abort_thread(void * v_maip)
+{
+    int res, err;
+    int n = 0;
+    int seed;
+    unsigned int rn;
+    Mrq_abort_info l_mai = *(Mrq_abort_info *)v_maip;
+    struct sg_io_v4 ctl_v4 {};
+
+#ifdef HAVE_GETRANDOM
+    {
+        ssize_t ssz = getrandom(&seed, sizeof(seed), GRND_NONBLOCK);
+
+        if (ssz < (ssize_t)sizeof(seed)) {
+            pr2serr("getrandom() failed, ret=%d\n", (int)ssz);
+            seed = (int)time(NULL);
+        }
+    }
+#else
+    seed = (int)time(NULL);    /* use seconds since epoch as proxy */
+#endif
+    if (l_mai.debug)
+        pr2serr_lk("%s: from_id=%d: to abort mrq_pack_id=%d\n", __func__,
+                   l_mai.from_tid, l_mai.mrq_id);
+    res = ioctl(l_mai.fd, SG_GET_NUM_WAITING, &n);
+    ++num_waiting_calls;
+    if (res < 0) {
+        err = errno;
+        pr2serr_lk("%s: ioctl(SG_GET_NUM_WAITING) failed: %s [%d]\n",
+                   __func__, safe_strerror(err), err);
+    } else if (l_mai.debug)
+        pr2serr_lk("%s: num_waiting=%d\n", __func__, n);
+
+    Rand_uint * ruip = new Rand_uint(5, 500, seed);
+    struct timespec tspec = {0, 4000 /* 4 usecs */};
+    rn = ruip->get();
+    tspec.tv_nsec = rn * 1000;
+    if (l_mai.debug > 1)
+        pr2serr_lk("%s: /dev/urandom seed=0x%x delay=%u microsecs\n",
+                   __func__, seed, rn);
+    if (rn >= 20)
+        nanosleep(&tspec, NULL);
+    else if (l_mai.debug > 1)
+        pr2serr_lk("%s: skipping nanosleep cause delay < 20 usecs\n",
+                   __func__);
+
+    ctl_v4.guard = 'Q';
+    ctl_v4.flags = SGV4_FLAG_MULTIPLE_REQS;
+    ctl_v4.request_extra = l_mai.mrq_id;
+    ++num_mrq_abort_req;
+    res = ioctl(l_mai.fd, SG_IOABORT, &ctl_v4);
+    if (res < 0) {
+        err = errno;
+        if (ENODATA == err)
+            pr2serr_lk("%s: ioctl(SG_IOABORT) no match on "
+                       "MRQ pack_id=%d\n", __func__, l_mai.mrq_id);
+        else
+            pr2serr_lk("%s: MRQ ioctl(SG_IOABORT) failed: %s [%d]\n",
+                       __func__, safe_strerror(err), err);
+    } else {
+        ++num_mrq_abort_req_success;
+        if (l_mai.debug > 1)
+            pr2serr_lk("%s: from_id=%d sent ioctl(SG_IOABORT) on MRQ rq_id="
+                       "%d, success\n", __func__, l_mai.from_tid,
+                       l_mai.mrq_id);
+    }
+    delete ruip;
+    return NULL;
+}
+
+static bool
+sg_share_prepare(int write_side_fd, int read_side_fd, int id, bool vb_b)
+{
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    seip->sei_wr_mask |= SG_SEIM_SHARE_FD;
+    seip->sei_rd_mask |= SG_SEIM_SHARE_FD;
+    seip->share_fd = read_side_fd;
+    if (ioctl(write_side_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr_lk("tid=%d: ioctl(EXTENDED(shared_fd=%d), failed "
+                   "errno=%d %s\n", id, read_side_fd, errno,
+                   strerror(errno));
+        return false;
+    }
+    if (vb_b)
+        pr2serr_lk("%s: tid=%d: ioctl(EXTENDED(shared_fd)) ok, "
+                   "read_side_fd=%d, write_side_fd=%d\n", __func__,
+                   id, read_side_fd, write_side_fd);
+    return true;
+}
+
+static void
+sg_unshare(int sg_fd, int id, bool vb_b)
+{
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+    seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS;
+    seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_UNSHARE;
+    seip->ctl_flags |= SG_CTL_FLAGM_UNSHARE; /* needs to be set to unshare */
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr_lk("tid=%d: ioctl(EXTENDED(UNSHARE), failed errno=%d %s\n",
+                   id,  errno, strerror(errno));
+        return;
+    }
+    if (vb_b)
+        pr2serr_lk("tid=%d: ioctl(UNSHARE) ok\n", id);
+}
+
+static void
+sg_noshare_enlarge(int sg_fd, bool vb_b)
+{
+    if (sg_version_ge_40045) {
+        struct sg_extended_info sei {};
+        struct sg_extended_info * seip = &sei;
+
+        sei.sei_wr_mask |= SG_SEIM_TOT_FD_THRESH;
+        seip->tot_fd_thresh = 96 * 1024 * 1024;
+        if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+            pr2serr_lk("%s: ioctl(EXTENDED(TOT_FD_THRESH), failed errno=%d "
+                       "%s\n", __func__, errno, strerror(errno));
+            return;
+        }
+        if (vb_b)
+            pr2serr_lk("ioctl(TOT_FD_THRESH) ok\n");
+    }
+}
+
+static void
+sg_take_snap(int sg_fd, int id, bool vb_b)
+{
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+    seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS;
+    seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_SNAP_DEV;
+    seip->ctl_flags &= ~SG_CTL_FLAGM_SNAP_DEV;   /* 0 --> append */
+    if (ioctl(sg_fd, SG_SET_GET_EXTENDED, seip) < 0) {
+        pr2serr_lk("tid=%d: ioctl(EXTENDED(SNAP_DEV), failed errno=%d %s\n",
+                   id,  errno, strerror(errno));
+        return;
+    }
+    if (vb_b)
+        pr2serr_lk("tid=%d: ioctl(SNAP_DEV) ok\n", id);
+}
+
+static void
+cleanup_in(void * v_clp)
+{
+    struct global_collection * clp = (struct global_collection *)v_clp;
+
+    pr2serr("thread cancelled while in mutex held\n");
+    stop_both(clp);
+    pthread_mutex_unlock(&clp->in_mutex);
+    pthread_cond_broadcast(&clp->out_sync_cv);
+}
+
+static void
+cleanup_out(void * v_clp)
+{
+    struct global_collection * clp = (struct global_collection *)v_clp;
+
+    pr2serr("thread cancelled while out_mutex held\n");
+    stop_both(clp);
+    pthread_mutex_unlock(&clp->out_mutex);
+    pthread_cond_broadcast(&clp->out_sync_cv);
+}
+
+static void inline buffp_onto_next(Rq_elem * rep)
+{
+    struct global_collection * clp = rep->clp;
+
+    if ((clp->nmrqs > 0) && clp->unbalanced_mrq) {
+        ++rep->mrq_index;
+        if (rep->mrq_index >= clp->nmrqs)
+            rep->mrq_index = 0;         /* wrap */
+    }
+}
+
+static inline uint8_t *
+get_buffp(Rq_elem * rep)
+{
+    struct global_collection * clp = rep->clp;
+
+    if ((clp->nmrqs > 0) && clp->unbalanced_mrq && (rep->mrq_index > 0))
+        return rep->buffp + (rep->mrq_index * clp->bs * clp->bpt);
+    else
+        return rep->buffp;
+}
+
+static void *
+read_write_thread(void * v_tip)
+{
+    Thread_info * tip;
+    struct global_collection * clp;
+    Rq_elem rel {};
+    Rq_elem * rep = &rel;
+    int n, sz, blocks, status, vb, err, res, wr_blks, c_addr;
+    int num_sg = 0;
+    int64_t my_index;
+    volatile bool stop_after_write = false;
+    bool own_infd = false;
+    bool in_is_sg, in_mmap, out_is_sg, out_mmap;
+    bool own_outfd = false;
+    bool own_out2fd = false;
+    bool share_and_ofreg;
+    mrq_arr_t deferred_arr;  /* MRQ deferred array (vector) */
+
+    tip = (Thread_info *)v_tip;
+    clp = tip->gcp;
+    vb = clp->verbose;
+    rep->bs = clp->bs;
+    sz = clp->bpt * rep->bs;
+    c_addr = clp->chkaddr;
+    in_is_sg = (FT_SG == clp->in_type);
+    in_mmap = (in_is_sg && (clp->in_flags.mmap > 0));
+    out_is_sg = (FT_SG == clp->out_type);
+    out_mmap = (out_is_sg && (clp->out_flags.mmap > 0));
+    /* Following clp members are constant during lifetime of thread */
+    rep->clp = clp;
+    rep->id = tip->id;
+    if (vb > 2)
+        pr2serr_lk("%d <-- Starting worker thread\n", rep->id);
+    if (! (in_mmap || out_mmap)) {
+        n = sz;
+        if (clp->unbalanced_mrq)
+            n *= clp->nmrqs;
+        rep->buffp = sg_memalign(n, 0 /* page align */, &rep->alloc_bp,
+                                 false);
+        if (NULL == rep->buffp)
+            err_exit(ENOMEM, "out of memory creating user buffers\n");
+    }
+    rep->infd = clp->infd;
+    rep->outfd = clp->outfd;
+    rep->out2fd = clp->out2fd;
+    rep->outregfd = clp->outregfd;
+    rep->rep_count = 0;
+    if (clp->unbalanced_mrq && (clp->nmrqs > 0))
+        rep->mrq_index = clp->nmrqs - 1;
+
+    if (rep->infd == rep->outfd) {
+        if (in_is_sg)
+            rep->same_sg = true;
+    } else if (in_is_sg && out_is_sg)
+        rep->both_sg = true;
+    else if (in_is_sg)
+        rep->only_in_sg = true;
+    else if (out_is_sg)
+        rep->only_out_sg = true;
+
+    if (clp->in_flags.random) {
+#ifdef HAVE_GETRANDOM
+        ssize_t ssz = getrandom(&rep->seed, sizeof(rep->seed), GRND_NONBLOCK);
+
+        if (ssz < (ssize_t)sizeof(rep->seed)) {
+            pr2serr_lk("thread=%d: getrandom() failed, ret=%d\n",
+                       rep->id, (int)ssz);
+            rep->seed = (long)time(NULL);
+        }
+#else
+        rep->seed = (long)time(NULL);    /* use seconds since epoch as proxy */
+#endif
+        if (vb > 1)
+            pr2serr_lk("thread=%d: seed=%ld\n", rep->id, rep->seed);
+#ifdef HAVE_SRAND48_R
+        srand48_r(rep->seed, &rep->drand);
+#else
+        srand48(rep->seed);
+#endif
+    }
+    if (clp->in_flags.same_fds || clp->out_flags.same_fds)
+        ;
+    else {
+        int fd;
+
+        if (in_is_sg && clp->infp) {
+            fd = sg_in_open(clp, clp->infp, (in_mmap ? &rep->buffp : NULL),
+                            (in_mmap ? &rep->mmap_len : NULL));
+            if (fd < 0)
+                goto fini;
+            rep->infd = fd;
+            rep->mmap_active = in_mmap ? clp->in_flags.mmap : 0;
+            if (in_mmap && (vb > 4))
+                pr2serr_lk("thread=%d: mmap buffp=%p\n", rep->id, rep->buffp);
+            own_infd = true;
+            ++num_sg;
+            if (vb > 2)
+                pr2serr_lk("thread=%d: opened local sg IFILE\n", rep->id);
+        }
+        if (out_is_sg && clp->outfp) {
+            fd = sg_out_open(clp, clp->outfp, (out_mmap ? &rep->buffp : NULL),
+                             (out_mmap ? &rep->mmap_len : NULL));
+            if (fd < 0)
+                goto fini;
+            rep->outfd = fd;
+            if (! rep->mmap_active)
+                rep->mmap_active = out_mmap ? clp->out_flags.mmap : 0;
+            if (out_mmap && (vb > 4))
+                pr2serr_lk("thread=%d: mmap buffp=%p\n", rep->id, rep->buffp);
+            own_outfd = true;
+            ++num_sg;
+            if (vb > 2)
+                pr2serr_lk("thread=%d: opened local sg OFILE\n", rep->id);
+        }
+        if ((FT_SG == clp->out2_type) && clp->out2fp) {
+            fd = sg_out_open(clp, clp->out2fp,
+                             (out_mmap ? &rep->buffp : NULL),
+                             (out_mmap ? &rep->mmap_len : NULL));
+            if (fd < 0)
+                goto fini;
+            rep->out2fd = fd;
+            own_out2fd = true;
+            if (vb > 2)
+                pr2serr_lk("thread=%d: opened local sg OFILE2\n", rep->id);
+        }
+    }
+    if (vb > 2) {
+        if (in_is_sg && (! own_infd))
+            pr2serr_lk("thread=%d: using global sg IFILE, fd=%d\n", rep->id,
+                       rep->infd);
+        if (out_is_sg && (! own_outfd))
+            pr2serr_lk("thread=%d: using global sg OFILE, fd=%d\n", rep->id,
+                       rep->outfd);
+        if ((FT_SG == clp->out2_type) && (! own_out2fd))
+            pr2serr_lk("thread=%d: using global sg OFILE2, fd=%d\n", rep->id,
+                       rep->out2fd);
+    }
+    if (!sg_version_ge_40045) {
+        if (vb > 4)
+            pr2serr_lk("thread=%d: Skipping share because driver too old\n",
+                       rep->id);
+    } else if (clp->noshare) {
+        if (vb > 4)
+            pr2serr_lk("thread=%d: Skipping IFILE share with OFILE due to "
+                       "noshare=1\n", rep->id);
+    } else if (sg_version_ge_40045 && in_is_sg && out_is_sg)
+        rep->has_share = sg_share_prepare(rep->outfd, rep->infd, rep->id,
+                                          vb > 9);
+    if (vb > 9)
+        pr2serr_lk("tid=%d, has_share=%s\n", rep->id,
+                   (rep->has_share ? "true" : "false"));
+    share_and_ofreg = (rep->has_share && (rep->outregfd >= 0));
+
+    /* vvvvvvvvvvvvvv  Main segment copy loop  vvvvvvvvvvvvvvvvvvvvvvv */
+    while (1) {
+        rep->wr = false;
+        my_index = atomic_fetch_add(&pos_index, (long int)clp->bpt);
+        /* Start of READ half of a segment */
+        buffp_onto_next(rep);
+        status = pthread_mutex_lock(&clp->in_mutex);
+        if (0 != status) err_exit(status, "lock in_mutex");
+
+        if (dd_count >= 0) {
+            if (my_index >= dd_count) {
+                status = pthread_mutex_unlock(&clp->in_mutex);
+                if (0 != status) err_exit(status, "unlock in_mutex");
+                if ((clp->nmrqs > 0) && (deferred_arr.first.size() > 0)) {
+                    if (vb > 2)
+                        pr2serr_lk("thread=%d: tail-end my_index>=dd_count, "
+                                   "to_do=%u\n", rep->id,
+                                   (uint32_t)deferred_arr.first.size());
+                    res = sgh_do_deferred_mrq(rep, deferred_arr);
+                    if (res)
+                        pr2serr_lk("%s tid=%d: sgh_do_deferred_mrq failed\n",
+                                   __func__, rep->id);
+                }
+                break;  /* at or beyond end, so leave loop >>>>>>>>>>  */
+            } else if ((my_index + clp->bpt) > dd_count)
+                blocks = dd_count - my_index;
+            else
+                blocks = clp->bpt;
+        } else
+            blocks = clp->bpt;
+
+        rep->iblk = clp->skip + my_index;
+        rep->oblk = clp->seek + my_index;
+        rep->num_blks = blocks;
+
+        // clp->in_blk += blocks;
+        // clp->in_count -= blocks;
+
+        pthread_cleanup_push(cleanup_in, (void *)clp);
+        if (in_is_sg)
+            sg_in_rd_cmd(clp, rep, deferred_arr);
+        else {
+            stop_after_write = normal_in_rd(rep, blocks);
+            status = pthread_mutex_unlock(&clp->in_mutex);
+            if (0 != status) err_exit(status, "unlock in_mutex");
+        }
+        if (c_addr && (rep->bs > 3)) {
+            int k, j, off, num;
+            uint32_t addr = (uint32_t)rep->iblk;
+
+            num = (1 == c_addr) ? 4 : (rep->bs - 3);
+            for (k = 0, off = 0; k < blocks; ++k, ++addr, off += rep->bs) {
+                for (j = 0; j < num; j += 4) {
+                    if (addr != sg_get_unaligned_be32(rep->buffp + off + j))
+                        break;
+                }
+                if (j < num)
+                    break;
+            }
+            if (k < blocks) {
+                pr2serr("%s: chkaddr failure at addr=0x%x\n", __func__, addr);
+                exit_status = SG_LIB_CAT_MISCOMPARE;
+                ++num_miscompare;
+                stop_both(clp);
+            }
+        }
+        pthread_cleanup_pop(0);
+        ++rep->rep_count;
+
+        /* Start of WRITE part of a segment */
+        rep->wr = true;
+        status = pthread_mutex_lock(&clp->out_mutex);
+        if (0 != status) err_exit(status, "lock out_mutex");
+
+        /* Make sure the OFILE (+ OFREG) are in same sequence as IFILE */
+        if (clp->in_flags.random)
+            goto skip_force_out_sequence;
+        if ((rep->outregfd < 0) && in_is_sg && out_is_sg)
+            goto skip_force_out_sequence;
+        if (share_and_ofreg || (FT_DEV_NULL != clp->out_type)) {
+            while ((! clp->out_stop.load()) &&
+                   (rep->oblk != clp->out_blk.load())) {
+                /* if write would be out of sequence then wait */
+                pthread_cleanup_push(cleanup_out, (void *)clp);
+                status = pthread_cond_wait(&clp->out_sync_cv, &clp->out_mutex);
+                if (0 != status) err_exit(status, "cond out_sync_cv");
+                pthread_cleanup_pop(0);
+            }
+        }
+
+skip_force_out_sequence:
+        if (clp->out_stop.load() || (clp->out_count.load() <= 0)) {
+            if (! clp->out_stop.load())
+                clp->out_stop = true;
+            status = pthread_mutex_unlock(&clp->out_mutex);
+            if (0 != status) err_exit(status, "unlock out_mutex");
+            break;      /* stop requested so leave loop >>>>>>>>>>  */
+        }
+        if (stop_after_write)
+            clp->out_stop = true;
+
+        clp->out_count -= blocks;
+        clp->out_blk += blocks;
+
+        pthread_cleanup_push(cleanup_out, (void *)clp);
+        if (rep->outregfd >= 0) {
+            res = write(rep->outregfd, get_buffp(rep),
+                        rep->bs * rep->num_blks);
+            err = errno;
+            if (res < 0)
+                pr2serr_lk("%s: tid=%d: write(outregfd) failed: %s\n",
+                           __func__, rep->id, strerror(err));
+            else if (vb > 9)
+                pr2serr_lk("%s: tid=%d: write(outregfd), fd=%d, num_blks=%d"
+                           "\n", __func__, rep->id, rep->outregfd,
+                           rep->num_blks);
+        }
+        /* Output to OFILE */
+        wr_blks = rep->num_blks;
+        if (out_is_sg) {
+            sg_out_wr_cmd(rep, deferred_arr, false, clp->prefetch);
+            ++rep->rep_count;
+        } else if (FT_DEV_NULL == clp->out_type) {
+            /* skip actual write operation */
+            wr_blks = 0;
+            clp->out_rem_count -= blocks;
+            status = pthread_mutex_unlock(&clp->out_mutex);
+            if (0 != status) err_exit(status, "unlock out_mutex");
+        } else {
+            normal_out_wr(rep, blocks);
+            status = pthread_mutex_unlock(&clp->out_mutex);
+            if (0 != status) err_exit(status, "unlock out_mutex");
+            ++rep->rep_count;
+        }
+        pthread_cleanup_pop(0);
+
+        /* Output to OFILE2 if sg device */
+        if ((clp->out2fd >= 0) && (FT_SG == clp->out2_type)) {
+            pthread_cleanup_push(cleanup_out, (void *)clp);
+            status = pthread_mutex_lock(&clp->out2_mutex);
+            if (0 != status) err_exit(status, "lock out2_mutex");
+            /* releases out2_mutex mid operation */
+            sg_out_wr_cmd(rep, deferred_arr, true, false);
+
+            pthread_cleanup_pop(0);
+        }
+        if (0 == rep->num_blks) {
+            if ((clp->nmrqs > 0) && (deferred_arr.first.size() > 0)) {
+                if (wr_blks > 0)
+                    rep->out_mrq_q_blks += wr_blks;
+                if (vb > 2)
+                    pr2serr_lk("thread=%d: tail-end, to_do=%u\n", rep->id,
+                               (uint32_t)deferred_arr.first.size());
+                res = sgh_do_deferred_mrq(rep, deferred_arr);
+                if (res)
+                    pr2serr_lk("%s tid=%d: sgh_do_deferred_mrq failed\n",
+                               __func__, rep->id);
+            }
+            clp->out_stop = true;
+            stop_after_write = true;
+            break;      /* read nothing so leave loop >>>>>>>>>>  */
+        }
+        // if ((! rep->has_share) && (FT_DEV_NULL != clp->out_type))
+        pthread_cond_broadcast(&clp->out_sync_cv);
+        if (stop_after_write)
+            break;      /* leaving main loop >>>>>>>>> */
+    }   /* ^^^^^^^^^^ end of main while loop which copies segments ^^^^^^ */
+
+    status = pthread_mutex_lock(&clp->in_mutex);
+    if (0 != status) err_exit(status, "lock in_mutex");
+    if (! clp->in_stop.load())
+        clp->in_stop = true;  /* flag other workers to stop */
+    status = pthread_mutex_unlock(&clp->in_mutex);
+    if (0 != status) err_exit(status, "unlock in_mutex");
+
+fini:
+    if ((1 == rep->mmap_active) && (rep->mmap_len > 0)) {
+        if (munmap(rep->buffp, rep->mmap_len) < 0) {
+            err = errno;
+            char bb[STRERR_BUFF_LEN + 1];
+
+            pr2serr_lk("thread=%d: munmap() failed: %s\n", rep->id,
+                       tsafe_strerror(err, bb));
+        }
+        if (vb > 4)
+            pr2serr_lk("thread=%d: munmap(%p, %d)\n", rep->id, rep->buffp,
+                       rep->mmap_len);
+        rep->mmap_active = 0;
+    }
+    if (rep->alloc_bp) {
+        free(rep->alloc_bp);
+        rep->alloc_bp = NULL;
+        rep->buffp = NULL;
+    }
+
+    if (sg_version_ge_40045) {
+        if (clp->noshare) {
+            if ((clp->nmrqs > 0) && clp->unshare)
+                sg_unshare(rep->infd, rep->id, vb > 9);
+        } else if (in_is_sg && out_is_sg)
+            if (clp->unshare)
+            sg_unshare(rep->infd, rep->id, vb > 9);
+    }
+    if (own_infd && (rep->infd >= 0)) {
+        if (vb && in_is_sg) {
+            ++num_waiting_calls;
+            if (ioctl(rep->infd, SG_GET_NUM_WAITING, &n) >= 0) {
+                if (n > 0)
+                    pr2serr_lk("%s: tid=%d: num_waiting=%d prior close(in)\n",
+                               __func__, rep->id, n);
+            } else {
+                err = errno;
+                pr2serr_lk("%s: [%d] ioctl(SG_GET_NUM_WAITING) errno=%d: "
+                           "%s\n", __func__, rep->id, err, strerror(err));
+            }
+        }
+        close(rep->infd);
+    }
+    if (own_outfd && (rep->outfd >= 0)) {
+        if (vb && out_is_sg) {
+            ++num_waiting_calls;
+            if (ioctl(rep->outfd, SG_GET_NUM_WAITING, &n) >= 0) {
+                if (n > 0)
+                    pr2serr_lk("%s: tid=%d: num_waiting=%d prior "
+                               "close(out)\n", __func__, rep->id, n);
+            } else {
+                err = errno;
+                pr2serr_lk("%s: [%d] ioctl(SG_GET_NUM_WAITING) errno=%d: "
+                           "%s\n", __func__, rep->id, err, strerror(err));
+            }
+        }
+        close(rep->outfd);
+    }
+    if (own_out2fd && (rep->out2fd >= 0))
+        close(rep->out2fd);
+    pthread_cond_broadcast(&clp->out_sync_cv);
+    return stop_after_write ? NULL : clp;
+}
+
+/* N.B. A return of true means it wants to stop the copy. So false is the
+ * 'good' reply (i.e. keep going). */
+static bool
+normal_in_rd(Rq_elem * rep, int blocks)
+{
+    struct global_collection * clp = rep->clp;
+    bool stop_after_write = false;
+    bool same_fds = clp->in_flags.same_fds || clp->out_flags.same_fds;
+    int res;
+
+    if (clp->verbose > 4)
+        pr2serr_lk("%s: tid=%d: iblk=%" PRIu64 ", blocks=%d\n", __func__,
+                   rep->id, rep->iblk, blocks);
+    if (FT_RANDOM_0_FF == clp->in_type) {
+        int k, j;
+        const int jbump = sizeof(uint32_t);
+        long rn;
+        uint8_t * bp;
+
+        if (clp->in_flags.zero && clp->in_flags.ff && (rep->bs >= 4)) {
+            uint32_t pos = (uint32_t)rep->iblk;
+            uint32_t off;
+
+            for (k = 0, off = 0; k < blocks; ++k, off += rep->bs, ++pos) {
+                for (j = 0; j < (rep->bs - 3); j += 4)
+                    sg_put_unaligned_be32(pos, rep->buffp + off + j);
+            }
+        } else if (clp->in_flags.zero)
+            memset(rep->buffp, 0, blocks * rep->bs);
+        else if (clp->in_flags.ff)
+            memset(rep->buffp, 0xff, blocks * rep->bs);
+        else {
+            for (k = 0, bp = rep->buffp; k < blocks; ++k, bp += rep->bs) {
+                for (j = 0; j < rep->bs; j += jbump) {
+                    /* mrand48 takes uniformly from [-2^31, 2^31) */
+#ifdef HAVE_SRAND48_R
+                    mrand48_r(&rep->drand, &rn);
+#else
+                    rn = mrand48();
+#endif
+                    *((uint32_t *)(bp + j)) = (uint32_t)rn;
+                }
+            }
+        }
+        clp->in_rem_count -= blocks;
+        return stop_after_write;
+    }
+    if (! same_fds) {   /* each has own file pointer, so we need to move it */
+        int64_t pos = rep->iblk * rep->bs;
+
+        if (lseek64(rep->infd, pos, SEEK_SET) < 0) {    /* problem if pipe! */
+            pr2serr_lk("%s: tid=%d: >> lseek64(%" PRId64 "): %s\n", __func__,
+                       rep->id, pos, safe_strerror(errno));
+            stop_both(clp);
+            return true;
+        }
+    }
+    /* enters holding in_mutex */
+    while (((res = read(clp->infd, rep->buffp, blocks * rep->bs)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno)))
+        std::this_thread::yield();/* another thread may be able to progress */
+    if (res < 0) {
+        char strerr_buff[STRERR_BUFF_LEN + 1];
+
+        if (clp->in_flags.coe) {
+            memset(rep->buffp, 0, rep->num_blks * rep->bs);
+            pr2serr_lk("tid=%d: >> substituted zeros for in blk=%" PRId64
+                      " for %d bytes, %s\n", rep->id, rep->iblk,
+                       rep->num_blks * rep->bs,
+                       tsafe_strerror(errno, strerr_buff));
+            res = rep->num_blks * rep->bs;
+        }
+        else {
+            pr2serr_lk("tid=%d: error in normal read, %s\n", rep->id,
+                       tsafe_strerror(errno, strerr_buff));
+            stop_both(clp);
+            return true;
+        }
+    }
+    if (res < blocks * rep->bs) {
+        // int o_blocks = blocks;
+
+        stop_after_write = true;
+        blocks = res / rep->bs;
+        if ((res % rep->bs) > 0) {
+            blocks++;
+            clp->in_partial++;
+        }
+        /* Reverse out + re-apply blocks on clp */
+        // clp->in_blk -= o_blocks;
+        // clp->in_count += o_blocks;
+        rep->num_blks = blocks;
+        // clp->in_blk += blocks;
+        // clp->in_count -= blocks;
+    }
+    clp->in_rem_count -= blocks;
+    return stop_after_write;
+}
+
+static void
+normal_out_wr(Rq_elem * rep, int blocks)
+{
+    int res;
+    struct global_collection * clp = rep->clp;
+
+    /* enters holding out_mutex */
+    if (clp->verbose > 4)
+        pr2serr_lk("%s: tid=%d: oblk=%" PRIu64 ", blocks=%d\n", __func__,
+                   rep->id, rep->oblk, blocks);
+    while (((res = write(clp->outfd, rep->buffp, blocks * rep->bs))
+            < 0) && ((EINTR == errno) || (EAGAIN == errno)))
+        std::this_thread::yield();/* another thread may be able to progress */
+    if (res < 0) {
+        char strerr_buff[STRERR_BUFF_LEN + 1];
+
+        if (clp->out_flags.coe) {
+            pr2serr_lk("tid=%d: >> ignored error for out blk=%" PRId64
+                       " for %d bytes, %s\n", rep->id, rep->oblk,
+                       rep->num_blks * rep->bs,
+                       tsafe_strerror(errno, strerr_buff));
+            res = rep->num_blks * rep->bs;
+        }
+        else {
+            pr2serr_lk("tid=%d: error normal write, %s\n", rep->id,
+                       tsafe_strerror(errno, strerr_buff));
+            stop_both(clp);
+            return;
+        }
+    }
+    if (res < blocks * rep->bs) {
+        blocks = res / rep->bs;
+        if ((res % rep->bs) > 0) {
+            blocks++;
+            clp->out_partial++;
+        }
+        rep->num_blks = blocks;
+    }
+    clp->out_rem_count -= blocks;
+}
+
+static int
+sg_build_scsi_cdb(uint8_t * cdbp, int cdb_sz, unsigned int blocks,
+                  int64_t start_block, bool ver_true, bool write_true,
+                  bool fua, bool dpo)
+{
+    int rd_opcode[] = {0x8, 0x28, 0xa8, 0x88};
+    int ve_opcode[] = {0xff /* no VER(6) */, 0x2f, 0xaf, 0x8f};
+    int wr_opcode[] = {0xa, 0x2a, 0xaa, 0x8a};
+    int sz_ind;
+
+    memset(cdbp, 0, cdb_sz);
+    if (ver_true) {     /* only support VERIFY(10) */
+        if (cdb_sz < 10) {
+            pr2serr_lk("%sonly support VERIFY(10)\n", my_name);
+            return 1;
+        }
+        cdb_sz = 10;
+        fua = false;
+        cdbp[1] |= 0x2; /* BYTCHK=1 --> sending dout for comparison */
+        cdbp[0] = ve_opcode[1];
+    }
+    if (dpo)
+        cdbp[1] |= 0x10;
+    if (fua)
+        cdbp[1] |= 0x8;
+    switch (cdb_sz) {
+    case 6:
+        sz_ind = 0;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                         rd_opcode[sz_ind]);
+        sg_put_unaligned_be24(0x1fffff & start_block, cdbp + 1);
+        cdbp[4] = (256 == blocks) ? 0 : (uint8_t)blocks;
+        if (blocks > 256) {
+            pr2serr_lk("%sfor 6 byte commands, maximum number of blocks is "
+                       "256\n", my_name);
+            return 1;
+        }
+        if ((start_block + blocks - 1) & (~0x1fffff)) {
+            pr2serr_lk("%sfor 6 byte commands, can't address blocks beyond "
+                       "%d\n", my_name, 0x1fffff);
+            return 1;
+        }
+        if (dpo || fua) {
+            pr2serr_lk("%sfor 6 byte commands, neither dpo nor fua bits "
+                       "supported\n", my_name);
+            return 1;
+        }
+        break;
+    case 10:
+        if (! ver_true) {
+            sz_ind = 1;
+            cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                             rd_opcode[sz_ind]);
+        }
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be16((uint16_t)blocks, cdbp + 7);
+        if (blocks & (~0xffff)) {
+            pr2serr_lk("%sfor 10 byte commands, maximum number of blocks is "
+                       "%d\n", my_name, 0xffff);
+            return 1;
+        }
+        break;
+    case 12:
+        sz_ind = 2;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                         rd_opcode[sz_ind]);
+        sg_put_unaligned_be32((uint32_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 6);
+        break;
+    case 16:
+        sz_ind = 3;
+        cdbp[0] = (uint8_t)(write_true ? wr_opcode[sz_ind] :
+                                         rd_opcode[sz_ind]);
+        sg_put_unaligned_be64((uint64_t)start_block, cdbp + 2);
+        sg_put_unaligned_be32((uint32_t)blocks, cdbp + 10);
+        break;
+    default:
+        pr2serr_lk("%sexpected cdb size of 6, 10, 12, or 16 but got %d\n",
+                   my_name, cdb_sz);
+        return 1;
+    }
+    return 0;
+}
+
+/* Enters this function holding in_mutex */
+static void
+sg_in_rd_cmd(struct global_collection * clp, Rq_elem * rep,
+             mrq_arr_t & def_arr)
+{
+    int status, pack_id;
+
+    while (1) {
+        int res = sg_start_io(rep, def_arr, pack_id, NULL);
+
+        if (1 == res)
+            err_exit(ENOMEM, "sg starting in command");
+        else if (res < 0) {
+            pr2serr_lk("tid=%d: inputting to sg failed, blk=%" PRId64 "\n",
+                       rep->id, rep->iblk);
+            status = pthread_mutex_unlock(&clp->in_mutex);
+            if (0 != status) err_exit(status, "unlock in_mutex");
+            stop_both(clp);
+            return;
+        }
+        /* Now release in mutex to let other reads run in parallel */
+        status = pthread_mutex_unlock(&clp->in_mutex);
+        if (0 != status) err_exit(status, "unlock in_mutex");
+
+        res = sg_finish_io(rep->wr, rep, pack_id, NULL);
+        switch (res) {
+        case SG_LIB_CAT_ABORTED_COMMAND:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            /* try again with same addr, count info */
+            /* now re-acquire in mutex for balance */
+            /* N.B. This re-read could now be out of read sequence */
+            status = pthread_mutex_lock(&clp->in_mutex);
+            if (0 != status) err_exit(status, "lock in_mutex");
+            break;      /* will loop again */
+        case SG_LIB_CAT_MEDIUM_HARD:
+            if (0 == clp->in_flags.coe) {
+                pr2serr_lk("error finishing sg in command (medium)\n");
+                if (exit_status <= 0)
+                    exit_status = res;
+                stop_both(clp);
+                return;
+            } else {
+                memset(get_buffp(rep), 0, rep->num_blks * rep->bs);
+                pr2serr_lk("tid=%d: >> substituted zeros for in blk=%" PRId64
+                           " for %d bytes\n", rep->id, rep->iblk,
+                           rep->num_blks * rep->bs);
+            }
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        case 0:
+            status = pthread_mutex_lock(&clp->in_mutex);
+            if (0 != status) err_exit(status, "lock in_mutex");
+            if (rep->dio_incomplete_count || rep->resid) {
+                clp->dio_incomplete_count += rep->dio_incomplete_count;
+                clp->sum_of_resids += rep->resid;
+            }
+            clp->in_rem_count -= rep->num_blks;
+            status = pthread_mutex_unlock(&clp->in_mutex);
+            if (0 != status) err_exit(status, "unlock in_mutex");
+            return;
+        default:
+            pr2serr_lk("tid=%d: error finishing sg in command (%d)\n",
+                       rep->id, res);
+            if (exit_status <= 0)
+                exit_status = res;
+            stop_both(clp);
+            return;
+        }
+    }           /* end of while (1) loop */
+}
+
+static bool
+sg_wr_swap_share(Rq_elem * rep, int to_fd, bool before)
+{
+    bool not_first = false;
+    int err = 0;
+    int k;
+    int read_side_fd = rep->infd;
+    struct global_collection * clp = rep->clp;
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    if (rep->clp->verbose > 2)
+        pr2serr_lk("%s: tid=%d: to_fd=%d, before=%d\n", __func__, rep->id,
+                   to_fd, (int)before);
+    seip->sei_wr_mask |= SG_SEIM_CHG_SHARE_FD;
+    seip->sei_rd_mask |= SG_SEIM_CHG_SHARE_FD;
+    seip->share_fd = to_fd;
+    if (before) {
+        /* clear READ_SIDE_FINI bit: puts read-side in SG_RQ_SHR_SWAP state */
+        seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+        seip->sei_rd_mask |= SG_SEIM_CTL_FLAGS;
+        seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_READ_SIDE_FINI;
+        seip->ctl_flags &= ~SG_CTL_FLAGM_READ_SIDE_FINI;/* would be 0 anyway */
+    }
+    for (k = 0; (ioctl(read_side_fd, SG_SET_GET_EXTENDED, seip) < 0) &&
+                 (EBUSY == errno); ++k) {
+        err = errno;
+        if (k > 10000)
+            break;
+        if (! not_first) {
+            if (clp->verbose > 3)
+                pr2serr_lk("tid=%d: ioctl(EXTENDED(change_shared_fd=%d), "
+                           "failed errno=%d %s\n", rep->id, read_side_fd, err,
+                           strerror(err));
+            not_first = true;
+        }
+        err = 0;
+        std::this_thread::yield();/* another thread may be able to progress */
+    }
+    if (err) {
+        pr2serr_lk("tid=%d: ioctl(EXTENDED(change_shared_fd=%d), failed "
+                   "errno=%d %s\n", rep->id, read_side_fd, err, strerror(err));
+        return false;
+    }
+    if (clp->verbose > 15)
+        pr2serr_lk("%s: tid=%d: ioctl(EXTENDED(change_shared_fd)) ok, "
+                   "read_side_fd=%d, to_write_side_fd=%d\n", __func__, rep->id,
+                   read_side_fd, to_fd);
+    return true;
+}
+
+/* Enters this function holding out_mutex */
+static void
+sg_out_wr_cmd(Rq_elem * rep, mrq_arr_t & def_arr, bool is_wr2, bool prefetch)
+{
+    int res, status, pack_id, nblks;
+    struct global_collection * clp = rep->clp;
+    uint32_t ofsplit = clp->ofsplit;
+    pthread_mutex_t * mutexp = is_wr2 ? &clp->out2_mutex : &clp->out_mutex;
+    struct sg_io_extra xtr {};
+    struct sg_io_extra * xtrp = &xtr;
+    const char * wr_or_ver = clp->verify ? "verify" : "out";
+
+    xtrp->is_wr2 = is_wr2;
+    xtrp->prefetch = prefetch;
+    nblks = rep->num_blks;
+    if (rep->has_share && is_wr2)
+        sg_wr_swap_share(rep, rep->out2fd, true);
+
+    if (prefetch) {
+again:
+        res = sg_start_io(rep, def_arr, pack_id, xtrp);
+        if (1 == res)
+            err_exit(ENOMEM, "sg starting out command");
+        else if (res < 0) {
+            pr2serr_lk("%ssg %s failed, blk=%" PRId64 "\n",
+                       my_name, wr_or_ver, rep->oblk);
+            status = pthread_mutex_unlock(mutexp);
+            if (0 != status) err_exit(status, "unlock out_mutex");
+            stop_both(clp);
+            goto fini;
+        }
+        /* Now release in mutex to let other reads run in parallel */
+        status = pthread_mutex_unlock(mutexp);
+        if (0 != status) err_exit(status, "unlock out_mutex");
+
+        res = sg_finish_io(rep->wr, rep, pack_id, xtrp);
+        switch (res) {
+        case SG_LIB_CAT_ABORTED_COMMAND:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            /* try again with same addr, count info */
+            /* now re-acquire out mutex for balance */
+            /* N.B. This re-write could now be out of write sequence */
+            status = pthread_mutex_lock(mutexp);
+            if (0 != status) err_exit(status, "lock out_mutex");
+            goto again;
+        case SG_LIB_CAT_CONDITION_MET:
+        case 0:
+            status = pthread_mutex_lock(mutexp);
+            if (0 != status) err_exit(status, "unlock out_mutex");
+            break;
+        default:
+            pr2serr_lk("error finishing sg prefetch command (%d)\n", res);
+            if (exit_status <= 0)
+                exit_status = res;
+            stop_both(clp);
+            goto fini;
+        }
+    }
+
+    /* start write (or verify) on current segment on sg device */
+    xtrp->prefetch = false;
+    if ((ofsplit > 0) && (rep->num_blks > (int)ofsplit)) {
+        xtrp->dout_is_split = true;
+        xtrp->blk_offset = 0;
+        xtrp->blks = ofsplit;
+        nblks = ofsplit;
+        xtrp->hpv4_ind = 0;
+    }
+split_upper:
+    while (1) {
+        res = sg_start_io(rep, def_arr, pack_id, xtrp);
+        if (1 == res)
+            err_exit(ENOMEM, "sg starting out command");
+        else if (res < 0) {
+            pr2serr_lk("%ssg %s failed, blk=%" PRId64 "\n", my_name,
+                       wr_or_ver, rep->oblk);
+            status = pthread_mutex_unlock(mutexp);
+            if (0 != status) err_exit(status, "unlock out_mutex");
+            stop_both(clp);
+            goto fini;
+        }
+        /* Now release in mutex to let other reads run in parallel */
+        status = pthread_mutex_unlock(mutexp);
+        if (0 != status) err_exit(status, "unlock out_mutex");
+
+        res = sg_finish_io(rep->wr, rep, pack_id, xtrp);
+        switch (res) {
+        case SG_LIB_CAT_ABORTED_COMMAND:
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            /* try again with same addr, count info */
+            /* now re-acquire out mutex for balance */
+            /* N.B. This re-write could now be out of write sequence */
+            status = pthread_mutex_lock(mutexp);
+            if (0 != status) err_exit(status, "lock out_mutex");
+            break;      /* loops around */
+        case SG_LIB_CAT_MEDIUM_HARD:
+            if (0 == clp->out_flags.coe) {
+                pr2serr_lk("error finishing sg %s command (medium)\n",
+                           wr_or_ver);
+                if (exit_status <= 0)
+                    exit_status = res;
+                stop_both(clp);
+                goto fini;
+            } else
+                pr2serr_lk(">> ignored error for %s blk=%" PRId64 " for %d "
+                           "bytes\n", wr_or_ver, rep->oblk, nblks * rep->bs);
+#if defined(__GNUC__)
+#if (__GNUC__ >= 7)
+            __attribute__((fallthrough));
+            /* FALL THROUGH */
+#endif
+#endif
+        case SG_LIB_CAT_CONDITION_MET:
+        case 0:
+            if (! is_wr2) {
+                status = pthread_mutex_lock(mutexp);
+                if (0 != status) err_exit(status, "lock out_mutex");
+                if (rep->dio_incomplete_count || rep->resid) {
+                    clp->dio_incomplete_count += rep->dio_incomplete_count;
+                    clp->sum_of_resids += rep->resid;
+                }
+                clp->out_rem_count -= nblks;
+                status = pthread_mutex_unlock(mutexp);
+                if (0 != status) err_exit(status, "unlock out_mutex");
+            }
+            goto fini;
+        case SG_LIB_CAT_MISCOMPARE:
+            ++num_miscompare;
+            // fall through
+        default:
+            pr2serr_lk("error finishing sg %s command (%d)\n", wr_or_ver,
+                       res);
+            if (exit_status <= 0)
+                exit_status = res;
+            stop_both(clp);
+            goto fini;
+        }
+    }           /* end of while (1) loop */
+fini:
+    if (xtrp->dout_is_split) {  /* set up upper half of split */
+        if ((0 == xtrp->hpv4_ind) && (rep->num_blks > (int)ofsplit)) {
+            xtrp->hpv4_ind = 1;
+            xtrp->blk_offset = ofsplit;
+            xtrp->blks = rep->num_blks - ofsplit;
+            nblks = xtrp->blks;
+            status = pthread_mutex_lock(mutexp);
+            if (0 != status) err_exit(status, "lock out_mutex");
+            goto split_upper;
+        }
+    }
+    if (rep->has_share && is_wr2)
+        sg_wr_swap_share(rep, rep->outfd, false);
+}
+
+static int
+process_mrq_response(Rq_elem * rep, const struct sg_io_v4 * ctl_v4p,
+                     const struct sg_io_v4 * a_v4p, int num_mrq,
+                     uint32_t & good_inblks, uint32_t & good_outblks)
+{
+    struct global_collection * clp = rep->clp;
+    bool ok, all_good;
+    bool sb_in_co = !!(ctl_v4p->response);
+    int id = rep->id;
+    int resid = ctl_v4p->din_resid;
+    int sres = ctl_v4p->spare_out;
+    int n_subm = num_mrq - ctl_v4p->dout_resid;
+    int n_cmpl = ctl_v4p->info;
+    int n_good = 0;
+    int hole_count = 0;
+    int vb = clp->verbose;
+    int k, j, f1, slen, blen;
+    char b[160];
+
+    blen = sizeof(b);
+    good_inblks = 0;
+    good_outblks = 0;
+    if (vb > 2)
+        pr2serr_lk("[thread_id=%d] %s: num_mrq=%d, n_subm=%d, n_cmpl=%d\n",
+                   id, __func__, num_mrq, n_subm, n_cmpl);
+    if (n_subm < 0) {
+        pr2serr_lk("[%d] co.dout_resid(%d) > num_mrq(%d)\n", id,
+                   ctl_v4p->dout_resid, num_mrq);
+        return -1;
+    }
+    if (n_cmpl != (num_mrq - resid))
+        pr2serr_lk("[%d] co.info(%d) != (num_mrq(%d) - co.din_resid(%d))\n"
+                   "will use co.info\n", id, n_cmpl, num_mrq, resid);
+    if (n_cmpl > n_subm) {
+        pr2serr_lk("[%d] n_cmpl(%d) > n_subm(%d), use n_subm for both\n",
+                   id, n_cmpl, n_subm);
+        n_cmpl = n_subm;
+    }
+    if (sres) {
+        pr2serr_lk("[%d] secondary error: %s [%d], info=0x%x\n", id,
+                   strerror(sres), sres, ctl_v4p->info);
+        if (E2BIG == sres) {
+            sg_take_snap(rep->infd, id, true);
+            sg_take_snap(rep->outfd, id, true);
+        }
+    }
+    /* Check if those submitted have finished or not. N.B. If there has been
+     * an error then there may be "holes" (i.e. info=0x0) in the array due
+     * to completions being out-of-order. */
+    for (k = 0, j = 0; ((k < num_mrq) && (j < n_subm));
+         ++k, j += f1, ++a_v4p) {
+        slen = a_v4p->response_len;
+        if (! (SG_INFO_MRQ_FINI & a_v4p->info))
+            ++hole_count;
+        ok = true;
+        f1 = !!(a_v4p->info);   /* want to skip n_subm count if info is 0x0 */
+        if (SG_INFO_CHECK & a_v4p->info) {
+            ok = false;
+            pr2serr_lk("[%d] a_v4[%d]: SG_INFO_CHECK set [%s]\n", id, k,
+                       sg_info_str(a_v4p->info, sizeof(b), b));
+        }
+        if (sg_scsi_status_is_bad(a_v4p->device_status) ||
+            a_v4p->transport_status || a_v4p->driver_status) {
+            ok = false;
+            if (SAM_STAT_CHECK_CONDITION != a_v4p->device_status) {
+                pr2serr_lk("[%d] a_v4[%d]:\n", id, k);
+                if (vb)
+                    lk_chk_n_print4("  >>", a_v4p, vb > 4);
+            }
+        }
+        if (slen > 0) {
+            struct sg_scsi_sense_hdr ssh;
+            const uint8_t *sbp = (const uint8_t *)
+                        (sb_in_co ? ctl_v4p->response : a_v4p->response);
+
+            if (sg_scsi_normalize_sense(sbp, slen, &ssh) &&
+                (ssh.response_code >= 0x70)) {
+                if (ssh.response_code & 0x1)
+                    ok = true;
+                if (vb) {
+                    sg_get_sense_str("  ", sbp, slen, false, blen, b);
+                    pr2serr_lk("[%d] a_v4[%d]:\n%s\n", id, k, b);
+                }
+            }
+        }
+        if (ok && f1) {
+            ++n_good;
+            if (a_v4p->dout_xfer_len >= (uint32_t)rep->bs)
+                good_outblks += (a_v4p->dout_xfer_len - a_v4p->dout_resid) /
+                                rep->bs;
+            if (a_v4p->din_xfer_len >= (uint32_t)rep->bs)
+                good_inblks += (a_v4p->din_xfer_len - a_v4p->din_resid) /
+                               rep->bs;
+        }
+    }   /* end of request array scan loop */
+    if ((n_subm == num_mrq) || (vb < 3))
+        goto fini;
+    pr2serr_lk("[%d] checking response array _beyond_ number of "
+               "submissions [%d] to num_mrq:\n", id, k);
+    for (all_good = true; k < num_mrq; ++k, ++a_v4p) {
+        if (SG_INFO_MRQ_FINI & a_v4p->info) {
+            pr2serr_lk("[%d] a_v4[%d]: unexpected SG_INFO_MRQ_FINI set [%s]\n",
+                       id, k, sg_info_str(a_v4p->info, sizeof(b), b));
+            all_good = false;
+        }
+        if (a_v4p->device_status || a_v4p->transport_status ||
+            a_v4p->driver_status) {
+            pr2serr_lk("[%d] a_v4[%d]:\n", id, k);
+            lk_chk_n_print4("    ", a_v4p, vb > 4);
+            all_good = false;
+        }
+    }
+    if (all_good)
+        pr2serr_lk("    ... all good\n");
+fini:
+    return n_good;
+}
+
+#if 0
+static int
+chk_mrq_response(Rq_elem * rep, const struct sg_io_v4 * ctl_v4p,
+                 const struct sg_io_v4 * a_v4p, int nrq,
+                 uint32_t * good_inblksp, uint32_t * good_outblksp)
+{
+    struct global_collection * clp = rep->clp;
+    bool ok;
+    int id = rep->id;
+    int resid = ctl_v4p->din_resid;
+    int sres = ctl_v4p->spare_out;
+    int n_subm = nrq - ctl_v4p->dout_resid;
+    int n_cmpl = ctl_v4p->info;
+    int n_good = 0;
+    int vb = clp->verbose;
+    int k, slen, blen;
+    uint32_t good_inblks = 0;
+    uint32_t good_outblks = 0;
+    const struct sg_io_v4 * a_np = a_v4p;
+    char b[80];
+
+    blen = sizeof(b);
+    if (n_subm < 0) {
+        pr2serr_lk("[%d] %s: co.dout_resid(%d) > nrq(%d)\n", id, __func__,
+                   ctl_v4p->dout_resid, nrq);
+        return -1;
+    }
+    if (n_cmpl != (nrq - resid))
+        pr2serr_lk("[%d] %s: co.info(%d) != (nrq(%d) - co.din_resid(%d))\n"
+                   "will use co.info\n", id, __func__, n_cmpl, nrq, resid);
+    if (n_cmpl > n_subm) {
+        pr2serr_lk("[%d] %s: n_cmpl(%d) > n_subm(%d), use n_subm for both\n",
+                   id, __func__, n_cmpl, n_subm);
+        n_cmpl = n_subm;
+    }
+    if (sres) {
+        pr2serr_lk("[%d] %s: secondary error: %s [%d], info=0x%x\n", id,
+                   __func__, strerror(sres), sres, ctl_v4p->info);
+        if (E2BIG == sres) {
+            sg_take_snap(rep->infd, id, true);
+            sg_take_snap(rep->outfd, id, true);
+        }
+    }
+    /* Check if those submitted have finished or not */
+    for (k = 0; k < n_subm; ++k, ++a_np) {
+        slen = a_np->response_len;
+        if (! (SG_INFO_MRQ_FINI & a_np->info)) {
+            pr2serr_lk("[%d] %s, a_n[%d]: missing SG_INFO_MRQ_FINI [%s]\n",
+                       id, __func__, k, sg_info_str(a_np->info, blen, b));
+            v4hdr_out_lk("a_np", a_np, id);
+            v4hdr_out_lk("cop", ctl_v4p, id);
+        }
+        ok = true;
+        if (SG_INFO_CHECK & a_np->info) {
+            ok = false;
+            pr2serr_lk("[%d] a_n[%d]: SG_INFO_CHECK set [%s]\n", id, k,
+                       sg_info_str(a_np->info, sizeof(b), b));
+        }
+        if (sg_scsi_status_is_bad(a_np->device_status) ||
+            a_np->transport_status || a_np->driver_status) {
+            ok = false;
+            if (SAM_STAT_CHECK_CONDITION != a_np->device_status) {
+                pr2serr_lk("[%d] %s, a_n[%d]:\n", id, __func__, k);
+                if (vb)
+                    lk_chk_n_print4("  >>", a_np, false);
+            }
+        }
+        if (slen > 0) {
+            struct sg_scsi_sense_hdr ssh;
+            const uint8_t *sbp = (const uint8_t *)a_np->response;
+
+            if (sg_scsi_normalize_sense(sbp, slen, &ssh) &&
+                (ssh.response_code >= 0x70)) {
+                char b[256];
+
+                if (ssh.response_code & 0x1)
+                    ok = true;
+                if (vb) {
+                    sg_get_sense_str("  ", sbp, slen, false, sizeof(b), b);
+                    pr2serr_lk("[%d] %s, a_n[%d]:\n%s\n", id, __func__, k, b);
+                }
+            }
+        }
+        if (ok) {
+            ++n_good;
+            if (a_np->dout_xfer_len >= (uint32_t)rep->bs)
+                good_outblks += (a_np->dout_xfer_len - a_np->dout_resid) /
+                                rep->bs;
+            if (a_np->din_xfer_len >= (uint32_t)rep->bs)
+                good_inblks += (a_np->din_xfer_len - a_np->din_resid) /
+                               rep->bs;
+        }
+    }
+    if ((n_subm == nrq) || (vb < 3))
+        goto fini;
+    pr2serr_lk("[%d] %s: checking response array beyond number of "
+               "submissions:\n", id, __func__);
+    for (k = n_subm; k < nrq; ++k, ++a_np) {
+        if (SG_INFO_MRQ_FINI & a_np->info)
+            pr2serr_lk("[%d] %s, a_n[%d]: unexpected SG_INFO_MRQ_FINI set\n",
+                       id, __func__, k);
+        if (a_np->device_status || a_np->transport_status ||
+            a_np->driver_status) {
+            pr2serr_lk("[%d] %s, a_n[%d]:\n", id, __func__, k);
+            lk_chk_n_print4("    ", a_np, vb > 4);
+        }
+    }
+fini:
+    if (good_inblksp)
+        *good_inblksp = good_inblks;
+    if (good_outblksp)
+        *good_outblksp = good_outblks;
+    return n_good;
+}
+#endif
+
+/* do mrq 'full non-blocking' invocation so both submission and completion
+ * is async (i.e. uses SGV4_FLAG_IMMED flag). This type of mrq is
+ * restricted to a single file descriptor (i.e. the 'fd' argument). */
+static int
+sgh_do_async_mrq(Rq_elem * rep, mrq_arr_t & def_arr, int fd,
+                 struct sg_io_v4 * ctlop, int nrq)
+{
+    int half = nrq / 2;
+    int k, res, nwait, half_num, rest, err, num_good, b_len;
+    const int64_t wait_us = 10;
+    uint32_t in_fin_blks, out_fin_blks;
+    struct sg_io_v4 * a_v4p;
+    struct sg_io_v4 hold_ctlo;
+    struct global_collection * clp = rep->clp;
+    char b[80];
+
+    hold_ctlo = *ctlop;
+    b_len = sizeof(b);
+    a_v4p = def_arr.first.data();
+    ctlop->flags = SGV4_FLAG_MULTIPLE_REQS;
+    if (clp->in_flags.polled || clp->out_flags.polled) {
+        /* submit of full non-blocking with POLLED */
+        ctlop->flags |= (SGV4_FLAG_IMMED | SGV4_FLAG_POLLED);
+        if (!after1 && (clp->verbose > 1)) {
+            after1 = true;
+            pr2serr_lk("%s: %s\n", __func__, mrq_s_nb_s);
+        }
+    } else {
+        ctlop->flags |= SGV4_FLAG_IMMED;        /* submit non-blocking */
+        if (!after1 && (clp->verbose > 1)) {
+            after1 = true;
+            pr2serr_lk("%s: %s\n", __func__, mrq_s_nb_s);
+        }
+    }
+    if (clp->verbose > 4) {
+        pr2serr_lk("%s: Controlling object _before_ ioctl(SG_IOSUBMIT):\n",
+                   __func__);
+        if (clp->verbose > 5)
+            hex2stderr_lk((const uint8_t *)ctlop, sizeof(*ctlop), 1);
+        v4hdr_out_lk("Controlling object before", ctlop, rep->id);
+    }
+    res = ioctl(fd, SG_IOSUBMIT, ctlop);
+    if (res < 0) {
+        err = errno;
+        if (E2BIG == err)
+            sg_take_snap(fd, rep->id, true);
+        pr2serr_lk("%s: ioctl(SG_IOSUBMIT, %s)-->%d, errno=%d: %s\n", __func__,
+                   sg_flags_str(ctlop->flags, b_len, b), res, err,
+                   strerror(err));
+        return -1;
+    }
+    /* fetch first half */
+    for (k = 0; k < 100000; ++k) {
+        ++num_waiting_calls;
+        res = ioctl(fd, SG_GET_NUM_WAITING, &nwait);
+        if (res < 0) {
+            err = errno;
+            pr2serr_lk("%s: ioctl(SG_GET_NUM_WAITING)-->%d, errno=%d: %s\n",
+                       __func__, res, err, strerror(err));
+            return -1;
+        }
+        if (nwait >= half)
+            break;
+        this_thread::sleep_for(chrono::microseconds{wait_us});
+    }
+    ctlop->flags = (SGV4_FLAG_MULTIPLE_REQS | SGV4_FLAG_IMMED);
+    res = ioctl(fd, SG_IORECEIVE, ctlop);
+    if (res < 0) {
+        err = errno;
+        if (ENODATA != err) {
+            pr2serr_lk("%s: ioctl(SG_IORECEIVE, %s),1-->%d, errno=%d: %s\n",
+                       __func__, sg_flags_str(ctlop->flags, b_len, b), res,
+                       err, strerror(err));
+            return -1;
+        }
+        half_num = 0;
+    } else
+        half_num = ctlop->info;
+    if (clp->verbose > 4) {
+        pr2serr_lk("%s: Controlling object output by ioctl(SG_IORECEIVE),1: "
+                   "num_received=%d\n", __func__, half_num);
+        if (clp->verbose > 5)
+            hex2stderr_lk((const uint8_t *)ctlop, sizeof(*ctlop), 1);
+        v4hdr_out_lk("Controlling object after", ctlop, rep->id);
+        if (clp->verbose > 5) {
+            for (k = 0; k < half_num; ++k) {
+                pr2serr_lk("AFTER: def_arr[%d]:\n", k);
+                v4hdr_out_lk("normal v4 object", (a_v4p + k), rep->id);
+                // hex2stderr_lk((const uint8_t *)(a_v4p + k), sizeof(*a_v4p),
+                                  // 1);
+            }
+        }
+    }
+    in_fin_blks = 0;
+    out_fin_blks = 0;
+    num_good = process_mrq_response(rep, ctlop, a_v4p, half_num, in_fin_blks,
+                                    out_fin_blks);
+    if (clp->verbose > 2)
+        pr2serr_lk("%s: >>>1 num_good=%d, in_q/fin blks=%u/%u;  out_q/fin "
+                   "blks=%u/%u\n", __func__, num_good, rep->in_mrq_q_blks,
+                   in_fin_blks, rep->out_mrq_q_blks, out_fin_blks);
+
+    if (num_good < 0)
+        res = -1;
+    else if (num_good < half_num) {
+        int resid_blks = rep->in_mrq_q_blks - in_fin_blks;
+
+        if (resid_blks > 0)
+            gcoll.in_rem_count += resid_blks;
+        resid_blks = rep->out_mrq_q_blks - out_fin_blks;
+        if (resid_blks > 0)
+            gcoll.out_rem_count += resid_blks;
+
+        return -1;
+    }
+
+    rest = nrq - half_num;
+    if (rest < 1)
+        goto fini;
+    /* fetch remaining */
+    for (k = 0; k < 100000; ++k) {
+        ++num_waiting_calls;
+        res = ioctl(fd, SG_GET_NUM_WAITING, &nwait);
+        if (res < 0) {
+            pr2serr_lk("%s: ioctl(SG_GET_NUM_WAITING)-->%d, errno=%d: %s\n",
+                       __func__, res, errno, strerror(errno));
+            return -1;
+        }
+        if (nwait >= rest)
+            break;
+        this_thread::sleep_for(chrono::microseconds{wait_us});
+    }
+    ctlop = &hold_ctlo;
+    ctlop->din_xferp += (half_num * sizeof(struct sg_io_v4));
+    ctlop->din_xfer_len -= (half_num * sizeof(struct sg_io_v4));
+    ctlop->dout_xferp = ctlop->din_xferp;
+    ctlop->dout_xfer_len = ctlop->din_xfer_len;
+    ctlop->flags = (SGV4_FLAG_MULTIPLE_REQS | SGV4_FLAG_IMMED);
+    res = ioctl(fd, SG_IORECEIVE, ctlop);
+    if (res < 0) {
+        err = errno;
+        if (ENODATA != err) {
+            pr2serr_lk("%s: ioctl(SG_IORECEIVE, %s),2-->%d, errno=%d: %s\n",
+                       __func__, sg_flags_str(ctlop->flags, b_len, b), res,
+                       err, strerror(err));
+            return -1;
+        }
+        half_num = 0;
+    } else
+        half_num = ctlop->info;
+    if (clp->verbose > 4) {
+        pr2serr_lk("%s: Controlling object output by ioctl(SG_IORECEIVE),2: "
+                   "num_received=%d\n", __func__, half_num);
+        if (clp->verbose > 5)
+            hex2stderr_lk((const uint8_t *)ctlop, sizeof(*ctlop), 1);
+        v4hdr_out_lk("Controlling object after", ctlop, rep->id);
+        if (clp->verbose > 5) {
+            for (k = 0; k < half_num; ++k) {
+                pr2serr_lk("AFTER: def_arr[%d]:\n", k);
+                v4hdr_out_lk("normal v4 object", (a_v4p + k), rep->id);
+                // hex2stderr_lk((const uint8_t *)(a_v4p + k), sizeof(*a_v4p),
+                                  // 1);
+            }
+        }
+    }
+    in_fin_blks = 0;
+    out_fin_blks = 0;
+    num_good = process_mrq_response(rep, ctlop, a_v4p, half_num, in_fin_blks,
+                                    out_fin_blks);
+    if (clp->verbose > 2)
+        pr2serr_lk("%s: >>>2 num_good=%d, in_q/fin blks=%u/%u;  out_q/fin "
+                   "blks=%u/%u\n", __func__, num_good, rep->in_mrq_q_blks,
+                   in_fin_blks, rep->out_mrq_q_blks, out_fin_blks);
+
+    if (num_good < 0)
+        res = -1;
+    else if (num_good < half_num) {
+        int resid_blks = rep->in_mrq_q_blks - in_fin_blks;
+
+        if (resid_blks > 0)
+            gcoll.in_rem_count += resid_blks;
+        resid_blks = rep->out_mrq_q_blks - out_fin_blks;
+        if (resid_blks > 0)
+            gcoll.out_rem_count += resid_blks;
+
+        res = -1;
+    }
+
+fini:
+    return res;
+}
+
+/* Split def_arr into fd_def_arr and o_fd_arr based on whether each element's
+ * flags field has SGV4_FLAG_DO_ON_OTHER set. If it is set place in
+ * o_fd_def_arr and mask out SGV4_DO_ON_OTHER. Returns number of elements
+ * in o_fd_def_arr. */
+static int
+split_def_arr(const mrq_arr_t & def_arr, mrq_arr_t & fd_def_arr,
+              mrq_arr_t & o_fd_def_arr)
+{
+    int nrq, k;
+    int res = 0;
+    const struct sg_io_v4 * a_v4p;
+
+    a_v4p = def_arr.first.data();
+    nrq = def_arr.first.size();
+
+    for (k = 0; k < nrq; ++k) {
+        int flags;
+        const struct sg_io_v4 * h4p = a_v4p + k;
+
+        flags = h4p->flags;
+        if (flags & SGV4_FLAG_DO_ON_OTHER) {
+            o_fd_def_arr.first.push_back(def_arr.first[k]);
+            o_fd_def_arr.second.push_back(def_arr.second[k]);
+            flags &= ~SGV4_FLAG_DO_ON_OTHER;    /* mask out DO_ON_OTHER */
+            o_fd_def_arr.first[res].flags = flags;
+            ++res;
+        } else {
+            fd_def_arr.first.push_back(def_arr.first[k]);
+            fd_def_arr.second.push_back(def_arr.second[k]);
+        }
+    }
+    return res;
+}
+
+/* This function sets up a multiple request (mrq) transaction and sends it
+ * to the pass-through. Returns 0 on success, 1 if ENOMEM error else -1 for
+ * other errors. */
+static int
+sgh_do_deferred_mrq(Rq_elem * rep, mrq_arr_t & def_arr)
+{
+    bool launch_mrq_abort = false;
+    int nrq, k, res, fd, mrq_pack_id, status, id, num_good, b_len;
+    uint32_t in_fin_blks, out_fin_blks;
+    const int max_cdb_sz = 16;
+    struct sg_io_v4 * a_v4p;
+    struct sg_io_v4 ctl_v4 {};
+    uint8_t * cmd_ap = NULL;
+    struct global_collection * clp = rep->clp;
+    const char * iosub_str = "iosub_str";
+    char b[80];
+
+    id = rep->id;
+    b_len = sizeof(b);
+    ctl_v4.guard = 'Q';
+    a_v4p = def_arr.first.data();
+    nrq = def_arr.first.size();
+    if (nrq < 1) {
+        pr2serr_lk("[%d] %s: strange nrq=0, nothing to do\n", id, __func__);
+        return 0;
+    }
+    if (clp->mrq_cmds) {
+        cmd_ap = (uint8_t *)calloc(nrq, max_cdb_sz);
+        if (NULL == cmd_ap) {
+            pr2serr_lk("[%d] %s: no memory for calloc(%d * 16)\n", id,
+                       __func__, nrq);
+            return 1;
+        }
+    }
+    for (k = 0; k < nrq; ++k) {
+        struct sg_io_v4 * h4p = a_v4p + k;
+        uint8_t *cmdp = &def_arr.second[k].front();
+
+        if (clp->mrq_cmds) {
+            memcpy(cmd_ap + (k * max_cdb_sz), cmdp, h4p->request_len);
+            h4p->request = 0;
+        } else
+            h4p->request = (uint64_t)cmdp;
+        if (clp->verbose > 5) {
+            pr2serr_lk("%s%s[%d] def_arr[%d]", ((0 == k) ? __func__ : ""),
+                       ((0 == k) ? ": " : ""), id, k);
+            if (h4p->din_xferp)
+                pr2serr_lk(" [din=0x%p]:\n", (void *)h4p->din_xferp);
+            else if (h4p->dout_xferp)
+                pr2serr_lk(" [dout=0x%p]:\n", (void *)h4p->dout_xferp);
+            else
+                pr2serr_lk(":\n");
+            hex2stderr_lk((const uint8_t *)h4p, sizeof(*h4p), 1);
+        }
+    }
+    if (rep->both_sg || rep->same_sg)
+        fd = rep->infd;         /* assume share to rep->outfd */
+    else if (rep->only_in_sg)
+        fd = rep->infd;
+    else if (rep->only_out_sg)
+        fd = rep->outfd;
+    else {
+        pr2serr_lk("[%d] %s: why am I here? No sg devices\n", id, __func__);
+        res = -1;
+        goto fini;
+    }
+    res = 0;
+    if (clp->mrq_cmds) {
+        ctl_v4.request_len = nrq * max_cdb_sz;
+        ctl_v4.request = (uint64_t)cmd_ap;
+    }
+    ctl_v4.flags = SGV4_FLAG_MULTIPLE_REQS;
+    if (! clp->mrq_async) {
+        ctl_v4.flags |= SGV4_FLAG_STOP_IF;
+        if (clp->in_flags.mrq_svb || clp->out_flags.mrq_svb)
+            ctl_v4.flags |= SGV4_FLAG_SHARE;
+    }
+    ctl_v4.dout_xferp = (uint64_t)a_v4p;        /* request array */
+    ctl_v4.dout_xfer_len = nrq * sizeof(*a_v4p);
+    ctl_v4.din_xferp = (uint64_t)a_v4p;         /* response array */
+    ctl_v4.din_xfer_len = nrq * sizeof(*a_v4p);
+    mrq_pack_id = atomic_fetch_add(&mono_mrq_id, 1);
+    if ((clp->m_aen > 0) && (MONO_MRQ_ID_INIT != mrq_pack_id) &&
+        (0 == ((mrq_pack_id - MONO_MRQ_ID_INIT) % clp->m_aen))) {
+        launch_mrq_abort = true;
+        if (clp->verbose > 2)
+            pr2serr_lk("[%d] %s: Decide to launch MRQ abort thread, "
+                       "mrq_id=%d\n", id, __func__, mrq_pack_id);
+        memset(&rep->mai, 0, sizeof(rep->mai));
+        rep->mai.from_tid = id;
+        rep->mai.mrq_id = mrq_pack_id;
+        rep->mai.fd = fd;
+        rep->mai.debug = clp->verbose;
+
+        status = pthread_create(&rep->mrq_abort_thread_id, NULL,
+                                mrq_abort_thread, (void *)&rep->mai);
+        if (0 != status) err_exit(status, "pthread_create, sig...");
+    }
+    ctl_v4.request_extra = launch_mrq_abort ? mrq_pack_id : 0;
+    rep->mrq_id = mrq_pack_id;
+    if (clp->verbose && rep->both_sg && clp->mrq_async)
+        iosub_str = "SG_IOSUBMIT(variable)";
+    if (clp->verbose > 4) {
+        pr2serr_lk("%s: Controlling object _before_ ioctl(%s):\n",
+                   __func__, iosub_str);
+        if (clp->verbose > 5)
+            hex2stderr_lk((const uint8_t *)&ctl_v4, sizeof(ctl_v4), 1);
+        v4hdr_out_lk("Controlling object before", &ctl_v4, id);
+    }
+    if (clp->mrq_async && (! rep->both_sg)) {
+        /* do 'submit non-blocking' or 'full non-blocking' mrq */
+        mrq_arr_t fd_def_arr;
+        mrq_arr_t o_fd_def_arr;
+
+        /* need to deconstruct def_arr[] into two separate lists, one for
+         * the source, the other for the destination. */
+        int o_num_fd = split_def_arr(def_arr, fd_def_arr, o_fd_def_arr);
+        int num_fd = fd_def_arr.first.size();
+        if (num_fd > 0) {
+            struct sg_io_v4 fd_ctl = ctl_v4;
+            struct sg_io_v4 * aa_v4p = fd_def_arr.first.data();
+
+            for (k = 0; k < num_fd; ++k) {
+                struct sg_io_v4 * h4p = aa_v4p + k;
+                uint8_t *cmdp = &fd_def_arr.second[k].front();
+
+                if (clp->mrq_cmds) {
+                    memcpy(cmd_ap + (k * max_cdb_sz), cmdp, h4p->request_len);
+                    h4p->request = 0;
+                } else
+                    h4p->request = (uint64_t)cmdp;
+                if (clp->verbose > 5) {
+                    pr2serr_lk("[%d] df_def_arr[%d]:\n", id, k);
+                    hex2stderr_lk((const uint8_t *)(aa_v4p + k),
+                                  sizeof(*aa_v4p), 1);
+                }
+            }
+            fd_ctl.dout_xferp = (uint64_t)aa_v4p;        /* request array */
+            fd_ctl.dout_xfer_len = num_fd * sizeof(*aa_v4p);
+            fd_ctl.din_xferp = (uint64_t)aa_v4p;         /* response array */
+            fd_ctl.din_xfer_len = num_fd * sizeof(*aa_v4p);
+            fd_ctl.request_extra = launch_mrq_abort ? mrq_pack_id : 0;
+            /* this is the source side mrq command */
+            res = sgh_do_async_mrq(rep, fd_def_arr, fd, &fd_ctl, num_fd);
+            rep->in_mrq_q_blks = 0;
+            if (res)
+                goto fini;
+        }
+        if (o_num_fd > 0) {
+            struct sg_io_v4 o_fd_ctl = ctl_v4;
+            struct sg_io_v4 * aa_v4p = o_fd_def_arr.first.data();
+
+            for (k = 0; k < o_num_fd; ++k) {
+                struct sg_io_v4 * h4p = aa_v4p + k;
+                uint8_t *cmdp = &o_fd_def_arr.second[k].front();
+
+                if (clp->mrq_cmds) {
+                    memcpy(cmd_ap + (k * max_cdb_sz), cmdp, h4p->request_len);
+                    h4p->request = 0;
+                } else
+                    h4p->request = (uint64_t)cmdp;
+                if (clp->verbose > 5) {
+                    pr2serr_lk("[%d] o_fd_def_arr[%d]:\n", id, k);
+                    hex2stderr_lk((const uint8_t *)(aa_v4p + k),
+                                  sizeof(*aa_v4p), 1);
+                }
+            }
+            o_fd_ctl.dout_xferp = (uint64_t)aa_v4p;     /* request array */
+            o_fd_ctl.dout_xfer_len = o_num_fd * sizeof(*aa_v4p);
+            o_fd_ctl.din_xferp = (uint64_t)aa_v4p;      /* response array */
+            o_fd_ctl.din_xfer_len = o_num_fd * sizeof(*aa_v4p);
+            o_fd_ctl.request_extra = launch_mrq_abort ? mrq_pack_id : 0;
+            /* this is the destination side mrq command */
+            res = sgh_do_async_mrq(rep, o_fd_def_arr, rep->outfd, &o_fd_ctl,
+                                   o_num_fd);
+            rep->out_mrq_q_blks = 0;
+        }
+        goto fini;
+    }
+
+try_again:
+    if (clp->unbalanced_mrq) {
+        iosub_str = "SG_IOSUBMIT(variable_blocking)";
+        if (!after1 && (clp->verbose > 1)) {
+            after1 = true;
+            pr2serr_lk("%s: unbalanced %s\n", __func__, mrq_vb_s);
+        }
+        res = ioctl(fd, SG_IOSUBMIT, &ctl_v4);
+    } else {
+        if (clp->mrq_async) {
+            iosub_str = "SG_IOSUBMIT(variable_blocking)";
+            if (!after1 && (clp->verbose > 1)) {
+                after1 = true;
+                pr2serr_lk("%s: %s\n", __func__, mrq_vb_s);
+            }
+            res = ioctl(fd, SG_IOSUBMIT, &ctl_v4);
+        } else if (clp->in_flags.mrq_svb || clp->out_flags.mrq_svb) {
+            iosub_str = "SG_IOSUBMIT(shared_variable_blocking)";
+            if (!after1 && (clp->verbose > 1)) {
+                after1 = true;
+                pr2serr_lk("%s: %s\n", __func__, mrq_svb_s);
+            }
+            res = ioctl(fd, SG_IOSUBMIT, &ctl_v4);
+        } else {
+            iosub_str = "SG_IO(ordered_blocking)";
+            if (!after1 && (clp->verbose > 1)) {
+                after1 = true;
+                pr2serr_lk("%s: %s\n", __func__, mrq_blk_s);
+            }
+            res = ioctl(fd, SG_IO, &ctl_v4);
+        }
+    }
+    if (res < 0) {
+        int err = errno;
+
+        if (E2BIG == err)
+                sg_take_snap(fd, id, true);
+        else if (EBUSY == err) {
+            ++num_ebusy;
+            std::this_thread::yield();/* allow another thread to progress */
+            goto try_again;
+        }
+        pr2serr_lk("%s: ioctl(%s, %s)-->%d, errno=%d: %s\n",
+                   __func__, iosub_str, sg_flags_str(ctl_v4.flags, b_len, b),
+                   res, err, strerror(err));
+        res = -1;
+        goto fini;
+    }
+    if (clp->verbose && vb_first_time.load()) {
+        pr2serr_lk("First controlling object output by ioctl(%s), flags: "
+                   "%s\n", iosub_str, sg_flags_str(ctl_v4.flags, b_len, b));
+        vb_first_time.store(false);
+    } else if (clp->verbose > 4)
+        pr2serr_lk("%s: Controlling object output by ioctl(%s):\n",
+                   __func__, iosub_str);
+    if (clp->verbose > 4) {
+        if (clp->verbose > 5)
+            hex2stderr_lk((const uint8_t *)&ctl_v4, sizeof(ctl_v4), 1);
+        v4hdr_out_lk("Controlling object after", &ctl_v4, id);
+        if (clp->verbose > 5) {
+            for (k = 0; k < nrq; ++k) {
+                pr2serr_lk("AFTER: def_arr[%d]:\n", k);
+                v4hdr_out_lk("normal v4 object", (a_v4p + k), id);
+                // hex2stderr_lk((const uint8_t *)(a_v4p + k), sizeof(*a_v4p),
+                                  // 1);
+            }
+        }
+    }
+    in_fin_blks = 0;
+    out_fin_blks = 0;
+    num_good = process_mrq_response(rep, &ctl_v4, a_v4p, nrq, in_fin_blks,
+                                    out_fin_blks);
+    if (clp->verbose > 2)
+        pr2serr_lk("%s: >>> num_good=%d, in_q/fin blks=%u/%u;  out_q/fin "
+                   "blks=%u/%u\n", __func__, num_good, rep->in_mrq_q_blks,
+                   in_fin_blks, rep->out_mrq_q_blks, out_fin_blks);
+
+    if (num_good < 0)
+        res = -1;
+    else if (num_good < nrq) {
+        int resid_blks = rep->in_mrq_q_blks - in_fin_blks;
+
+        if (resid_blks > 0)
+            gcoll.in_rem_count += resid_blks;
+        resid_blks = rep->out_mrq_q_blks - out_fin_blks;
+        if (resid_blks > 0)
+            gcoll.out_rem_count += resid_blks;
+
+        res = -1;
+    }
+    rep->in_mrq_q_blks = 0;
+    rep->out_mrq_q_blks = 0;
+fini:
+    def_arr.first.clear();
+    def_arr.second.clear();
+    if (cmd_ap)
+        free(cmd_ap);
+    if (launch_mrq_abort) {
+        if (clp->verbose > 1)
+            pr2serr_lk("[%d] %s: About to join MRQ abort thread, "
+                       "mrq_id=%d\n", id, __func__, mrq_pack_id);
+
+        void * vp;      /* not used */
+        status = pthread_join(rep->mrq_abort_thread_id, &vp);
+        if (0 != status) err_exit(status, "pthread_join");
+    }
+    return res;
+}
+
+/* Returns 0 on success, 1 if ENOMEM error else -1 for other errors. */
+static int
+sg_start_io(Rq_elem * rep, mrq_arr_t & def_arr, int & pack_id,
+            struct sg_io_extra *xtrp)
+{
+    struct global_collection * clp = rep->clp;
+    bool wr = rep->wr;
+    bool fua = wr ? clp->out_flags.fua : clp->in_flags.fua;
+    bool dpo = wr ? clp->out_flags.dpo : clp->in_flags.dpo;
+    bool dio = wr ? clp->out_flags.dio : clp->in_flags.dio;
+    bool mmap = wr ? clp->out_flags.mmap : clp->in_flags.mmap;
+    bool noxfer = wr ? clp->out_flags.noxfer : clp->in_flags.noxfer;
+    bool v4 = wr ? clp->out_flags.v4 : clp->in_flags.v4;
+    bool qhead = wr ? clp->out_flags.qhead : clp->in_flags.qhead;
+    bool qtail = wr ? clp->out_flags.qtail : clp->in_flags.qtail;
+    bool polled = wr ? clp->out_flags.polled : clp->in_flags.polled;
+    bool mout_if = wr ? clp->out_flags.mout_if : clp->in_flags.mout_if;
+    bool prefetch = xtrp ? xtrp->prefetch : false;
+    bool is_wr2 = xtrp ? xtrp->is_wr2 : false;
+    int cdbsz = wr ? clp->cdbsz_out : clp->cdbsz_in;
+    int flags = 0;
+    int res, err, fd, b_len, nblks, blk_off;
+    int64_t blk = wr ? rep->oblk : rep->iblk;
+    struct sg_io_hdr * hp = &rep->io_hdr;
+    struct sg_io_v4 * h4p = &rep->io_hdr4[xtrp ? xtrp->hpv4_ind : 0];
+    const char * cp = "";
+    const char * crwp;
+    char b[80];
+
+    b_len = sizeof(b);
+    if (wr) {
+        fd = is_wr2 ? rep->out2fd : rep->outfd;
+        if (clp->verify) {
+            crwp = is_wr2 ? "verifying2" : "verifying";
+            if (prefetch)
+                crwp = is_wr2 ? "prefetch2" : "prefetch";
+        } else
+            crwp = is_wr2 ? "writing2" : "writing";
+    } else {
+        fd = rep->infd;
+        crwp = "reading";
+    }
+    if (qhead)
+        qtail = false;          /* qhead takes precedence */
+
+    if (v4 && xtrp && xtrp->dout_is_split) {
+        res = sg_build_scsi_cdb(rep->cmd, cdbsz, xtrp->blks,
+                                blk + (unsigned int)xtrp->blk_offset,
+                                clp->verify, true, fua, dpo);
+    } else
+        res = sg_build_scsi_cdb(rep->cmd, cdbsz, rep->num_blks, blk,
+                                wr ? clp->verify : false, wr, fua, dpo);
+    if (res) {
+        pr2serr_lk("%sbad cdb build, start_blk=%" PRId64 ", blocks=%d\n",
+                   my_name, blk, rep->num_blks);
+        return -1;
+    }
+    if (prefetch) {
+        if (cdbsz == 10)
+            rep->cmd[0] = SGP_PRE_FETCH10;
+        else if (cdbsz == 16)
+            rep->cmd[0] = SGP_PRE_FETCH16;
+        else {
+            pr2serr_lk("%sbad PRE-FETCH build, start_blk=%" PRId64 ", "
+                       "blocks=%d\n", my_name, blk, rep->num_blks);
+            return -1;
+        }
+        rep->cmd[1] = 0x2;      /* set IMMED (no fua or dpo) */
+    }
+    if (mmap && (clp->noshare || (rep->outregfd >= 0)))
+        flags |= SG_FLAG_MMAP_IO;
+    if (noxfer)
+        flags |= SG_FLAG_NO_DXFER;
+    if (dio)
+        flags |= SG_FLAG_DIRECT_IO;
+    if (polled)
+        flags |= SGV4_FLAG_POLLED;
+    if (qhead)
+        flags |= SG_FLAG_Q_AT_HEAD;
+    if (qtail)
+        flags |= SG_FLAG_Q_AT_TAIL;
+    if (mout_if)
+        flags |= SGV4_FLAG_META_OUT_IF;
+    if (rep->has_share) {
+        flags |= SGV4_FLAG_SHARE;
+        if (wr)
+            flags |= SGV4_FLAG_NO_DXFER;
+        else if (rep->outregfd < 0)
+            flags |= SGV4_FLAG_NO_DXFER;
+
+        cp = (wr ? " write_side active" : " read_side active");
+    } else
+        cp = (wr ? " write-side not sharing" : " read_side not sharing");
+    if (rep->both_sg) {
+        if (wr)
+            pack_id = rep->rd_p_id + 1;
+        else {
+            pack_id = 2 * atomic_fetch_add(&mono_pack_id, 1);
+            rep->rd_p_id = pack_id;
+        }
+    } else
+        pack_id = atomic_fetch_add(&mono_pack_id, 1);    /* fetch before */
+    rep->rq_id = pack_id;
+    nblks = rep->num_blks;
+    blk_off = 0;
+    if (clp->verbose && 0 == clp->nmrqs && vb_first_time.load()) {
+        vb_first_time.store(false);
+        pr2serr("First normal IO: %s, flags: %s\n", cp,
+                sg_flags_str(flags, b_len, b));
+    }
+    if (v4) {
+        memset(h4p, 0, sizeof(struct sg_io_v4));
+        if (clp->nmrqs > 0) {
+            if (rep->both_sg && (rep->outfd == fd))
+                flags |= SGV4_FLAG_DO_ON_OTHER;
+        }
+        if (xtrp && xtrp->dout_is_split && (nblks > 0)) {
+            if (1 == xtrp->hpv4_ind) {
+                flags |= SGV4_FLAG_DOUT_OFFSET;
+                blk_off = xtrp->blk_offset;
+                h4p->spare_in = clp->bs * blk_off;
+            }
+            nblks = xtrp->blks;
+            if ((0 == xtrp->hpv4_ind) && (nblks < rep->num_blks))
+                flags |= SGV4_FLAG_KEEP_SHARE;
+        }
+        if (clp->ofile2_given && wr && rep->has_share && ! is_wr2)
+            flags |= SGV4_FLAG_KEEP_SHARE; /* set on first write only */
+        else if (clp->fail_mask & 1)
+            flags |= SGV4_FLAG_KEEP_SHARE; /* troublemaking .... */
+    } else
+        memset(hp, 0, sizeof(struct sg_io_hdr));
+    if (clp->verbose > 3) {
+        bool lock = true;
+        char prefix[128];
+
+        if (4 == clp->verbose) {
+            snprintf(prefix, sizeof(prefix), "tid,rq_id=%d,%d: ", rep->id,
+                     pack_id);
+            lock = false;
+        } else {
+            prefix[0] = '\0';
+            pr2serr_lk("%s tid,rq_id=%d,%d: SCSI %s%s %s, blk=%" PRId64
+                       " num_blks=%d\n", __func__, rep->id, pack_id, crwp, cp,
+                       sg_flags_str(flags, b_len, b), blk + blk_off, nblks);
+        }
+        lk_print_command_len(prefix, rep->cmd, cdbsz, lock);
+    }
+    if (v4)
+        goto do_v4;     // <<<<<<<<<<<<<<< look further down
+
+    hp->interface_id = 'S';
+    hp->cmd_len = cdbsz;
+    hp->cmdp = rep->cmd;
+    hp->dxferp = get_buffp(rep);
+    hp->dxfer_len = clp->bs * rep->num_blks;
+    if (!wr)
+        hp->dxfer_direction = SG_DXFER_FROM_DEV;
+    else if (prefetch) {
+        hp->dxfer_direction = SG_DXFER_NONE;
+        hp->dxfer_len = 0;
+        hp->dxferp = NULL;
+    } else
+        hp->dxfer_direction = SG_DXFER_TO_DEV;
+    hp->mx_sb_len = sizeof(rep->sb);
+    hp->sbp = rep->sb;
+    hp->timeout = clp->cmd_timeout;
+    hp->usr_ptr = rep;
+    hp->pack_id = pack_id;
+    hp->flags = flags;
+
+    while (((res = write(fd, hp, sizeof(struct sg_io_hdr))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno))) {
+        if (EAGAIN == errno) {
+            ++num_start_eagain;
+#ifdef SGH_DD_SNAP_DEV
+            if (0 == (num_ebusy % 1000))
+                sg_take_snap(fd, rep->id, (clp->verbose > 2));
+#endif
+        } else if (EBUSY == errno) {
+            ++num_ebusy;
+#ifdef SGH_DD_SNAP_DEV
+            if (0 == (num_ebusy % 1000))
+                sg_take_snap(fd, rep->id, (clp->verbose > 2));
+#endif
+        }
+        std::this_thread::yield();/* another thread may be able to progress */
+    }
+    err = errno;
+    if (res < 0) {
+        if (ENOMEM == err)
+            return 1;
+        pr2serr_lk("%s tid=%d: %s %s write(2) failed: %s\n", __func__,
+                   rep->id, cp, sg_flags_str(hp->flags, b_len, b),
+                   strerror(err));
+        return -1;
+    }
+    return 0;
+
+do_v4:
+    h4p->guard = 'Q';
+    h4p->request_len = cdbsz;
+    h4p->request = (uint64_t)rep->cmd;
+    if (wr) {
+        if (prefetch) {
+            h4p->dout_xfer_len = 0;     // din_xfer_len is also 0
+            h4p->dout_xferp = 0;
+        } else {
+            h4p->dout_xfer_len = clp->bs * nblks;
+            h4p->dout_xferp = (uint64_t)get_buffp(rep);
+        }
+    } else if (nblks > 0) {
+        h4p->din_xfer_len = clp->bs * nblks;
+        h4p->din_xferp = (uint64_t)get_buffp(rep);
+    }
+    h4p->max_response_len = sizeof(rep->sb);
+    h4p->response = (uint64_t)rep->sb;
+    h4p->timeout = clp->cmd_timeout;
+    h4p->usr_ptr = (uint64_t)rep;
+    h4p->request_extra = pack_id;    /* this is the pack_id */
+    h4p->flags = flags;
+    if (clp->nmrqs > 0) {
+        big_cdb cdb_arr;
+        uint8_t * cmdp = &(cdb_arr[0]);
+
+        if (wr)
+            rep->out_mrq_q_blks += nblks;
+        else
+            rep->in_mrq_q_blks += nblks;
+        memcpy(cmdp, rep->cmd, cdbsz);
+        def_arr.first.push_back(*h4p);
+        def_arr.second.push_back(cdb_arr);
+        res = 0;
+        if ((int)def_arr.first.size() >= clp->nmrqs) {
+            res = sgh_do_deferred_mrq(rep, def_arr);
+            if (res)
+                pr2serr_lk("%s tid=%d: sgh_do_deferred_mrq failed\n",
+                           __func__, rep->id);
+        }
+        return res;
+    }
+    while (((res = ioctl(fd, SG_IOSUBMIT, h4p)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno))) {
+        if (EAGAIN == errno) {
+            ++num_start_eagain;
+#ifdef SGH_DD_SNAP_DEV
+            if (0 == (num_ebusy % 1000))
+                sg_take_snap(fd, rep->id, (clp->verbose > 2));
+#endif
+        } else if (EBUSY == errno) {
+            ++num_ebusy;
+#ifdef SGH_DD_SNAP_DEV
+            if (0 == (num_ebusy % 1000))
+                sg_take_snap(fd, rep->id, (clp->verbose > 2));
+#endif
+        }
+        std::this_thread::yield();/* another thread may be able to progress */
+    }
+    err = errno;
+    if (res < 0) {
+        if (ENOMEM == err)
+            return 1;
+        if (E2BIG == err)
+            sg_take_snap(fd, rep->id, true);
+        pr2serr_lk("%s tid=%d: %s %s ioctl(2) failed: %s\n", __func__,
+                   rep->id, cp, sg_flags_str(h4p->flags, b_len, b),
+                   strerror(err));
+        // v4hdr_out_lk("leadin", h4p, rep->id);
+        return -1;
+    }
+    if ((clp->aen > 0) && (rep->rep_count > 0)) {
+        if (0 == (rep->rq_id % clp->aen)) {
+            struct timespec tspec = {0, 4000 /* 4 usecs */};
+
+            nanosleep(&tspec, NULL);
+#if 0
+            struct pollfd a_poll;
+
+            a_poll.fd = fd;
+            a_poll.events = POLL_IN;
+            a_poll.revents = 0;
+            res = poll(&a_poll, 1 /* element */, 1 /* millisecond */);
+            if (res < 0)
+                pr2serr_lk("%s: poll() failed: %s [%d]\n",
+                           __func__, safe_strerror(errno), errno);
+            else if (0 == res) { /* timeout, cmd still inflight, so abort */
+            }
+#endif
+            ++num_abort_req;
+            res = ioctl(fd, SG_IOABORT, h4p);
+            if (res < 0) {
+                err = errno;
+                if (ENODATA == err) {
+                    if (clp->verbose > 2)
+                        pr2serr_lk("%s: ioctl(SG_IOABORT) no match on "
+                                   "pack_id=%d\n", __func__, pack_id);
+                } else
+                    pr2serr_lk("%s: ioctl(SG_IOABORT) failed: %s [%d]\n",
+                               __func__, safe_strerror(err), err);
+            } else {
+                ++num_abort_req_success;
+                if (clp->verbose > 2)
+                    pr2serr_lk("%s: sent ioctl(SG_IOABORT) on rq_id=%d, "
+                               "success\n", __func__, pack_id);
+            }
+        }   /* else got response, too late for timeout, so skip */
+    }
+    return 0;
+}
+
+/* 0 -> successful, SG_LIB_CAT_UNIT_ATTENTION or SG_LIB_CAT_ABORTED_COMMAND
+   -> try again, SG_LIB_CAT_NOT_READY, SG_LIB_CAT_MEDIUM_HARD,
+   -1 other errors */
+static int
+sg_finish_io(bool wr, Rq_elem * rep, int pack_id, struct sg_io_extra *xtrp)
+{
+    struct global_collection * clp = rep->clp;
+    bool v4 = wr ? clp->out_flags.v4 : clp->in_flags.v4;
+    bool mout_if = wr ? clp->out_flags.mout_if : clp->in_flags.mout_if;
+    bool is_wr2 = xtrp ? xtrp->is_wr2 : false;
+    bool prefetch = xtrp ? xtrp->prefetch : false;
+    int res, fd;
+    int64_t blk = wr ? rep->oblk : rep->iblk;
+    struct sg_io_hdr io_hdr;
+    struct sg_io_hdr * hp;
+    struct sg_io_v4 * h4p;
+    const char *cp;
+
+    if (wr) {
+        fd = is_wr2 ? rep->out2fd : rep->outfd;
+        cp = is_wr2 ? "writing2" : "writing";
+        if (clp->verify) {
+            cp = is_wr2 ? "verifying2" : "verifying";
+            if (prefetch)
+                cp = is_wr2 ? "prefetch2" : "prefetch";
+        }
+    } else {
+        fd = rep->infd;
+        cp = "reading";
+    }
+    if (v4)
+        goto do_v4;
+    memset(&io_hdr, 0 , sizeof(struct sg_io_hdr));
+    /* FORCE_PACK_ID active set only read packet with matching pack_id */
+    io_hdr.interface_id = 'S';
+    io_hdr.dxfer_direction = wr ? SG_DXFER_TO_DEV : SG_DXFER_FROM_DEV;
+    io_hdr.pack_id = pack_id;
+
+    while (((res = read(fd, &io_hdr, sizeof(struct sg_io_hdr))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno))) {
+        if (EAGAIN == errno) {
+            ++num_fin_eagain;
+#ifdef SGH_DD_SNAP_DEV
+            if (0 == (num_ebusy % 1000))
+                sg_take_snap(fd, rep->id, (clp->verbose > 2));
+#endif
+        } else if (EBUSY == errno) {
+            ++num_ebusy;
+#ifdef SGH_DD_SNAP_DEV
+            if (0 == (num_ebusy % 1000))
+                sg_take_snap(fd, rep->id, (clp->verbose > 2));
+#endif
+        }
+        std::this_thread::yield();/* another thread may be able to progress */
+    }
+    if (res < 0) {
+        perror("finishing io [read(2)] on sg device, error");
+        return -1;
+    }
+    if (rep != (Rq_elem *)io_hdr.usr_ptr)
+        err_exit(0, "sg_finish_io: bad usr_ptr, request-response mismatch\n");
+    memcpy(&rep->io_hdr, &io_hdr, sizeof(struct sg_io_hdr));
+    hp = &rep->io_hdr;
+
+    res = sg_err_category3(hp);
+    switch (res) {
+    case SG_LIB_CAT_CLEAN:
+    case SG_LIB_CAT_CONDITION_MET:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        lk_chk_n_print3(cp, hp, false);
+        break;
+    case SG_LIB_CAT_ABORTED_COMMAND:
+    case SG_LIB_CAT_UNIT_ATTENTION:
+        if (clp->verbose > 3)
+            lk_chk_n_print3(cp, hp, false);
+        return res;
+    case SG_LIB_CAT_MISCOMPARE:
+        ++num_miscompare;
+        // fall through
+    case SG_LIB_CAT_NOT_READY:
+    default:
+        {
+            char ebuff[EBUFF_SZ];
+
+            snprintf(ebuff, EBUFF_SZ, "%s blk=%" PRId64, cp, blk);
+            lk_chk_n_print3(ebuff, hp, clp->verbose > 1);
+            return res;
+        }
+    }
+    if ((wr ? clp->out_flags.dio : clp->in_flags.dio) &&
+        (! (hp->info & SG_INFO_DIRECT_IO_MASK)))
+        rep->dio_incomplete_count = 1; /* count dios done as indirect IO */
+    else
+        rep->dio_incomplete_count = 0;
+    rep->resid = hp->resid;
+    if (clp->verbose > 3)
+        pr2serr_lk("%s: tid=%d: completed %s\n", __func__, rep->id, cp);
+    return 0;
+
+do_v4:
+    if (clp->nmrqs > 0) {
+        rep->resid = 0;
+        return 0;
+    }
+    h4p = &rep->io_hdr4[xtrp ? xtrp->hpv4_ind : 0];
+    h4p->request_extra = pack_id;
+    if (mout_if) {
+        h4p->info = 0;
+        h4p->din_resid = 0;
+    }
+    while (((res = ioctl(fd, SG_IORECEIVE, h4p)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno))) {
+        if (EAGAIN == errno) {
+            ++num_fin_eagain;
+#ifdef SGH_DD_SNAP_DEV
+            if (0 == (num_ebusy % 1000))
+                sg_take_snap(fd, rep->id, (clp->verbose > 2));
+#endif
+        } else if (EBUSY == errno) {
+            ++num_ebusy;
+#ifdef SGH_DD_SNAP_DEV
+            if (0 == (num_ebusy % 1000))
+                sg_take_snap(fd, rep->id, (clp->verbose > 2));
+#endif
+        }
+        std::this_thread::yield();/* another thread may be able to progress */
+    }
+    if (res < 0) {
+        perror("finishing io [SG_IORECEIVE] on sg device, error");
+        return -1;
+    }
+    if (mout_if && (0 == h4p->info) && (0 == h4p->din_resid))
+        goto all_good;
+    if (rep != (Rq_elem *)h4p->usr_ptr)
+        err_exit(0, "sg_finish_io: bad usr_ptr, request-response mismatch\n");
+    res = sg_err_category_new(h4p->device_status, h4p->transport_status,
+                              h4p->driver_status,
+                              (const uint8_t *)h4p->response,
+                              h4p->response_len);
+    switch (res) {
+    case SG_LIB_CAT_CLEAN:
+    case SG_LIB_CAT_CONDITION_MET:
+        break;
+    case SG_LIB_CAT_RECOVERED:
+        lk_chk_n_print4(cp, h4p, false);
+        break;
+    case SG_LIB_CAT_ABORTED_COMMAND:
+    case SG_LIB_CAT_UNIT_ATTENTION:
+        if (clp->verbose > 3)
+            lk_chk_n_print4(cp, h4p, false);
+        return res;
+    case SG_LIB_CAT_MISCOMPARE:
+        ++num_miscompare;
+        // fall through
+    case SG_LIB_CAT_NOT_READY:
+    default:
+        {
+            char ebuff[EBUFF_SZ];
+
+            snprintf(ebuff, EBUFF_SZ, "%s rq_id=%d, blk=%" PRId64, cp,
+                     pack_id, blk);
+            lk_chk_n_print4(ebuff, h4p, clp->verbose > 1);
+            if ((clp->verbose > 4) && h4p->info)
+                pr2serr_lk(" info=0x%x sg_info_check=%d direct=%d "
+                           "detaching=%d aborted=%d\n", h4p->info,
+                           !!(h4p->info & SG_INFO_CHECK),
+                           !!(h4p->info & SG_INFO_DIRECT_IO),
+                           !!(h4p->info & SG_INFO_DEVICE_DETACHING),
+                           !!(h4p->info & SG_INFO_ABORTED));
+            return res;
+        }
+    }
+    if ((wr ? clp->out_flags.dio : clp->in_flags.dio) &&
+        ! (h4p->info & SG_INFO_DIRECT_IO))
+        rep->dio_incomplete_count = 1; /* count dios done as indirect IO */
+    else
+        rep->dio_incomplete_count = 0;
+    rep->resid = h4p->din_resid;
+    if (clp->verbose > 4) {
+        pr2serr_lk("%s: tid,rq_id=%d,%d: completed %s\n", __func__, rep->id,
+                   pack_id, cp);
+        if ((clp->verbose > 4) && h4p->info)
+            pr2serr_lk(" info=0x%x sg_info_check=%d direct=%d "
+                       "detaching=%d aborted=%d\n", h4p->info,
+                       !!(h4p->info & SG_INFO_CHECK),
+                       !!(h4p->info & SG_INFO_DIRECT_IO),
+                       !!(h4p->info & SG_INFO_DEVICE_DETACHING),
+                       !!(h4p->info & SG_INFO_ABORTED));
+    }
+all_good:
+    return 0;
+}
+
+/* Returns reserved_buffer_size/mmap_size if success, else 0 for failure */
+static int
+sg_prepare_resbuf(int fd, bool is_in, struct global_collection *clp,
+                  uint8_t **mmpp)
+{
+    static bool done = false;
+    bool def_res = is_in ? clp->in_flags.defres : clp->out_flags.defres;
+    bool no_dur = is_in ? clp->in_flags.no_dur : clp->out_flags.no_dur;
+    bool masync = is_in ? clp->in_flags.masync : clp->out_flags.masync;
+    bool wq_excl = is_in ? clp->in_flags.wq_excl : clp->out_flags.wq_excl;
+    bool skip_thresh = is_in ? clp->in_flags.no_thresh :
+                               clp->out_flags.no_thresh;
+    int res, t;
+    int num = 0;
+    uint8_t *mmp;
+    struct sg_extended_info sei {};
+    struct sg_extended_info * seip = &sei;
+
+    res = ioctl(fd, SG_GET_VERSION_NUM, &t);
+    if ((res < 0) || (t < 40000)) {
+        if (ioctl(fd, SG_GET_RESERVED_SIZE, &num) < 0) {
+            perror("SG_GET_RESERVED_SIZE ioctl failed");
+            return 0;
+        }
+        if (! done) {
+            done = true;
+            sg_version_lt_4 = true;
+            pr2serr_lk("%ssg driver prior to 4.0.00, reduced functionality\n",
+                       my_name);
+        }
+        goto bypass;
+    }
+    if (! sg_version_ge_40045)
+        goto bypass;
+    if (clp->elem_sz >= 4096) {
+        seip->sei_rd_mask |= SG_SEIM_SGAT_ELEM_SZ;
+        res = ioctl(fd, SG_SET_GET_EXTENDED, seip);
+        if (res < 0)
+            pr2serr_lk("%s%s: SG_SET_GET_EXTENDED(SGAT_ELEM_SZ) rd "
+                       "error: %s\n", my_name, __func__, strerror(errno));
+        if (clp->elem_sz != (int)seip->sgat_elem_sz) {
+            memset(seip, 0, sizeof(*seip));
+            seip->sei_wr_mask |= SG_SEIM_SGAT_ELEM_SZ;
+            seip->sgat_elem_sz = clp->elem_sz;
+            res = ioctl(fd, SG_SET_GET_EXTENDED, seip);
+            if (res < 0)
+                pr2serr_lk("%s%s: SG_SET_GET_EXTENDED(SGAT_ELEM_SZ) wr "
+                           "error: %s\n", my_name, __func__, strerror(errno));
+        }
+    }
+    if (no_dur || masync || skip_thresh) {
+        memset(seip, 0, sizeof(*seip));
+        seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+        if (no_dur) {
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_NO_DURATION;
+            seip->ctl_flags |= SG_CTL_FLAGM_NO_DURATION;
+        }
+        if (masync) {
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_MORE_ASYNC;
+            seip->ctl_flags |= SG_CTL_FLAGM_MORE_ASYNC;
+        }
+        if (wq_excl) {
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_EXCL_WAITQ;
+            seip->ctl_flags |= SG_CTL_FLAGM_EXCL_WAITQ;
+        }
+        if (skip_thresh) {
+            seip->tot_fd_thresh = 0;
+            sei.sei_wr_mask |= SG_SEIM_TOT_FD_THRESH;
+        }
+        res = ioctl(fd, SG_SET_GET_EXTENDED, seip);
+        if (res < 0)
+            pr2serr_lk("%s%s: SG_SET_GET_EXTENDED(NO_DURATION) error: %s\n",
+                       my_name, __func__, strerror(errno));
+    }
+bypass:
+    if (! def_res) {
+        num = clp->bs * clp->bpt;
+        res = ioctl(fd, SG_SET_RESERVED_SIZE, &num);
+        if (res < 0) {
+            perror("sgh_dd: SG_SET_RESERVED_SIZE error");
+            return 0;
+        } else {
+            int nn;
+
+            res = ioctl(fd, SG_GET_RESERVED_SIZE, &nn);
+            if (res < 0) {
+                perror("sgh_dd: SG_GET_RESERVED_SIZE error");
+                return 0;
+            }
+            if (nn < num) {
+                pr2serr_lk("%s: SG_GET_RESERVED_SIZE shows size truncated, "
+                           "wanted %d got %d\n", __func__, num, nn);
+                return 0;
+            }
+        }
+        if (mmpp) {
+            mmp = (uint8_t *)mmap(NULL, num, PROT_READ | PROT_WRITE,
+                                  MAP_SHARED, fd, 0);
+            if (MAP_FAILED == mmp) {
+                int err = errno;
+
+                pr2serr_lk("%s%s: sz=%d, fd=%d, mmap() failed: %s\n",
+                           my_name, __func__, num, fd, strerror(err));
+                return 0;
+            }
+            *mmpp = mmp;
+        }
+    }
+    t = 1;
+    res = ioctl(fd, SG_SET_FORCE_PACK_ID, &t);
+    if (res < 0)
+        perror("sgh_dd: SG_SET_FORCE_PACK_ID error");
+    if (clp->unit_nanosec && sg_version_ge_40045) {
+        memset(seip, 0, sizeof(*seip));
+        seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+        seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+        seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+        if (ioctl(fd, SG_SET_GET_EXTENDED, seip) < 0) {
+            pr2serr_lk("ioctl(EXTENDED(TIME_IN_NS)) failed, errno=%d %s\n",
+                       errno, strerror(errno));
+        }
+    }
+    t = 1;
+    res = ioctl(fd, SG_SET_DEBUG, &t);  /* more info in the kernel log */
+    if (res < 0)
+        perror("sgs_dd: SG_SET_DEBUG error");
+    return (res < 0) ? 0 : num;
+}
+
+static bool
+process_flags(const char * arg, struct flags_t * fp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no flag found\n");
+        return false;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "00"))
+            fp->zero = true;
+        else if (0 == strcmp(cp, "append"))
+            fp->append = true;
+        else if (0 == strcmp(cp, "coe"))
+            fp->coe = true;
+        else if (0 == strcmp(cp, "defres"))
+            fp->defres = true;
+        else if (0 == strcmp(cp, "dio"))
+            fp->dio = true;
+        else if (0 == strcmp(cp, "direct"))
+            fp->direct = true;
+        else if (0 == strcmp(cp, "dpo"))
+            fp->dpo = true;
+        else if (0 == strcmp(cp, "dsync"))
+            fp->dsync = true;
+        else if (0 == strcmp(cp, "excl"))
+            fp->excl = true;
+        else if (0 == strcmp(cp, "ff"))
+            fp->ff = true;
+        else if (0 == strcmp(cp, "fua"))
+            fp->fua = true;
+        else if (0 == strcmp(cp, "hipri"))
+            fp->polled = true;
+        else if (0 == strcmp(cp, "masync"))
+            fp->masync = true;
+        else if (0 == strcmp(cp, "mmap"))
+            ++fp->mmap;         /* mmap > 1 stops munmap() being called */
+        else if (0 == strcmp(cp, "mrq_imm"))
+            fp->mrq_immed = true;
+        else if (0 == strcmp(cp, "mrq_immed"))
+            fp->mrq_immed = true;
+        else if (0 == strcmp(cp, "mrq_svb"))
+            fp->mrq_svb = true;
+        else if (0 == strcmp(cp, "nodur"))
+            fp->no_dur = true;
+        else if (0 == strcmp(cp, "no_dur"))
+            fp->no_dur = true;
+        else if (0 == strcmp(cp, "nocreat"))
+            fp->nocreat = true;
+        else if (0 == strcmp(cp, "noshare"))
+            fp->noshare = true;
+        else if (0 == strcmp(cp, "no_share"))
+            fp->noshare = true;
+        else if (0 == strcmp(cp, "no_thresh"))
+            fp->no_thresh = true;
+        else if (0 == strcmp(cp, "no-thresh"))
+            fp->no_thresh = true;
+        else if (0 == strcmp(cp, "nothresh"))
+            fp->no_thresh = true;
+        else if (0 == strcmp(cp, "no_unshare"))
+            fp->no_unshare = true;
+        else if (0 == strcmp(cp, "no-unshare"))
+            fp->no_unshare = true;
+        else if (0 == strcmp(cp, "no_waitq"))
+            fp->no_waitq = true;
+        else if (0 == strcmp(cp, "no-waitq"))
+            fp->no_waitq = true;
+        else if (0 == strcmp(cp, "nowaitq"))
+            fp->no_waitq = true;
+        else if (0 == strcmp(cp, "noxfer"))
+            fp->noxfer = true;
+        else if (0 == strcmp(cp, "no_xfer"))
+            fp->noxfer = true;
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else if (0 == strcmp(cp, "polled"))
+            fp->polled = true;
+        else if (0 == strcmp(cp, "qhead"))
+            fp->qhead = true;
+        else if (0 == strcmp(cp, "qtail"))
+            fp->qtail = true;
+        else if (0 == strcmp(cp, "random"))
+            fp->random = true;
+        else if ((0 == strcmp(cp, "mout_if")) || (0 == strcmp(cp, "mout-if")))
+            fp->mout_if = true;
+        else if (0 == strcmp(cp, "same_fds"))
+            fp->same_fds = true;
+        else if (0 == strcmp(cp, "swait"))
+            fp->swait = true;
+        else if (0 == strcmp(cp, "v3"))
+            fp->v3 = true;
+        else if (0 == strcmp(cp, "v4")) {
+            fp->v4 = true;
+            fp->v4_given = true;
+        } else if (0 == strcmp(cp, "wq_excl"))
+            fp->wq_excl = true;
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return false;
+        }
+        cp = np;
+    } while (cp);
+    return true;
+}
+
+/* Returns the number of times 'ch' is found in string 's' given the
+ * string's length. */
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+static int
+sg_in_open(struct global_collection *clp, const char *inf, uint8_t **mmpp,
+           int * mmap_lenp)
+{
+    int fd, n;
+    int flags = O_RDWR;
+
+    if (clp->in_flags.direct)
+        flags |= O_DIRECT;
+    if (clp->in_flags.excl)
+        flags |= O_EXCL;
+    if (clp->in_flags.dsync)
+        flags |= O_SYNC;
+
+    if ((fd = open(inf, flags)) < 0) {
+        int err = errno;
+        char ebuff[EBUFF_SZ];
+
+        snprintf(ebuff, EBUFF_SZ, "%s: could not open %s for sg reading",
+                 __func__, inf);
+        perror(ebuff);
+        return -sg_convert_errno(err);
+    }
+    n = sg_prepare_resbuf(fd, true, clp, mmpp);
+    if (n <= 0) {
+        close(fd);
+        return -SG_LIB_FILE_ERROR;
+    }
+    if (clp->noshare)
+        sg_noshare_enlarge(fd, clp->verbose > 3);
+    if (mmap_lenp)
+        *mmap_lenp = n;
+    return fd;
+}
+
+static int
+sg_out_open(struct global_collection *clp, const char *outf, uint8_t **mmpp,
+            int * mmap_lenp)
+{
+    int fd, n;
+    int flags = O_RDWR;
+
+    if (clp->out_flags.direct)
+        flags |= O_DIRECT;
+    if (clp->out_flags.excl)
+        flags |= O_EXCL;
+    if (clp->out_flags.dsync)
+        flags |= O_SYNC;
+
+    if ((fd = open(outf, flags)) < 0) {
+        int err = errno;
+        char ebuff[EBUFF_SZ];
+
+        snprintf(ebuff,  EBUFF_SZ, "%s: could not open %s for sg %s",
+                 __func__, outf, (clp->verify ? "verifying" : "writing"));
+        perror(ebuff);
+        return -sg_convert_errno(err);
+    }
+    n = sg_prepare_resbuf(fd, false, clp, mmpp);
+    if (n <= 0) {
+        close(fd);
+        return -SG_LIB_FILE_ERROR;
+    }
+    if (clp->noshare)
+        sg_noshare_enlarge(fd, clp->verbose > 3);
+    if (mmap_lenp)
+        *mmap_lenp = n;
+    return fd;
+}
+
+/* Process arguments given to 'conv=" option. Returns 0 on success,
+ * 1 on error. */
+static int
+process_conv(const char * arg, struct flags_t * ifp, struct flags_t * ofp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no conversions found\n");
+        return 1;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "nocreat"))
+            ofp->nocreat = true;
+        else if (0 == strcmp(cp, "noerror"))
+            ifp->coe = true;         /* will still fail on write error */
+        else if (0 == strcmp(cp, "notrunc"))
+            ;         /* this is the default action of sg_dd so ignore */
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else if (0 == strcmp(cp, "sync"))
+            ;   /* dd(susv4): pad errored block(s) with zeros but sg_dd does
+                 * that by default. Typical dd use: 'conv=noerror,sync' */
+        else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return 1;
+        }
+        cp = np;
+    } while (cp);
+    return 0;
+}
+
+#define STR_SZ 1024
+#define INOUTF_SZ 512
+
+static int
+parse_cmdline_sanity(int argc, char * argv[], struct global_collection * clp,
+                     char * inf, char * outf, char * out2f, char * outregf)
+{
+    bool verbose_given = false;
+    bool version_given = false;
+    bool verify_given = false;
+    bool bpt_given = false;
+    int ibs = 0;
+    int obs = 0;
+    int k, keylen, n, res;
+    char str[STR_SZ];
+    char * key;
+    char * buf;
+    const char * cp;
+
+    for (k = 1; k < argc; k++) {
+        if (argv[k]) {
+            strncpy(str, argv[k], STR_SZ);
+            str[STR_SZ - 1] = '\0';
+        }
+        else
+            continue;
+        for (key = str, buf = key; *buf && *buf != '=';)
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        keylen = strlen(key);
+        if (0 == strcmp(key, "ae")) {
+            clp->aen = sg_get_num(buf);
+            if (clp->aen < 0) {
+                pr2serr("%sbad AEN argument to 'ae=', want 0 or higher\n",
+                        my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            cp = strchr(buf, ',');
+            if (cp) {
+                clp->m_aen = sg_get_num(cp + 1);
+                if (clp->m_aen < 0) {
+                    pr2serr("%sbad MAEN argument to 'ae=', want 0 or "
+                            "higher\n", my_name);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                clp->m_aen_given = true;
+            }
+            clp->aen_given = true;
+        } else if (0 == strcmp(key, "bpt")) {
+            clp->bpt = sg_get_num(buf);
+            if ((clp->bpt < 0) || (clp->bpt > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'bpt='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            bpt_given = true;
+        } else if (0 == strcmp(key, "bs")) {
+            clp->bs = sg_get_num(buf);
+            if ((clp->bs < 0) || (clp->bs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'bs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "cdbsz")) {
+            clp->cdbsz_in = sg_get_num(buf);
+            if ((clp->cdbsz_in < 6) || (clp->cdbsz_in > 32)) {
+                pr2serr("%s'cdbsz' expects 6, 10, 12, 16 or 32\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            clp->cdbsz_out = clp->cdbsz_in;
+            clp->cdbsz_given = true;
+        } else if (0 == strcmp(key, "coe")) {
+            clp->in_flags.coe = !! sg_get_num(buf);
+            clp->out_flags.coe = clp->in_flags.coe;
+        } else if (0 == strcmp(key, "conv")) {
+            if (process_conv(buf, &clp->in_flags, &clp->out_flags)) {
+                pr2serr("%s: bad argument to 'conv='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "count")) {
+            if (0 != strcmp("-1", buf)) {
+                dd_count = sg_get_llnum(buf);
+                if ((dd_count < 0) || (dd_count > MAX_COUNT_SKIP_SEEK)) {
+                    pr2serr("%sbad argument to 'count='\n", my_name);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+            }   /* treat 'count=-1' as calculate count (same as not given) */
+        } else if (0 == strcmp(key, "dio")) {
+            clp->in_flags.dio = !! sg_get_num(buf);
+            clp->out_flags.dio = clp->in_flags.dio;
+        } else if (0 == strcmp(key, "elemsz_kb")) {
+            n = sg_get_num(buf);
+            if (n < 1) {
+                pr2serr("elemsz_kb=EKB wants an integer > 0\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            if (n & (n - 1)) {
+                pr2serr("elemsz_kb=EKB wants EKB to be power of 2\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            clp->elem_sz = n * 1024;
+        } else if ((0 == strcmp(key, "fail_mask")) ||
+                   (0 == strcmp(key, "fail-mask"))) {
+            clp->fail_mask = sg_get_num(buf);
+            if (clp->fail_mask < 0) {
+                pr2serr("fail_mask: couldn't decode argument\n");
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "fua")) {
+            n = sg_get_num(buf);
+            if (n & 1)
+                clp->out_flags.fua = true;
+            if (n & 2)
+                clp->in_flags.fua = true;
+        } else if (0 == strcmp(key, "ibs")) {
+            ibs = sg_get_num(buf);
+            if ((ibs < 0) || (ibs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'ibs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "if")) {
+            if ('\0' != inf[0]) {
+                pr2serr("Second 'if=' argument??\n");
+                return SG_LIB_SYNTAX_ERROR;
+            } else {
+                memcpy(inf, buf, INOUTF_SZ);
+                inf[INOUTF_SZ - 1] = '\0';      /* noisy compiler */
+            }
+        } else if (0 == strcmp(key, "iflag")) {
+            if (! process_flags(buf, &clp->in_flags)) {
+                pr2serr("%sbad argument to 'iflag='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "mrq")) {
+            if (isdigit(buf[0]))
+                cp = buf;
+            else {
+                if ('I' == isupper(buf[0]))
+                    clp->is_mrq_i = true;
+                else if ('O' == isupper(buf[0]))
+                    clp->is_mrq_o = true;
+                else {
+                    pr2serr("%sonly mrq=i,NRQS or mrq=o,NRQS allowed here\n",
+                            my_name);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                cp = strchr(buf, ',');
+                ++cp;
+            }
+            clp->nmrqs = sg_get_num(cp);
+            if (clp->nmrqs < 0) {
+                pr2serr("%sbad argument to 'mrq='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            cp = strchr(cp, ',');
+            if (cp && ('C' == toupper(cp[1])))
+                clp->mrq_cmds = true;
+        } else if (0 == strcmp(key, "noshare")) {
+            clp->noshare = !! sg_get_num(buf);
+        } else if (0 == strcmp(key, "obs")) {
+            obs = sg_get_num(buf);
+            if ((obs < 0) || (obs > MAX_BPT_VALUE)) {
+                pr2serr("%sbad argument to 'obs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key, "of2") == 0) {
+            if ('\0' != out2f[0]) {
+                pr2serr("Second OFILE2 argument??\n");
+                return SG_LIB_CONTRADICT;
+            } else {
+                memcpy(out2f, buf, INOUTF_SZ);
+                out2f[INOUTF_SZ - 1] = '\0';    /* noisy compiler */
+            }
+        } else if (strcmp(key, "ofreg") == 0) {
+            if ('\0' != outregf[0]) {
+                pr2serr("Second OFREG argument??\n");
+                return SG_LIB_CONTRADICT;
+            } else {
+                memcpy(outregf, buf, INOUTF_SZ);
+                outregf[INOUTF_SZ - 1] = '\0';  /* noisy compiler */
+            }
+        } else if (0 == strcmp(key, "ofsplit")) {
+            clp->ofsplit = sg_get_num(buf);
+            if (-1 == clp->ofsplit) {
+                pr2serr("%sbad argument to 'ofsplit='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key, "of") == 0) {
+            if ('\0' != outf[0]) {
+                pr2serr("Second 'of=' argument??\n");
+                return SG_LIB_SYNTAX_ERROR;
+            } else {
+                memcpy(outf, buf, INOUTF_SZ);
+                outf[INOUTF_SZ - 1] = '\0';     /* noisy compiler */
+            }
+        } else if (0 == strcmp(key, "oflag")) {
+            if (! process_flags(buf, &clp->out_flags)) {
+                pr2serr("%sbad argument to 'oflag='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "sdt")) {
+            cp = strchr(buf, ',');
+            n = sg_get_num(buf);
+            if (n < 0) {
+                pr2serr("%sbad argument to 'sdt=CRT[,ICT]'\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            clp->sdt_crt = n;
+            if (cp) {
+                n = sg_get_num(cp + 1);
+                if (n < 0) {
+                    pr2serr("%sbad 2nd argument to 'sdt=CRT,ICT'\n",
+                            my_name);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                clp->sdt_ict = n;
+            }
+        } else if (0 == strcmp(key, "seek")) {
+            clp->seek = sg_get_llnum(buf);
+            if (clp->seek < 0) {
+                pr2serr("%sbad argument to 'seek='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "skip")) {
+            clp->skip = sg_get_llnum(buf);
+            if (clp->skip < 0) {
+                pr2serr("%sbad argument to 'skip='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key, "sync"))
+            do_sync = !! sg_get_num(buf);
+        else if (0 == strcmp(key, "thr"))
+            num_threads = sg_get_num(buf);
+        else if (0 == strcmp(key, "time")) {
+            do_time = sg_get_num(buf);
+            if (do_time < 0) {
+                pr2serr("%sbad argument to 'time=0|1|2'\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            cp = strchr(buf, ',');
+            if (cp) {
+                n = sg_get_num(cp + 1);
+                if (n < 0) {
+                    pr2serr("%sbad argument to 'time=0|1|2,TO'\n", my_name);
+                    return SG_LIB_SYNTAX_ERROR;
+                }
+                clp->cmd_timeout = n ? (n * 1000) : DEF_TIMEOUT;
+            }
+        } else if (0 == strcmp(key, "unshare"))
+            clp->unshare = !! sg_get_num(buf);  /* default: true */
+        else if (0 == strncmp(key, "verb", 4))
+            clp->verbose = sg_get_num(buf);
+        else if ((keylen > 1) && ('-' == key[0]) && ('-' != key[1])) {
+            res = 0;
+            n = num_chs_in_str(key + 1, keylen - 1, 'c');
+            clp->chkaddr += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'd');
+            clp->dry_run += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'h');
+            clp->help += n;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'p');
+            if (n > 0)
+                clp->prefetch = true;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'v');
+            if (n > 0)
+                verbose_given = true;
+            clp->verbose += n;   /* -v  ---> --verbose */
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'V');
+            if (n > 0)
+                version_given = true;
+            res += n;
+            n = num_chs_in_str(key + 1, keylen - 1, 'x');
+            if (n > 0)
+                verify_given = true;
+            res += n;
+
+            if (res < (keylen - 1)) {
+                pr2serr("Unrecognised short option in '%s', try '--help'\n",
+                        key);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strncmp(key, "--chkaddr", 9))
+            ++clp->chkaddr;
+        else if ((0 == strncmp(key, "--dry-run", 9)) ||
+                 (0 == strncmp(key, "--dry_run", 9)))
+            ++clp->dry_run;
+        else if ((0 == strncmp(key, "--help", 6)) ||
+                   (0 == strcmp(key, "-?")))
+            ++clp->help;
+        else if ((0 == strncmp(key, "--prefetch", 10)) ||
+                 (0 == strncmp(key, "--pre-fetch", 11)))
+            clp->prefetch = true;
+        else if (0 == strncmp(key, "--verb", 6)) {
+            verbose_given = true;
+            ++clp->verbose;      /* --verbose */
+        } else if (0 == strncmp(key, "--veri", 6))
+            verify_given = true;
+        else if (0 == strncmp(key, "--vers", 6))
+            version_given = true;
+        else {
+            pr2serr("Unrecognized option '%s'\n", key);
+            pr2serr("For more information use '--help' or '-h'\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+
+#ifdef DEBUG
+    pr2serr("In DEBUG mode, ");
+    if (verbose_given && version_given) {
+        pr2serr("but override: '-vV' given, zero verbose and continue\n");
+        verbose_given = false;
+        version_given = false;
+        clp->verbose = 0;
+    } else if (! verbose_given) {
+        pr2serr("set '-vv'\n");
+        clp->verbose = 2;
+    } else
+        pr2serr("keep verbose=%d\n", clp->verbose);
+#else
+    if (verbose_given && version_given)
+        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
+#endif
+    if (version_given) {
+        pr2serr("%s%s\n", my_name, version_str);
+        return SG_LIB_OK_FALSE;
+    }
+    if (clp->help > 0) {
+        usage(clp->help);
+        return SG_LIB_OK_FALSE;
+    }
+    if (clp->bs <= 0) {
+        clp->bs = DEF_BLOCK_SIZE;
+        pr2serr("Assume default 'bs' ((logical) block size) of %d bytes\n",
+                clp->bs);
+    }
+    if (verify_given) {
+        pr2serr("Doing verify/cmp rather than copy\n");
+        clp->verify = true;
+    }
+    if ((ibs && (ibs != clp->bs)) || (obs && (obs != clp->bs))) {
+        pr2serr("If 'ibs' or 'obs' given must be same as 'bs'\n");
+        usage(0);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((clp->skip < 0) || (clp->seek < 0)) {
+        pr2serr("skip and seek cannot be negative\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (clp->out_flags.append) {
+        if (clp->seek > 0) {
+            pr2serr("Can't use both append and seek switches\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if (verify_given) {
+            pr2serr("Can't use both append and verify switches\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+    }
+    if (clp->bpt < 1) {
+        pr2serr("bpt must be greater than 0\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (clp->in_flags.mmap && clp->out_flags.mmap) {
+        pr2serr("mmap flag on both IFILE and OFILE doesn't work\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (! clp->noshare) {
+        if (clp->in_flags.noshare || clp->out_flags.noshare)
+            clp->noshare = true;
+    }
+    if (clp->unshare) {
+        if (clp->in_flags.no_unshare || clp->out_flags.no_unshare)
+            clp->unshare = false;
+    }
+    if (clp->out_flags.mmap && ! clp->noshare) {
+        pr2serr("oflag=mmap needs either noshare=1\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((clp->in_flags.mmap || clp->out_flags.mmap) &&
+        (clp->in_flags.same_fds || clp->out_flags.same_fds)) {
+        pr2serr("can't have both 'mmap' and 'same_fds' flags\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((! clp->noshare) && (clp->in_flags.dio || clp->out_flags.dio)) {
+        pr2serr("dio flag can only be used with noshare=1\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (clp->nmrqs > 0) {
+        if (clp->in_flags.mrq_immed || clp->out_flags.mrq_immed)
+            clp->mrq_async = true;
+    }
+    /* defaulting transfer size to 128*2048 for CD/DVDs is too large
+       for the block layer in lk 2.6 and results in an EIO on the
+       SG_IO ioctl. So reduce it in that case. */
+    if ((clp->bs >= 2048) && (! bpt_given))
+        clp->bpt = DEF_BLOCKS_PER_2048TRANSFER;
+    if (clp->ofsplit >= clp->bpt) {
+        pr2serr("ofsplit when given must be less than BPT\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if ((num_threads < 1) || (num_threads > MAX_NUM_THREADS)) {
+        pr2serr("too few or too many threads requested\n");
+        usage(1);
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (clp->in_flags.swait || clp->out_flags.swait) {
+        if (clp->verbose)
+            pr2serr("the 'swait' flag is now ignored\n");
+        /* remnants ... */
+        if (clp->in_flags.swait && (! clp->out_flags.swait))
+            clp->out_flags.swait = true;
+    }
+    clp->unit_nanosec = (do_time > 1) || !!getenv("SG3_UTILS_LINUX_NANO");
+#if 0
+    if (clp->verbose) {
+        pr2serr("%sif=%s skip=%" PRId64 " of=%s seek=%" PRId64 " count=%"
+                PRId64, my_name, inf, clp->skip, outf, clp->seek, dd_count);
+        if (clp->nmrqs > 0)
+            pr2serr(" mrq=%d%s\n", clp->nmrqs, (clp->mrq_cmds ? ",C" : ""));
+        else
+            pr2serr("\n");
+    }
+#endif
+    return 0;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    char inf[INOUTF_SZ];
+    char outf[INOUTF_SZ];
+    char out2f[INOUTF_SZ];
+    char outregf[INOUTF_SZ];
+    int res, k, err;
+    int64_t in_num_sect = 0;
+    int64_t out_num_sect = 0;
+    int in_sect_sz, out_sect_sz, status, flags;
+    void * vp;
+    const char * ccp = NULL;
+    const char * cc2p;
+    struct global_collection * clp = &gcoll;
+    Thread_info thread_arr[MAX_NUM_THREADS] {};
+    char ebuff[EBUFF_SZ];
+#if SG_LIB_ANDROID
+    struct sigaction actions;
+
+    memset(&actions, 0, sizeof(actions));
+    sigemptyset(&actions.sa_mask);
+    actions.sa_flags = 0;
+    actions.sa_handler = thread_exit_handler;
+    sigaction(SIGUSR1, &actions, NULL);
+    sigaction(SIGUSR2, &actions, NULL);
+#endif
+    /* memset(clp, 0, sizeof(*clp)); */
+    clp->bpt = DEF_BLOCKS_PER_TRANSFER;
+    clp->cmd_timeout = DEF_TIMEOUT;
+    clp->in_type = FT_OTHER;
+    /* change dd's default: if of=OFILE not given, assume /dev/null */
+    clp->out_type = FT_DEV_NULL;
+    clp->out2_type = FT_DEV_NULL;
+    clp->cdbsz_in = DEF_SCSI_CDBSZ;
+    clp->cdbsz_out = DEF_SCSI_CDBSZ;
+    clp->sdt_ict = DEF_SDT_ICT_MS;
+    clp->sdt_crt = DEF_SDT_CRT_SEC;
+    clp->nmrqs = DEF_NUM_MRQS;
+    clp->unshare = true;
+    inf[0] = '\0';
+    outf[0] = '\0';
+    out2f[0] = '\0';
+    outregf[0] = '\0';
+    fetch_sg_version();
+    if (sg_version >= 40045)
+        sg_version_ge_40045 = true;
+
+    res = parse_cmdline_sanity(argc, argv, clp, inf, outf, out2f, outregf);
+    if (SG_LIB_OK_FALSE == res)
+        return 0;
+    if (res)
+        return res;
+    if (sg_version > 40000) {
+        if (! clp->in_flags.v3)
+            clp->in_flags.v4 = true;
+        if (! clp->out_flags.v3)
+            clp->out_flags.v4 = true;
+    }
+
+    install_handler(SIGINT, interrupt_handler);
+    install_handler(SIGQUIT, interrupt_handler);
+    install_handler(SIGPIPE, interrupt_handler);
+    install_handler(SIGUSR1, siginfo_handler);
+    install_handler(SIGUSR2, siginfo2_handler);
+
+    clp->infd = STDIN_FILENO;
+    clp->outfd = STDOUT_FILENO;
+    if (clp->in_flags.ff && clp->in_flags.zero) {
+        ccp = "<addr_as_data>";
+        cc2p = "addr_as_data";
+    } else if (clp->in_flags.ff) {
+        ccp = "<0xff bytes>";
+        cc2p = "ff";
+    } else if (clp->in_flags.random) {
+        ccp = "<random>";
+        cc2p = "random";
+    } else if (clp->in_flags.zero) {
+        ccp = "<zero bytes>";
+        cc2p = "00";
+    }
+    if (ccp) {
+        if (inf[0]) {
+            pr2serr("%siflag=%s and if=%s contradict\n", my_name, cc2p, inf);
+            return SG_LIB_CONTRADICT;
+        }
+        clp->in_type = FT_RANDOM_0_FF;
+        clp->infp = ccp;
+        clp->infd = -1;
+    } else if (inf[0] && ('-' != inf[0])) {
+        clp->in_type = dd_filetype(inf, clp->in_st_size);
+
+        if (FT_ERROR == clp->in_type) {
+            pr2serr("%sunable to access %s\n", my_name, inf);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_ST == clp->in_type) {
+            pr2serr("%sunable to use scsi tape device %s\n", my_name, inf);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_CHAR == clp->in_type) {
+            pr2serr("%sunable to use unknown char device %s\n", my_name, inf);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_SG == clp->in_type) {
+            clp->infd = sg_in_open(clp, inf, NULL, NULL);
+            if (clp->verbose > 2)
+                pr2serr("using sg v%c interface on %s\n",
+                        (clp->in_flags.v4 ? '4' : '3'), inf);
+            if (clp->infd < 0)
+                return -clp->infd;
+        } else {
+            flags = O_RDONLY;
+            if (clp->in_flags.direct)
+                flags |= O_DIRECT;
+            if (clp->in_flags.excl)
+                flags |= O_EXCL;
+            if (clp->in_flags.dsync)
+                flags |= O_SYNC;
+
+            if ((clp->infd = open(inf, flags)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ, "%scould not open %s for reading",
+                         my_name, inf);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            } else if (clp->skip > 0) {
+                off64_t offset = clp->skip;
+
+                offset *= clp->bs;       /* could exceed 32 here! */
+                if (lseek64(clp->infd, offset, SEEK_SET) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, "%scouldn't skip to required "
+                             "position on %s", my_name, inf);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+        }
+        clp->infp = inf;
+        if ((clp->in_flags.v3 || clp->in_flags.v4_given) &&
+            (FT_SG != clp->in_type)) {
+            clp->in_flags.v3 = false;
+            clp->in_flags.v4 = false;
+            pr2serr("%siflag= v3 and v4 both ignored when IFILE is not sg "
+                    "device\n", my_name);
+        }
+    }
+    if (clp->verbose && (clp->in_flags.no_waitq || clp->out_flags.no_waitq))
+        pr2serr("no_waitq: flag no longer does anything\n");
+    if (outf[0])
+        clp->ofile_given = true;
+    if (outf[0] && ('-' != outf[0])) {
+        clp->out_type = dd_filetype(outf, clp->out_st_size);
+
+        if ((FT_SG != clp->out_type) && clp->verify) {
+            pr2serr("%s --verify only supported by sg OFILEs\n", my_name);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_ST == clp->out_type) {
+            pr2serr("%sunable to use scsi tape device %s\n", my_name, outf);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_CHAR == clp->out_type) {
+            pr2serr("%sunable to use unknown char device %s\n", my_name, outf);
+            return SG_LIB_FILE_ERROR;
+        } else if (FT_SG == clp->out_type) {
+            clp->outfd = sg_out_open(clp, outf, NULL, NULL);
+            if (clp->verbose > 2)
+                pr2serr("using sg v%c interface on %s\n",
+                        (clp->out_flags.v4 ? '4' : '3'), outf);
+            if (clp->outfd < 0)
+                return -clp->outfd;
+        } else if (FT_DEV_NULL == clp->out_type)
+            clp->outfd = -1; /* don't bother opening */
+        else {
+            flags = O_WRONLY;
+            if (! clp->out_flags.nocreat)
+                flags |= O_CREAT;
+            if (clp->out_flags.direct)
+                flags |= O_DIRECT;
+            if (clp->out_flags.excl)
+                flags |= O_EXCL;
+            if (clp->out_flags.dsync)
+                flags |= O_SYNC;
+            if (clp->out_flags.append)
+                flags |= O_APPEND;
+
+            if ((clp->outfd = open(outf, flags, 0666)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ, "%scould not open %s for "
+                         "writing", my_name, outf);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+            if (clp->seek > 0) {
+                off64_t offset = clp->seek;
+
+                offset *= clp->bs;       /* could exceed 32 bits here! */
+                if (lseek64(clp->outfd, offset, SEEK_SET) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, "%scouldn't seek to required "
+                             "position on %s", my_name, outf);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+        }
+        clp->outfp = outf;
+        if ((clp->out_flags.v3 || clp->out_flags.v4_given) &&
+            (FT_SG != clp->out_type)) {
+            clp->out_flags.v3 = false;
+            clp->out_flags.v4 = false;
+            pr2serr("%soflag= v3 and v4 both ignored when OFILE is not sg "
+                    "device\n", my_name);
+        }
+    }
+
+    if (out2f[0])
+        clp->ofile2_given = true;
+    if (out2f[0] && ('-' != out2f[0])) {
+        off_t out2_st_size;
+
+        clp->out2_type = dd_filetype(out2f, out2_st_size);
+        if (FT_ST == clp->out2_type) {
+            pr2serr("%sunable to use scsi tape device %s\n", my_name, out2f);
+            return SG_LIB_FILE_ERROR;
+        }
+        else if (FT_SG == clp->out2_type) {
+            clp->out2fd = sg_out_open(clp, out2f, NULL, NULL);
+            if (clp->out2fd < 0)
+                return -clp->out2fd;
+        }
+        else if (FT_DEV_NULL == clp->out2_type)
+            clp->out2fd = -1; /* don't bother opening */
+        else {
+            flags = O_WRONLY;
+            if (! clp->out_flags.nocreat)
+                flags |= O_CREAT;
+            if (clp->out_flags.direct)
+                flags |= O_DIRECT;
+            if (clp->out_flags.excl)
+                flags |= O_EXCL;
+            if (clp->out_flags.dsync)
+                flags |= O_SYNC;
+            if (clp->out_flags.append)
+                flags |= O_APPEND;
+
+            if ((clp->out2fd = open(out2f, flags, 0666)) < 0) {
+                err = errno;
+                snprintf(ebuff, EBUFF_SZ, "%scould not open %s for "
+                         "writing", my_name, out2f);
+                perror(ebuff);
+                return sg_convert_errno(err);
+            }
+            if (clp->seek > 0) {
+                off64_t offset = clp->seek;
+
+                offset *= clp->bs;       /* could exceed 32 bits here! */
+                if (lseek64(clp->out2fd, offset, SEEK_SET) < 0) {
+                    err = errno;
+                    snprintf(ebuff, EBUFF_SZ, "%scouldn't seek to required "
+                             "position on %s", my_name, out2f);
+                    perror(ebuff);
+                    return sg_convert_errno(err);
+                }
+            }
+        }
+        clp->out2fp = out2f;
+    }
+    if ((FT_SG == clp->in_type ) && (FT_SG == clp->out_type)) {
+        if (clp->nmrqs > 0) {
+            if (clp->is_mrq_i == clp->is_mrq_o) {
+                if (clp->ofsplit > 0) {
+                    if (0 != (clp->nmrqs % 3)) {
+                        pr2serr("When both IFILE+OFILE sg devices and OSP>0, "
+                                "mrq=NRQS must be divisible by 3\n");
+                        pr2serr("    triple NRQS to avoid error\n");
+                        clp->nmrqs *= 3;
+                    }
+                } else if (0 != (clp->nmrqs % 2)) {
+                    pr2serr("When both IFILE+OFILE sg devices (and OSP=0), "
+                            "mrq=NRQS must be even\n");
+                    pr2serr("    double NRQS to avoid error\n");
+                    clp->nmrqs *= 2;
+                }
+            }
+            if (clp->is_mrq_i && clp->is_mrq_o)
+                ;
+            else if (clp->is_mrq_i || clp->is_mrq_o)
+                clp->unbalanced_mrq = true;
+        }
+        if (clp->in_flags.v4_given && (! clp->out_flags.v3)) {
+            if (! clp->out_flags.v4_given) {
+                clp->out_flags.v4 = true;
+                if (clp->verbose)
+                    pr2serr("Changing OFILE from v3 to v4, use oflag=v3 to "
+                            "force v3\n");
+            }
+        }
+        if (clp->out_flags.v4_given && (! clp->in_flags.v3)) {
+            if (! clp->in_flags.v4_given) {
+                clp->in_flags.v4 = true;
+                if (clp->verbose)
+                    pr2serr("Changing IFILE from v3 to v4, use iflag=v3 to "
+                            "force v3\n");
+            }
+        }
+#if 0
+        if (clp->mrq_async && !(clp->noshare)) {
+            pr2serr("With mrq_immed also need noshare on sg-->sg copy\n");
+            return SG_LIB_SYNTAX_ERROR;
+        }
+#endif
+    } else if ((FT_SG == clp->in_type ) || (FT_SG == clp->out_type)) {
+        if (clp->nmrqs > 0)
+            clp->unbalanced_mrq = true;
+    }
+    if (outregf[0]) {
+        off_t outrf_st_size;
+        int ftyp = dd_filetype(outregf, outrf_st_size);
+
+        clp->outreg_type = ftyp;
+        if (! ((FT_OTHER == ftyp) || (FT_ERROR == ftyp) ||
+               (FT_DEV_NULL == ftyp))) {
+            pr2serr("File: %s can only be regular file or pipe (or "
+                    "/dev/null)\n", outregf);
+            return SG_LIB_SYNTAX_ERROR;
+        }
+        if ((clp->outregfd = open(outregf, O_WRONLY | O_CREAT, 0666)) < 0) {
+            err = errno;
+            snprintf(ebuff, EBUFF_SZ, "could not open %s for writing",
+                     outregf);
+            perror(ebuff);
+            return sg_convert_errno(err);
+        }
+        if (clp->verbose > 1)
+            pr2serr("ofreg=%s opened okay, fd=%d\n", outregf, clp->outregfd);
+        if (FT_ERROR == ftyp)
+            clp->outreg_type = FT_OTHER;        /* regular file created */
+    } else
+        clp->outregfd = -1;
+
+    if ((STDIN_FILENO == clp->infd) && (STDOUT_FILENO == clp->outfd)) {
+        pr2serr("Won't default both IFILE to stdin _and_ OFILE to "
+                "/dev/null\n");
+        pr2serr("For more information use '--help' or '-h'\n");
+        return SG_LIB_SYNTAX_ERROR;
+    }
+    if (dd_count < 0) {
+        in_num_sect = -1;
+        if (FT_SG == clp->in_type) {
+            res = scsi_read_capacity(clp->infd, &in_num_sect, &in_sect_sz);
+            if (2 == res) {
+                pr2serr("Unit attention, media changed(in), continuing\n");
+                res = scsi_read_capacity(clp->infd, &in_num_sect,
+                                         &in_sect_sz);
+            }
+            if (0 != res) {
+                if (res == SG_LIB_CAT_INVALID_OP)
+                    pr2serr("read capacity not supported on %s\n", inf);
+                else if (res == SG_LIB_CAT_NOT_READY)
+                    pr2serr("read capacity failed, %s not ready\n", inf);
+                else
+                    pr2serr("Unable to read capacity on %s\n", inf);
+                return SG_LIB_FILE_ERROR;
+            } else if (clp->bs != in_sect_sz) {
+                pr2serr(">> warning: logical block size on %s confusion: "
+                        "bs=%d, device claims=%d\n", clp->infp, clp->bs,
+                        in_sect_sz);
+                return SG_LIB_FILE_ERROR;
+            }
+        } else if (FT_BLOCK == clp->in_type) {
+            if (0 != read_blkdev_capacity(clp->infd, &in_num_sect,
+                                          &in_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", inf);
+                in_num_sect = -1;
+            }
+            if (clp->bs != in_sect_sz) {
+                pr2serr("logical block size on %s confusion; bs=%d, from "
+                        "device=%d\n", inf, clp->bs, in_sect_sz);
+                in_num_sect = -1;
+            }
+        } else if (FT_OTHER == clp->in_type) {
+            in_num_sect = clp->in_st_size / clp->bs;
+            if (clp->in_st_size % clp->bs) {
+                ++in_num_sect;
+                pr2serr("Warning: the file size of %s is not a multiple of BS "
+                        "[%d]\n", inf, clp->bs);
+            }
+        }
+        if (in_num_sect > clp->skip)
+            in_num_sect -= clp->skip;
+
+        out_num_sect = -1;
+        if (FT_SG == clp->out_type) {
+            res = scsi_read_capacity(clp->outfd, &out_num_sect, &out_sect_sz);
+            if (2 == res) {
+                pr2serr("Unit attention, media changed(out), continuing\n");
+                res = scsi_read_capacity(clp->outfd, &out_num_sect,
+                                         &out_sect_sz);
+            }
+            if (0 != res) {
+                if (res == SG_LIB_CAT_INVALID_OP)
+                    pr2serr("read capacity not supported on %s\n", outf);
+                else if (res == SG_LIB_CAT_NOT_READY)
+                    pr2serr("read capacity failed, %s not ready\n", outf);
+                else
+                    pr2serr("Unable to read capacity on %s\n", outf);
+                out_num_sect = -1;
+                return SG_LIB_FILE_ERROR;
+            } else if (clp->bs != out_sect_sz) {
+                pr2serr(">> warning: logical block size on %s confusion: "
+                        "bs=%d, device claims=%d\n", clp->outfp, clp->bs,
+                        out_sect_sz);
+                return SG_LIB_FILE_ERROR;
+            }
+        } else if (FT_BLOCK == clp->out_type) {
+            if (0 != read_blkdev_capacity(clp->outfd, &out_num_sect,
+                                          &out_sect_sz)) {
+                pr2serr("Unable to read block capacity on %s\n", outf);
+                out_num_sect = -1;
+            }
+            if (clp->bs != out_sect_sz) {
+                pr2serr("logical block size on %s confusion: bs=%d, from "
+                        "device=%d\n", outf, clp->bs, out_sect_sz);
+                out_num_sect = -1;
+            }
+        } else if (FT_OTHER == clp->out_type) {
+            out_num_sect = clp->out_st_size / clp->bs;
+            if (clp->out_st_size % clp->bs) {
+                ++out_num_sect;
+                pr2serr("Warning: the file size of %s is not a multiple of BS "
+                        "[%d]\n", outf, clp->bs);
+            }
+        }
+        if (out_num_sect > clp->seek)
+            out_num_sect -= clp->seek;
+
+        if (in_num_sect > 0) {
+            if (out_num_sect > 0)
+                dd_count = (in_num_sect > out_num_sect) ? out_num_sect :
+                                                          in_num_sect;
+            else
+                dd_count = in_num_sect;
+        }
+        else
+            dd_count = out_num_sect;
+    }
+    if (clp->verbose > 2)
+        pr2serr("Start of loop, count=%" PRId64 ", in_num_sect=%" PRId64
+                ", out_num_sect=%" PRId64 "\n", dd_count, in_num_sect,
+                out_num_sect);
+    if (dd_count < 0) {
+        pr2serr("Couldn't calculate count, please give one\n");
+        return SG_LIB_CAT_OTHER;
+    }
+    if (! clp->cdbsz_given) {
+        if ((FT_SG == clp->in_type) && (MAX_SCSI_CDBSZ != clp->cdbsz_in) &&
+            (((dd_count + clp->skip) > UINT_MAX) || (clp->bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'if')\n");
+            clp->cdbsz_in = MAX_SCSI_CDBSZ;
+        }
+        if ((FT_SG == clp->out_type) && (MAX_SCSI_CDBSZ != clp->cdbsz_out) &&
+            (((dd_count + clp->seek) > UINT_MAX) || (clp->bpt > USHRT_MAX))) {
+            pr2serr("Note: SCSI command size increased to 16 bytes (for "
+                    "'of')\n");
+            clp->cdbsz_out = MAX_SCSI_CDBSZ;
+        }
+    }
+
+    // clp->in_count = dd_count;
+    clp->in_rem_count = dd_count;
+    clp->out_count = dd_count;
+    clp->out_rem_count = dd_count;
+    clp->out_blk = clp->seek;
+    status = pthread_mutex_init(&clp->in_mutex, NULL);
+    if (0 != status) err_exit(status, "init in_mutex");
+    status = pthread_mutex_init(&clp->out_mutex, NULL);
+    if (0 != status) err_exit(status, "init out_mutex");
+    status = pthread_mutex_init(&clp->out2_mutex, NULL);
+    if (0 != status) err_exit(status, "init out2_mutex");
+    status = pthread_cond_init(&clp->out_sync_cv, NULL);
+    if (0 != status) err_exit(status, "init out_sync_cv");
+
+    if (clp->dry_run > 0) {
+        pr2serr("Due to --dry-run option, bypass copy/read\n");
+        goto fini;
+    }
+    if (! clp->ofile_given)
+        pr2serr("of=OFILE not given so only read from IFILE, to output to "
+                "stdout use 'of=-'\n");
+
+    sigemptyset(&signal_set);
+    sigaddset(&signal_set, SIGINT);
+    sigaddset(&signal_set, SIGUSR2);
+    status = pthread_sigmask(SIG_BLOCK, &signal_set, &orig_signal_set);
+    if (0 != status) err_exit(status, "pthread_sigmask");
+    status = pthread_create(&sig_listen_thread_id, NULL,
+                            sig_listen_thread, (void *)clp);
+    if (0 != status) err_exit(status, "pthread_create, sig...");
+
+    if (do_time) {
+        start_tm.tv_sec = 0;
+        start_tm.tv_usec = 0;
+        gettimeofday(&start_tm, NULL);
+    }
+
+/* vvvvvvvvvvv  Start worker threads  vvvvvvvvvvvvvvvvvvvvvvvv */
+    if ((clp->out_rem_count.load() > 0) && (num_threads > 0)) {
+        Thread_info *tip = thread_arr + 0;
+
+        tip->gcp = clp;
+        tip->id = 0;
+        /* Run 1 work thread to shake down infant retryable stuff */
+        status = pthread_mutex_lock(&clp->out_mutex);
+        if (0 != status) err_exit(status, "lock out_mutex");
+        status = pthread_create(&tip->a_pthr, NULL, read_write_thread,
+                                (void *)tip);
+        if (0 != status) err_exit(status, "pthread_create");
+
+        /* wait for any broadcast */
+        pthread_cleanup_push(cleanup_out, (void *)clp);
+        status = pthread_cond_wait(&clp->out_sync_cv, &clp->out_mutex);
+        if (0 != status) err_exit(status, "cond out_sync_cv");
+        pthread_cleanup_pop(0);
+        status = pthread_mutex_unlock(&clp->out_mutex);
+        if (0 != status) err_exit(status, "unlock out_mutex");
+
+        /* now start the rest of the threads */
+        for (k = 1; k < num_threads; ++k) {
+            tip = thread_arr + k;
+            tip->gcp = clp;
+            tip->id = k;
+            status = pthread_create(&tip->a_pthr, NULL, read_write_thread,
+                                    (void *)tip);
+            if (0 != status) err_exit(status, "pthread_create");
+        }
+
+        /* now wait for worker threads to finish */
+        for (k = 0; k < num_threads; ++k) {
+            tip = thread_arr + k;
+            status = pthread_join(tip->a_pthr, &vp);
+            if (0 != status) err_exit(status, "pthread_join");
+            if (clp->verbose > 2)
+                pr2serr_lk("%d <-- Worker thread terminated, vp=%s\n", k,
+                           ((vp == clp) ? "clp" : "NULL (or !clp)"));
+        }
+    }   /* started worker threads and here after they have all exited */
+
+    if (do_time && (start_tm.tv_sec || start_tm.tv_usec))
+        calc_duration_throughput(0);
+
+    shutting_down = true;
+    status = pthread_join(sig_listen_thread_id, &vp);
+    if (0 != status) err_exit(status, "pthread_join");
+#if 0
+    /* pthread_cancel() has issues and is not supported in Android */
+    status = pthread_kill(sig_listen_thread_id, SIGUSR2);
+    if (0 != status) err_exit(status, "pthread_kill");
+    std::this_thread::yield();      // not enough it seems
+    {   /* allow time for SIGUSR2 signal to get through */
+        struct timespec tspec = {0, 400000}; /* 400 usecs */
+
+        nanosleep(&tspec, NULL);
+    }
+#endif
+
+    if (do_sync) {
+        if (FT_SG == clp->out_type) {
+            pr2serr_lk(">> Synchronizing cache on %s\n", outf);
+            res = sg_ll_sync_cache_10(clp->outfd, 0, 0, 0, 0, 0, false, 0);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr_lk("Unit attention(out), continuing\n");
+                res = sg_ll_sync_cache_10(clp->outfd, 0, 0, 0, 0, 0, false,
+                                          0);
+            }
+            if (0 != res)
+                pr2serr_lk("Unable to synchronize cache\n");
+        }
+        if (FT_SG == clp->out2_type) {
+            pr2serr_lk(">> Synchronizing cache on %s\n", out2f);
+            res = sg_ll_sync_cache_10(clp->out2fd, 0, 0, 0, 0, 0, false, 0);
+            if (SG_LIB_CAT_UNIT_ATTENTION == res) {
+                pr2serr_lk("Unit attention(out2), continuing\n");
+                res = sg_ll_sync_cache_10(clp->out2fd, 0, 0, 0, 0, 0, false,
+                                          0);
+            }
+            if (0 != res)
+                pr2serr_lk("Unable to synchronize cache (of2)\n");
+        }
+    }
+
+fini:
+    if ((STDIN_FILENO != clp->infd) && (clp->infd >= 0))
+        close(clp->infd);
+    if ((STDOUT_FILENO != clp->outfd) && (FT_DEV_NULL != clp->out_type) &&
+        (clp->outfd >= 0))
+        close(clp->outfd);
+    if ((clp->out2fd >= 0) && (STDOUT_FILENO != clp->out2fd) &&
+        (FT_DEV_NULL != clp->out2_type))
+        close(clp->out2fd);
+    if ((clp->outregfd >= 0) && (STDOUT_FILENO != clp->outregfd) &&
+        (FT_DEV_NULL != clp->outreg_type))
+        close(clp->outregfd);
+    res = exit_status;
+    if ((0 != clp->out_count.load()) && (0 == clp->dry_run)) {
+        pr2serr(">>>> Some error occurred, remaining blocks=%" PRId64 "\n",
+                clp->out_count.load());
+        if (0 == res)
+            res = SG_LIB_CAT_OTHER;
+    }
+    print_stats("");
+    if (clp->dio_incomplete_count.load()) {
+        int fd;
+        char c;
+
+        pr2serr(">> Direct IO requested but incomplete %d times\n",
+                clp->dio_incomplete_count.load());
+        if ((fd = open(sg_allow_dio, O_RDONLY)) >= 0) {
+            if (1 == read(fd, &c, 1)) {
+                if ('0' == c)
+                    pr2serr(">>> %s set to '0' but should be set to '1' for "
+                            "direct IO\n", sg_allow_dio);
+            }
+            close(fd);
+        }
+    }
+    if (clp->sum_of_resids.load())
+        pr2serr(">> Non-zero sum of residual counts=%d\n",
+               clp->sum_of_resids.load());
+    if (clp->verbose && (num_start_eagain > 0))
+        pr2serr("Number of start EAGAINs: %d\n", num_start_eagain.load());
+    if (clp->verbose && (num_fin_eagain > 0))
+        pr2serr("Number of finish EAGAINs: %d\n", num_fin_eagain.load());
+    if (clp->verbose && (num_ebusy > 0))
+        pr2serr("Number of EBUSYs: %d\n", num_ebusy.load());
+    if (clp->verbose && clp->aen_given && (num_abort_req > 0)) {
+        pr2serr("Number of Aborts: %d\n", num_abort_req.load());
+        pr2serr("Number of successful Aborts: %d\n",
+                num_abort_req_success.load());
+    }
+    if (clp->verbose && clp->m_aen_given && (num_mrq_abort_req > 0)) {
+        pr2serr("Number of MRQ Aborts: %d\n", num_mrq_abort_req.load());
+        pr2serr("Number of successful MRQ Aborts: %d\n",
+                num_mrq_abort_req_success.load());
+    }
+    if (clp->verbose && (num_miscompare > 0))
+        pr2serr("Number of miscompare%s: %d\n",
+                (num_miscompare > 1) ? "s" : "", num_miscompare.load());
+    if (clp->verbose > 1) {
+        if (clp->verbose > 3)
+            pr2serr("Final pack_id=%d, mrq_id=%d\n", mono_pack_id.load(),
+                    mono_mrq_id.load());
+        pr2serr("Number of SG_GET_NUM_WAITING calls=%ld\n",
+                num_waiting_calls.load());
+    }
+    if (clp->verify && (SG_LIB_CAT_MISCOMPARE == res))
+        pr2serr("Verify/compare failed due to miscompare\n");
+    return (res >= 0) ? res : SG_LIB_CAT_OTHER;
+}
diff --git a/testing/sgs_dd.c b/testing/sgs_dd.c
new file mode 100644
index 0000000..c139a17
--- /dev/null
+++ b/testing/sgs_dd.c
@@ -0,0 +1,1667 @@
+/*
+ * Test code for the extensions to the Linux OS SCSI generic ("sg")
+ * device driver.
+ * Copyright (C) 1999-2022 D. Gilbert and P. Allworth
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is a specialization of the Unix "dd" command in which
+ * one or both of the given files is a scsi generic device. A block size
+ * ('bs') is assumed to be 512 if not given. This program complains if
+ * 'ibs' or 'obs' are given with some other value than 'bs'.
+ * If 'if' is not given or 'if=-' then stdin is assumed. If 'of' is
+ * not given of 'of=-' then stdout assumed. The multipliers "c, b, k, m"
+ * are recognized on numeric arguments.
+ *
+ * A non-standard argument "bpt" (blocks per transfer) is added to control
+ * the maximum number of blocks in each transfer. The default bpt value is
+ * (64 * 1024 * 1024 / bs) or 1 if the first expression is 0. That is an
+ * integer division (rounds toward 0). For example if "bs=512" and "bpt=32"
+ * are given then a maximum of 32 blocks (16KB in this case) are transferred
+ * to or from the sg device in a single SCSI command.
+ *
+ * BEWARE: If the 'of' file is a 'sg' device (eg a disk) then it _will_
+ * be written to, potentially destroying its previous contents.
+ *
+ * This version should compile with Linux sg drivers with version numbers
+ * >= 30000 . Also this version also allows SIGPOLL or a RT signal to be
+ * chosen. SIGIO is a synonym for SIGPOLL; SIGIO seems to be deprecated.
+ */
+
+/* We need F_SETSIG, (signal redirect), so following define */
+#define _GNU_SOURCE 1
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <poll.h>
+#include <signal.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>           /* for mmap() system call */
+#include <sys/eventfd.h>
+#include <sys/epoll.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#ifndef HAVE_LINUX_SG_V4_HDR
+/* Kernel uapi header contain __user decorations on user space pointers
+ * to indicate they are unsafe in the kernel space. However glibc takes
+ * all those __user decorations out from headers in /usr/include/linux .
+ * So to stop compile errors when directly importing include/uapi/scsi/sg.h
+ * undef __user before doing that include. */
+#define __user
+
+/* Want to block the original sg.h header from also being included. That
+ * causes lots of multiple definition errors. This will only work if this
+ * header is included _before_ the original sg.h header.  */
+#define _SCSI_GENERIC_H         /* original kernel header guard */
+#define _SCSI_SG_H              /* glibc header guard */
+
+#include "uapi_sg.h"    /* local copy of include/uapi/scsi/sg.h */
+
+#else
+#define __user
+#endif  /* end of: ifndef HAVE_LINUX_SG_V4_HDR */
+
+#include "sg_lib.h"
+#include "sg_linux_inc.h"
+#include "sg_io_linux.h"
+#include "sg_pr2serr.h"
+#include "sg_unaligned.h"
+
+
+static const char * version_str = "4.24 20221020";
+static const char * my_name = "sgs_dd";
+
+#ifndef SGV4_FLAG_POLLED
+#define SGV4_FLAG_POLLED 0x800
+#endif
+
+#define DEF_BLOCK_SIZE 512
+#define DEF_BPT_TIMES_BS_SZ (64 * 1024) /* 64 KB */
+
+#define SENSE_BUFF_LEN 32       /* Arbitrary, could be larger */
+#define DEF_TIMEOUT 40000       /* 40,000 millisecs == 40 seconds */
+#define S_RW_LEN 10             /* Use SCSI READ(10) and WRITE(10) */
+#define SGQ_MAX_RD_AHEAD 32
+#define SGQ_MAX_WR_AHEAD 32
+#define SGQ_NUM_ELEMS (SGQ_MAX_RD_AHEAD + SGQ_MAX_WR_AHEAD + 1)
+#define MAX_BPT_VALUE (1 << 24)         /* used for maximum bs as well */
+#define MAX_COUNT_SKIP_SEEK (1LL << 48) /* coverity wants upper bound */
+
+#define SGQ_FREE 0
+#define SGQ_IO_STARTED 1
+#define SGQ_IO_FINISHED 2
+#define SGQ_IO_ERR 3
+#define SGQ_IO_WAIT 4
+
+#define SGQ_CAN_DO_NOTHING 0    /* only temporarily in use */
+#define SGQ_CAN_READ 1
+#define SGQ_CAN_WRITE 2
+#define SGQ_TIMEOUT 4
+
+#define DEF_SIGTIMEDWAIT_USEC 100
+
+
+#define STR_SZ 1024
+#define INOUTF_SZ 900
+#define EBUFF_SZ 1024
+
+struct flags_t {
+    bool dio;
+    bool evfd;
+    bool excl;
+    bool immed;
+    bool mmap;
+    bool noxfer;
+    bool pack;
+    bool polled;
+    bool tag;
+    bool v3;
+    bool v4;
+    bool given_v3v4;
+};
+
+typedef struct request_element
+{
+    struct request_element * nextp;
+    bool stop_after_wr;
+    bool wr;
+    int state;
+    int blk;
+    int num_blks;
+    uint8_t * buffp;
+    uint8_t * free_buffp;
+    sg_io_hdr_t io_hdr;
+    struct sg_io_v4 io_v4;
+    struct flags_t * iflagp;
+    struct flags_t * oflagp;
+    uint8_t cmd[S_RW_LEN];
+    uint8_t sb[SENSE_BUFF_LEN];
+    int result;
+} Rq_elem;
+
+typedef struct request_collection
+{
+    bool in_is_sg;
+    bool out_is_sg;
+    bool no_sig;
+    bool use_rt_sig;
+    bool both_mmap;
+    int infd;
+    int in_evfd;
+    int in_blk;                 /* most recent read */
+    int in_count;               /* most recent read */
+    int in_done_count;          /* count of completed in blocks */
+    int in_partial;
+    int outfd;
+    int out_evfd;
+    int lowest_seek;
+    int out_blk;                /* most recent write */
+    int out_count;              /* most recent write */
+    int out_done_count;         /* count of completed out blocks */
+    int out_partial;
+    int bs;
+    int bpt;
+    int dio_incomplete;
+    int sum_of_resids;
+    int poll_ms;
+    int pollerr_count;
+    int debug;                  /* also set with -v up to -vvvvv */
+    sigset_t blocked_sigs;
+    int sigs_waiting;
+    int sigs_rt_received;
+    int sigs_io_received;
+    int blk_poll_count;
+    Rq_elem * rd_posp;
+    Rq_elem * wr_posp;
+    uint8_t * in_mmapp;
+    uint8_t * out_mmapp;
+    struct flags_t iflag;
+    struct flags_t oflag;
+    Rq_elem elem[SGQ_NUM_ELEMS];
+} Rq_coll;
+
+static bool sgs_old_sg_driver = false;  /* true if VERSION_NUM < 4.00.00 */
+static bool sgs_full_v4_sg_driver = false; /* set if VERSION_NUM >= 4.00.30 */
+static bool sgs_nanosec_unit = false;
+
+static int sgq_rd_ahead_lim = SGQ_MAX_RD_AHEAD;
+static int sgq_wr_ahead_lim = SGQ_MAX_WR_AHEAD;
+static int sgq_num_elems = (SGQ_MAX_RD_AHEAD + SGQ_MAX_WR_AHEAD + 1);
+
+
+static void
+usage(int pg_num)
+{
+    if (pg_num > 1)
+        goto second_page;
+    printf("Usage: "
+           "sgs_dd  [bpt=BPT] [bs=BS] [count=NUM] [deb=DEB] [if=IFILE]\n"
+           "               [iflag=FLAGS] [no_sig=0|1] [of=OFILE] "
+           "[oflag=FLAGS]\n"
+           "               [poll_ms=MS] [rt_sig=0|1] [seek=SEEK] "
+           "[skip=SKIP]\n"
+           "               [--help] [--version]\n"
+           "where:\n"
+           "  bpt      blocks_per_transfer (default: 65536/bs (or 128 for "
+           "bs=512))\n"
+           "  bs       must be the logical block size of device (def: 512)\n"
+           "  deb      debug: 0->no debug (def); > 0 -> more debug\n"
+           "           -v (up to -vvvvv) sets deb value to number of 'v's\n"
+           "  iflag    comma separated list from: dio,evfd,excl,immed,mmap,"
+           "noxfer,\n"
+           "           null,pack,polled,tag,v3,v4 bound to IFILE\n"
+           "  no_sig   0-> use signals; 1-> no signals, hard polling "
+           "instead;\n"
+           "           default 0, unless polled flag(s) given then it's 1\n"
+           "  oflag    same flags as iflag but bound to OFILE\n"
+           "  poll_ms    number of milliseconds to wait on poll (def: 0)\n"
+           "  rt_sig   0->use SIGIO (def); 1->use RT sig (SIGRTMIN + 1)\n"
+           "  <other operands>     as per dd command\n\n");
+    printf("dd clone for testing Linux sg driver SIGPOLL and/or polling. "
+           "Either\nIFILE or OFILE must be a scsi generic device. If OFILE "
+           "not given then\n/dev/null assumed (rather than stdout like "
+           "dd). Use '-hh' for flag\ninformation.\n");
+    return;
+second_page:
+    printf("flag description:\n"
+           "  dio      this driver's version of O_DIRECT\n"
+           "  evfd     when poll() gives POLLIN, use eventfd to find "
+           "out how many\n"
+           "  excl     open IFILE or OFILE with O_EXCL\n"
+           "  hipri    same as 'polled'; name 'hipri' is deprecated\n"
+           "  immed    use SGV4_FLAG_IMMED flag on each request\n"
+           "  mmap     use mmap()-ed IO on IFILE or OFILE\n"
+           "  noxfer    no transfer between user space and kernel IO "
+           "buffers\n"
+           "  null      does nothing, placeholder\n"
+           "  pack      submit with rising pack_id, complete matching "
+           "each pack_id\n"
+           "  polled    set POLLED flag and use blk_poll() for completion\n"
+           "  tag       use tag (from block layer) rather than "
+           "pack_id\n"
+           "  v3        use sg v3 interface (default)\n"
+           "  v4        use sg vr interface (i.e. struct sg_io_v4)\n");
+}
+
+static int
+get_mmap_addr(int fd, int num, uint8_t ** mmpp)
+{
+    uint8_t * mmp;
+
+    if (! mmpp)
+        return -EINVAL;
+    mmp = (uint8_t *)mmap(NULL, num, PROT_READ | PROT_WRITE,
+                          MAP_SHARED, fd, 0);
+    if (MAP_FAILED == mmp) {
+        int err = errno;
+
+        pr2serr("%s%s: sz=%d, fd=%d, mmap() failed: %s\n",
+                my_name, __func__, num, fd, strerror(err));
+        return -err;
+    }
+    *mmpp = mmp;
+    return 0;
+}
+
+/* Return of 0 -> success, -1 -> failure, 2 -> try again */
+static int
+read_capacity(int sg_fd, int * num_sect, int * sect_sz)
+{
+    int res;
+    uint8_t rcCmdBlk [10] = {0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t rcBuff[64];
+    uint8_t sense_b[64];
+    sg_io_hdr_t io_hdr;
+
+    memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
+    io_hdr.interface_id = 'S';
+    io_hdr.cmd_len = sizeof(rcCmdBlk);
+    io_hdr.mx_sb_len = sizeof(sense_b);
+    io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+    io_hdr.dxfer_len = sizeof(rcBuff);
+    io_hdr.dxferp = rcBuff;
+    io_hdr.cmdp = rcCmdBlk;
+    io_hdr.sbp = sense_b;
+    io_hdr.timeout = DEF_TIMEOUT;
+
+    if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
+        res = -errno;
+        perror("read_capacity (SG_IO) error");
+        return res;
+    }
+    res = sg_err_category3(&io_hdr);
+    if (SG_LIB_CAT_UNIT_ATTENTION == res)
+        return 2; /* probably have another go ... */
+    else if (SG_LIB_CAT_CLEAN != res) {
+        sg_chk_n_print3("read capacity", &io_hdr, true);
+        return -1;
+    }
+    *num_sect = sg_get_unaligned_be32(rcBuff + 0) + 1;
+    *sect_sz = sg_get_unaligned_be32(rcBuff + 4);
+    return 0;
+}
+
+/* -ve -> unrecoverable error, 0 -> successful, 1 -> recoverable (ENOMEM) */
+static int
+sg_start_io(Rq_coll * clp, Rq_elem * rep)
+{
+    bool is_wr = rep->wr;
+    int res;
+    int fd = is_wr ? clp->outfd : clp->infd;
+    int num_bytes = clp->bs * rep->num_blks;
+    struct flags_t * flagp = is_wr ? rep->oflagp : rep->iflagp;
+    sg_io_hdr_t * hp = &rep->io_hdr;
+    struct sg_io_v4 * h4p = &rep->io_v4;
+
+    if (clp->both_mmap && is_wr)
+        memcpy(clp->out_mmapp, clp->in_mmapp, num_bytes);
+    memset(rep->cmd, 0, sizeof(rep->cmd));
+    rep->cmd[0] = is_wr ? 0x2a : 0x28;
+    sg_put_unaligned_be32((uint32_t)rep->blk, rep->cmd + 2);
+    sg_put_unaligned_be16((uint16_t)rep->num_blks, rep->cmd + 7);
+    if (flagp->v4)
+        goto do_v4;
+
+    memset(hp, 0, sizeof(sg_io_hdr_t));
+    hp->interface_id = 'S';
+    hp->cmd_len = sizeof(rep->cmd);
+    hp->cmdp = rep->cmd;
+    hp->dxfer_direction = is_wr ? SG_DXFER_TO_DEV : SG_DXFER_FROM_DEV;
+    hp->dxfer_len = num_bytes;
+    hp->mx_sb_len = sizeof(rep->sb);
+    hp->sbp = rep->sb;
+    hp->timeout = DEF_TIMEOUT;
+    hp->usr_ptr = rep;
+    hp->pack_id = rep->blk;
+    if (flagp->dio)
+        hp->flags |= SG_FLAG_DIRECT_IO;
+    if (flagp->noxfer)
+        hp->flags |= SG_FLAG_NO_DXFER;
+    if (flagp->immed)
+        hp->flags |= SGV4_FLAG_IMMED;
+    if (flagp->polled)
+        hp->flags |= SGV4_FLAG_POLLED;
+    if (flagp->mmap) {
+        hp->flags |= SG_FLAG_MMAP_IO;
+        hp->dxferp = is_wr ? clp->out_mmapp : clp->in_mmapp;
+    } else
+        hp->dxferp = rep->buffp;
+    if (flagp->evfd)
+        hp->flags |= SGV4_FLAG_EVENTFD;
+    if (clp->debug > 5) {
+        pr2serr("%s: SCSI %s, blk=%d num_blks=%d\n", __func__,
+                is_wr ? "WRITE" : "READ", rep->blk, rep->num_blks);
+        sg_print_command(hp->cmdp);
+        pr2serr("dir=%d, len=%d, dxfrp=%p, cmd_len=%d\n", hp->dxfer_direction,
+                hp->dxfer_len, hp->dxferp, hp->cmd_len);
+    }
+
+    while (((res = write(fd, hp, sizeof(sg_io_hdr_t))) < 0) &&
+           (EINTR == errno))
+        ;
+    if (res < 0) {
+        if (ENOMEM == errno)
+            return 1;
+        if ((EDOM == errno) || (EAGAIN == errno) || (EBUSY == errno)) {
+            rep->state = SGQ_IO_WAIT;   /* busy so wait */
+            return 0;
+        }
+        pr2serr("%s: write(): %s [%d]\n", __func__, strerror(errno), errno);
+        rep->state = SGQ_IO_ERR;
+        return res;
+    }
+    rep->state = SGQ_IO_STARTED;
+    if (! clp->no_sig)
+        clp->sigs_waiting++;
+    return 0;
+do_v4:
+    memset(h4p, 0, sizeof(struct sg_io_v4));
+    h4p->guard = 'Q';
+    h4p->request_len = sizeof(rep->cmd);
+    h4p->request = (uint64_t)(uintptr_t)rep->cmd;
+    if (is_wr)
+        h4p->dout_xfer_len = num_bytes;
+    else if (rep->num_blks > 0)
+        h4p->din_xfer_len = num_bytes;
+    h4p->max_response_len = sizeof(rep->sb);
+    h4p->response = (uint64_t)(uintptr_t)rep->sb;
+    h4p->timeout = DEF_TIMEOUT;
+    h4p->usr_ptr = (uint64_t)(uintptr_t)rep;
+    h4p->request_extra = rep->blk;/* N.B. blk --> pack_id --> request_extra */
+    if (flagp->dio)
+        h4p->flags |= SG_FLAG_DIRECT_IO;
+    if (flagp->noxfer)
+        h4p->flags |= SG_FLAG_NO_DXFER;
+    if (flagp->immed)
+        h4p->flags |= SGV4_FLAG_IMMED;
+    if (flagp->polled)
+        h4p->flags |= SGV4_FLAG_POLLED;
+    if (flagp->mmap) {
+        h4p->flags |= SG_FLAG_MMAP_IO;
+        hp->dxferp = is_wr ? clp->out_mmapp : clp->in_mmapp;
+    } else {
+        if (is_wr)
+            h4p->dout_xferp = (uint64_t)(uintptr_t)rep->buffp;
+        else if (rep->num_blks > 0)
+            h4p->din_xferp = (uint64_t)(uintptr_t)rep->buffp;
+    }
+    if (flagp->tag)
+        h4p->flags |= SGV4_FLAG_YIELD_TAG;
+    if (flagp->evfd)
+        h4p->flags |= SGV4_FLAG_EVENTFD;
+    if (! clp->no_sig)
+        h4p->flags |= SGV4_FLAG_SIGNAL;
+
+    while (((res = ioctl(fd, SG_IOSUBMIT, h4p)) < 0) && (EINTR == errno))
+        ;
+    if (res < 0) {
+        if (ENOMEM == errno)
+            return 1;
+        if ((EDOM == errno) || (EAGAIN == errno) || (EBUSY == errno)) {
+            rep->state = SGQ_IO_WAIT;   /* busy so wait */
+            return 0;
+        }
+        pr2serr("%s: ioctl(SG_IOSUBMIT): %s [%d]\n", __func__,
+                strerror(errno), errno);
+        rep->state = SGQ_IO_ERR;
+        return res;
+    }
+    rep->state = SGQ_IO_STARTED;
+    if (! clp->no_sig)
+        clp->sigs_waiting++;
+    if (clp->debug > 5) {
+        if (is_wr ? clp->oflag.tag : clp->iflag.tag)
+            pr2serr("%s:  generated_tag=0x%" PRIx64 "\n", __func__,
+                    (uint64_t)h4p->generated_tag);
+    }
+    return 0;
+}
+
+/* -1 -> unrecoverable error, 0 -> successful, 1 -> try again */
+static int
+sg_finish_io(Rq_coll * clp, bool wr, Rq_elem ** repp)
+{
+    struct flags_t *flagsp = wr ? &clp->oflag : &clp->iflag;
+    bool dio = false;
+    bool is_v4 = flagsp->v4;
+    bool use_pack = flagsp->pack;
+    bool use_tag = flagsp->tag;
+    int fd = wr ? clp->outfd : clp->infd;
+    int res, id, n;
+    sg_io_hdr_t io_hdr;
+    sg_io_hdr_t * hp;
+    struct sg_io_v4 io_v4;
+    struct sg_io_v4 * h4p;
+    Rq_elem * rep;
+
+    if (is_v4)
+        goto do_v4;
+    if (use_pack) {
+        while (true) {
+            if ( ((res = ioctl(fd, SG_GET_NUM_WAITING, &n))) < 0) {
+                res = -errno;
+                pr2serr("%s: ioctl(SG_GET_NUM_WAITING): %s [%d]\n",
+                        __func__, strerror(errno), errno);
+                return res;
+            }
+            if (n > 0) {
+                if ( (ioctl(fd, SG_GET_PACK_ID, &id)) < 0) {
+                    res = errno;
+                    pr2serr("%s: ioctl(SG_GET_PACK_ID): %s [%d]\n",
+                            __func__, strerror(res), res);
+                    return -res;
+                }
+                /* got pack_id or tag of first waiting */
+                break;
+            }
+        }
+    }
+    memset(&io_hdr, 0 , sizeof(sg_io_hdr_t));
+    if (use_pack)
+        io_hdr.pack_id = id;
+    while (((res = read(fd, &io_hdr, sizeof(sg_io_hdr_t))) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    rep = (Rq_elem *)io_hdr.usr_ptr;
+    if (rep) {
+        dio = flagsp->dio;
+        if (rep->io_hdr.flags & SGV4_FLAG_POLLED)
+            ++clp->blk_poll_count;
+    }
+    if (res < 0) {
+        res = -errno;
+        pr2serr("%s: read(): %s [%d]\n", __func__, strerror(errno), errno);
+        if (rep)
+            rep->state = SGQ_IO_ERR;
+        return res;
+    }
+    if (! (rep && (SGQ_IO_STARTED == rep->state))) {
+        pr2serr("%s: bad usr_ptr\n", __func__);
+        if (rep)
+            rep->state = SGQ_IO_ERR;
+        return -1;
+    }
+    memcpy(&rep->io_hdr, &io_hdr, sizeof(sg_io_hdr_t));
+    hp = &rep->io_hdr;
+    if (repp)
+        *repp = rep;
+
+    switch (sg_err_category3(hp)) {
+        case SG_LIB_CAT_CLEAN:
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            pr2serr("Recovered error on block=%d, num=%d\n", rep->blk,
+                    rep->num_blks);
+            break;
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            return 1;
+        default:
+            sg_chk_n_print3(wr ? "writing": "reading", hp, true);
+            rep->state = SGQ_IO_ERR;
+            return -1;
+    }
+    if (dio && ((hp->info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+        ++clp->dio_incomplete; /* count dios done as indirect IO */
+    clp->sum_of_resids += hp->resid;
+    rep->state = SGQ_IO_FINISHED;
+    if (clp->debug > 5) {
+        pr2serr("%s: %s  ", __func__, wr ? "writing" : "reading");
+        pr2serr("    SGQ_IO_FINISHED elem idx=%zd\n", rep - clp->elem);
+    }
+    return 0;
+do_v4:
+    id = -1;
+    if (use_pack || use_tag) {
+        while (true) {
+            if ( ((res = ioctl(fd, SG_GET_NUM_WAITING, &n))) < 0) {
+                res = -errno;
+                pr2serr("%s: ioctl(SG_GET_NUM_WAITING): %s [%d]\n",
+                        __func__, strerror(errno), errno);
+                return res;
+            }
+            if (n > 0) {
+                if ( (ioctl(fd, SG_GET_PACK_ID, &id)) < 0) {
+                    res = errno;
+                    pr2serr("%s: ioctl(SG_GET_PACK_ID): %s [%d]\n",
+                            __func__, strerror(res), res);
+                    return -res;
+                }
+                /* got pack_id or tag of first waiting */
+                break;
+            }
+        }
+    }
+    memset(&io_v4, 0 , sizeof(io_v4));
+    io_v4.guard = 'Q';
+    if (use_tag)
+        io_v4.request_tag = id;
+    else if (use_pack)
+        io_v4.request_extra = id;
+    io_v4.flags |= SGV4_FLAG_IMMED;
+    if (flagsp->evfd)
+        io_v4.flags |= SGV4_FLAG_EVENTFD;
+    while (((res = ioctl(fd, SG_IORECEIVE, &io_v4)) < 0) &&
+           ((EINTR == errno) || (EAGAIN == errno) || (EBUSY == errno)))
+        ;
+    rep = (Rq_elem *)(unsigned long)io_v4.usr_ptr;
+    if (res < 0) {
+        res = -errno;
+        pr2serr("%s: ioctl(SG_IORECEIVE): %s [%d]\n", __func__,
+                strerror(errno), errno);
+        if (rep)
+            rep->state = SGQ_IO_ERR;
+        return res;
+    }
+    if (rep) {
+        if (rep->io_v4.flags & SGV4_FLAG_POLLED)
+            ++clp->blk_poll_count;
+    }
+    if (! (rep && (SGQ_IO_STARTED == rep->state))) {
+        pr2serr("%s: bad usr_ptr=0x%p\n", __func__, (void *)rep);
+        if (rep)
+            rep->state = SGQ_IO_ERR;
+        return -1;
+    }
+    memcpy(&rep->io_v4, &io_v4, sizeof(struct sg_io_v4));
+    h4p = &rep->io_v4;
+    if (repp)
+        *repp = rep;
+
+    res = sg_err_category_new(h4p->device_status, h4p->transport_status,
+                              h4p->driver_status,
+                      (const uint8_t *)(unsigned long)h4p->response,
+                              h4p->response_len);
+    switch (res) {
+        case SG_LIB_CAT_CLEAN:
+            break;
+        case SG_LIB_CAT_RECOVERED:
+            pr2serr("Recovered error on block=%d, num=%d\n", rep->blk,
+                    rep->num_blks);
+            break;
+        case SG_LIB_CAT_UNIT_ATTENTION:
+            return 1;
+        default:
+            sg_linux_sense_print(wr ? "writing": "reading",
+                                 h4p->device_status, h4p->transport_status,
+                                 h4p->driver_status,
+                         (const uint8_t *)(unsigned long)h4p->response,
+                                 h4p->response_len, true);
+            rep->state = SGQ_IO_ERR;
+            return -1;
+    }
+    if (dio && ((h4p->info & SG_INFO_DIRECT_IO_MASK) != SG_INFO_DIRECT_IO))
+        ++clp->dio_incomplete; /* count dios done as indirect IO */
+    clp->sum_of_resids += h4p->din_resid;
+    rep->state = SGQ_IO_FINISHED;
+    if (clp->debug > 5) {
+        pr2serr("%s: %s  ", __func__, wr ? "writing" : "reading");
+        pr2serr("    SGQ_IO_FINISHED elem idx=%zd\n", rep - clp->elem);
+        if (use_pack)
+            pr2serr("%s:  pack_id=%d\n", __func__, h4p->request_extra);
+        else if (use_tag)
+            pr2serr("%s:  request_tag=0x%" PRIx64 "\n", __func__,
+                    (uint64_t)h4p->request_tag);
+    }
+    return 0;
+}
+
+static int
+sz_reserve(Rq_coll * clp, bool is_in)
+{
+    const struct flags_t *flagsp = is_in ? &clp->iflag : &clp->oflag;
+    bool pack = flagsp->pack;
+    bool vb = clp->debug;
+    int res, t, flags, err;
+    int fd = is_in ? clp->infd : clp->outfd;
+    int tag = flagsp->tag;
+    struct sg_extended_info sei;
+    struct sg_extended_info * seip;
+
+    seip = &sei;
+    res = ioctl(fd, SG_GET_VERSION_NUM, &t);
+    if ((res < 0) || (t < 30000)) {
+        pr2serr("%s: sg driver prior to 3.0.00\n", my_name);
+        return 1;
+    } else if (t < 40000) {
+        if (vb)
+            pr2serr("%s: warning: sg driver prior to 4.0.00\n", my_name);
+        sgs_old_sg_driver = true;
+    } else if (t < 40045) {
+        sgs_old_sg_driver = false;
+        sgs_full_v4_sg_driver = false;
+    } else
+        sgs_full_v4_sg_driver = true;
+    t = clp->bs * clp->bpt;
+    res = ioctl(fd, SG_SET_RESERVED_SIZE, &t);
+    if (res < 0)
+        perror("sgs_dd: SG_SET_RESERVED_SIZE error");
+
+    if (sgs_full_v4_sg_driver) {
+        if (sgs_nanosec_unit) {
+            memset(seip, 0, sizeof(*seip));
+            seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+            seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TIME_IN_NS;
+            seip->ctl_flags |= SG_CTL_FLAGM_TIME_IN_NS;
+            if (ioctl(fd, SG_SET_GET_EXTENDED, seip) < 0) {
+                pr2serr("ioctl(EXTENDED(TIME_IN_NS)) failed, errno=%d %s\n",
+                        errno, strerror(errno));
+                return 1;
+            }
+        }
+        if (tag || pack) {
+            t = 1;
+            if (ioctl(fd, SG_SET_FORCE_PACK_ID, &t) < 0) {
+                pr2serr("ioctl(SG_SET_FORCE_PACK_ID(on)) failed, errno=%d "
+                        "%s\n", errno, strerror(errno));
+                return 1;
+            }
+            if (tag) {
+                memset(seip, 0, sizeof(*seip));
+                seip->sei_wr_mask |= SG_SEIM_CTL_FLAGS;
+                seip->ctl_flags_wr_mask |= SG_CTL_FLAGM_TAG_FOR_PACK_ID;
+                seip->ctl_flags |= SG_CTL_FLAGM_TAG_FOR_PACK_ID;
+                if (ioctl(fd, SG_SET_GET_EXTENDED, seip) < 0) {
+                    pr2serr("ioctl(EXTENDED(TAG_FOR_PACK_ID)) failed, "
+                            "errno=%d %s\n", errno, strerror(errno));
+                    return 1;
+                }
+            }
+        }
+        if (flagsp->evfd) {
+            int evfd = eventfd(0,0);
+
+            if (evfd < 0) {
+                err = errno;
+                pr2serr("eventfd() failed: %s\n", strerror(err));
+                return 1;
+            }
+            if (is_in)
+                clp->in_evfd = evfd;
+            else
+                clp->out_evfd = evfd;
+
+            memset(seip, 0, sizeof(*seip));
+            seip->sei_wr_mask |= SG_SEIM_EVENTFD;
+            seip->sei_rd_mask |= SG_SEIM_EVENTFD;
+            seip->share_fd = evfd;
+            if (ioctl(fd, SG_SET_GET_EXTENDED, seip) < 0) {
+                err = errno;
+                pr2serr("ioctl(EXTENDED(SG_SEIM_EVENTFD)) failed, "
+                        "errno=%d %s\n", err, strerror(err));
+                return 1;
+            }
+        }
+    }
+    if (!clp->no_sig) {
+        if (-1 == fcntl(fd, F_SETOWN, getpid())) {
+            perror("fcntl(F_SETOWN)");
+            return 1;
+        }
+        flags = fcntl(fd, F_GETFL, 0);
+        if (-1 == fcntl(fd, F_SETFL, flags | O_ASYNC)) {
+            perror("fcntl(F_SETFL)");
+            return 1;
+        }
+        if (clp->use_rt_sig) {/* displaces SIGIO/SIGPOLL with SIGRTMIN + 1 */
+            if (-1 == fcntl(fd, F_SETSIG, SIGRTMIN + 1))
+                perror("fcntl(F_SETSIG)");
+        }
+    }
+    return 0;
+}
+
+static int
+init_elems(Rq_coll * clp)
+{
+    bool either_mmap = false;
+    int res = 0;
+    int num_bytes = clp->bpt * clp->bs;
+    int k;
+    Rq_elem * rep;
+
+    clp->wr_posp = &clp->elem[0]; /* making ring buffer */
+    clp->rd_posp = clp->wr_posp;
+    if (clp->iflag.mmap || clp->oflag.mmap) {
+        int res;
+
+        either_mmap = true;
+        sgq_num_elems = 2;
+        sgq_rd_ahead_lim = 1;
+        sgq_wr_ahead_lim = 1;
+        if (clp->iflag.mmap) {
+            res = get_mmap_addr(clp->infd, num_bytes, &clp->in_mmapp);
+            if (res < 0)
+                return res;
+        }
+        if (clp->oflag.mmap) {
+            res = get_mmap_addr(clp->outfd, num_bytes, &clp->out_mmapp);
+            if (res < 0)
+                return res;
+        }
+    }
+    for (k = 0; k < sgq_num_elems - 1; ++k)
+        clp->elem[k].nextp = &clp->elem[k + 1];
+    clp->elem[sgq_num_elems - 1].nextp = &clp->elem[0];
+    for (k = 0; k < sgq_num_elems; ++k) {
+        rep = &clp->elem[k];
+        rep->state = SGQ_FREE;
+        rep->iflagp = &clp->iflag;
+        rep->oflagp = &clp->oflag;
+        if (either_mmap) {
+            if (clp->both_mmap)
+                continue;
+            if (clp->iflag.mmap)
+                rep->buffp = clp->in_mmapp;
+            else
+                rep->buffp = clp->out_mmapp;
+            continue;
+        }
+        rep->buffp = sg_memalign(num_bytes, 0, &rep->free_buffp, false);
+        if (NULL == rep->buffp) {
+            pr2serr("out of memory creating user buffers\n");
+            res = -ENOMEM;
+        }
+    }
+    return res;
+}
+
+static void
+remove_elems(Rq_coll * clp)
+{
+    Rq_elem * rep;
+    int k;
+
+    for (k = 0; k < sgq_num_elems; ++k) {
+        rep = &clp->elem[k];
+        if (rep->free_buffp)
+            free(rep->free_buffp);
+    }
+}
+
+static int
+start_read(Rq_coll * clp)
+{
+    int blocks = (clp->in_count > clp->bpt) ? clp->bpt : clp->in_count;
+    Rq_elem * rep = clp->rd_posp;
+    int buf_sz, res;
+    char ebuff[EBUFF_SZ];
+
+    if (clp->debug > 5)
+        pr2serr("%s: elem idx=%zd\n", __func__, rep - clp->elem);
+    rep->wr = false;
+    rep->blk = clp->in_blk;
+    rep->num_blks = blocks;
+    clp->in_blk += blocks;
+    clp->in_count -= blocks;
+    if (clp->in_is_sg) {
+        res = sg_start_io(clp, rep);
+        if (1 == res) {     /* ENOMEM, find what's available+try that */
+            if (ioctl(clp->infd, SG_GET_RESERVED_SIZE, &buf_sz) < 0) {
+                res = -errno;
+                perror("RESERVED_SIZE ioctls failed");
+                return res;
+            }
+            clp->bpt = (buf_sz + clp->bs - 1) / clp->bs;
+            pr2serr("Reducing blocks per transfer to %d\n", clp->bpt);
+            if (clp->bpt < 1)
+                return -ENOMEM;
+            res = sg_start_io(clp, rep);
+            if (1 == res)
+                res = -ENOMEM;
+        }
+        if (res < 0) {
+            pr2serr("%s: inputting from sg failed, blk=%d\n", my_name,
+                    rep->blk);
+            rep->state = SGQ_IO_ERR;
+            return res;
+        }
+    }
+    else {
+        rep->state = SGQ_IO_STARTED;
+        while (((res = read(clp->infd, rep->buffp, blocks * clp->bs)) < 0) &&
+               (EINTR == errno))
+            ;
+        if (res < 0) {
+            res = -errno;
+            snprintf(ebuff, EBUFF_SZ, "%s: reading, in_blk=%d ", my_name,
+                     rep->blk);
+            perror(ebuff);
+            rep->state = SGQ_IO_ERR;
+            return res;
+        }
+        if (res < blocks * clp->bs) {
+            int o_blocks = blocks;
+            rep->stop_after_wr = true;
+            blocks = res / clp->bs;
+            if ((res % clp->bs) > 0) {
+                blocks++;
+                clp->in_partial++;
+            }
+            /* Reverse out + re-apply blocks on clp */
+            clp->in_blk -= o_blocks;
+            clp->in_count += o_blocks;
+            rep->num_blks = blocks;
+            clp->in_blk += blocks;
+            clp->in_count -= blocks;
+        }
+        clp->in_done_count -= blocks;
+        rep->state = SGQ_IO_FINISHED;
+    }
+    clp->rd_posp = rep->nextp;
+    return blocks;
+}
+
+static int
+start_write(Rq_coll * clp)
+{
+    Rq_elem * rep = clp->wr_posp;
+    int res, blocks;
+    char ebuff[EBUFF_SZ];
+
+    while ((0 != rep->wr) || (SGQ_IO_FINISHED != rep->state)) {
+        rep = rep->nextp;
+        if (rep == clp->rd_posp)
+            return -1;
+    }
+    if (clp->debug > 5)
+        pr2serr("%s: elem idx=%zd\n", __func__, rep - clp->elem);
+    rep->wr = true;
+    blocks = rep->num_blks;
+    rep->blk = clp->out_blk;
+    clp->out_blk += blocks;
+    clp->out_count -= blocks;
+    if (clp->out_is_sg) {
+        res = sg_start_io(clp, rep);
+        if (1 == res)      /* ENOMEM, give up */
+            return -ENOMEM;
+        else if (res < 0) {
+            pr2serr("%s: output to sg failed, blk=%d\n", my_name, rep->blk);
+            rep->state = SGQ_IO_ERR;
+            return res;
+        }
+    }
+    else {
+        rep->state = SGQ_IO_STARTED;
+        while (((res = write(clp->outfd, rep->buffp,
+                     rep->num_blks * clp->bs)) < 0) && (EINTR == errno))
+            ;
+        if (res < 0) {
+            res = -errno;
+            snprintf(ebuff, EBUFF_SZ, "%s: output, out_blk=%d ", my_name,
+                     rep->blk);
+            perror(ebuff);
+            rep->state = SGQ_IO_ERR;
+            return res;
+        }
+        if (res < blocks * clp->bs) {
+            blocks = res / clp->bs;
+            if ((res % clp->bs) > 0) {
+                blocks++;
+                clp->out_partial++;
+            }
+            rep->num_blks = blocks;
+        }
+        rep->state = SGQ_IO_FINISHED;
+    }
+    return blocks;
+}
+
+/* Returns 0 if SIGIO/SIGPOLL or (SIGRTMIN + 1) received, else returns negated
+ * errno value; -EAGAIN for timeout. */
+static int
+do_sigwait(Rq_coll * clp, bool inc1_clear0)
+{
+    siginfo_t info;
+    struct timespec ts;
+
+    if (clp->debug > 9)
+        pr2serr("%s: inc1_clear0=%d\n", __func__, (int)inc1_clear0);
+    ts.tv_sec = 0;
+    ts.tv_nsec = DEF_SIGTIMEDWAIT_USEC * 1000;
+    while (sigtimedwait(&clp->blocked_sigs, &info, &ts) < 0) {
+        int err = errno;
+
+        if (EINTR != err) {
+
+            if (EAGAIN != err)
+                pr2serr("%s: sigtimedwait(): %s [%d]\n", __func__,
+                        strerror(err), err);
+            return -err;        /* EAGAIN is timeout error */
+        }
+    }
+    if ((SIGRTMIN + 1) == info.si_signo) {
+        if (inc1_clear0) {
+            clp->sigs_waiting--;
+            clp->sigs_rt_received++;
+        } else
+            clp->sigs_waiting = 0;
+    } else if (SIGPOLL == info.si_signo) {
+        if (inc1_clear0) {
+            clp->sigs_waiting--;
+            clp->sigs_io_received++;
+        } else
+            clp->sigs_waiting = 0;
+    } else {
+        pr2serr("%s: sigwaitinfo() returned si_signo=%d\n",
+                __func__, info.si_signo);
+        return -EINVAL;
+    }
+    return 0;
+}
+
+/* Returns 1 (or more) on success (found), 0 on not found, -1 on error. */
+static int
+do_num_poll_in(Rq_coll * clp, int fd, bool is_evfd)
+{
+    int err, res;
+    struct pollfd a_pollfd = {0, POLLIN | POLLOUT, 0};
+
+    if (! clp->no_sig) {
+        if (clp->sigs_waiting) {
+            int res = do_sigwait(clp, true);
+
+            if ((res < 0) && (-EAGAIN != res))
+                return res;
+        }
+    }
+    a_pollfd.fd = fd;
+    if (poll(&a_pollfd, 1, clp->poll_ms) < 0) {
+        err = errno;
+        pr2serr("%s: poll(): %s [%d]\n", __func__, strerror(err), err);
+        return -err;
+    }
+    /* pr2serr("%s: revents=0x%x\n", __func__, a_pollfd.revents); */
+    if (a_pollfd.revents & POLLIN) {
+        if (is_evfd) {
+            uint64_t count;
+
+            if ((res = read(fd, &count, sizeof(count))) < 0) {
+                err = errno;
+                pr2serr("%s: read(): %s [%d]\n", __func__,
+                        strerror(err), err);
+                return -err;
+            }
+            return (res < (int)sizeof(uint64_t)) ? 0 : (int)count;
+        } else
+            return 1;   /* could be more but don't know without evfd */
+    } else if (a_pollfd.revents & POLLERR)
+        ++clp->pollerr_count;
+
+    return 0;
+}
+
+static int
+can_read_write(Rq_coll * clp)
+{
+    Rq_elem * rep = NULL;
+    bool writeable = false;
+    bool in_is_evfd = (clp->in_evfd >= 0);
+    bool out_is_evfd = (clp->out_evfd >= 0);
+    int res = 0;
+    int reading = 0;
+    int writing = 0;
+    int rd_waiting = 0;
+    int wr_waiting = 0;
+    int sg_finished = 0;
+    int num;
+    int ofd = out_is_evfd ? clp->out_evfd : clp->outfd;
+    int ifd= in_is_evfd ? clp->in_evfd : clp->infd;
+
+    /* if write completion pending, then complete it + start read */
+    if (clp->out_is_sg) {
+        while ((res = do_num_poll_in(clp, ofd, out_is_evfd))) {
+            if (res < 0)
+                return res;
+            num = res;
+            while (--num >= 0) {
+                res = sg_finish_io(clp, true /* write */, &rep);
+                if (res < 0)
+                    return res;
+                else if (1 == res) {
+                    res = sg_start_io(clp, rep);
+                    if (0 != res)
+                        return -1;  /* give up if any problems with retry */
+                } else
+                    sg_finished++;
+            }
+        }
+        while ((rep = clp->wr_posp) && (SGQ_IO_FINISHED == rep->state) &&
+               rep->wr && (rep != clp->rd_posp)) {
+            rep->state = SGQ_FREE;
+            clp->out_done_count -= rep->num_blks;
+            clp->wr_posp = rep->nextp;
+            if (rep->stop_after_wr)
+                return -1;
+        }
+    }
+    else if ((rep = clp->wr_posp) && rep->wr &&
+             (SGQ_IO_FINISHED == rep->state)) {
+        rep->state = SGQ_FREE;
+        clp->out_done_count -= rep->num_blks;
+        clp->wr_posp = rep->nextp;
+        if (rep->stop_after_wr)
+            return -1;
+    }
+
+    /* if read completion pending, then complete it + start maybe write */
+    if (clp->in_is_sg) {
+        while ((res = do_num_poll_in(clp, ifd, in_is_evfd))) {
+            if (res < 0)
+                return res;
+            num = res;
+            while (--num >= 0) {
+                res = sg_finish_io(clp, false /* read */, &rep);
+                if (res < 0)
+                    return res;
+                if (1 == res) {
+                    res = sg_start_io(clp, rep);
+                    if (0 != res)
+                        return -1;  /* give up if any problems with retry */
+                } else {
+                    sg_finished++;
+                    clp->in_done_count -= rep->num_blks;
+                }
+            }
+        }
+    }
+
+    for (rep = clp->wr_posp, res = 1;
+         rep && (rep != clp->rd_posp); rep = rep->nextp) {
+        if (SGQ_IO_STARTED == rep->state) {
+            if (rep->wr)
+                ++writing;
+            else {
+                res = 0;
+                ++reading;
+            }
+        }
+        else if ((! rep->wr) && (SGQ_IO_FINISHED == rep->state)) {
+            if (res)
+                writeable = true;
+        }
+        else if (SGQ_IO_WAIT == rep->state) {
+            res = 0;
+            if (rep->wr)
+                ++wr_waiting;
+            else
+                ++rd_waiting;
+        }
+        else
+            res = 0;
+    }
+    if (clp->debug > 6) {
+        if ((clp->debug > 7) || wr_waiting || rd_waiting) {
+            pr2serr("%d/%d (nwb/nrb): read=%d/%d (do/wt) "
+                    "write=%d/%d (do/wt) writeable=%d sg_fin=%d\n",
+                    clp->out_blk, clp->in_blk, reading, rd_waiting,
+                    writing, wr_waiting, (int)writeable, sg_finished);
+        }
+        // fflush(stdout);
+    }
+    if (writeable && (writing < sgq_wr_ahead_lim) && (clp->out_count > 0))
+        return SGQ_CAN_WRITE;
+    if ((reading < sgq_rd_ahead_lim) && (clp->in_count > 0) &&
+        (0 == rd_waiting) && (clp->rd_posp->nextp != clp->wr_posp))
+        return SGQ_CAN_READ;
+
+    if (clp->out_done_count <= 0)
+        return SGQ_CAN_DO_NOTHING;
+
+    /* usleep(10000); */      /* hang about for 10 milliseconds */
+    if ((! clp->no_sig) && clp->sigs_waiting) {
+        res = do_sigwait(clp, false);
+        if ((res < 0) && (-EAGAIN != res))
+            return res;     /* wasn't timeout */
+    }
+    /* Now check the _whole_ buffer for pending requests */
+    for (rep = clp->rd_posp->nextp; rep && (rep != clp->rd_posp);
+         rep = rep->nextp) {
+        if (SGQ_IO_WAIT == rep->state) {
+            res = sg_start_io(clp, rep);
+            if (res < 0)
+                return res;
+            if (res > 0)
+                return -1;
+            break;
+        }
+    }
+    return SGQ_CAN_DO_NOTHING;
+}
+
+static bool
+process_flags(const char * arg, struct flags_t * fp)
+{
+    char buff[256];
+    char * cp;
+    char * np;
+
+    strncpy(buff, arg, sizeof(buff));
+    buff[sizeof(buff) - 1] = '\0';
+    if ('\0' == buff[0]) {
+        pr2serr("no flag found, 'null' can be used as a placeholder\n");
+        return false;
+    }
+    cp = buff;
+    do {
+        np = strchr(cp, ',');
+        if (np)
+            *np++ = '\0';
+        if (0 == strcmp(cp, "dio"))
+            fp->dio = true;
+        else if (0 == strcmp(cp, "evfd"))
+            fp->evfd = true;
+        else if (0 == strcmp(cp, "excl"))
+            fp->excl = true;
+        else if (0 == strcmp(cp, "hipri"))
+            fp->polled = true;
+        else if (0 == strcmp(cp, "immed"))
+            fp->immed = true;
+        else if (0 == strcmp(cp, "mmap"))
+            fp->mmap = true;
+        else if (0 == strcmp(cp, "noxfer"))
+            fp->noxfer = true;
+        else if (0 == strcmp(cp, "null"))
+            ;
+        else if (0 == strcmp(cp, "pack"))
+            fp->pack = true;
+        else if (0 == strcmp(cp, "polled"))
+            fp->polled = true;
+        else if (0 == strcmp(cp, "tag"))
+            fp->tag = true;
+        else if (0 == strcmp(cp, "v3")) {
+            fp->v3 = true;
+            fp->v4 = false;
+            fp->given_v3v4 = true;
+        } else if (0 == strcmp(cp, "v4")) {
+            fp->v3 = false;
+            fp->v4 = true;
+            fp->given_v3v4 = true;
+        } else {
+            pr2serr("unrecognised flag: %s\n", cp);
+            return false;
+        }
+        cp = np;
+    } while (cp);
+    if (fp->dio && fp->mmap) {
+        pr2serr(" Can't set both mmap and dio\n");
+        return false;
+    }
+    if ((fp->dio || fp->mmap) && fp->noxfer) {
+        pr2serr(" Can't have mmap or dio with noxfer\n");
+        return false;
+    }
+    return true;
+}
+
+
+int
+main(int argc, char * argv[])
+{
+    bool bs_given = false;
+    bool no_sig_given = false;
+    bool polled_present;
+    int skip = 0;
+    int seek = 0;
+    int ibs = 0;
+    int obs = 0;
+    int count = -1;
+    int in_num_sect = 0;
+    int out_num_sect = 0;
+    int help_pg = 0;
+    int res, k, in_sect_sz, out_sect_sz, crw, open_fl;
+    char str[STR_SZ];
+    char * key;
+    char * buf;
+    char inf[INOUTF_SZ];
+    char outf[INOUTF_SZ];
+    char ebuff[EBUFF_SZ];
+    Rq_coll rcoll;
+    Rq_coll * clp = &rcoll;
+
+    memset(clp, 0, sizeof(*clp));
+    clp->bpt = 0;
+    clp->in_evfd = -1;
+    clp->out_evfd = -1;
+    clp->iflag.v3 = true;
+    clp->oflag.v3 = true;
+    inf[0] = '\0';
+    outf[0] = '\0';
+    if (argc < 2) {
+        usage(1);
+        return 1;
+    }
+    sgs_nanosec_unit = !!getenv("SG3_UTILS_LINUX_NANO");
+
+    for(k = 1; k < argc; k++) {
+        if (argv[k]) {
+            strncpy(str, argv[k], STR_SZ);
+            str[STR_SZ - 1] = '\0';
+        }
+        else
+            continue;
+        for(key = str, buf = key; *buf && *buf != '=';)
+            buf++;
+        if (*buf)
+            *buf++ = '\0';
+        if (0 == strcmp(key,"bpt")) {
+            clp->bpt = sg_get_num(buf);
+            if ((clp->bpt < 0) || (clp->bpt > MAX_BPT_VALUE)) {
+                pr2serr("%s: bad argument to 'bpt='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"bs")) {
+            clp->bs = sg_get_num(buf);
+            if ((clp->bs < 0) || (clp->bs > MAX_BPT_VALUE)) {
+                pr2serr("%s: bad argument to 'bs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"count")) {
+            count = sg_get_num(buf);
+            if (count < 0) {
+                pr2serr("%s: bad argument to 'count='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"deb"))
+            clp->debug += sg_get_num(buf);
+        else if (0 == strcmp(key,"ibs")) {
+            ibs = sg_get_num(buf);
+            if ((ibs < 0) || (ibs > MAX_BPT_VALUE)) {
+                pr2serr("%s: bad argument to 'ibs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key,"if") == 0) {
+            memcpy(inf, buf, INOUTF_SZ);
+            inf[INOUTF_SZ - 1] = '\0';
+        } else if (0 == strcmp(key, "iflag")) {
+            if (! process_flags(buf, &clp->iflag)) {
+                pr2serr("%s: bad argument to 'iflag='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key,"mrq") == 0)
+            ;           /* do nothing */
+        else if (0 == strcmp(key,"no_sig")) { /* default changes */
+            clp->no_sig = !!sg_get_num(buf);
+            no_sig_given = true;
+        } else if (0 == strcmp(key,"obs")) {
+            obs = sg_get_num(buf);
+            if ((obs < 0) || (obs > MAX_BPT_VALUE)) {
+                pr2serr("%s: bad argument to 'obs='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (strcmp(key,"of") == 0) {
+            memcpy(outf, buf, INOUTF_SZ);
+            outf[INOUTF_SZ - 1] = '\0';
+        } else if (0 == strcmp(key, "oflag")) {
+            if (! process_flags(buf, &clp->oflag)) {
+                pr2serr("%s: bad argument to 'oflag='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"poll_ms"))
+            clp->poll_ms = sg_get_num(buf);
+        else if (0 == strcmp(key,"rt_sig"))
+            clp->use_rt_sig = !!sg_get_num(buf);
+        else if (0 == strcmp(key,"seek")) {
+            seek = sg_get_num(buf);
+            if (seek < 0) {
+                pr2serr("%s: bad argument to 'seek='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"skip")) {
+            skip = sg_get_num(buf);
+            if (skip < 0) {
+                pr2serr("%s: bad argument to 'skip='\n", my_name);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+        } else if (0 == strcmp(key,"time"))
+            ;           /* do nothing */
+        else if ((0 == strcmp(key,"-V")) || (0 == strcmp(key,"--version"))) {
+            pr2serr("%s: version: %s\n", my_name, version_str);
+            return 0;
+        } else if (0 == strncmp(key,"-vvvvvvv", 8))
+            clp->debug += 7;
+        else if (0 == strncmp(key,"-vvvvvv", 7))
+            clp->debug += 6;
+        else if (0 == strncmp(key,"-vvvvv", 6))
+            clp->debug += 5;
+        else if (0 == strncmp(key,"-vvvv", 5))
+            clp->debug += 4;
+        else if (0 == strncmp(key,"-vvv", 4))
+            clp->debug += 3;
+        else if (0 == strncmp(key,"-vv", 3))
+            clp->debug += 2;
+        else if ((0 == strcmp(key,"--verbose")) || (0 == strncmp(key,"-v", 2)))
+            ++clp->debug;
+        else if (0 == strcmp(key,"-hhhh"))
+            help_pg += 4;
+        else if (0 == strcmp(key,"-hhh"))
+            help_pg += 3;
+        else if (0 == strcmp(key,"-hh"))
+            help_pg += 2;
+        else if ((0 == strcmp(key,"-h")) || (0 == strcmp(key,"--help")))
+            ++help_pg;
+        else {
+            pr2serr("Unrecognized argument '%s'\n", key);
+            usage(help_pg);
+            return 1;
+        }
+    }
+    if (clp->bs <= 0) {
+        clp->bs = DEF_BLOCK_SIZE;
+    } else
+        bs_given = true;
+
+    if (help_pg > 0) {
+        usage(help_pg);
+        return 0;
+    }
+
+    polled_present = (clp->iflag.polled || clp->oflag.polled);
+    if (no_sig_given) {
+        if ((0 == clp->no_sig) && polled_present)
+            pr2serr("Warning: signalling doesn't work with polled flag\n");
+    } else      /* no_sig default varies: 0 normally and 1 if polled present */
+        clp->no_sig = polled_present ? 1 : 0;
+
+    if ((ibs && (ibs != clp->bs)) || (obs && (obs != clp->bs))) {
+        pr2serr("If 'ibs' or 'obs' given must be same as 'bs'\n");
+        usage(1);
+        return 1;
+    }
+    if (clp->bpt <= 0) {
+        clp->bpt = (DEF_BPT_TIMES_BS_SZ / clp->bs);
+        if (0 == clp->bpt)
+            clp->bpt = 1;
+        if (! bs_given)
+            pr2serr("Assume blocks size bs=%d [bytes] and blocks "
+                    "per transfer bpt=%d\n", clp->bs, clp->bpt);
+    } else if (! bs_given)
+        pr2serr("Assume 'bs' (block size) of %d bytes\n", clp->bs);
+
+    if ((skip < 0) || (seek < 0)) {
+        pr2serr("%s: skip and seek cannot be negative\n", my_name);
+        return 1;
+    }
+    if (clp->iflag.mmap && clp->oflag.mmap)
+        clp->both_mmap = true;
+
+    if (clp->debug > 3)
+        pr2serr("%s: if=%s skip=%d of=%s seek=%d count=%d\n", my_name,
+                inf, skip, outf, seek, count);
+    if (! clp->no_sig) {
+        /* Need to block signals before SIGPOLL is enabled in sz_reserve() */
+        sigemptyset(&clp->blocked_sigs);
+        if (clp->use_rt_sig)
+            sigaddset(&clp->blocked_sigs, SIGRTMIN + 1);
+        sigaddset(&clp->blocked_sigs, SIGINT);
+        sigaddset(&clp->blocked_sigs, SIGPOLL);
+        sigprocmask(SIG_BLOCK, &clp->blocked_sigs, 0);
+    }
+
+    clp->infd = STDIN_FILENO;
+    clp->outfd = STDOUT_FILENO;
+    if (inf[0] && ('-' != inf[0])) {
+        open_fl = clp->iflag.excl ? O_EXCL : 0;
+        if ((clp->infd = open(inf, open_fl | O_RDONLY)) < 0) {
+            snprintf(ebuff, EBUFF_SZ, "%s: could not open %s for reading",
+                     my_name, inf);
+            perror(ebuff);
+            return 1;
+        }
+        if (ioctl(clp->infd, SG_GET_TIMEOUT, 0) < 0) {
+            clp->in_is_sg = false;
+            if (skip > 0) {
+                off_t offset = skip;
+
+                offset *= clp->bs;       /* could overflow here! */
+                if (lseek(clp->infd, offset, SEEK_SET) < 0) {
+                    snprintf(ebuff, EBUFF_SZ, "%s: couldn't skip to required "
+                                              "position on %s", my_name, inf);
+                    perror(ebuff);
+                    return 1;
+                }
+            }
+        } else { /* looks like sg device so close then re-open it RW */
+            close(clp->infd);
+            open_fl = clp->iflag.excl ? O_EXCL : 0;
+            open_fl |= (O_RDWR | O_NONBLOCK);
+            if ((clp->infd = open(inf, open_fl)) < 0) {
+                pr2serr("If %s is a sg device, need read+write "
+                        "permissions, even to read it!\n", inf);
+                return 1;
+            }
+            clp->in_is_sg = true;
+            if (sz_reserve(clp, true /* is_in */))
+                return 1;
+            if (sgs_old_sg_driver && (clp->iflag.v4 || clp->oflag.v4)) {
+                pr2serr("Unable to implement v4 flag because sg driver too "
+                        "old\n");
+                return 1;
+            }
+        }
+    }
+    if (outf[0] && ('-' != outf[0])) {
+        open_fl = clp->oflag.excl ? O_EXCL : 0;
+        open_fl |= (O_RDWR | O_NONBLOCK);
+        if ((clp->outfd = open(outf, open_fl)) >= 0) {
+            if (ioctl(clp->outfd, SG_GET_TIMEOUT, 0) < 0) {
+                /* not a scsi generic device so now try and open RDONLY */
+                close(clp->outfd);
+                clp->outfd = -1;
+            }
+            else {
+                clp->out_is_sg = true;
+                if (sz_reserve(clp, false /* hence ! is_in */))
+                    return 1;
+                if (sgs_old_sg_driver && (clp->iflag.v4 || clp->oflag.v4)) {
+                    pr2serr("Unable to implement v4 flag because sg driver "
+                            "too old\n");
+                    return 1;
+                }
+            }
+        }
+        if (! clp->out_is_sg) {
+            if (clp->outfd >= 0) {
+                close(clp->outfd);
+                clp->outfd = -1;
+            }
+            open_fl = clp->oflag.excl ? O_EXCL : 0;
+            open_fl |= (O_WRONLY | O_CREAT);
+            if ((clp->outfd = open(outf, open_fl, 0666)) < 0) {
+                snprintf(ebuff, EBUFF_SZ,
+                         "%s: could not open %s for writing", my_name, outf);
+                perror(ebuff);
+                return 1;
+            }
+            else if (seek > 0) {
+                off_t offset = seek;
+
+                offset *= clp->bs;       /* could overflow here! */
+                if (lseek(clp->outfd, offset, SEEK_SET) < 0) {
+                    snprintf(ebuff, EBUFF_SZ, "%s: couldn't seek to required "
+                             "position on %s", my_name, outf);
+                    perror(ebuff);
+                    return 1;
+                }
+            }
+        }
+    } else if ('\0' == outf[0]) {
+        if (STDIN_FILENO == clp->infd) {
+            pr2serr("Can't have both 'if' as stdin _and_ 'of' as "
+                    "/dev/null\n");
+            return 1;
+        }
+        clp->outfd = open("/dev/null", O_RDWR);
+        if (clp->outfd < 0) {
+            perror("sgs_dd: could not open /dev/null");
+            return 1;
+        }
+        clp->out_is_sg = false;
+        /* ignore any seek */
+    } else {    /* must be '-' for stdout */
+        if (STDIN_FILENO == clp->infd) {
+            pr2serr("Can't have both 'if' as stdin _and_ 'of' as stdout\n");
+            return 1;
+        }
+    }
+    if ((clp->in_is_sg || clp->out_is_sg) && !clp->iflag.given_v3v4 &&
+        !clp->oflag.given_v3v4 && (clp->debug > 0)) {
+        clp->iflag.v3 = true;
+        pr2serr("using sg driver version 3 interface on %s\n",
+                clp->in_is_sg ? inf : outf);
+    }
+
+    if (0 == count)
+        return 0;
+    else if (count < 0) {
+        if (clp->in_is_sg) {
+            res = read_capacity(clp->infd, &in_num_sect, &in_sect_sz);
+            if (2 == res) {
+                pr2serr("Unit attention, media changed(in), try again\n");
+                res = read_capacity(clp->infd, &in_num_sect, &in_sect_sz);
+            }
+            if (0 != res) {
+                pr2serr("Unable to read capacity on %s\n", inf);
+                in_num_sect = -1;
+            } else {
+                if (clp->debug > 4)
+                    pr2serr("ifile: number of sectors=%d, sector size=%d\n",
+                            in_num_sect, in_sect_sz);
+                if (in_num_sect > skip)
+                    in_num_sect -= skip;
+            }
+        }
+        if (clp->out_is_sg) {
+            res = read_capacity(clp->outfd, &out_num_sect, &out_sect_sz);
+            if (2 == res) {
+                pr2serr("Unit attention, media changed(out), try again\n");
+                res = read_capacity(clp->outfd, &out_num_sect, &out_sect_sz);
+            }
+            if (0 != res) {
+                pr2serr("Unable to read capacity on %s\n", outf);
+                out_num_sect = -1;
+            } else {
+                if (clp->debug > 4)
+                    pr2serr("ofile: number of sectors=%d, sector size=%d\n",
+                            out_num_sect, out_sect_sz);
+                if (out_num_sect > seek)
+                    out_num_sect -= seek;
+            }
+        }
+        if (clp->debug > 3)
+            pr2serr("Start of loop, count=%d, in_num_sect=%d, "
+                    "out_num_sect=%d\n", count, in_num_sect, out_num_sect);
+        if (in_num_sect > 0) {
+            if (out_num_sect > 0)
+                count = (in_num_sect > out_num_sect) ? out_num_sect :
+                                                       in_num_sect;
+            else
+                count = in_num_sect;
+        }
+        else
+            count = out_num_sect;
+    }
+    if (clp->debug > 4)
+        pr2serr("Start of loop, count=%d, bpt=%d\n", count, clp->bpt);
+
+    clp->in_count = count;
+    clp->in_done_count = count;
+    clp->in_blk = skip;
+    clp->out_count = count;
+    clp->out_done_count = count;
+    clp->out_blk = seek;
+    res = init_elems(clp);
+    if (res < 0)
+        pr2serr("init_elems() failed, res=%d\n", res);
+    res = 0;
+
+/* vvvvvvvvvvvvvvvvv  Main Loop  vvvvvvvvvvvvvvvvvvvvvvvv */
+    while (clp->out_done_count > 0) {
+        crw = can_read_write(clp);
+        if (crw < 0)
+            break;
+        if (SGQ_CAN_READ & crw) {
+            res = start_read(clp);
+            if (res <= 0) {
+                pr2serr("start_read: res=%d\n", res);
+                break;
+            }
+            res = 0;
+        }
+        if (SGQ_CAN_WRITE & crw) {
+            res = start_write(clp);
+            if (res <= 0) {
+                pr2serr("start_write: res=%d\n", res);
+                break;
+            }
+            res = 0;
+        }
+    }
+
+    if ((STDIN_FILENO != clp->infd) && (clp->infd >= 0))
+        close(clp->infd);
+    if ((STDOUT_FILENO != clp->outfd) && (clp->outfd >= 0))
+        close(clp->outfd);
+    if (0 != clp->out_count) {
+        pr2serr("Some error occurred, remaining blocks=%d\n", clp->out_count);
+        res = 1;
+    }
+    pr2serr("%d+%d records in\n", count - clp->in_done_count,
+            clp->in_partial);
+    pr2serr("%d+%d records out\n", count - clp->out_done_count,
+            clp->out_partial);
+    if (clp->dio_incomplete)
+        pr2serr(">> Direct IO requested but incomplete %d times\n",
+                clp->dio_incomplete);
+    if (clp->sum_of_resids)
+        pr2serr(">> Non-zero sum of residual counts=%d\n",
+                clp->sum_of_resids);
+    if (clp->debug > 0) {
+        if (! clp->no_sig)
+            pr2serr("SIGIO/SIGPOLL signals received: %d, RT sigs: %d\n",
+                    clp->sigs_io_received, clp->sigs_rt_received);
+        if (polled_present)
+            pr2serr("POLLED (blk_poll) used to complete %d commands\n",
+                    clp->blk_poll_count);
+    }
+    if (clp->pollerr_count > 0)
+        pr2serr(">> poll() system call gave POLLERR %d times\n",
+                clp->pollerr_count);
+    remove_elems(clp);
+    return res < 0 ? 99 : res;
+}
diff --git a/testing/tst_sg_lib.c b/testing/tst_sg_lib.c
new file mode 100644
index 0000000..10cf3bb
--- /dev/null
+++ b/testing/tst_sg_lib.c
@@ -0,0 +1,734 @@
+/*
+ * Copyright (c) 2013-2022 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <getopt.h>
+#include <ctype.h>
+#include <errno.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#include <time.h>
+
+#if defined(__GNUC__) && ! defined(SG_LIB_FREEBSD)
+#include <byteswap.h>
+#endif
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"     /* need this to see if HAVE_BYTESWAP_H */
+#endif
+
+#include "sg_lib.h"
+#include "sg_pr2serr.h"
+
+/* Uncomment the next two undefs to force use of the generic (i.e. shifting)
+ * unaligned functions (i.e. sg_get_* and sg_put_*). Use "-b 16|32|64
+ * -n 100m" to see the differences in timing. */
+/* #undef HAVE_CONFIG_H */
+/* #undef HAVE_BYTESWAP_H */
+#include "sg_unaligned.h"
+
+/*
+ * A utility program to test sg_libs string handling, specifically
+ * related to snprintf().
+ */
+
+static const char * version_str = "1.17 20220717";
+
+
+#define MY_NAME "tst_sg_lib"
+
+#define MAX_LINE_LEN 1024
+
+
+static struct option long_options[] = {
+        {"byteswap",  required_argument, 0, 'b'},
+        {"exit", no_argument, 0, 'e'},
+        {"help", no_argument, 0, 'h'},
+        {"hex2",  no_argument, 0, 'H'},
+        {"json", optional_argument, 0, 'j'},
+        {"leadin",  required_argument, 0, 'l'},
+        {"num",  required_argument, 0, 'n'},
+        {"printf", no_argument, 0, 'p'},
+        {"sense", no_argument, 0, 's'},
+        {"unaligned", no_argument, 0, 'u'},
+        {"verbose", no_argument, 0, 'v'},
+        {"version", no_argument, 0, 'V'},
+        {0, 0, 0, 0},   /* sentinel */
+};
+
+static const uint8_t desc_sense_data1[] = {
+   /* unrec_err, excessive_writes, sdat_ovfl, additional_len=? */
+    0x72, 0x1, 0x3, 0x2, 0x80, 0x0, 0x0, 12+12+8+4+8+4+28,
+   /* Information: 0x11223344556677bb */
+    0x0, 0xa, 0x80, 0x0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0xbb,
+   /* command specific: 0x3344556677bbccff */
+    0x1, 0xa, 0x0, 0x0, 0x33, 0x44, 0x55, 0x66, 0x77, 0xbb, 0xcc, 0xff,
+   /* sense key specific: SKSV=1, actual_count=257 (hex: 0x101) */
+    0x2, 0x6, 0x0, 0x0, 0x80, 0x1, 0x1, 0x0,
+   /* field replaceable code=0x45 */
+    0x3, 0x2, 0x0, 0x45,
+   /* another progress report indicator */
+    0xa, 0x6, 0x2, 0x1, 0x2, 0x0, 0x32, 0x01,
+   /* incorrect length indicator (ILI) */
+    0x5, 0x2, 0x0, 0x20,
+   /* user data segment referral */
+    0xb, 26, 0x1, 0x0,
+        0,0,0,1, 0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,
+                 0x1,0x2,0x3,0x4,0x55,0x6,0x7,0x8,
+        2,0,0x12,0x34,
+    };
+
+static const uint8_t desc_sense_data2[] = {
+   /* ill_req, inv fld in para list, additional_len=? */
+    0x72, 0x5, 0x26, 0x0, 0x0, 0x0, 0x0, 8+4,
+   /* sense key specific: SKSV=1, C/D*=0, bitp=7 bytep=34 */
+    0x2, 0x6, 0x0, 0x0, 0x8f, 0x0, 0x34, 0x0,
+   /* field replaceable code=0x45 */
+    0x3, 0x2, 0x0, 0x45,
+    };
+
+static const uint8_t desc_sense_data3[] = {
+   /* medium err, vibration induced ..., additional_len=? */
+    0x72, 0x3, 0x9, 0x5, 0x0, 0x0, 0x0, 32+16,
+   /* 0xd: block dev: sense key specific: SKSV=1, retry_count=257, fru=0x45
+    * info=0x1122334455, command_specific=0x1   */
+    0xd, 0x1e, 0xa0, 0x0, 0x80, 0x1, 0x1, 0x45,
+    0x0, 0x0, 0x0, 0x11, 0x22, 0x33, 0x44, 0x55,
+    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
+    /* following sbc3 (standard) and sbc4r10 inconsistency; add padding */
+    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+    /* 0xe: reason: send_to_given+henceforth, lu, naa-5, 0x5333333000001f40 */
+    0xe, 0xe, 0x0, 0x1, 0x1, 0x3, 0x0, 0x8,
+        0x53, 0x33, 0x33, 0x30, 0x0, 0x0, 0x1f, 0x40,
+    };
+
+static const uint8_t desc_sense_data4[] = {
+   /* ill_req, inv fld in para list, additional_len=? */
+    0x72, 0x5, 0x26, 0x0, 0x0, 0x0, 0x0, 24,
+   /* Forwarded sense data, FSDT=0, sd_src=7, f_status=2 */
+    0xc, 22, 0x7, 0x2,
+   /* ill_req, inv fld in para list, additional_len=? */
+    0x72, 0x5, 0x26, 0x0, 0x0, 0x0, 0x0, 8+4,
+   /* sense key specific: SKSV=1, C/D*=0, bitp=7 bytep=34 */
+    0x2, 0x6, 0x0, 0x0, 0x8f, 0x0, 0x34, 0x0,
+   /* field replaceable code=0x45 */
+    0x3, 0x2, 0x0, 0x45,
+    };
+
+static const uint8_t desc_sense_data5[] = {
+   /* no_sense, ATA info available */
+    0x72, 0x0, 0x0, 0x1d, 0x0, 0x0, 0x0, 14+14,
+   /* ATA descriptor extend=1 */
+    0x9, 0xc, 0x1, 0x0, 0x34, 0x12, 0x44, 0x11,
+    0x55, 0x22, 0x66, 0x33, 0x1, 0x0,
+   /* ATA descriptor extend=0 */
+    0x9, 0xc, 0x0, 0x0, 0x34, 0x12, 0x44, 0x11,
+    0x55, 0x22, 0x66, 0x33, 0x1, 0x0,
+    };
+
+static const uint8_t desc_sense_data6[] = {
+   /* UA, req, subsidiary binding */
+    0x72, 0x6, 0x3f, 0x1a, 0x0, 0x0, 0x0, 26+12+12,
+    /* 0xe: designator, reason: preferred admin lu, uuid */
+    0xe, 0x18, 0x0, 0x4, 0x1, 0xa, 0x0, 0x12,
+        0x10, 0x0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
+        0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
+        0xfe, 0xdc,
+    /* 0x0: Information(valid): lun */
+    0x0, 0xa, 0x80, 0x0,
+    0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+    /* 0x1: Command specific: 0x1 */
+    0x1, 0xa, 0x0, 0x0,
+    0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
+    };
+
+static const char * leadin = NULL;
+
+
+static void
+usage()
+{
+    fprintf(stderr,
+            "Usage: tst_sg_lib [--exit] [--help] [--hex2] [--leadin=STR] "
+            "[--printf]\n"
+            "                  [--sense] [--unaligned] [--verbose] "
+            "[--version]\n"
+            "  where:\n"
+#if defined(__GNUC__) && ! defined(SG_LIB_FREEBSD)
+            "    --byteswap=B|-b B    B is 16, 32 or 64; tests NUM "
+            "byteswaps\n"
+            "                         compared to sg_unaligned "
+            "equivalent\n"
+            "    --exit|-e          test exit status strings\n"
+#else
+            "    --exit|-e          test exit status strings\n"
+#endif
+            "    --help|-h          print out usage message\n"
+            "    --hex2|-H          test hex2* variants\n"
+            "    --leadin=STR|-l STR    every line output by --sense "
+            "should\n"
+            "                           be prefixed by STR\n"
+            "    --num=NUM|-n NUM    number of iterations (def=1)\n"
+            "    --printf|-p        test library printf variants\n"
+            "    --sense|-s         test sense data handling\n"
+            "    --unaligned|-u     test unaligned data handling\n"
+            "    --verbose|-v       increase verbosity\n"
+            "    --version|-V       print version string and exit\n\n"
+            "Test various parts of sg_lib, see options. Sense data tests "
+            "overlap\nsomewhat with examples/sg_sense_test .\n"
+           );
+
+}
+
+static char *
+get_exit_status_str(int exit_status, bool longer, int b_len, char * b)
+{
+    int n;
+
+    n = sg_scnpr(b, b_len, "  ES=%d: ", exit_status);
+    if (n >= (b_len - 1))
+        return b;
+    if (sg_exit2str(exit_status, longer, b_len - n, b + n)) {
+        n = (int)strlen(b);
+        if (n < (b_len - 1))
+            sg_scnpr(b + n, b_len - n, " [ok=true]");
+        return b;
+    } else
+        snprintf(b, b_len, "  No ES string for %d%s", exit_status,
+                 (longer ? " [ok=false]" : ""));
+    return b;
+}
+
+static uint8_t arr[64];
+
+#define OFF 7   /* in byteswap mode, can test different alignments (def: 8) */
+
+int
+main(int argc, char * argv[])
+{
+    bool as_json = false;
+    bool do_exit_status = false;
+    bool ok;
+    int k, c, n, len;
+    int byteswap_sz = 0;
+    int do_hex2 = 0;
+    int do_num = 1;
+    int do_printf = 0;
+    int do_sense = 0;
+    int do_unaligned = 0;
+    int did_something = 0;
+    int vb = 0;
+    int ret = 0;
+    sgj_opaque_p jop = NULL;
+    sgj_opaque_p jo2p;
+    sgj_state json_st SG_C_CPP_ZERO_INIT;
+    sgj_state * jsp = &json_st;
+    char b[2048];
+    char bb[256];
+    const int b_len = sizeof(b);
+
+    while (1) {
+        int option_index = 0;
+
+        c = getopt_long(argc, argv, "b:ehHj::l:n:psuvV", long_options,
+                        &option_index);
+        if (c == -1)
+            break;
+
+        switch (c) {
+        case 'b':
+            byteswap_sz = sg_get_num(optarg);
+            if (! ((16 == byteswap_sz) || (32 == byteswap_sz) ||
+                   (64 == byteswap_sz))) {
+                fprintf(stderr, "--byteswap= requires 16, 32 or 64\n");
+                return 1;
+            }
+            break;
+        case 'e':
+            do_exit_status = true;
+            break;
+        case 'h':
+        case '?':
+            usage();
+            return 0;
+        case 'H':
+            ++do_hex2;
+            break;
+        case 'j':
+            if (! sgj_init_state(&json_st, optarg)) {
+                pr2serr("bad argument to --json= option, unrecognized "
+                        "character '%c'\n", json_st.first_bad_char);
+                return SG_LIB_SYNTAX_ERROR;
+            }
+            break;
+
+        case 'l':
+            leadin = optarg;
+            break;
+        case 'n':
+            do_num = sg_get_num(optarg);
+            if (do_num < 0) {
+                fprintf(stderr, "--num= unable decode argument as number\n");
+                return 1;
+            }
+            break;
+        case 'p':
+            ++do_printf;
+            break;
+        case 's':
+            ++do_sense;
+            break;
+        case 'u':
+            ++do_unaligned;
+            break;
+        case 'v':
+            ++vb;
+            break;
+        case 'V':
+            fprintf(stderr, "version: %s\n", version_str);
+            return 0;
+        default:
+            fprintf(stderr, "unrecognised switch code 0x%x ??\n", c);
+            usage();
+            return 1;
+        }
+    }
+    if (optind < argc) {
+        if (optind < argc) {
+            for (; optind < argc; ++optind)
+                fprintf(stderr, "Unexpected extra argument: %s\n",
+                        argv[optind]);
+            usage();
+            return 1;
+        }
+    }
+
+    as_json = json_st.pr_as_json;
+    if (as_json)
+        jop = sgj_start_r(MY_NAME, version_str, argc, argv, jsp);
+
+    if (do_exit_status) {
+        ++did_something;
+
+        printf("Test Exit Status strings (add -v for long version):\n");
+        printf("  No error (es=0): %s\n",
+               sg_get_category_sense_str(0, b_len, b, vb));
+        ok = sg_exit2str(0, true, b_len, b);
+        printf("  No error (force verbose): %s\n", b);
+        if (vb)
+            printf("    for previous line sg_exit2str() returned: %s\n",
+                   (ok ? "true" : "false"));
+        printf("%s\n", get_exit_status_str(1, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(2, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(3, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(4, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(5, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(6, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(7, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(8, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(25, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(33, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(36, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(48, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(50, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(51, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(96, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(97, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(97, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(255, (vb > 0), b_len, b));
+        printf("%s\n", get_exit_status_str(-1, (vb > 0), b_len, b));
+
+        printf("\n");
+    }
+
+    if (do_sense ) {
+        ++did_something;
+        if (as_json) {
+            jo2p = sgj_named_subobject_r(jsp, jop, "desc_sense_data__test1");
+            sgj_js_sense(jsp, jo2p,  desc_sense_data1,
+                         (int)sizeof(desc_sense_data1));
+        } else {
+            printf("desc_sense_data test1:\n");
+            sg_print_sense(leadin, desc_sense_data1,
+                           (int)sizeof(desc_sense_data1), vb);
+            printf("\n");
+        }
+#if 1
+        if (as_json) {
+            sgj_js_str_out(jsp, "sg_get_sense_str(ds_data1)", 999);
+            sg_get_sense_str(leadin, desc_sense_data1,
+                             sizeof(desc_sense_data1), vb, b_len, b);
+            sgj_js_str_out(jsp, b, strlen(b));
+
+        } else {
+            printf("sg_get_sense_str(ds_data1):\n");
+            sg_get_sense_str(leadin, desc_sense_data1,
+                             sizeof(desc_sense_data1), vb, b_len, b);
+            printf("sg_get_sense_str: strlen(b)->%u\n", (uint32_t)strlen(b));
+            printf("%s", b);
+            printf("\n");
+        }
+#endif
+        if (as_json) {
+            jo2p = sgj_named_subobject_r(jsp, jop, "desc_sense_data__test2");
+            sgj_js_sense(jsp, jo2p,  desc_sense_data2,
+                         (int)sizeof(desc_sense_data2));
+        } else {
+            printf("desc_sense_data test2\n");
+            sg_print_sense(leadin, desc_sense_data2,
+                           (int)sizeof(desc_sense_data2), vb);
+            printf("\n");
+        }
+        if (as_json) {
+            jo2p = sgj_named_subobject_r(jsp, jop,
+                                         "desc_sense_block_combo_test3");
+            sgj_js_sense(jsp, jo2p,  desc_sense_data3,
+                            (int)sizeof(desc_sense_data3));
+        } else {
+            printf("desc_sense block dev combo plus designator test3\n");
+            sg_print_sense(leadin, desc_sense_data3,
+                           (int)sizeof(desc_sense_data3), vb);
+            printf("\n");
+        }
+        if (as_json) {
+            jo2p = sgj_named_subobject_r(jsp, jop,
+                                         "desc_sense_forwarded_sense_test4");
+            sgj_js_sense(jsp, jo2p,  desc_sense_data4,
+                         (int)sizeof(desc_sense_data4));
+        } else {
+            printf("desc_sense forwarded sense test4\n");
+            sg_print_sense(leadin, desc_sense_data4,
+                           (int)sizeof(desc_sense_data4), vb);
+            printf("\n");
+        }
+        if (as_json) {
+            jo2p = sgj_named_subobject_r(jsp, jop,
+                                         "desc_sense_ata_info_test5");
+            sgj_js_sense(jsp, jo2p,  desc_sense_data5,
+                         (int)sizeof(desc_sense_data5));
+        } else {
+            printf("desc_sense ATA Info test5\n");
+            sg_print_sense(leadin, desc_sense_data5,
+                           (int)sizeof(desc_sense_data5), vb);
+            printf("\n");
+        }
+        if (as_json) {
+            jo2p = sgj_named_subobject_r(jsp, jop,
+                                         "desc_sense_ua_binding_test6");
+            sgj_js_sense(jsp, jo2p,  desc_sense_data6,
+                         (int)sizeof(desc_sense_data6));
+        } else {
+            printf("desc_sense UA subsidiary binding changed test6\n");
+            sg_print_sense(leadin, desc_sense_data6,
+                           (int)sizeof(desc_sense_data6), vb);
+            printf("\n");
+            printf("\n");
+        }
+    }
+
+    if (do_printf) {
+        ++did_something;
+        printf("Testing sg_scnpr():\n");
+        b[0] = '\0';
+        len = b_len;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = -1;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = 0;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = 1;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = 2;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = 3;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = 4;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = 5;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = 6;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+
+        b[0] = '\0';
+        len = 7;
+        n = sg_scnpr(b, len, "%s", "test");
+        printf("sg_scnpr(,%d,,\"test\") -> %d; strlen(b) -> %u\n",
+               len, n, (uint32_t)strlen(b));
+        if (strlen(b) > 0)
+            printf("Resulting string: %s\n", b);
+    }
+    if (do_hex2) {
+        uint8_t b[] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
+                       0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50,
+                       0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58};
+
+        ++did_something;
+        for (k = 0; k < 19; ++k) {
+            printf("k=%d:\n", k);
+            hex2stdout(b, k, 0);
+            hex2str(b, k, "h2str0: ", 0, sizeof(bb), bb);
+            printf("%s", bb);
+            hex2stdout(b, k, 1);
+            hex2str(b, k, "h2str1: ", 1, sizeof(bb), bb);
+            printf("%s", bb);
+            hex2str(b, k, "h2str2: ", 2, sizeof(bb), bb);
+            printf("%s\n", bb);
+            hex2stdout(b, k, -1);
+            printf("\n");
+        }
+    }
+    if (do_unaligned) {
+        uint16_t u16 = 0x55aa;
+        uint16_t u16r;
+        uint32_t u24 = 0x224488;
+        uint32_t u24r;
+        uint32_t u32 = 0x224488aa;
+        uint32_t u32r;
+        uint64_t u48 = 0x112233445566ULL;
+        uint64_t u48r;
+        uint64_t u64 = 0x1122334455667788ULL;
+        uint64_t u64r;
+        uint8_t u8[64];
+
+        ++did_something;
+        if (vb)
+            memset(u8, 0, sizeof(u8));
+        printf("u16=0x%" PRIx16 "\n", u16);
+        sg_put_unaligned_le16(u16, u8);
+        printf("  le16:\n");
+        hex2stdout(u8, vb ? 10 : 2, -1);
+        u16r = sg_get_unaligned_le16(u8);
+        printf("  u16r=0x%" PRIx16 "\n", u16r);
+        sg_put_unaligned_be16(u16, u8);
+        printf("  be16:\n");
+        hex2stdout(u8, vb ? 10 : 2, -1);
+        u16r = sg_get_unaligned_be16(u8);
+        printf("  u16r=0x%" PRIx16 "\n\n", u16r);
+
+        printf("u24=0x%" PRIx32 "\n", u24);
+        sg_put_unaligned_le24(u24, u8);
+        printf("  le24:\n");
+        hex2stdout(u8, vb ? 10 : 3, -1);
+        u24r = sg_get_unaligned_le24(u8);
+        printf("  u24r=0x%" PRIx32 "\n", u24r);
+        sg_put_unaligned_be24(u24, u8);
+        printf("  be24:\n");
+        hex2stdout(u8, vb ? 10 : 3, -1);
+        u24r = sg_get_unaligned_be24(u8);
+        printf("  u24r=0x%" PRIx32 "\n\n", u24r);
+
+        printf("u32=0x%" PRIx32 "\n", u32);
+        sg_put_unaligned_le32(u32, u8);
+        printf("  le32:\n");
+        hex2stdout(u8, vb ? 10 : 4, -1);
+        u32r = sg_get_unaligned_le32(u8);
+        printf("  u32r=0x%" PRIx32 "\n", u32r);
+        sg_put_unaligned_be32(u32, u8);
+        printf("  be32:\n");
+        hex2stdout(u8, vb ? 10 : 4, -1);
+        u32r = sg_get_unaligned_be32(u8);
+        printf("  u32r=0x%" PRIx32 "\n\n", u32r);
+
+        printf("u48=0x%" PRIx64 "\n", u48);
+        sg_put_unaligned_le48(u48, u8);
+        printf("  le48:\n");
+        hex2stdout(u8, vb ? 10 : 6, -1);
+        u48r = sg_get_unaligned_le48(u8);
+        printf("  u48r=0x%" PRIx64 "\n", u48r);
+        sg_put_unaligned_be48(u48, u8);
+        printf("  be48:\n");
+        hex2stdout(u8, vb ? 10 : 6, -1);
+        u48r = sg_get_unaligned_be48(u8);
+        printf("  u48r=0x%" PRIx64 "\n\n", u48r);
+
+        printf("u64=0x%" PRIx64 "\n", u64);
+        sg_put_unaligned_le64(u64, u8);
+        printf("  le64:\n");
+        hex2stdout(u8, vb ? 10 : 8, -1);
+        u64r = sg_get_unaligned_le64(u8);
+        printf("  u64r=0x%" PRIx64 "\n", u64r);
+        sg_put_unaligned_be64(u64, u8);
+        printf("  be64:\n");
+        hex2stdout(u8, vb ? 10 : 8, -1);
+        u64r = sg_get_unaligned_be64(u8);
+        printf("  u64r=0x%" PRIx64 "\n\n", u64r);
+
+        printf("  be[v=8 bytes]:\n");
+        hex2stdout(u8, vb ? 10 : 8, -1);
+        u64r = sg_get_unaligned_be(8, u8);
+        printf("  u64r[v=8 bytes]=0x%" PRIx64 "\n", u64r);
+        printf("  le[v=8 bytes]:\n");
+        hex2stdout(u8, vb ? 10 : 8, -1);
+        u64r = sg_get_unaligned_le(8, u8);
+        printf("  u64r[v=8 bytes]=0x%" PRIx64 "\n\n", u64r);
+    }
+
+#if defined(__GNUC__) && ! defined(SG_LIB_FREEBSD)
+    if (byteswap_sz > 0) {
+        uint32_t elapsed_msecs;
+        uint16_t count16 = 0;
+        uint32_t count32 = 0;
+        uint64_t count64 = 0;
+        struct timespec start_tm, end_tm;
+
+        ++did_something;
+        if (0 != clock_gettime(CLOCK_MONOTONIC, &start_tm)) {
+            perror("clock_gettime(CLOCK_MONOTONIC)\n");
+            return 1;
+        }
+        for (k = 0; k < do_num; ++k) {
+            switch (byteswap_sz) {
+            case 16:
+                sg_put_unaligned_be16(count16 + 1, arr + OFF);
+                count16 = sg_get_unaligned_be16(arr + OFF);
+                break;
+            case 32:
+                sg_put_unaligned_be32(count32 + 1, arr + OFF);
+                count32 = sg_get_unaligned_be32(arr + OFF);
+                break;
+            case 64:
+                sg_put_unaligned_be64(count64 + 1, arr + OFF);
+                count64 = sg_get_unaligned_be64(arr + OFF);
+                break;
+            default:
+                break;
+            }
+        }
+        if (0 != clock_gettime(CLOCK_MONOTONIC, &end_tm)) {
+            perror("clock_gettime(CLOCK_MONOTONIC)\n");
+            return 1;
+        }
+        elapsed_msecs = (end_tm.tv_sec - start_tm.tv_sec) * 1000;
+        elapsed_msecs += (end_tm.tv_nsec - start_tm.tv_nsec) / 1000000;
+        if (16 == byteswap_sz)
+            printf("  last k=%d, last count16=%u\n", k, count16);
+        else if (32 == byteswap_sz)
+            printf("  last k=%d, last count32=%u\n", k, count32);
+        else
+            printf("  last k=%d, last count64=%" PRIu64 "\n", k, count64);
+        printf("Unaligned elapsed milliseconds: %u\n", elapsed_msecs);
+        count16 = 0;
+        count32 = 0;
+        count64 = 0;
+
+        if (0 != clock_gettime(CLOCK_MONOTONIC, &start_tm)) {
+            perror("clock_gettime(CLOCK_MONOTONIC)\n");
+            return 1;
+        }
+        for (k = 0; k < do_num; ++k) {
+            switch (byteswap_sz) {
+            case 16:
+                count16 = bswap_16(count16 + 1);
+                memcpy(arr + OFF, &count16, 2);
+                memcpy(&count16, arr + OFF, 2);
+                count16 = bswap_16(count16);
+                break;
+            case 32:
+                count32 = bswap_32(count32 + 1);
+                memcpy(arr + OFF, &count32, 4);
+                memcpy(&count32, arr + OFF, 4);
+                count32 = bswap_32(count32);
+                break;
+            case 64:
+                count64 = bswap_64(count64 + 1);
+                memcpy(arr + OFF, &count64, 8);
+                memcpy(&count64, arr + OFF, 8);
+                count64 = bswap_64(count64);
+                break;
+            default:
+                break;
+            }
+        }
+        if (0 != clock_gettime(CLOCK_MONOTONIC, &end_tm)) {
+            perror("clock_gettime(CLOCK_MONOTONIC)\n");
+            return 1;
+        }
+        elapsed_msecs = (end_tm.tv_sec - start_tm.tv_sec) * 1000;
+        elapsed_msecs += (end_tm.tv_nsec - start_tm.tv_nsec) / 1000000;
+        if (16 == byteswap_sz)
+            printf("  last k=%d, last count16=%u\n", k, count16);
+        else if (32 == byteswap_sz)
+            printf("  last k=%d, last count32=%u\n", k, count32);
+        else
+            printf("  last k=%d, last count64=%" PRIu64 "\n", k, count64);
+        printf("Byteswap/memcpy elapsed milliseconds: %u\n", elapsed_msecs);
+        count16 = 0;
+        count32 = 0;
+        count64 = 0;
+    }
+#endif
+
+    if (0 == did_something)
+        printf("Looks like no tests done, check usage with '-h'\n");
+    ret = (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
+    if (as_json) {
+        if (0 == do_hex2)
+            sgj_js2file(jsp, NULL, ret, stdout);
+        sgj_finish(jsp);
+    }
+    return ret;
+}
diff --git a/testing/uapi_sg.h b/testing/uapi_sg.h
new file mode 100644
index 0000000..41377c6
--- /dev/null
+++ b/testing/uapi_sg.h
@@ -0,0 +1,493 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _UAPI_SCSI_SG_H
+#define _UAPI_SCSI_SG_H
+
+/*
+ * History:
+ *  Started: Aug 9 by Lawrence Foard (entropy@world.std.com), to allow user
+ *  process control of SCSI devices.
+ *  Development Sponsored by Killy Corp. NY NY
+ *
+ * Original driver (sg.h):
+ *   Copyright (C) 1992 Lawrence Foard
+ *
+ * Later extensions (versions 2, 3 and 4) to driver:
+ *   Copyright (C) 1998 - 2021 Douglas Gilbert
+ *
+ * Version 4.0.47 (20210605)
+ *  This version is for Linux 4 and 5 series kernels.
+ *
+ * Documentation
+ * =============
+ * A web site for the SG device driver can be found at:
+ *   https://sg.danny.cz/sg  [alternatively check the MAINTAINERS file]
+ * The documentation for the sg version 3 driver can be found at:
+ *   https://sg.danny.cz/sg/p/sg_v3_ho.html
+ * Also see: <kernel_source>/Documentation/scsi/scsi-generic.txt
+ *
+ * For utility and test programs see: https://sg.danny.cz/sg/sg3_utils.html
+ */
+
+#include <stddef.h>
+#include <linux/types.h>
+#include <linux/major.h>
+
+/*
+ * bsg.h contains the sg v4 user space interface structure (sg_io_v4).
+ * That structure is also used as the controlling object when multiple
+ * requests are issued with one ioctl() call.
+ */
+#include <linux/bsg.h>
+
+/*
+ * Same structure as used by readv() call. It defines one scatter-gather
+ * element. "Scatter-gather" is abbreviated to "sgat" in this driver to
+ * avoid confusion with this driver's name.
+ */
+typedef struct sg_iovec	{
+	void __user *iov_base;	/* Starting address (of a byte) */
+	size_t iov_len;		/* Length in bytes */
+} sg_iovec_t;
+
+
+typedef struct sg_io_hdr {
+	int interface_id;	/* [i] 'S' for SCSI generic (required) */
+	int dxfer_direction;	/* [i] data transfer direction  */
+	unsigned char cmd_len;	/* [i] SCSI command length */
+	unsigned char mx_sb_len;/* [i] max length to write to sbp */
+	unsigned short iovec_count;	/* [i] 0 implies no sgat list */
+	unsigned int dxfer_len;	/* [i] byte count of data transfer */
+	/* dxferp points to data transfer memory or scatter gather list */
+	void __user *dxferp;	/* [i], [*io] */
+	unsigned char __user *cmdp;/* [i], [*i] points to command to perform */
+	void __user *sbp;	/* [i], [*o] points to sense_buffer memory */
+	unsigned int timeout;	/* [i] MAX_UINT->no timeout (unit: millisec) */
+	unsigned int flags;	/* [i] 0 -> default, see SG_FLAG... */
+	int pack_id;		/* [i->o] unused internally (normally) */
+	void __user *usr_ptr;	/* [i->o] unused internally */
+	unsigned char status;	/* [o] scsi status */
+	unsigned char masked_status;/* [o] shifted, masked scsi status */
+	unsigned char msg_status;/* [o] messaging level data (optional) */
+	unsigned char sb_len_wr; /* [o] byte count actually written to sbp */
+	unsigned short host_status; /* [o] errors from host adapter */
+	unsigned short driver_status;/* [o] errors from software driver */
+	int resid;		/* [o] dxfer_len - actual_transferred */
+	/* unit may be nanoseconds after SG_SET_GET_EXTENDED ioctl use */
+	unsigned int duration;	/* [o] time taken by cmd (unit: millisec) */
+	unsigned int info;	/* [o] auxiliary information */
+} sg_io_hdr_t;
+
+#define SG_INTERFACE_ID_ORIG 'S'
+
+/* Use negative values to flag difference from original sg_header structure */
+#define SG_DXFER_NONE (-1)	/* e.g. a SCSI Test Unit Ready command */
+#define SG_DXFER_TO_DEV (-2)	/* data-out buffer e.g. SCSI WRITE command */
+#define SG_DXFER_FROM_DEV (-3)	/* data-in buffer e.g. SCSI READ command */
+/*
+ * SG_DXFER_TO_FROM_DEV is treated like SG_DXFER_FROM_DEV with the additional
+ * property than during indirect IO the user buffer is copied into the kernel
+ * buffers _before_ the transfer from the device takes place. Useful if short
+ * DMA transfers (less than requested) are not reported (e.g. resid always 0).
+ */
+#define SG_DXFER_TO_FROM_DEV (-4)
+#define SG_DXFER_UNKNOWN (-5)	/* Unknown data direction, do not use */
+
+/* following flag values can be OR-ed together in v3::flags or v4::flags */
+#define SG_FLAG_DIRECT_IO 1	/* default is indirect IO */
+/* SG_FLAG_UNUSED_LUN_INHIBIT is ignored in sg v4 driver */
+#define SG_FLAG_UNUSED_LUN_INHIBIT 2  /* ignored, was LUN overwrite in cdb */
+#define SG_FLAG_MMAP_IO 4	/* request memory mapped IO */
+/* no transfers between kernel<-->user space; keep device<-->kernel xfers */
+#define SG_FLAG_NO_DXFER 0x10000 /* See comment on previous line! */
+/* defaults: for sg driver (v3_v4): Q_AT_HEAD; for block layer: Q_AT_TAIL */
+#define SG_FLAG_Q_AT_TAIL 0x10
+#define SG_FLAG_Q_AT_HEAD 0x20
+
+/*
+ * Flags used by ioctl(SG_IOSUBMIT) [abbrev: SG_IOS] and ioctl(SG_IORECEIVE)
+ * [abbrev: SG_IOR] OR-ed into sg_io_v4::flags. The sync v4 interface uses
+ * ioctl(SG_IO) and can take these new flags, as can the v3 interface.
+ * These flags apply for SG_IOS unless otherwise noted. May be OR-ed together.
+ */
+#define SGV4_FLAG_DIRECT_IO SG_FLAG_DIRECT_IO
+#define SGV4_FLAG_MMAP_IO SG_FLAG_MMAP_IO
+#define SGV4_FLAG_YIELD_TAG 0x8  /* sg_io_v4::generated_tag set after SG_IOS */
+#define SGV4_FLAG_Q_AT_TAIL SG_FLAG_Q_AT_TAIL
+#define SGV4_FLAG_Q_AT_HEAD SG_FLAG_Q_AT_HEAD
+#define SGV4_FLAG_DOUT_OFFSET  0x40	/* dout byte offset in v4::spare_in */
+#define SGV4_FLAG_EVENTFD 0x80		/* signal completion on ... */
+#define SGV4_FLAG_COMPLETE_B4  0x100	/* mrq: complete this rq before next */
+#define SGV4_FLAG_SIGNAL 0x200	/* v3: ignored; v4 signal on completion */
+#define SGV4_FLAG_IMMED 0x400   /* issue request and return immediately ... */
+#define SGV4_FLAG_HIPRI 0x800 /* use blk_poll (deprecated name, use POLLED) */
+#define SGV4_FLAG_POLLED 0x800 /* request will use blk_poll to complete */
+#define SGV4_FLAG_STOP_IF 0x1000	/* Stops sync mrq if error or warning */
+#define SGV4_FLAG_DEV_SCOPE 0x2000 /* permit SG_IOABORT to have wider scope */
+#define SGV4_FLAG_SHARE 0x4000	/* share IO buffer; needs SG_SEIM_SHARE_FD */
+#define SGV4_FLAG_DO_ON_OTHER 0x8000 /* available on either of shared pair */
+#define SGV4_FLAG_NO_DXFER SG_FLAG_NO_DXFER /* but keep dev<-->kernel xfr */
+#define SGV4_FLAG_KEEP_SHARE 0x20000  /* ... buffer for another dout command */
+#define SGV4_FLAG_MULTIPLE_REQS 0x40000	/* 1 or more sg_io_v4-s in data-in */
+#define SGV4_FLAG_ORDERED_WR 0x80000	/* svb: issue in-order writes */
+#define SGV4_FLAG_REC_ORDER 0x100000 /* receive order in v4:request_priority */
+#define SGV4_FLAG_META_OUT_IF 0x200000  /* ... there is something to report */
+
+/* Output (potentially OR-ed together) in v3::info or v4::info field */
+#define SG_INFO_OK_MASK 0x1
+#define SG_INFO_OK 0x0		/* no sense, host nor driver "noise" */
+#define SG_INFO_CHECK 0x1	/* something abnormal happened */
+
+#define SG_INFO_DIRECT_IO_MASK 0x6
+#define SG_INFO_INDIRECT_IO 0x0	/* data xfer via kernel buffers (or no xfer) */
+#define SG_INFO_DIRECT_IO 0x2	/* direct IO requested and performed */
+#define SG_INFO_MIXED_IO 0x4	/* not used, always 0 */
+#define SG_INFO_DEVICE_DETACHING 0x8	/* completed successfully but ... */
+#define SG_INFO_ABORTED 0x10	/* this command has been aborted */
+#define SG_INFO_MRQ_FINI 0x20	/* marks multi-reqs that have finished */
+
+/*
+ * Pointer to object of this structure filled by ioctl(SG_GET_SCSI_ID). Last
+ * field changed in v4 driver, was 'int unused[2]' so remains the same size.
+ */
+typedef struct sg_scsi_id {
+	int host_no;	/* as in "scsi<n>" where 'n' is one of 0, 1, 2 etc */
+	int channel;
+	int scsi_id;	/* scsi id of target device */
+	int lun;	/* lower 32 bits of internal 64 bit integer */
+	int scsi_type;	/* TYPE_... defined in scsi/scsi.h */
+	short h_cmd_per_lun;/* host (adapter) maximum commands per lun */
+	short d_queue_depth;/* device (or adapter) maximum queue length */
+	union {
+		int unused[2];	/* as per version 3 driver */
+		__u8 scsi_lun[8];  /* full 8 byte SCSI LUN [in v4 driver] */
+	};
+} sg_scsi_id_t;
+
+/* For backward compatibility v4 driver yields at most SG_MAX_QUEUE of these */
+typedef struct sg_req_info {	/* used by SG_GET_REQUEST_TABLE ioctl() */
+	char req_state;	/* See 'enum sg_rq_state' definition in v4 driver */
+	char orphan;	/* 0 -> normal request, 1 -> from interrupted SG_IO */
+	/* sg_io_owned set imples synchronous, clear implies asynchronous */
+	char sg_io_owned;/* 0 -> complete with read(), 1 -> owned by SG_IO */
+	char problem;	/* 0 -> no problem detected, 1 -> error to report */
+	/* If SG_CTL_FLAGM_TAG_FOR_PACK_ID set on fd then next field is tag */
+	int pack_id;	/* pack_id, in v4 driver may be tag instead */
+	void __user *usr_ptr;	/* user provided pointer in v3+v4 interface */
+	/*
+	 * millisecs elapsed since the command started (req_state==1) or
+	 * command duration (req_state==2). Will be in nanoseconds after
+	 * the SG_SET_GET_EXTENDED{TIME_IN_NS} ioctl.
+	 */
+	unsigned int duration;
+	int unused;
+} sg_req_info_t;
+
+/*
+ * The following defines are for manipulating struct sg_extended_info which
+ * is abbreviated to "SEI". A following "M" (i.e. "_SEIM_") indicates a
+ * mask. Most mask values correspond to a integer (usually a uint32_t) apart
+ * from SG_SEIM_CTL_FLAGS which is for boolean values packed into an integer.
+ * The mask values for those booleans start with "SG_CTL_FLAGM_". The scope
+ * of these settings, like most other ioctls, is usually that of the file
+ * descriptor the ioctl is executed on. The "rd:" indication means read-only,
+ * attempts to write to them are ignored. "rd>" means action when reading.
+ */
+#define SG_SEIM_CTL_FLAGS	0x1	/* ctl_flags_mask bits in ctl_flags */
+#define SG_SEIM_READ_VAL	0x2	/* write SG_SEIRV_*, read back value */
+#define SG_SEIM_RESERVED_SIZE	0x4	/* reserved_sz of reserve request */
+#define SG_SEIM_TOT_FD_THRESH	0x8	/* tot_fd_thresh of data buffers */
+#define SG_SEIM_MINOR_INDEX	0x10	/* sg device minor index number */
+#define SG_SEIM_SHARE_FD	0x20	/* write-side gives fd of read-side */
+#define SG_SEIM_CHG_SHARE_FD	0x40	/* read-side given new write-side fd */
+#define SG_SEIM_SGAT_ELEM_SZ	0x80	/* sgat element size (>= PAGE_SIZE) */
+#define SG_SEIM_BLK_POLL	0x100	/* call blk_poll, uses 'num' field */
+#define SG_SEIM_EVENTFD		0x200	/* pass eventfd to driver */
+#define SG_SEIM_ALL_BITS	0x3ff	/* should be OR of previous items */
+
+/* flag and mask values for boolean fields follow */
+#define SG_CTL_FLAGM_TIME_IN_NS	0x1	/* time: nanosecs (def: millisecs) */
+#define SG_CTL_FLAGM_TAG_FOR_PACK_ID 0x2 /* prefer tag over pack_id (def) */
+#define SG_CTL_FLAGM_OTHER_OPENS 0x4	/* rd: other sg fd_s on this dev */
+#define SG_CTL_FLAGM_ORPHANS	0x8	/* rd: orphaned requests on this fd */
+#define SG_CTL_FLAGM_Q_TAIL	0x10	/* used for future cmds on this fd */
+#define SG_CTL_FLAGM_IS_SHARE	0x20	/* rd: fd is read-side or write-side share */
+#define SG_CTL_FLAGM_IS_READ_SIDE 0x40	/* rd: this fd is read-side share */
+#define SG_CTL_FLAGM_UNSHARE	0x80	/* undo share after inflight cmd */
+/* rd> 1: read-side finished, 0: not; wr> 1: finish share post read-side */
+#define SG_CTL_FLAGM_READ_SIDE_FINI 0x100 /* wr> 0: setup for repeat write-side req */
+#define SG_CTL_FLAGM_READ_SIDE_ERR 0x200 /* rd: sharing, read-side got error */
+#define SG_CTL_FLAGM_NO_DURATION 0x400	/* don't calc command duration */
+#define SG_CTL_FLAGM_MORE_ASYNC	0x800	/* yield EAGAIN in more cases */
+#define SG_CTL_FLAGM_EXCL_WAITQ 0x1000	/* only 1 wake up per response */
+#define SG_CTL_FLAGM_SNAP_DEV	0x2000	/* output to debugfs::snapped */
+#define SG_CTL_FLAGM_RM_EVENTFD	0x4000	/* only if new eventfd wanted */
+#define SG_CTL_FLAGM_ALL_BITS	0x7fff	/* should be OR of previous items */
+
+/* Write one of the following values to sg_extended_info::read_value, get... */
+#define SG_SEIRV_INT_MASK	0x0	/* get SG_SEIM_ALL_BITS */
+#define SG_SEIRV_BOOL_MASK	0x1	/* get SG_CTL_FLAGM_ALL_BITS */
+#define SG_SEIRV_VERS_NUM	0x2	/* get driver version number as int */
+#define SG_SEIRV_INACT_RQS	0x3	/* number of inactive requests */
+#define SG_SEIRV_DEV_INACT_RQS	0x4	/* sum(inactive rqs) on owning dev */
+#define SG_SEIRV_SUBMITTED	0x5	/* number of mrqs submitted+unread */
+#define SG_SEIRV_DEV_SUBMITTED	0x6	/* sum(submitted) on all dev's fds */
+#define SG_SEIRV_MAX_RSV_REQS	0x7	/* maximum reserve requests */
+#define SG_SEIRV_DEV_TS_LOWER	0x8	/* device timestamp's lower 32 bits */
+#define SG_SEIRV_DEV_TS_UPPER	0x9	/* device timestamp's upper 32 bits */
+
+/*
+ * A pointer to the following structure is passed as the third argument to
+ * ioctl(SG_SET_GET_EXTENDED). Each bit in the *_wr_mask fields causes the
+ * corresponding integer (e.g. reserved_sz) or bit (e.g. the
+ * SG_CTL_FLAG_TIME_IN_NS bit in ctl_flags) to be read from the user space
+ * and modify the driver. Each bit in the *_rd_mask fields causes the
+ * corresponding integer or bit to be fetched from the driver and written
+ * back to the user space. If the same bit is set in both the *_wr_mask and
+ * corresponding *_rd_mask fields, then which one comes first depends on the
+ * setting but no other operation will split the two. This structure is
+ * padded to 96 bytes to allow for new values to be added in the future.
+ */
+
+/* If both sei_wr_mask and sei_rd_mask are 0, this ioctl does nothing */
+struct sg_extended_info {
+	__u32	sei_wr_mask;	/* OR-ed SG_SEIM_* user->driver values */
+	__u32	sei_rd_mask;	/* OR-ed SG_SEIM_* driver->user values */
+	__u32	ctl_flags_wr_mask;	/* OR-ed SG_CTL_FLAGM_* values */
+	__u32	ctl_flags_rd_mask;	/* OR-ed SG_CTL_FLAGM_* values */
+	__u32	ctl_flags;	/* bit values OR-ed, see SG_CTL_FLAGM_* */
+	__u32	read_value;	/* write SG_SEIRV_*, read back related */
+
+	__u32	reserved_sz;	/* data/sgl size of pre-allocated request */
+	__u32	tot_fd_thresh;	/* total data/sgat for this fd, 0: no limit */
+	__u32	minor_index;	/* rd: kernel's sg device minor number */
+	__u32	share_fd;	/* for SHARE_FD, CHG_SHARE_FD or EVENTFD */
+	__u32	sgat_elem_sz;	/* sgat element size (must be power of 2) */
+	__s32	num;		/* blk_poll: loop_count (-1 -> spin)) */
+	__u8	pad_to_96[48];	/* pad so struct is 96 bytes long */
+};
+
+/*
+ * IOCTLs: Those ioctls that are relevant to the SG 3.x drivers follow.
+ * [Those that only apply to the SG 2.x drivers are at the end of the file.]
+ * (_GET_s yield result via 'int *' 3rd argument unless otherwise indicated)
+ */
+
+#define SG_EMULATED_HOST 0x2203	/* true for emulated host adapter (ATAPI) */
+
+/*
+ * Used to configure SCSI command transformation layer for ATAPI devices.
+ * Only supported by the ide-scsi driver. 20181014 No longer supported, this
+ * driver passes them to the mid-level which returns a EINVAL (22) errno.
+ *
+ * Original note: N.B. 3rd arg is not pointer but value: 3rd arg = 0 to
+ * disable transform, 1 to enable it
+ */
+#define SG_SET_TRANSFORM 0x2204
+#define SG_GET_TRANSFORM 0x2205
+
+#define SG_SET_RESERVED_SIZE 0x2275  /* request new reserved buffer size */
+#define SG_GET_RESERVED_SIZE 0x2272  /* actual size of reserved buffer */
+
+/*
+ * Historically the scsi/sg driver has used 0x22 as it ioctl base number.
+ * Add a define for that value and use it for several new ioctls added in
+ * version 4.0.01 sg driver and later.
+ */
+#define SG_IOCTL_MAGIC_NUM 0x22
+
+#define SG_SET_GET_EXTENDED _IOWR(SG_IOCTL_MAGIC_NUM, 0x51,	\
+				  struct sg_extended_info)
+
+/* The following ioctl has a 'sg_scsi_id_t *' object as its 3rd argument. */
+#define SG_GET_SCSI_ID 0x2276   /* Yields fd's bus, chan, dev, lun + type */
+/* SCSI id information can also be obtained from SCSI_IOCTL_GET_IDLUN */
+
+/* Override host setting and always DMA using low memory ( <16MB on i386) */
+#define SG_SET_FORCE_LOW_DMA 0x2279  /* 0-> use adapter setting, 1-> force */
+#define SG_GET_LOW_DMA 0x227a	/* 0-> use all ram for dma; 1-> low dma ram */
+
+/*
+ * When SG_SET_FORCE_PACK_ID set to 1, pack_id (or tag) is input to read() or
+ * ioctl(SG_IO_RECEIVE). These functions wait until matching packet (request/
+ * command) is finished but they will return with EAGAIN quickly if the file
+ * descriptor was opened O_NONBLOCK or (in v4) if SGV4_FLAG_IMMED is given.
+ * The tag is used when SG_CTL_FLAGM_TAG_FOR_PACK_ID is set on the parent
+ * file descriptor (default: use pack_id). If pack_id or tag is -1 then read
+ * oldest waiting and this is the same action as when FORCE_PACK_ID is
+ * clear on the parent file descriptor. In the v4 interface the pack_id is
+ * placed the in sg_io_v4::request_extra field .
+ */
+#define SG_SET_FORCE_PACK_ID 0x227b	/* pack_id or in v4 can be tag */
+#define SG_GET_PACK_ID 0x227c  /* Yields oldest readable pack_id/tag, or -1 */
+
+#define SG_GET_NUM_WAITING 0x227d /* Number of commands awaiting read() */
+
+/* Yields max scatter gather tablesize allowed by current host adapter */
+#define SG_GET_SG_TABLESIZE 0x227F  /* 0 implies can't do scatter gather */
+
+/*
+ * Integer form of version number: [x]xyyzz where [x] empty when x=0 .
+ * String form of version number: "[x]x.[y]y.zz"
+ */
+#define SG_GET_VERSION_NUM 0x2282 /* Example: version "2.1.34" yields 20134 */
+
+/* Returns -EBUSY if occupied. 3rd argument pointer to int (see next) */
+#define SG_SCSI_RESET 0x2284
+/*
+ * Associated values that can be given to SG_SCSI_RESET follow.
+ * SG_SCSI_RESET_NO_ESCALATE may be OR-ed to the _DEVICE, _TARGET, _BUS
+ * or _HOST reset value so only that action is attempted.
+ */
+#define		SG_SCSI_RESET_NOTHING	0
+#define		SG_SCSI_RESET_DEVICE	1
+#define		SG_SCSI_RESET_BUS	2
+#define		SG_SCSI_RESET_HOST	3
+#define		SG_SCSI_RESET_TARGET	4
+#define		SG_SCSI_RESET_NO_ESCALATE	0x100
+
+/* synchronous SCSI command ioctl, (for version 3 and 4 interface) */
+#define SG_IO 0x2285	/* similar effect as write() followed by read() */
+
+#define SG_GET_REQUEST_TABLE 0x2286	/* yields table of active requests */
+
+/* How to treat EINTR during SG_IO ioctl(), only in sg v3 and v4 driver */
+#define SG_SET_KEEP_ORPHAN 0x2287 /* 1 -> hold for read(), 0 -> drop (def) */
+#define SG_GET_KEEP_ORPHAN 0x2288
+
+/*
+ * Yields scsi midlevel's access_count for this SCSI device. 20181014 No
+ * longer available, always yields 1.
+ */
+#define SG_GET_ACCESS_COUNT 0x2289
+
+
+/*
+ * Default size (in bytes) a single scatter-gather list element can have.
+ * The value used by the driver is 'max(SG_SCATTER_SZ, PAGE_SIZE)'. This
+ * value should be a power of 2 (and may be rounded up internally). In the
+ * v4 driver this can be changed by ioctl(SG_SET_GET_EXTENDED{SGAT_ELEM_SZ}).
+ */
+#define SG_SCATTER_SZ (8 * 4096)
+
+/* sg driver users' code should handle retries (e.g. from Unit Attentions) */
+#define SG_DEFAULT_RETRIES 0
+
+/* Defaults, commented if they differ from original sg driver */
+#define SG_DEF_FORCE_PACK_ID 0
+#define SG_DEF_KEEP_ORPHAN 0
+#define SG_DEF_RESERVED_SIZE SG_SCATTER_SZ /* load time option */
+
+/*
+ * Maximum outstanding requests (i.e write()s without corresponding read()s)
+ * yields EDOM from write() if exceeded. This limit only applies prior to
+ * version 3.9 . It is still used as a maximum number of sg_req_info objects
+ * that are returned from the SG_GET_REQUEST_TABLE ioctl.
+ */
+#define SG_MAX_QUEUE 16
+
+#define SG_BIG_BUFF SG_DEF_RESERVED_SIZE    /* for backward compatibility */
+
+/*
+ * Alternate style type names, "..._t" variants (as found in the
+ * 'typedef struct * {};' definitions above) are preferred to these:
+ */
+typedef struct sg_io_hdr Sg_io_hdr;
+typedef struct sg_io_vec Sg_io_vec;
+typedef struct sg_scsi_id Sg_scsi_id;
+typedef struct sg_req_info Sg_req_info;
+
+
+/* vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv */
+/*   The v1+v2 SG interface based on the 'sg_header' structure follows.   */
+/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
+
+#define SG_MAX_SENSE 16	/* this only applies to the sg_header interface */
+
+struct sg_header {
+	int pack_len;	/* [o] reply_len (ie useless), ignored as input */
+	int reply_len;	/* [i] max length of expected reply (inc. sg_header) */
+	int pack_id;	/* [io] id number of packet (use ints >= 0) */
+	int result;	/* [o] 0==ok, else (+ve) Unix errno (best ignored) */
+	unsigned int twelve_byte:1;
+	    /* [i] Force 12 byte command length for group 6 & 7 commands  */
+	unsigned int target_status:5;	/* [o] scsi status from target */
+	unsigned int host_status:8;	/* [o] host status (see "DID" codes) */
+	unsigned int driver_status:8;	/* [o] driver status+suggestion */
+	unsigned int other_flags:10;	/* unused */
+	unsigned char sense_buffer[SG_MAX_SENSE];
+	/*
+	 * [o] Output in 3 cases:
+	 *	when target_status is CHECK_CONDITION or
+	 *	when target_status is COMMAND_TERMINATED or
+	 *	when (driver_status & DRIVER_SENSE) is true.
+	 */
+};
+
+/*
+ * IOCTLs: The following are not required (or ignored) when the v3 or v4
+ * interface is used as those structures contain a timeout field. These
+ * ioctls are kept for backward compatibility with v1+v2 interfaces.
+ */
+
+#define SG_SET_TIMEOUT 0x2201  /* unit: (user space) jiffies */
+#define SG_GET_TIMEOUT 0x2202  /* yield timeout as _return_ value */
+
+/*
+ * Get/set command queuing state per fd (default is SG_DEF_COMMAND_Q.
+ * Each time a sg_io_hdr_t object is seen on this file descriptor, this
+ * command queuing flag is set on (overriding the previous setting).
+ * This setting defaults to 0 (i.e. no queuing) but gets set the first
+ * time that fd sees a v3 or v4 interface request.
+ */
+#define SG_GET_COMMAND_Q 0x2270   /* Yields 0 (queuing off) or 1 (on) */
+#define SG_SET_COMMAND_Q 0x2271   /* Change queuing state with 0 or 1 */
+
+/*
+ * Turn on/off error sense trace in the kernel log (1 and 0 respectively, default is
+ * off).
+ */
+#define SG_SET_DEBUG 0x227e    /* 0 -> turn off debug */
+
+/*
+ * override SCSI command length with given number on the next write() on
+ * this file descriptor (v1 and v2 interface only)
+ */
+#define SG_NEXT_CMD_LEN 0x2283
+
+/*
+ * New ioctls to replace async (non-blocking) write()/read() interface.
+ * Present in version 4 and later of the sg driver [>20190427]. The
+ * SG_IOSUBMIT_V3 and SG_IORECEIVE_V3 ioctls accept the sg_v3 interface
+ * based on struct sg_io_hdr shown above. The SG_IOSUBMIT and SG_IORECEIVE
+ * ioctls accept the sg_v4 interface based on struct sg_io_v4 found in
+ * <include/uapi/linux/bsg.h>. These objects are passed by a pointer in
+ * the third argument of the ioctl.
+ *
+ * Data may be transferred both from the user space to the driver by these
+ * ioctls. Hence the _IOWR macro is used here to generate the ioctl number
+ * rather than _IOW or _IOR.
+ */
+/* Submits a v4 interface object to driver, optionally receive tag back */
+#define SG_IOSUBMIT _IOWR(SG_IOCTL_MAGIC_NUM, 0x41, struct sg_io_v4)
+
+/* Gives some v4 identifying info to driver, receives associated response */
+#define SG_IORECEIVE _IOWR(SG_IOCTL_MAGIC_NUM, 0x42, struct sg_io_v4)
+
+/* Submits a v3 interface object to driver */
+#define SG_IOSUBMIT_V3 _IOWR(SG_IOCTL_MAGIC_NUM, 0x45, struct sg_io_hdr)
+
+/* Gives some v3 identifying info to driver, receives associated response */
+#define SG_IORECEIVE_V3 _IOWR(SG_IOCTL_MAGIC_NUM, 0x46, struct sg_io_hdr)
+
+/* Provides identifying info about a prior submission (e.g. a tag) */
+#define SG_IOABORT _IOW(SG_IOCTL_MAGIC_NUM, 0x43, struct sg_io_v4)
+
+/* command queuing is always on when the v3 or v4 interface is used */
+#define SG_DEF_COMMAND_Q 0
+
+#define SG_DEF_UNDERRUN_FLAG 0
+
+/* If the timeout value in the v3_v4 interfaces is 0, this value is used */
+#define SG_DEFAULT_TIMEOUT	(60*HZ)	/* HZ == 'jiffies in 1 second' */
+
+#endif		/* end of _UAPI_SCSI_SG_H guard */
diff --git a/utils/Makefile b/utils/Makefile
new file mode 100644
index 0000000..9558f9e
--- /dev/null
+++ b/utils/Makefile
@@ -0,0 +1,56 @@
+SHELL = /bin/sh
+
+PREFIX=/usr/local
+INSTDIR=$(DESTDIR)/$(PREFIX)/bin
+MANDIR=$(DESTDIR)/$(PREFIX)/share/man
+
+CC = gcc
+LD = gcc
+
+EXECS = hxascdmp
+EXTRA_EXECS = hxascdmp
+
+MAN_PGS = hxascdmp.1
+MAN_PREF = man1
+
+CFLAGS = -g -O2 -W -Wall -iquote ../include
+# CFLAGS = -g -O2 -W -iquote ../include -pedantic -std=c99
+
+LDFLAGS =
+
+all: $(EXECS)
+
+depend dep:
+	for i in *.c; do $(CC) $(INCLUDES) $(CFLAGS) -M $$i; \
+	done > .depend
+
+clean:
+	/bin/rm -f *.o $(EXTRA_EXECS) core .depend
+
+hxascdmp: hxascdmp.o
+	$(LD) -o $@ $(LDFLAGS) $^
+
+
+install: $(EXECS)
+	install -d $(INSTDIR)
+	for name in $^; \
+	 do install -s -o root -g root -m 755 $$name $(INSTDIR); \
+	done
+	install -d $(MANDIR)/$(MAN_PREF)
+	for mp in $(MAN_PGS); \
+	 do install -o root -g root -m 644 $$mp $(MANDIR)/$(MAN_PREF); \
+	 gzip -9f $(MANDIR)/$(MAN_PREF)/$$mp; \
+	done
+
+uninstall:
+	dists="$(EXECS)"; \
+	for name in $$dists; do \
+	 rm -f $(INSTDIR)/$$name; \
+	done
+	for mp in $(MAN_PGS); do \
+	 rm -f $(MANDIR)/$(MAN_PREF)/$$mp.gz; \
+	done
+
+ifeq (.depend,$(wildcard .depend))
+include .depend
+endif
diff --git a/utils/Makefile.cygwin b/utils/Makefile.cygwin
new file mode 100644
index 0000000..86e9fa3
--- /dev/null
+++ b/utils/Makefile.cygwin
@@ -0,0 +1,33 @@
+# Assumes Makefile is used in a cygwin shell
+
+SHELL = /bin/sh
+
+CC = gcc
+LD = gcc
+
+EXECS =	hxascdmp
+
+EXE_S =	hxascdmp.exe
+
+# OS_FLAGS = -DSG_LIB_WIN32 -DSPTD
+OS_FLAGS = -DSG_LIB_WIN32
+LARGE_FILE_FLAGS = -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64
+EXTRA_FLAGS = $(OS_FLAGS) $(LARGE_FILE_FLAGS)
+
+# CFLAGS = -O2 -Wall -W $(EXTRA_FLAGS)
+CFLAGS = -g -O2 -Wall -W $(EXTRA_FLAGS)
+# CFLAGS = -g -O2 -Wall -W -pedantic -std=c99 $(EXTRA_FLAGS)
+
+LDFLAGS = 
+
+all: $(EXECS)
+
+clean:
+	rm *.o $(EXE_S)
+
+.c.o:
+	$(CC) $(INCLUDES) $(CFLAGS) $(S_CFLAGS) -c -o $@ $<
+
+hxascdmp: hxascdmp.o
+	$(LD) -o $@ $(LDFLAGS) $@.o
+
diff --git a/utils/Makefile.freebsd b/utils/Makefile.freebsd
new file mode 100644
index 0000000..a43a569
--- /dev/null
+++ b/utils/Makefile.freebsd
@@ -0,0 +1,52 @@
+SHELL = /bin/sh
+
+PREFIX=/usr/local
+INSTDIR=$(DESTDIR)/$(PREFIX)/bin
+MANDIR=$(DESTDIR)/$(PREFIX)/man
+
+CC = clang
+LD = clang
+
+EXECS = hxascdmp
+
+MAN_PGS = 
+MAN_PREF = man8
+
+CFLAGS = -g -O2 -W 
+# CFLAGS = -g -O2 -W -pedantic -std=c99
+
+LDFLAGS =
+
+all: $(EXECS)
+
+depend dep:
+	for i in *.c; do $(CC) $(INCLUDES) $(CFLAGS) -M $$i; \
+	done > .depend
+
+clean:
+	/bin/rm -f *.o $(EXECS) core .depend
+
+hxascdmp: hxascdmp.o
+	$(LD) -o $@ $(LDFLAGS) $@.o
+
+
+install: $(EXECS)
+	install -d $(INSTDIR)
+	for name in $(EXECS); \
+	 do install -s -m 755 $$name $(INSTDIR); \
+	done
+	install -d $(MANDIR)/$(MAN_PREF)
+	for mp in $(MAN_PGS); \
+	 do install -m 644 $$mp $(MANDIR)/$(MAN_PREF); \
+	 gzip -9f $(MANDIR)/$(MAN_PREF)/$$mp; \
+	done
+
+uninstall:
+	dists="$(EXECS)"; \
+	for name in $$dists; do \
+	 rm -f $(INSTDIR)/$$name; \
+	done
+	for mp in $(MAN_PGS); do \
+	 rm -f $(MANDIR)/$(MAN_PREF)/$$mp.gz; \
+	done
+
diff --git a/utils/Makefile.mingw b/utils/Makefile.mingw
new file mode 100644
index 0000000..6fed348
--- /dev/null
+++ b/utils/Makefile.mingw
@@ -0,0 +1,33 @@
+# Assumes makefile is used in a MSYS shell with a MinGW compiler available.
+
+SHELL = /bin/sh
+
+CC = gcc
+LD = gcc
+
+EXECS =	hxascdmp
+
+EXE_S =	hxascdmp.exe
+
+# OS_FLAGS = -DSG_LIB_WIN32 -DSG_LIB_MINGW -DSPTD
+OS_FLAGS = -DSG_LIB_WIN32 -DSG_LIB_MINGW
+LARGE_FILE_FLAGS = -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64
+EXTRA_FLAGS = $(OS_FLAGS) $(LARGE_FILE_FLAGS)
+
+# CFLAGS = -O2 -Wall -W $(EXTRA_FLAGS)
+CFLAGS = -g -O2 -Wall -W $(EXTRA_FLAGS)
+# CFLAGS = -g -O2 -Wall -W -pedantic -std=c99 $(EXTRA_FLAGS)
+
+LDFLAGS = 
+
+all: $(EXECS)
+
+clean:
+	rm *.o $(EXE_S)
+
+.c.o:
+	$(CC) $(INCLUDES) $(CFLAGS) $(S_CFLAGS) -c -o $@ $<
+
+hxascdmp: hxascdmp.o
+	$(LD) -o $@ $(LDFLAGS) $@.o
+
diff --git a/utils/Makefile.solaris b/utils/Makefile.solaris
new file mode 100644
index 0000000..075d004
--- /dev/null
+++ b/utils/Makefile.solaris
@@ -0,0 +1,51 @@
+SHELL = /bin/sh
+
+PREFIX=/usr/local
+INSTDIR=$(DESTDIR)/$(PREFIX)/bin
+MANDIR=$(DESTDIR)/$(PREFIX)/man
+
+CC = gcc
+LD = gcc
+
+EXECS = hxascdmp
+
+MAN_PGS = 
+MAN_PREF = man8
+
+CFLAGS = -g -O2 -W 
+# CFLAGS = -g -O2 -W -pedantic -std=c99
+
+LDFLAGS =
+
+all: $(EXECS)
+
+depend dep:
+	for i in *.c; do $(CC) $(INCLUDES) $(CFLAGS) -M $$i; \
+	done > .depend
+
+clean:
+	/bin/rm -f *.o $(EXECS) core .depend
+
+hxascdmp: hxascdmp.o
+	$(LD) -o $@ $(LDFLAGS) $@.o
+
+
+install: $(EXECS)
+	install -d $(INSTDIR)
+	for name in $(EXECS); \
+	 do install -s -f $(INSTDIR) $$name; \
+	done
+	install -d $(MANDIR)/$(MAN_PREF)
+	for mp in $(MAN_PGS); \
+	 do install -m 644 -f $(MANDIR)/$(MAN_PREF) $$mp; \
+	done
+
+uninstall:
+	dists="$(EXECS)"; \
+	for name in $$dists; do \
+	 rm -f $(INSTDIR)/$$name; \
+	done
+	for mp in $(MAN_PGS); do \
+	 rm -f $(MANDIR)/$(MAN_PREF)/$$mp.gz; \
+	done
+
diff --git a/utils/README b/utils/README
new file mode 100644
index 0000000..c00c22a
--- /dev/null
+++ b/utils/README
@@ -0,0 +1,20 @@
+This directory contains these utilities:
+  - hxascdmp: takes a binary stream and converts it to hexadecimal ASCII
+    which is sent to stdout. The incoming binary stream can either be
+    from a file or, in the absence of a file name, from stdin. Similar to
+    the Unix "od" command. By default, it decodes 16 bytes per line with
+    an ASCII interpretation to the right of each line. See its
+    hxascdmp(1) man page.
+  - sg_chk_asc and tst_sg_lib: are no longer here, they have been moved
+    to the 'testing' directory (a sibling of this directory).
+
+
+By default, the Makefile only builds the hxascdmp utility. The 'Makefile'
+file (i.e. with no suffix) builds for Linux; the 'Makefile.freebsd' file
+builds for FreeBSD (e.g. 'make -f Makefile.freebsd'); the
+'Makefile.solaris' file builds for Solaris; the 'Makefile.mingw' builds
+in the Windows MinGW environment (e.g.  msys shell); and 'Makefile.cygwin'
+builds in the Windows Cygwin environment.
+
+Douglas Gilbert
+4th November 2017
diff --git a/utils/hxascdmp.1 b/utils/hxascdmp.1
new file mode 100644
index 0000000..2281492
--- /dev/null
+++ b/utils/hxascdmp.1
@@ -0,0 +1,111 @@
+.TH HXASCDMP "1" "February 2019" "sg3_utils\-1.45" SG3_UTILS
+.SH NAME
+hxascdmp \- hexadecimal ASCII dump
+.SH SYNOPSIS
+.B hxascdmp
+[\fI\-b=BPL\fR] [\fI\-h\fR] [\fI\-H\fR] [\fI\-N\fR] [\fI\-o=OFF\fR]
+[\fI\-V\fR] [\fIFILE+\fR]
+.SH DESCRIPTION
+.\" Add any additional description here
+.PP
+This utility reads one or more \fIFILE\fR names and dumps them in hexadecimal
+and ASCII to stdout. If no \fIFILE\fR is given then stdin is read instead;
+reading continues (or stalls) until an EOF is received.
+.PP
+The default format is to start each line with the hexadecimal address (offset
+from the start of file) followed by 16 hexadecimal bytes separated by a
+single space (apart from the 8th and 9th bytes which are separated by two
+spaces). If the \fI\-H\fR is not given, there is then a string of 16 ASCII
+characters corresponding to the hexadecimal bytes earlier in the line; only
+bytes in the range 0x20 to 0x7e are printed in ASCII, other bytes values are
+printed as '.' . If the \fI\-H\fR is not given, each \fIFILE\fR name that
+appears on the command line is printed on a separate line prior to that
+file's hexadecimal ASCII dump.
+.PP
+If the \fI\-N\fR option is given then no address is printed out and each
+line starts with the next hexadecimal byte.
+.PP
+This utility is pretty close to the 'hexdump -C' variant of BSD's
+.B hexdump(1)
+command.
+.SH OPTIONS
+.TP
+\fB\-b\fR=\fIBPL\fR
+where \fIBPL\fR specifies the number of bytes per line. The default value is
+16. 16 bytes per line is just enough to allow the address, 16 bytes in
+hexadecimal followed by 16 bytes as ASCII to fit on a standard 80 column
+wide terminal.
+.TP
+\fB\-h\fR
+output the usage message then exit.
+.TP
+\fB\-H\fR
+output hexadecimal only (i.e. don't place an ASCII representation at the
+end of each line).
+.TP
+\fB\-N\fR
+no address; so each line starts with the next hexadecimal byte.
+.TP
+\fB\-o\fR=\fIOFF\fR
+where \fIOFF\fR specifies the byte offset from the beginning of the pipe or
+the beginning of each file that the output starts. If the address is being
+printed out then it starts at \fIOFF\fR. The default is an \fIOFF\fR of 0 .
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the version string and then exit.
+.SH NOTES
+In Windows the given file (or files) are set to binary mode.
+.SH EXIT STATUS
+The exit status of hxascdmp is 0 when it is successful. If any of the
+given \fIFILE\fR names cannot be opened then the exit status is 1.
+.SH EXAMPLES
+First we manufacture a short file with a mix of data in it: mostly ASCII with
+some control characters and 0xaa (which the echo command only accepts in
+octal (0252):
+.PP
+   $ echo -e "three blind mice,\t\r\0252" > 3bm.txt
+.PP
+Now we use this utility to see exactly what is in the file. To avoid
+problems with line wrapping, the bytes per line option is set to 8:
+.PP
+   $ hxascdmp -b=8 3bm.txt
+.br
+ASCII hex dump of file: 3bm.txt
+.br
+ 00      74 68 72 65  65 20 62 6c   three bl
+.br
+ 08      69 6e 64 20  6d 69 63 65   ind mice
+.br
+ 10      2c 09 0d aa  0a            ,....
+.PP
+Using the same file, use this utility to output only hexadecimal formatted
+16 bytes per line.
+.PP
+   $ hxascdmp -H 3bm.txt
+.br
+hex dump of file: 3bm.txt
+.br
+ 00      74 68 72 65 65 20 62 6c  69 6e 64 20 6d 69 63 65
+.br
+ 10      2c 09 0d aa 0a
+.PP
+For comparison the hexdump utility gives similar output:
+.PP
+   $ hexdump -C 3bm.txt
+.br
+00000000  74 68 72 65 65 20 62 6c  69 6e 64 20 6d 69 63 65  |three blind mice|
+.br
+00000010  2c 09 0d aa 0a                                    |,....|
+.br
+00000015
+.SH AUTHORS
+Written by Douglas Gilbert.
+.SH "REPORTING BUGS"
+Report bugs to <dgilbert at interlog dot com>.
+.SH COPYRIGHT
+Copyright \(co 2004\-2019 Douglas Gilbert
+.br
+This software is distributed under a FreeBSD license. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.B hexdump(1)
diff --git a/utils/hxascdmp.c b/utils/hxascdmp.c
new file mode 100644
index 0000000..f08d031
--- /dev/null
+++ b/utils/hxascdmp.c
@@ -0,0 +1,521 @@
+/*
+ * Copyright (c) 2004-2019 Douglas Gilbert.
+ * All rights reserved.
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the BSD_LICENSE file.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#define __STDC_FORMAT_MACROS 1
+#include <inttypes.h>
+
+#define DEF_BYTES_PER_LINE 16
+
+static int bytes_per_line = DEF_BYTES_PER_LINE;
+
+static const char * version_str = "1.11 20190527";
+
+#define CHARS_PER_HEX_BYTE 3
+#define BINARY_START_COL 6
+#define MAX_LINE_LENGTH 257
+
+
+#ifdef SG_LIB_MINGW
+/* Non Unix OSes distinguish between text and binary files.
+   Set text mode on fd. Does nothing in Unix. Returns negative number on
+   failure. */
+int
+sg_set_text_mode(int fd)
+{
+    return setmode(fd, O_TEXT);
+}
+
+/* Set binary mode on fd. Does nothing in Unix. Returns negative number on
+   failure. */
+int
+sg_set_binary_mode(int fd)
+{
+    return setmode(fd, O_BINARY);
+}
+
+#else
+/* For Unix the following functions are dummies. */
+int
+sg_set_text_mode(int fd)
+{
+    return fd;  /* fd should be >= 0 */
+}
+
+int
+sg_set_binary_mode(int fd)
+{
+    return fd;
+}
+#endif
+
+/* Returns the number of times 'ch' is found in string 's' given the
+ * string's length. */
+static int
+num_chs_in_str(const char * s, int slen, int ch)
+{
+    int res = 0;
+
+    while (--slen >= 0) {
+        if (ch == s[slen])
+            ++res;
+    }
+    return res;
+}
+
+/* If the number in 'buf' can be decoded or the multiplier is unknown
+ * then -1LL is returned. Accepts a hex prefix (0x or 0X) or a decimal
+ * multiplier suffix (as per GNU's dd (since 2002: SI and IEC 60027-2)).
+ * Main (SI) multipliers supported: K, M, G, T, P. Ignore leading spaces
+ * and tabs; accept comma, hyphen, space, tab and hash as terminator. */
+int64_t
+sg_get_llnum(const char * buf)
+{
+    int res, len, n;
+    int64_t num, ll;
+    uint64_t unum;
+    char * cp;
+    const char * b;
+    char c = 'c';
+    char c2 = '\0';     /* keep static checker happy */
+    char c3 = '\0';     /* keep static checker happy */
+    char lb[32];
+
+    if ((NULL == buf) || ('\0' == buf[0]))
+        return -1LL;
+    len = strlen(buf);
+    n = strspn(buf, " \t");
+    if (n > 0) {
+        if (n == len)
+            return -1LL;
+        buf += n;
+        len -= n;
+    }
+    /* following hack to keep C++ happy */
+    cp = strpbrk((char *)buf, " \t,#-");
+    if (cp) {
+        len = cp - buf;
+        n = (int)sizeof(lb) - 1;
+        len = (len < n) ? len : n;
+        memcpy(lb, buf, len);
+        lb[len] = '\0';
+        b = lb;
+    } else
+        b = buf;
+    if (('0' == b[0]) && (('x' == b[1]) || ('X' == b[1]))) {
+        res = sscanf(b + 2, "%" SCNx64 , &unum);
+        num = unum;
+    } else if ('H' == toupper((int)b[len - 1])) {
+        res = sscanf(b, "%" SCNx64 , &unum);
+        num = unum;
+    } else
+        res = sscanf(b, "%" SCNd64 "%c%c%c", &num, &c, &c2, &c3);
+    if (res < 1)
+        return -1LL;
+    else if (1 == res)
+        return num;
+    else {
+        if (res > 2)
+            c2 = toupper((int)c2);
+        if (res > 3)
+            c3 = toupper((int)c3);
+        switch (toupper((int)c)) {
+        case 'C':
+            return num;
+        case 'W':
+            return num * 2;
+        case 'B':
+            return num * 512;
+        case 'K':
+            if (2 == res)
+                return num * 1024;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1024;
+            return -1LL;
+        case 'M':
+            if (2 == res)
+                return num * 1048576;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1048576;
+            return -1LL;
+        case 'G':
+            if (2 == res)
+                return num * 1073741824;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1073741824;
+            return -1LL;
+        case 'T':
+            if (2 == res)
+                return num * 1099511627776LL;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000000000LL;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1099511627776LL;
+            return -1LL;
+        case 'P':
+            if (2 == res)
+                return num * 1099511627776LL * 1024;
+            if (('B' == c2) || ('D' == c2))
+                return num * 1000000000000LL * 1000;
+            if (('I' == c2) && (4 == res) && ('B' == c3))
+                return num * 1099511627776LL * 1024;
+            return -1LL;
+        case 'X':
+            cp = (char *)strchr(b, 'x');
+            if (NULL == cp)
+                cp = (char *)strchr(b, 'X');
+            if (cp) {
+                ll = sg_get_llnum(cp + 1);
+                if (-1LL != ll)
+                    return num * ll;
+            }
+            return -1LL;
+        default:
+            fprintf(stderr, "unrecognized multiplier\n");
+            return -1LL;
+        }
+    }
+}
+
+static void
+dStrHex(const char* str, int len, long start, int noAddr)
+{
+    const char* p = str;
+    unsigned char c;
+    char buff[MAX_LINE_LENGTH];
+    long a = start;
+    int bpstart, cpstart;
+    int j, k, line_length, nl, cpos, bpos, midline_space;
+
+    if (noAddr) {
+        bpstart = 0;
+        cpstart = ((CHARS_PER_HEX_BYTE * bytes_per_line) + 1) + 5;
+    } else {
+        bpstart = BINARY_START_COL;
+        cpstart = BINARY_START_COL +
+                        ((CHARS_PER_HEX_BYTE * bytes_per_line) + 1) + 5;
+    }
+    cpos = cpstart;
+    bpos = bpstart;
+    midline_space = ((bytes_per_line + 1) / 2);
+
+    if (len <= 0)
+        return;
+    line_length = BINARY_START_COL +
+                  (bytes_per_line * (1 + CHARS_PER_HEX_BYTE)) + 7;
+    if (line_length >= MAX_LINE_LENGTH) {
+        fprintf(stderr, "bytes_per_line causes maximum line length of %d "
+                        "to be exceeded\n", MAX_LINE_LENGTH);
+        return;
+    }
+    memset(buff, ' ', line_length);
+    buff[line_length] = '\0';
+    if (0 == noAddr) {
+        k = sprintf(buff + 1, "%.2lx", a);
+        buff[k + 1] = ' ';
+    }
+
+    for(j = 0; j < len; j++) {
+        nl = (0 == (j % bytes_per_line));
+        if ((j > 0) && nl) {
+            printf("%s\n", buff);
+            bpos = bpstart;
+            cpos = cpstart;
+            a += bytes_per_line;
+            memset(buff,' ', line_length);
+            if (0 == noAddr) {
+                k = sprintf(buff + 1, "%.2lx", a);
+                buff[k + 1] = ' ';
+            }
+        }
+        c = *p++;
+        bpos += (nl && noAddr) ?  0 : CHARS_PER_HEX_BYTE;
+        if ((bytes_per_line > 4) && ((j % bytes_per_line) == midline_space))
+            bpos++;
+        sprintf(&buff[bpos], "%.2x", (int)(unsigned char)c);
+        buff[bpos + 2] = ' ';
+        if ((c < ' ') || (c >= 0x7f))
+            c='.';
+        buff[cpos++] = c;
+    }
+    if (cpos > cpstart)
+        printf("%s\n", buff);
+}
+
+static void
+dStrHexOnly(const char* str, int len, long start, int noAddr)
+{
+    const char* p = str;
+    unsigned char c;
+    char buff[MAX_LINE_LENGTH];
+    long a = start;
+    int bpstart, bpos, nl;
+    int midline_space = ((bytes_per_line + 1) / 2);
+    int j, k, line_length;
+
+    if (len <= 0)
+        return;
+    bpstart = (noAddr ? 0 : BINARY_START_COL);
+    bpos = bpstart;
+    line_length = (noAddr ? 0 : BINARY_START_COL) +
+                  (bytes_per_line * CHARS_PER_HEX_BYTE) + 4;
+    if (line_length >= MAX_LINE_LENGTH) {
+        fprintf(stderr, "bytes_per_line causes maximum line length of %d "
+                        "to be exceeded\n", MAX_LINE_LENGTH);
+        return;
+    }
+    memset(buff, ' ', line_length);
+    buff[line_length] = '\0';
+    if (0 == noAddr) {
+        k = sprintf(buff + 1, "%.2lx", a);
+        buff[k + 1] = ' ';
+    }
+
+    for(j = 0; j < len; j++) {
+        nl = (0 == (j % bytes_per_line));
+        if ((j > 0) && nl) {
+            printf("%s\n", buff);
+            bpos = bpstart;
+            a += bytes_per_line;
+            memset(buff,' ', line_length);
+            if (0 == noAddr) {
+                k = sprintf(buff + 1, "%.2lx", a);
+                buff[k + 1] = ' ';
+            }
+        }
+        c = *p++;
+        bpos += (nl && noAddr) ? 0 : CHARS_PER_HEX_BYTE;
+        if ((bytes_per_line > 4) && ((j % bytes_per_line) == midline_space))
+            bpos++;
+        sprintf(&buff[bpos], "%.2x", (int)(unsigned char)c);
+        buff[bpos + 2] = ' ';
+    }
+    if (bpos > bpstart)
+        printf("%s\n", buff);
+}
+
+static void
+usage()
+{
+    fprintf(stderr, "Usage: hxascdmp [-1] [-2] [-b=<n>] [-h] [-H] [-N] "
+            "[-o=<off>] [-q]\n"
+            "                [-V] [-?]  [<file>+]\n");
+    fprintf(stderr, "  where:\n");
+    fprintf(stderr, "    -1         print first byte in hex, prepend '0x' "
+            "if '-H' given\n");
+    fprintf(stderr, "    -2         like '-1' but print first two bytes\n");
+    fprintf(stderr, "    -b=<n>     bytes per line to display "
+                    "(def: 16)\n");
+    fprintf(stderr, "    -h         print this usage message\n");
+    fprintf(stderr, "    -H         print hex only (i.e. no ASCII "
+            "to right)\n");
+    fprintf(stderr, "    -N         no address, start in first column\n");
+    fprintf(stderr, "    -o=<off>    start decoding at byte <off>. Suffix "
+            "multipliers allowed\n");
+    fprintf(stderr, "    -q         quiet: suppress output of header "
+            "info\n");
+    fprintf(stderr, "    -V         print version string then exits\n");
+    fprintf(stderr, "    -?         print this usage message\n");
+    fprintf(stderr, "    <file>+    reads file(s) and outputs each "
+                    "as hex ASCII\n");
+    fprintf(stderr, "               if no <file> then reads stdin\n\n");
+    fprintf(stderr, "Sends hex ASCII dump of stdin/file to stdout\n");
+}
+
+int
+main(int argc, const char ** argv)
+{
+    char buff[8192];
+    int num = 8192;
+    long start = 0;
+    int64_t offset = 0;
+    int res, k, u, len, n;
+    int inFile = STDIN_FILENO;
+    int doHelp = 0;
+    int doHex = 0;
+    int noAddr = 0;
+    int doVersion = 0;
+    int hasFilename = 0;
+    int quiet = 0;
+    int print1 = 0;
+    int print2 = 0;
+    int ret = 0;
+    const char * cp;
+
+    for (k = 1; k < argc; k++) {
+        cp = argv[k];
+        len = strlen(cp);
+        if (0 == strncmp("-b=", cp, 3)) {
+            res = sscanf(cp + 3, "%d", &u);
+            if ((1 != res) || (u < 1)) {
+                fprintf(stderr, "Bad value after '-b=' option\n");
+                usage();
+                return 1;
+            }
+            bytes_per_line = u;
+        } else if (0 == strncmp("-o=", cp, 3)) {
+            int64_t off = sg_get_llnum(cp + 3);
+
+            if (off == -1) {
+                fprintf(stderr, "Bad value after '-o=' option\n");
+                usage();
+                return 1;
+            }
+            offset = off;
+        } else if ((len > 1) && ('-' == cp[0]) && ('-' != cp[1])) {
+            res = 0;
+            n = num_chs_in_str(cp + 1, len - 1, '1');
+            print1 += n;
+            res += n;
+            n = num_chs_in_str(cp + 1, len - 1, '2');
+            print2 += n;
+            res += n;
+            n = num_chs_in_str(cp + 1, len - 1, 'h');
+            doHelp += n;
+            res += n;
+            n = num_chs_in_str(cp + 1, len - 1, 'H');
+            doHex += n;
+            res += n;
+            n = num_chs_in_str(cp + 1, len - 1, 'N');
+            noAddr += n;
+            res += n;
+            n = num_chs_in_str(cp + 1, len - 1, 'q');
+            quiet += n;
+            res += n;
+            n = num_chs_in_str(cp + 1, len - 1, 'V');
+            doVersion += n;
+            res += n;
+            n = num_chs_in_str(cp + 1, len - 1, '?');
+            doHelp += n;
+            res += n;
+            if (0 == res) {
+                fprintf(stderr, "No option recognized in str: %s\n", cp);
+                usage();
+                return 1;
+            }
+        } else if (0 == strcmp("-?", argv[k]))
+            ++doHelp;
+        else if (*argv[k] == '-') {
+            fprintf(stderr, "unknown switch: %s\n", argv[k]);
+            usage();
+            return 1;
+        } else {
+            hasFilename = 1;
+            break;
+        }
+        if (print2)
+            print1 += print2 + print2;
+    }
+    if (doVersion) {
+        printf("%s\n", version_str);
+        return 0;
+    }
+    if (doHelp) {
+        usage();
+        return 0;
+    }
+
+    /* Make sure num to fetch is integral multiple of bytes_per_line */
+    if (0 != (num % bytes_per_line))
+        num = (num / bytes_per_line) * bytes_per_line;
+
+    if (hasFilename) {
+        for ( ; k < argc; k++)
+        {
+            inFile = open(argv[k], O_RDONLY);
+            if (inFile < 0) {
+                fprintf(stderr, "Couldn't open file: %s\n", argv[k]);
+                ret = 1;
+            } else {
+                sg_set_binary_mode(inFile);
+                if (offset > 0) {
+                    int err;
+                    int64_t off_res;
+
+                    off_res = lseek(inFile, offset, SEEK_SET);
+                    if (off_res < 0) {
+                        err = errno;
+                        fprintf(stderr, "failed moving filepos: wanted=%"
+                                PRId64 " [0x%" PRIx64 "]\nlseek error: %s\n",
+                                offset, offset, strerror(err));
+                        goto fini1;
+                    }
+                    start = offset;
+                } else
+                    start = 0;
+                if (! (doHex || quiet || print1))
+                    printf("ASCII hex dump of file: %s\n", argv[k]);
+                while ((res = read(inFile, buff, num)) > 0) {
+                    if (print1) {
+                        if (1 == print1) {
+                            if (doHex)
+                                printf("0x%02x\n", (uint8_t)(buff[0]));
+                            else
+                                printf("%02x\n", (uint8_t)(buff[0]));
+                        } else {
+                            uint16_t us;
+
+                            memcpy(&us, buff, 2);
+                            if (doHex)
+                                printf("0x%04x\n", us);
+                            else
+                                printf("%04x\n", us);
+                        }
+                        break;
+                    }
+                    if (doHex)
+                        dStrHexOnly(buff, res, start, noAddr);
+                    else
+                        dStrHex(buff, res, start, noAddr);
+                    start += (long)res;
+                }
+            }
+fini1:
+            close(inFile);
+        }
+    } else {
+        sg_set_binary_mode(inFile);
+        if (offset > 0) {
+            start = offset;
+            do {        /* eat up offset bytes */
+                if ((res = read(inFile, buff,
+                                (num > offset ? offset : num))) > 0)
+                    offset -= res;
+                else {
+                    fprintf(stderr, "offset read() error: %s\n",
+                            strerror(errno));
+                     break;
+                }
+            } while (offset > 0);
+        }
+        while ((res = read(inFile, buff, num)) > 0) {
+            if (doHex)
+                dStrHexOnly(buff, res, start, noAddr);
+            else
+                dStrHex(buff, res, start, noAddr);
+            start += (long)res;
+        }
+    }
+    return ret;
+}